huff_neo_codegen/irgen/arg_calls.rs
1use crate::{Codegen, MAX_MACRO_RECURSION_DEPTH};
2use huff_neo_utils::ast::span::AstSpan;
3use huff_neo_utils::bytecode::{BytecodeSegments, JumpPlaceholderData, PushOpcode};
4use huff_neo_utils::prelude::*;
5use huff_neo_utils::scope::ScopeManager;
6use std::str::FromStr;
7
8/// Maximum iterations allowed when resolving argument references through the scope chain.
9/// Prevents infinite loops when circular argument references exist (e.g., arg1 -> arg2 -> arg1).
10const MAX_ARG_RESOLUTION_ITERATIONS: usize = 100;
11
12/// Maximum iterations for the argument bubbling loop in `bubble_arg_call`.
13/// Prevents infinite loops when bubbling arguments up through nested macro calls.
14const MAX_BUBBLE_ARG_ITERATIONS: usize = 1000;
15
16/// Resolves an argument reference to the actual macro name it represents.
17///
18/// Searches through the macro call stack to find where the argument is defined and
19/// follows any chained argument references until reaching a concrete macro name.
20///
21/// # Arguments
22/// * `arg_name` - The argument to resolve (e.g., "foo" from `<foo>`)
23/// * `scope_mgr` - Call stack context for argument lookup
24/// * `max_depth` - How far up the call stack to search
25/// * `contract` - Source of macro definitions
26///
27/// # Returns
28/// The resolved macro name, or an error if the argument is undefined or invalid.
29fn resolve_argument_through_scope<'a>(
30 arg_name: &str,
31 scope_mgr: &ScopeManager<'a>,
32 max_depth: usize,
33 contract: &Contract,
34) -> Result<String, CodegenErrorKind> {
35 let mut current_arg_name = arg_name.to_string();
36 let mut iteration_count = 0;
37
38 // Iterative resolution to prevent stack overflow
39 loop {
40 iteration_count += 1;
41 if iteration_count > MAX_ARG_RESOLUTION_ITERATIONS {
42 return Err(CodegenErrorKind::ArgResolutionLimitExceeded(current_arg_name, MAX_ARG_RESOLUTION_ITERATIONS));
43 }
44
45 tracing::debug!(target: "codegen", "Resolving argument '{}' through scope up to depth {} (iteration {})", current_arg_name, max_depth, iteration_count);
46
47 // Search through the frames in reverse (from deepest to root, up to max_depth)
48 let mut found = false;
49 for depth in (0..max_depth.min(scope_mgr.depth())).rev() {
50 if let Some(frame) = scope_mgr.frames.get(depth)
51 && let Some(ref invocation) = frame.invocation
52 {
53 tracing::debug!(target: "codegen", "Checking frame at depth {} for macro '{}'", depth, invocation.macro_name);
54
55 // Find the macro definition
56 if let Some(invoked_macro) = contract.find_macro_by_name(&invocation.macro_name) {
57 // Check if this macro has our argument
58 if let Some(param_index) = invoked_macro.parameters.iter().position(|p| p.name.as_deref() == Some(¤t_arg_name)) {
59 tracing::debug!(target: "codegen", "Found parameter '{}' at index {} in macro '{}'", current_arg_name, param_index, invocation.macro_name);
60
61 // Get the corresponding argument value
62 if let Some(arg_value) = invocation.args.get(param_index) {
63 match arg_value {
64 MacroArg::Ident(macro_name) => {
65 // Direct macro name - we're done
66 tracing::info!(target: "codegen", "Resolved '{}' to macro: {}", current_arg_name, macro_name);
67 return Ok(macro_name.clone());
68 }
69 MacroArg::ArgCall(arg_call) => {
70 // The argument is itself an argument reference - continue iterating
71 tracing::debug!(target: "codegen", "Argument '{}' references another argument '{}', continuing resolution", current_arg_name, arg_call.name);
72 current_arg_name = arg_call.name.clone();
73 found = true;
74 break; // Break inner loop to restart search with new arg name
75 }
76 MacroArg::ArgCallMacroInvocation(inner_arg_name, _) => {
77 // The argument is an invocation of another argument - resolve that argument
78 tracing::debug!(target: "codegen", "Argument '{}' is a macro invocation of argument '{}', continuing resolution", current_arg_name, inner_arg_name);
79 current_arg_name = inner_arg_name.clone();
80 found = true;
81 break; // Break inner loop to restart search with new arg name
82 }
83 MacroArg::Noop => {
84 // __NOOP cannot be invoked as a macro
85 return Err(CodegenErrorKind::InvalidMacroArgumentType(format!(
86 "Cannot invoke __NOOP as a macro in argument '{}'",
87 current_arg_name
88 )));
89 }
90 _ => {
91 // Other types of arguments not supported for macro invocation
92 tracing::debug!(target: "codegen", "Argument type {:?} not supported for macro invocation, continuing search", arg_value);
93 continue;
94 }
95 }
96 }
97 }
98 }
99 }
100 }
101
102 // If we didn't find anything that requires continuing the search, we're done
103 if !found {
104 tracing::error!(target: "codegen", "Failed to resolve argument '{}' in scope up to depth {}", current_arg_name, max_depth);
105 return Err(CodegenErrorKind::MissingArgumentDefinition(current_arg_name));
106 }
107 }
108}
109
110// Arguments can be literals, labels, opcodes, constants, or macro calls
111// !! IF THERE IS AMBIGUOUS NOMENCLATURE
112// !! (E.G. BOTH OPCODE AND LABEL ARE THE SAME STRING)
113// !! COMPILATION _WILL_ ERROR
114
115/// Resolves a macro argument reference and generates its bytecode.
116///
117/// When a macro body contains `<arg>`, this resolves what `arg` refers to (opcode, label,
118/// literal, or another macro call) and generates the appropriate bytecode.
119#[allow(clippy::too_many_arguments)]
120pub fn bubble_arg_call<'a>(
121 evm_version: &EVMVersion,
122 mut arg_parent_macro_name: String,
123 mut arg_name: String,
124 bytes: &mut BytecodeSegments,
125 mut macro_def: &'a MacroDefinition,
126 contract: &'a Contract,
127 scope_mgr: &mut ScopeManager<'a>,
128 offset: &mut usize,
129 jump_table: &mut JumpTable,
130 table_instances: &mut Jumps,
131 utilized_tables: &mut Vec<TableDefinition>,
132 span: &AstSpan,
133 relax_jumps: bool,
134) -> Result<(), CodegenError> {
135 tracing::debug!(target: "codegen", "bubble_arg_call: arg_name={}, macro_def={}", arg_name, macro_def.name);
136
137 // Safety check to prevent infinite loops
138 if scope_mgr.depth() > MAX_MACRO_RECURSION_DEPTH {
139 let macro_chain = scope_mgr.macro_stack().iter().map(|m| m.name.as_str()).collect::<Vec<_>>().join(" -> ");
140 return Err(CodegenError {
141 kind: CodegenErrorKind::InvalidMacroArgumentType(format!(
142 "Maximum macro recursion depth ({}) exceeded while resolving argument '{}' in macro '{}'.\nMacro call chain: {}\nThis likely indicates circular macro invocations or infinitely recursive argument resolution.",
143 MAX_MACRO_RECURSION_DEPTH, arg_name, arg_parent_macro_name, macro_chain
144 )),
145 span: span.clone_box(),
146 token: None,
147 });
148 }
149
150 let starting_offset = *offset;
151
152 // Start with full depth for searching
153 let mut search_depth = scope_mgr.depth();
154
155 let mut iteration_count = 0;
156
157 // Iterative loop instead of recursion
158 loop {
159 iteration_count += 1;
160 if iteration_count > MAX_BUBBLE_ARG_ITERATIONS {
161 return Err(CodegenError {
162 kind: CodegenErrorKind::BubbleArgLimitExceeded(arg_name.to_string(), MAX_BUBBLE_ARG_ITERATIONS),
163 span: span.clone_box(),
164 token: None,
165 });
166 }
167
168 tracing::debug!(
169 target: "codegen",
170 "bubble_arg_call iteration {}: {} -> {}, search_depth={}, current_macro={}",
171 iteration_count,
172 arg_parent_macro_name,
173 arg_name,
174 search_depth,
175 macro_def.name
176 );
177
178 // Get the current macro's invocation for looking up arguments in this macro
179 let current_invoc = scope_mgr.current_invocation();
180 if let Some(current_macro_invoc) = current_invoc {
181 let macro_invoc_tuple = (starting_offset, current_macro_invoc.clone());
182
183 // First get this arg_name position in the correct macro definition params
184 // We need to use the macro that corresponds to the arg_parent_macro_name, not the current macro_def
185 let target_macro = if arg_parent_macro_name == macro_def.name {
186 macro_def
187 } else {
188 // Find the macro that corresponds to the arg_parent_macro_name
189 scope_mgr.macro_stack().iter().find(|m| m.name == arg_parent_macro_name).map_or(macro_def, |v| v)
190 };
191
192 // Find the macro invocation that corresponds to the target macro
193 // Search through parent frames (using search_depth to respect exclusions)
194 let target_macro_invoc = if target_macro.name == macro_invoc_tuple.1.macro_name {
195 macro_invoc_tuple.clone()
196 } else {
197 // Search parent frames for the target macro's invocation, respecting search_depth
198 scope_mgr.frames[..search_depth.min(scope_mgr.depth())]
199 .iter()
200 .rev()
201 .find_map(|frame| {
202 frame
203 .invocation
204 .as_ref()
205 .and_then(|inv| if inv.macro_name == target_macro.name { Some((frame.offset, inv.clone())) } else { None })
206 })
207 .unwrap_or(macro_invoc_tuple.clone())
208 };
209
210 if let Some(pos) = target_macro.parameters.iter().position(|r| r.name.as_ref().is_some_and(|s| s.eq(&arg_name))) {
211 tracing::debug!(target: "codegen", "GOT \"{}\" POS IN ARG LIST: {} (from macro {} invocation {})", arg_name, pos, target_macro.name, target_macro_invoc.1.macro_name);
212
213 if let Some(arg) = target_macro_invoc.1.args.get(pos) {
214 tracing::debug!(target: "codegen", "ARG VALUE: {:?}", arg);
215
216 // Special case: if the argument is an ArgCallMacroInvocation, expand it here
217 if let MacroArg::ArgCallMacroInvocation(inner_arg_name, invoc_args) = arg {
218 tracing::info!(target: "codegen", "GOT ArgCallMacroInvocation for argument '{}', expanding it", inner_arg_name);
219
220 // We need to resolve inner_arg_name to get the actual macro name
221 // Use search_depth which may be reduced to exclude problematic frames
222 match resolve_argument_through_scope(inner_arg_name, scope_mgr, search_depth, contract) {
223 Ok(actual_macro_name) => {
224 if let Some(called_macro) = contract.find_macro_by_name(&actual_macro_name) {
225 let inner_mi = MacroInvocation {
226 macro_name: actual_macro_name.clone(),
227 args: invoc_args.clone(),
228 span: span.clone(),
229 };
230
231 scope_mgr.push_macro_with_invocation(called_macro, *offset, inner_mi);
232 let result =
233 Codegen::macro_to_bytecode(evm_version, called_macro, contract, scope_mgr, *offset, relax_jumps);
234 // Note: macro_to_bytecode already pops the macro from scope_mgr
235 let expanded = result?;
236 let byte_len: usize = expanded.bytes.iter().map(|seg| seg.bytes.len()).sum();
237 bytes.extend(expanded.bytes);
238 *offset += byte_len;
239
240 // Handle jumps and tables
241 for jump in expanded.unmatched_jumps {
242 if let Some(existing) = jump_table.get_mut(&jump.bytecode_index) {
243 existing.push(jump);
244 } else {
245 jump_table.insert(jump.bytecode_index, vec![jump]);
246 }
247 }
248 for table_instance in expanded.table_instances {
249 table_instances.push(table_instance);
250 }
251 for table in expanded.utilized_tables {
252 if !utilized_tables.contains(&table) {
253 utilized_tables.push(table);
254 }
255 }
256 return Ok(());
257 } else {
258 return Err(CodegenError {
259 kind: CodegenErrorKind::MissingMacroDefinition(actual_macro_name),
260 span: span.clone_box(),
261 token: None,
262 });
263 }
264 }
265 Err(e) => {
266 // Could not resolve the argument
267 return Err(CodegenError { kind: e, span: span.clone_box(), token: None });
268 }
269 }
270 }
271
272 // Original handling for other argument types
273 tracing::info!(target: "codegen", "GOT \"{:?}\" ARG FROM MACRO INVOCATION", arg);
274 match arg {
275 MacroArg::HexLiteral(l) => {
276 tracing::info!(target: "codegen", "GOT HEX LITERAL {} ARG FROM MACRO INVOCATION", bytes32_to_hex_string(l, false));
277 let push_bytes = literal_gen(evm_version, l);
278 *offset += push_bytes.len() / 2;
279 bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
280 }
281 MacroArg::Opcode(o) => {
282 tracing::info!(target: "codegen", "GOT \"{:?}\" OPCODE FROM MACRO INVOCATION", o);
283 let b = Bytes::Raw(o.to_string());
284 *offset += b.len();
285 bytes.push_with_offset(starting_offset, b);
286 }
287 MacroArg::Noop => {
288 tracing::info!(target: "codegen", "GOT __NOOP ARG FROM MACRO INVOCATION - GENERATING NO BYTECODE");
289 // Generate no bytecode - __NOOP is a no-op
290 }
291 MacroArg::BuiltinFunctionCall(bf) => {
292 tracing::info!(target: "codegen", "GOT BUILTIN FUNCTION CALL {:?} ARG FROM MACRO INVOCATION", bf.kind);
293 let push_value = Codegen::gen_builtin_bytecode(contract, bf, bf.span.clone())?;
294
295 // For __RIGHTPAD, always use PUSH32 with full 32 bytes (no optimization)
296 let push_bytes = match bf.kind {
297 BuiltinFunctionKind::RightPad => push_value.to_hex_full_with_opcode(),
298 _ => push_value.to_hex_with_opcode(evm_version),
299 };
300
301 *offset += push_bytes.len() / 2;
302 bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
303 }
304 MacroArg::ArgCall(ArgCall { macro_name: ac_parent_macro_name, name: ac, span: _arg_span }) => {
305 tracing::info!(target: "codegen", "GOT ARG CALL \"{}\" ARG FROM MACRO INVOCATION", ac);
306 tracing::debug!(target: "codegen", "~~~ BUBBLING UP ARG CALL");
307 let parent_scope_id = scope_mgr.parent_scope();
308 let bubbled_macro_exists = scope_mgr.macro_at_depth(parent_scope_id.depth).is_some();
309 tracing::debug!(target: "codegen", "BUBBLING UP WITH MACRO EXISTS: {}", bubbled_macro_exists);
310 tracing::debug!(target: "codegen", "CURRENT MACRO DEF: {}", macro_def.name);
311
312 // Verify we have a parent invocation to bubble from
313 if search_depth <= 1 {
314 return Err(CodegenError {
315 kind: CodegenErrorKind::MissingMacroInvocation(macro_def.name.to_string()),
316 span: span.clone_box(),
317 token: None,
318 });
319 }
320
321 if !bubbled_macro_exists {
322 return Err(CodegenError {
323 kind: CodegenErrorKind::MissingMacroInvocation(macro_def.name.to_string()),
324 span: span.clone_box(),
325 token: None,
326 });
327 }
328
329 // Get parent macro
330 let bubbled_macro_def = scope_mgr.macro_at_depth(parent_scope_id.depth).ok_or_else(|| CodegenError {
331 kind: CodegenErrorKind::MissingMacroInvocation(macro_def.name.to_string()),
332 span: span.clone_box(),
333 token: None,
334 })?;
335
336 // Check if current invocation matches current macro (prevents infinite loops)
337 // This replicates: if last_mi.1.macro_name.eq(¯o_def.name)
338 let should_exclude_current_frame = if let Some(current_inv) = scope_mgr.current_invocation() {
339 current_inv.macro_name == macro_def.name
340 } else {
341 false
342 };
343
344 tracing::debug!(target: "codegen", "Continuing to parent macro: {}, exclude_current={}", bubbled_macro_def.name, should_exclude_current_frame);
345
346 // Update loop variables for next iteration instead of recursing
347 arg_parent_macro_name = ac_parent_macro_name.clone();
348 arg_name = ac.clone();
349 macro_def = bubbled_macro_def;
350
351 // If we should exclude current frame, reduce search depth
352 if should_exclude_current_frame {
353 search_depth = search_depth.saturating_sub(1);
354 }
355
356 // Continue the loop to process with updated variables
357 continue;
358 }
359 MacroArg::Ident(iden) => {
360 tracing::warn!(target: "codegen", "Found MacroArg::Ident IN \"{}\" Macro Invocation: \"{}\"! (arg_name={}, target_macro={}, search_depth={})", target_macro_invoc.1.macro_name, iden, arg_name, target_macro.name, search_depth);
361
362 // Check for a constant first (clone to release the lock)
363 let const_value_opt = {
364 let constants = contract
365 .constants
366 .lock()
367 .map_err(|_| CodegenError::new(CodegenErrorKind::LockingError, AstSpan(vec![]), None))?;
368 constants.iter().find(|const_def| const_def.name.eq(iden)).map(|c| c.value.clone())
369 };
370
371 if let Some(const_value) = const_value_opt {
372 tracing::info!(target: "codegen", "ARGCALL IS CONSTANT: {:?}", const_value);
373 let push_bytes = match &const_value {
374 ConstVal::Bytes(bytes) => {
375 // Normalize so odd-nibble literals (e.g. 0x140) are byte-aligned,
376 // matching the direct `[CONST]` path.
377 literal_gen(evm_version, &str_to_bytes32(&bytes.as_str()))
378 }
379 ConstVal::String(_s) => {
380 return Err(CodegenError {
381 kind: CodegenErrorKind::StringConstantNotBytes(format!(
382 "String constant [{}] cannot be pushed directly. Use __BYTES([{}]) to convert it to bytes.",
383 iden, iden
384 )),
385 span: Box::new(target_macro_invoc.1.span.clone()),
386 token: None,
387 });
388 }
389 ConstVal::StoragePointer(sp) => {
390 let hex_literal: String = bytes32_to_hex_string(sp, false);
391 format!("{:02x}{hex_literal}", 95 + hex_literal.len() / 2)
392 }
393 ConstVal::BuiltinFunctionCall(bf) => {
394 let push_value = Codegen::gen_builtin_bytecode(contract, bf, target_macro_invoc.1.span.clone())?;
395 // For __RIGHTPAD, always use PUSH32 with full 32 bytes (no optimization)
396 match bf.kind {
397 BuiltinFunctionKind::RightPad => push_value.to_hex_full_with_opcode(),
398 _ => push_value.to_hex_with_opcode(evm_version),
399 }
400 }
401 ConstVal::Expression(expr) => {
402 tracing::info!(target: "codegen", "EVALUATING CONSTANT EXPRESSION FOR \"{iden}\"");
403 let evaluated = contract.evaluate_constant_expression(expr)?;
404 literal_gen(evm_version, &evaluated)
405 }
406 ConstVal::Noop => {
407 tracing::info!(target: "codegen", "CONSTANT \"{iden}\" IS __NOOP - GENERATING NO BYTECODE IN ARGCALL");
408 String::new() // Generate no bytecode
409 }
410 ConstVal::FreeStoragePointer(fsp) => {
411 // If this is reached in codegen stage,
412 // `derive_storage_pointers`
413 // method was not called on the AST.
414 tracing::error!(target: "codegen", "STORAGE POINTERS INCORRECTLY DERIVED FOR \"{fsp:?}\"");
415 return Err(CodegenError {
416 kind: CodegenErrorKind::StoragePointersNotDerived,
417 span: AstSpan(vec![]).boxed(),
418 token: None,
419 });
420 }
421 };
422 *offset += push_bytes.len() / 2;
423 tracing::info!(target: "codegen", "OFFSET: {}, PUSH BYTES: {:?}", offset, push_bytes);
424 bytes.push_with_offset(starting_offset, Bytes::Raw(push_bytes));
425 } else if let Ok(o) = Opcode::from_str(iden) {
426 tracing::debug!(target: "codegen", "Found Opcode: {}", o);
427 // The argument carries only the opcode name, so the stack operands
428 // that must follow DUPN, SWAPN and EXCHANGE would be missing
429 if o.has_stack_immediate() {
430 return Err(CodegenError {
431 kind: CodegenErrorKind::InvalidMacroArgumentType(format!(
432 "Opcode \"{iden}\" requires stack operands and cannot be passed as a macro argument; \
433 wrap it in a macro instead"
434 )),
435 span: target_macro_invoc.1.span.clone_box(),
436 token: None,
437 });
438 }
439 let b = Bytes::Raw(o.to_string());
440 *offset += b.len();
441 bytes.push_with_offset(starting_offset, b);
442 } else {
443 tracing::debug!(target: "codegen", "Found Label Call: {}", iden);
444
445 // This should be equivalent to a label call.
446 bytes.push_with_offset(
447 *offset,
448 Bytes::JumpPlaceholder(JumpPlaceholderData::new(iden.to_owned(), PushOpcode::Push2, String::new())),
449 );
450
451 let scope_id = scope_mgr.parent_scope();
452 jump_table.insert(
453 *offset,
454 vec![Jump {
455 label: iden.to_owned(),
456 bytecode_index: 0,
457 span: target_macro_invoc.1.span.clone(),
458 scope_id,
459 }],
460 );
461 *offset += 3;
462 }
463 }
464 MacroArg::ArgCallMacroInvocation(arg_name, invoc_args) => {
465 // Resolve the argument to get the actual macro name
466 // Use search_depth which may be reduced to exclude problematic frames
467 match resolve_argument_through_scope(arg_name, scope_mgr, search_depth, contract) {
468 Ok(actual_macro_name) => {
469 // Now invoke the resolved macro with the provided arguments
470 if let Some(called_macro) = contract.find_macro_by_name(&actual_macro_name) {
471 let inner_mi = MacroInvocation {
472 macro_name: actual_macro_name.clone(),
473 args: invoc_args.clone(),
474 span: span.clone(),
475 };
476
477 scope_mgr.push_macro_with_invocation(called_macro, *offset, inner_mi);
478 let result = Codegen::macro_to_bytecode(
479 evm_version,
480 called_macro,
481 contract,
482 scope_mgr,
483 *offset,
484 relax_jumps,
485 );
486 // Note: macro_to_bytecode already pops the macro from scope_mgr
487
488 let expanded_macro = result?;
489 let byte_len: usize = expanded_macro.bytes.iter().map(|seg| seg.bytes.len()).sum();
490 bytes.extend(expanded_macro.bytes);
491 *offset += byte_len;
492
493 // Bubble up jumps and tables
494 for unmatched_jump in expanded_macro.unmatched_jumps {
495 let existing_jumps =
496 jump_table.get(&unmatched_jump.bytecode_index).cloned().unwrap_or_else(Vec::new);
497 let mut new_jumps = existing_jumps;
498 new_jumps.push(unmatched_jump.clone());
499 jump_table.insert(unmatched_jump.bytecode_index, new_jumps);
500 }
501 for table_instance in expanded_macro.table_instances {
502 table_instances.push(table_instance);
503 }
504 for table in expanded_macro.utilized_tables {
505 if !utilized_tables.contains(&table) {
506 utilized_tables.push(table);
507 }
508 }
509 } else {
510 return Err(CodegenError {
511 kind: CodegenErrorKind::MissingMacroDefinition(actual_macro_name),
512 span: span.clone_box(),
513 token: None,
514 });
515 }
516 }
517 Err(e) => {
518 return Err(CodegenError { kind: e, span: span.clone_box(), token: None });
519 }
520 }
521 }
522 MacroArg::MacroCall(inner_mi) => {
523 // Check if this macro is already on the scope stack AND we're in bubbling mode
524 // to prevent infinite recursion. Bubbling mode is when search_depth < full depth.
525 let is_already_in_scope = scope_mgr.macro_stack().iter().any(|m| m.name == inner_mi.macro_name);
526 let is_bubbling = search_depth < scope_mgr.depth();
527
528 if is_already_in_scope && is_bubbling {
529 tracing::warn!(target: "codegen", "Macro {} is already in scope stack while bubbling, skipping to prevent infinite recursion", inner_mi.macro_name);
530 // Don't expand - this macro is already being executed higher up in the stack
531 // and we're bubbling to resolve an ArgCall, which would cause infinite recursion
532 // This prevents M2(M3(<arg>)) where M3 tries to resolve <arg> from infinitely recursing
533 } else if !inner_mi.args.is_empty() {
534 // This is an inline expansion case (macro with arguments)
535 tracing::debug!(target: "codegen", "Processing inline MacroCall argument: {}", inner_mi.macro_name);
536
537 if let Some(called_macro) = contract.find_macro_by_name(&inner_mi.macro_name) {
538 scope_mgr.push_macro_with_invocation(called_macro, *offset, inner_mi.clone());
539 let result =
540 Codegen::macro_to_bytecode(evm_version, called_macro, contract, scope_mgr, *offset, relax_jumps);
541 // Note: macro_to_bytecode already pops the macro from scope_mgr
542 match result {
543 Ok(expanded_macro) => {
544 let byte_len: usize = expanded_macro.bytes.iter().map(|seg| seg.bytes.len()).sum();
545 bytes.extend(expanded_macro.bytes);
546 *offset += byte_len;
547
548 // Bubble up unmatched jumps
549 for unmatched_jump in expanded_macro.unmatched_jumps {
550 let existing_jumps =
551 jump_table.get(&unmatched_jump.bytecode_index).cloned().unwrap_or_else(Vec::new);
552 let mut new_jumps = existing_jumps;
553 new_jumps.push(unmatched_jump.clone());
554 jump_table.insert(unmatched_jump.bytecode_index, new_jumps);
555 }
556
557 // Bubble up table instances
558 for table_instance in expanded_macro.table_instances {
559 table_instances.push(table_instance.clone());
560 }
561
562 // Bubble up utilized tables
563 for table in expanded_macro.utilized_tables {
564 if !utilized_tables.contains(&table) {
565 utilized_tables.push(table);
566 }
567 }
568 }
569 Err(e) => {
570 return Err(e);
571 }
572 }
573 } else {
574 return Err(CodegenError {
575 kind: CodegenErrorKind::MissingMacroDefinition(inner_mi.macro_name.clone()),
576 span: inner_mi.span.clone_box(),
577 token: None,
578 });
579 }
580 } else {
581 // This is a parameter substitution case (macro without arguments)
582 // Process it normally for substitution where <arg> appears
583 tracing::debug!(target: "codegen", "Processing parameter substitution MacroCall: {}", inner_mi.macro_name);
584
585 if let Some(called_macro) = contract.find_macro_by_name(&inner_mi.macro_name) {
586 tracing::debug!(target: "codegen", "Found valid macro: {}", called_macro.name);
587
588 scope_mgr.push_macro_with_invocation(called_macro, *offset, inner_mi.clone());
589 let result =
590 Codegen::macro_to_bytecode(evm_version, called_macro, contract, scope_mgr, *offset, relax_jumps);
591 // Note: macro_to_bytecode already pops the macro from scope_mgr
592 match result {
593 Ok(expanded_macro) => {
594 let byte_len: usize = expanded_macro.bytes.iter().map(|seg| seg.bytes.len()).sum();
595 bytes.extend(expanded_macro.bytes);
596 *offset += byte_len;
597
598 // Bubble up unmatched jumps from the expanded macro to the parent scope
599 // This is crucial for label resolution across macro boundaries
600 for unmatched_jump in expanded_macro.unmatched_jumps {
601 // Add the unmatched jump to the parent's jump table
602 let existing_jumps =
603 jump_table.get(&unmatched_jump.bytecode_index).cloned().unwrap_or_else(Vec::new);
604 let mut new_jumps = existing_jumps;
605 new_jumps.push(unmatched_jump.clone());
606 jump_table.insert(unmatched_jump.bytecode_index, new_jumps);
607
608 tracing::debug!(target: "codegen", "Bubbled up unmatched jump for label '{}' at index {} from MacroCall",
609 unmatched_jump.label, unmatched_jump.bytecode_index);
610 }
611
612 // Bubble up table instances from the expanded macro to the parent scope
613 // This is crucial for table resolution across macro boundaries
614 for table_instance in expanded_macro.table_instances {
615 table_instances.push(table_instance.clone());
616
617 tracing::debug!(target: "codegen", "Bubbled up table instance for label '{}' at index {} from MacroCall",
618 table_instance.label, table_instance.bytecode_index);
619 }
620
621 // Bubble up utilized tables from the expanded macro to the parent scope
622 for table in expanded_macro.utilized_tables {
623 if !utilized_tables.contains(&table) {
624 utilized_tables.push(table);
625 }
626 }
627 }
628 Err(e) => {
629 return Err(e);
630 }
631 }
632 } else {
633 return Err(CodegenError {
634 kind: CodegenErrorKind::MissingMacroDefinition(inner_mi.macro_name.clone()),
635 span: inner_mi.span.clone_box(),
636 token: None,
637 });
638 }
639 }
640 }
641 }
642
643 // Successfully handled the argument, return
644 return Ok(());
645 } else {
646 tracing::warn!(target: "codegen", "\"{}\" FOUND IN MACRO DEF \"{}\" BUT NOT IN MACRO INVOCATION \"{}\"!", arg_name, target_macro.name, target_macro_invoc.1.macro_name);
647 }
648 } else {
649 // Argument not found in current macro parameters.
650 // Before bubbling up, verify that the argument was actually passed to this macro.
651 tracing::debug!(target: "codegen", "Argument \"{}\" not found in macro \"{}\", checking if it was passed to this macro", arg_name, macro_def.name);
652
653 // Check if we can bubble - only if we're in a nested macro (depth > 1)
654 // In the test case: MAIN (depth 0) -> M1 (depth 1) -> M2 (depth 2)
655 // When M2 tries to access <arg>, we should NOT bubble because M2() was called without args
656 // The key is: we only bubble if the current macro itself has parameters that might reference parent args
657
658 // However, the current logic has an issue: when we recurse, we get the same `mis` from scope_mgr
659 // So we need a different approach: check if the macro definition has this parameter
660 // If not in current macro AND we're looking for an arg that wasn't passed to this macro, error out
661
662 // The parent_scope_id.depth should point to a valid parent
663 let parent_scope_id = scope_mgr.parent_scope();
664
665 // Check: do we have a parent macro AND is that parent's invocation still in our mis?
666 // The key insight: if this macro (macro_def) has NO parameters, it shouldn't access any args
667 if macro_def.parameters.is_empty() {
668 // This macro has no parameters, so it can't legally access any argument
669 tracing::debug!(target: "codegen", "Macro \"{}\" has no parameters, cannot access argument \"{}\"", macro_def.name, arg_name);
670 return Err(CodegenError {
671 kind: CodegenErrorKind::MissingArgumentDefinition(arg_name.to_string()),
672 span: span.clone_box(),
673 token: None,
674 });
675 }
676
677 // This macro has parameters, but the argument wasn't found
678 // Check if we can bubble to parent
679 if search_depth > 1 {
680 // Check if parent exists
681 let parent_exists = scope_mgr.macro_at_depth(parent_scope_id.depth).is_some();
682 if parent_exists {
683 // Get the parent macro
684 if let Some(bubbled_macro) = scope_mgr.macro_at_depth(parent_scope_id.depth) {
685 tracing::debug!(target: "codegen", "Argument \"{}\" not in macro \"{}\" params, bubbling up to macro \"{}\"", arg_name, macro_def.name, bubbled_macro.name);
686
687 // Always reduce search depth when bubbling up (replicates old code behavior)
688 search_depth = search_depth.saturating_sub(1);
689 macro_def = bubbled_macro;
690
691 // Continue the loop to process with updated variables
692 continue;
693 }
694 }
695 }
696
697 // No parent scope or at the top level - this is truly an undefined argument
698 return Err(CodegenError {
699 kind: CodegenErrorKind::MissingArgumentDefinition(arg_name.to_string()),
700 span: span.clone_box(),
701 token: None,
702 });
703 }
704 } else {
705 // This is a label call
706 tracing::info!(target: "codegen", "RECURSE_BYTECODE ARG CALL DEFAULTING TO LABEL CALL: \"{}\"", arg_name);
707
708 // Get the current invocation's span and offset
709 let (invoc_offset, new_span) = scope_mgr
710 .current_invocation_with_offset()
711 .map(|(off, invoc_opt)| (off, invoc_opt.map(|inv| inv.span.clone()).unwrap_or_else(|| AstSpan(vec![]))))
712 .unwrap_or((0, AstSpan(vec![])));
713
714 let scope_id = scope_mgr.parent_scope();
715 jump_table.insert(invoc_offset, vec![Jump { label: arg_name.to_owned(), bytecode_index: 0, span: new_span, scope_id }]);
716 bytes.push_with_offset(
717 *offset,
718 Bytes::JumpPlaceholder(JumpPlaceholderData::new(arg_name.to_owned(), PushOpcode::Push2, String::new())),
719 );
720 *offset += 3;
721 }
722
723 // Successfully processed the argument, exit the loop
724 return Ok(());
725 } // end of loop
726}