Skip to main content

neo_devpack_solidity/ir/context/builtins/
resolve.rs

1fn resolve_builtin_call(expr: &Expression) -> Option<BuiltinCall> {
2    if let Expression::MemberAccess(_, inner, member) = expr {
3        if let Expression::Variable(base) = inner.as_ref() {
4            let member_name = member.name.as_str();
5            match base.name.as_str() {
6                "Runtime" => return resolve_runtime_member(member_name),
7                "abi" => return resolve_abi_member(member_name),
8                "Storage" => return resolve_storage_member(member_name),
9                "Syscalls" => return resolve_syscalls_member(member_name),
10                "NativeCalls" => return resolve_native_calls_member(member_name),
11                "Neo" => return resolve_neo_member(member_name),
12                // Task — `StdLib.<method>(...)` and `CryptoLib.<method>(...)`
13                // in Solidity source must lower to a `BuiltinCall::NativeCall`
14                // to the matching Neo N3 native contract. Before this was
15                // wired, the compiler fell through to the generic function-
16                // call handler, which evaluated and dropped the args then
17                // pushed PUSH0 as the result. Fuzz regression: the
18                // Solidity→bytecode path for `StdLib.itoa(12345, 10)` and
19                // `CryptoLib.sha256(bytes)` returned zeros at runtime even
20                // though the underlying native implementations are correct
21                // (see baseline_tests.rs::callt_stdlib_itoa_roundtrip_via_token).
22                "StdLib" => return resolve_stdlib_member(member_name),
23                "CryptoLib" => return resolve_cryptolib_member(member_name),
24                _ => {}
25            }
26        }
27    }
28
29    if let Expression::Variable(identifier) = expr {
30        if identifier.name == "ecrecover" {
31            return Some(BuiltinCall::Ecrecover);
32        }
33        if identifier.name == "keccak256" {
34            return Some(BuiltinCall::Keccak256);
35        }
36        // Solidity exposes `sha256(bytes)` and `ripemd160(bytes)` as
37        // global built-in hashers (EVM precompiles 0x02 and 0x03). Route
38        // them to the CryptoLib native contract's `sha256`/`ripemd160`
39        // methods. Before this wiring the compiler fell through to the
40        // generic function-call handler, which evaluated and dropped the
41        // argument and pushed 0 — so `sha256(b"abc")` returned 8 zero
42        // bytes at runtime instead of the canonical digest. `keccak256`
43        // already has a dedicated `BuiltinCall::Keccak256` variant
44        // because its EVM semantics differ from Neo's default; `sha256`
45        // and `ripemd160` have a direct 1:1 native mapping.
46        if identifier.name == "sha256" {
47            return Some(BuiltinCall::NativeCall {
48                contract: NativeContract::CryptoLib,
49                method: "sha256".to_string(),
50            });
51        }
52        if identifier.name == "ripemd160" {
53            return Some(BuiltinCall::NativeCall {
54                contract: NativeContract::CryptoLib,
55                method: "ripemd160".to_string(),
56            });
57        }
58        if identifier.name == "type" {
59            return Some(BuiltinCall::TypeOf);
60        }
61    }
62
63    None
64}
65
66/// The builtin-library base names the compiler lowers as intrinsics
67/// (their devpack Solidity bodies are never compiled).
68pub const BUILTIN_LIBRARY_BASES: &[&str] =
69    &["Runtime", "abi", "Storage", "Syscalls", "NativeCalls", "Neo"];
70
71/// Introspection over the complete builtin intrinsic surface:
72/// `(base, supported members)` for every builtin library base.
73///
74/// INVARIANT (pinned by `tests/gap_hasrole_tests.rs`): every member listed
75/// here MUST actually lower — either via the `resolve_*_member` table in this
76/// file / `syscalls.rs` / `native_calls.rs`, or via a bespoke handler in
77/// `src/ir/expressions/calls/builtins/member_*.rs`. A whitelisted member
78/// without a lowering is an uncallable intrinsic: the diagnostic for
79/// `Base.member(...)` would then claim the member is supported while every
80/// call fails (this happened with `Syscalls.hasRole`, which resolved to
81/// `None` while being advertised). Add new members here only together with
82/// their lowering.
83pub fn builtin_intrinsic_surface() -> Vec<(&'static str, &'static [&'static str])> {
84    BUILTIN_LIBRARY_BASES
85        .iter()
86        .map(|base| {
87            (
88                *base,
89                builtin_library_supported_members(base)
90                    .expect("every builtin library base has a member whitelist"),
91            )
92        })
93        .collect()
94}
95
96fn builtin_library_supported_members(base: &str) -> Option<&'static [&'static str]> {
97    match base {
98        "Runtime" => Some(&[
99            "notify",
100            "notifyIndexed",
101            "checkWitness",
102            "requireWitness",
103            "checkAnyWitness",
104            "checkAllWitnesses",
105            "checkMultiSigWitness",
106            "gasLeft",
107            "burnGas",
108            "log",
109            "getTime",
110            "getTrigger",
111            "getInvocationCounter",
112            "getCurrentSigners",
113            "getCallFlags",
114            "getScriptContainer",
115            "loadScript",
116            "initializeServices",
117            "getNetwork",
118            "getPlatform",
119            "getAddressVersion",
120            "getRandom",
121            "getExecutingScriptHash",
122            "getCallingScriptHash",
123            "getEntryScriptHash",
124        ]),
125        "abi" => Some(&[
126            "encode",
127            "encodePacked",
128            "encodeCall",
129            "encodeWithSignature",
130            "encodeWithSelector",
131            "decode",
132        ]),
133        // Only members with faithful Neo N3 lowerings are listed here. The
134        // former `*Local` family lowered to fictional `System.Storage.Local.*`
135        // syscalls that do not exist in Neo N3's interop table (instant FAULT
136        // on real nodes), and convenience helpers such as `batchPut`,
137        // `batchGet`, `batchDelete`, `count`, `findValues`, `findKeys`,
138        // `clearPrefix`, `exists`, `isValidKey`, and `getUsage` silently
139        // miscompiled to single raw syscalls with the wrong arity/semantics.
140        // All of them now fail compilation with a loud diagnostic instead.
141        "Storage" => Some(&[
142            "find",
143            "put",
144            "get",
145            "remove",
146            "asReadOnly",
147            "initializeContext",
148            "getContext",
149            "getReadOnlyContext",
150            "putContractMetadata",
151        ]),
152        "Syscalls" => Some(&[
153            "contractCall",
154            "contractCallWithFlags",
155            "getCallFlags",
156            "contractCreate",
157            "contractUpdate",
158            "contractDestroy",
159            "createStandardAccount",
160            "createMultisigAccount",
161            "notify",
162            "getCurrentIndex",
163            "getCurrentHash",
164            "getBlock",
165            "getTransaction",
166            "getTransactionHeight",
167            "getTransactionFromBlock",
168            "getTransactionSigners",
169            "getTransactionVMState",
170            "getExecutingScriptHash",
171            "getCallingScriptHash",
172            "getEntryScriptHash",
173            "getScriptContainer",
174            "loadScript",
175            "getStorageContext",
176            "getReadOnlyStorageContext",
177            "storageAsReadOnly",
178            "storageGet",
179            "storagePut",
180            "storageDelete",
181            "storageFind",
182            "checkWitness",
183            "getTime",
184            "gasLeft",
185            "getPlatform",
186            "getTrigger",
187            "getNotifications",
188            "log",
189            "getCurrentSigners",
190            "checkSig",
191            "checkMultisig",
192            "sha256",
193            "ripemd160",
194            "verifyWithECDsa",
195            "murmur32",
196            "keccak256",
197            "recoverSecp256K1",
198            "verifyWithEd25519",
199            "bls12381Serialize",
200            "bls12381Deserialize",
201            "bls12381Equal",
202            "bls12381Add",
203            "bls12381Mul",
204            "bls12381Pairing",
205            // G1/G2 affine ops mirror the CryptoLib.* path resolver at
206            // roughly line 585. Keeping them out of the Syscalls whitelist
207            // would make `Syscalls.bls12381G1Add(...)` fail with an
208            // "unsupported builtin library call" diagnostic even though the
209            // CryptoLib side accepts the call — the audit agent flagged
210            // this asymmetry.
211            "bls12381G1Add",
212            "bls12381G1Mul",
213            "bls12381G1Neg",
214            "bls12381G2Add",
215            "bls12381G2Mul",
216            "bls12381G2Neg",
217            // Alias for `keccak256` — the devpack's `Syscalls.sol` exposes
218            // it as `neoKeccak256` so the name doesn't shadow Solidity's
219            // bare `keccak256` intrinsic. Unwired here before the audit.
220            "neoKeccak256",
221            "serialize",
222            "deserialize",
223            "itoa",
224            "atoi",
225            "jsonSerialize",
226            "jsonDeserialize",
227            "base64Encode",
228            "base64Decode",
229            "base64UrlEncode",
230            "base64UrlDecode",
231            "base58Encode",
232            "base58Decode",
233            "base58CheckEncode",
234            "base58CheckDecode",
235            "hexEncode",
236            "hexDecode",
237            "memoryCompare",
238            "memorySearch",
239            "stringSplit",
240            "strLen",
241            "iteratorNext",
242            "iteratorValue",
243            "getCurrentRandom",
244            "getNetwork",
245            "getAddressVersion",
246            "burnGas",
247            "getInvocationCounter",
248            "getFeePerByte",
249            "getExecFeeFactor",
250            "getExecPicoFeeFactor",
251            "getStoragePrice",
252            "getMillisecondsPerBlock",
253            "getMaxValidUntilBlockIncrement",
254            "getMaxTraceableBlocks",
255            "getAttributeFee",
256            "isBlocked",
257            "oracleRequest",
258            "getOraclePrice",
259            "getDesignatedByRole",
260            "scriptHashToAddress",
261            "addressToScriptHash",
262            "isValidAddress",
263            "getContractScript",
264            "contractExists",
265        ]),
266        "NativeCalls" => Some(&[
267            "neoTotalSupply",
268            "neoBalanceOf",
269            "neoTransfer",
270            "neoDecimals",
271            "neoSymbol",
272            "neoName",
273            "vote",
274            "getCandidates",
275            "registerCandidate",
276            "unregisterCandidate",
277            "getGasPerBlock",
278            "getRegisterPrice",
279            "setRegisterPrice",
280            "setGasPerBlock",
281            "getAccountState",
282            "unclaimedGas",
283            "getCandidateVote",
284            "getCommittee",
285            "getCommitteeAddress",
286            "isCommittee",
287            "getNextBlockValidators",
288            "getAllCandidates",
289            "isValidator",
290            "gasTotalSupply",
291            "gasBalanceOf",
292            "gasTransfer",
293            "gasDecimals",
294            "gasSymbol",
295            "gasName",
296            "deployContract",
297            "updateContract",
298            "destroyContract",
299            "getContract",
300            "listContracts",
301            "hasMethod",
302            "getMinimumDeploymentFee",
303            "setMinimumDeploymentFee",
304            "getContractById",
305            "isContract",
306            "getFeePerByte",
307            "setFeePerByte",
308            "getExecFeeFactor",
309            "getExecPicoFeeFactor",
310            "setExecFeeFactor",
311            "getStoragePrice",
312            "getMillisecondsPerBlock",
313            "setMillisecondsPerBlock",
314            "getMaxValidUntilBlockIncrement",
315            "setMaxValidUntilBlockIncrement",
316            "getMaxTraceableBlocks",
317            "setMaxTraceableBlocks",
318            "getAttributeFee",
319            "setAttributeFee",
320            "setStoragePrice",
321            "blockAccount",
322            "unblockAccount",
323            "isBlocked",
324            "getBlockedAccounts",
325            "recoverFund",
326            "setWhitelistFeeContract",
327            "removeWhitelistFeeContract",
328            "getWhitelistFeeContracts",
329            "requestOracleData",
330            "getOraclePrice",
331            "setOraclePrice",
332            "oracleFinish",
333            "oracleVerify",
334            "oracleRequest",
335            "designateAsRole",
336            "getDesignatedByRole",
337            "currentIndex",
338            "currentHash",
339            "getBlock",
340            "getBlockHash",
341            "getBlockByIndex",
342            "getBlockByHash",
343            "getTransaction",
344            "getTransactionHeight",
345            "getTransactionFromBlock",
346            "getTransactionSigners",
347            "getTransactionVMState",
348            "getBlockSystemFee",
349            "notaryVerify",
350            "notaryBalanceOf",
351            "notaryExpirationOf",
352            "notaryLockDepositUntil",
353            "notaryWithdraw",
354            "notaryGetMaxNotValidBeforeDelta",
355            "notarySetMaxNotValidBeforeDelta",
356            "notaryOnNEP17Payment",
357            "treasuryVerify",
358            "treasuryOnNEP17Payment",
359            "treasuryOnNEP11Payment",
360            "isNativeContract",
361            "getNativeContractName",
362            "getAllNativeContracts",
363            "estimateNativeCallGas",
364            "batchNativeCalls",
365            "getNetworkConfiguration",
366            "getNativeContractManifest",
367            "safeNativeCall",
368            "externalNativeCall",
369        ]),
370        "Neo" => Some(&[
371            "verifySignature",
372            "callContract",
373            "deployContract",
374            "getNeoBalance",
375            "getGasBalance",
376            "getGasPrice",
377            "getStoragePrice",
378            "getCommittee",
379            "getValidators",
380            "isCommittee",
381            "isValidator",
382            "getRandom",
383            "getBlockHeight",
384            "getCurrentBlock",
385            "getBlockByIndex",
386            "getBlockTime",
387            "getTransaction",
388            "getTransactionHeight",
389            "transactionExists",
390            "getNetworkMagic",
391            "verifyWithWitness",
392            "sha256Hash",
393            "ripemd160Hash",
394            // `transferNeo`/`transferGas` are lowered by
395            // `try_lower_value_transfer_helpers` (value_transfer.rs) to the
396            // NEO/GAS native `transfer` methods rather than via
397            // `resolve_neo_member`, but they are part of the supported
398            // devpack surface and belong in this diagnostic list.
399            "transferNeo",
400            "transferGas",
401        ]),
402        _ => None,
403    }
404}
405
406fn resolve_runtime_member(member: &str) -> Option<BuiltinCall> {
407    match member {
408        "notify" => Some(BuiltinCall::RuntimeNotify),
409        "checkWitness" => Some(BuiltinCall::RuntimeCheckWitness),
410        "gasLeft" => Some(BuiltinCall::Syscall("System.Runtime.GasLeft".to_string())),
411        "burnGas" => Some(BuiltinCall::Syscall("System.Runtime.BurnGas".to_string())),
412        "log" => Some(BuiltinCall::Syscall("System.Runtime.Log".to_string())),
413        "getTime" => Some(BuiltinCall::Syscall("System.Runtime.GetTime".to_string())),
414        "getTrigger" => Some(BuiltinCall::Syscall(
415            "System.Runtime.GetTrigger".to_string(),
416        )),
417        "getInvocationCounter" => Some(BuiltinCall::Syscall(
418            "System.Runtime.GetInvocationCounter".to_string(),
419        )),
420        "getCurrentSigners" => Some(BuiltinCall::Syscall(
421            "System.Runtime.CurrentSigners".to_string(),
422        )),
423        "getCallFlags" => Some(BuiltinCall::Syscall(
424            "System.Contract.GetCallFlags".to_string(),
425        )),
426        "getScriptContainer" => Some(BuiltinCall::Syscall(
427            "System.Runtime.GetScriptContainer".to_string(),
428        )),
429        "loadScript" => Some(BuiltinCall::Syscall(
430            "System.Runtime.LoadScript".to_string(),
431        )),
432        "getNetwork" => Some(BuiltinCall::Syscall(
433            "System.Runtime.GetNetwork".to_string(),
434        )),
435        "getPlatform" => Some(BuiltinCall::Syscall("System.Runtime.Platform".to_string())),
436        "getAddressVersion" => Some(BuiltinCall::Syscall(
437            "System.Runtime.GetAddressVersion".to_string(),
438        )),
439        "getRandom" => Some(BuiltinCall::Syscall("System.Runtime.GetRandom".to_string())),
440        "getExecutingScriptHash" => Some(BuiltinCall::Syscall(
441            "System.Runtime.GetExecutingScriptHash".to_string(),
442        )),
443        "getCallingScriptHash" => Some(BuiltinCall::Syscall(
444            "System.Runtime.GetCallingScriptHash".to_string(),
445        )),
446        "getEntryScriptHash" => Some(BuiltinCall::Syscall(
447            "System.Runtime.GetEntryScriptHash".to_string(),
448        )),
449        _ => None,
450    }
451}
452
453fn resolve_abi_member(member: &str) -> Option<BuiltinCall> {
454    match member {
455        "encode" => Some(BuiltinCall::AbiEncode),
456        "encodePacked" => Some(BuiltinCall::AbiEncodePacked),
457        "encodeCall" => Some(BuiltinCall::AbiEncodeCall),
458        "encodeWithSignature" => Some(BuiltinCall::AbiEncodeWithSignature),
459        "encodeWithSelector" => Some(BuiltinCall::AbiEncodeWithSignature),
460        "decode" => Some(BuiltinCall::AbiDecode),
461        _ => None,
462    }
463}
464
465fn resolve_storage_member(member: &str) -> Option<BuiltinCall> {
466    // Note: every mapping here must target a syscall that exists in Neo N3's
467    // ApplicationEngine interop table. The former `*Local` family lowered to
468    // fictional `System.Storage.Local.*` names (sha256-hashed interop IDs that
469    // no real node registers → unknown-syscall FAULT at runtime), and helpers
470    // like `batchPut`/`count`/`exists`/`clearPrefix` were lowered to a single
471    // raw syscall with the wrong arity and semantics. They were removed so
472    // calls fail at compile time via the "unsupported builtin library call"
473    // diagnostic instead of corrupting state or faulting on-chain.
474    match member {
475        "find" => Some(BuiltinCall::StorageFind),
476        "put" => Some(BuiltinCall::StoragePut),
477        "get" => Some(BuiltinCall::StorageGet),
478        "remove" => Some(BuiltinCall::StorageDelete),
479        "initializeContext" => Some(BuiltinCall::Syscall(
480            "System.Storage.GetContext".to_string(),
481        )),
482        "getContext" => Some(BuiltinCall::Syscall(
483            "System.Storage.GetContext".to_string(),
484        )),
485        "getReadOnlyContext" => Some(BuiltinCall::Syscall(
486            "System.Storage.GetReadOnlyContext".to_string(),
487        )),
488        "asReadOnly" => Some(BuiltinCall::Syscall(
489            "System.Storage.AsReadOnly".to_string(),
490        )),
491        _ => None,
492    }
493}
494
495fn resolve_neo_member(member: &str) -> Option<BuiltinCall> {
496    match member {
497        "verifySignature" => Some(BuiltinCall::VerifySignature),
498        "callContract" => Some(BuiltinCall::ContractCall),
499        "deployContract" => Some(BuiltinCall::DeployContract),
500        "getNeoBalance" => Some(BuiltinCall::NativeCall {
501            contract: NativeContract::Neo,
502            method: "balanceOf".to_string(),
503        }),
504        "getGasBalance" => Some(BuiltinCall::NativeCall {
505            contract: NativeContract::Gas,
506            method: "balanceOf".to_string(),
507        }),
508        "getGasPrice" => Some(BuiltinCall::NativeCall {
509            contract: NativeContract::Policy,
510            method: "getFeePerByte".to_string(),
511        }),
512        "getStoragePrice" => Some(BuiltinCall::NativeCall {
513            contract: NativeContract::Policy,
514            method: "getStoragePrice".to_string(),
515        }),
516        "getCommittee" => Some(BuiltinCall::NativeCall {
517            contract: NativeContract::Neo,
518            method: "getCommittee".to_string(),
519        }),
520        "getValidators" => Some(BuiltinCall::NativeCall {
521            contract: NativeContract::Neo,
522            method: "getNextBlockValidators".to_string(),
523        }),
524        "isCommittee" => Some(BuiltinCall::NativeCall {
525            contract: NativeContract::Neo,
526            method: "getCommittee".to_string(),
527        }),
528        "isValidator" => Some(BuiltinCall::NativeCall {
529            contract: NativeContract::Neo,
530            method: "getNextBlockValidators".to_string(),
531        }),
532        "getRandom" => Some(BuiltinCall::Syscall("System.Runtime.GetRandom".to_string())),
533        "getBlockHeight" => Some(BuiltinCall::NativeCall {
534            contract: NativeContract::Ledger,
535            method: "currentIndex".to_string(),
536        }),
537        "getCurrentBlock" => Some(BuiltinCall::NativeCall {
538            contract: NativeContract::Ledger,
539            method: "currentIndex".to_string(),
540        }),
541        "transactionExists" => Some(BuiltinCall::NativeCall {
542            contract: NativeContract::Ledger,
543            method: "getTransaction".to_string(),
544        }),
545        "getNetworkMagic" => Some(BuiltinCall::Syscall(
546            "System.Runtime.GetNetwork".to_string(),
547        )),
548        "getBlockByIndex" => Some(BuiltinCall::NativeCall {
549            contract: NativeContract::Ledger,
550            method: "getBlock".to_string(),
551        }),
552        "getBlockTime" => Some(BuiltinCall::Syscall("System.Runtime.GetTime".to_string())),
553        "getTransaction" => Some(BuiltinCall::NativeCall {
554            contract: NativeContract::Ledger,
555            method: "getTransaction".to_string(),
556        }),
557        "getTransactionHeight" => Some(BuiltinCall::NativeCall {
558            contract: NativeContract::Ledger,
559            method: "getTransactionHeight".to_string(),
560        }),
561        "verifyWithWitness" => Some(BuiltinCall::Syscall(
562            "System.Runtime.CheckWitness".to_string(),
563        )),
564        "sha256Hash" => Some(BuiltinCall::NativeCall {
565            contract: NativeContract::CryptoLib,
566            method: "sha256".to_string(),
567        }),
568        "ripemd160Hash" => Some(BuiltinCall::NativeCall {
569            contract: NativeContract::CryptoLib,
570            method: "ripemd160".to_string(),
571        }),
572        _ => None,
573    }
574}
575
576/// `StdLib.<method>(...)` — resolve bare member-access into a NativeCall to
577/// the StdLib native contract (hash c0ef39cee0e4e925c6c2a06a79e1440dd86fceac).
578/// Solidity source can write either `StdLib.itoa(v, 10)` or the devpack's
579/// `Syscalls.itoa(v, 10)`; both must land at the same CALLT. See
580/// `resolve_syscalls_member` for the sibling dispatch.
581fn resolve_stdlib_member(member: &str) -> Option<BuiltinCall> {
582    match member {
583        "serialize" | "deserialize" | "jsonSerialize" | "jsonDeserialize" | "itoa" | "atoi"
584        | "base64Encode" | "base64Decode" | "base64UrlEncode" | "base64UrlDecode"
585        | "base58Encode" | "base58Decode" | "base58CheckEncode" | "base58CheckDecode"
586        | "hexEncode" | "hexDecode" | "memoryCompare" | "memorySearch" | "stringSplit"
587        | "strLen" => Some(BuiltinCall::NativeCall {
588            contract: NativeContract::StdLib,
589            method: member.to_string(),
590        }),
591        _ => None,
592    }
593}
594
595/// `CryptoLib.<method>(...)` — resolve bare member-access into a NativeCall
596/// to the CryptoLib native contract (hash 1bf575ab11896884136110a35a12886cde0b66c72).
597/// Keeps parity with `Syscalls.sha256` / `Syscalls.ripemd160` / etc.
598fn resolve_cryptolib_member(member: &str) -> Option<BuiltinCall> {
599    match member {
600        "sha256" | "ripemd160" | "verifyWithECDsa" | "murmur32" | "keccak256"
601        | "recoverSecp256K1" | "verifyWithEd25519" | "bls12381Serialize"
602        | "bls12381Deserialize" | "bls12381Equal" | "bls12381Add" | "bls12381Mul"
603        | "bls12381Pairing" | "bls12381G1Add" | "bls12381G1Mul" | "bls12381G2Add"
604        | "bls12381G2Mul" | "bls12381G1Neg" | "bls12381G2Neg" => {
605            Some(BuiltinCall::NativeCall {
606                contract: NativeContract::CryptoLib,
607                method: member.to_string(),
608            })
609        }
610        _ => None,
611    }
612}
613
614/// CI probe (devpack agent): the devpack `Runtime`/`Storage`/`Neo` libraries
615/// are compiler intrinsics whose Solidity bodies are never compiled, so every
616/// non-private function they declare MUST have an intrinsic lowering —
617/// otherwise the shipped sources advertise an API that hard-fails at compile
618/// time (or worse, used to silently miscompile, e.g. the fictional
619/// `System.Storage.Local.*` syscall family). These tests keep the `.sol`
620/// surface, the `builtin_library_supported_members` diagnostic whitelist, and
621/// the actual lowerings in sync.
622#[cfg(test)]
623mod devpack_intrinsic_surface_tests {
624    use super::*;
625    use std::path::PathBuf;
626
627    /// Members lowered by dedicated expansion handlers rather than
628    /// `resolve_*_member`:
629    /// - Runtime: `try_lower_runtime_member_builtin`
630    ///   (src/ir/expressions/calls/builtins/member_runtime.rs)
631    /// - Storage: `try_lower_storage_member_builtin`
632    ///   (src/ir/expressions/calls/builtins/member_storage.rs)
633    /// - Neo: `try_lower_neo_member_builtin`
634    ///   (src/ir/expressions/calls/builtins/member_neo.rs) and
635    ///   `try_lower_value_transfer_helpers`
636    ///   (src/ir/expressions/calls/value_transfer.rs for
637    ///   `transferGas`/`transferNeo`)
638    fn special_case_lowerings(base: &str) -> &'static [&'static str] {
639        match base {
640            "Runtime" => &[
641                "initializeServices",
642                "notifyIndexed",
643                "notify",
644                "requireWitness",
645                "checkAnyWitness",
646                "checkAllWitnesses",
647                "checkMultiSigWitness",
648            ],
649            "Storage" => &["putContractMetadata"],
650            "Neo" => &[
651                "isCommittee",
652                "getCommittee",
653                "getValidators",
654                "isValidator",
655                "transferGas",
656                "transferNeo",
657            ],
658            _ => &[],
659        }
660    }
661
662    fn has_lowering(base: &str, member: &str) -> bool {
663        let resolved = match base {
664            "Runtime" => resolve_runtime_member(member).is_some(),
665            "Storage" => resolve_storage_member(member).is_some(),
666            "Neo" => resolve_neo_member(member).is_some(),
667            other => panic!("unexpected library base '{other}'"),
668        };
669        resolved || special_case_lowerings(base).contains(&member)
670    }
671
672    fn devpack_library_path(base: &str) -> PathBuf {
673        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
674            .join("devpack")
675            .join("libraries")
676            .join(format!("{base}.sol"))
677    }
678
679    /// Extract the names of all non-private function declarations from a
680    /// devpack library source. Private helpers (declared `private` or named
681    /// with a leading underscore) are not part of the public surface.
682    fn declared_public_functions(source: &str) -> Vec<String> {
683        let mut names = Vec::new();
684        for line in source.lines() {
685            let trimmed = line.trim_start();
686            let Some(rest) = trimmed.strip_prefix("function ") else {
687                continue;
688            };
689            let Some(name) = rest.split('(').next() else {
690                continue;
691            };
692            let name = name.trim();
693            if name.is_empty() || name.starts_with('_') {
694                continue;
695            }
696            // The visibility keyword may appear on the declaration line or a
697            // follow-up line; devpack sources keep `private` on the same line.
698            if trimmed.contains(" private ") || trimmed.ends_with(" private") {
699                continue;
700            }
701            names.push(name.to_string());
702        }
703        names
704    }
705
706    #[test]
707    fn every_declared_devpack_library_function_has_an_intrinsic_lowering() {
708        for base in ["Runtime", "Storage", "Neo"] {
709            let path = devpack_library_path(base);
710            let source = std::fs::read_to_string(&path)
711                .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
712            let declared = declared_public_functions(&source);
713            assert!(
714                !declared.is_empty(),
715                "no function declarations found in {} — extraction regressed?",
716                path.display()
717            );
718
719            let missing: Vec<&String> = declared
720                .iter()
721                .filter(|name| !has_lowering(base, name))
722                .collect();
723            assert!(
724                missing.is_empty(),
725                "devpack/libraries/{base}.sol declares functions with no compiler \
726                 lowering (calls would hard-fail; prune them or add a lowering): {missing:?}"
727            );
728        }
729    }
730
731    #[test]
732    fn every_declared_devpack_library_function_is_whitelisted() {
733        // The "supported intrinsics" diagnostic emitted for unsupported
734        // members lists `builtin_library_supported_members`; everything the
735        // shipped sources declare should appear there.
736        for base in ["Runtime", "Storage", "Neo"] {
737            let path = devpack_library_path(base);
738            let source = std::fs::read_to_string(&path)
739                .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
740            let whitelist =
741                builtin_library_supported_members(base).expect("builtin library whitelist");
742            let missing: Vec<String> = declared_public_functions(&source)
743                .into_iter()
744                .filter(|name| !whitelist.contains(&name.as_str()))
745                .collect();
746            assert!(
747                missing.is_empty(),
748                "devpack/libraries/{base}.sol declares functions missing from \
749                 builtin_library_supported_members: {missing:?}"
750            );
751        }
752    }
753
754    #[test]
755    fn every_whitelisted_member_has_an_intrinsic_lowering() {
756        // The converse direction: the whitelist is what the compiler prints
757        // as "supported intrinsics", so a whitelisted member without a
758        // lowering would advertise an API that still hard-fails.
759        for base in ["Runtime", "Storage", "Neo"] {
760            let whitelist =
761                builtin_library_supported_members(base).expect("builtin library whitelist");
762            let missing: Vec<&&str> = whitelist
763                .iter()
764                .filter(|member| !has_lowering(base, member))
765                .collect();
766            assert!(
767                missing.is_empty(),
768                "builtin_library_supported_members(\"{base}\") lists members \
769                 with no compiler lowering: {missing:?}"
770            );
771        }
772    }
773
774    #[test]
775    fn fictional_local_storage_syscalls_are_not_resolvable() {
776        // Regression: `Storage.putLocal`/`getLocal`/`removeLocal`/`findLocal`
777        // and the matching `Syscalls.storage*Local` wrappers lowered to
778        // `System.Storage.Local.*` — syscalls that do not exist in Neo N3's
779        // interop table, so compiled contracts faulted on real nodes. They
780        // must stay unresolvable (callers now get the loud "unsupported
781        // builtin library call" diagnostic).
782        for member in ["putLocal", "getLocal", "removeLocal", "findLocal"] {
783            assert!(
784                resolve_storage_member(member).is_none(),
785                "Storage.{member} must not resolve to an intrinsic"
786            );
787            assert!(
788                !builtin_library_supported_members("Storage")
789                    .expect("Storage whitelist")
790                    .contains(&member),
791                "Storage.{member} must not be whitelisted"
792            );
793        }
794        for member in [
795            "storageGetLocal",
796            "storagePutLocal",
797            "storageDeleteLocal",
798            "storageFindLocal",
799        ] {
800            assert!(
801                resolve_syscalls_member(member).is_none(),
802                "Syscalls.{member} must not resolve to an intrinsic"
803            );
804            assert!(
805                !builtin_library_supported_members("Syscalls")
806                    .expect("Syscalls whitelist")
807                    .contains(&member),
808                "Syscalls.{member} must not be whitelisted"
809            );
810        }
811    }
812
813    #[test]
814    fn miscompiling_storage_helpers_are_not_resolvable() {
815        // Regression: these documented helpers used to lower to a single raw
816        // syscall with the wrong arity/semantics (e.g. `count` returned an
817        // iterator handle, `batchPut` issued one put with the arrays as
818        // key/value, `getUsage` was a hardcoded PUSH0 stub). They must stay
819        // unresolvable until faithful expansions exist.
820        for member in [
821            "batchPut",
822            "batchGet",
823            "batchDelete",
824            "count",
825            "countLocal",
826            "findValues",
827            "findLocalValues",
828            "findKeys",
829            "findLocalKeys",
830            "clearPrefix",
831            "exists",
832            "isValidKey",
833            "getUsage",
834        ] {
835            assert!(
836                resolve_storage_member(member).is_none(),
837                "Storage.{member} must not resolve to an intrinsic"
838            );
839            assert!(
840                !builtin_library_supported_members("Storage")
841                    .expect("Storage whitelist")
842                    .contains(&member),
843                "Storage.{member} must not be whitelisted"
844            );
845        }
846    }
847
848    #[test]
849    fn no_resolver_emits_syscalls_outside_the_neo_n3_interop_table() {
850        // Every BuiltinCall::Syscall name produced by the resolvers must be a
851        // real Neo N3 ApplicationEngine interop. Guards against reintroducing
852        // fictional names like System.Storage.Local.* or
853        // System.Storage.GetUsage (interop IDs are sha256-derived, so an
854        // unknown name compiles fine and FAULTs on real nodes).
855        const CANONICAL: &[&str] = &[
856            "System.Contract.Call",
857            "System.Contract.CallNative",
858            "System.Contract.CreateMultisigAccount",
859            "System.Contract.CreateStandardAccount",
860            "System.Contract.GetCallFlags",
861            "System.Contract.NativeOnPersist",
862            "System.Contract.NativePostPersist",
863            "System.Crypto.CheckMultisig",
864            "System.Crypto.CheckSig",
865            "System.Iterator.Next",
866            "System.Iterator.Value",
867            "System.Runtime.BurnGas",
868            "System.Runtime.CheckWitness",
869            "System.Runtime.CurrentSigners",
870            "System.Runtime.GasLeft",
871            "System.Runtime.GetAddressVersion",
872            "System.Runtime.GetCallingScriptHash",
873            "System.Runtime.GetEntryScriptHash",
874            "System.Runtime.GetExecutingScriptHash",
875            "System.Runtime.GetInvocationCounter",
876            "System.Runtime.GetNetwork",
877            "System.Runtime.GetNotifications",
878            "System.Runtime.GetRandom",
879            "System.Runtime.GetScriptContainer",
880            "System.Runtime.GetTime",
881            "System.Runtime.GetTrigger",
882            "System.Runtime.LoadScript",
883            "System.Runtime.Log",
884            "System.Runtime.Notify",
885            "System.Runtime.Platform",
886            "System.Storage.AsReadOnly",
887            "System.Storage.Delete",
888            "System.Storage.Find",
889            "System.Storage.Get",
890            "System.Storage.GetContext",
891            "System.Storage.GetReadOnlyContext",
892            "System.Storage.Put",
893        ];
894
895        let probe = |label: &str, builtin: Option<BuiltinCall>| {
896            if let Some(BuiltinCall::Syscall(name)) = builtin {
897                assert!(
898                    CANONICAL.contains(&name.as_str()),
899                    "{label} lowers to syscall '{name}' which is not in the \
900                     Neo N3 interop table"
901                );
902            }
903        };
904
905        for base in ["Runtime", "Storage", "Syscalls", "Neo"] {
906            let members = builtin_library_supported_members(base).expect("whitelist");
907            for member in members {
908                let builtin = match base {
909                    "Runtime" => resolve_runtime_member(member),
910                    "Storage" => resolve_storage_member(member),
911                    "Syscalls" => resolve_syscalls_member(member),
912                    "Neo" => resolve_neo_member(member),
913                    _ => unreachable!(),
914                };
915                probe(&format!("{base}.{member}"), builtin);
916            }
917        }
918    }
919}