Skip to content

Commit

Permalink
✨ Error on mutually dependent code inclusion
Browse files Browse the repository at this point in the history
  • Loading branch information
Philogy committed Nov 7, 2024
1 parent 97b9959 commit dd7c00a
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 32 deletions.
82 changes: 77 additions & 5 deletions crates/analysis/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
use ariadne::{Color, Config, Fmt, IndexType, Label, Report, ReportKind};
use huff_ast::{Definition, IdentifiableNode, Instruction, Macro, Spanned};

#[derive(Debug, Clone, PartialEq, Eq)]
type InvokeChain<'src, 'ast> = Box<[(&'ast Macro<'src>, &'ast Spanned<&'src str>)]>;

#[derive(Debug, Clone)]
pub struct Inclusion<'src, 'ast: 'src> {
pub entry_point: Spanned<&'src str>,
pub invoke_stack: InvokeChain<'src, 'ast>,
pub inclusion: Spanned<&'src str>,
}

#[derive(Debug, Clone)]
pub enum AnalysisError<'ast, 'src> {
/// When two different definitions have the same.
DefinitionNameCollision {
collided: Box<[&'ast Definition<'src>]>,
duplicate_name: &'src str,
},
RecursiveMacroInvocation {
invocation_chain: Box<[(&'ast Macro<'src>, &'ast Spanned<&'src str>)]>,
invocation_chain: InvokeChain<'src, 'ast>,
},
RecursiveCodeInclusion {
linking_inclusions: Box<[Inclusion<'src, 'ast>]>,
},
LabelNotFound {
scope: &'ast Macro<'src>,
invocation_chain: Box<[(&'ast Macro<'src>, &'ast Spanned<&'src str>)]>,
invocation_chain: InvokeChain<'src, 'ast>,
not_found: &'ast Spanned<&'src str>,
},
MacroArgNotFound {
Expand Down Expand Up @@ -69,7 +81,7 @@ impl AnalysisError<'_, '_> {
.with_config(Config::default().with_index_type(IndexType::Byte))
.with_message(format!(
"Definitions with duplicate name '{}'",
duplicate_name.escape_debug().fg(Color::Red)
duplicate_name.fg(Color::Red)
));

base_report
Expand Down Expand Up @@ -100,7 +112,7 @@ impl AnalysisError<'_, '_> {
.with_config(Config::default().with_index_type(IndexType::Byte))
.with_message(format!(
"Cannot expand macro {} with recursive dependency on itself",
first_invoke.0.ident().escape_debug().fg(Color::Red)
first_invoke.0.ident().fg(Color::Red)
));

invocation_chain
Expand Down Expand Up @@ -355,6 +367,66 @@ impl AnalysisError<'_, '_> {
)
.finish()
}
Self::RecursiveCodeInclusion { linking_inclusions } => {
let recursing_inclusion = linking_inclusions.last().unwrap().inclusion;
let recursing_name = recursing_inclusion.ident();

let base_report = Report::build(
ReportKind::Error,
filename.clone(),
recursing_inclusion.1.start,
)
.with_config(Config::default().with_index_type(IndexType::Byte))
.with_message(format!(
"Macro {} cannot be included because it recursively includes itself",
recursing_name.fg(Color::Red),
));

linking_inclusions
.iter()
.enumerate()
.skip_while(|(_i, inclusion)| inclusion.entry_point.ident() != recursing_name)
.fold(base_report, |report, (i, inclusion)| {
let report = report.with_label(
Label::new((filename.clone(), inclusion.entry_point.1.into_range()))
.with_color(Color::Blue),
);

let report = inclusion.invoke_stack.iter().fold(
report,
|report, (scope, invoking)| {
report
.with_label(
Label::new((filename.clone(), scope.name.1.into_range()))
.with_color(Color::Red),
)
.with_label(
Label::new((filename.clone(), invoking.1.into_range()))
.with_color(Color::Yellow),
)
},
);

let is_last = i == linking_inclusions.len() - 1;
if !is_last {
report.with_label(
Label::new((filename.clone(), inclusion.inclusion.1.into_range()))
.with_color(Color::Yellow),
)
} else {
report.with_label(
Label::new((filename.clone(), inclusion.inclusion.1.into_range()))
.with_message("Recursing inclusion")
.with_color(Color::Red),
)
}
})
.with_help(
"__codeoffset/__codesize attempts to include the target and all \
its dependencies, which it cannot do if the dependencies are cyclic.",
)
.finish()
}
}
}
}
100 changes: 78 additions & 22 deletions crates/analysis/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pub mod errors;
pub mod label_stack;

use crate::errors::AnalysisError;
use crate::errors::{AnalysisError, Inclusion};
use crate::label_stack::LabelStack;
use huff_ast::{Definition, IdentifiableNode, Instruction, Invoke, Macro, MacroStatement, Spanned};
use std::collections::BTreeMap;
Expand All @@ -26,19 +26,27 @@ pub fn analyze_global_for_dups<'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 's
.collect()
}

fn get_macro_def<'src, 'ast: 'src>(
global_defs: &BTreeMap<&'src str, Vec<&'ast Definition<'src>>>,
name: &'src str,
) -> Option<&'ast Macro<'src>> {
let possible_defs = global_defs.get(name)?;
possible_defs.into_iter().find_map(|def| match def {
Definition::Macro(entry_point) => Some(entry_point),
_ => None,
})
}

pub fn analyze_entry_point<'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)>(
global_defs: &BTreeMap<&'src str, Vec<&'ast Definition<'src>>>,
entry_point_name: &'src str,
mut emit_error: E,
macros_to_analyze: &mut Vec<&'src str>,
macros_to_include: &mut Vec<CodeInclusionFrame<'src, 'ast>>,
) {
let mut invoke_stack = Vec::with_capacity(32);
let mut label_stack = LabelStack::default();

let entry_point = if let Some(Definition::Macro(entry_point)) = global_defs
.get(entry_point_name)
.and_then(|defs| defs.first())
{
let entry_point = if let Some(entry_point) = get_macro_def(global_defs, entry_point_name) {
entry_point
} else {
emit_error(AnalysisError::EntryPointNotFound {
Expand All @@ -59,31 +67,50 @@ pub fn analyze_entry_point<'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)
&mut label_stack,
&mut invoke_stack,
&mut emit_error,
macros_to_analyze,
macros_to_include,
);
}

macro_rules! global_exists {
($global_defs:expr, $ident:expr, $pattern:pat) => {
$global_defs
.get($ident)
.map(|defs| {
defs.iter().any(|def| match def {
$pattern => true,
_ => false,
})
$global_defs.get($ident).map_or(false, |defs| {
defs.iter().any(|def| match def {
$pattern => true,
_ => false,
})
.unwrap_or(false)
})
};
}

#[derive(Debug, Clone)]
pub struct CodeInclusionFrame<'src, 'ast: 'src> {
pub name: &'src str,
linking_inclusions: Vec<Inclusion<'src, 'ast>>,
}

impl<'src> CodeInclusionFrame<'src, '_> {
pub fn top(name: &'src str) -> Self {
Self {
name,
linking_inclusions: vec![],
}
}

fn already_included(&self, name: &'src str) -> bool {
self.linking_inclusions
.iter()
.any(|inclusion| inclusion.entry_point.ident() == name)
}
}

#[derive(Debug)]
struct MacroAnalysis<'a, 'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)> {
global_defs: &'a BTreeMap<&'src str, Vec<&'ast Definition<'src>>>,
m: &'ast Macro<'src>,
label_stack: &'a mut LabelStack<'src, ()>,
invoke_stack: &'a mut Vec<(&'ast Macro<'src>, &'ast Spanned<&'src str>)>,
emit_error: &'a mut E,
macros_to_analyze: &'a mut Vec<&'src str>,
macros_to_include: &'a mut Vec<CodeInclusionFrame<'src, 'ast>>,
}

impl<'a, 'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)> MacroAnalysis<'a, 'src, 'ast, E> {
Expand All @@ -97,19 +124,27 @@ impl<'a, 'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)> MacroAnalysis<'a
label_stack: &'a mut LabelStack<'src, ()>,
invoke_stack: &'a mut Vec<(&'ast Macro<'src>, &'ast Spanned<&'src str>)>,
emit_error: &'a mut E,
macros_to_analyze: &mut Vec<&'src str>,
macros_to_include: &mut Vec<CodeInclusionFrame<'src, 'ast>>,
) {
MacroAnalysis {
global_defs,
m,
label_stack,
invoke_stack,
emit_error,
macros_to_analyze,
macros_to_include,
}
.analyze();
}

fn entry_point(&self) -> &Macro<'src> {
get_macro_def(self.global_defs, self.current_frame().name).unwrap()
}

fn current_frame(&self) -> &CodeInclusionFrame<'src, 'ast> {
self.macros_to_include.last().unwrap()
}

fn analyze(&mut self) {
let name = self.m.name.0;

Expand Down Expand Up @@ -205,7 +240,7 @@ impl<'a, 'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)> MacroAnalysis<'a
self.label_stack,
self.invoke_stack,
self.emit_error,
self.macros_to_analyze,
self.macros_to_include,
);
});
self.invoke_stack.pop().unwrap();
Expand Down Expand Up @@ -235,23 +270,44 @@ impl<'a, 'src, 'ast: 'src, E: FnMut(AnalysisError<'ast, 'src>)> MacroAnalysis<'a
def_type: "macro",
not_found: code_ref,
});
return;
}
if self
.global_defs
.get(code_ref.ident())
.map(|defs| {
.map_or(false, |defs| {
defs.iter().any(
|def| matches!(def, Definition::Macro(m) if m.args.0.len() > 0),
)
})
.unwrap_or(false)
{
self.emit(AnalysisError::NotYetSupported {
intent: "code introspection for macros with arguments".to_owned(),
span: ((), code_ref.1),
});
return;
}
let mut linking_inclusions = self.current_frame().linking_inclusions.clone();

let entry_point_name = self.entry_point().name;
let inclusion = Inclusion {
invoke_stack: self.invoke_stack.clone().into_boxed_slice(),
entry_point: entry_point_name,
inclusion: *code_ref,
};
linking_inclusions.push(inclusion);
if self.current_frame().already_included(code_ref.ident())
&& code_ref.ident() != entry_point_name.ident()
{
self.emit(AnalysisError::RecursiveCodeInclusion {
linking_inclusions: linking_inclusions.into_boxed_slice(),
})
} else {
self.macros_to_include.push(CodeInclusionFrame {
name: code_ref.ident(),
linking_inclusions,
});
}
self.macros_to_analyze.push(code_ref.ident());
}
Invoke::BuiltinFuncSig(func_or_error_ref)
| Invoke::BuiltinError(func_or_error_ref) => {
Expand Down
10 changes: 6 additions & 4 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let unique_defs = analyze_global_for_dups(&global_defs, |err| analysis_errors.push(err));

{
let mut to_analyze_stack = vec![cli.entry_point.as_str()];
let mut to_analyze_stack = vec![CodeInclusionFrame::top(cli.entry_point.as_str())];
let mut analyzed_macros = BTreeSet::new();
while let Some(next_entrypoint) = to_analyze_stack.pop() {
if analyzed_macros.insert(next_entrypoint) {
while let Some(next_entrypoint) = to_analyze_stack.last() {
let idx_to_remove = to_analyze_stack.len() - 1;
if analyzed_macros.insert(next_entrypoint.name) {
analyze_entry_point(
&global_defs,
next_entrypoint,
next_entrypoint.name,
|err| analysis_errors.push(err),
&mut to_analyze_stack,
);
}
to_analyze_stack.remove(idx_to_remove);
}
}

Expand Down
3 changes: 2 additions & 1 deletion crates/compilation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ pub fn generate_for_entrypoint<'src>(
&mut included_macros,
&mut asm,
);
asm.push(Asm::Mark(end_id));

included_macros.into_iter().skip(1).for_each(|included| {
let section_macro =
Expand All @@ -74,6 +73,8 @@ pub fn generate_for_entrypoint<'src>(
asm.push(Asm::Mark(included.end_id));
});

asm.push(Asm::Mark(end_id));

globals.assemble(asm.as_slice())
}

Expand Down
12 changes: 12 additions & 0 deletions examples/errors/MutualInclusion.huff
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#define macro MAIN() = {
__codeoffset(OTHER)
}

#define macro OTHER() = {
WOW()
}

#define macro WOW() = {
__codeoffset(MAIN)

}
9 changes: 9 additions & 0 deletions examples/features/Quine.huff
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#define macro MAIN() = takes(0) returns(0) {
__codesize(MAIN)
dup1
__codeoffset(MAIN)
0x0
codecopy
0x0
return
}

0 comments on commit dd7c00a

Please sign in to comment.