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}