Skip to main content

neo_devpack_solidity/ir/context/
lowering_context.rs

1/// Task #91 — inlinable metadata for a library function whose first
2/// parameter is a storage-pointer struct. `param_names[0]` is bound as a
3/// storage alias to the receiver's `StorageReference`; `param_names[1..]`
4/// are bound as locals populated from the call-site args. See
5/// `member_calls.rs::inline_library_storage_call`.
6#[derive(Debug, Clone)]
7struct LibraryStorageBody {
8    param_names: Vec<String>,
9    value_param_types: Vec<Option<ValueType>>,
10    body: Statement,
11    return_type: Option<ValueType>,
12}
13
14/// Structured diagnostic emitted during IR lowering.
15///
16/// Captures the originating function name, a human-readable message, and an
17/// optional actionable suggestion so that CLI consumers can render richer
18/// error output than a bare string.
19#[derive(Debug, Clone, serde::Serialize)]
20pub struct IrDiagnostic {
21    pub function_name: String,
22    pub message: String,
23    pub suggestion: Option<String>,
24    pub code: Option<String>,
25}
26
27impl IrDiagnostic {
28    /// Format as a human-readable error string.
29    pub fn display(&self) -> String {
30        let mut out = format!("function '{}': {}", self.function_name, self.message);
31        if let Some(ref suggestion) = self.suggestion {
32            out.push_str(&format!("\n  help: {suggestion}"));
33        }
34        out
35    }
36}
37
38struct LoweringContext<'a> {
39    function_name: String,
40    /// Name of the contract that owns this lowering context.
41    ///
42    /// Used by member-access selector resolution to look up `this.method.selector`
43    /// expressions against the current contract's method table so they can be
44    /// lowered to their Ethereum keccak-4 selectors (matching how the AST shape
45    /// `MemberAccess(MemberAccess(Variable("this"), method), selector)` is
46    /// expected to behave in Solidity).
47    current_contract_name: String,
48    current_function_selector: [u8; 4],
49    is_safe: bool,
50    /// Task #64 — whether this function is directly callable from the host
51    /// (External or Public visibility). When true, multi-value returns are
52    /// lowered through `abiEncode` so the main-frame RET emits EVM-canonical
53    /// BE-packed bytes rather than a `StackItem::Array` that would leak as
54    /// serde_json at `stack_item_to_bytes`. Internal/Private functions keep
55    /// the Array shape so callers can destructure via `ArrayGet`.
56    is_externally_callable: bool,
57    param_index_map: HashMap<String, usize>,
58    param_types: &'a [ValueType],
59    return_slots: Vec<Option<usize>>,
60    return_types: Vec<ValueType>,
61    /// Task #185 — original Solidity type string for each declared return
62    /// parameter (e.g. `uint[3][2]`). Used by `lower_return_statement` to
63    /// detect nested fixed-size arrays (`T[N1][N2]...[Nk]`) so the return
64    /// path can emit flat EVM-canonical static encoding instead of the
65    /// dynamic-array offset+length wrapper. ValueType alone cannot carry
66    /// fixed-size info today (adding `fixed_size: Option<u64>` to
67    /// `ValueType::Array` would cascade through 55+ call sites), so the
68    /// fixed-shape hint rides alongside as a string.
69    return_type_strings: Vec<String>,
70    state_variables: &'a [StateVariableMetadata],
71    state_index_map: &'a HashMap<String, usize>,
72    state_types: &'a [ValueType],
73    /// Canonical struct type definitions available to the compilation unit.
74    ///
75    /// This enables resolving user-defined structs even when they are only used
76    /// in local variables (i.e., not present in state/param/return types).
77    defined_struct_types: &'a [ValueType],
78    /// Compile-time bound `N` for every fixed-size array struct field
79    /// (`struct S { uint256[3] arr; }`), keyed by `(struct_name, field_name)`.
80    ///
81    /// `ValueType::Array` collapses `T[N]` and `T[]` into one variant, but the
82    /// storage layout differs: fixed-size fields never maintain a length slot,
83    /// so the struct-field array bounds guard (storage-soundness fix, see
84    /// `lower_array_subscript_expression`) must use the declared `N` instead
85    /// of loading a length that would always read 0.
86    struct_fixed_array_bounds: &'a HashMap<(String, String), u64>,
87    event_index_map: &'a HashMap<String, usize>,
88    event_signature_map: &'a HashMap<String, Vec<ManifestType>>,
89    event_params_map: &'a HashMap<String, EventSignature>,
90    /// Declared custom `error` signatures keyed by error name. Used by the
91    /// revert/require lowering to compute EVM custom-error selectors from
92    /// the DECLARED parameter types and to reorder named arguments into
93    /// declaration order.
94    error_signature_map: &'a HashMap<String, ErrorAbiSignature>,
95    enum_variant_map: &'a HashMap<String, HashMap<String, u64>>,
96    contract_types: &'a HashSet<String>,
97    selector_registry: &'a SelectorRegistry,
98    function_names: &'a HashSet<String>,
99    function_overloads: &'a FunctionOverloadTable,
100    /// Every observed first-parameter type for each overload key
101    /// (name, arg_count).
102    ///
103    /// Used to enforce Solidity-style receiver compatibility for `using for`
104    /// member calls (`x.f(...)` lowers to `f(x, ...)`). Solidity allows
105    /// overloading by parameter type — `toInt128(int256)` and
106    /// `toInt128(uint256)` share the same `(name, arity)` key — so the
107    /// bucket stores ALL observed first-parameter types and the receiver
108    /// check accepts a match against ANY of them.
109    function_first_param_types: &'a HashMap<(String, usize), Vec<ValueType>>,
110    /// Task #191 — first return-parameter type for each overload key
111    /// (name, arg_count). Used by `infer_type_from_expression` to resolve
112    /// member access on a FunctionCall result (e.g. `makeCounter().value`
113    /// or `c.inc().value`), so the struct-field-access lowering can pick
114    /// the right field index instead of falling through to the drop-and-
115    /// push-zero compatibility branch. Only the FIRST return type is
116    /// recorded because member access on a multi-return function call is
117    /// not valid Solidity — `(a, b) = f()` uses tuple destructuring.
118    function_return_types: &'a HashMap<(String, usize), ValueType>,
119    /// Normalized target types from parsed `using` directives.
120    ///
121    /// `None` means wildcard target (`for *`).
122    using_target_types: &'a [Option<String>],
123    /// Function-list constraints from `using {f, g} for T`.
124    ///
125    /// Map key is normalized function name (lowercase) and values are allowed
126    /// target types (`None` means wildcard).
127    using_function_list_targets: &'a HashMap<String, Vec<Option<String>>>,
128    /// Target scopes that have at least one `using { ... } for T` directive.
129    using_function_list_scope_targets: &'a [Option<String>],
130    /// Ordered parameter names for each function overload, keyed by (name, arg_count).
131    /// Used to reorder named function call arguments into positional order.
132    function_param_names: &'a HashMap<(String, usize), Vec<String>>,
133    /// Functions that return void (empty return_parameters). Used to avoid
134    /// emitting DROP after calling a void internal function as a statement.
135    void_functions: &'a HashSet<String>,
136    /// Mapping from original method name to renamed super-method name.
137    /// Used to resolve `super.method()` calls during IR lowering.
138    super_method_map: &'a HashMap<String, String>,
139    /// Task #91 — library functions whose first parameter is `T storage`,
140    /// keyed by (name, arg_count). `member_calls.rs` inlines the body at
141    /// the call site rather than emitting `CallFunction`, so storage writes
142    /// (`d.x = v`) hit the caller's slot instead of a materialised copy.
143    library_storage_bodies: &'a HashMap<(String, usize), LibraryStorageBody>,
144    /// Task #196 — zero-arg internal functions that trivially return a
145    /// storage pointer to a state variable (body is `return <state_var>;`
146    /// and the return parameter is declared `T storage`). When a call site
147    /// references `foo()` whose result feeds into a storage operation
148    /// (`foo().push(v)`, `foo().length`, `foo()[i] = v`), the resolver
149    /// unwraps the call into the backing `Variable(state_var)` so the
150    /// downstream storage-reference machinery can alias the actual slot
151    /// instead of the raw `LoadState` value (which for an array state
152    /// variable is the LENGTH, not the backing Array — see
153    /// `emit_coerce_storage_value` for `ValueType::Array`). Extends Task
154    /// #117's local-binding alias fix (`uint[] storage a = arr;`) across
155    /// the function-return boundary.
156    storage_pointer_returning_fns: &'a HashMap<String, String>,
157    /// Task #91 — stack of (inline-return slot, end-label). When set,
158    /// `lower_return_statement` redirects `return expr;` to store into
159    /// `slot` and jump to `end_label` instead of emitting a raw `Return`
160    /// that would exit the caller.
161    inline_return_stack: Vec<(Option<usize>, usize)>,
162    /// Task #114 — modifier-epilogue return redirect. When set by
163    /// `function.rs` for a function whose body was wrapped by at least one
164    /// modifier with an epilogue (statements after `_;`), every
165    /// `Statement::Return(expr)` in the body must store into `slots` (one
166    /// per declared return parameter, already allocated at function
167    /// prologue) and jump to the INNERMOST modifier-wrap break label (see
168    /// `modifier_break_stack`) — or to `end_label` as a fallback when the
169    /// wrap hasn't been entered yet. The modifier-wrap break label lands
170    /// inside the Solidity expansion BETWEEN the inlined body and the
171    /// modifier epilogue, so tail statements (`locked = 0;` and friends)
172    /// still run before the actual RET. Distinct from the library-inline
173    /// single-slot mechanism (`inline_return_stack`): modifier returns must
174    /// carry multi-value tuples when the function declares `returns (T, U)`.
175    modifier_return_redirect: Option<(Vec<Option<usize>>, usize)>,
176    /// Task #114 — stack of break labels belonging to the synthetic
177    /// `do { body } while(false)` wrappers emitted by
178    /// `apply_modifier_calls_to_body_with_epilogue`. Unlike the normal
179    /// `loop_stack`, this tracks ONLY modifier-wrap scopes so a `return`
180    /// inside a user loop jumps past the user loop and into the OUTERMOST
181    /// modifier wrap's epilogue chain. Innermost wrap sits at the top.
182    modifier_break_stack: Vec<usize>,
183    local_index_map: HashMap<String, Vec<usize>>,
184    local_types: HashMap<usize, ValueType>,
185    scope_stack: Vec<Vec<String>>,
186    storage_aliases: HashMap<String, StorageReference>,
187    call_data_locals: HashMap<usize, String>,
188    local_count: u16,
189    /// Lazily-allocated pool of scratch local slots reused by the inline
190    /// software uint256 routines (add/sub/mul over 128-bit limbs). The routines
191    /// consume their scratch transiently and leave their result on the stack, so
192    /// every uint256 arith site in a function can share one pool — avoiding a
193    /// per-site allocation that would blow past NeoVM's local-slot limit.
194    u256_scratch: Vec<usize>,
195    /// Depth-indexed scratch-local pool for the nested-dynamic ABI
196    /// encoder/decoder (`emit_abi_dynamic_nested_array_tail` /
197    /// `emit_abi_decode_nested_array_tail_runtime`). `abi_nested_scratch[d]`
198    /// holds the reusable locals for nesting depth `d`: distinct depths never
199    /// alias (an inner `string[][]` element is encoded while the outer array's
200    /// locals are live), but every call site at the same depth shares one
201    /// block — so a function with many `abi.encode(string[])` calls does not
202    /// allocate a fresh batch of slots per site and blow NeoVM's 255 limit.
203    abi_nested_scratch: Vec<Vec<usize>>,
204    label_counter: usize,
205    loop_stack: Vec<LoopLabels>,
206    /// State variable indices currently being inlined (constant resolution).
207    /// Used to break infinite recursion when a constant's initializer
208    /// transitively references itself through a cross-contract alias.
209    resolving_constants: Vec<usize>,
210    errors: Vec<IrDiagnostic>,
211    warnings: Vec<crate::solidity::Diagnostic>,
212    /// Task #30: nested `unchecked { }` depth. When > 0, binary-arithmetic
213    /// lowerings skip the Solidity-0.8.x checked overflow guard emission.
214    /// Counter semantics (vs. a bool) correctly handle nested blocks:
215    /// ```text
216    /// unchecked { unchecked { a + b; } }  // depth 2 inside; guard still skipped
217    /// ```
218    unchecked_depth: usize,
219    /// Task #186 — function-pointer parameter/local bindings keyed by the
220    /// binding name. Populated by `Function::from_metadata_with_warnings` for
221    /// parameters whose declared Solidity type starts with `function` (the
222    /// only source of function-pointer values currently supported). Consumed
223    /// by `try_lower_variable_call` to emit a `CallIndirect` through `CALLA`
224    /// instead of the legacy "drop args, push 0" compatibility fallback.
225    function_pointer_bindings: HashMap<String, FunctionPointerBinding>,
226}
227
228/// Task #186 — per-binding metadata for an internal function-pointer local or
229/// parameter. Knowing `arg_count` lets the bytecode emitter pick the correct
230/// REVERSEN window size; `has_return` controls whether the `CallIndirect`
231/// result is treated as a value or a statement-expression.
232#[derive(Debug, Clone, Copy)]
233struct FunctionPointerBinding {
234    arg_count: usize,
235    has_return: bool,
236}
237
238/// Dispatch table for same-name functions: `(name, arity)` maps to every
239/// overload sharing that key, each carrying its parameter ValueTypes and the
240/// type-mangled neo_name. See [`LoweringContext::resolve_overload`].
241pub(crate) type FunctionOverloadTable =
242    HashMap<(String, usize), Vec<(Vec<ValueType>, String)>>;
243
244/// Compatibility test for same-arity overload resolution: is a call argument
245/// of type `arg` an acceptable match for a parameter of type `param`?
246///
247/// Integers match by SIGNEDNESS only — Solidity rejects same-arity overloads
248/// that differ solely by integer width as ambiguous, so width never has to
249/// distinguish them, whereas `int` vs `uint` can. Everything else (address,
250/// bool, bytesN, string, struct, ...) must match exactly so e.g. `f(uint256)`
251/// and `f(address)` are told apart.
252fn overload_arg_matches(arg: &ValueType, param: &ValueType) -> bool {
253    match (arg, param) {
254        (
255            ValueType::Integer { signed: a, .. },
256            ValueType::Integer { signed: p, .. },
257        ) => a == p,
258        _ => arg == param,
259    }
260}
261
262impl<'a> LoweringContext<'a> {
263    #[allow(clippy::too_many_arguments)]
264    fn new(
265        function_name: &str,
266        current_contract_name: &str,
267        current_function_selector: [u8; 4],
268        is_safe: bool,
269        is_externally_callable: bool,
270        param_index_map: HashMap<String, usize>,
271        param_types: &'a [ValueType],
272        state_variables: &'a [StateVariableMetadata],
273        state_index_map: &'a HashMap<String, usize>,
274        state_types: &'a [ValueType],
275        defined_struct_types: &'a [ValueType],
276        struct_fixed_array_bounds: &'a HashMap<(String, String), u64>,
277        event_index_map: &'a HashMap<String, usize>,
278        event_signature_map: &'a HashMap<String, Vec<ManifestType>>,
279        event_params_map: &'a HashMap<String, EventSignature>,
280        error_signature_map: &'a HashMap<String, ErrorAbiSignature>,
281        enum_variant_map: &'a HashMap<String, HashMap<String, u64>>,
282        contract_types: &'a HashSet<String>,
283        selector_registry: &'a SelectorRegistry,
284        function_names: &'a HashSet<String>,
285        function_overloads: &'a FunctionOverloadTable,
286        function_first_param_types: &'a HashMap<(String, usize), Vec<ValueType>>,
287        function_return_types: &'a HashMap<(String, usize), ValueType>,
288        using_target_types: &'a [Option<String>],
289        using_function_list_targets: &'a HashMap<String, Vec<Option<String>>>,
290        using_function_list_scope_targets: &'a [Option<String>],
291        function_param_names: &'a HashMap<(String, usize), Vec<String>>,
292        void_functions: &'a HashSet<String>,
293        super_method_map: &'a HashMap<String, String>,
294        library_storage_bodies: &'a HashMap<(String, usize), LibraryStorageBody>,
295        storage_pointer_returning_fns: &'a HashMap<String, String>,
296    ) -> Self {
297        Self {
298            function_name: function_name.to_string(),
299            current_contract_name: current_contract_name.to_string(),
300            current_function_selector,
301            is_safe,
302            is_externally_callable,
303            param_index_map,
304            param_types,
305            return_slots: Vec::new(),
306            return_types: Vec::new(),
307            return_type_strings: Vec::new(),
308            state_variables,
309            state_index_map,
310            state_types,
311            defined_struct_types,
312            struct_fixed_array_bounds,
313            event_index_map,
314            event_signature_map,
315            event_params_map,
316            error_signature_map,
317            enum_variant_map,
318            contract_types,
319            selector_registry,
320            function_names,
321            function_overloads,
322            function_first_param_types,
323            function_return_types,
324            using_target_types,
325            using_function_list_targets,
326            using_function_list_scope_targets,
327            function_param_names,
328            void_functions,
329            super_method_map,
330            library_storage_bodies,
331            storage_pointer_returning_fns,
332            inline_return_stack: Vec::new(),
333            modifier_return_redirect: None,
334            modifier_break_stack: Vec::new(),
335            local_index_map: HashMap::new(),
336            local_types: HashMap::new(),
337            scope_stack: vec![Vec::new()],
338            storage_aliases: HashMap::new(),
339            call_data_locals: HashMap::new(),
340            local_count: 0,
341            u256_scratch: Vec::new(),
342            abi_nested_scratch: Vec::new(),
343            label_counter: 0,
344            loop_stack: Vec::new(),
345            resolving_constants: Vec::new(),
346            errors: Vec::new(),
347            warnings: Vec::new(),
348            unchecked_depth: 0,
349            function_pointer_bindings: HashMap::new(),
350        }
351    }
352
353    /// Task #186 — register a function-pointer binding for a parameter or
354    /// local. `name` is the Solidity source name; `arg_count` and `has_return`
355    /// are derived from the declared `function(...)` type.
356    fn register_function_pointer_binding(
357        &mut self,
358        name: &str,
359        arg_count: usize,
360        has_return: bool,
361    ) {
362        self.function_pointer_bindings.insert(
363            name.to_string(),
364            FunctionPointerBinding {
365                arg_count,
366                has_return,
367            },
368        );
369    }
370
371    /// Task #186 — look up a function-pointer binding for an identifier.
372    fn function_pointer_binding(&self, name: &str) -> Option<&FunctionPointerBinding> {
373        self.function_pointer_bindings.get(name)
374    }
375
376    /// Returns `true` if currently lowering inside an `unchecked { ... }` block.
377    /// Used by `lower_binary_expr` to suppress the Solidity 0.8.x checked
378    /// overflow guard emission for Add/Sub/Mul.
379    fn in_unchecked_block(&self) -> bool {
380        self.unchecked_depth > 0
381    }
382
383    /// Increment unchecked depth when entering `unchecked { ... }`.
384    fn enter_unchecked_block(&mut self) {
385        self.unchecked_depth = self.unchecked_depth.saturating_add(1);
386    }
387
388    /// Decrement unchecked depth when leaving an `unchecked { ... }` block.
389    fn exit_unchecked_block(&mut self) {
390        self.unchecked_depth = self.unchecked_depth.saturating_sub(1);
391    }
392
393    fn current_function_selector(&self) -> [u8; 4] {
394        self.current_function_selector
395    }
396
397    /// Returns the name of the contract that owns this lowering context.
398    ///
399    /// Used to resolve `this.method.selector` expressions against the current
400    /// contract's method registry when the inner expression is
401    /// `Expression::Variable("this")`.
402    fn current_contract_name(&self) -> &str {
403        &self.current_contract_name
404    }
405
406    fn set_return_info(&mut self, slots: Vec<Option<usize>>, types: Vec<ValueType>) {
407        self.return_slots = slots;
408        self.return_types = types;
409    }
410
411    /// Task #185 — record the original Solidity type strings for each
412    /// declared return parameter. Called once per function during
413    /// `Function::from_metadata_with_warnings`. The strings retain static
414    /// fixed-size-array dimensions (e.g. `uint[3][2]`) that `ValueType`
415    /// doesn't preserve, enabling `lower_return_statement` to pick the
416    /// flat EVM-canonical encoding for nested fixed-size array returns.
417    fn set_return_type_strings(&mut self, type_strings: Vec<String>) {
418        self.return_type_strings = type_strings;
419    }
420
421    fn return_type_strings(&self) -> &[String] {
422        &self.return_type_strings
423    }
424
425    /// Task #91 — temporarily hide caller parameters that collide with an
426    /// inlined library parameter name (`setX(uint256 v) { d.store(v); }` would
427    /// otherwise resolve `v` inside the body to the caller's
428    /// `LoadParameter(0)`). Returns the previous entry for later restoration.
429    fn hide_param_binding(&mut self, name: &str) -> Option<usize> {
430        self.param_index_map.remove(name)
431    }
432
433    fn restore_param_binding(&mut self, name: String, index: Option<usize>) {
434        if let Some(idx) = index {
435            self.param_index_map.insert(name, idx);
436        }
437    }
438
439    /// Task #91 — push/pop an inline-return redirect; see
440    /// `inline_return_stack` docs and `lower_return_statement`.
441    fn push_inline_return(&mut self, slot: Option<usize>, end_label: usize) {
442        self.inline_return_stack.push((slot, end_label));
443    }
444
445    fn pop_inline_return(&mut self) {
446        self.inline_return_stack.pop();
447    }
448
449    /// Return the current inline-return target, if any.
450    fn inline_return_target(&self) -> Option<(Option<usize>, usize)> {
451        self.inline_return_stack.last().copied()
452    }
453
454    /// Task #114 — activate the modifier-epilogue return redirect. `slots`
455    /// is a per-return-parameter `LoadLocal` index (one per declared return),
456    /// and `end_label` is emitted after the expanded body. Inside
457    /// `lower_return_statement`, a Return expression stores into the slots
458    /// (in declaration order for multi-return) and jumps to `end_label`,
459    /// bypassing the raw RET that would otherwise skip the modifier
460    /// epilogue (e.g. `locked = 0;` after `_;`).
461    fn set_modifier_return_redirect(&mut self, slots: Vec<Option<usize>>, end_label: usize) {
462        self.modifier_return_redirect = Some((slots, end_label));
463    }
464
465    fn clear_modifier_return_redirect(&mut self) {
466        self.modifier_return_redirect = None;
467    }
468
469    /// Return the current modifier-return redirect (slots, end_label) if set.
470    /// The slots Vec mirrors `return_slots` (one entry per declared return).
471    fn modifier_return_target(&self) -> Option<(Vec<Option<usize>>, usize)> {
472        self.modifier_return_redirect.clone()
473    }
474
475    /// Task #114 — push a modifier-wrap break label (from the synthetic
476    /// `do { body } while(false)` emitted by the Solidity expander). Used by
477    /// `lower_do_while_statement` when `had_modifier_epilogue` is active AND
478    /// the condition is the constant `false` we inject — see
479    /// `src/solidity/analyse/modifiers/expand.rs`.
480    fn push_modifier_break_label(&mut self, label: usize) {
481        self.modifier_break_stack.push(label);
482    }
483
484    fn pop_modifier_break_label(&mut self) {
485        self.modifier_break_stack.pop();
486    }
487
488    /// Return the innermost modifier-wrap break label, or `None` if we are
489    /// not currently inside a modifier-epilogue scope. Used by
490    /// `lower_return_statement` to pick the correct jump target when
491    /// redirecting `return expr;` past user loops.
492    fn innermost_modifier_break_label(&self) -> Option<usize> {
493        self.modifier_break_stack.last().copied()
494    }
495
496    /// Returns `true` iff this function was flagged with
497    /// `had_modifier_epilogue` at the Solidity analyse layer. Mirrors the
498    /// `modifier_return_redirect` being Some — that redirect is only set by
499    /// `function.rs` when the flag was true.
500    fn in_modifier_epilogue_scope(&self) -> bool {
501        self.modifier_return_redirect.is_some()
502    }
503
504    fn return_slots(&self) -> &[Option<usize>] {
505        &self.return_slots
506    }
507
508    fn return_types(&self) -> &[ValueType] {
509        &self.return_types
510    }
511
512    fn is_externally_callable(&self) -> bool {
513        self.is_externally_callable
514    }
515
516    fn next_label(&mut self) -> usize {
517        let label = self.label_counter;
518        self.label_counter += 1;
519        label
520    }
521
522    fn push_loop(&mut self, continue_label: usize, break_label: usize) {
523        self.loop_stack.push(LoopLabels {
524            continue_label,
525            break_label,
526        });
527    }
528
529    fn pop_loop(&mut self) {
530        self.loop_stack.pop();
531    }
532
533    fn break_target(&self) -> Option<usize> {
534        self.loop_stack.last().map(|labels| labels.break_label)
535    }
536
537    fn continue_target(&self) -> Option<usize> {
538        self.loop_stack.last().map(|labels| labels.continue_label)
539    }
540
541    fn record_error(&mut self, message: impl Into<String>) {
542        self.errors.push(IrDiagnostic {
543            function_name: self.function_name.clone(),
544            message: message.into(),
545            suggestion: None,
546            code: None,
547        });
548    }
549
550    fn record_error_with_suggestion(
551        &mut self,
552        message: impl Into<String>,
553        suggestion: impl Into<String>,
554    ) {
555        self.errors.push(IrDiagnostic {
556            function_name: self.function_name.clone(),
557            message: message.into(),
558            suggestion: Some(suggestion.into()),
559            code: None,
560        });
561    }
562
563    fn record_warning(&mut self, message: impl Into<String>) {
564        self.warnings
565            .push(crate::solidity::Diagnostic::warning(message));
566    }
567
568    fn record_warning_with_suggestion(
569        &mut self,
570        message: impl Into<String>,
571        suggestion: impl Into<String>,
572    ) {
573        self.warnings
574            .push(crate::solidity::Diagnostic::warning(message).with_suggestion(suggestion));
575    }
576
577    fn set_call_data_local(&mut self, local_index: usize, method: String) {
578        self.call_data_locals.insert(local_index, method);
579    }
580
581    fn clear_call_data_local(&mut self, local_index: usize) {
582        self.call_data_locals.remove(&local_index);
583    }
584
585    fn call_data_method_for_local(&self, local_index: usize) -> Option<&str> {
586        self.call_data_locals
587            .get(&local_index)
588            .map(|method| method.as_str())
589    }
590
591    fn is_contract_type_name(&self, name: &str) -> bool {
592        self.contract_types.contains(name)
593    }
594
595    /// Returns `true` if the given state variable index is currently being
596    /// resolved (constant inlining). Used to break infinite recursion when a
597    /// constant's initializer transitively references itself.
598    fn is_resolving_constant(&self, index: usize) -> bool {
599        self.resolving_constants.contains(&index)
600    }
601
602    fn push_resolving_constant(&mut self, index: usize) {
603        self.resolving_constants.push(index);
604    }
605
606    fn pop_resolving_constant(&mut self) {
607        self.resolving_constants.pop();
608    }
609
610    fn type_method_selectors(&self, type_name: &str, method_name: &str) -> Option<&Vec<[u8; 4]>> {
611        self.selector_registry
612            .type_method_selectors
613            .get(type_name)
614            .and_then(|methods| methods.get(method_name))
615    }
616
617    fn is_interface_type_name(&self, name: &str) -> bool {
618        self.selector_registry.interface_types.contains(name)
619    }
620
621    fn interface_id_for_type(&self, type_name: &str) -> Option<[u8; 4]> {
622        let methods = self
623            .selector_registry
624            .type_method_selectors
625            .get(type_name)?;
626        let mut selectors: HashSet<[u8; 4]> = HashSet::new();
627        for overloads in methods.values() {
628            for selector in overloads {
629                selectors.insert(*selector);
630            }
631        }
632
633        let mut interface_id = [0u8; 4];
634        for selector in selectors {
635            for (idx, byte) in selector.iter().enumerate() {
636                interface_id[idx] ^= byte;
637            }
638        }
639        Some(interface_id)
640    }
641
642    fn state_type(&self, index: usize) -> Option<&ValueType> {
643        self.state_types.get(index)
644    }
645
646    fn state_metadata(&self, index: usize) -> Option<&StateVariableMetadata> {
647        self.state_variables.get(index)
648    }
649
650    /// Compile-time bound `N` when `struct_name.field_name` is declared as a
651    /// fixed-size array (`T[N]`), `None` for dynamic (`T[]`) or non-array
652    /// fields. See `struct_fixed_array_bounds`.
653    fn struct_fixed_array_bound(&self, struct_name: &str, field_name: &str) -> Option<u64> {
654        self.struct_fixed_array_bounds
655            .get(&(struct_name.to_string(), field_name.to_string()))
656            .copied()
657    }
658
659    fn can_write_state(&self, state_index: usize) -> bool {
660        let Some(meta) = self.state_metadata(state_index) else {
661            return true;
662        };
663
664        if !meta.is_immutable {
665            return true;
666        }
667
668        // Sibling-merge (Task #198 in solidity_analyse.rs) renames sibling
669        // constructors to `__ctor__<SiblingName>` and re-types them as
670        // `FunctionTy::Function` so the host's `_deploy` prologue doesn't
671        // accidentally re-run them. The body still semantically runs as
672        // construction code (it's invoked from the host's `new Sibling(...)`
673        // lowering), so it must remain allowed to assign to immutable
674        // state vars — same as a regular constructor.
675        //
676        // Inheritance flattening additionally preserves a base constructor
677        // body under `__super_<original>` / `__super2_<original>` etc. when
678        // a derived contract overrides the constructor's name. Those super-
679        // bodies also run as construction code from the derived constructor,
680        // so they need the same exemption. Concrete repro: Chainlink
681        // AutomationRegistry2_3 inherits from AutomationForwarder whose
682        // constructor writes to `immutable i_target`; the flattener stores
683        // the base body as `__super___ctor__AutomationForwarder` and the
684        // immutable-write check rejected it without this clause.
685        self.function_name == "constructor"
686            || self.function_name == "_deploy"
687            || self.function_name.starts_with("__ctor__")
688            || self.function_name.contains("__ctor__")
689    }
690
691    fn ensure_state_writable(&mut self, state_index: usize) -> bool {
692        if self.can_write_state(state_index) {
693            return true;
694        }
695
696        let variable_name = self
697            .state_metadata(state_index)
698            .and_then(|meta| meta.name.as_deref())
699            .unwrap_or("<unnamed>")
700            .to_string();
701
702        self.record_error_with_suggestion(
703            format!(
704                "cannot assign to immutable state variable '{variable_name}' outside constructor/deploy initialization"
705            ),
706            "initialize immutable values in the declaration or constructor only",
707        );
708        false
709    }
710
711    fn parameter_type(&self, name: &str) -> Option<&ValueType> {
712        self.param_index_map
713            .get(name)
714            .and_then(|idx| self.param_types.get(*idx))
715    }
716
717    fn local_type(&self, index: usize) -> Option<&ValueType> {
718        self.local_types.get(&index)
719    }
720
721    fn variable_type(&self, name: &str) -> Option<ValueType> {
722        if let Some(reference) = self.storage_alias(name) {
723            return Some(reference.value_type.clone());
724        }
725        if let Some(index) = self.state_index_map.get(name) {
726            if let Some(ty) = self.state_type(*index) {
727                return Some(ty.clone());
728            }
729        }
730        if let Some(ty) = self.parameter_type(name) {
731            return Some(ty.clone());
732        }
733        if let Some(local_index) = self.resolve_local(name) {
734            if let Some(ty) = self.local_type(local_index) {
735                return Some(ty.clone());
736            }
737        }
738        None
739    }
740
741    fn neo_function_name(&self, name: &str, arg_count: usize) -> Option<String> {
742        self.function_overloads
743            .get(&(name.to_string(), arg_count))
744            .and_then(|bucket| bucket.first().map(|(_, neo_name)| neo_name.clone()))
745    }
746
747    /// Resolve a same-arity overload by argument type. Solidity allows
748    /// `f(uint256)` and `f(address)` to share `(name, arity)`; the frontend
749    /// mangles them into distinct neo_names, and this picks the right one by
750    /// matching the call's inferred argument types against each overload's
751    /// declared parameter types. With a single overload it returns that name
752    /// directly (the common, non-overloaded case). Returns `None` when several
753    /// overloads exist and none matches confidently, so the caller fails loud
754    /// instead of dispatching to the wrong function.
755    fn resolve_overload(
756        &self,
757        name: &str,
758        arg_count: usize,
759        arg_types: &[Option<ValueType>],
760    ) -> Option<String> {
761        let bucket = self.function_overloads.get(&(name.to_string(), arg_count))?;
762        if bucket.len() == 1 {
763            return Some(bucket[0].1.clone());
764        }
765        let mut best: Option<(usize, &String)> = None;
766        for (params, neo_name) in bucket {
767            if params.len() != arg_count {
768                continue;
769            }
770            let mut score = 0usize;
771            let mut compatible = true;
772            for (param, arg) in params.iter().zip(arg_types.iter()) {
773                match arg {
774                    Some(arg) if overload_arg_matches(arg, param) => score += 1,
775                    Some(_) => {
776                        compatible = false;
777                        break;
778                    }
779                    None => {} // unknown arg type — neither matches nor disqualifies
780                }
781            }
782            if compatible && best.is_none_or(|(best_score, _)| score > best_score) {
783                best = Some((score, neo_name));
784            }
785        }
786        best.map(|(_, neo_name)| neo_name.clone())
787    }
788
789    /// Task #91 — fetch the inlinable body for a library function whose first
790    /// parameter is a storage-pointer struct. Returns `None` when the call
791    /// should go through the normal `CallFunction` path.
792    fn library_storage_body(&self, name: &str, arg_count: usize) -> Option<&LibraryStorageBody> {
793        self.library_storage_bodies
794            .get(&(name.to_string(), arg_count))
795    }
796
797    /// Task #196 — look up the state variable name aliased by a zero-arg
798    /// internal function returning `T storage`. Returns `None` when the
799    /// function isn't a simple storage-pointer alias. Used by the
800    /// storage-reference resolver and the `.length` fast path to unwrap
801    /// `fn()` into the backing state-var Expression so the caller can
802    /// write to the actual storage slot instead of a materialised copy.
803    fn storage_pointer_returning_fn(&self, name: &str) -> Option<&str> {
804        self.storage_pointer_returning_fns
805            .get(name)
806            .map(|s| s.as_str())
807    }
808
809    fn has_using_directives(&self) -> bool {
810        !self.using_target_types.is_empty()
811    }
812
813    fn using_target_allows_receiver(&self, receiver_type: &ValueType) -> bool {
814        if self.using_target_types.is_empty() {
815            return false;
816        }
817
818        let receiver_sig =
819            normalize_solidity_like_type_signature(&value_type_signature(receiver_type));
820        self.using_target_types.iter().any(|target| match target {
821            None => true,
822            Some(target_type) => using_target_matches_signature(target_type, &receiver_sig),
823        })
824    }
825
826    fn using_function_list_allows_receiver(
827        &self,
828        function_name: &str,
829        receiver_type: Option<&ValueType>,
830    ) -> bool {
831        let Some(receiver_type) = receiver_type else {
832            // Unknown receiver type: preserve compatibility, defer to runtime behavior.
833            return true;
834        };
835
836        let receiver_sig =
837            normalize_solidity_like_type_signature(&value_type_signature(receiver_type));
838        let list_scope_applies = self.using_function_list_scope_targets.iter().any(|target| {
839            target
840                .as_ref()
841                .is_none_or(|expected| using_target_matches_signature(expected, &receiver_sig))
842        });
843
844        // No function-list directives apply to this receiver type.
845        if !list_scope_applies {
846            return true;
847        }
848
849        let key = function_name.to_ascii_lowercase();
850        let Some(targets) = self.using_function_list_targets.get(&key) else {
851            // Function-list directives apply, but this function name was not listed.
852            return false;
853        };
854
855        targets.iter().any(|target| match target {
856            None => true,
857            Some(target_type) => using_target_matches_signature(target_type, &receiver_sig),
858        })
859    }
860
861    fn receiver_matches_function_overload(
862        &self,
863        function_name: &str,
864        arg_count: usize,
865        receiver_type: &ValueType,
866    ) -> bool {
867        let key = (function_name.to_string(), arg_count);
868        let Some(expected_types) = self.function_first_param_types.get(&key) else {
869            // Without type metadata, don't over-constrain existing behavior.
870            return true;
871        };
872        // A library `using` directive resolves the receiver against ANY
873        // overload of the named function — Solidity's overload resolution
874        // picks the first one whose parameter types are implicitly
875        // convertible from the actuals. We mirror that here: accept the call
876        // if any registered overload's first parameter accepts the receiver.
877        // This fixes Uniswap V4 `int256.toInt128()` after we taught the IR
878        // builder to keep BOTH `toInt128(int256)` and `toInt128(uint256)`
879        // entries in the same bucket.
880        expected_types
881            .iter()
882            .any(|expected| is_implicitly_convertible(receiver_type, expected))
883    }
884
885    /// Returns the ordered parameter names for a function overload, if known.
886    fn get_function_param_names(&self, name: &str, arg_count: usize) -> Option<&[String]> {
887        self.function_param_names
888            .get(&(name.to_string(), arg_count))
889            .map(|v| v.as_slice())
890    }
891
892    /// Task #191 — look up the first return type for a function overload.
893    /// `None` means no metadata (the function may be external, have no
894    /// return value, or the build pipeline didn't register it). Callers
895    /// should degrade gracefully (e.g., `infer_type_from_expression` falls
896    /// through to the existing type-inference logic).
897    fn get_function_return_type(&self, name: &str, arg_count: usize) -> Option<&ValueType> {
898        self.function_return_types
899            .get(&(name.to_string(), arg_count))
900    }
901
902    /// Returns true if the named function returns void (no return values).
903    fn is_void_function(&self, name: &str) -> bool {
904        self.void_functions.contains(name)
905    }
906
907    /// Returns the renamed super-method name for `super.method()` resolution.
908    ///
909    /// Task #85 — look up the caller-qualified key first
910    /// (`"{current_fn}::{method_name}"`) so that inside a preserved base body
911    /// `__super_foo` the nested `super.foo()` resolves to the NEXT-older
912    /// `__super2_foo`, not back to itself. Falls back to the unqualified key
913    /// for the top-of-chain derived frame and for any legacy call sites.
914    fn super_method_name(&self, method_name: &str) -> Option<&str> {
915        let qualified = format!("{}::{}", self.function_name, method_name);
916        if let Some(target) = self.super_method_map.get(&qualified) {
917            return Some(target.as_str());
918        }
919        self.super_method_map.get(method_name).map(|s| s.as_str())
920    }
921
922    fn event_signature(&self, event_name: &str) -> Option<&[ManifestType]> {
923        self.event_signature_map
924            .get(event_name)
925            .map(|sig| sig.as_slice())
926    }
927
928    fn event_evm_signature(&self, event_name: &str) -> Option<&EventSignature> {
929        self.event_params_map.get(event_name)
930    }
931
932    /// Declared signature for a custom `error`, or `None` when the name was
933    /// never declared in (or inherited by / file-level-merged into) the
934    /// current contract — callers then fall back to inferring canonical
935    /// types from the revert-site argument expressions.
936    fn error_signature(&self, error_name: &str) -> Option<&ErrorAbiSignature> {
937        self.error_signature_map.get(error_name)
938    }
939
940    fn allocate_local(&mut self, name: String, value_type: Option<ValueType>) -> usize {
941        let index = self.local_count as usize;
942        self.local_count = self.local_count.checked_add(1).unwrap_or(self.local_count);
943        if let Some(scope) = self.scope_stack.last_mut() {
944            scope.push(name.clone());
945        }
946        self.local_index_map.entry(name).or_default().push(index);
947        if let Some(ty) = value_type {
948            self.local_types.insert(index, ty);
949        }
950        index
951    }
952
953    /// Return `n` shared scratch local slots for the inline uint256 routines,
954    /// allocating (and caching) more on first demand. Reused across every
955    /// uint256 arith site in the current function.
956    fn u256_scratch_locals(&mut self, n: usize) -> Vec<usize> {
957        while self.u256_scratch.len() < n {
958            let i = self.u256_scratch.len();
959            let idx = self.allocate_local(format!("__u256_scratch_{i}"), None);
960            self.u256_scratch.push(idx);
961        }
962        self.u256_scratch[..n].to_vec()
963    }
964
965    /// Return `n` reusable scratch locals for the nested-dynamic ABI
966    /// encoder/decoder at nesting `depth`. Locals are lazily allocated and
967    /// shared across every call site reaching the same depth (see
968    /// [`Self::abi_nested_scratch`]).
969    fn abi_nested_scratch_locals(&mut self, depth: usize, n: usize) -> Vec<usize> {
970        while self.abi_nested_scratch.len() <= depth {
971            self.abi_nested_scratch.push(Vec::new());
972        }
973        while self.abi_nested_scratch[depth].len() < n {
974            let i = self.abi_nested_scratch[depth].len();
975            let idx = self.allocate_local(format!("__abi_nested_{depth}_{i}"), None);
976            self.abi_nested_scratch[depth].push(idx);
977        }
978        self.abi_nested_scratch[depth][..n].to_vec()
979    }
980
981    fn resolve_local(&self, name: &str) -> Option<usize> {
982        self.local_index_map
983            .get(name)
984            .and_then(|stack| stack.last().copied())
985    }
986
987    fn ensure_local(&mut self, name: &str) -> usize {
988        if let Some(index) = self.resolve_local(name) {
989            index
990        } else {
991            self.allocate_local(name.to_string(), None)
992        }
993    }
994
995    fn enter_scope(&mut self) {
996        self.scope_stack.push(Vec::new());
997    }
998
999    fn exit_scope(&mut self) {
1000        if let Some(names) = self.scope_stack.pop() {
1001            for name in names {
1002                if let Some(stack) = self.local_index_map.get_mut(&name) {
1003                    if let Some(index) = stack.pop() {
1004                        self.local_types.remove(&index);
1005                    }
1006                    if stack.is_empty() {
1007                        self.local_index_map.remove(&name);
1008                    }
1009                }
1010                self.storage_aliases.remove(&name);
1011            }
1012        }
1013    }
1014
1015    fn is_local_in_current_scope(&self, name: &str) -> bool {
1016        self.scope_stack
1017            .last()
1018            .is_some_and(|scope| scope.iter().any(|existing| existing == name))
1019    }
1020
1021    fn set_storage_alias(&mut self, name: String, alias: StorageReference) {
1022        self.storage_aliases.insert(name, alias);
1023    }
1024
1025    fn storage_alias(&self, name: &str) -> Option<&StorageReference> {
1026        self.storage_aliases.get(name)
1027    }
1028}
1029
1030fn normalize_solidity_like_type_signature(raw: &str) -> String {
1031    let compact = raw
1032        .chars()
1033        .filter(|c| !c.is_ascii_whitespace())
1034        .collect::<String>()
1035        .replace("payable", "");
1036    let lowered = compact.to_ascii_lowercase();
1037    match lowered.as_str() {
1038        "uint" => "uint256".to_string(),
1039        "int" => "int256".to_string(),
1040        "byte" => "bytes1".to_string(),
1041        other => other.to_string(),
1042    }
1043}
1044
1045fn value_type_signature(value_type: &ValueType) -> String {
1046    match value_type {
1047        ValueType::Integer { signed: true, bits } => format!("int{bits}"),
1048        ValueType::Integer {
1049            signed: false,
1050            bits,
1051        } => format!("uint{bits}"),
1052        ValueType::Boolean => "bool".to_string(),
1053        ValueType::String => "string".to_string(),
1054        ValueType::Address => "address".to_string(),
1055        ValueType::ByteArray {
1056            fixed_len: Some(len),
1057        } => format!("bytes{len}"),
1058        ValueType::ByteArray { fixed_len: None } => "bytes".to_string(),
1059        ValueType::Array(inner) => format!("{}[]", value_type_signature(inner)),
1060        ValueType::Mapping { key, value } => format!(
1061            "mapping({}=>{})",
1062            value_type_signature(key),
1063            value_type_signature(value)
1064        ),
1065        ValueType::Struct { name, .. } => name.to_ascii_lowercase(),
1066        ValueType::Any => "any".to_string(),
1067    }
1068}
1069
1070/// Task #91 — match a `using X for T` directive target against a receiver
1071/// signature. The frontend renders `using L for L.Data;` as target
1072/// `"l.data"`, but `ValueType::Struct { name: "Data" }` normalises to
1073/// `"data"` (`lookup_struct` strips the qualifier). Fall back to matching
1074/// by the last `.`-separated segment so storage-pointer struct receivers
1075/// dispatch correctly.
1076fn using_target_matches_signature(target: &str, receiver_sig: &str) -> bool {
1077    target == receiver_sig
1078        || target
1079            .rsplit_once('.')
1080            .is_some_and(|(_, last)| last == receiver_sig)
1081}
1082
1083fn is_implicitly_convertible(actual: &ValueType, expected: &ValueType) -> bool {
1084    match (actual, expected) {
1085        (_, ValueType::Any) | (ValueType::Any, _) => true,
1086        (
1087            ValueType::Integer {
1088                signed: actual_signed,
1089                bits: actual_bits,
1090            },
1091            ValueType::Integer {
1092                signed: expected_signed,
1093                bits: expected_bits,
1094            },
1095        ) => actual_signed == expected_signed && actual_bits <= expected_bits,
1096        (ValueType::Boolean, ValueType::Boolean)
1097        | (ValueType::String, ValueType::String)
1098        | (ValueType::Address, ValueType::Address) => true,
1099        (
1100            ValueType::ByteArray {
1101                fixed_len: actual_len,
1102            },
1103            ValueType::ByteArray {
1104                fixed_len: expected_len,
1105            },
1106        ) => match (actual_len, expected_len) {
1107            (_, None) => true,
1108            (Some(actual), Some(expected)) => actual == expected,
1109            (None, Some(_)) => false,
1110        },
1111        (ValueType::Array(actual_inner), ValueType::Array(expected_inner)) => {
1112            is_implicitly_convertible(actual_inner, expected_inner)
1113        }
1114        (
1115            ValueType::Mapping {
1116                key: actual_key,
1117                value: actual_value,
1118            },
1119            ValueType::Mapping {
1120                key: expected_key,
1121                value: expected_value,
1122            },
1123        ) => {
1124            is_implicitly_convertible(actual_key, expected_key)
1125                && is_implicitly_convertible(actual_value, expected_value)
1126        }
1127        (
1128            ValueType::Struct {
1129                name: actual_name, ..
1130            },
1131            ValueType::Struct {
1132                name: expected_name,
1133                ..
1134            },
1135        ) => actual_name.eq_ignore_ascii_case(expected_name),
1136        _ => false,
1137    }
1138}