Skip to content

Commit 953a627

Browse files
authored
feat: Nested macro calls (#44)
1 parent 79efa66 commit 953a627

File tree

7 files changed

+213
-6
lines changed

7 files changed

+213
-6
lines changed

CHANGELOG.md

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

55
## [Unreleased]
6+
- Support nesting of macro calls e.g. `MACRO1(MACRO2(0x1, 0x2), 0x3)`. (See: #40)
7+
- Thank you very much to @Mouradif for the contribution!
68

79
## [1.1.4] - 2025-03-17
810
- Update dependencies to the latest version.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ getrandom = { version = "0.3.1", features = ["wasm_js"] }
6969
rayon = "1.10.0"
7070

7171
[profile.test]
72-
debug = 1
72+
debug = "full"
7373
incremental = true
7474

7575
[profile.release]

book/huff-language/macros-and-functions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ by the constructor.
3939

4040
### Macro Arguments
4141

42-
Macros can accept arguments to be "called" inside the macro or passed as a reference. Macro arguments may be one of: label, opcode, literal, or a constant. Since macros are inlined at compile-time, the arguments are not evaluated at runtime and are instead inlined as well.
42+
Macros can accept arguments, which can be used within the macro itself or passed as reference. These arguments can be labels, opcodes, literals, constants, or other macro calls. Since macros are inlined at compile time, their arguments are also inlined and not evaluated at runtime.
4343

4444
#### Example
4545

@@ -137,7 +137,7 @@ your contract, and it is essentially a trade-off of decreasing contract size
137137
for a small extra runtime gas cost (`22 + n_inputs * 3 + n_outputs * 3` gas
138138
per invocation, to be exact).
139139

140-
Functions are one of the only high-level abstractions
140+
Functions are one of the few high-level abstractions
141141
in Huff, so it is important to understand what the compiler adds to your code
142142
when they are utilized. It is not always beneficial to re-use code, especially
143143
if it is a small / inexpensive set of operations. However, for larger contracts

crates/codegen/src/irgen/arg_calls.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::Codegen;
22
use huff_neo_utils::ast::span::AstSpan;
33
use huff_neo_utils::prelude::*;
44
use std::str::FromStr;
5-
// Arguments can be literals, labels, opcodes, or constants
5+
// Arguments can be literals, labels, opcodes, constants, or macro calls
66
// !! IF THERE IS AMBIGUOUS NOMENCLATURE
77
// !! (E.G. BOTH OPCODE AND LABEL ARE THE SAME STRING)
88
// !! COMPILATION _WILL_ ERROR
@@ -136,6 +136,40 @@ pub fn bubble_arg_call(
136136
*offset += 3;
137137
}
138138
}
139+
MacroArg::MacroCall(inner_mi) => {
140+
tracing::debug!(target: "codegen", "Found MacroArg::MacroCall IN \"{}\" Macro Invocation: \"{}\"!", macro_invoc.1.macro_name, inner_mi.macro_name);
141+
142+
if let Some(called_macro) = contract.find_macro_by_name(&inner_mi.macro_name) {
143+
tracing::debug!(target: "codegen", "Found valid macro: {}", called_macro.name);
144+
let mut new_scope = scope.to_vec();
145+
new_scope.push(called_macro);
146+
let mut new_mis = mis.to_vec();
147+
new_mis.push((starting_offset, inner_mi.clone()));
148+
match Codegen::macro_to_bytecode(
149+
evm_version,
150+
called_macro,
151+
contract,
152+
&mut new_scope,
153+
starting_offset,
154+
&mut new_mis,
155+
false,
156+
None,
157+
) {
158+
Ok(expanded_macro) => {
159+
bytes.extend(expanded_macro.bytes);
160+
}
161+
Err(e) => {
162+
return Err(e);
163+
}
164+
}
165+
} else {
166+
return Err(CodegenError {
167+
kind: CodegenErrorKind::MissingMacroDefinition(inner_mi.macro_name.clone()),
168+
span: inner_mi.span.clone(),
169+
token: None,
170+
});
171+
}
172+
}
139173
}
140174
} else {
141175
tracing::warn!(target: "codegen", "\"{}\" FOUND IN MACRO DEF BUT NOT IN MACRO INVOCATION!", arg_name);

crates/core/tests/macro_invoc_args.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,155 @@ fn test_bubbled_arg_with_different_name() {
309309
// Check the bytecode
310310
assert_eq!(main_bytecode, expected_bytecode);
311311
}
312+
313+
#[test]
314+
fn test_nested_macro_call_no_args() {
315+
let source = r#"
316+
#define macro COMPUTE_FIXED(a) = takes(0) returns(0) {
317+
0x01
318+
0x01
319+
<a>
320+
}
321+
322+
#define macro ADD() = takes(0) returns(0) {
323+
add
324+
}
325+
326+
#define macro SUB() = takes(0) returns(0) {
327+
sub
328+
}
329+
330+
#define macro MAIN() = takes(0) returns(0) {
331+
COMPUTE_FIXED(ADD())
332+
COMPUTE_FIXED(SUB())
333+
}
334+
"#;
335+
336+
// Lex + Parse
337+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
338+
let lexer = Lexer::new(flattened_source);
339+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
340+
let mut parser = Parser::new(tokens, None);
341+
let mut contract = parser.parse().unwrap();
342+
contract.derive_storage_pointers();
343+
344+
let evm_version = EVMVersion::default();
345+
346+
// Create main and constructor bytecode
347+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None).unwrap();
348+
349+
// Full expected bytecode output (generated from huff-neo) (placed here as a reference)
350+
let expected_bytecode = "60016001016001600103";
351+
352+
// Check the bytecode
353+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
354+
}
355+
356+
#[test]
357+
fn test_nested_macro_calls() {
358+
let source = r#"
359+
#define macro ADD(a, b) = takes(0) returns(1) {
360+
<a> <b> add
361+
}
362+
363+
#define macro SUB(a, b) = takes(0) returns(1) {
364+
<a> <b> sub
365+
}
366+
367+
#define macro COMPUTE_AND_STORE(operation, offset) = takes(0) returns(0) {
368+
<operation>
369+
<offset> mstore
370+
}
371+
372+
#define macro MAIN() = takes(0) returns(0) {
373+
COMPUTE_AND_STORE(ADD(0x05, 0x06), 0x00)
374+
COMPUTE_AND_STORE(SUB(0x05, 0x06), 0x20)
375+
}
376+
"#;
377+
378+
// Lex + Parse
379+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
380+
let lexer = Lexer::new(flattened_source);
381+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
382+
let mut parser = Parser::new(tokens, None);
383+
let mut contract = parser.parse().unwrap();
384+
contract.derive_storage_pointers();
385+
386+
let evm_version = EVMVersion::default();
387+
388+
// Create main and constructor bytecode
389+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None).unwrap();
390+
391+
// Full expected bytecode output (generated from huff-neo) (placed here as a reference)
392+
let expected_bytecode = "60056006016000526005600603602052";
393+
394+
// Check the bytecode
395+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
396+
}
397+
398+
#[test]
399+
fn test_very_nested_macro_calls() {
400+
let source = r#"
401+
#define macro ADD(a, b) = takes(0) returns(1) {
402+
<a> <b> add
403+
}
404+
405+
#define macro SUB(a, b) = takes(0) returns(1) {
406+
<a> <b> sub
407+
}
408+
409+
#define macro COMPUTE_AND_STORE(operation, offset) = takes(0) returns(0) {
410+
<operation> <offset> mstore
411+
}
412+
413+
#define macro MAIN() = takes(0) returns(0) {
414+
COMPUTE_AND_STORE(ADD(0x05, SUB(0x05, ADD(ADD(0x0a, 0x16), 0x04))), 0x00)
415+
}
416+
"#;
417+
418+
// Lex + Parse
419+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
420+
let lexer = Lexer::new(flattened_source);
421+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
422+
let mut parser = Parser::new(tokens, None);
423+
let mut contract = parser.parse().unwrap();
424+
contract.derive_storage_pointers();
425+
426+
let evm_version = EVMVersion::default();
427+
428+
// Create main and constructor bytecode
429+
let main_bytecode = Codegen::generate_main_bytecode(&evm_version, &contract, None).unwrap();
430+
431+
// Full expected bytecode output (generated from huff-neo) (placed here as a reference)
432+
let expected_bytecode = "60056005600a6016016004010301600052";
433+
434+
// Check the bytecode
435+
assert_eq!(main_bytecode.to_lowercase(), expected_bytecode.to_lowercase());
436+
}
437+
438+
#[test]
439+
fn test_nested_macro_call_missing_macro_definition() {
440+
let source = r#"
441+
#define macro MACRO1(a) = takes(0) returns(0) {
442+
<a>
443+
}
444+
445+
#define macro MAIN() = takes(0) returns(0) {
446+
MACRO1(MISSING_MACRO())
447+
}
448+
"#;
449+
450+
// Lex + Parse
451+
let flattened_source = FullFileSource { source, file: None, spans: vec![] };
452+
let lexer = Lexer::new(flattened_source);
453+
let tokens = lexer.into_iter().map(|x| x.unwrap()).collect::<Vec<Token>>();
454+
let mut parser = Parser::new(tokens, None);
455+
let mut contract = parser.parse().unwrap();
456+
contract.derive_storage_pointers();
457+
458+
// Codegen should fail with an error
459+
let codegen_result = Codegen::generate_main_bytecode(&EVMVersion::default(), &contract, None);
460+
461+
assert!(codegen_result.is_err());
462+
assert_eq!(codegen_result.unwrap_err().kind, CodegenErrorKind::MissingMacroDefinition(String::from("MISSING_MACRO")));
463+
}

crates/parser/src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,8 +1033,25 @@ impl Parser {
10331033
self.consume();
10341034
}
10351035
TokenKind::Ident(ident) => {
1036-
args.push(MacroArg::Ident(ident));
1037-
self.consume();
1036+
if self.peek().unwrap().kind == TokenKind::OpenParen {
1037+
// It's a nested macro call
1038+
let mut curr_spans = vec![self.current_token.span.clone()];
1039+
self.match_kind(TokenKind::Ident("MACRO_NAME".to_string()))?;
1040+
// Parse Macro Call
1041+
let lit_args = self.parse_macro_call_args()?;
1042+
// Grab all spans following our macro invocation spam
1043+
if let Some(i) = self.spans.iter().position(|s| s.eq(&curr_spans[0])) {
1044+
curr_spans.append(&mut self.spans[(i + 1)..].to_vec());
1045+
}
1046+
args.push(MacroArg::MacroCall(MacroInvocation {
1047+
macro_name: ident,
1048+
args: lit_args,
1049+
span: AstSpan(curr_spans.clone()),
1050+
}));
1051+
} else {
1052+
args.push(MacroArg::Ident(ident));
1053+
self.consume();
1054+
}
10381055
}
10391056
TokenKind::Calldata => {
10401057
args.push(MacroArg::Ident("calldata".to_string()));

crates/utils/src/ast/huff.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,8 @@ pub enum MacroArg {
472472
Ident(String),
473473
/// An Arg Call
474474
ArgCall(String),
475+
/// A Nested Macro Call
476+
MacroCall(MacroInvocation),
475477
}
476478

477479
/// Free Storage Pointer Unit Struct

0 commit comments

Comments
 (0)