neo_solidity/ir/expressions/calls/
member_calls.rs

1fn try_lower_member_call(
2    func: &Expression,
3    args: &[Expression],
4    ctx: &mut LoweringContext,
5    instructions: &mut Vec<Instruction>,
6) -> Option<bool> {
7    fn format_builtin_member_list(members: &[&str]) -> String {
8        const MAX_SHOWN: usize = 12;
9        if members.len() <= MAX_SHOWN {
10            return members.join(", ");
11        }
12
13        format!(
14            "{} … (+{} more)",
15            members[..MAX_SHOWN].join(", "),
16            members.len() - MAX_SHOWN
17        )
18    }
19
20    fn resolve_static_library_base(inner: &Expression, ctx: &LoweringContext) -> Option<String> {
21        match inner {
22            Expression::Variable(lib_id)
23                if !ctx.param_index_map.contains_key(&lib_id.name)
24                    && ctx.resolve_local(&lib_id.name).is_none()
25                    && !ctx.state_index_map.contains_key(&lib_id.name) =>
26            {
27                Some(lib_id.name.clone())
28            }
29            Expression::MemberAccess(_, namespace_expr, imported_symbol)
30                if matches!(
31                    namespace_expr.as_ref(),
32                    Expression::Variable(namespace_id)
33                        if !ctx.param_index_map.contains_key(&namespace_id.name)
34                            && ctx.resolve_local(&namespace_id.name).is_none()
35                            && !ctx.state_index_map.contains_key(&namespace_id.name)
36                            && !ctx.is_contract_type_name(&namespace_id.name)
37                ) && ctx.is_contract_type_name(&imported_symbol.name) =>
38            {
39                Some(imported_symbol.name.clone())
40            }
41            _ => None,
42        }
43    }
44
45    fn resolve_contract_type_name(inner: &Expression, ctx: &LoweringContext) -> Option<String> {
46        match inner {
47            Expression::Variable(type_id) if ctx.is_contract_type_name(&type_id.name) => {
48                Some(type_id.name.clone())
49            }
50            Expression::MemberAccess(_, namespace_expr, type_id)
51                if matches!(
52                    namespace_expr.as_ref(),
53                    Expression::Variable(namespace_id)
54                        if !ctx.param_index_map.contains_key(&namespace_id.name)
55                            && ctx.resolve_local(&namespace_id.name).is_none()
56                            && !ctx.state_index_map.contains_key(&namespace_id.name)
57                            && !ctx.is_contract_type_name(&namespace_id.name)
58                ) && ctx.is_contract_type_name(&type_id.name) =>
59            {
60                Some(type_id.name.clone())
61            }
62            _ => None,
63        }
64    }
65
66    if let Expression::MemberAccess(_, inner, member) = func {
67        // `super.method()` — resolve to the renamed base method preserved during
68        // inheritance flattening. The flattener stores overridden base methods as
69        // `__super_{methodName}` and records the mapping in `super_method_map`.
70        if matches!(inner.as_ref(), Expression::Variable(id) if id.name == "super") {
71            if let Some(super_name) = ctx.super_method_name(&member.name) {
72                let super_name = super_name.to_string();
73                let mut success = true;
74                for arg in args {
75                    if !lower_expression(arg, ctx, instructions) {
76                        success = false;
77                    }
78                }
79                if success {
80                    let neo_name = ctx
81                        .neo_function_name(&super_name, args.len())
82                        .unwrap_or_else(|| super_name.clone());
83                    instructions.push(Instruction::CallFunction {
84                        name: neo_name,
85                        arg_count: args.len(),
86                    });
87                }
88                if ctx.is_void_function(&super_name) {
89                    return Some(false);
90                }
91                return Some(success);
92            }
93
94            // No super method found — the method was not overridden or has no body.
95            ctx.record_error_with_suggestion(
96                format!(
97                    "super.{}() cannot be resolved; no overridden base method with a body was found",
98                    member.name
99                ),
100                "ensure the base contract defines this function with a body and marks it 'virtual'",
101            );
102            instructions.push(Instruction::PushLiteral(
103                LiteralValue::Integer(BigInt::zero()),
104            ));
105            return Some(false);
106        }
107
108        // User-defined value type `wrap`/`unwrap` — compile as no-ops.
109        // `TypeName.wrap(value)` and `TypeName.unwrap(value)` are identity operations
110        // on NeoVM since user-defined value types are transparent type aliases.
111        if (member.name == "wrap" || member.name == "unwrap") && args.len() == 1 {
112            if let Expression::Variable(type_id) = inner.as_ref() {
113                let is_type_alias = !ctx.param_index_map.contains_key(&type_id.name)
114                    && ctx.resolve_local(&type_id.name).is_none()
115                    && !ctx.state_index_map.contains_key(&type_id.name);
116                if is_type_alias {
117                    // No-op: just lower the single argument — the value passes through.
118                    let ok = lower_expression(&args[0], ctx, instructions);
119                    return Some(ok);
120                }
121            }
122        }
123
124        // Fallback: treat unresolved member calls on address-like values as external
125        // contract calls. Lower to System.Contract.Call with default flags.
126        let is_external_target = matches!(
127            infer_type_from_expression(inner.as_ref(), ctx),
128            Some(ValueType::Address)
129        ) || matches!(
130            inner.as_ref(),
131            Expression::FunctionCall(_, cast_func, cast_args)
132                if cast_args.len() == 1
133                    && resolve_contract_type_name(cast_func.as_ref(), ctx).is_some()
134                    && (matches!(
135                        infer_type_from_expression(&cast_args[0], ctx),
136                        Some(ValueType::Address)
137                    ) || address_bytes_le_from_expression(&cast_args[0]).is_some())
138        );
139
140        if is_external_target {
141            if is_low_level_evm_member(&member.name) {
142                let suggestion = match member.name.as_str() {
143                    "delegatecall" => "delegatecall is not available on Neo N3; Neo contracts have isolated storage. Use Syscalls.contractCall() for cross-contract calls",
144                    "staticcall" => "staticcall is not available on Neo N3; use view/pure functions or Syscalls.contractCallWithFlags() with ReadOnly flags",
145                    _ => "Neo N3 does not support low-level EVM calls; use NativeCalls.sol for contract-to-contract interactions",
146                };
147                ctx.record_error_with_suggestion(
148                    format!("unsupported low-level EVM call '{}'", member.name),
149                    suggestion,
150                );
151                return Some(false);
152            }
153
154            // NativeCalls exposes native contract hashes as constants. When users write
155            // `NativeCalls.GAS_CONTRACT.totalSupply()` etc we can resolve the target at
156            // compile time and emit a `NativeCall` builtin. This avoids wildcard manifest
157            // permissions and unlocks CALLT/method-token optimizations.
158            let native_contract = match inner.as_ref() {
159                Expression::MemberAccess(_, base, constant)
160                    if matches!(base.as_ref(), Expression::Variable(id) if id.name == "NativeCalls") =>
161                {
162                    match constant.name.as_str() {
163                        "NEO_CONTRACT" => Some(NativeContract::Neo),
164                        "GAS_CONTRACT" => Some(NativeContract::Gas),
165                        "CONTRACT_MANAGEMENT" => Some(NativeContract::ContractManagement),
166                        "POLICY_CONTRACT" => Some(NativeContract::Policy),
167                        "ORACLE_CONTRACT" => Some(NativeContract::Oracle),
168                        "ROLE_MANAGEMENT" => Some(NativeContract::RoleManagement),
169                        "NOTARY_CONTRACT" => Some(NativeContract::Notary),
170                        "TREASURY_CONTRACT" => Some(NativeContract::Treasury),
171                        "LEDGER_CONTRACT" => Some(NativeContract::Ledger),
172                        "CRYPTO_LIB" => Some(NativeContract::CryptoLib),
173                        "STD_LIB" => Some(NativeContract::StdLib),
174                        _ => None,
175                    }
176                }
177                _ => None,
178            };
179
180            if let Some(contract) = native_contract {
181                let mut success = true;
182                for arg in args {
183                    if !lower_expression(arg, ctx, instructions) {
184                        success = false;
185                    }
186                }
187
188                if success {
189                    instructions.push(Instruction::CallBuiltin {
190                        builtin: BuiltinCall::NativeCall {
191                            contract,
192                            method: member.name.clone(),
193                        },
194                        arg_count: args.len(),
195                    });
196                }
197
198                return Some(success);
199            }
200
201            // Neo N3 syscall convention: the first argument is at the top of the stack.
202            // `System.Contract.Call(hash, method, flags, args)` therefore expects stack order:
203            // `[args, flags, method, hash]` (top-of-stack is `hash`).
204            //
205            // Preserve Solidity evaluation order by evaluating the target expression first,
206            // then the arguments, and only then arranging the stack for the syscall.
207            let tmp_id = ctx.next_label();
208            let target_slot = ctx.allocate_local(format!("__neo_extcall_target_{tmp_id}"), None);
209
210            if !lower_expression(inner.as_ref(), ctx, instructions) {
211                return Some(false);
212            }
213            instructions.push(Instruction::StoreLocal(target_slot));
214
215            // Build the argument array directly (more efficient than serialize+deserialize).
216            let args_slot = ctx.allocate_local(
217                format!("__neo_extcall_args_{tmp_id}"),
218                Some(ValueType::Array(Box::new(ValueType::Any))),
219            );
220
221            instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::from(
222                args.len(),
223            ))));
224            instructions.push(Instruction::NewArray {
225                element_type: ValueType::Any,
226            });
227            instructions.push(Instruction::StoreLocal(args_slot));
228
229            for (index, arg) in args.iter().enumerate() {
230                instructions.push(Instruction::LoadLocal(args_slot));
231                instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
232                    BigInt::from(index as u64),
233                )));
234
235                if !lower_expression(arg, ctx, instructions) {
236                    instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
237                        BigInt::zero(),
238                    )));
239                }
240
241                instructions.push(Instruction::ArraySet);
242            }
243
244            instructions.push(Instruction::LoadLocal(args_slot));
245
246            // Use read-only call flags in `view`/`pure` contexts to align with Solidity's
247            // static-call behavior and Neo N3 `safe` method expectations.
248            let flags = if ctx.is_safe { 0x05u8 } else { 0x0Fu8 };
249            instructions.push(Instruction::PushLiteral(LiteralValue::Integer(BigInt::from(
250                flags,
251            ))));
252            instructions.push(Instruction::PushLiteral(LiteralValue::String(
253                member.name.as_bytes().to_vec(),
254            )));
255            instructions.push(Instruction::LoadLocal(target_slot));
256
257            instructions.push(Instruction::CallBuiltin {
258                builtin: BuiltinCall::Syscall("System.Contract.Call".to_string()),
259                arg_count: 4,
260            });
261            return Some(true);
262        }
263
264        // Attempt to lower member calls as internal/library calls.
265        if ctx.function_names.contains(&member.name) {
266            let static_library_base = resolve_static_library_base(inner.as_ref(), ctx);
267            let is_library_static = static_library_base.is_some();
268
269            let mut success = true;
270            if is_library_static {
271                // Built-in libraries are lowered directly as compiler intrinsics. If builtin
272                // resolution didn't match the member, do not fall back to calling an internal
273                // function with the same name (which can silently miscompile into recursion).
274                if static_library_base.as_deref().is_some_and(|base| {
275                    matches!(
276                        base,
277                        "Runtime" | "abi" | "Storage" | "Syscalls" | "Neo" | "NativeCalls"
278                    )
279                }) {
280                    let base = static_library_base.as_deref().unwrap_or("<library>");
281                    let mut message =
282                        format!("unsupported builtin library call '{base}.{}'", member.name);
283                    if let Some(supported) = builtin_library_supported_members(base) {
284                        message.push_str(&format!(
285                            "; supported {base} intrinsics: {}",
286                            format_builtin_member_list(supported)
287                        ));
288                    }
289                    message.push_str(
290                        ". (Builtin devpack libraries are compiler intrinsics; their Solidity bodies are not compiled.)",
291                    );
292                    ctx.record_error(message);
293                    return Some(false);
294                }
295
296                for arg in args {
297                    if !lower_expression(arg, ctx, instructions) {
298                        success = false;
299                    }
300                }
301
302                if success {
303                    if let Some(neo_name) = ctx.neo_function_name(&member.name, args.len()) {
304                        instructions.push(Instruction::CallFunction {
305                            name: neo_name,
306                            arg_count: args.len(),
307                        });
308                    } else {
309                        ctx.record_error(format!(
310                            "no overload of '{}' with {} argument(s)",
311                            member.name,
312                            args.len()
313                        ));
314                        success = false;
315                    }
316                }
317                // Void functions don't push a value onto the stack, so return
318                // false to prevent the caller from emitting a spurious DROP.
319                if ctx.is_void_function(&member.name) {
320                    return Some(false);
321                }
322                return Some(success);
323            }
324
325            if !lower_expression(inner.as_ref(), ctx, instructions) {
326                success = false;
327            }
328
329            for arg in args {
330                if !lower_expression(arg, ctx, instructions) {
331                    success = false;
332                }
333            }
334
335            if success {
336                let arg_count = args.len() + 1;
337                if let Some(neo_name) = ctx.neo_function_name(&member.name, arg_count) {
338                    instructions.push(Instruction::CallFunction {
339                        name: neo_name,
340                        arg_count,
341                    });
342                } else {
343                    ctx.record_error(format!(
344                        "no overload of '{}' with {} argument(s)",
345                        member.name, arg_count
346                    ));
347                    success = false;
348                }
349            }
350
351            return Some(success);
352        }
353
354        // NeoVM iterator handles are advanced via syscalls. Some devpacks express this as
355        // `iterator.next()` / `iterator.value()` on a handle-like type; treat those as
356        // `System.Iterator.Next` / `System.Iterator.Value` when no user-defined overload exists.
357        if !ctx.function_names.contains(&member.name) && args.is_empty() {
358            if member.name == "next" {
359                if !lower_expression(inner.as_ref(), ctx, instructions) {
360                    return Some(false);
361                }
362                instructions.push(Instruction::CallBuiltin {
363                    builtin: BuiltinCall::Syscall("System.Iterator.Next".to_string()),
364                    arg_count: 1,
365                });
366                return Some(true);
367            }
368
369            if member.name == "value" {
370                if !lower_expression(inner.as_ref(), ctx, instructions) {
371                    return Some(false);
372                }
373                instructions.push(Instruction::CallBuiltin {
374                    builtin: BuiltinCall::Syscall("System.Iterator.Value".to_string()),
375                    arg_count: 1,
376                });
377                instructions.push(Instruction::PushLiteral(LiteralValue::Integer(
378                    BigInt::one(),
379                )));
380                instructions.push(Instruction::ArrayGet);
381                return Some(true);
382            }
383        }
384
385        // Builtin helper libraries (Runtime/Storage/Syscalls/Neo/NativeCalls/abi) are lowered
386        // directly by the compiler. When a call targets one of these libraries but does not map
387        // to a supported intrinsic, emit a targeted diagnostic rather than a generic
388        // "unsupported external/library call" error.
389        if let Some(base) = resolve_static_library_base(inner.as_ref(), ctx) {
390            if let Some(supported) = builtin_library_supported_members(base.as_str()) {
391                ctx.record_error(format!(
392                    "unsupported builtin library call '{}.{}'; supported {} intrinsics: {}. (Builtin devpack libraries are compiler intrinsics; their Solidity bodies are not compiled.)",
393                    base,
394                    member.name,
395                    base,
396                    format_builtin_member_list(supported)
397                ));
398                return Some(false);
399            }
400        }
401
402        ctx.record_error(format!(
403            "unsupported external/library call '{}'",
404            member.name
405        ));
406        return Some(false);
407    }
408
409    None
410}