Skip to main content

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(&current_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(&macro_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}