Skip to content

Commit 92f2934

Browse files
authored
fix: properly searches through the entire invocation stack (#128)
1 parent 130e719 commit 92f2934

File tree

5 files changed

+187
-75
lines changed

5 files changed

+187
-75
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
# Huff Neo Compiler changelog
44

55
## Unreleased
6+
7+
## [1.5.2] - 2025-11-05
68
- Add compile-time if/else if/else statements.
79
- Example: `if ([MODE] == 0x01) { 0xAA } else if ([MODE] == 0x02) { 0xBB } else { 0xCC }`
810
- Fix constant substitution failing when referencing builtin functions (fixes #122).
911
- Example: `#define constant C1 = __RIGHTPAD(0x)` and `#define constant C2 = [C1]` now works correctly.
1012
- Applies to all builtin functions: `__FUNC_SIG`, `__EVENT_HASH`, `__RIGHTPAD`, `__LEFTPAD`, `__BYTES`.
13+
- Fix nested macro invocation argument scoping issue (fixes #123).
14+
- The compiler now properly searches through the entire invocation stack to resolve argument names.
1115

1216
## [1.5.1] - 2025-11-04
1317
- Throw error for circular constant dependencies to prevent infinite loops during constant evaluation.

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0"
1111
readme = "README.md"
1212
repository = "https://github.com/foundry-rs/huff-neo"
1313
rust-version = "1.89"
14-
version = "1.5.1"
14+
version = "1.5.2"
1515

1616
[workspace.dependencies]
1717
huff-neo-codegen = { path = "crates/codegen" }

crates/codegen/src/irgen/arg_calls.rs

Lines changed: 130 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,73 @@ use huff_neo_utils::ast::span::AstSpan;
33
use huff_neo_utils::prelude::*;
44
use std::str::FromStr;
55

6+
/// Resolves an argument name to its actual value by searching through the macro invocation stack.
7+
///
8+
/// This function recursively traverses the macro invocation stack (`mis`) to find where an argument
9+
/// is defined and returns its resolved value. It handles cases where arguments reference other
10+
/// arguments (ArgCall) or are macro invocations with arguments (ArgCallMacroInvocation).
11+
///
12+
/// # Arguments
13+
/// * `arg_name` - The name of the argument to resolve
14+
/// * `mis` - The macro invocation stack to search through
15+
/// * `contract` - The contract containing macro definitions
16+
///
17+
/// # Returns
18+
/// * `Ok(String)` - The resolved macro name
19+
/// * `Err(CodegenErrorKind)` - If the argument cannot be resolved
20+
fn resolve_argument_through_mis(arg_name: &str, mis: &[(usize, MacroInvocation)], contract: &Contract) -> Result<String, CodegenErrorKind> {
21+
tracing::debug!(target: "codegen", "Resolving argument '{}' through mis stack of length {}", arg_name, mis.len());
22+
23+
// Search through the macro invocation stack in reverse (from current to root)
24+
for (idx, (_, mi)) in mis.iter().enumerate().rev() {
25+
tracing::debug!(target: "codegen", "Checking invocation {} for macro '{}'", idx, mi.macro_name);
26+
27+
// Find the macro definition
28+
if let Some(invoked_macro) = contract.find_macro_by_name(&mi.macro_name) {
29+
// Check if this macro has our argument
30+
if let Some(param_index) = invoked_macro.parameters.iter().position(|p| p.name.as_deref() == Some(arg_name)) {
31+
tracing::debug!(target: "codegen", "Found parameter '{}' at index {} in macro '{}'", arg_name, param_index, mi.macro_name);
32+
33+
// Get the corresponding argument value
34+
if let Some(arg_value) = mi.args.get(param_index) {
35+
match arg_value {
36+
MacroArg::Ident(macro_name) => {
37+
// Direct macro name - we're done
38+
tracing::info!(target: "codegen", "Resolved '{}' to macro: {}", arg_name, macro_name);
39+
return Ok(macro_name.clone());
40+
}
41+
MacroArg::ArgCall(arg_call) => {
42+
// The argument is itself an argument reference - need to resolve it recursively
43+
tracing::debug!(target: "codegen", "Argument '{}' references another argument '{}', resolving recursively", arg_name, arg_call.name);
44+
return resolve_argument_through_mis(&arg_call.name, mis, contract);
45+
}
46+
MacroArg::ArgCallMacroInvocation(inner_arg_name, _) => {
47+
// The argument is an invocation of another argument - resolve that argument
48+
tracing::debug!(target: "codegen", "Argument '{}' is a macro invocation of argument '{}', resolving recursively", arg_name, inner_arg_name);
49+
return resolve_argument_through_mis(inner_arg_name, mis, contract);
50+
}
51+
MacroArg::Noop => {
52+
// __NOOP cannot be invoked as a macro
53+
return Err(CodegenErrorKind::InvalidMacroArgumentType(format!(
54+
"Cannot invoke __NOOP as a macro in argument '{}'",
55+
arg_name
56+
)));
57+
}
58+
_ => {
59+
// Other types of arguments not supported for macro invocation
60+
tracing::debug!(target: "codegen", "Argument type {:?} not supported for macro invocation, continuing search", arg_value);
61+
continue;
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
68+
69+
tracing::error!(target: "codegen", "Failed to resolve argument '{}' in mis stack", arg_name);
70+
Err(CodegenErrorKind::MissingArgumentDefinition(arg_name.to_string()))
71+
}
72+
673
/// Computes the scope context for label resolution in argument calls.
774
///
875
/// When a label is passed as an argument (e.g., `<label>` in a macro body), it must be
@@ -91,62 +158,67 @@ pub fn bubble_arg_call(
91158
if let MacroArg::ArgCallMacroInvocation(inner_arg_name, invoc_args) = arg {
92159
tracing::info!(target: "codegen", "GOT ArgCallMacroInvocation for argument '{}', expanding it", inner_arg_name);
93160

94-
// We need to resolve inner_arg_name and expand the macro
95-
// Look in the parent scope for the actual macro name
96-
if scope.len() > 1 {
97-
let parent_macro = scope[scope.len() - 2];
98-
if let Some(parent_param_idx) = parent_macro.parameters.iter().position(|p| p.name.as_ref() == Some(inner_arg_name))
99-
&& mis.len() > 1
100-
&& let Some(parent_invoc) = mis.get(mis.len() - 2)
101-
&& let Some(MacroArg::Ident(actual_macro_name)) = parent_invoc.1.args.get(parent_param_idx)
102-
&& let Some(called_macro) = contract.find_macro_by_name(actual_macro_name)
103-
{
104-
let inner_mi =
105-
MacroInvocation { macro_name: actual_macro_name.clone(), args: invoc_args.clone(), span: span.clone() };
106-
107-
let mut new_scope = scope.to_vec();
108-
new_scope.push(called_macro);
109-
let mut new_mis = mis.to_vec();
110-
new_mis.push((*offset, inner_mi));
111-
112-
match Codegen::macro_to_bytecode(
113-
evm_version,
114-
called_macro,
115-
contract,
116-
&mut new_scope,
117-
*offset,
118-
&mut new_mis,
119-
false,
120-
None,
121-
) {
122-
Ok(expanded) => {
123-
let byte_len: usize = expanded.bytes.iter().map(|(_, b)| b.0.len() / 2).sum();
124-
bytes.extend(expanded.bytes);
125-
*offset += byte_len;
126-
127-
// Handle jumps and tables
128-
for jump in expanded.unmatched_jumps {
129-
if let Some(existing) = jump_table.get_mut(&jump.bytecode_index) {
130-
existing.push(jump);
131-
} else {
132-
jump_table.insert(jump.bytecode_index, vec![jump]);
161+
// We need to resolve inner_arg_name to get the actual macro name
162+
// Use the helper function to search through the entire mis stack
163+
match resolve_argument_through_mis(inner_arg_name, mis, contract) {
164+
Ok(actual_macro_name) => {
165+
if let Some(called_macro) = contract.find_macro_by_name(&actual_macro_name) {
166+
let inner_mi =
167+
MacroInvocation { macro_name: actual_macro_name.clone(), args: invoc_args.clone(), span: span.clone() };
168+
169+
let mut new_scope = scope.to_vec();
170+
new_scope.push(called_macro);
171+
let mut new_mis = mis.to_vec();
172+
new_mis.push((*offset, inner_mi));
173+
174+
match Codegen::macro_to_bytecode(
175+
evm_version,
176+
called_macro,
177+
contract,
178+
&mut new_scope,
179+
*offset,
180+
&mut new_mis,
181+
false,
182+
None,
183+
) {
184+
Ok(expanded) => {
185+
let byte_len: usize = expanded.bytes.iter().map(|(_, b)| b.0.len() / 2).sum();
186+
bytes.extend(expanded.bytes);
187+
*offset += byte_len;
188+
189+
// Handle jumps and tables
190+
for jump in expanded.unmatched_jumps {
191+
if let Some(existing) = jump_table.get_mut(&jump.bytecode_index) {
192+
existing.push(jump);
193+
} else {
194+
jump_table.insert(jump.bytecode_index, vec![jump]);
195+
}
133196
}
134-
}
135-
for table_instance in expanded.table_instances {
136-
table_instances.push(table_instance);
137-
}
138-
for table in expanded.utilized_tables {
139-
if !utilized_tables.contains(&table) {
140-
utilized_tables.push(table);
197+
for table_instance in expanded.table_instances {
198+
table_instances.push(table_instance);
141199
}
200+
for table in expanded.utilized_tables {
201+
if !utilized_tables.contains(&table) {
202+
utilized_tables.push(table);
203+
}
204+
}
205+
return Ok(());
142206
}
143-
return Ok(());
207+
Err(e) => return Err(e),
144208
}
145-
Err(e) => return Err(e),
209+
} else {
210+
return Err(CodegenError {
211+
kind: CodegenErrorKind::MissingMacroDefinition(actual_macro_name),
212+
span: span.clone_box(),
213+
token: None,
214+
});
146215
}
147216
}
217+
Err(e) => {
218+
// Could not resolve the argument
219+
return Err(CodegenError { kind: e, span: span.clone_box(), token: None });
220+
}
148221
}
149-
// If we couldn't expand it, fall through to normal handling
150222
}
151223

152224
// Original handling for other argument types
@@ -302,14 +374,15 @@ pub fn bubble_arg_call(
302374
}
303375
MacroArg::ArgCallMacroInvocation(arg_name, invoc_args) => {
304376
// Resolve the argument to get the actual macro name
305-
if let Some(param_idx) = target_macro.parameters.iter().position(|p| p.name.as_ref() == Some(arg_name)) {
306-
if let Some(MacroArg::Ident(actual_macro_name)) = target_macro_invoc.1.args.get(param_idx) {
377+
// Use the helper function to search through the entire mis stack
378+
match resolve_argument_through_mis(arg_name, mis, contract) {
379+
Ok(actual_macro_name) => {
307380
// Now invoke the resolved macro with the provided arguments
308-
if let Some(called_macro) = contract.find_macro_by_name(actual_macro_name) {
381+
if let Some(called_macro) = contract.find_macro_by_name(&actual_macro_name) {
309382
let inner_mi = MacroInvocation {
310383
macro_name: actual_macro_name.clone(),
311384
args: invoc_args.clone(),
312-
span: AstSpan(vec![]), // We don't have the original span here
385+
span: span.clone(),
313386
};
314387

315388
let mut new_scope = scope.to_vec();
@@ -353,24 +426,15 @@ pub fn bubble_arg_call(
353426
}
354427
} else {
355428
return Err(CodegenError {
356-
kind: CodegenErrorKind::MissingMacroDefinition(actual_macro_name.clone()),
429+
kind: CodegenErrorKind::MissingMacroDefinition(actual_macro_name),
357430
span: span.clone_box(),
358431
token: None,
359432
});
360433
}
361-
} else {
362-
return Err(CodegenError {
363-
kind: CodegenErrorKind::MissingArgumentDefinition(arg_name.clone()),
364-
span: span.clone_box(),
365-
token: None,
366-
});
367434
}
368-
} else {
369-
return Err(CodegenError {
370-
kind: CodegenErrorKind::MissingArgumentDefinition(arg_name.clone()),
371-
span: span.clone_box(),
372-
token: None,
373-
});
435+
Err(e) => {
436+
return Err(CodegenError { kind: e, span: span.clone_box(), token: None });
437+
}
374438
}
375439
}
376440
MacroArg::MacroCall(inner_mi) => {

crates/core/tests/macro_invoc_args.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,3 +1310,47 @@ fn test_nested_evaluated_constants_as_macro_arg() {
13101310
let expected_bytecode = "6010";
13111311
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
13121312
}
1313+
1314+
#[test]
1315+
fn test_nested_macro_invocation_with_arg_scoping() {
1316+
// Test nested macro invocation argument scoping
1317+
// When an ArgCallMacroInvocation like <arg1>(__NOOP) is passed as an argument to M2,
1318+
// the compiler should properly resolve arg1 through the entire invocation stack
1319+
let source = r#"
1320+
#define macro MAIN() = {
1321+
M1(IDENTITY)
1322+
}
1323+
1324+
#define macro M1(arg1) = {
1325+
M2(M3(<arg1>(__NOOP)))
1326+
}
1327+
1328+
#define macro M2(arg2) = {
1329+
<arg2>
1330+
}
1331+
1332+
#define macro M3(arg3) = {
1333+
<arg3>
1334+
}
1335+
1336+
#define macro IDENTITY(arg) = {
1337+
<arg>
1338+
}
1339+
"#;
1340+
1341+
// Lex + Parse
1342+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
1343+
let lexer = Lexer::new(flattened_source);
1344+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
1345+
let mut parser = Parser::new(tokens, None);
1346+
let mut contract = parser.parse().unwrap();
1347+
contract.derive_storage_pointers();
1348+
1349+
let evm_version = EVMVersion::default();
1350+
1351+
// This should compile successfully
1352+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None).unwrap();
1353+
1354+
// Expected bytecode: IDENTITY(__NOOP) should produce nothing (since __NOOP generates no bytecode)
1355+
assert!(main_bytecode.is_empty());
1356+
}

0 commit comments

Comments
 (0)