Skip to content

Commit 935391a

Browse files
authored
fix: nested macro argument resolution with labels (#137)
1 parent 70bbb4f commit 935391a

File tree

3 files changed

+185
-3
lines changed

3 files changed

+185
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- Builtin functions can now be passed as macro arguments: `__FUNC_SIG`, `__EVENT_HASH`, `__BYTES`, `__RIGHTPAD`.
1616
- Example: `MACRO(__FUNC_SIG(transfer))`
1717
- Reject macros names that are equal to reserved builtin function names (fixes #131).
18+
- Fix stack overflow and argument resolution errors in nested macro invocations with labels (fixes #133).
19+
- Example: `M2(M3(<arg>))` followed by a label now compiles without errors.
1820

1921
## [1.5.2] - 2025-11-05
2022
- Add compile-time if/else if/else statements.

crates/codegen/src/irgen/statements.rs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,26 @@ pub fn statement_gen<'a>(
391391
if let Some(parent_macro) = contract.find_macro_by_name(&parent_mi.macro_name)
392392
&& let Some(param_idx) =
393393
parent_macro.parameters.iter().position(|p| p.name.as_deref() == Some(arg_name))
394-
&& let Some(MacroArg::Ident(macro_name)) = parent_mi.args.get(param_idx)
395394
{
396-
resolved_macro_name = Some(macro_name.clone());
397-
break;
395+
// Check what type of argument value we have
396+
if let Some(arg_value) = parent_mi.args.get(param_idx) {
397+
match arg_value {
398+
MacroArg::Ident(macro_name) => {
399+
// Direct macro name reference
400+
resolved_macro_name = Some(macro_name.clone());
401+
break;
402+
}
403+
MacroArg::MacroCall(macro_call) => {
404+
// The argument is a macro invocation - extract the macro name
405+
resolved_macro_name = Some(macro_call.macro_name.clone());
406+
break;
407+
}
408+
_ => {
409+
// Other types not supported for macro invocation, continue searching
410+
continue;
411+
}
412+
}
413+
}
398414
}
399415
}
400416

@@ -410,6 +426,36 @@ pub fn statement_gen<'a>(
410426
resolved_args.push(arg.clone());
411427
}
412428
}
429+
MacroArg::MacroCall(macro_call) => {
430+
// Recursively resolve any ArgCalls in the macro call's arguments
431+
let mut resolved_macro_args = Vec::new();
432+
for macro_arg in &macro_call.args {
433+
match macro_arg {
434+
MacroArg::ArgCall(arg_call) => {
435+
// Resolve this ArgCall by looking it up in the current context
436+
let mut resolved = None;
437+
for (_, parent_mi) in mis.iter().rev() {
438+
if let Some(parent_macro) = contract.find_macro_by_name(&parent_mi.macro_name)
439+
&& let Some(param_idx) =
440+
parent_macro.parameters.iter().position(|p| p.name.as_deref() == Some(&arg_call.name))
441+
&& let Some(arg_value) = parent_mi.args.get(param_idx)
442+
{
443+
resolved = Some(arg_value.clone());
444+
break;
445+
}
446+
}
447+
resolved_macro_args.push(resolved.unwrap_or_else(|| macro_arg.clone()));
448+
}
449+
_ => resolved_macro_args.push(macro_arg.clone()),
450+
}
451+
}
452+
// Create a new MacroCall with resolved arguments
453+
resolved_args.push(MacroArg::MacroCall(MacroInvocation {
454+
macro_name: macro_call.macro_name.clone(),
455+
args: resolved_macro_args,
456+
span: macro_call.span.clone(),
457+
}));
458+
}
413459
_ => resolved_args.push(arg.clone()),
414460
}
415461
}

crates/core/tests/macro_invoc_args.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,3 +1354,137 @@ fn test_nested_macro_invocation_with_arg_scoping() {
13541354
// Expected bytecode: IDENTITY(__NOOP) should produce nothing (since __NOOP generates no bytecode)
13551355
assert!(main_bytecode.is_empty());
13561356
}
1357+
1358+
#[test]
1359+
fn test_nested_macro_invocation_with_label_identical_params() {
1360+
// Tests nested macro invocations as arguments with labels between invocations.
1361+
// Ensures the compiler correctly resolves nested macro calls when labels
1362+
// change bytecode offsets, even with identical parameter names across macros.
1363+
let source = r#"
1364+
#define macro M3(x) = takes(0) returns(0) {
1365+
<x>
1366+
}
1367+
1368+
#define macro M2(y) = takes(0) returns(0) {
1369+
M3(<y>)
1370+
}
1371+
1372+
#define macro MAIN() = takes(0) returns(0) {
1373+
M2(M3(0x01))
1374+
lbl:
1375+
M2(M3(0x02))
1376+
}
1377+
"#;
1378+
1379+
// Lex + Parse
1380+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
1381+
let lexer = Lexer::new(flattened_source);
1382+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
1383+
let mut parser = Parser::new(tokens, None);
1384+
let mut contract = parser.parse().unwrap();
1385+
contract.derive_storage_pointers();
1386+
1387+
let evm_version = EVMVersion::default();
1388+
1389+
// This should compile successfully without stack overflow
1390+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None, false).unwrap();
1391+
1392+
// Expected bytecode:
1393+
// M2(M3(0x01)) -> M3(M3(0x01)) -> M3(0x01) -> 0x01 = PUSH1 0x01
1394+
// lbl: -> JUMPDEST
1395+
// M2(M3(0x02)) -> M3(M3(0x02)) -> M3(0x02) -> 0x02 = PUSH1 0x02
1396+
// Total: 6001 5b 6002
1397+
let expected_bytecode = "60015b6002";
1398+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
1399+
}
1400+
1401+
#[test]
1402+
fn test_nested_macro_invocation_with_label_different_params() {
1403+
// Tests nested macro invocations as arguments with labels and different parameter names.
1404+
// Verifies argument resolution works correctly across macro boundaries when parameter
1405+
// names differ and labels are present.
1406+
let source = r#"
1407+
#define macro M3(arg) = takes(0) returns(0) {
1408+
<arg>
1409+
}
1410+
1411+
#define macro M2(y) = takes(0) returns(0) {
1412+
M3(<y>)
1413+
}
1414+
1415+
#define macro MAIN() = takes(0) returns(0) {
1416+
M2(M3(0x01))
1417+
lbl:
1418+
M2(M3(0x02))
1419+
}
1420+
"#;
1421+
1422+
// Lex + Parse
1423+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
1424+
let lexer = Lexer::new(flattened_source);
1425+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
1426+
let mut parser = Parser::new(tokens, None);
1427+
let mut contract = parser.parse().unwrap();
1428+
contract.derive_storage_pointers();
1429+
1430+
let evm_version = EVMVersion::default();
1431+
1432+
// This should compile successfully without "Missing Argument Definition" error
1433+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None, false).unwrap();
1434+
1435+
// Expected bytecode: same as above
1436+
// M2(M3(0x01)) -> M3(M3(0x01)) -> M3(0x01) -> 0x01 = PUSH1 0x01
1437+
// lbl: -> JUMPDEST
1438+
// M2(M3(0x02)) -> M3(M3(0x02)) -> M3(0x02) -> 0x02 = PUSH1 0x02
1439+
// Total: 6001 5b 6002
1440+
let expected_bytecode = "60015b6002";
1441+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
1442+
}
1443+
1444+
#[test]
1445+
fn test_nested_argcall_in_macrocall_with_label() {
1446+
// Tests recursive argument resolution in nested macro calls with labels.
1447+
// Verifies that when a MacroCall contains an ArgCall (e.g., M3(<arg>)), the compiler
1448+
// correctly resolves nested arguments without infinite recursion or stack overflow.
1449+
let source = r#"
1450+
#define macro MAIN() = {
1451+
M1(__NOOP)
1452+
}
1453+
1454+
#define macro M1(arg) = {
1455+
M2(M3(<arg>))
1456+
lbl:
1457+
M2(M3(<arg>))
1458+
}
1459+
1460+
#define macro M2(arg) = {
1461+
<arg>
1462+
}
1463+
1464+
#define macro M3(arg) = {
1465+
<arg>
1466+
}
1467+
"#;
1468+
1469+
// Lex + Parse
1470+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
1471+
let lexer = Lexer::new(flattened_source);
1472+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
1473+
let mut parser = Parser::new(tokens, None);
1474+
let mut contract = parser.parse().unwrap();
1475+
contract.derive_storage_pointers();
1476+
1477+
let evm_version = EVMVersion::default();
1478+
1479+
// This should compile successfully without stack overflow
1480+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None, false).unwrap();
1481+
1482+
// Expected bytecode:
1483+
// M1(__NOOP) expands to:
1484+
// M2(M3(__NOOP)) -> M2(__NOOP) -> __NOOP (no output)
1485+
// lbl: -> JUMPDEST (5b)
1486+
// M2(M3(__NOOP)) -> M2(__NOOP) -> __NOOP (no output)
1487+
// Total: just the JUMPDEST
1488+
let expected_bytecode = "5b";
1489+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
1490+
}

0 commit comments

Comments
 (0)