neo_devpack_solidity/solidity/solidity_analyse.rs
1pub fn analyse_source(source: &str) -> Result<ContractMetadata, SolidityError> {
2 let mut contracts = analyse_all_sources(source)?;
3 Ok(contracts.swap_remove(0))
4}
5
6pub fn analyse_all_sources(source: &str) -> Result<Vec<ContractMetadata>, SolidityError> {
7 fn is_builtin_library_name(name: &str) -> bool {
8 matches!(
9 name,
10 "Runtime" | "abi" | "Storage" | "Syscalls" | "Neo" | "NativeCalls"
11 )
12 }
13
14 fn normalize_library_for_neo(mut contract: ContractIR) -> ContractIR {
15 if !matches!(contract.kind, ContractKind::Library) {
16 return contract;
17 }
18
19 // Neo N3 libraries are inlined into contracts; treat externally visible
20 // library functions as internal helper functions to avoid exposing them
21 // through the contract ABI.
22 for function in &mut contract.functions {
23 if !matches!(function.ty, FunctionTy::Function) {
24 continue;
25 }
26 if matches!(
27 function.visibility,
28 VisibilityKind::External | VisibilityKind::Public
29 ) {
30 function.visibility = VisibilityKind::Internal;
31 }
32 }
33
34 // Keep merged library state as internal implementation detail.
35 // Public library constants would otherwise synthesize contract-level
36 // getters and create ABI/name collisions in the consuming contract.
37 for state in &mut contract.state_variables {
38 state.visibility = Some("internal".to_string());
39 }
40
41 contract
42 }
43
44 fn collect_contract_types(
45 contract_map: &std::collections::HashMap<String, ContractIR>,
46 ) -> Vec<String> {
47 let mut contract_types: Vec<String> = Vec::new();
48 let mut seen_contract_types = std::collections::HashSet::new();
49
50 for contract in contract_map.values() {
51 let include_as_contract_type = match contract.kind {
52 ContractKind::Contract | ContractKind::AbstractContract | ContractKind::Interface => {
53 true
54 }
55 ContractKind::Library => !is_builtin_library_name(contract.name.as_str()),
56 };
57
58 if include_as_contract_type
59 && seen_contract_types.insert(contract.name.to_ascii_lowercase())
60 {
61 contract_types.push(contract.name.clone());
62 }
63 }
64
65 contract_types
66 }
67
68 let mut primary = Vec::new();
69 let mut fallback = Vec::new();
70
71 let contracts = parse_source(source)?;
72 for contract in contracts {
73 if matches!(
74 contract.kind,
75 ContractKind::Contract | ContractKind::AbstractContract
76 ) {
77 primary.push(contract);
78 } else {
79 fallback.push(contract);
80 }
81 }
82
83 let has_primary = !primary.is_empty();
84 let pre_merge_contract_map: std::collections::HashMap<String, ContractIR> = primary
85 .iter()
86 .chain(fallback.iter())
87 .map(|contract| (contract.name.clone(), contract.clone()))
88 .collect();
89 let contract_types = collect_contract_types(&pre_merge_contract_map);
90
91 let raw_libraries: Vec<ContractIR> = if has_primary {
92 fallback
93 .iter()
94 .filter(|contract| matches!(contract.kind, ContractKind::Library))
95 // Built-in helper libraries (Runtime/Storage/Syscalls/Neo) are lowered directly during
96 // IR generation. Avoid merging their Solidity bodies into user contracts since they
97 // may contain EVM-only stubs or unsupported constructs, and they would bloat bytecode.
98 .filter(|contract| !is_builtin_library_name(contract.name.as_str()))
99 .cloned()
100 .collect()
101 } else {
102 Vec::new()
103 };
104
105 // Validate user libraries before merging. Convert each library to metadata
106 // and run the standard validation pipeline to catch library-specific errors
107 // (state variables, constructors, external functions) early.
108 //
109 // Cross-library struct references — e.g. `function executeInitReserve(
110 // ConfiguratorInputTypes.InitReserveInput calldata input)` declared in
111 // library `ConfiguratorLogic` and referencing a struct from library
112 // `ConfiguratorInputTypes` (both shipped in @aave/core-v3) — require that
113 // each library's validation pass see the structs declared in its peers.
114 // Otherwise `NeoType::from_solidity` can't resolve the qualified type,
115 // `param.neo_type` stays `None`, and the external-function check fires a
116 // spurious "uses unsupported type" error.
117 //
118 // We solve this by pre-merging every other library's structs (and enums,
119 // for symmetry) into each library's struct table before running its
120 // validation. Doing the merge here (instead of at flatten time) keeps the
121 // mutation scoped to a clone and means downstream stages still see the
122 // original, un-merged library tree.
123 let library_struct_pool: Vec<StructIR> = raw_libraries
124 .iter()
125 .flat_map(|lib| lib.structs.iter().cloned())
126 .collect();
127 let library_enum_pool: Vec<EnumIR> = raw_libraries
128 .iter()
129 .flat_map(|lib| lib.enums.iter().cloned())
130 .collect();
131 for lib in &raw_libraries {
132 let mut lib_with_peers = lib.clone();
133 for s in &library_struct_pool {
134 if !lib_with_peers.structs.iter().any(|own| own.name == s.name) {
135 lib_with_peers.structs.push(s.clone());
136 }
137 }
138 for e in &library_enum_pool {
139 if !lib_with_peers.enums.iter().any(|own| own.name == e.name) {
140 lib_with_peers.enums.push(e.clone());
141 }
142 }
143 // Run normalize first so the validation sees the post-merge
144 // semantics — library external functions get converted to internal
145 // BEFORE validate enforces "no storage parameter on external
146 // functions". Otherwise the validator rejects legitimate library
147 // patterns like `EModeLogic.executeSetUserEMode(mapping storage, ...)`
148 // (Aave) where the function is `external` in source but operates as
149 // an internal helper on Neo (libraries inline into their callers).
150 let normalized_lib = normalize_library_for_neo(lib_with_peers);
151 let lib_metadata = convert_contract(
152 normalized_lib,
153 &[],
154 &contract_types,
155 std::sync::Arc::new(SelectorRegistry::default()),
156 );
157 let lib_diagnostics = validate_contract(&lib_metadata);
158 let lib_errors: Vec<Diagnostic> = lib_diagnostics
159 .into_iter()
160 .filter(|d| matches!(d.severity, DiagnosticSeverity::Error))
161 .collect();
162 if !lib_errors.is_empty() {
163 let messages: Vec<String> = lib_errors.iter().map(|d| {
164 let mut msg = d.message.clone();
165 if let Some(suggestion) = &d.suggestion {
166 msg.push_str(&format!("\n suggestion: {suggestion}"));
167 }
168 msg
169 }).collect();
170 return Err(SolidityError::analysis(messages.join("\n")));
171 }
172 }
173
174 let libraries: Vec<ContractIR> = raw_libraries
175 .into_iter()
176 .map(normalize_library_for_neo)
177 .collect();
178
179 // Task #83 — when a primary contract `A` runs `B b = new B(); b.foo();`
180 // the compiler emits a 20-byte zero placeholder for `b` and lowers
181 // `b.foo()` as `System.Contract.Call([0;20], "foo", flags, args)`. B's
182 // compiled body is a separate artifact, so without help the call would
183 // return `Null` and A's return value would silently go empty. Fix:
184 // merge every sibling primary's public/external functions that A
185 // references via `new X()` into A's own function table (name-preserving,
186 // host-wins-on-collision); the runtime then routes the zero-hash call
187 // through `self_method_offsets` — see the Task #83 branch in
188 // `execution_impl_part2_contract_call.rs`.
189 if has_primary {
190 let sibling_fn_map: std::collections::HashMap<String, Vec<FunctionIR>> = primary
191 .iter()
192 .map(|c| {
193 (
194 c.name.clone(),
195 c.functions
196 .iter()
197 .filter(|f| {
198 // Include abstract internal function declarations
199 // (body = None) alongside concrete externals.
200 // When sibling-merge pulls in an external body
201 // like `rawFulfillRandomWords` whose body calls
202 // an abstract sibling-internal function (e.g.
203 // VRFConsumerBaseV2's `fulfillRandomWords(uint256,
204 // uint256[])`), the host's IR-lowering pass would
205 // otherwise fail the overload lookup with
206 // "no overload of 'fulfillRandomWords' with 2
207 // argument(s)". Importing the abstract declaration
208 // satisfies the lookup; at runtime the call lands
209 // on an empty stub (RET-only) which is acceptable
210 // for the dead-code paths that typically include
211 // such helpers transitively.
212 let is_abstract_internal = matches!(f.ty, FunctionTy::Function)
213 && f.body.is_none();
214 // Task #126 — include Fallback (and Receive) alongside
215 // ordinary external/public named functions so that a
216 // primary contract whose only entrypoint is
217 // `fallback()` still contributes its dispatcher to
218 // the caller's merged function table when the caller
219 // invokes a method the callee doesn't declare.
220 //
221 // Without this, `try Target(t).nonExistentMethod()`
222 // (where TargetImpl only defines `fallback()`)
223 // would never be able to route through the zero-
224 // placeholder self-offsets path: the fallback entry
225 // simply wouldn't be in the merge set, and the
226 // runtime's unknown-method path would silently
227 // return Null rather than propagating the fallback's
228 // revert back to the caller's catch clause.
229 let is_named_external = matches!(f.ty, FunctionTy::Function)
230 && matches!(
231 f.visibility,
232 VisibilityKind::External | VisibilityKind::Public
233 );
234 let is_fallback_like = matches!(
235 f.ty,
236 FunctionTy::Fallback | FunctionTy::Receive
237 );
238 // (We deliberately do NOT include
239 // `is_abstract_internal` here. Including abstract
240 // internal declarations would let merged bodies
241 // reference them, but it also fails the "all
242 // abstract methods implemented" validation since
243 // those functions don't have a body in the host.
244 // We handle the missing-overload case at
245 // IR-lowering time by emitting a runtime trap
246 // instead of a compile error.)
247 let _ = is_abstract_internal;
248 is_named_external || is_fallback_like
249 })
250 .cloned()
251 .collect::<Vec<_>>(),
252 )
253 })
254 .collect();
255 // Companion map: every sibling's modifier definitions — INCLUDING
256 // modifiers reachable through the sibling's inheritance chain. When
257 // sibling functions are merged into a host below, any modifier they
258 // apply (`function upgrade(...) public payable virtual onlyOwner`)
259 // must still resolve in the host. The host's local `modifier_defs`
260 // only sees ITS OWN modifier declarations — so without a parallel
261 // merge pass, merged `upgrade`'s `onlyOwner` lookup fails with
262 // "unresolved modifier 'onlyOwner' with 0 argument(s)". Repro: OZ
263 // TransparentUpgradeableProxy / ProxyAdmin import cycle, where
264 // ProxyAdmin's `onlyOwner` lives in its base contract Ownable.
265 //
266 // Because the sibling's own inheritance flattening hasn't happened
267 // yet at this point in the pipeline, we walk each sibling's
268 // linearized base chain ourselves and union their modifier
269 // definitions per sibling, keyed by (name, arity), preferring
270 // bodied modifiers over abstract declarations.
271 let sibling_modifier_map: std::collections::HashMap<String, Vec<FunctionIR>> = primary
272 .iter()
273 .map(|c| {
274 let mut seen: std::collections::HashMap<(String, usize), FunctionIR> =
275 std::collections::HashMap::new();
276 let mut visit = |contract: &ContractIR| {
277 for f in &contract.functions {
278 if !matches!(f.ty, FunctionTy::Modifier) {
279 continue;
280 }
281 let key = (f.name.clone(), f.parameters.len());
282 match seen.get(&key) {
283 Some(existing) if existing.body.is_some() => {}
284 _ => {
285 seen.insert(key, f.clone());
286 }
287 }
288 }
289 };
290 visit(c);
291 // Try to walk the linearization. If it fails (shouldn't —
292 // we already ran it during pre-merge analysis to detect
293 // cycles), fall back to direct base inspection.
294 if let Ok(chain) =
295 contract_linearization_base_to_derived(&c.name, &pre_merge_contract_map)
296 {
297 for ancestor_name in &chain {
298 if ancestor_name == &c.name {
299 continue;
300 }
301 if let Some(ancestor) = pre_merge_contract_map.get(ancestor_name) {
302 visit(ancestor);
303 }
304 }
305 }
306 (c.name.clone(), seen.into_values().collect::<Vec<_>>())
307 })
308 .collect();
309 // Task #197 — parallel state-variable map. When a sibling's external
310 // function body references state variables (e.g. Mock.balanceOf
311 // reading `_bal[a]`), merging only the FunctionIR leaves the
312 // identifier unresolved in the caller's `state_index_map`, so the
313 // variable lowering falls through to the `Integer(0)` placeholder
314 // path (src/ir/expressions/variable.rs) and downstream opcodes like
315 // SIZE/PICKITEM fault on the wrong StackItem type. The storage key
316 // is derived from the state variable's name (see
317 // `value_types.rs::compute_state_slot`), so merging Mock's `_bal`
318 // into Client lets Client's compiled balanceOf read the same
319 // storage slot Mock.mint wrote to.
320 let sibling_state_map: std::collections::HashMap<String, Vec<StateVariableIR>> =
321 primary
322 .iter()
323 .map(|c| (c.name.clone(), c.state_variables.clone()))
324 .collect();
325 // Task #198 — parallel constructor map. For `new Child(x, y)` inside a
326 // Parent contract, the compiled Child lives in a separate artifact, so
327 // the Parent's runtime invocation of its own `_deploy` never runs the
328 // Child constructor. Without executing the ctor body, Child's state
329 // variables (`a`, `b`) stay at their zero defaults — and the follow-up
330 // `c.a()` / `c.b()` cross-contract calls (routed through sibling-merge
331 // self-offsets; Task #83) therefore observe zeros.
332 //
333 // Fix: expose each sibling's constructor as a regular, internal,
334 // name-mangled function (`__ctor__<SiblingName>`) in the caller's
335 // merged function table. The `new Child(x, y)` lowering then calls
336 // `__ctor__Child(x, y)` in-line, running the ctor body against the
337 // already-merged sibling state-variable slots (Task #197). The address
338 // return value stays at the 20-byte zero placeholder that Task #83
339 // already routes to self-offsets dispatch.
340 let sibling_ctor_map: std::collections::HashMap<String, FunctionIR> = primary
341 .iter()
342 .filter_map(|c| {
343 let ctor = c
344 .functions
345 .iter()
346 .find(|f| matches!(f.ty, FunctionTy::Constructor))?;
347 Some((c.name.clone(), ctor.clone()))
348 })
349 .collect();
350 let primary_contract_map: std::collections::HashMap<String, ContractIR> = primary
351 .iter()
352 .map(|c| (c.name.clone(), c.clone()))
353 .collect();
354 let primary_names: std::collections::HashSet<String> =
355 primary.iter().map(|c| c.name.clone()).collect();
356
357 // Task #126 — a primary contract's `fallback()` acts as a universal
358 // catch-all dispatcher: every unknown method name falls through to
359 // it. For interface-cast routing `Target(t).someMethod()` where
360 // `TargetImpl` has only `fallback()` (no named external methods),
361 // we must still treat `TargetImpl` as a valid implementor of the
362 // `Target` interface so the sibling-merge pass pulls its fallback
363 // body into the caller's function table. This mirrors Solidity's
364 // own runtime semantics: the ABI dispatcher routes unknown
365 // selectors to `fallback()` when present.
366 let primary_has_fallback: std::collections::HashSet<String> = primary
367 .iter()
368 .filter(|c| {
369 c.functions
370 .iter()
371 .any(|f| matches!(f.ty, FunctionTy::Fallback))
372 })
373 .map(|c| c.name.clone())
374 .collect();
375
376 // Task #115 — collect interface kind names and their external method
377 // sets. An expression like `I(t).getR()` in contract `C` (where `I`
378 // is an interface declared in the same source unit) is a
379 // cross-contract call routed through an `address`-typed receiver.
380 // At runtime the 20-byte zero placeholder triggers self-offsets
381 // dispatch (see `handle_contract_call` / Task #83 branch), so the
382 // callee method must live in the caller's merged function table.
383 // We match the interface to any sibling primary whose public/external
384 // method set is a superset of the interface's method names, and
385 // include those siblings in the sibling merge below.
386 //
387 // This mirrors the `new B()` / `B(addr)` / `B public b;` patterns
388 // already handled — without this hook, interface-typed dispatch
389 // silently returns `Null` (the `invoke_native_contract` fallback for
390 // unknown-hash calls), which then blows up inside `r.a` /
391 // `r.b` member accesses downstream.
392 let interface_methods: std::collections::HashMap<
393 String,
394 std::collections::HashSet<String>,
395 > = pre_merge_contract_map
396 .values()
397 .filter(|c| matches!(c.kind, ContractKind::Interface))
398 .map(|c| {
399 (
400 c.name.clone(),
401 c.functions
402 .iter()
403 .filter(|f| {
404 matches!(f.ty, FunctionTy::Function)
405 && matches!(
406 f.visibility,
407 VisibilityKind::External | VisibilityKind::Public
408 )
409 })
410 .map(|f| f.name.clone())
411 .collect(),
412 )
413 })
414 .collect();
415
416 // Reverse map: interface name → list of primary contracts whose
417 // method set covers the interface's method set. We pre-compute this
418 // once so we don't re-walk the primary function tables per function
419 // body.
420 let primary_method_names: std::collections::HashMap<
421 String,
422 std::collections::HashSet<String>,
423 > = primary
424 .iter()
425 .map(|c| {
426 (
427 c.name.clone(),
428 c.functions
429 .iter()
430 .filter(|f| {
431 matches!(f.ty, FunctionTy::Function)
432 && matches!(
433 f.visibility,
434 VisibilityKind::External | VisibilityKind::Public
435 )
436 })
437 .map(|f| f.name.clone())
438 .collect(),
439 )
440 })
441 .collect();
442
443 let interface_impls: std::collections::HashMap<String, Vec<String>> =
444 interface_methods
445 .iter()
446 .map(|(iface_name, iface_method_set)| {
447 let mut impls: Vec<String> = primary_method_names
448 .iter()
449 .filter_map(|(prim_name, prim_set)| {
450 // Task #126 — a primary with a `fallback()` catches
451 // any interface method that isn't explicitly declared,
452 // so it's always a valid implementor for sibling-
453 // merge purposes (at runtime the call routes through
454 // the merged `fallback` entry, which may itself
455 // revert — that revert is what we propagate to the
456 // caller's try/catch).
457 if iface_method_set.is_subset(prim_set)
458 || primary_has_fallback.contains(prim_name)
459 {
460 Some(prim_name.clone())
461 } else {
462 None
463 }
464 })
465 .collect();
466 // Deterministic order → reproducible bytecode offsets.
467 impls.sort();
468 (iface_name.clone(), impls)
469 })
470 .collect();
471
472 let interface_names: std::collections::HashSet<String> =
473 interface_methods.keys().cloned().collect();
474
475 for contract in primary.iter_mut() {
476 let mut referenced: std::collections::HashSet<String> =
477 std::collections::HashSet::new();
478 let mut iface_refs: std::collections::HashSet<String> =
479 std::collections::HashSet::new();
480 // Task #194 — collect method names statically resolvable from
481 // low-level-call payloads like
482 // `addr.call(abi.encodeWithSelector(bytes4(keccak256("m(T)"))))`,
483 // `addr.call(abi.encodeWithSignature("m(T)", …))`, or
484 // `addr.call(abi.encodeCall(Iface.m, …))`. Previously the
485 // sibling-merge pass only detected references through `new X()`,
486 // `X(addr)` casts, interface casts, and typed params/returns/
487 // state-vars. A low-level `.call()` with a constant selector is
488 // semantically identical to a typed `X(addr).m(…)` — the
489 // compiler routes it through the zero-placeholder
490 // `self_method_offsets` dispatch (see Task #83) — but without
491 // this scan the target method never lands in the merged table
492 // and the call silently returns `Null`.
493 let mut low_level_method_refs: std::collections::HashSet<String> =
494 std::collections::HashSet::new();
495 for function in &contract.functions {
496 if let Some(body) = function.body.as_ref() {
497 collect_new_contract_refs(body, &primary_names, &mut referenced);
498 // Task #115 — interface casts `I(expr)` in statements.
499 collect_interface_casts_stmt(body, &interface_names, &mut iface_refs);
500 // Task #194 — low-level calls whose payload encodes a
501 // statically resolvable method name.
502 collect_low_level_call_method_refs_stmt(
503 body,
504 &mut low_level_method_refs,
505 );
506 }
507 // Task K4 — function params/returns typed as a sibling contract
508 // (e.g. `function bounce() external returns (B) {...}`, or
509 // `function xfer(C to, ...)`) mean the function is wired to
510 // call into the sibling. Merge so self-call routing can see
511 // the target method at runtime.
512 for p in function.parameters.iter().chain(function.returns.iter()) {
513 if primary_names.contains(&p.ty) {
514 referenced.insert(p.ty.clone());
515 }
516 // Task #115 — also scan for interface-typed parameters.
517 if interface_names.contains(&p.ty) {
518 iface_refs.insert(p.ty.clone());
519 }
520 }
521 }
522 // Task K4 — also scan state-variable types and initializers.
523 // `B public b;` means A is wired to call into B via the storage
524 // slot without ever going through `new B()`. Without this hook,
525 // K4 (cross-contract reentrancy) fails: `b.bounce()` routes
526 // through `System.Contract.Call([0;20], "bounce", …)` which then
527 // returns `Null` because B wasn't merged.
528 for state in &contract.state_variables {
529 if primary_names.contains(&state.ty) {
530 referenced.insert(state.ty.clone());
531 }
532 if interface_names.contains(&state.ty) {
533 iface_refs.insert(state.ty.clone());
534 }
535 if let Some(init) = state.initializer.as_ref() {
536 collect_new_refs_expr(init, &primary_names, &mut referenced);
537 collect_interface_casts_expr(init, &interface_names, &mut iface_refs);
538 collect_low_level_call_method_refs_expr(
539 init,
540 &mut low_level_method_refs,
541 );
542 }
543 }
544 // Task #115 — expand interface references to the primary contracts
545 // that implement them. Multiple primaries may satisfy the same
546 // interface; merge all of them so dispatch sees any signature.
547 for iface in &iface_refs {
548 if let Some(impls) = interface_impls.get(iface) {
549 for prim in impls {
550 if prim != &contract.name {
551 referenced.insert(prim.clone());
552 }
553 }
554 }
555 }
556 // Task #194 — expand low-level-call method references to every
557 // sibling primary that declares a method of that name. When
558 // multiple siblings satisfy the same name (e.g. both X and Y
559 // declare `foo()`), merge all of them so the dispatcher sees the
560 // union; which one actually fires at runtime is decided by the
561 // caller's address (handled in `handle_contract_call`). We skip
562 // the host contract itself — its methods are already visible
563 // through normal dispatch.
564 if !low_level_method_refs.is_empty() {
565 for (prim_name, prim_methods) in &primary_method_names {
566 if prim_name == &contract.name {
567 continue;
568 }
569 if low_level_method_refs
570 .iter()
571 .any(|m| prim_methods.contains(m))
572 {
573 referenced.insert(prim_name.clone());
574 }
575 }
576 }
577 // Task #206 — close over TRANSITIVE sibling references. The
578 // zero-hash self-dispatch table is built from the caller
579 // artifact's manifest only, so if `Client` references `Middle`
580 // and `Middle` references `Target`, the merged `Client`
581 // artifact must carry both `wrap` and `fail`. Without this
582 // closure, the grandchild call silently falls through
583 // `handle_contract_call`'s zero-hash branch and returns `Null`.
584 let mut transitive_queue: Vec<String> = referenced.iter().cloned().collect();
585 while let Some(sibling_name) = transitive_queue.pop() {
586 let Some(sibling_contract) = primary_contract_map.get(&sibling_name) else {
587 continue;
588 };
589 let transitive_refs = collect_direct_sibling_contract_refs(
590 sibling_contract,
591 &primary_names,
592 &interface_names,
593 &interface_impls,
594 &primary_method_names,
595 );
596 for transitive in transitive_refs {
597 if transitive == contract.name {
598 continue;
599 }
600 if referenced.insert(transitive.clone()) {
601 transitive_queue.push(transitive);
602 }
603 }
604 }
605 referenced.remove(&contract.name);
606 if referenced.is_empty() {
607 continue;
608 }
609 let mut existing_sigs: std::collections::HashSet<(String, usize)> = contract
610 .functions
611 .iter()
612 .map(|f| (f.name.clone(), f.parameters.len()))
613 .collect();
614 // Deterministic order → reproducible bytecode offsets.
615 let mut sibling_names: Vec<String> = referenced.into_iter().collect();
616 sibling_names.sort();
617 for sibling_name in &sibling_names {
618 let Some(sibling_fns) = sibling_fn_map.get(sibling_name) else {
619 continue;
620 };
621 for sibling_fn in sibling_fns {
622 let sig = (sibling_fn.name.clone(), sibling_fn.parameters.len());
623 if existing_sigs.insert(sig) {
624 contract.functions.push(sibling_fn.clone());
625 }
626 }
627 // Pull in the sibling's modifier definitions so the merged
628 // external bodies can still resolve their `onlyOwner` /
629 // `onlyRole` / etc. references when the host's modifier-
630 // expansion pass runs. Modifiers don't conflict on plain
631 // function-signature equality (different `ty`), so we track
632 // them in a small local set keyed on (name, arity).
633 if let Some(sibling_modifiers) = sibling_modifier_map.get(sibling_name) {
634 for sibling_mod in sibling_modifiers {
635 let already_present = contract.functions.iter().any(|existing| {
636 matches!(existing.ty, FunctionTy::Modifier)
637 && existing.name == sibling_mod.name
638 && existing.parameters.len() == sibling_mod.parameters.len()
639 });
640 if !already_present {
641 contract.functions.push(sibling_mod.clone());
642 }
643 }
644 }
645 // Also pull in the sibling's `using` directives. Merged
646 // external bodies may rely on contract-scope `using L for T;`
647 // declarations declared in the sibling — e.g. Gnosis Safe
648 // declares `using SafeMath for uint256;` and its
649 // `execTransaction` body calls `gas.max(other)`. When
650 // CompatibilityFallbackHandler triggers a sibling-merge of
651 // Safe's external methods, the `execTransaction` body needs
652 // its `using SafeMath` directive to remain in scope or the
653 // IR-lowering pass reports "member-style call '...' requires
654 // an explicit `using` directive". We dedup on
655 // (target_type, function_names) so reinjected duplicates
656 // don't grow the table.
657 if let Some(sibling_contract) = pre_merge_contract_map.get(sibling_name) {
658 for directive in &sibling_contract.using_directives {
659 if !contract.using_directives.iter().any(|existing| {
660 existing.target_type == directive.target_type
661 && existing.function_names == directive.function_names
662 }) {
663 contract.using_directives.push(directive.clone());
664 }
665 }
666 for lib_name in &sibling_contract.using_for_libraries {
667 if !contract.using_for_libraries.contains(lib_name) {
668 contract.using_for_libraries.push(lib_name.clone());
669 }
670 }
671 contract.has_using_for_star =
672 contract.has_using_for_star || sibling_contract.has_using_for_star;
673 contract.has_using_function_list = contract.has_using_function_list
674 || sibling_contract.has_using_function_list;
675 }
676 }
677
678 // Task #197 — merge sibling state variables after their external
679 // functions. Without this, a merged stateful method like
680 // `Mock.balanceOf` → `return _bal[a]` would resolve `_bal`
681 // against the caller's (Client's) `state_index_map`, find no
682 // match, and fall through `variable.rs::lower_variable_expression`'s
683 // final compatibility arm which pushes `Integer(0)` as a neutral
684 // placeholder. Downstream SIZE/PICKITEM opcodes then fault on
685 // the scalar, surfacing as "SIZE: unsupported type" at runtime.
686 //
687 // Storage-key derivation is name-based (see
688 // `value_types.rs::compute_state_slot`), so merging Mock's
689 // `_bal` into Client produces the same keccak-derived slot
690 // that Mock.mint writes to — the cross-contract read lands on
691 // the same storage entry. Host-wins-on-collision preserves any
692 // state variable the caller already declares.
693 let mut existing_state_names: std::collections::HashSet<String> = contract
694 .state_variables
695 .iter()
696 .filter_map(|s| s.name.clone())
697 .collect();
698 for sibling_name in &sibling_names {
699 let Some(sibling_states) = sibling_state_map.get(sibling_name) else {
700 continue;
701 };
702 for sibling_state in sibling_states {
703 if let Some(name) = sibling_state.name.as_ref() {
704 if existing_state_names.insert(name.clone()) {
705 contract.state_variables.push(sibling_state.clone());
706 } else if let Some(existing) = contract
707 .state_variables
708 .iter()
709 .find(|s| s.name.as_deref() == Some(name.as_str()))
710 {
711 // Storage-soundness guard — slots are derived
712 // from the BARE variable name (`sha256(name)`,
713 // see `storage_key::compute_state_slot`), so a
714 // host/sibling pair declaring the same name with
715 // DIFFERENT types would silently collapse two
716 // semantically distinct lvalues onto one slot
717 // (e.g. a sibling's `mapping(address=>uint256)
718 // _bal` aliasing the host's scalar `uint256
719 // _bal`), miscompiling both. Same-name SAME-type
720 // sharing stays allowed: that is the documented
721 // Task #197 design (merged sibling bodies must
722 // hit the same name-keyed slot).
723 if normalize_state_type_for_merge(&existing.ty)
724 != normalize_state_type_for_merge(&sibling_state.ty)
725 {
726 return Err(SolidityError::analysis(format!(
727 "state variable '{name}' is declared with conflicting types \
728 across merged contracts: '{}' in '{}' vs '{}' in '{sibling_name}'. \
729 Storage slots are derived from the bare variable name, so both \
730 declarations would silently alias the same storage entry; \
731 rename one of the variables.",
732 existing.ty, contract.name, sibling_state.ty
733 )));
734 }
735 }
736 }
737 }
738 }
739
740 // Task #198 — merge sibling constructors as name-mangled internal
741 // regular functions so the caller's `new Child(args)` lowering can
742 // invoke the ctor body in-line. Without this, `new Child(x, y)`
743 // silently drops its args and the follow-up `c.a()` / `c.b()`
744 // reads land on uninitialized storage (all zeros). Re-typing from
745 // `Constructor` to `Function` prevents the caller's own `_deploy`
746 // prologue from accidentally calling the merged entry at deploy
747 // time (constructor_indices is populated by FunctionKind, so
748 // Regular entries are skipped there).
749 for sibling_name in &sibling_names {
750 let Some(sibling_ctor) = sibling_ctor_map.get(sibling_name) else {
751 continue;
752 };
753 let mangled_name = format!("__ctor__{sibling_name}");
754 let sig = (mangled_name.clone(), sibling_ctor.parameters.len());
755 if !existing_sigs.insert(sig) {
756 continue;
757 }
758 let mut cloned = sibling_ctor.clone();
759 cloned.name = mangled_name;
760 cloned.ty = FunctionTy::Function;
761 cloned.visibility = VisibilityKind::Internal;
762 // Base-constructor invocations (`base_or_modifiers`) were
763 // already resolved by `apply_modifiers_and_base_constructors`
764 // in the owning contract's pipeline; clear the residue so the
765 // caller's modifier-application pass (which ran before this
766 // merge) doesn't re-expand anything.
767 cloned.base_or_modifiers.clear();
768 contract.functions.push(cloned);
769 }
770 }
771 }
772
773 // Make non-inherited enum/struct namespaces visible across compilation
774 // units so expressions like `Enum.Operation.DelegateCall` can resolve even
775 // when the defining type lives in another top-level contract/library file.
776 if has_primary {
777 let shared_type_defs: Vec<(String, Vec<StructIR>, Vec<EnumIR>)> = pre_merge_contract_map
778 .values()
779 .filter(|contract| {
780 !matches!(contract.kind, ContractKind::Library)
781 || !is_builtin_library_name(contract.name.as_str())
782 })
783 .map(|contract| {
784 (
785 contract.name.clone(),
786 contract.structs.clone(),
787 contract.enums.clone(),
788 )
789 })
790 .collect();
791
792 for contract in primary.iter_mut() {
793 let mut seen_structs: std::collections::HashSet<String> = contract
794 .structs
795 .iter()
796 .map(|item| item.name.to_ascii_lowercase())
797 .collect();
798 let mut seen_enums: std::collections::HashSet<String> = contract
799 .enums
800 .iter()
801 .map(|item| item.name.to_ascii_lowercase())
802 .collect();
803
804 for (owner_name, structs, enums) in &shared_type_defs {
805 if owner_name == &contract.name {
806 continue;
807 }
808 for item in structs {
809 let key = item.name.to_ascii_lowercase();
810 if seen_structs.insert(key) {
811 contract.structs.push(item.clone());
812 }
813 }
814 for item in enums {
815 let key = item.name.to_ascii_lowercase();
816 if seen_enums.insert(key) {
817 contract.enums.push(item.clone());
818 }
819 }
820 }
821 }
822 }
823
824 // Build a lookup map for inheritance flattening and modifier expansion.
825 let contract_map: std::collections::HashMap<String, ContractIR> = primary
826 .iter()
827 .chain(fallback.iter())
828 .map(|contract| (contract.name.clone(), contract.clone()))
829 .collect();
830
831 // Task #106 — gather struct fields across all contracts so canonical
832 // signatures can expand struct params into their `(field1,field2,...)` tuple
833 // form per the EVM ABI spec. Without this, the selector for
834 // `f(P memory p)` where `struct P { uint256 a; bool b; }` is computed from
835 // `f(P)` — which does not match the Solidity-spec selector for
836 // `f((uint256,bool))`.
837 let mut struct_fields_map: std::collections::HashMap<
838 String,
839 Vec<(String, String)>,
840 > = std::collections::HashMap::new();
841 for contract in contract_map.values() {
842 for struct_def in &contract.structs {
843 let entries: Vec<(String, String)> = struct_def
844 .fields
845 .iter()
846 .map(|f| (f.name.clone(), f.ty.clone()))
847 .collect();
848 struct_fields_map
849 .entry(struct_def.name.clone())
850 .or_insert(entries);
851 }
852 }
853
854 // Build a shared selector registry so `.selector` expressions can resolve against
855 // any contract/interface visible to this compilation unit (including those defined
856 // after the primary contract in the same file).
857 // Every visible type name (contract/interface/library) — contract-typed
858 // params resolve to `address` for ABI canonicalization.
859 let registry_contract_types: Vec<String> =
860 contract_map.values().map(|c| c.name.clone()).collect();
861 let mut type_method_selectors: std::collections::HashMap<
862 String,
863 std::collections::HashMap<String, Vec<[u8; 4]>>,
864 > = std::collections::HashMap::new();
865 let mut interface_types: std::collections::HashSet<String> = std::collections::HashSet::new();
866 for contract in contract_map.values() {
867 if matches!(contract.kind, ContractKind::Interface) {
868 interface_types.insert(contract.name.clone());
869 }
870
871 // When building selector lookups for `.selector` / `.interfaceId`, include inherited
872 // interface methods as part of the derived interface. This matches Solidity behavior
873 // and supports patterns like `type(IChild).interfaceId` where `IChild is IParent`.
874 let selector_contract = match contract.kind {
875 ContractKind::Contract | ContractKind::AbstractContract | ContractKind::Interface => {
876 flatten_contract_inheritance(contract.clone(), &contract_map)
877 .map(|(ir, _warnings)| ir)
878 .unwrap_or_else(|_| contract.clone())
879 }
880 ContractKind::Library => contract.clone(),
881 };
882
883 let mut per_type: std::collections::HashMap<String, Vec<[u8; 4]>> =
884 std::collections::HashMap::new();
885
886 // Resolve each `.selector` parameter through the SAME canonicalization as
887 // the manifest selector (`FunctionMetadata.selector`, built via
888 // `NeoType::canonical_abi_type` in convert/functions.rs): structs expand to
889 // tuples, enums render as `uint8`, integer widths are explicit. The two
890 // paths must produce identical selectors — both drive on-chain dispatch and
891 // a contract's `this.f.selector` must match what external callers compute.
892 let sel_struct_types: Vec<StructTypeMetadata> = selector_contract
893 .structs
894 .iter()
895 .map(|s| StructTypeMetadata {
896 name: s.name.clone(),
897 fields: s
898 .fields
899 .iter()
900 .map(|f| NeoStructFieldMetadata {
901 name: f.name.clone(),
902 ty: f.ty.clone(),
903 })
904 .collect(),
905 })
906 .collect();
907 let sel_enum_types: Vec<EnumTypeMetadata> = selector_contract
908 .enums
909 .iter()
910 .map(|e| EnumTypeMetadata {
911 name: e.name.clone(),
912 variants: e.values.len(),
913 })
914 .collect();
915
916 for function in &selector_contract.functions {
917 if !matches!(function.ty, FunctionTy::Function) {
918 continue;
919 }
920
921 if !matches!(
922 function.visibility,
923 VisibilityKind::External | VisibilityKind::Public
924 ) {
925 continue;
926 }
927
928 let param_signatures: Vec<String> = function
929 .parameters
930 .iter()
931 .map(|param| {
932 match NeoType::from_solidity(
933 ¶m.ty,
934 &sel_struct_types,
935 &sel_enum_types,
936 ®istry_contract_types,
937 ) {
938 Ok(neo_type) => neo_type.canonical_abi_type(),
939 // Fall back to the struct-aware string canonicalizer only
940 // when the type cannot be resolved (keeps prior behavior).
941 Err(_) => crate::utils::canonical_param_type_with_structs(
942 ¶m.ty,
943 &struct_fields_map,
944 ),
945 }
946 })
947 .collect();
948 let selector = compute_function_selector(&function.name, ¶m_signatures);
949 per_type
950 .entry(function.name.clone())
951 .or_default()
952 .push(selector);
953 }
954
955 type_method_selectors.insert(contract.name.clone(), per_type);
956 }
957 let selector_registry = std::sync::Arc::new(SelectorRegistry {
958 type_method_selectors,
959 interface_types,
960 });
961
962 let mut selected = if has_primary { primary } else { fallback };
963
964 if selected.is_empty() {
965 return Ok(Vec::new());
966 }
967
968 let mut metadatas = Vec::new();
969 for contract in selected.drain(..) {
970 let (mut flattened, flatten_warnings) =
971 flatten_contract_inheritance(contract, &contract_map)?;
972 // Merge user-defined libraries AFTER inheritance flattening so the
973 // flattener doesn't mistake cloned library helpers for inheritance
974 // overrides. The final flattened contract still needs the library
975 // helpers/types present before `convert_contract` so direct library
976 // calls and `using for` member-style calls lower correctly.
977 if has_primary && !libraries.is_empty() {
978 for lib in &libraries {
979 flattened.functions.extend(lib.functions.clone());
980 flattened.state_variables.extend(lib.state_variables.clone());
981 flattened.structs.extend(lib.structs.clone());
982 flattened.enums.extend(lib.enums.clone());
983 // Merge the library's own `using` directives into the host.
984 // Library function bodies are inlined verbatim above, so any
985 // member-style call resolved by a library-scope `using` (e.g.
986 // OZ Strings.sol declares `using SafeCast for *;` then calls
987 // `someBool.toUint()` inside its own helpers) must continue to
988 // resolve after the body lives inside the host contract.
989 // Without this, the IR-lowering pass at
990 // `src/ir/expressions/calls/member_calls.rs:432` reports
991 // "member-style call '...' requires an explicit `using`
992 // directive" for the inlined library code.
993 for directive in &lib.using_directives {
994 if !flattened.using_directives.iter().any(|existing| {
995 existing.target_type == directive.target_type
996 && existing.function_names == directive.function_names
997 }) {
998 flattened.using_directives.push(directive.clone());
999 }
1000 }
1001 for lib_name in &lib.using_for_libraries {
1002 if !flattened.using_for_libraries.contains(lib_name) {
1003 flattened.using_for_libraries.push(lib_name.clone());
1004 }
1005 }
1006 flattened.has_using_for_star =
1007 flattened.has_using_for_star || lib.has_using_for_star;
1008 flattened.has_using_function_list =
1009 flattened.has_using_function_list || lib.has_using_function_list;
1010 }
1011 }
1012 apply_modifiers_and_base_constructors(&mut flattened, &contract_map)?;
1013 let mut metadata = convert_contract(
1014 flattened,
1015 &[],
1016 &contract_types,
1017 selector_registry.clone(),
1018 );
1019 metadata.flatten_warnings = flatten_warnings;
1020 metadatas.push(metadata);
1021 }
1022
1023 Ok(metadatas)
1024}
1025
1026/// Task #83 — walk a statement tree collecting every `new X()` target name
1027/// that matches a known primary contract. Mirrors the ast_scan permissions
1028/// pass but accumulates matches instead of returning a boolean.
1029fn collect_new_contract_refs(
1030 stmt: &Statement,
1031 primary_names: &std::collections::HashSet<String>,
1032 sink: &mut std::collections::HashSet<String>,
1033) {
1034 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1035 collect_new_contract_refs_inner(stmt, primary_names, sink)
1036 })
1037}
1038
1039fn collect_new_contract_refs_inner(
1040 stmt: &Statement,
1041 primary_names: &std::collections::HashSet<String>,
1042 sink: &mut std::collections::HashSet<String>,
1043) {
1044 match stmt {
1045 Statement::Block { statements, .. } => {
1046 for s in statements {
1047 collect_new_contract_refs(s, primary_names, sink);
1048 }
1049 }
1050 Statement::If(_, cond, t, e) => {
1051 collect_new_refs_expr(cond, primary_names, sink);
1052 collect_new_contract_refs(t, primary_names, sink);
1053 if let Some(s) = e {
1054 collect_new_contract_refs(s, primary_names, sink);
1055 }
1056 }
1057 Statement::While(_, cond, body) | Statement::DoWhile(_, body, cond) => {
1058 collect_new_refs_expr(cond, primary_names, sink);
1059 collect_new_contract_refs(body, primary_names, sink);
1060 }
1061 Statement::Expression(_, expr) => collect_new_refs_expr(expr, primary_names, sink),
1062 Statement::VariableDefinition(_, _, Some(expr)) => {
1063 collect_new_refs_expr(expr, primary_names, sink);
1064 }
1065 Statement::VariableDefinition(_, _, None) => {}
1066 Statement::For(_, i, c, n, b) => {
1067 if let Some(s) = i {
1068 collect_new_contract_refs(s, primary_names, sink);
1069 }
1070 if let Some(e) = c {
1071 collect_new_refs_expr(e, primary_names, sink);
1072 }
1073 if let Some(e) = n {
1074 collect_new_refs_expr(e, primary_names, sink);
1075 }
1076 if let Some(s) = b {
1077 collect_new_contract_refs(s, primary_names, sink);
1078 }
1079 }
1080 Statement::Return(_, Some(expr)) | Statement::Emit(_, expr) => {
1081 collect_new_refs_expr(expr, primary_names, sink);
1082 }
1083 Statement::Revert(_, _, args) => {
1084 for e in args {
1085 collect_new_refs_expr(e, primary_names, sink);
1086 }
1087 }
1088 Statement::Try(_, expr, returns, clauses) => {
1089 collect_new_refs_expr(expr, primary_names, sink);
1090 if let Some((_, b)) = returns {
1091 collect_new_contract_refs(b, primary_names, sink);
1092 }
1093 for c in clauses {
1094 match c {
1095 CatchClause::Simple(_, _, b) | CatchClause::Named(_, _, _, b) => {
1096 collect_new_contract_refs(b, primary_names, sink);
1097 }
1098 }
1099 }
1100 }
1101 _ => {}
1102 }
1103}
1104
1105/// Task #115 — statement-level walk that collects every `I(expr).method(...)`
1106/// interface-cast receiver where `I` is a known interface declared in the
1107/// same source unit. Mirrors `collect_new_contract_refs` but tracks a
1108/// different alphabet of names (interface kinds, not primary contracts).
1109fn collect_interface_casts_stmt(
1110 stmt: &Statement,
1111 interface_names: &std::collections::HashSet<String>,
1112 sink: &mut std::collections::HashSet<String>,
1113) {
1114 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1115 collect_interface_casts_stmt_inner(stmt, interface_names, sink)
1116 })
1117}
1118
1119fn collect_interface_casts_stmt_inner(
1120 stmt: &Statement,
1121 interface_names: &std::collections::HashSet<String>,
1122 sink: &mut std::collections::HashSet<String>,
1123) {
1124 match stmt {
1125 Statement::Block { statements, .. } => {
1126 for s in statements {
1127 collect_interface_casts_stmt(s, interface_names, sink);
1128 }
1129 }
1130 Statement::If(_, cond, t, e) => {
1131 collect_interface_casts_expr(cond, interface_names, sink);
1132 collect_interface_casts_stmt(t, interface_names, sink);
1133 if let Some(s) = e {
1134 collect_interface_casts_stmt(s, interface_names, sink);
1135 }
1136 }
1137 Statement::While(_, cond, body) | Statement::DoWhile(_, body, cond) => {
1138 collect_interface_casts_expr(cond, interface_names, sink);
1139 collect_interface_casts_stmt(body, interface_names, sink);
1140 }
1141 Statement::Expression(_, expr) => {
1142 collect_interface_casts_expr(expr, interface_names, sink)
1143 }
1144 Statement::VariableDefinition(_, _, Some(expr)) => {
1145 collect_interface_casts_expr(expr, interface_names, sink);
1146 }
1147 Statement::VariableDefinition(_, _, None) => {}
1148 Statement::For(_, i, c, n, b) => {
1149 if let Some(s) = i {
1150 collect_interface_casts_stmt(s, interface_names, sink);
1151 }
1152 if let Some(e) = c {
1153 collect_interface_casts_expr(e, interface_names, sink);
1154 }
1155 if let Some(e) = n {
1156 collect_interface_casts_expr(e, interface_names, sink);
1157 }
1158 if let Some(s) = b {
1159 collect_interface_casts_stmt(s, interface_names, sink);
1160 }
1161 }
1162 Statement::Return(_, Some(expr)) | Statement::Emit(_, expr) => {
1163 collect_interface_casts_expr(expr, interface_names, sink);
1164 }
1165 Statement::Revert(_, _, args) => {
1166 for e in args {
1167 collect_interface_casts_expr(e, interface_names, sink);
1168 }
1169 }
1170 Statement::Try(_, expr, returns, clauses) => {
1171 collect_interface_casts_expr(expr, interface_names, sink);
1172 if let Some((_, b)) = returns {
1173 collect_interface_casts_stmt(b, interface_names, sink);
1174 }
1175 for c in clauses {
1176 match c {
1177 CatchClause::Simple(_, _, b) | CatchClause::Named(_, _, _, b) => {
1178 collect_interface_casts_stmt(b, interface_names, sink);
1179 }
1180 }
1181 }
1182 }
1183 _ => {}
1184 }
1185}
1186
1187/// Task #115 — expression-level half of `collect_interface_casts_stmt`.
1188/// Matches `FunctionCall(Variable(I), _)` where `I` is a known interface
1189/// name. The parser emits this shape for interface casts like `I(addr)`.
1190fn collect_interface_casts_expr(
1191 expr: &Expression,
1192 interface_names: &std::collections::HashSet<String>,
1193 sink: &mut std::collections::HashSet<String>,
1194) {
1195 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1196 collect_interface_casts_expr_inner(expr, interface_names, sink)
1197 })
1198}
1199
1200fn collect_interface_casts_expr_inner(
1201 expr: &Expression,
1202 interface_names: &std::collections::HashSet<String>,
1203 sink: &mut std::collections::HashSet<String>,
1204) {
1205 if let Expression::FunctionCall(_, func, _) = expr {
1206 if let Expression::Variable(id) = func.as_ref() {
1207 if interface_names.contains(&id.name) {
1208 sink.insert(id.name.clone());
1209 }
1210 }
1211 }
1212 match expr {
1213 Expression::New(_, i)
1214 | Expression::Parenthesis(_, i)
1215 | Expression::MemberAccess(_, i, _)
1216 | Expression::Delete(_, i) => collect_interface_casts_expr(i, interface_names, sink),
1217 Expression::FunctionCall(_, func, args) => {
1218 collect_interface_casts_expr(func, interface_names, sink);
1219 for a in args {
1220 collect_interface_casts_expr(a, interface_names, sink);
1221 }
1222 }
1223 // Task #125 — symmetric fix with `collect_new_refs_expr`: the
1224 // `try-expr { success-block }` lowering parks the call inside a
1225 // FunctionCallBlock, so interface-cast chains such as
1226 // `try I(t).getR() returns (R r) { ... } catch ...` would also
1227 // silently skip the sibling-merge trigger without this arm.
1228 Expression::FunctionCallBlock(_, call, block) => {
1229 collect_interface_casts_expr(call, interface_names, sink);
1230 collect_interface_casts_stmt(block, interface_names, sink);
1231 }
1232 Expression::NamedFunctionCall(_, func, args) => {
1233 collect_interface_casts_expr(func, interface_names, sink);
1234 for a in args {
1235 collect_interface_casts_expr(&a.expr, interface_names, sink);
1236 }
1237 }
1238 Expression::ArraySubscript(_, a, b) => {
1239 collect_interface_casts_expr(a, interface_names, sink);
1240 if let Some(e) = b {
1241 collect_interface_casts_expr(e, interface_names, sink);
1242 }
1243 }
1244 Expression::ConditionalOperator(_, c, a, b) => {
1245 collect_interface_casts_expr(c, interface_names, sink);
1246 collect_interface_casts_expr(a, interface_names, sink);
1247 collect_interface_casts_expr(b, interface_names, sink);
1248 }
1249 Expression::Assign(_, a, b) => {
1250 collect_interface_casts_expr(a, interface_names, sink);
1251 collect_interface_casts_expr(b, interface_names, sink);
1252 }
1253 Expression::ArrayLiteral(_, values) => {
1254 for v in values {
1255 collect_interface_casts_expr(v, interface_names, sink);
1256 }
1257 }
1258 _ => {}
1259 }
1260}
1261
1262/// Task #83 — expression-level half of `collect_new_contract_refs`. Matches
1263/// `Expression::New(FunctionCall(Variable(name), _))` and recurses through
1264/// the usual expression containers.
1265fn collect_new_refs_expr(
1266 expr: &Expression,
1267 primary_names: &std::collections::HashSet<String>,
1268 sink: &mut std::collections::HashSet<String>,
1269) {
1270 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1271 collect_new_refs_expr_inner(expr, primary_names, sink)
1272 })
1273}
1274
1275fn collect_new_refs_expr_inner(
1276 expr: &Expression,
1277 primary_names: &std::collections::HashSet<String>,
1278 sink: &mut std::collections::HashSet<String>,
1279) {
1280 if let Expression::New(_, inner) = expr {
1281 if let Expression::FunctionCall(_, func, _) = inner.as_ref() {
1282 if let Expression::Variable(id) = func.as_ref() {
1283 if primary_names.contains(&id.name) {
1284 sink.insert(id.name.clone());
1285 }
1286 }
1287 }
1288 }
1289 // Task K4 — `B(addr)` cast expressions mean A plans to call into B
1290 // through an address typed as B. The parser lowers these as
1291 // `FunctionCall(Variable("B"), [addr])`, identical in shape to a
1292 // `B.staticCall(addr)` helper, so we match on that before the generic
1293 // FunctionCall recursion below picks off the args.
1294 if let Expression::FunctionCall(_, func, _) = expr {
1295 if let Expression::Variable(id) = func.as_ref() {
1296 if primary_names.contains(&id.name) {
1297 sink.insert(id.name.clone());
1298 }
1299 }
1300 }
1301 match expr {
1302 Expression::New(_, i)
1303 | Expression::Parenthesis(_, i)
1304 | Expression::MemberAccess(_, i, _)
1305 | Expression::Delete(_, i) => collect_new_refs_expr(i, primary_names, sink),
1306 Expression::FunctionCall(_, func, args) => {
1307 collect_new_refs_expr(func, primary_names, sink);
1308 for a in args {
1309 collect_new_refs_expr(a, primary_names, sink);
1310 }
1311 }
1312 // Task #125 — `try X { ... } catch ...` parses the leading
1313 // `try-expr { success-block }` as `FunctionCallBlock(call, block)`
1314 // on the expression-tree side, so `try Target(t).willRevert() { ... }`
1315 // arrives here with `call = FunctionCall(MemberAccess(FunctionCall(
1316 // Variable("Target"), [t]), "willRevert"), [])` wrapped in a
1317 // FunctionCallBlock. Without this arm the walker's `_ => {}`
1318 // silently dropped the cast chain, so `Target` never made the
1319 // sibling-merge `referenced` set and `willRevert` never entered
1320 // C's `self_method_offsets` table — the runtime's
1321 // `handle_contract_call` then fell through to `invoke_native_contract`
1322 // which returned `Null` for the zero-placeholder hash, so the
1323 // target's `revert("bad")` was never dispatched and the outer
1324 // try-arm fired with its literal "ok" instead of the expected
1325 // `catch Error(string)` binding. The success block body is a
1326 // Statement, not an Expression, so we use the statement walker
1327 // for it — symmetric with the `Statement::Try` arm above.
1328 Expression::FunctionCallBlock(_, call, block) => {
1329 collect_new_refs_expr(call, primary_names, sink);
1330 collect_new_contract_refs(block, primary_names, sink);
1331 }
1332 Expression::NamedFunctionCall(_, func, args) => {
1333 collect_new_refs_expr(func, primary_names, sink);
1334 for a in args {
1335 collect_new_refs_expr(&a.expr, primary_names, sink);
1336 }
1337 }
1338 Expression::ArraySubscript(_, a, b) => {
1339 collect_new_refs_expr(a, primary_names, sink);
1340 if let Some(e) = b {
1341 collect_new_refs_expr(e, primary_names, sink);
1342 }
1343 }
1344 Expression::ConditionalOperator(_, c, a, b) => {
1345 collect_new_refs_expr(c, primary_names, sink);
1346 collect_new_refs_expr(a, primary_names, sink);
1347 collect_new_refs_expr(b, primary_names, sink);
1348 }
1349 Expression::Assign(_, a, b) => {
1350 collect_new_refs_expr(a, primary_names, sink);
1351 collect_new_refs_expr(b, primary_names, sink);
1352 }
1353 Expression::ArrayLiteral(_, values) => {
1354 for v in values {
1355 collect_new_refs_expr(v, primary_names, sink);
1356 }
1357 }
1358 _ => {}
1359 }
1360}
1361
1362/// Task #194 — statement walker that collects statically resolvable method
1363/// names from low-level `addr.call(...)` / `addr.staticcall(...)` payloads.
1364/// Mirrors the shape of `collect_new_contract_refs` but feeds a different
1365/// alphabet: plain method names (e.g. `"getValue"`) that the sibling-merge
1366/// pass later cross-references against every sibling primary's declared
1367/// method set.
1368fn collect_low_level_call_method_refs_stmt(
1369 stmt: &Statement,
1370 sink: &mut std::collections::HashSet<String>,
1371) {
1372 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1373 collect_low_level_call_method_refs_stmt_inner(stmt, sink)
1374 })
1375}
1376
1377fn collect_low_level_call_method_refs_stmt_inner(
1378 stmt: &Statement,
1379 sink: &mut std::collections::HashSet<String>,
1380) {
1381 match stmt {
1382 Statement::Block { statements, .. } => {
1383 for s in statements {
1384 collect_low_level_call_method_refs_stmt(s, sink);
1385 }
1386 }
1387 Statement::If(_, cond, t, e) => {
1388 collect_low_level_call_method_refs_expr(cond, sink);
1389 collect_low_level_call_method_refs_stmt(t, sink);
1390 if let Some(s) = e {
1391 collect_low_level_call_method_refs_stmt(s, sink);
1392 }
1393 }
1394 Statement::While(_, cond, body) | Statement::DoWhile(_, body, cond) => {
1395 collect_low_level_call_method_refs_expr(cond, sink);
1396 collect_low_level_call_method_refs_stmt(body, sink);
1397 }
1398 Statement::Expression(_, expr) => {
1399 collect_low_level_call_method_refs_expr(expr, sink);
1400 }
1401 Statement::VariableDefinition(_, _, Some(expr)) => {
1402 collect_low_level_call_method_refs_expr(expr, sink);
1403 }
1404 Statement::VariableDefinition(_, _, None) => {}
1405 Statement::For(_, i, c, n, b) => {
1406 if let Some(s) = i {
1407 collect_low_level_call_method_refs_stmt(s, sink);
1408 }
1409 if let Some(e) = c {
1410 collect_low_level_call_method_refs_expr(e, sink);
1411 }
1412 if let Some(e) = n {
1413 collect_low_level_call_method_refs_expr(e, sink);
1414 }
1415 if let Some(s) = b {
1416 collect_low_level_call_method_refs_stmt(s, sink);
1417 }
1418 }
1419 Statement::Return(_, Some(expr)) | Statement::Emit(_, expr) => {
1420 collect_low_level_call_method_refs_expr(expr, sink);
1421 }
1422 Statement::Revert(_, _, args) => {
1423 for e in args {
1424 collect_low_level_call_method_refs_expr(e, sink);
1425 }
1426 }
1427 Statement::Try(_, expr, returns, clauses) => {
1428 collect_low_level_call_method_refs_expr(expr, sink);
1429 if let Some((_, b)) = returns {
1430 collect_low_level_call_method_refs_stmt(b, sink);
1431 }
1432 for c in clauses {
1433 match c {
1434 CatchClause::Simple(_, _, b) | CatchClause::Named(_, _, _, b) => {
1435 collect_low_level_call_method_refs_stmt(b, sink);
1436 }
1437 }
1438 }
1439 }
1440 _ => {}
1441 }
1442}
1443
1444/// Task #194 — expression walker that recognises `<receiver>.call(payload)`
1445/// / `<receiver>.staticcall(payload)` / `<receiver>.delegatecall(payload)`
1446/// shapes, then peels the `abi.encodeWith{Selector,Signature}` /
1447/// `abi.encodeCall` wrapper on the payload to extract the Solidity method
1448/// name when it can be resolved at compile time. The extracted name is
1449/// later matched against every sibling primary's declared public/external
1450/// method set.
1451///
1452/// Static resolution handles:
1453/// - `abi.encodeWithSignature("m(T)", …)` — literal signature string,
1454/// name taken from the pre-`(` fragment.
1455/// - `abi.encodeWithSelector(bytes4(keccak256("m(T)")))` —
1456/// compile-time hash of a literal signature string.
1457/// - `abi.encodeWithSelector(Type.method.selector)` /
1458/// `abi.encodeCall(Type.method, (…))` — static member-access.
1459///
1460/// Runtime-computed selectors (e.g. `abi.encodeWithSelector(someRuntimeSel,
1461/// …)`) stay unresolved and yield nothing — the compiler's caller-side
1462/// lowering similarly cannot route those through sibling-merge, so they
1463/// fall through to the real cross-contract dispatch path.
1464fn collect_low_level_call_method_refs_expr(
1465 expr: &Expression,
1466 sink: &mut std::collections::HashSet<String>,
1467) {
1468 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1469 collect_low_level_call_method_refs_expr_inner(expr, sink)
1470 })
1471}
1472
1473fn collect_low_level_call_method_refs_expr_inner(
1474 expr: &Expression,
1475 sink: &mut std::collections::HashSet<String>,
1476) {
1477 if let Expression::FunctionCall(_, func, args) = expr {
1478 if args.len() == 1 {
1479 if let Expression::MemberAccess(_, _recv, member) = func.as_ref() {
1480 let is_low_level = matches!(
1481 member.name.as_str(),
1482 "call" | "staticcall" | "delegatecall"
1483 );
1484 if is_low_level {
1485 if let Some(name) = extract_static_method_name_from_payload(&args[0]) {
1486 if !name.trim().is_empty() {
1487 sink.insert(name);
1488 }
1489 }
1490 }
1491 }
1492 }
1493 }
1494 match expr {
1495 Expression::New(_, i)
1496 | Expression::Parenthesis(_, i)
1497 | Expression::MemberAccess(_, i, _)
1498 | Expression::Delete(_, i) => collect_low_level_call_method_refs_expr(i, sink),
1499 Expression::FunctionCall(_, func, args) => {
1500 collect_low_level_call_method_refs_expr(func, sink);
1501 for a in args {
1502 collect_low_level_call_method_refs_expr(a, sink);
1503 }
1504 }
1505 Expression::FunctionCallBlock(_, call, block) => {
1506 collect_low_level_call_method_refs_expr(call, sink);
1507 collect_low_level_call_method_refs_stmt(block, sink);
1508 }
1509 Expression::NamedFunctionCall(_, func, args) => {
1510 collect_low_level_call_method_refs_expr(func, sink);
1511 for a in args {
1512 collect_low_level_call_method_refs_expr(&a.expr, sink);
1513 }
1514 }
1515 Expression::ArraySubscript(_, a, b) => {
1516 collect_low_level_call_method_refs_expr(a, sink);
1517 if let Some(e) = b {
1518 collect_low_level_call_method_refs_expr(e, sink);
1519 }
1520 }
1521 Expression::ConditionalOperator(_, c, a, b) => {
1522 collect_low_level_call_method_refs_expr(c, sink);
1523 collect_low_level_call_method_refs_expr(a, sink);
1524 collect_low_level_call_method_refs_expr(b, sink);
1525 }
1526 Expression::Assign(_, a, b) => {
1527 collect_low_level_call_method_refs_expr(a, sink);
1528 collect_low_level_call_method_refs_expr(b, sink);
1529 }
1530 Expression::ArrayLiteral(_, values) => {
1531 for v in values {
1532 collect_low_level_call_method_refs_expr(v, sink);
1533 }
1534 }
1535 _ => {}
1536 }
1537}
1538
1539/// Task #194 — peel the `abi.encodeWith{Selector,Signature}` /
1540/// `abi.encodeCall` wrapper of a low-level call payload to extract the
1541/// Solidity method name when it is statically resolvable.
1542fn extract_static_method_name_from_payload(expr: &Expression) -> Option<String> {
1543 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1544 extract_static_method_name_from_payload_inner(expr)
1545 })
1546}
1547
1548fn extract_static_method_name_from_payload_inner(expr: &Expression) -> Option<String> {
1549 match expr {
1550 Expression::Parenthesis(_, inner) => extract_static_method_name_from_payload(inner),
1551 Expression::FunctionCall(_, func, args) => {
1552 // `bytes(<inner>)` / `bytes4(<inner>)` / `string(<inner>)` casts
1553 // are transparent — recurse through them.
1554 if args.len() == 1 {
1555 if let Expression::Variable(id) = func.as_ref() {
1556 if id.name == "bytes" || id.name == "string" {
1557 return extract_static_method_name_from_payload(&args[0]);
1558 }
1559 }
1560 if matches!(func.as_ref(), Expression::Type(_, _)) {
1561 return extract_static_method_name_from_payload(&args[0]);
1562 }
1563 }
1564
1565 let Expression::MemberAccess(_, inner, member) = func.as_ref() else {
1566 return None;
1567 };
1568
1569 if !matches!(inner.as_ref(), Expression::Variable(id) if id.name == "abi") {
1570 return None;
1571 }
1572
1573 match member.name.as_str() {
1574 "encodeWithSignature" => {
1575 let first = args.first()?;
1576 let signature = extract_static_signature_string(first)?;
1577 let name = signature
1578 .split('(')
1579 .next()
1580 .unwrap_or(signature.as_str())
1581 .trim()
1582 .to_string();
1583 if name.is_empty() {
1584 None
1585 } else {
1586 Some(name)
1587 }
1588 }
1589 "encodeWithSelector" => {
1590 let first = args.first()?;
1591 extract_static_selector_method_name(first)
1592 }
1593 "encodeCall" => {
1594 // `abi.encodeCall(X.method, (…))` — member-access
1595 // function reference resolves to the member name.
1596 let first = args.first()?;
1597 extract_static_encode_call_method_name(first)
1598 }
1599 _ => None,
1600 }
1601 }
1602 _ => None,
1603 }
1604}
1605
1606/// Task #194 — analogue of `resolve_selector_method_name` in
1607/// `ir/build/selectors.rs` that operates on raw `solang_parser::pt`
1608/// expressions (the analyse pass runs before the IR is built).
1609fn extract_static_selector_method_name(expr: &Expression) -> Option<String> {
1610 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1611 extract_static_selector_method_name_inner(expr)
1612 })
1613}
1614
1615fn extract_static_selector_method_name_inner(expr: &Expression) -> Option<String> {
1616 match expr {
1617 Expression::Parenthesis(_, inner) => extract_static_selector_method_name(inner),
1618 Expression::MemberAccess(_, inner, member) => {
1619 if member.name == "selector" {
1620 match inner.as_ref() {
1621 Expression::MemberAccess(_, _, function_name) => {
1622 let name = function_name.name.trim();
1623 if name.is_empty() {
1624 None
1625 } else {
1626 Some(name.to_string())
1627 }
1628 }
1629 Expression::Variable(function_name) => {
1630 let name = function_name.name.trim();
1631 if name.is_empty() {
1632 None
1633 } else {
1634 Some(name.to_string())
1635 }
1636 }
1637 _ => None,
1638 }
1639 } else {
1640 None
1641 }
1642 }
1643 Expression::FunctionCall(_, func, args) => {
1644 if matches!(func.as_ref(), Expression::Type(_, _)) && args.len() == 1 {
1645 return extract_static_selector_method_name(&args[0]);
1646 }
1647 if let Expression::Variable(id) = func.as_ref() {
1648 if (id.name == "bytes" || id.name == "string") && args.len() == 1 {
1649 return extract_static_selector_method_name(&args[0]);
1650 }
1651 if id.name == "keccak256" && args.len() == 1 {
1652 let signature = extract_static_signature_string(&args[0])?;
1653 let name = signature
1654 .split('(')
1655 .next()
1656 .unwrap_or(signature.as_str())
1657 .trim()
1658 .to_string();
1659 if name.is_empty() {
1660 return None;
1661 }
1662 return Some(name);
1663 }
1664 }
1665 None
1666 }
1667 _ => None,
1668 }
1669}
1670
1671/// Task #194 — recognise the function reference argument of
1672/// `abi.encodeCall(funcRef, tuple)`. Accepts `Type.method`,
1673/// `instance.method`, or nested member-access chains and returns the
1674/// outermost member name.
1675fn extract_static_encode_call_method_name(expr: &Expression) -> Option<String> {
1676 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1677 extract_static_encode_call_method_name_inner(expr)
1678 })
1679}
1680
1681fn extract_static_encode_call_method_name_inner(expr: &Expression) -> Option<String> {
1682 match expr {
1683 Expression::Parenthesis(_, inner) => extract_static_encode_call_method_name(inner),
1684 Expression::MemberAccess(_, _inner, member) => {
1685 if member.name == "selector" {
1686 // `abi.encodeCall(X.method.selector, …)` — uncommon but we
1687 // can still recover the method name by looking one level up.
1688 if let Expression::MemberAccess(_, _, function_name) = _inner.as_ref() {
1689 let name = function_name.name.trim();
1690 if !name.is_empty() {
1691 return Some(name.to_string());
1692 }
1693 }
1694 return None;
1695 }
1696 let name = member.name.trim();
1697 if name.is_empty() {
1698 None
1699 } else {
1700 Some(name.to_string())
1701 }
1702 }
1703 _ => None,
1704 }
1705}
1706
1707/// Task #194 — compile-time constant string extraction. Peels `bytes(…)`
1708/// / `string(…)` casts and unwraps `Parenthesis` but stops at the first
1709/// non-literal (e.g. `constant`-stored strings are not read here because
1710/// the analyse pass doesn't have access to the lowering context yet).
1711fn extract_static_signature_string(expr: &Expression) -> Option<String> {
1712 stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
1713 extract_static_signature_string_inner(expr)
1714 })
1715}
1716
1717fn extract_static_signature_string_inner(expr: &Expression) -> Option<String> {
1718 match expr {
1719 Expression::Parenthesis(_, inner) => extract_static_signature_string(inner),
1720 Expression::StringLiteral(parts) => {
1721 let mut bytes = Vec::new();
1722 for part in parts {
1723 bytes.extend_from_slice(part.string.as_bytes());
1724 }
1725 Some(String::from_utf8_lossy(&bytes).to_string())
1726 }
1727 Expression::FunctionCall(_, func, args) if args.len() == 1 => match func.as_ref() {
1728 Expression::Type(_, _) => extract_static_signature_string(&args[0]),
1729 Expression::Variable(id) if id.name == "bytes" || id.name == "string" => {
1730 extract_static_signature_string(&args[0])
1731 }
1732 _ => None,
1733 },
1734 _ => None,
1735 }
1736}
1737
1738/// Task #206 — compute the DIRECT sibling-primary references a contract body
1739/// introduces. Used by the sibling-merge closure so multi-hop cross-contract
1740/// call chains pull every reachable primary into the root artifact's
1741/// self-dispatch table.
1742fn collect_direct_sibling_contract_refs(
1743 contract: &ContractIR,
1744 primary_names: &std::collections::HashSet<String>,
1745 interface_names: &std::collections::HashSet<String>,
1746 interface_impls: &std::collections::HashMap<String, Vec<String>>,
1747 primary_method_names: &std::collections::HashMap<
1748 String,
1749 std::collections::HashSet<String>,
1750 >,
1751) -> std::collections::HashSet<String> {
1752 let mut referenced: std::collections::HashSet<String> =
1753 std::collections::HashSet::new();
1754 let mut iface_refs: std::collections::HashSet<String> =
1755 std::collections::HashSet::new();
1756 let mut low_level_method_refs: std::collections::HashSet<String> =
1757 std::collections::HashSet::new();
1758
1759 for function in &contract.functions {
1760 if let Some(body) = function.body.as_ref() {
1761 collect_new_contract_refs(body, primary_names, &mut referenced);
1762 collect_interface_casts_stmt(body, interface_names, &mut iface_refs);
1763 collect_low_level_call_method_refs_stmt(body, &mut low_level_method_refs);
1764 }
1765 for p in function.parameters.iter().chain(function.returns.iter()) {
1766 if primary_names.contains(&p.ty) {
1767 referenced.insert(p.ty.clone());
1768 }
1769 if interface_names.contains(&p.ty) {
1770 iface_refs.insert(p.ty.clone());
1771 }
1772 }
1773 }
1774
1775 for state in &contract.state_variables {
1776 if primary_names.contains(&state.ty) {
1777 referenced.insert(state.ty.clone());
1778 }
1779 if interface_names.contains(&state.ty) {
1780 iface_refs.insert(state.ty.clone());
1781 }
1782 if let Some(init) = state.initializer.as_ref() {
1783 collect_new_refs_expr(init, primary_names, &mut referenced);
1784 collect_interface_casts_expr(init, interface_names, &mut iface_refs);
1785 collect_low_level_call_method_refs_expr(init, &mut low_level_method_refs);
1786 }
1787 }
1788
1789 for iface in &iface_refs {
1790 if let Some(impls) = interface_impls.get(iface) {
1791 for prim in impls {
1792 if prim != &contract.name {
1793 referenced.insert(prim.clone());
1794 }
1795 }
1796 }
1797 }
1798
1799 if !low_level_method_refs.is_empty() {
1800 for (prim_name, prim_methods) in primary_method_names {
1801 if prim_name == &contract.name {
1802 continue;
1803 }
1804 if low_level_method_refs
1805 .iter()
1806 .any(|method| prim_methods.contains(method))
1807 {
1808 referenced.insert(prim_name.clone());
1809 }
1810 }
1811 }
1812
1813 referenced.remove(&contract.name);
1814 referenced
1815}
1816
1817/// Normalize a state-variable type string for the sibling-merge collision
1818/// check so that equivalent spellings compare equal: whitespace is dropped
1819/// and the bare `uint`/`int` aliases expand to their canonical 256-bit
1820/// forms (`uint256[3]` == `uint [3]` == `uint256 [ 3 ]`,
1821/// `mapping(address=>uint)` == `mapping(address => uint256)`).
1822fn normalize_state_type_for_merge(ty: &str) -> String {
1823 let mut out = String::with_capacity(ty.len());
1824 let mut word = String::new();
1825 let flush = |word: &mut String, out: &mut String| {
1826 if word.is_empty() {
1827 return;
1828 }
1829 match word.as_str() {
1830 "uint" => out.push_str("uint256"),
1831 "int" => out.push_str("int256"),
1832 other => out.push_str(other),
1833 }
1834 word.clear();
1835 };
1836 for ch in ty.chars() {
1837 if ch.is_alphanumeric() || ch == '_' || ch == '$' {
1838 word.push(ch);
1839 } else {
1840 flush(&mut word, &mut out);
1841 if !ch.is_whitespace() {
1842 out.push(ch);
1843 }
1844 }
1845 }
1846 flush(&mut word, &mut out);
1847 out
1848}