From c580604280875e1cac3d088f8a59ba286322ecde Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:59:24 +0200 Subject: [PATCH 01/41] Add debugger infrastructure + CLI debugger --- Cargo.lock | 270 +++- README.md | 14 +- silverscript-lang/Cargo.toml | 10 +- silverscript-lang/src/ast.rs | 83 +- silverscript-lang/src/bin/common/mod.rs | 151 ++ silverscript-lang/src/bin/sil-debug.rs | 240 ++++ silverscript-lang/src/compiler.rs | 1260 +++++++++-------- .../src/compiler/debug_recording.rs | 352 +++++ silverscript-lang/src/debug.rs | 220 +++ silverscript-lang/src/debug/session.rs | 991 +++++++++++++ silverscript-lang/src/lib.rs | 1 + silverscript-lang/tests/compiler_tests.rs | 50 +- silverscript-lang/tests/date_literal_tests.rs | 8 +- .../tests/debug_session_tests.rs | 360 +++++ silverscript-lang/tests/debugger_cli_tests.rs | 50 + 15 files changed, 3401 insertions(+), 659 deletions(-) create mode 100644 silverscript-lang/src/bin/common/mod.rs create mode 100644 silverscript-lang/src/bin/sil-debug.rs create mode 100644 silverscript-lang/src/compiler/debug_recording.rs create mode 100644 silverscript-lang/src/debug.rs create mode 100644 silverscript-lang/src/debug/session.rs create mode 100644 silverscript-lang/tests/debug_session_tests.rs create mode 100644 silverscript-lang/tests/debugger_cli_tests.rs diff --git a/Cargo.lock b/Cargo.lock index e7b610eb..e336876e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -47,9 +53,9 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" dependencies = [ "rustversion", ] @@ -362,11 +368,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -397,6 +418,19 @@ dependencies = [ "windows-link", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -415,7 +449,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] @@ -483,6 +517,31 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -674,15 +733,15 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -694,6 +753,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "futures" version = "0.3.31" @@ -852,12 +917,29 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -873,6 +955,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexplay" version = "0.3.0" @@ -973,6 +1061,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1001,7 +1098,7 @@ dependencies = [ [[package]] name = "kaspa-addresses" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "borsh", "js-sys", @@ -1016,7 +1113,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-core" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "arc-swap", "async-trait", @@ -1053,7 +1150,7 @@ dependencies = [ [[package]] name = "kaspa-core" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "anyhow", "cfg-if", @@ -1084,7 +1181,7 @@ dependencies = [ [[package]] name = "kaspa-hashes" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "blake2b_simd", "blake3", @@ -1104,7 +1201,7 @@ dependencies = [ [[package]] name = "kaspa-math" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "borsh", "faster-hex", @@ -1124,7 +1221,7 @@ dependencies = [ [[package]] name = "kaspa-merkle" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "kaspa-hashes", ] @@ -1132,7 +1229,7 @@ dependencies = [ [[package]] name = "kaspa-muhash" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "kaspa-hashes", "kaspa-math", @@ -1143,7 +1240,7 @@ dependencies = [ [[package]] name = "kaspa-txscript" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "blake2b_simd", "borsh", @@ -1175,7 +1272,7 @@ dependencies = [ [[package]] name = "kaspa-txscript-errors" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "kaspa-hashes", "secp256k1", @@ -1185,7 +1282,7 @@ dependencies = [ [[package]] name = "kaspa-utils" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "arc-swap", "async-channel 2.5.0", @@ -1215,7 +1312,7 @@ dependencies = [ [[package]] name = "kaspa-wasm-core" version = "1.1.0-rc.2" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#d0c5196a906b921fffe2aacdc7c113b60ef153c6" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "faster-hex", "hexplay", @@ -1351,6 +1448,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "mac_address" version = "1.1.8" @@ -1409,6 +1515,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mock_instant" version = "0.6.0" @@ -1578,6 +1696,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pest" version = "2.8.5" @@ -1788,6 +1912,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.1.14", +] + [[package]] name = "rayon" version = "1.11.0" @@ -2062,6 +2206,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2078,6 +2233,9 @@ version = "0.1.0" dependencies = [ "blake2b_simd", "chrono", + "crossterm", + "hex", + "js-sys", "kaspa-addresses", "kaspa-consensus-core", "kaspa-txscript", @@ -2085,6 +2243,7 @@ dependencies = [ "pest", "pest_derive", "rand 0.8.5", + "ratatui", "secp256k1", "serde", "serde_json", @@ -2099,9 +2258,9 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2112,6 +2271,44 @@ dependencies = [ "serde", ] +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.114", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.114", +] + [[package]] name = "syn" version = "1.0.109" @@ -2346,6 +2543,23 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" @@ -3026,18 +3240,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.35" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.35" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", @@ -3046,6 +3260,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/README.md b/README.md index a0f2673d..ee887248 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,21 @@ This repository is a Rust workspace. The main crate is `silverscript-lang`. cargo test -p silverscript-lang ``` +## Debugger + +The workspace includes a source-level debugger for stepping through scripts: + +```bash +cargo run -p silverscript-lang --bin sil-debug -- \ + silverscript-lang/tests/examples/if_statement.sil \ + --function hello \ + --ctor-arg 3 --ctor-arg 10 \ + --arg 1 --arg 2 +``` + ## Layout -- `silverscript-lang/` – compiler, parser, and tests +- `silverscript-lang/` – compiler, parser, debugger, and tests - `silverscript-lang/tests/examples/` – example contracts (`.sil` files) ## Documentation diff --git a/silverscript-lang/Cargo.toml b/silverscript-lang/Cargo.toml index 4f944ee5..fbf6409d 100644 --- a/silverscript-lang/Cargo.toml +++ b/silverscript-lang/Cargo.toml @@ -23,8 +23,16 @@ rand.workspace = true secp256k1.workspace = true thiserror.workspace = true serde = { version = "1.0", features = ["derive"] } +hex = "0.4" serde_json = "1.0" +# Native-only dependencies (not compiled for wasm32) +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +ratatui = "0.26" +crossterm = "0.27" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3" + [dev-dependencies] kaspa-addresses.workspace = true - diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 67938827..71ff84b3 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -16,6 +16,22 @@ pub struct ContractAst { pub functions: Vec, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub struct SourceSpan { + pub line: u32, + pub col: u32, + pub end_line: u32, + pub end_col: u32, +} + +impl SourceSpan { + pub fn from_span(span: pest::Span<'_>) -> Self { + let (line, col) = span.start_pos().line_col(); + let (end_line, end_col) = span.end_pos().line_col(); + Self { line: line as u32, col: col as u32, end_line: end_line as u32, end_col: end_col as u32 } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionAst { pub name: String, @@ -33,9 +49,17 @@ pub struct ParamAst { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Statement { + #[serde(skip)] + pub span: Option, + #[serde(flatten)] + pub kind: StatementKind, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum Statement { +pub enum StatementKind { VariableDefinition { type_name: String, modifiers: Vec, name: String, expr: Option }, TupleAssignment { left_type: String, left_name: String, right_type: String, right_name: String, expr: Expr }, ArrayPush { name: String, expr: Expr }, @@ -269,14 +293,17 @@ fn parse_function_definition(pair: Pair<'_, Rule>) -> Result) -> Result { - match pair.as_rule() { - Rule::statement => { - if let Some(inner) = pair.into_inner().next() { - parse_statement(inner) - } else { - Err(CompilerError::Unsupported("empty statement".to_string())) - } - } + if pair.as_rule() == Rule::statement { + return if let Some(inner) = pair.into_inner().next() { + parse_statement(inner) + } else { + Err(CompilerError::Unsupported("empty statement".to_string())) + }; + } + + let span = Some(SourceSpan::from_span(pair.as_span())); + + let kind = match pair.as_rule() { Rule::variable_definition => { let mut inner = pair.into_inner(); let type_name = inner @@ -297,7 +324,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()))?; validate_user_identifier(ident.as_str())?; let expr = inner.next().map(parse_expression).transpose()?; - Ok(Statement::VariableDefinition { type_name, modifiers, name: ident.as_str().to_string(), expr }) + StatementKind::VariableDefinition { type_name, modifiers, name: ident.as_str().to_string(), expr } } Rule::tuple_assignment => { let mut inner = pair.into_inner(); @@ -312,27 +339,27 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::TupleAssignment { + StatementKind::TupleAssignment { left_type, left_name: left_ident.as_str().to_string(), right_type, right_name: right_ident.as_str().to_string(), expr, - }) + } } Rule::push_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::ArrayPush { name: ident.as_str().to_string(), expr }) + StatementKind::ArrayPush { name: ident.as_str().to_string(), expr } } Rule::assign_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::Assign { name: ident.as_str().to_string(), expr }) + StatementKind::Assign { name: ident.as_str().to_string(), expr } } Rule::time_op_statement => { let mut inner = pair.into_inner(); @@ -346,14 +373,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { "tx.time" => TimeVar::TxTime, other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), }; - Ok(Statement::TimeOp { tx_var, expr, message }) + StatementKind::TimeOp { tx_var, expr, message } } Rule::require_statement => { let mut inner = pair.into_inner(); let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; let message = inner.next().map(parse_require_message).transpose()?; let expr = parse_expression(expr_pair)?; - Ok(Statement::Require { expr, message }) + StatementKind::Require { expr, message } } Rule::if_statement => { let mut inner = pair.into_inner(); @@ -362,14 +389,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let then_block = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()))?; let then_branch = parse_block(then_block)?; let else_branch = inner.next().map(parse_block).transpose()?; - Ok(Statement::If { condition: cond_expr, then_branch, else_branch }) + StatementKind::If { condition: cond_expr, then_branch, else_branch } } Rule::call_statement => { let mut inner = pair.into_inner(); let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Ok(Statement::FunctionCall { name, args }), - _ => Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::function_call_assignment => { @@ -397,8 +424,8 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Ok(Statement::FunctionCallAssign { bindings, name, args }), - _ => Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::for_statement => { @@ -413,7 +440,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let end_expr = parse_expression(end_pair)?; let body = parse_block(block_pair)?; - Ok(Statement::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body }) + StatementKind::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } } Rule::yield_statement => { let mut inner = pair.into_inner(); @@ -422,7 +449,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.len() != 1 { return Err(CompilerError::Unsupported("yield() expects a single argument".to_string())); } - Ok(Statement::Yield { expr: args[0].clone() }) + StatementKind::Yield { expr: args[0].clone() } } Rule::return_statement => { let mut inner = pair.into_inner(); @@ -431,16 +458,18 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.is_empty() { return Err(CompilerError::Unsupported("return() expects at least one argument".to_string())); } - Ok(Statement::Return { exprs: args }) + StatementKind::Return { exprs: args } } Rule::console_statement => { let mut inner = pair.into_inner(); let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; let args = parse_console_parameter_list(list_pair)?; - Ok(Statement::Console { args }) + StatementKind::Console { args } } - _ => Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), - } + _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), + }; + + Ok(Statement { span, kind }) } fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { diff --git a/silverscript-lang/src/bin/common/mod.rs b/silverscript-lang/src/bin/common/mod.rs new file mode 100644 index 00000000..45447595 --- /dev/null +++ b/silverscript-lang/src/bin/common/mod.rs @@ -0,0 +1,151 @@ +#![allow(dead_code)] + +use std::env; +use std::error::Error; + +use silverscript_lang::ast::Expr; + +pub struct DebugCliArgs { + pub script_path: String, + pub without_selector: bool, + pub function_name: Option, + pub raw_ctor_args: Vec, + pub raw_args: Vec, +} + +pub fn print_usage(bin_name: &str) { + eprintln!( + "Usage: {bin_name} [--no-selector] [--function ] [--ctor-arg ...] [--arg ...]\n\n --ctor-arg is typed by the contract constructor params.\n --arg is typed by the selected function ABI.\n\nExamples:\n # constructor (int x, int y), function hello(int a, int b)\n {bin_name} if_statement.sil --function hello --ctor-arg 3 --ctor-arg 10 --arg 1 --arg 2\n\nValue formats:\n int: 123 (or 0x7b)\n bool: true|false\n string: hello (shell quoting handles spaces)\n bytes*: 0xdeadbeef\n" + ); +} + +pub fn parse_cli_args_or_help(bin_name: &str) -> Result, Box> { + parse_cli_args_or_help_from(bin_name, env::args().skip(1)) +} + +fn parse_cli_args_or_help_from( + bin_name: &str, + mut args: impl Iterator, +) -> Result, Box> { + let mut script_path: Option = None; + let mut without_selector = false; + let mut function_name: Option = None; + let mut raw_ctor_args: Vec = Vec::new(); + let mut raw_args: Vec = Vec::new(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "--no-selector" => without_selector = true, + "--function" | "-f" => { + function_name = args.next(); + if function_name.is_none() { + print_usage(bin_name); + return Err("missing function name".into()); + } + } + "--ctor-arg" => { + let value = args.next(); + if value.is_none() { + print_usage(bin_name); + return Err("missing --ctor-arg value".into()); + } + raw_ctor_args.push(value.expect("checked")); + } + "--arg" | "-a" => { + let value = args.next(); + if value.is_none() { + print_usage(bin_name); + return Err("missing --arg value".into()); + } + raw_args.push(value.expect("checked")); + } + "-h" | "--help" => { + print_usage(bin_name); + return Ok(None); + } + _ => { + if script_path.is_some() { + print_usage(bin_name); + return Err("unexpected extra argument".into()); + } + script_path = Some(arg); + } + } + } + + let script_path = match script_path { + Some(path) => path, + None => { + print_usage(bin_name); + return Err("missing contract path".into()); + } + }; + + Ok(Some(DebugCliArgs { script_path, without_selector, function_name, raw_ctor_args, raw_args })) +} + +fn parse_int_arg(raw: &str) -> Result> { + let cleaned = raw.replace('_', ""); + if let Some(hex) = cleaned.strip_prefix("0x").or_else(|| cleaned.strip_prefix("0X")) { + return Ok(i64::from_str_radix(hex, 16)?); + } + Ok(cleaned.parse::()?) +} + +fn parse_hex_bytes(raw: &str) -> Result, Box> { + let trimmed = raw.trim(); + let hex_str = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")).unwrap_or(trimmed); + if hex_str.is_empty() { + return Ok(vec![]); + } + // Allow odd length by implicitly left-padding with 0 + let normalized = if hex_str.len() % 2 != 0 { format!("0{hex_str}") } else { hex_str.to_string() }; + Ok(hex::decode(normalized)?) +} + +pub fn parse_typed_arg(type_name: &str, raw: &str) -> Result> { + // Support array inputs until the LSP exists by allowing: + // - JSON arrays: [1,2,3] or ["0x01","0x02"] + // - raw hex bytes: 0x... (treated as encoded array bytes) + if let Some(element_type) = type_name.strip_suffix("[]") { + let trimmed = raw.trim(); + if trimmed.starts_with('[') { + let values = serde_json::from_str::>(trimmed)?; + let mut out = Vec::with_capacity(values.len()); + for v in values { + let expr = match v { + serde_json::Value::Number(n) => Expr::Int(n.as_i64().ok_or("invalid int in array")?), + serde_json::Value::Bool(b) => Expr::Bool(b), + serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, + _ => return Err("unsupported array element (expected number/bool/string)".into()), + }; + out.push(expr); + } + return Ok(Expr::Array(out)); + } + // If not JSON, accept hex bytes for already-encoded arrays. + return Ok(Expr::Bytes(parse_hex_bytes(trimmed)?)); + } + + match type_name { + "int" => Ok(Expr::Int(parse_int_arg(raw)?)), + "bool" => match raw { + "true" => Ok(Expr::Bool(true)), + "false" => Ok(Expr::Bool(false)), + _ => Err(format!("invalid bool '{raw}' (expected true/false)").into()), + }, + "string" => Ok(Expr::String(raw.to_string())), + "bytes" | "byte" | "pubkey" | "sig" | "datasig" => Ok(Expr::Bytes(parse_hex_bytes(raw)?)), + other => { + if let Some(size) = other.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != size { + return Err(format!("{other} expects {size} bytes, got {}", bytes.len()).into()); + } + Ok(Expr::Bytes(bytes)) + } else { + Err(format!("unsupported arg type '{other}'").into()) + } + } + } +} diff --git a/silverscript-lang/src/bin/sil-debug.rs b/silverscript-lang/src/bin/sil-debug.rs new file mode 100644 index 00000000..b26b487d --- /dev/null +++ b/silverscript-lang/src/bin/sil-debug.rs @@ -0,0 +1,240 @@ +use std::fs; +use std::io::{self, BufRead, Write}; + +use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_txscript::caches::Cache; +use kaspa_txscript::{EngineCtx, EngineFlags}; + +use silverscript_lang::ast::parse_contract_ast; +use silverscript_lang::compiler::{CompileOptions, compile_contract}; +use silverscript_lang::debug::session::{DebugEngine, DebugSession}; + +mod common; + +const PROMPT: &str = "(sdb) "; + +fn show_stack(session: &DebugSession<'_>) { + println!("Stack:"); + let stack = session.stack(); + for (i, item) in stack.iter().enumerate().rev() { + println!("[{i}] {item}"); + } +} + +fn show_source_context(session: &DebugSession<'_>) { + let Some(context) = session.source_context() else { + println!("No source context available."); + return; + }; + + for line in context.lines { + let marker = if line.is_active { "→" } else { " " }; + println!("{marker} {:>4} | {}", line.line, line.text); + } +} + +fn show_vars(session: &DebugSession<'_>) { + match session.list_variables() { + Ok(variables) => { + if variables.is_empty() { + println!("No variables in scope."); + } else { + for var in variables { + let constant_suffix = if var.is_constant { " (const)" } else { "" }; + println!( + "{}{} ({}) = {}", + var.name, + constant_suffix, + var.type_name, + session.format_value(&var.type_name, &var.value) + ); + } + } + } + Err(err) => println!("ERROR: {err}"), + } +} + +fn show_step_view(session: &DebugSession<'_>) { + show_source_context(session); + show_vars(session); +} + +fn run_repl(session: &mut DebugSession<'_>) -> Result<(), kaspa_txscript_errors::TxScriptError> { + let stdin = io::stdin(); + loop { + print!("{PROMPT}"); + io::stdout().flush().ok(); + + let mut cmd = String::new(); + if stdin.lock().read_line(&mut cmd).is_err() { + println!("Failed to read input."); + continue; + } + + let cmd = cmd.trim(); + if cmd.is_empty() || cmd == "n" || cmd == "next" { + match session.step_over()? { + Some(_) => show_step_view(session), + None => { + println!("Done."); + break; + } + } + continue; + } + + let mut parts = cmd.split_whitespace(); + match parts.next().unwrap_or("") { + "step" | "s" => match session.step_into()? { + Some(_) => show_step_view(session), + None => { + println!("Done."); + break; + } + }, + "si" => match session.step_opcode()? { + Some(_) => show_step_view(session), + None => { + println!("Done."); + break; + } + }, + "finish" | "out" => match session.step_out()? { + Some(_) => show_step_view(session), + None => { + println!("Done."); + break; + } + }, + "c" | "continue" => match session.continue_to_breakpoint()? { + Some(_) => show_step_view(session), + None => { + println!("Done."); + break; + } + }, + "b" | "break" => { + if let Some(arg) = parts.next() { + match arg.parse::() { + Ok(line) => { + if session.add_breakpoint(line) { + println!("Breakpoint set at line {line}"); + } else { + println!("Warning: no statement at line {line}, breakpoint not set"); + } + } + Err(_) => println!("Invalid line number."), + } + } else { + let lines = session.breakpoints(); + if lines.is_empty() { + println!("No breakpoints set."); + } else { + println!("Breakpoints: {}", lines.iter().map(|line| line.to_string()).collect::>().join(", ")); + } + } + } + "l" | "list" => show_source_context(session), + "vars" => show_vars(session), + "print" | "p" => { + if let Some(name) = parts.next() { + match session.variable_by_name(name) { + Ok(var) => { + let constant_suffix = if var.is_constant { " (const)" } else { "" }; + println!( + "{}{} ({}) = {}", + var.name, + constant_suffix, + var.type_name, + session.format_value(&var.type_name, &var.value) + ); + } + Err(err) => println!("ERROR: {err}"), + } + } else { + println!("Usage: print "); + } + } + "stack" => show_stack(session), + "q" | "quit" => break, + "help" | "h" | "?" => { + println!( + "Commands: next/over (n), step/into (s), step opcode (si), finish/out, continue (c), break (b ), list (l), vars, print , stack, quit (q)" + ) + } + _ => println!( + "Commands: next/over (n), step/into (s), step opcode (si), finish/out, continue (c), break (b ), list (l), vars, print , stack, quit (q)" + ), + } + } + Ok(()) +} + +fn main() -> Result<(), Box> { + let Some(cli) = common::parse_cli_args_or_help("sil-debug")? else { + return Ok(()); + }; + let script_path = cli.script_path; + let without_selector = cli.without_selector; + let function_name = cli.function_name; + let raw_ctor_args = cli.raw_ctor_args; + let raw_args = cli.raw_args; + + let source = fs::read_to_string(&script_path)?; + let parsed_contract = parse_contract_ast(&source)?; + + let entrypoint_count = parsed_contract.functions.iter().filter(|func| func.entrypoint).count(); + if without_selector && entrypoint_count != 1 { + return Err("--no-selector requires exactly one entrypoint function".into()); + } + + if parsed_contract.params.len() != raw_ctor_args.len() { + return Err(format!("constructor expects {} arguments, got {}", parsed_contract.params.len(), raw_ctor_args.len()).into()); + } + + let mut ctor_args = Vec::with_capacity(raw_ctor_args.len()); + for (param, raw) in parsed_contract.params.iter().zip(raw_ctor_args.iter()) { + ctor_args.push(common::parse_typed_arg(¶m.type_name, raw)?); + } + + let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(&source, &ctor_args, compile_opts)?; + let debug_info = compiled.debug_info.clone(); + + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); + + let flags = EngineFlags { covenants_enabled: true }; + let engine = DebugEngine::new(ctx, flags); + + // Seed the stack like a real spend: run sigscript pushes before locking script. + let default_name = compiled.abi.first().map(|entry| entry.name.clone()).ok_or("contract has no functions")?; + let selected_name = function_name.unwrap_or(default_name); + let entry = compiled + .abi + .iter() + .find(|entry| entry.name == selected_name) + .ok_or_else(|| format!("function '{selected_name}' not found"))?; + + if entry.inputs.len() != raw_args.len() { + return Err(format!("function '{selected_name}' expects {} arguments, got {}", entry.inputs.len(), raw_args.len()).into()); + } + + let mut typed_args = Vec::with_capacity(raw_args.len()); + for (input, raw) in entry.inputs.iter().zip(raw_args.iter()) { + typed_args.push(common::parse_typed_arg(&input.type_name, raw)?); + } + + // Always seed: even in --no-selector mode the function params must be pushed. + let sigscript = compiled.build_sig_script(&selected_name, typed_args)?; + let mut session = DebugSession::full(&sigscript, &compiled.script, &source, debug_info, engine)?; + + println!("Stepping through {} bytes of script", compiled.script.len()); + session.run_to_first_executed_statement()?; + show_source_context(&session); + run_repl(&mut session)?; + + Ok(()) +} diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index cce0d063..3a16c437 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,11 +6,18 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - BinaryOp, ContractAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, Statement, TimeVar, UnaryOp, parse_contract_ast, + BinaryOp, ConsoleArg, ContractAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, SplitPart, Statement, + StatementKind, TimeVar, UnaryOp, parse_contract_ast, }; +use crate::debug::DebugInfo; +use crate::debug::labels::synthetic; use crate::parser::Rule; use chrono::NaiveDateTime; +mod debug_recording; + +use debug_recording::{DebugSink, FunctionDebugRecorder, record_synthetic_range}; + #[derive(Debug, Error)] pub enum CompilerError { #[error("parse error: {0}")] @@ -31,6 +38,7 @@ pub enum CompilerError { pub struct CompileOptions { pub allow_yield: bool, pub allow_entrypoint_return: bool, + pub record_debug_infos: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -54,17 +62,27 @@ pub struct CompiledContract { pub ast: ContractAst, pub abi: FunctionAbi, pub without_selector: bool, + pub debug_info: Option, } pub fn compile_contract(source: &str, constructor_args: &[Expr], options: CompileOptions) -> Result { let contract = parse_contract_ast(source)?; - compile_contract_ast(&contract, constructor_args, options) + compile_contract_impl(&contract, constructor_args, options, Some(source)) } pub fn compile_contract_ast( contract: &ContractAst, constructor_args: &[Expr], options: CompileOptions, +) -> Result { + compile_contract_impl(contract, constructor_args, options, None) +} + +fn compile_contract_impl( + contract: &ContractAst, + constructor_args: &[Expr], + options: CompileOptions, + source: Option<&str>, ) -> Result { if contract.functions.is_empty() { return Err(CompilerError::Unsupported("contract has no functions".to_string())); @@ -97,10 +115,14 @@ pub fn compile_contract_ast( contract.functions.iter().enumerate().map(|(index, func)| (func.name.clone(), index)).collect::>(); let abi = build_function_abi(contract); let uses_script_size = contract_uses_script_size(contract); - let mut script_size = if uses_script_size { Some(100i64) } else { None }; + for _ in 0..32 { let mut compiled_entrypoints = Vec::new(); + // Create a recorder (active/non-active based on compilation options) to collect debug info + let mut recorder = DebugSink::new(options.record_debug_infos); + recorder.record_constructor_constants(&contract.params, constructor_args); + for (index, func) in contract.functions.iter().enumerate() { if func.entrypoint { compiled_entrypoints.push(compile_function( @@ -116,56 +138,72 @@ pub fn compile_contract_ast( } let script = if without_selector { - compiled_entrypoints + let compiled = compiled_entrypoints .first() - .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))? - .1 - .clone() + .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, 0); + compiled.script.clone() } else { let mut builder = ScriptBuilder::new(); let total = compiled_entrypoints.len(); - for (index, (_, script)) in compiled_entrypoints.iter().enumerate() { - builder.add_op(OpDup)?; - builder.add_i64(index as i64)?; - builder.add_op(OpNumEqual)?; - builder.add_op(OpIf)?; - builder.add_op(OpDrop)?; - builder.add_ops(script)?; - if index == total - 1 { - builder.add_op(OpElse)?; + + for (index, compiled) in compiled_entrypoints.iter().enumerate() { + record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_GUARD, |builder| { + builder.add_op(OpDup)?; + builder.add_i64(index as i64)?; + builder.add_op(OpNumEqual)?; + builder.add_op(OpIf)?; builder.add_op(OpDrop)?; - builder.add_op(OpFalse)?; - builder.add_op(OpVerify)?; - } else { + Ok(()) + })?; + + let func_start = builder.script().len(); + builder.add_ops(&compiled.script)?; + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, func_start); + + record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ELSE, |builder| { builder.add_op(OpElse)?; - } + if index == total - 1 { + builder.add_op(OpDrop)?; + builder.add_op(OpFalse)?; + builder.add_op(OpVerify)?; + } + Ok(()) + })?; } - for _ in 0..total { - builder.add_op(OpEndIf)?; - } + record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ENDIFS, |builder| { + for _ in 0..total { + builder.add_op(OpEndIf)?; + } + Ok(()) + })?; builder.drain() }; if !uses_script_size { + let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); return Ok(CompiledContract { contract_name: contract.name.clone(), script, ast: contract.clone(), abi, without_selector, + debug_info, }); } let actual_size = script.len() as i64; if Some(actual_size) == script_size { + let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); return Ok(CompiledContract { contract_name: contract.name.clone(), script, ast: contract.clone(), abi, without_selector, + debug_info, }); } script_size = Some(actual_size); @@ -174,6 +212,13 @@ pub fn compile_contract_ast( Err(CompilerError::Unsupported("script size did not stabilize".to_string())) } +#[derive(Debug)] +struct CompiledFunction { + name: String, + script: Vec, + debug: FunctionDebugRecorder, +} + fn contract_uses_script_size(contract: &ContractAst) -> bool { if contract.constants.values().any(expr_uses_script_size) { return true; @@ -182,52 +227,48 @@ fn contract_uses_script_size(contract: &ContractAst) -> bool { } fn statement_uses_script_size(stmt: &Statement) -> bool { - match stmt { - Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), - Statement::FunctionCall { args, .. } => args.iter().any(expr_uses_script_size), - Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - Statement::Assign { expr, .. } => expr_uses_script_size(expr), - Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), - Statement::Require { expr, .. } => expr_uses_script_size(expr), - Statement::If { condition, then_branch, else_branch } => { + match &stmt.kind { + StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), + StatementKind::FunctionCall { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), + StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), + StatementKind::Require { expr, .. } => expr_uses_script_size(expr), + StatementKind::If { condition, then_branch, else_branch, .. } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - Statement::For { start, end, body, .. } => { + StatementKind::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - Statement::Yield { expr } => expr_uses_script_size(expr), - Statement::Return { exprs } => exprs.iter().any(expr_uses_script_size), - Statement::Console { args } => args.iter().any(|arg| match arg { - crate::ast::ConsoleArg::Identifier(_) => false, - crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), - }), + StatementKind::Yield { expr, .. } => expr_uses_script_size(expr), + StatementKind::Return { exprs, .. } => exprs.iter().any(expr_uses_script_size), + StatementKind::Console { args, .. } => { + args.iter().any(|arg| matches!(arg, ConsoleArg::Literal(e) if expr_uses_script_size(e))) + } } } fn expr_uses_script_size(expr: &Expr) -> bool { match expr { - Expr::Nullary(NullaryOp::ThisScriptSize) => true, - Expr::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, - Expr::Unary { expr, .. } => expr_uses_script_size(expr), - Expr::Binary { left, right, .. } => expr_uses_script_size(left) || expr_uses_script_size(right), - Expr::IfElse { condition, then_expr, else_expr } => { - expr_uses_script_size(condition) || expr_uses_script_size(then_expr) || expr_uses_script_size(else_expr) - } - Expr::Array(values) => values.iter().any(expr_uses_script_size), - Expr::Call { args, .. } => args.iter().any(expr_uses_script_size), - Expr::New { args, .. } => args.iter().any(expr_uses_script_size), + Expr::Int(_) | Expr::Bool(_) | Expr::Bytes(_) | Expr::String(_) | Expr::Identifier(_) => false, + Expr::Array(items) => items.iter().any(expr_uses_script_size), + Expr::Call { args, .. } | Expr::New { args, .. } => args.iter().any(expr_uses_script_size), Expr::Split { source, index, .. } => expr_uses_script_size(source) || expr_uses_script_size(index), Expr::Slice { source, start, end } => { expr_uses_script_size(source) || expr_uses_script_size(start) || expr_uses_script_size(end) } Expr::ArrayIndex { source, index } => expr_uses_script_size(source) || expr_uses_script_size(index), + Expr::Unary { expr, .. } => expr_uses_script_size(expr), + Expr::Binary { left, right, .. } => expr_uses_script_size(left) || expr_uses_script_size(right), + Expr::IfElse { condition, then_expr, else_expr } => { + expr_uses_script_size(condition) || expr_uses_script_size(then_expr) || expr_uses_script_size(else_expr) + } + Expr::Nullary(op) => matches!(op, NullaryOp::ThisScriptSize | NullaryOp::ThisScriptSizeDataPrefix), Expr::Introspection { index, .. } => expr_uses_script_size(index), - Expr::Int(_) | Expr::Bool(_) | Expr::Bytes(_) | Expr::String(_) | Expr::Identifier(_) => false, - Expr::Nullary(_) => false, } } @@ -307,23 +348,23 @@ fn array_element_size(type_name: &str) -> Option { } fn contains_return(stmt: &Statement) -> bool { - match stmt { - Statement::Return { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + match &stmt.kind { + StatementKind::Return { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - Statement::For { body, .. } => body.iter().any(contains_return), + StatementKind::For { body, .. } => body.iter().any(contains_return), _ => false, } } fn contains_yield(stmt: &Statement) -> bool { - match stmt { - Statement::Yield { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + match &stmt.kind { + StatementKind::Yield { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - Statement::For { body, .. } => body.iter().any(contains_yield), + StatementKind::For { body, .. } => body.iter().any(contains_yield), _ => false, } } @@ -494,29 +535,28 @@ fn compile_function( functions: &HashMap, function_order: &HashMap, script_size: Option, -) -> Result<(String, Vec), CompilerError> { +) -> Result { + let mut builder = ScriptBuilder::new(); + let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function); + + let mut env = constants.clone(); + let mut types = HashMap::new(); + let mut params = HashMap::new(); + let param_count = function.params.len(); - let params = function - .params - .iter() - .map(|param| param.name.clone()) - .enumerate() - .map(|(index, name)| (name, (param_count - 1 - index) as i64)) - .collect::>(); - let mut types = function.params.iter().map(|param| (param.name.clone(), param.type_name.clone())).collect::>(); - for param in &function.params { + for (index, param) in function.params.iter().enumerate() { if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); } + params.insert(param.name.clone(), (param_count - 1 - index) as i64); + types.insert(param.name.clone(), param.type_name.clone()); } + for return_type in &function.return_types { if is_array_type(return_type) && array_element_size(return_type).is_none() { return Err(CompilerError::Unsupported(format!("array element type must have known size: {return_type}"))); } } - let mut env: HashMap = constants.clone(); - let mut builder = ScriptBuilder::new(); - let mut yields: Vec = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); @@ -528,7 +568,7 @@ fn compile_function( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement::Return { .. })) { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -537,454 +577,540 @@ fn compile_function( if function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); } - if function.return_types.is_empty() { - return Err(CompilerError::Unsupported("return requires function return types".to_string())); - } } - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - if matches!(stmt, Statement::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let Statement::Return { exprs } = stmt else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); - } - continue; - } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - &mut builder, + let mut yields: Vec = Vec::new(); + { + let mut body_compiler = FunctionBodyCompiler { + builder: &mut builder, options, - constants, + debug_recorder: &mut recorder, + contract_constants: constants, functions, function_order, function_index, - &mut yields, script_size, - )?; - } + inline_frame_counter: 1, + }; + for stmt in &function.body { + if matches!(stmt.kind, StatementKind::Return { .. }) { + let StatementKind::Return { exprs, .. } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + continue; + } - let yield_count = yields.len(); - if yield_count == 0 { - for _ in 0..param_count { - builder.add_op(OpDrop)?; + body_compiler.compile_statement(stmt, &mut env, ¶ms, &mut types, &mut yields)?; } - builder.add_op(OpTrue)?; - } else { - let mut stack_depth = 0i64; - for expr in &yields { - compile_expr(expr, &env, ¶ms, &types, &mut builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + } + + if function.entrypoint { + if !has_return && !function.return_types.is_empty() { + return Err(CompilerError::Unsupported("entrypoint function must not have return types unless it returns".to_string())); } - for _ in 0..param_count { - builder.add_i64(yield_count as i64)?; - builder.add_op(OpRoll)?; - builder.add_op(OpDrop)?; + let yield_count = yields.len(); + if yield_count == 0 { + for _ in 0..param_count { + builder.add_op(OpDrop)?; + } + builder.add_op(OpTrue)?; + } else { + let mut stack_depth = 0i64; + for expr in &yields { + compile_expr(expr, &env, ¶ms, &types, &mut builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + } + for _ in 0..param_count { + builder.add_i64(yield_count as i64)?; + builder.add_op(OpRoll)?; + builder.add_op(OpDrop)?; + } } } - Ok((function.name.clone(), builder.drain())) -} -#[allow(clippy::too_many_arguments)] -fn compile_statement( - stmt: &Statement, - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - builder: &mut ScriptBuilder, + Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) +} +struct FunctionBodyCompiler<'a> { + builder: &'a mut ScriptBuilder, options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, + debug_recorder: &'a mut FunctionDebugRecorder, + contract_constants: &'a HashMap, + functions: &'a HashMap, + function_order: &'a HashMap, function_index: usize, - yields: &mut Vec, script_size: Option, -) -> Result<(), CompilerError> { - match stmt { - Statement::VariableDefinition { type_name, name, expr, .. } => { - if is_array_type(type_name) { - if array_element_size(type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {type_name}"))); - } - let initial = match expr { - Some(Expr::Identifier(other)) => match types.get(other) { - Some(other_type) if other_type == type_name => Expr::Identifier(other.clone()), - Some(_) => { - return Err(CompilerError::Unsupported("array assignment requires compatible array types".to_string())); - } - None => return Err(CompilerError::UndefinedIdentifier(other.clone())), - }, - Some(_) => return Err(CompilerError::Unsupported("array initializer must be another array".to_string())), - None => Expr::Bytes(Vec::new()), - }; - env.insert(name.clone(), initial); - types.insert(name.clone(), type_name.clone()); - Ok(()) - } else { - let expr = - expr.clone().ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; - env.insert(name.clone(), expr); - types.insert(name.clone(), type_name.clone()); - Ok(()) - } - } - Statement::ArrayPush { name, expr } => { - let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; - if !is_array_type(array_type) { - return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); - } - let element_type = array_element_type(array_type) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - let element_size = array_element_size(array_type) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - let element_expr = if element_type == "int" { - Expr::Call { name: "bytes8".to_string(), args: vec![expr.clone()] } - } else if element_type == "byte" { - Expr::Call { name: "bytes1".to_string(), args: vec![expr.clone()] } - } else if element_type.starts_with("bytes") { - if expr_is_bytes(expr, env, types) { - expr.clone() + inline_frame_counter: u32, +} + +impl<'a> FunctionBodyCompiler<'a> { + fn compile_statement( + &mut self, + stmt: &Statement, + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + yields: &mut Vec, + ) -> Result<(), CompilerError> { + let start = self.builder.script().len(); + let mut variables = Vec::new(); + + match &stmt.kind { + StatementKind::VariableDefinition { type_name, name, expr, .. } => { + if is_array_type(type_name) { + if array_element_size(type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {type_name}"))); + } + let initial = match expr { + Some(Expr::Identifier(other)) => match types.get(other) { + Some(other_type) if other_type == type_name => Expr::Identifier(other.clone()), + Some(_) => { + return Err(CompilerError::Unsupported( + "array assignment requires compatible array types".to_string(), + )); + } + None => return Err(CompilerError::UndefinedIdentifier(other.clone())), + }, + Some(_) => return Err(CompilerError::Unsupported("array initializer must be another array".to_string())), + None => Expr::Bytes(Vec::new()), + }; + self.debug_recorder.variable_update(env, &mut variables, name, type_name, initial.clone())?; + env.insert(name.clone(), initial); + types.insert(name.clone(), type_name.clone()); } else { - Expr::Call { name: format!("bytes{element_size}"), args: vec![expr.clone()] } + let expr = expr + .clone() + .ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; + self.debug_recorder.variable_update(env, &mut variables, name, type_name, expr.clone())?; + env.insert(name.clone(), expr); + types.insert(name.clone(), type_name.clone()); } - } else { - return Err(CompilerError::Unsupported("array element type not supported".to_string())); - }; - - let current = env.get(name).cloned().unwrap_or_else(|| Expr::Bytes(Vec::new())); - let updated = Expr::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }; - env.insert(name.clone(), updated); - Ok(()) - } - Statement::Require { expr, .. } => { - let mut stack_depth = 0i64; - compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; - builder.add_op(OpVerify)?; - Ok(()) - } - Statement::TimeOp { tx_var, expr, .. } => { - compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size) - } - Statement::If { condition, then_branch, else_branch } => compile_if_statement( - condition, - then_branch, - else_branch.as_deref(), - env, - params, - types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - ), - Statement::For { ident, start, end, body } => compile_for_statement( - ident, - start, - end, - body, - env, - params, - types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - ), - Statement::Yield { expr } => { - let mut visiting = HashSet::new(); - let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; - yields.push(resolved); - Ok(()) - } - Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), - Statement::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { - Expr::Split { source, index, .. } => { - env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); - env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); - Ok(()) } - _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), - }, - Statement::FunctionCall { name, args } => { - let returns = compile_inline_call( - name, - args, - types, - env, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - script_size, - )?; - if !returns.is_empty() { - let mut stack_depth = 0i64; - for expr in returns { - compile_expr(&expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; - builder.add_op(OpDrop)?; - stack_depth -= 1; + StatementKind::ArrayPush { name, expr, .. } => { + let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; + if !is_array_type(array_type) { + return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); } + let element_type = array_element_type(array_type) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let element_size = array_element_size(array_type) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let element_expr = if element_type == "int" { + Expr::Call { name: "bytes8".to_string(), args: vec![expr.clone()] } + } else if element_type == "byte" { + Expr::Call { name: "bytes1".to_string(), args: vec![expr.clone()] } + } else if element_type.starts_with("bytes") { + if expr_is_bytes(expr, env, types) { + expr.clone() + } else { + Expr::Call { name: format!("bytes{element_size}"), args: vec![expr.clone()] } + } + } else { + return Err(CompilerError::Unsupported("array element type not supported".to_string())); + }; + + let current = env.get(name).cloned().unwrap_or_else(|| Expr::Bytes(Vec::new())); + let updated = Expr::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }; + self.debug_recorder.variable_update(env, &mut variables, name, array_type, updated.clone())?; + env.insert(name.clone(), updated); } - Ok(()) - } - Statement::FunctionCallAssign { bindings, name, args } => { - let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if function.return_types.is_empty() { - return Err(CompilerError::Unsupported("function has no return types".to_string())); + StatementKind::Require { expr, .. } => { + let mut stack_depth = 0i64; + compile_expr( + expr, + env, + params, + types, + self.builder, + self.options, + &mut HashSet::new(), + &mut stack_depth, + self.script_size, + )?; + self.builder.add_op(OpVerify)?; + } + StatementKind::TimeOp { tx_var, expr, .. } => { + compile_time_op_statement(tx_var, expr, env, params, types, self.builder, self.options, self.script_size)?; + } + StatementKind::If { condition, then_branch, else_branch, .. } => { + self.compile_if_statement(condition, then_branch, else_branch.as_deref(), env, params, types, yields)?; + } + StatementKind::For { ident, start, end, body, .. } => { + self.compile_for_statement(ident, start, end, body, env, params, types, yields, stmt.span)?; + } + StatementKind::Yield { expr, .. } => { + let mut visiting = HashSet::new(); + let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; + yields.push(resolved); } - if function.return_types.len() != bindings.len() { - return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); + StatementKind::Return { .. } => { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - for (binding, return_type) in bindings.iter().zip(function.return_types.iter()) { - if binding.type_name != *return_type { - return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); + StatementKind::TupleAssignment { left_type, left_name, right_type, right_name, expr, .. } => match expr.clone() { + Expr::Split { source, index, .. } => { + let left_expr = Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }; + let right_expr = Expr::Split { source, index, part: SplitPart::Right }; + self.debug_recorder.variable_update(env, &mut variables, left_name, left_type, left_expr.clone())?; + self.debug_recorder.variable_update(env, &mut variables, right_name, right_type, right_expr.clone())?; + env.insert(left_name.clone(), left_expr); + env.insert(right_name.clone(), right_expr); + } + _ => return Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), + }, + StatementKind::FunctionCall { name, args, .. } => { + let returns = self.compile_inline_call(name, args, params, types, env, stmt.span)?; + if !returns.is_empty() { + let mut stack_depth = 0i64; + for expr in returns { + compile_expr( + &expr, + env, + params, + types, + self.builder, + self.options, + &mut HashSet::new(), + &mut stack_depth, + self.script_size, + )?; + self.builder.add_op(OpDrop)?; + stack_depth -= 1; + } } } - let returns = compile_inline_call( - name, - args, - types, - env, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - script_size, - )?; - if returns.len() != bindings.len() { - return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); - } - for (binding, expr) in bindings.iter().zip(returns.into_iter()) { - env.insert(binding.name.clone(), expr); - types.insert(binding.name.clone(), binding.type_name.clone()); + StatementKind::FunctionCallAssign { bindings, name, args, .. } => { + let return_types = { + let function = self + .functions + .get(name) + .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if function.return_types.is_empty() { + return Err(CompilerError::Unsupported("function has no return types".to_string())); + } + if function.return_types.len() != bindings.len() { + return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); + } + for (binding, return_type) in bindings.iter().zip(function.return_types.iter()) { + if binding.type_name != *return_type { + return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); + } + } + function.return_types.clone() + }; + let returns = self.compile_inline_call(name, args, params, types, env, stmt.span)?; + if returns.len() != return_types.len() { + return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); + } + for (binding, expr) in bindings.iter().zip(returns.into_iter()) { + if self.options.record_debug_infos { + let resolved = resolve_expr_for_debug(expr.clone(), env, &mut HashSet::new())?; + variables.push((binding.name.clone(), binding.type_name.clone(), resolved)); + } + env.insert(binding.name.clone(), expr); + types.insert(binding.name.clone(), binding.type_name.clone()); + } } - Ok(()) - } - Statement::Assign { name, expr } => { - if let Some(type_name) = types.get(name) { - if is_array_type(type_name) { - match expr { - Expr::Identifier(other) => match types.get(other) { - Some(other_type) if other_type == type_name => { - env.insert(name.clone(), Expr::Identifier(other.clone())); - return Ok(()); - } - Some(_) => { + StatementKind::Assign { name, expr, .. } => { + if let Some(type_name) = types.get(name) { + if is_array_type(type_name) { + match expr { + Expr::Identifier(other) => match types.get(other) { + Some(other_type) if other_type == type_name => { + self.debug_recorder.variable_update( + env, + &mut variables, + name, + type_name, + Expr::Identifier(other.clone()), + )?; + env.insert(name.clone(), Expr::Identifier(other.clone())); + } + Some(_) => { + return Err(CompilerError::Unsupported( + "array assignment requires compatible array types".to_string(), + )); + } + None => return Err(CompilerError::UndefinedIdentifier(other.clone())), + }, + _ => { return Err(CompilerError::Unsupported( - "array assignment requires compatible array types".to_string(), + "array assignment only supports array identifiers".to_string(), )); } - None => return Err(CompilerError::UndefinedIdentifier(other.clone())), - }, - _ => return Err(CompilerError::Unsupported("array assignment only supports array identifiers".to_string())), + } + } else { + let updated = + if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; + let resolved = resolve_expr(updated, env, &mut HashSet::new())?; + self.debug_recorder.variable_update(env, &mut variables, name, type_name, resolved.clone())?; + env.insert(name.clone(), resolved); } + } else { + let updated = + if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; + let resolved = resolve_expr(updated, env, &mut HashSet::new())?; + let type_name = "unknown"; + self.debug_recorder.variable_update(env, &mut variables, name, type_name, resolved.clone())?; + env.insert(name.clone(), resolved); } } - let updated = if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; - let resolved = resolve_expr(updated, env, &mut HashSet::new())?; - env.insert(name.clone(), resolved); - Ok(()) + StatementKind::Console { .. } => {} } - Statement::Console { .. } => Ok(()), - } -} - -#[allow(clippy::too_many_arguments)] -fn compile_inline_call( - name: &str, - args: &[Expr], - caller_types: &mut HashMap, - caller_env: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - caller_index: usize, - script_size: Option, -) -> Result, CompilerError> { - let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - let callee_index = - function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= caller_index { - return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); - } - if function.params.len() != args.len() { - return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); - } - for (param, arg) in function.params.iter().zip(args.iter()) { - if !expr_matches_type_with_env(arg, ¶m.type_name, caller_types) { - return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param.type_name))); + let end = self.builder.script().len(); + let stmt_seq = self.debug_recorder.record_statement(stmt, start, end - start); + // Record updates at the end of the statement so variables reflect post-statement state + // when the debugger is paused at the next byte offset. + if let Some(sequence) = stmt_seq { + self.debug_recorder.record_variable_updates(variables, end, stmt.span, sequence); } + Ok(()) } - let mut types = function.params.iter().map(|param| (param.name.clone(), param.type_name.clone())).collect::>(); - for param in &function.params { - if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); + fn compile_inline_call( + &mut self, + name: &str, + args: &[Expr], + caller_params: &HashMap, + caller_types: &mut HashMap, + caller_env: &mut HashMap, + call_span: Option, + ) -> Result, CompilerError> { + let function = self.functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let callee_index = self + .function_order + .get(name) + .copied() + .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= self.function_index { + return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); } - } - let mut env: HashMap = contract_constants.clone(); - for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { - let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let temp_name = format!("__arg_{name}_{index}"); - env.insert(temp_name.clone(), resolved.clone()); - types.insert(temp_name.clone(), param.type_name.clone()); - env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); - caller_env.insert(temp_name.clone(), resolved); - caller_types.insert(temp_name, param.type_name.clone()); - } - - if !options.allow_yield && function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); - } + if function.params.len() != args.len() { + return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); + } + for (param, arg) in function.params.iter().zip(args.iter()) { + if !expr_matches_type_with_env(arg, ¶m.type_name, caller_types) { + return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param.type_name))); + } + } - if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { - return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); - } + let mut types = function.params.iter().map(|param| (param.name.clone(), param.type_name.clone())).collect::>(); + for param in &function.params { + if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); + } + } - let has_return = function.body.iter().any(contains_return); - if has_return { - if !matches!(function.body.last(), Some(Statement::Return { .. })) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + let mut env: HashMap = self.contract_constants.clone(); + for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { + let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; + let temp_name = format!("__arg_{name}_{index}"); + env.insert(temp_name.clone(), resolved.clone()); + types.insert(temp_name.clone(), param.type_name.clone()); + env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); + caller_env.insert(temp_name.clone(), resolved); + caller_types.insert(temp_name, param.type_name.clone()); } - if function.body[..function.body.len() - 1].iter().any(contains_return) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + + if !self.options.allow_yield && function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); } - if function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + + if function.entrypoint && !self.options.allow_entrypoint_return && function.body.iter().any(contains_return) { + return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); } - } - let mut yields: Vec = Vec::new(); - let params = HashMap::new(); - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - if matches!(stmt, Statement::Return { .. }) { - if index != body_len - 1 { + let has_return = function.body.iter().any(contains_return); + if has_return { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - let Statement::Return { exprs } = stmt else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); + if function.body[..function.body.len() - 1].iter().any(contains_return) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + } + } + + let mut yields: Vec = Vec::new(); + let params = caller_params.clone(); + let call_offset = self.builder.script().len(); + self.debug_recorder.record_inline_call_enter(call_span, call_offset, name); + + // Compile callee statements using an isolated inline debug recorder so emitted + // events/variable updates carry the callee frame id and call depth. + let frame_id = self.inline_frame_counter; + self.inline_frame_counter = self.inline_frame_counter.saturating_add(1); + let mut debug_recorder = FunctionDebugRecorder::inline( + self.debug_recorder.enabled, + self.debug_recorder.function_name.clone(), + self.debug_recorder.call_depth().saturating_add(1), + frame_id, + ); + // Inline params are not stack-mapped like normal function params; materialize + // them as variable updates at the inline entry virtual step. + debug_recorder.record_inline_param_updates(function, &env, call_span, call_offset)?; + let mut callee_compiler = FunctionBodyCompiler { + builder: &mut *self.builder, + options: self.options, + debug_recorder: &mut debug_recorder, + contract_constants: self.contract_constants, + functions: self.functions, + function_order: self.function_order, + function_index: callee_index, + script_size: self.script_size, + inline_frame_counter: self.inline_frame_counter, + }; + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + if matches!(stmt.kind, StatementKind::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let StatementKind::Return { exprs, .. } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + continue; } - continue; + callee_compiler.compile_statement(stmt, &mut env, ¶ms, &mut types, &mut yields)?; } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - builder, - options, - contract_constants, - functions, - function_order, - callee_index, - &mut yields, - script_size, - )?; - } + self.inline_frame_counter = callee_compiler.inline_frame_counter; + drop(callee_compiler); + // Remap inline-local sequence numbers and merge events/updates back into + // the parent function recorder. + self.debug_recorder.merge_inline_events(&debug_recorder); + self.debug_recorder.record_inline_call_exit(call_span, self.builder.script().len(), name); - for (name, value) in env.iter() { - if name.starts_with("__arg_") { - if let Some(type_name) = types.get(name) { - caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + for (name, value) in &env { + if name.starts_with("__arg_") { + if let Some(type_name) = types.get(name) { + caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + caller_env.entry(name.clone()).or_insert_with(|| value.clone()); } - caller_env.entry(name.clone()).or_insert_with(|| value.clone()); } - } - Ok(yields) -} + Ok(yields) + } -#[allow(clippy::too_many_arguments)] -fn compile_if_statement( - condition: &Expr, - then_branch: &[Statement], - else_branch: Option<&[Statement]>, - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, -) -> Result<(), CompilerError> { - let mut stack_depth = 0i64; - compile_expr(condition, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; - builder.add_op(OpIf)?; - - let original_env = env.clone(); - let mut then_env = original_env.clone(); - let mut then_types = types.clone(); - compile_block( - then_branch, - &mut then_env, - params, - &mut then_types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - )?; - - let mut else_env = original_env.clone(); - if let Some(else_branch) = else_branch { - builder.add_op(OpElse)?; - let mut else_types = types.clone(); - compile_block( - else_branch, - &mut else_env, + fn compile_if_statement( + &mut self, + condition: &Expr, + then_branch: &[Statement], + else_branch: Option<&[Statement]>, + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + yields: &mut Vec, + ) -> Result<(), CompilerError> { + let mut stack_depth = 0i64; + compile_expr( + condition, + env, params, - &mut else_types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, + types, + self.builder, + self.options, + &mut HashSet::new(), + &mut stack_depth, + self.script_size, )?; + self.builder.add_op(OpIf)?; + + let original_env = env.clone(); + let mut then_env = original_env.clone(); + let mut then_types = types.clone(); + self.compile_block(then_branch, &mut then_env, params, &mut then_types, yields)?; + + let mut else_env = original_env.clone(); + if let Some(else_branch) = else_branch { + self.builder.add_op(OpElse)?; + let mut else_types = types.clone(); + self.compile_block(else_branch, &mut else_env, params, &mut else_types, yields)?; + } + + self.builder.add_op(OpEndIf)?; + + let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; + merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); + Ok(()) } - builder.add_op(OpEndIf)?; + fn compile_block( + &mut self, + statements: &[Statement], + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + yields: &mut Vec, + ) -> Result<(), CompilerError> { + for stmt in statements { + self.compile_statement(stmt, env, params, types, yields)?; + } + Ok(()) + } - let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; - merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); - Ok(()) + fn compile_for_statement( + &mut self, + ident: &str, + start_expr: &Expr, + end_expr: &Expr, + body: &[Statement], + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + yields: &mut Vec, + span: Option, + ) -> Result<(), CompilerError> { + let start = eval_const_int(start_expr, self.contract_constants)?; + let end = eval_const_int(end_expr, self.contract_constants)?; + if end < start { + return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); + } + + let name = ident.to_string(); + let previous = env.get(&name).cloned(); + let previous_type = types.get(&name).cloned(); + types.insert(name.clone(), "int".to_string()); + for value in start..end { + let index_expr = Expr::Int(value); + env.insert(name.clone(), index_expr.clone()); + if let Some(sequence) = self.debug_recorder.record_virtual_step(span, self.builder.script().len()) { + self.debug_recorder.record_variable_updates( + vec![(name.clone(), "int".to_string(), index_expr)], + self.builder.script().len(), + span, + sequence, + ); + } + self.compile_block(body, env, params, types, yields)?; + } + + match previous { + Some(expr) => { + env.insert(name, expr); + } + None => { + env.remove(&name); + } + } + match previous_type { + Some(type_name) => { + types.insert(ident.to_string(), type_name); + } + None => { + types.remove(ident); + } + } + + Ok(()) + } } fn merge_env_after_if( @@ -1038,94 +1164,20 @@ fn compile_time_op_statement( Ok(()) } -#[allow(clippy::too_many_arguments)] -fn compile_block( - statements: &[Statement], - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, -) -> Result<(), CompilerError> { - for stmt in statements { - compile_statement( - stmt, - env, - params, - types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - )?; - } - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -fn compile_for_statement( - ident: &str, - start_expr: &Expr, - end_expr: &Expr, - body: &[Statement], - env: &mut HashMap, +/// Compiles a pre-resolved expression for debugger evaluation. +/// +/// The debugger uses this to evaluate variables by executing the compiled expression +/// on a shadow VM seeded with the current function parameters. +pub fn compile_debug_expr( + expr: &Expr, params: &HashMap, - types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, -) -> Result<(), CompilerError> { - let start = eval_const_int(start_expr, contract_constants)?; - let end = eval_const_int(end_expr, contract_constants)?; - if end < start { - return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); - } - - let name = ident.to_string(); - let previous = env.get(&name).cloned(); - for value in start..end { - env.insert(name.clone(), Expr::Int(value)); - compile_block( - body, - env, - params, - types, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - )?; - } - - match previous { - Some(expr) => { - env.insert(name, expr); - } - None => { - env.remove(&name); - } - } - - Ok(()) + types: &HashMap, +) -> Result, CompilerError> { + let env = HashMap::new(); + let mut builder = ScriptBuilder::new(); + let mut stack_depth = 0i64; + compile_expr(expr, &env, params, types, &mut builder, CompileOptions::default(), &mut HashSet::new(), &mut stack_depth, None)?; + Ok(builder.drain()) } fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result { @@ -1164,64 +1216,91 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { + resolve_expr_internal(expr, env, visiting, true) +} + +pub(super) fn resolve_expr_for_debug( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, +) -> Result { + resolve_expr_internal(expr, env, visiting, false) +} + +fn resolve_expr_internal( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, + preserve_inline_args: bool, +) -> Result { match expr { Expr::Identifier(name) => { - if name.starts_with("__arg_") { + if preserve_inline_args && name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr(value.clone(), env, visiting)?; + let resolved = resolve_expr_internal(value.clone(), env, visiting, preserve_inline_args)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::Identifier(name)) } } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), + Expr::Unary { op, expr } => { + Ok(Expr::Unary { op, expr: Box::new(resolve_expr_internal(*expr, env, visiting, preserve_inline_args)?) }) + } Expr::Binary { op, left, right } => Ok(Expr::Binary { op, - left: Box::new(resolve_expr(*left, env, visiting)?), - right: Box::new(resolve_expr(*right, env, visiting)?), + left: Box::new(resolve_expr_internal(*left, env, visiting, preserve_inline_args)?), + right: Box::new(resolve_expr_internal(*right, env, visiting, preserve_inline_args)?), }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr(*condition, env, visiting)?), - then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), - else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + condition: Box::new(resolve_expr_internal(*condition, env, visiting, preserve_inline_args)?), + then_expr: Box::new(resolve_expr_internal(*then_expr, env, visiting, preserve_inline_args)?), + else_expr: Box::new(resolve_expr_internal(*else_expr, env, visiting, preserve_inline_args)?), }), Expr::Array(values) => { let mut resolved = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr(value, env, visiting)?); + resolved.push(resolve_expr_internal(value, env, visiting, preserve_inline_args)?); } Ok(Expr::Array(resolved)) } Expr::Call { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + resolved.push(resolve_expr_internal(arg, env, visiting, preserve_inline_args)?); } Ok(Expr::Call { name, args: resolved }) } Expr::New { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + resolved.push(resolve_expr_internal(arg, env, visiting, preserve_inline_args)?); } Ok(Expr::New { name, args: resolved }) } Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), + source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), + index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), part, }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), + source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), + index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), + }), + Expr::Slice { source, start, end } => Ok(Expr::Slice { + source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), + start: Box::new(resolve_expr_internal(*start, env, visiting, preserve_inline_args)?), + end: Box::new(resolve_expr_internal(*end, env, visiting, preserve_inline_args)?), + }), + Expr::Introspection { kind, index } => Ok(Expr::Introspection { + kind, + index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), }), - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), other => Ok(other), } } @@ -2067,7 +2146,7 @@ fn data_prefix(data_len: usize) -> Vec { #[cfg(test)] mod tests { - use super::{Op0, OpPushData1, OpPushData2, data_prefix}; + use super::{CompileOptions, Expr, Op0, OpPushData1, OpPushData2, compile_contract, data_prefix}; #[test] fn data_prefix_encodes_small_pushes() { @@ -2088,4 +2167,51 @@ mod tests { fn data_prefix_encodes_pushdata2() { assert_eq!(data_prefix(256), vec![OpPushData2, 0x00, 0x01]); } + + #[test] + fn debug_info_keeps_all_constructor_args() { + let source = r#" + pragma silverscript ^0.1.0; + contract C(int start, int stop, int bias, int minScore) { + entrypoint function f() { require(start + bias >= minScore); } + } + "#; + let constructor_args = vec![Expr::Int(0), Expr::Int(5), Expr::Int(1), Expr::Int(2)]; + let options = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &constructor_args, options).expect("compile succeeds"); + let debug_info = compiled.debug_info.expect("debug info enabled"); + let constant_names = debug_info.constants.iter().map(|constant| constant.name.as_str()).collect::>(); + assert_eq!(constant_names, vec!["start", "stop", "bias", "minScore"]); + } + + #[test] + fn debug_info_records_for_index_updates() { + let source = r#" + pragma silverscript ^0.1.0; + contract C() { + entrypoint function f() { + int sum = 0; + for (i, 0, 3) { + sum = sum + i; + } + require(sum >= 0); + } + } + "#; + let options = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let debug_info = compiled.debug_info.expect("debug info enabled"); + + let index_values = debug_info + .variable_updates + .iter() + .filter(|update| update.function == "f" && update.name == "i") + .filter_map(|update| match update.expr { + Expr::Int(value) => Some(value), + _ => None, + }) + .collect::>(); + + assert_eq!(index_values, vec![0, 1, 2]); + } } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs new file mode 100644 index 00000000..0551f1cf --- /dev/null +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -0,0 +1,352 @@ +use std::collections::{HashMap, HashSet}; + +use kaspa_txscript::script_builder::ScriptBuilder; + +use crate::ast::{Expr, FunctionAst, ParamAst, SourceSpan, Statement}; +use crate::debug::{ + DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, + DebugVariableUpdate, +}; + +use super::{CompilerError, resolve_expr_for_debug}; + +pub(super) fn record_synthetic_range( + builder: &mut ScriptBuilder, + recorder: &mut DebugSink, + label: &'static str, + f: impl FnOnce(&mut ScriptBuilder) -> Result<(), CompilerError>, +) -> Result<(), CompilerError> { + let start = builder.script().len(); + f(builder)?; + let end = builder.script().len(); + recorder.record_synthetic_range(start, end, label); + Ok(()) +} + +/// Per-function debug recorder active during function compilation. +/// Records params, statements, and variable updates for a single function. +#[derive(Debug, Default)] +pub struct FunctionDebugRecorder { + pub function_name: String, + pub enabled: bool, + pub events: Vec, + pub variable_updates: Vec, + pub param_mappings: Vec, + next_seq: u32, + call_depth: u32, + frame_id: u32, +} + +impl FunctionDebugRecorder { + pub fn new(enabled: bool, function: &FunctionAst) -> Self { + let mut recorder = Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, ..Default::default() }; + recorder.record_params(function); + recorder + } + + pub fn inline(enabled: bool, function_name: String, call_depth: u32, frame_id: u32) -> Self { + Self { function_name, enabled, call_depth, frame_id, ..Default::default() } + } + + pub fn sequence_count(&self) -> u32 { + self.next_seq + } + + pub fn call_depth(&self) -> u32 { + self.call_depth + } + + fn next_sequence(&mut self) -> u32 { + let seq = self.next_seq; + self.next_seq = self.next_seq.saturating_add(1); + seq + } + + fn push_event( + &mut self, + bytecode_start: usize, + bytecode_end: usize, + span: Option, + kind: DebugEventKind, + ) -> Option { + if !self.enabled { + return None; + } + let sequence = self.next_sequence(); + self.events.push(DebugEvent { + bytecode_start, + bytecode_end, + span, + kind, + sequence, + call_depth: self.call_depth, + frame_id: self.frame_id, + }); + Some(sequence) + } + + fn record_params(&mut self, function: &FunctionAst) { + if !self.enabled { + return; + } + let param_count = function.params.len(); + for (index, param) in function.params.iter().enumerate() { + self.param_mappings.push(DebugParamMapping { + name: param.name.clone(), + type_name: param.type_name.clone(), + stack_index: (param_count - 1 - index) as i64, + function: function.name.clone(), + }); + } + } + + pub fn record_statement(&mut self, stmt: &Statement, bytecode_start: usize, bytecode_len: usize) -> Option { + let kind = if bytecode_len == 0 { DebugEventKind::Virtual {} } else { DebugEventKind::Statement {} }; + self.push_event(bytecode_start, bytecode_start + bytecode_len, stmt.span, kind) + } + + pub fn record_virtual_step(&mut self, span: Option, bytecode_offset: usize) -> Option { + self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) + } + + pub fn record_inline_param_updates( + &mut self, + function: &FunctionAst, + env: &HashMap, + span: Option, + bytecode_offset: usize, + ) -> Result<(), CompilerError> { + if let Some(sequence) = self.record_virtual_step(span, bytecode_offset) { + let mut variables = Vec::new(); + for param in &function.params { + self.variable_update( + env, + &mut variables, + ¶m.name, + ¶m.type_name, + env.get(¶m.name).cloned().unwrap_or(Expr::Identifier(param.name.clone())), + )?; + } + self.record_variable_updates(variables, bytecode_offset, span, sequence); + } + Ok(()) + } + + pub fn record_inline_call_enter( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + ) -> Option { + self.push_event( + bytecode_offset, + bytecode_offset, + span, + DebugEventKind::InlineCallEnter { callee: callee.to_string() }, + ) + } + + pub fn record_inline_call_exit( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + ) -> Option { + self.push_event( + bytecode_offset, + bytecode_offset, + span, + DebugEventKind::InlineCallExit { callee: callee.to_string() }, + ) + } + + pub fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { + if !self.enabled { + return; + } + let mut seq_map: HashMap = HashMap::new(); + let mut events = inline.events.clone(); + events.sort_by_key(|event| event.sequence); + + for mut event in events { + let local_seq = event.sequence; + let merged_seq = self.next_sequence(); + event.sequence = merged_seq; + self.events.push(event); + seq_map.insert(local_seq, merged_seq); + } + + let mut updates = inline.variable_updates.clone(); + updates.sort_by_key(|update| update.sequence); + for mut update in updates { + if let Some(merged_seq) = seq_map.get(&update.sequence) { + update.sequence = *merged_seq; + self.variable_updates.push(update); + } + } + } + + pub(super) fn record_variable_updates( + &mut self, + variables: Vec<(String, String, Expr)>, + bytecode_offset: usize, + span: Option, + sequence: u32, + ) { + if !self.enabled { + return; + } + for (name, type_name, expr) in variables { + self.variable_updates.push(DebugVariableUpdate { + name, + type_name, + expr, + bytecode_offset, + span, + function: self.function_name.clone(), + sequence, + frame_id: self.frame_id, + }); + } + } + + /// Records a variable update by resolving its expression against the current environment. + /// This expands all local variable references inline, leaving only param identifiers. + /// The resolved expression is what enables shadow VM evaluation at debug time. + pub(super) fn variable_update( + &self, + env: &HashMap, + variables: &mut Vec<(String, String, Expr)>, + name: &str, + type_name: &str, + expr: Expr, + ) -> Result<(), CompilerError> { + if !self.enabled { + return Ok(()); + } + let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; + variables.push((name.to_string(), type_name.to_string(), resolved)); + Ok(()) + } +} + +/// Global debug recording sink that can be enabled or disabled. +/// When Off, all recording calls become no-ops with zero overhead. +pub enum DebugSink { + Off, + On(DebugRecorder), +} + +impl DebugSink { + pub fn new(enabled: bool) -> Self { + if enabled { Self::On(DebugRecorder::default()) } else { Self::Off } + } + + pub fn record(&mut self, event: DebugEvent) { + if let Self::On(rec) = self { + rec.record(event); + } + } + + pub fn record_variable_update(&mut self, update: DebugVariableUpdate) { + if let Self::On(rec) = self { + rec.record_variable_update(update); + } + } + + pub fn record_param(&mut self, param: DebugParamMapping) { + if let Self::On(rec) = self { + rec.record_param(param); + } + } + + pub fn record_function(&mut self, function: DebugFunctionRange) { + if let Self::On(rec) = self { + rec.record_function(function); + } + } + + pub fn record_constant(&mut self, constant: DebugConstantMapping) { + if let Self::On(rec) = self { + rec.record_constant(constant); + } + } + + pub fn record_constructor_constants(&mut self, params: &[ParamAst], values: &[Expr]) { + for (param, value) in params.iter().zip(values.iter()) { + self.record_constant(DebugConstantMapping { + name: param.name.clone(), + type_name: param.type_name.clone(), + value: value.clone(), + }); + } + } + + pub fn record_synthetic_range(&mut self, start: usize, end: usize, label: &'static str) { + if end <= start { + return; + } + if let Self::On(rec) = self { + let sequence = rec.next_sequence(); + rec.record(DebugEvent { + bytecode_start: start, + bytecode_end: end, + span: None, + kind: DebugEventKind::Synthetic { label: label.to_string() }, + sequence, + call_depth: 0, + frame_id: 0, + }); + } + } + + pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionDebugRecorder, offset: usize) { + let seq_base = if let Self::On(rec) = self { rec.reserve_sequence_block(debug.sequence_count()) } else { 0 }; + emit_events_with_offset(&debug.events, offset, seq_base, self); + emit_variable_updates_with_offset(&debug.variable_updates, offset, seq_base, self); + self.record_function(DebugFunctionRange { name: name.to_string(), bytecode_start: offset, bytecode_end: offset + script_len }); + record_param_mappings(&debug.param_mappings, self); + } + + pub fn into_debug_info(self, source: String) -> Option { + match self { + Self::Off => None, + Self::On(rec) => Some(rec.into_debug_info(source)), + } + } +} + +fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, sink: &mut DebugSink) { + for event in events { + sink.record(DebugEvent { + bytecode_start: event.bytecode_start + offset, + bytecode_end: event.bytecode_end + offset, + span: event.span, + kind: event.kind.clone(), + sequence: seq_base.saturating_add(event.sequence), + call_depth: event.call_depth, + frame_id: event.frame_id, + }); + } +} + +fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: usize, seq_base: u32, sink: &mut DebugSink) { + for update in updates { + sink.record_variable_update(DebugVariableUpdate { + name: update.name.clone(), + type_name: update.type_name.clone(), + expr: update.expr.clone(), + bytecode_offset: update.bytecode_offset + offset, + span: update.span, + function: update.function.clone(), + sequence: seq_base.saturating_add(update.sequence), + frame_id: update.frame_id, + }); + } +} + +fn record_param_mappings(params: &[DebugParamMapping], sink: &mut DebugSink) { + for param in params { + sink.record_param(param.clone()); + } +} diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs new file mode 100644 index 00000000..28395047 --- /dev/null +++ b/silverscript-lang/src/debug.rs @@ -0,0 +1,220 @@ +use crate::ast::{Expr, SourceSpan}; +use serde::{Deserialize, Serialize}; + +pub mod session; + +pub mod labels { + pub mod synthetic { + /// Checks which function was selected (DUP, PUSH index, NUMEQUAL, IF, DROP). + pub const DISPATCHER_GUARD: &str = "dispatcher.guard"; + /// Function didn't match — try next, or fail if last. + pub const DISPATCHER_ELSE: &str = "dispatcher.else"; + /// Closes all dispatcher if/else branches. + pub const DISPATCHER_ENDIFS: &str = "dispatcher.endifs"; + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DebugEventKind { + Statement {}, + Virtual {}, + InlineCallEnter { callee: String }, + InlineCallExit { callee: String }, + Synthetic { label: String }, +} + +/// Single debug mapping recorded during compilation. +/// Maps a bytecode range to source location and event type (statement or synthetic). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugEvent { + pub bytecode_start: usize, + pub bytecode_end: usize, + pub span: Option, + pub kind: DebugEventKind, + #[serde(default)] + pub sequence: u32, + #[serde(default)] + pub call_depth: u32, + #[serde(default)] + pub frame_id: u32, +} + +/// Accumulates debug metadata during compilation. +/// Collects events, variable updates, param mappings, function ranges, and constants. +/// Converted to `DebugInfo` after compilation completes. +#[derive(Debug, Default)] +pub struct DebugRecorder { + events: Vec, + variable_updates: Vec, + params: Vec, + functions: Vec, + constants: Vec, + next_sequence: u32, +} + +impl DebugRecorder { + pub fn record(&mut self, event: DebugEvent) { + self.events.push(event); + } + + pub fn record_variable_update(&mut self, update: DebugVariableUpdate) { + self.variable_updates.push(update); + } + + pub fn record_param(&mut self, param: DebugParamMapping) { + self.params.push(param); + } + + pub fn record_function(&mut self, function: DebugFunctionRange) { + self.functions.push(function); + } + + pub fn record_constant(&mut self, constant: DebugConstantMapping) { + self.constants.push(constant); + } + + pub fn next_sequence(&mut self) -> u32 { + let sequence = self.next_sequence; + self.next_sequence = self.next_sequence.saturating_add(1); + sequence + } + + pub fn reserve_sequence_block(&mut self, count: u32) -> u32 { + let base = self.next_sequence; + self.next_sequence = self.next_sequence.saturating_add(count); + base + } + + pub fn into_events(self) -> Vec { + self.events + } + + pub fn into_debug_info(self, source: String) -> DebugInfo { + DebugInfo { + source, + mappings: self.events.into_iter().map(DebugMapping::from).collect(), + variable_updates: self.variable_updates, + params: self.params, + functions: self.functions, + constants: self.constants, + } + } +} + +/// Complete debug metadata attached to compiled contract. +/// Contains everything needed to map bytecode execution back to source and evaluate variables. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugInfo { + pub source: String, + pub mappings: Vec, + pub variable_updates: Vec, + pub params: Vec, + pub functions: Vec, + pub constants: Vec, +} + +impl DebugInfo { + pub fn empty() -> Self { + Self { + source: String::new(), + mappings: Vec::new(), + variable_updates: Vec::new(), + params: Vec::new(), + functions: Vec::new(), + constants: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugVariableUpdate { + pub name: String, + pub type_name: String, + /// Pre-resolved expression with all local variable references expanded inline. + /// Only function parameter Identifiers remain. Enables shadow VM evaluation. + pub expr: Expr, + pub bytecode_offset: usize, + pub span: Option, + pub function: String, + #[serde(default)] + pub sequence: u32, + #[serde(default)] + pub frame_id: u32, +} + +/// Maps function parameter to its stack position. +/// Stack index is measured from stack top (0 = topmost param). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugParamMapping { + pub name: String, + pub type_name: String, + pub stack_index: i64, + pub function: String, +} + +/// Bytecode range for a compiled function. +/// Used to determine which function is executing at a given bytecode offset. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugFunctionRange { + pub name: String, + pub bytecode_start: usize, + pub bytecode_end: usize, +} + +/// Constructor constant (contract instantiation parameter). +/// Recorded for display in debugger variable list. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugConstantMapping { + pub name: String, + pub type_name: String, + pub value: Expr, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DebugMapping { + pub bytecode_start: usize, + pub bytecode_end: usize, + pub span: Option, + pub kind: MappingKind, + #[serde(default)] + pub sequence: u32, + #[serde(default)] + pub call_depth: u32, + #[serde(default)] + pub frame_id: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MappingKind { + Statement {}, + Virtual {}, + InlineCallEnter { callee: String }, + InlineCallExit { callee: String }, + Synthetic { label: String }, +} + +impl From for MappingKind { + fn from(kind: DebugEventKind) -> Self { + match kind { + DebugEventKind::Statement {} => MappingKind::Statement {}, + DebugEventKind::Virtual {} => MappingKind::Virtual {}, + DebugEventKind::InlineCallEnter { callee } => MappingKind::InlineCallEnter { callee }, + DebugEventKind::InlineCallExit { callee } => MappingKind::InlineCallExit { callee }, + DebugEventKind::Synthetic { label } => MappingKind::Synthetic { label }, + } + } +} + +impl From for DebugMapping { + fn from(event: DebugEvent) -> Self { + DebugMapping { + bytecode_start: event.bytecode_start, + bytecode_end: event.bytecode_end, + span: event.span, + kind: event.kind.into(), + sequence: event.sequence, + call_depth: event.call_depth, + frame_id: event.frame_id, + } + } +} diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs new file mode 100644 index 00000000..a98ac257 --- /dev/null +++ b/silverscript-lang/src/debug/session.rs @@ -0,0 +1,991 @@ +use std::collections::{HashMap, HashSet}; + +use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_consensus_core::tx::PopulatedTransaction; +use kaspa_txscript::caches::Cache; +use kaspa_txscript::script_builder::ScriptBuilder; +use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; +use serde::{Deserialize, Serialize}; + +use crate::ast::{Expr, SourceSpan}; +use crate::compiler::compile_debug_expr; +use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind}; + +pub type DebugTx<'a> = PopulatedTransaction<'a>; +pub type DebugReused = SigHashReusedValuesUnsync; +pub type DebugOpcode<'a> = DynOpcodeImplementation, DebugReused>; +pub type DebugEngine<'a> = TxScriptEngine<'a, DebugTx<'a>, DebugReused>; + +#[derive(Debug, Clone)] +pub enum DebugValue { + Int(i64), + Bool(bool), + Bytes(Vec), + String(String), + Array(Vec), + /// Value could not be evaluated (e.g., from inline function return) + Unknown(std::string::String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VariableOrigin { + Local, + Param, + Constant, +} + +impl VariableOrigin { + pub fn label(self) -> &'static str { + match self { + Self::Local => "local", + Self::Param => "arg", + Self::Constant => "const", + } + } +} + +#[derive(Debug, Clone)] +pub struct Variable { + pub name: String, + pub type_name: String, + pub value: DebugValue, + pub is_constant: bool, + pub origin: VariableOrigin, +} + +#[derive(Debug, Clone)] +pub struct SourceContextLine { + pub line: u32, + pub text: String, + pub is_active: bool, +} + +#[derive(Debug, Clone)] +pub struct SourceContext { + pub lines: Vec, +} + +#[derive(Debug, Clone)] +pub struct SessionState { + pub pc: usize, + pub opcode: Option, + pub mapping: Option, + pub stack: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StackSnapshot { + pub dstack: Vec, + pub astack: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpcodeMeta { + pub index: usize, + pub byte_offset: usize, + pub display: String, + pub mapping: Option, +} + +pub struct DebugSession<'a> { + engine: DebugEngine<'a>, + opcodes: Vec>>, + op_displays: Vec, + opcode_offsets: Vec, + script_len: usize, + pc: usize, + debug_info: DebugInfo, + source_mappings: Vec, + current_step_index: Option, + uses_sequence_order: bool, + source_lines: Vec, + breakpoints: HashSet, +} + +struct ShadowParamValue { + name: String, + type_name: String, + stack_index: i64, + value: Vec, +} + +impl<'a> DebugSession<'a> { + // --- Session construction + stepping --- + + /// Creates a debug session for lockscript-only execution. + /// Use this when debugging pure contract logic without sigscript setup. + pub fn lockscript_only( + script: &[u8], + source: &str, + debug_info: Option, + engine: DebugEngine<'a>, + ) -> Result { + Self::from_scripts(script, source, debug_info, engine) + } + + /// Creates a debug session simulating a full transaction spend. + /// Executes sigscript first to seed the stack, then debugs lockscript execution. + pub fn full( + sigscript: &[u8], + lockscript: &[u8], + source: &str, + debug_info: Option, + mut engine: DebugEngine<'a>, + ) -> Result { + seed_engine_with_sigscript(&mut engine, sigscript)?; + Self::from_scripts(lockscript, source, debug_info, engine) + } + + /// Internal constructor: parses script, prepares opcodes, extracts statement mappings. + pub fn from_scripts( + script: &[u8], + source: &str, + debug_info: Option, + engine: DebugEngine<'a>, + ) -> Result { + let debug_info = debug_info.unwrap_or_else(DebugInfo::empty); + let opcodes = parse_script::, DebugReused>(script).collect::, _>>()?; + let op_displays = opcodes.iter().map(|op| format!("{op:?}")).collect(); + let opcodes: Vec>> = opcodes.into_iter().map(Some).collect(); + let source_lines: Vec = source.lines().map(String::from).collect(); + let (opcode_offsets, script_len) = build_opcode_offsets(&opcodes); + + let uses_sequence_order = debug_info.mappings.iter().any(|mapping| mapping.sequence != 0) + || debug_info.variable_updates.iter().any(|update| update.sequence != 0); + let mut source_mappings: Vec = debug_info + .mappings + .iter() + .filter(|mapping| { + matches!( + &mapping.kind, + MappingKind::Statement {} + | MappingKind::Virtual {} + | MappingKind::InlineCallEnter { .. } + | MappingKind::InlineCallExit { .. } + ) + }) + .cloned() + .collect(); + if uses_sequence_order { + source_mappings.sort_by_key(|mapping| (mapping.sequence, mapping.bytecode_start, mapping.bytecode_end)); + } else { + source_mappings.sort_by_key(|mapping| (mapping.bytecode_start, mapping.bytecode_end)); + } + + Ok(Self { + engine, + opcodes, + op_displays, + opcode_offsets, + script_len, + pc: 0, + debug_info, + source_mappings, + current_step_index: None, + uses_sequence_order, + source_lines, + breakpoints: HashSet::new(), + }) + } + + /// Executes a single opcode and advances the program counter. + pub fn step_opcode(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + if self.pc >= self.opcodes.len() { + return Ok(None); + } + + let opcode = self.opcodes[self.pc].take().expect("opcode already executed"); + self.engine.execute_opcode(opcode)?; + self.pc += 1; + Ok(Some(self.state())) + } + + /// Step into: advance to next source step regardless of call depth. + pub fn step_into(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + self.step_with_depth_predicate(|_, _| true) + } + + /// Step over: advance to next source step at the same or shallower call depth. + pub fn step_over(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + self.step_with_depth_predicate(|candidate, current| candidate <= current) + } + + /// Step out: advance to next source step at a shallower call depth. + pub fn step_out(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + self.step_with_depth_predicate(|candidate, current| candidate < current) + } + + /// Backward-compatible statement stepping alias. + pub fn step_statement(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + self.step_over() + } + + fn step_with_depth_predicate( + &mut self, + predicate: impl Fn(u32, u32) -> bool, + ) -> Result, kaspa_txscript_errors::TxScriptError> { + if self.source_mappings.is_empty() { + return self.step_opcode(); + } + + let current_depth = self.current_step_mapping().map(|mapping| mapping.call_depth).unwrap_or(0); + let mut search_from = self.current_step_index; + + loop { + let Some(target_index) = self.next_steppable_mapping_index(search_from, |mapping| { + predicate(mapping.call_depth, current_depth) + }) else { + while self.step_opcode()?.is_some() {} + return Ok(None); + }; + + if self.advance_to_mapping(target_index)? { + self.current_step_index = Some(target_index); + return Ok(Some(self.state())); + } + + search_from = Some(target_index); + } + } + + fn advance_to_mapping(&mut self, target_index: usize) -> Result { + let Some(target) = self.source_mappings.get(target_index).cloned() else { + return Ok(false); + }; + loop { + let offset = self.current_byte_offset(); + + if offset > target.bytecode_start { + return Ok(false); + } + + if mapping_matches_offset(&target, offset) { + if self.engine.is_executing() { + return Ok(true); + } + } + + if self.step_opcode()?.is_none() { + return Ok(false); + } + } + } + + /// Advances execution to the first user statement, skipping dispatcher/synthetic bytecode. + /// Call this after session creation to skip over contract setup code. + /// Skips opcodes until the first source-mapped statement is encountered. + pub fn run_to_first_executed_statement(&mut self) -> Result<(), kaspa_txscript_errors::TxScriptError> { + if self.source_mappings.is_empty() { + return Ok(()); + } + loop { + if self.pc >= self.opcodes.len() { + return Ok(()); + } + let offset = self.current_byte_offset(); + if self.engine.is_executing() { + let found = self.source_mappings.iter().enumerate().find(|(_, mapping)| { + self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset) + }); + if let Some((index, _)) = found { + self.current_step_index = Some(index); + return Ok(()); + } + } + if self.step_opcode()?.is_none() { + return Ok(()); + } + } + } + + /// Continues execution until a breakpoint is hit or script completes. + pub fn continue_to_breakpoint(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + if self.breakpoints.is_empty() { + while self.step_opcode()?.is_some() {} + return Ok(None); + } + loop { + if self.step_into()?.is_none() { + return Ok(None); + } + if let Some(mapping) = self.current_step_mapping() { + if self.mapping_hits_breakpoint(mapping) { + return Ok(Some(self.state())); + } + } + } + } + + /// Returns the current execution state snapshot. + pub fn state(&self) -> SessionState { + let executed = self.pc.saturating_sub(1); + let opcode = self.op_displays.get(executed).cloned(); + SessionState { + pc: self.pc, + opcode, + mapping: self.current_location(), + stack: self.stack(), + } + } + + /// Returns true if the script engine is still running. + pub fn is_executing(&self) -> bool { + self.engine.is_executing() + } + + /// Returns the current data and alt stack contents. + pub fn stacks_snapshot(&self) -> StackSnapshot { + let stacks = self.engine.stacks(); + StackSnapshot { + dstack: stacks.dstack.iter().map(|item| hex::encode(item)).collect(), + astack: stacks.astack.iter().map(|item| hex::encode(item)).collect(), + } + } + + /// Returns metadata for all opcodes (executed/pending status, byte offset). + pub fn opcode_metas(&self) -> Vec { + (0..self.op_displays.len()) + .map(|index| { + let byte_offset = self.opcode_offsets.get(index).copied().unwrap_or(self.script_len); + OpcodeMeta { + index, + byte_offset, + display: self.op_displays.get(index).cloned().unwrap_or_default(), + mapping: self.mapping_for_offset(byte_offset).cloned(), + } + }) + .collect() + } + + /// Returns the total number of opcodes in the script. + pub fn opcode_count(&self) -> usize { + self.op_displays.len() + } + + pub fn debug_info(&self) -> &DebugInfo { + &self.debug_info + } + + // --- Mapping + source context --- + + /// Returns source lines around the current statement (radius = 6 lines). + /// Active line is marked via `is_active` field. Returns None if no source mapping exists. + /// Returns surrounding source lines with the current line highlighted. + pub fn source_context(&self) -> Option { + let span = self.current_span()?; + let line = span.line.saturating_sub(1) as usize; + let radius = 6; + let start = line.saturating_sub(radius); + let end = (line + radius).min(self.source_lines.len().saturating_sub(1)); + + let mut lines = Vec::new(); + for idx in start..=end { + let display_line = idx + 1; + let content = self.source_lines.get(idx).map(String::as_str).unwrap_or(""); + lines.push(SourceContextLine { line: display_line as u32, text: content.to_string(), is_active: idx == line }); + } + + Some(SourceContext { lines }) + } + + /// Adds a breakpoint at the given line number. Returns true if added. + pub fn add_breakpoint(&mut self, line: u32) -> bool { + let valid = self + .source_mappings + .iter() + .filter(|mapping| self.is_steppable_mapping(mapping)) + .any(|mapping| mapping.span.map_or(false, |span| line >= span.line && line <= span.end_line)); + if valid { + self.breakpoints.insert(line); + } + valid + } + + /// Returns all currently set breakpoint line numbers. + pub fn breakpoints(&self) -> Vec { + let mut lines = self.breakpoints.iter().copied().collect::>(); + lines.sort_unstable(); + lines + } + + /// Removes the breakpoint at the given line number. + pub fn clear_breakpoint(&mut self, line: u32) { + self.breakpoints.remove(&line); + } + + // --- Variable inspection --- + + /// Returns all variables in scope at current execution point. + /// Includes params, local variables (up to current offset), and constructor constants. + /// Values are computed via shadow VM evaluation. + pub fn list_variables(&self) -> Result, String> { + let (sequence, frame_id) = self.current_step_sequence_and_frame(); + self.collect_variables(sequence, frame_id) + } + + pub fn list_variables_at_sequence(&self, sequence: u32, frame_id: u32) -> Result, String> { + self.collect_variables(sequence, frame_id) + } + + fn collect_variables(&self, sequence: u32, frame_id: u32) -> Result, String> { + let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; + let offset = self.current_byte_offset(); + let var_updates = self.current_variable_updates(function_name, offset, sequence, frame_id); + + let mut variables: Vec = Vec::new(); + let mut seen_names: HashSet = HashSet::new(); + + for (name, update) in &var_updates { + let value = self.evaluate_update_with_shadow_vm(function_name, update).unwrap_or_else(DebugValue::Unknown); + variables.push(Variable { + name: name.clone(), + type_name: update.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Local, + }); + seen_names.insert(name.clone()); + } + + for param in self.debug_info.params.iter().filter(|param| param.function == function_name) { + if seen_names.contains(¶m.name) { + continue; + } + let value = self.read_param_value(param)?; + variables.push(Variable { + name: param.name.clone(), + type_name: param.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Param, + }); + seen_names.insert(param.name.clone()); + } + + for constant in &self.debug_info.constants { + if seen_names.contains(&constant.name) { + continue; + } + let value = self.evaluate_constant(&constant.value); + variables.push(Variable { + name: constant.name.clone(), + type_name: constant.type_name.clone(), + value, + is_constant: true, + origin: VariableOrigin::Constant, + }); + seen_names.insert(constant.name.clone()); + } + + variables.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(variables) + } + + /// Returns a specific variable by name, or error if not in scope. + /// Retrieves a specific variable by name with its current value. + pub fn variable_by_name(&self, name: &str) -> Result { + let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; + let offset = self.current_byte_offset(); + let (sequence, frame_id) = self.current_step_sequence_and_frame(); + let var_updates = self.current_variable_updates(function_name, offset, sequence, frame_id); + + if let Some(update) = var_updates.get(name) { + let value = self.evaluate_update_with_shadow_vm(function_name, update).unwrap_or_else(DebugValue::Unknown); + return Ok(Variable { + name: name.to_string(), + type_name: update.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Local, + }); + } + + if let Some(param) = self.debug_info.params.iter().find(|param| param.function == function_name && param.name == name) { + let value = self.read_param_value(param)?; + return Ok(Variable { + name: name.to_string(), + type_name: param.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Param, + }); + } + + // Check constructor constants + if let Some(constant) = self.debug_info.constants.iter().find(|c| c.name == name) { + let value = self.evaluate_constant(&constant.value); + return Ok(Variable { + name: name.to_string(), + type_name: constant.type_name.clone(), + value, + is_constant: true, + origin: VariableOrigin::Constant, + }); + } + + Err(format!("unknown variable '{name}'")) + } + + // --- DebugValue formatting --- + /// Formats a debug value for display based on its type. + pub fn format_value(&self, type_name: &str, value: &DebugValue) -> String { + let element_type = type_name.strip_suffix("[]"); + match (type_name, value) { + ("int", DebugValue::Int(number)) => number.to_string(), + ("bool", DebugValue::Bool(value)) => value.to_string(), + ("string", DebugValue::String(value)) => value.clone(), + (_, DebugValue::Unknown(reason)) => { + if reason.trim().is_empty() { + "".to_string() + } else if reason.contains("failed to compile debug expression") + || reason.contains("undefined identifier") + || reason.contains("__arg_") + { + "".to_string() + } else if reason.contains("failed to execute shadow script") { + "".to_string() + } else { + format!("", concise_reason(reason)) + } + } + (_, DebugValue::Bytes(bytes)) if element_type.is_some() => { + let element_type = element_type.expect("checked"); + let Some(element_size) = array_element_size(element_type) else { + return format!("0x{}", hex::encode(bytes)); + }; + if element_size == 0 || bytes.len() % element_size != 0 { + return format!("0x{}", hex::encode(bytes)); + } + + let mut values: Vec = Vec::new(); + for chunk in bytes.chunks(element_size) { + let decoded = match element_type { + "int" => DebugValue::Int(decode_i64(chunk).unwrap_or(0)), + "bool" => DebugValue::Bool(decode_i64(chunk).unwrap_or(0) != 0), + _ => DebugValue::Bytes(chunk.to_vec()), + }; + values.push(self.format_value(element_type, &decoded)); + } + format!("[{}]", values.join(", ")) + } + (_, DebugValue::Bytes(bytes)) => format!("0x{}", hex::encode(bytes)), + (_, DebugValue::Int(number)) => number.to_string(), + (_, DebugValue::Bool(value)) => value.to_string(), + (_, DebugValue::String(value)) => value.clone(), + (_, DebugValue::Array(values)) => { + let value_type = element_type.unwrap_or(type_name); + format!("[{}]", values.iter().map(|v| self.format_value(value_type, v)).collect::>().join(", ")) + } + } + } + + /// Returns the debug mapping for the current bytecode position. + pub fn current_location(&self) -> Option { + self.current_step_mapping() + .cloned() + .or_else(|| self.mapping_for_offset(self.current_byte_offset()).cloned()) + } + + /// Returns the current bytecode offset in the script. + pub fn current_byte_offset(&self) -> usize { + self.opcode_offsets.get(self.pc).copied().unwrap_or(self.script_len) + } + + /// Returns the source span (line/col range) at the current position. + pub fn current_span(&self) -> Option { + self.current_location().and_then(|mapping| mapping.span) + } + + pub fn call_stack(&self) -> Vec { + let mut stack = Vec::new(); + let Some(current) = self.current_step_index else { + return stack; + }; + for mapping in self.source_mappings.iter().take(current + 1) { + match &mapping.kind { + MappingKind::InlineCallEnter { callee } => stack.push(callee.clone()), + MappingKind::InlineCallExit { .. } => { + stack.pop(); + } + _ => {} + } + } + stack + } + + /// Returns the name of the function currently being executed. + pub fn current_function_name(&self) -> Option<&str> { + self.current_function_range().map(|range| range.name.as_str()) + } + + fn current_function_range(&self) -> Option<&DebugFunctionRange> { + let offset = self.current_byte_offset(); + self.debug_info.functions.iter().find(|function| offset >= function.bytecode_start && offset < function.bytecode_end) + } + + fn current_variable_updates( + &self, + function_name: &str, + offset: usize, + sequence: u32, + frame_id: u32, + ) -> HashMap { + let mut latest: HashMap = HashMap::new(); + for update in self.debug_info.variable_updates.iter().filter(|update| { + if update.function != function_name { + return false; + } + if self.uses_sequence_order { + update.frame_id == frame_id && update.sequence <= sequence + } else { + update.bytecode_offset <= offset + } + }) { + if self.uses_sequence_order { + match latest.get(&update.name) { + Some(existing) if existing.sequence > update.sequence => {} + _ => { + latest.insert(update.name.clone(), update); + } + } + } else { + match latest.get(&update.name) { + Some(existing) if existing.bytecode_offset > update.bytecode_offset => {} + _ => { + latest.insert(update.name.clone(), update); + } + } + } + } + latest + } + + /// Best mapping = smallest bytecode span containing `offset`. + fn mapping_for_offset(&self, offset: usize) -> Option<&DebugMapping> { + let mut best: Option<&DebugMapping> = None; + let mut best_len = usize::MAX; + for mapping in &self.debug_info.mappings { + if mapping_matches_offset(mapping, offset) { + let len = mapping.bytecode_end.saturating_sub(mapping.bytecode_start); + if len < best_len { + best = Some(mapping); + best_len = len; + } + } + } + best + } + + fn current_step_mapping(&self) -> Option<&DebugMapping> { + self.current_step_index.and_then(|index| self.source_mappings.get(index)) + } + + fn current_step_sequence_and_frame(&self) -> (u32, u32) { + self.current_step_mapping().map(|mapping| (mapping.sequence, mapping.frame_id)).unwrap_or((0, 0)) + } + + fn is_steppable_mapping(&self, mapping: &DebugMapping) -> bool { + matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {}) + } + + fn next_steppable_mapping_index( + &self, + from: Option, + predicate: impl Fn(&DebugMapping) -> bool, + ) -> Option { + let start = from.map(|index| index.saturating_add(1)).unwrap_or(0); + for index in start..self.source_mappings.len() { + let mapping = self.source_mappings.get(index)?; + if !self.is_steppable_mapping(mapping) { + continue; + } + if predicate(mapping) { + return Some(index); + } + } + None + } + + fn mapping_hits_breakpoint(&self, mapping: &DebugMapping) -> bool { + mapping.span.map(|span| (span.line..=span.end_line).any(|line| self.breakpoints.contains(&line))).unwrap_or(false) + } + + /// Returns the current main stack as hex-encoded strings. + pub fn stack(&self) -> Vec { + let stacks = self.engine.stacks(); + stacks.dstack.iter().map(|item| hex::encode(item)).collect() + } + + fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate) -> Result { + self.evaluate_expr_with_shadow_vm(function_name, &update.type_name, &update.expr) + } + + /// Evaluates an expression using shadow VM execution. + /// + /// Strategy: compile the pre-resolved expression to bytecode, build a mini-script + /// that pushes current param values then executes the bytecode, run on fresh VM, + /// read result from top of stack. This guarantees debugger sees same semantics as + /// real execution without duplicating evaluation logic. + fn evaluate_expr_with_shadow_vm(&self, function_name: &str, type_name: &str, expr: &Expr) -> Result { + let params = self.shadow_param_values(function_name)?; + let mut param_indexes = HashMap::new(); + let mut param_types = HashMap::new(); + for param in ¶ms { + param_indexes.insert(param.name.clone(), param.stack_index); + param_types.insert(param.name.clone(), param.type_name.clone()); + } + let bytecode = compile_debug_expr(expr, ¶m_indexes, ¶m_types) + .map_err(|err| format!("failed to compile debug expression: {err}"))?; + let script = self.build_shadow_script(¶ms, &bytecode)?; + let bytes = self.execute_shadow_script(&script)?; + decode_value_by_type(type_name, bytes) + } + + fn shadow_param_values(&self, function_name: &str) -> Result, String> { + let mut params = Vec::new(); + for param in self.debug_info.params.iter().filter(|param| param.function == function_name) { + params.push(ShadowParamValue { + name: param.name.clone(), + type_name: param.type_name.clone(), + stack_index: param.stack_index, + value: self.read_stack_at_index(param.stack_index)?, + }); + } + // Push higher stack indexes first so index 0 remains the top parameter. + params.sort_by(|left, right| right.stack_index.cmp(&left.stack_index)); + Ok(params) + } + + fn build_shadow_script(&self, params: &[ShadowParamValue], expr_bytecode: &[u8]) -> Result, String> { + let mut builder = ScriptBuilder::new(); + for param in params { + builder.add_data(¶m.value).map_err(|err| err.to_string())?; + } + builder.add_ops(expr_bytecode).map_err(|err| err.to_string())?; + Ok(builder.drain()) + } + + fn execute_shadow_script(&self, script: &[u8]) -> Result, String> { + let sig_cache = Cache::new(0); + let reused_values = SigHashReusedValuesUnsync::new(); + let mut engine: DebugEngine<'_> = + TxScriptEngine::new(EngineCtx::new(&sig_cache).with_reused(&reused_values), EngineFlags { covenants_enabled: true }); + for opcode in parse_script::, DebugReused>(script) { + let opcode = opcode.map_err(|err| format!("failed to parse shadow script: {err}"))?; + engine.execute_opcode(opcode).map_err(|err| format!("failed to execute shadow script: {err}"))?; + } + engine.stacks().dstack.last().cloned().ok_or_else(|| "shadow VM produced an empty stack".to_string()) + } + + fn read_param_value(&self, param: &DebugParamMapping) -> Result { + let bytes = self.read_stack_at_index(param.stack_index)?; + decode_value_by_type(¶m.type_name, bytes) + } + + fn evaluate_constant(&self, expr: &Expr) -> DebugValue { + match expr { + Expr::Int(v) => DebugValue::Int(*v), + Expr::Bool(v) => DebugValue::Bool(*v), + Expr::Bytes(v) => DebugValue::Bytes(v.clone()), + Expr::String(v) => DebugValue::String(v.clone()), + _ => DebugValue::Unknown("complex expression".to_string()), + } + } + + fn read_stack_at_index(&self, index: i64) -> Result, String> { + if index < 0 { + return Err("negative stack index".to_string()); + } + let stacks = self.engine.stacks(); + let stack = stacks.dstack; + let idx = index as usize; + if idx >= stack.len() { + return Err("stack index out of range".to_string()); + } + let stack_index = stack.len() - 1 - idx; + Ok(stack.get(stack_index).cloned().unwrap_or_default()) + } +} + +/// Returns byte size for fixed-size array elements (e.g., bytes32 → 32), or None for variable-size. +fn array_element_size(element_type: &str) -> Option { + match element_type { + "int" => Some(8), + "bool" => Some(1), + "byte" => Some(1), + other => other.strip_prefix("bytes").and_then(|v| v.parse::().ok()), + } +} + +/// Decodes raw bytes into a typed debug value based on the type name. +fn decode_value_by_type(type_name: &str, bytes: Vec) -> Result { + match type_name { + "int" => Ok(DebugValue::Int(decode_i64(&bytes)?)), + "bool" => Ok(DebugValue::Bool(decode_i64(&bytes)? != 0)), + "string" => match String::from_utf8(bytes.clone()) { + Ok(value) => Ok(DebugValue::String(value)), + Err(_) => Ok(DebugValue::Bytes(bytes)), + }, + _ => Ok(DebugValue::Bytes(bytes)), + } +} + +/// Truncates error messages to 96 chars for display in debugger UI. +fn concise_reason(reason: &str) -> String { + let trimmed = reason.trim(); + if trimmed.is_empty() { + return "unknown".to_string(); + } + let first_line = trimmed.lines().next().unwrap_or(trimmed); + const MAX_CHARS: usize = 96; + if first_line.chars().count() <= MAX_CHARS { + first_line.to_string() + } else { + let mut out = String::new(); + for ch in first_line.chars().take(MAX_CHARS) { + out.push(ch); + } + out.push_str("..."); + out + } +} + +/// Decode a sign-magnitude little-endian integer +fn decode_i64(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(0); + } + if bytes.len() > 8 { + return Err("numeric value is longer than 8 bytes".to_string()); + } + let msb = bytes[bytes.len() - 1]; + let sign = 1 - 2 * ((msb >> 7) as i64); + let first_byte = (msb & 0x7f) as i64; + let mut value = first_byte; + for byte in bytes[..bytes.len() - 1].iter().rev() { + value = (value << 8) + (*byte as i64); + } + Ok(value * sign) +} + +/// Executes sigscript to seed the stack before debugging lockscript. +fn seed_engine_with_sigscript(engine: &mut DebugEngine<'_>, sigscript: &[u8]) -> Result<(), kaspa_txscript_errors::TxScriptError> { + for opcode in parse_script::, DebugReused>(sigscript) { + engine.execute_opcode(opcode?)?; + } + Ok(()) +} + +fn build_opcode_offsets(opcodes: &[Option>]) -> (Vec, usize) { + let mut offsets = Vec::with_capacity(opcodes.len() + 1); + let mut offset = 0usize; + for opcode in opcodes { + offsets.push(offset); + if let Some(op) = opcode { + offset = offset.saturating_add(op.serialize().len()); + } + } + (offsets, offset) +} + +fn mapping_matches_offset(mapping: &DebugMapping, offset: usize) -> bool { + if mapping.bytecode_start == mapping.bytecode_end { + offset == mapping.bytecode_start + } else { + offset >= mapping.bytecode_start && offset < mapping.bytecode_end + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::ast::{BinaryOp, Expr}; + use crate::debug::{DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugVariableUpdate}; + + fn make_session( + params: Vec, + updates: Vec, + sigscript: &[u8], + ) -> Result, kaspa_txscript_errors::TxScriptError> { + let sig_cache = Box::leak(Box::new(Cache::new(10_000))); + let reused_values: &'static SigHashReusedValuesUnsync = Box::leak(Box::new(SigHashReusedValuesUnsync::new())); + let engine: DebugEngine<'static> = + TxScriptEngine::new(EngineCtx::new(sig_cache).with_reused(reused_values), EngineFlags { covenants_enabled: true }); + let debug_info = DebugInfo { + source: String::new(), + mappings: vec![], + variable_updates: updates, + params, + functions: vec![DebugFunctionRange { name: "f".to_string(), bytecode_start: 0, bytecode_end: 1 }], + constants: vec![DebugConstantMapping { name: "K".to_string(), type_name: "int".to_string(), value: Expr::Int(7) }], + }; + DebugSession::full(sigscript, &[], "", Some(debug_info), engine) + } + + #[test] + fn decode_i64_handles_basic_values() { + assert_eq!(decode_i64(&[]).unwrap(), 0); + assert_eq!(decode_i64(&[1]).unwrap(), 1); + assert_eq!(decode_i64(&[0x81]).unwrap(), -1); + assert_eq!(decode_i64(&[0, 0x80]).unwrap(), 0); + } + + #[test] + fn shadow_vm_evaluates_param_expression() { + let mut sig_builder = ScriptBuilder::new(); + sig_builder.add_i64(3).unwrap(); + sig_builder.add_i64(9).unwrap(); + let sigscript = sig_builder.drain(); + + let session = make_session( + vec![ + DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 1, function: "f".to_string() }, + DebugParamMapping { name: "b".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }, + ], + vec![], + &sigscript, + ) + .unwrap(); + + let value = session + .evaluate_expr_with_shadow_vm( + "f", + "int", + &Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::Identifier("a".to_string())), + right: Box::new(Expr::Identifier("b".to_string())), + }, + ) + .unwrap(); + assert!(matches!(value, DebugValue::Int(12))); + } + + #[test] + fn list_variables_returns_unknown_for_uncompilable_expr() { + let mut sig_builder = ScriptBuilder::new(); + sig_builder.add_i64(5).unwrap(); + let sigscript = sig_builder.drain(); + + let session = make_session( + vec![DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }], + vec![DebugVariableUpdate { + name: "x".to_string(), + type_name: "int".to_string(), + expr: Expr::Identifier("missing".to_string()), + bytecode_offset: 0, + span: None, + function: "f".to_string(), + sequence: 0, + frame_id: 0, + }], + &sigscript, + ) + .unwrap(); + + let vars = session.list_variables().unwrap(); + let x = vars.into_iter().find(|var| var.name == "x").expect("x variable"); + assert!(matches!(x.value, DebugValue::Unknown(_))); + } +} diff --git a/silverscript-lang/src/lib.rs b/silverscript-lang/src/lib.rs index abe745b8..3ace8479 100644 --- a/silverscript-lang/src/lib.rs +++ b/silverscript-lang/src/lib.rs @@ -1,3 +1,4 @@ pub mod ast; pub mod compiler; +pub mod debug; pub mod parser; diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 8b75ddf6..e35f3571 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -14,6 +14,8 @@ use kaspa_txscript::{EngineCtx, EngineFlags, SeqCommitAccessor, TxScriptEngine, use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, CompiledContract, compile_contract, compile_contract_ast, function_branch_index}; +const OPTIONS: CompileOptions = CompileOptions { allow_yield: false, allow_entrypoint_return: false, record_debug_infos: false }; + fn run_script_with_selector(script: Vec, selector: Option) -> Result<(), kaspa_txscript_errors::TxScriptError> { let sigscript = selector_sigscript(selector); run_script_with_sigscript(script, sigscript) @@ -590,8 +592,7 @@ fn compiles_int_array_length_to_expected_script() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -630,8 +631,7 @@ fn compiles_int_array_push_to_expected_script() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -678,8 +678,7 @@ fn compiles_int_array_index_to_expected_script() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -735,8 +734,7 @@ fn runs_array_runtime_examples() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array runtime example failed: {}", result.unwrap_err()); @@ -753,8 +751,7 @@ fn compiles_bytes20_array_push_without_num2bin() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let value = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14]; @@ -802,8 +799,7 @@ fn runs_bytes20_array_runtime_example() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "bytes20 array runtime example failed: {}", result.unwrap_err()); @@ -822,8 +818,7 @@ fn allows_array_equality_comparison() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array equality runtime failed: {}", result.unwrap_err()); @@ -842,8 +837,7 @@ fn fails_array_equality_comparison() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_err()); @@ -863,8 +857,7 @@ fn allows_array_inequality_with_different_sizes() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array inequality runtime failed: {}", result.unwrap_err()); @@ -885,8 +878,7 @@ fn runs_array_for_loop_example() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array for-loop runtime failed: {}", result.unwrap_err()); @@ -908,8 +900,7 @@ fn runs_array_for_loop_with_length_guard() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = compiled.build_sig_script("main", vec![vec![1i64, 2i64, 3i64, 4i64].into()]).expect("sigscript builds"); @@ -962,8 +953,7 @@ fn allows_array_assignment_with_compatible_types() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array assignment runtime failed: {}", result.unwrap_err()); @@ -978,8 +968,7 @@ fn rejects_unsized_array_type() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - assert!(compile_contract(source, &[], options).is_err()); + assert!(compile_contract(source, &[], OPTIONS).is_err()); } #[test] @@ -992,8 +981,7 @@ fn rejects_array_element_assignment() { } } "#; - let options = CompileOptions { allow_yield: false, allow_entrypoint_return: false }; - assert!(compile_contract(source, &[], options).is_err()); + assert!(compile_contract(source, &[], OPTIONS).is_err()); } #[test] @@ -1007,7 +995,7 @@ fn locking_bytecode_p2pk_matches_pay_to_address_script() { } "#; - let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let pubkey = vec![0x11u8; 32]; let address = Address::new(Prefix::Mainnet, Version::PubKey, &pubkey); let spk = pay_to_address_script(&address); @@ -1031,7 +1019,7 @@ fn locking_bytecode_p2sh_matches_pay_to_address_script() { } "#; - let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let hash = vec![0x22u8; 32]; let address = Address::new(Prefix::Mainnet, Version::ScriptHash, &hash); let spk = pay_to_address_script(&address); @@ -1055,7 +1043,7 @@ fn locking_bytecode_p2sh_from_redeem_script_matches_pay_to_script_hash_script() } "#; - let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); let redeem_script = vec![OpTrue]; let spk = pay_to_script_hash_script(&redeem_script); let mut expected = Vec::new(); diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index 1fef2c33..e0606afc 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, Statement, parse_contract_ast}; +use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; fn extract_first_expr(source: &str) -> Expr { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; - match statement { - Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - Statement::Require { expr, .. } => expr.clone(), + match &statement.kind { + StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + StatementKind::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs new file mode 100644 index 00000000..0e22cfa8 --- /dev/null +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -0,0 +1,360 @@ +use std::collections::HashSet; +use std::error::Error; +use std::fs; +use std::path::PathBuf; + +use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_txscript::caches::Cache; +use kaspa_txscript::{EngineCtx, EngineFlags}; + +use silverscript_lang::ast::{Expr, parse_contract_ast}; +use silverscript_lang::compiler::{CompileOptions, compile_contract}; +use silverscript_lang::debug::MappingKind; +use silverscript_lang::debug::session::DebugSession; + +fn example_contract_path() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir.join("tests/examples/if_statement.sil") +} + +// Convenience harness for the canonical example contract used by baseline session tests. +fn with_session(mut f: F) -> Result<(), Box> +where + F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, +{ + let contract_path = example_contract_path(); + assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); + + let source = fs::read_to_string(&contract_path)?; + with_session_for_source( + &source, + vec![Expr::Int(3), Expr::Int(10)], + "hello", + vec![Expr::Int(5), Expr::Int(5)], + &mut f, + ) +} + +// Generic harness that compiles a contract and boots a debugger session for a selected function call. +fn with_session_for_source( + source: &str, + ctor_args: Vec, + function_name: &str, + function_args: Vec, + mut f: F, +) -> Result<(), Box> +where + F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, +{ + let parsed_contract = parse_contract_ast(source)?; + assert_eq!(parsed_contract.params.len(), ctor_args.len()); + + // Compile with debug metadata enabled so line mappings and variable updates are available. + let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &ctor_args, compile_opts)?; + let debug_info = compiled.debug_info.clone(); + + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); + + let flags = EngineFlags { covenants_enabled: true }; + let engine = silverscript_lang::debug::session::DebugEngine::new(ctx, flags); + + let entry = compiled + .abi + .iter() + .find(|entry| entry.name == function_name) + .ok_or_else(|| format!("function '{function_name}' not found"))?; + + assert_eq!(entry.inputs.len(), function_args.len()); + + // Seed stack with sigscript args and then execute the lockscript in debug mode. + let sigscript = compiled.build_sig_script(function_name, function_args)?; + let mut session = DebugSession::full(&sigscript, &compiled.script, source, debug_info, engine)?; + + f(&mut session) +} + +#[test] +fn debug_session_provides_source_context_and_vars() -> Result<(), Box> { + with_session(|session| { + // Skip dispatcher setup and land on first user statement. + session.run_to_first_executed_statement()?; + let context = session.source_context(); + assert!(context.is_some(), "expected source context"); + + let vars = session.list_variables().expect("variables available"); + let names = vars.iter().map(|var| var.name.as_str()).collect::>(); + assert!(names.contains("a"), "expected param 'a' in variables"); + assert!(names.contains("b"), "expected param 'b' in variables"); + + Ok(()) + }) +} + +#[test] +fn debug_session_steps_forward() -> Result<(), Box> { + with_session(|session| { + session.run_to_first_executed_statement()?; + let before = session.state().pc; + let before_span = session.current_span(); + session.step_statement()?; + let after = session.state().pc; + let after_span = session.current_span(); + assert!(after > before || after_span != before_span, "expected statement step to make source progress"); + Ok(()) + }) +} + +#[test] +fn debug_session_breakpoint_management() -> Result<(), Box> { + with_session(|session| { + session.run_to_first_executed_statement()?; + let span = session.current_span().ok_or("no current span")?; + let line = span.line; + + session.add_breakpoint(line); + assert!(session.breakpoints().contains(&line)); + + session.clear_breakpoint(line); + assert!(!session.breakpoints().contains(&line)); + Ok(()) + }) +} + +#[test] +fn debug_session_tracks_array_assignment_updates() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Arr() { + entrypoint function main() { + int[] a; + int[] b; + b.push(1); + a = b; + require(length(a) == 1); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![], |session| { + session.run_to_first_executed_statement()?; + assert!(session.add_breakpoint(9), "require line should accept breakpoints"); + session.continue_to_breakpoint()?; + + let a = session.variable_by_name("a")?; + assert_eq!(session.format_value(&a.type_name, &a.value), "[1]"); + Ok(()) + }) +} + +#[test] +fn debug_session_hits_multiline_breakpoints() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract BP() { + entrypoint function main(int a) { + require(a == 1); + require(a == 1); + require( + a == 1 + ); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(1)], |session| { + session.run_to_first_executed_statement()?; + // Line 8 is inside a multiline `require(...)` span and should still be hit. + assert!(session.add_breakpoint(8), "expected breakpoint line to be valid"); + + let hit = session.continue_to_breakpoint()?; + assert!(hit.is_some(), "expected to stop at multiline statement breakpoint"); + + let span = session.current_span().ok_or("expected source span at breakpoint")?; + assert!((span.line..=span.end_line).contains(&8)); + Ok(()) + }) +} + +#[test] +fn debug_session_dedupes_shadowed_constructor_constants() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Shadow(int x) { + entrypoint function main(int x) { + require(x == x); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(7)], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + + // Function param `x` should shadow constructor constant `x` in visible debugger variables. + let vars = session.list_variables()?; + let x_count = vars.iter().filter(|var| var.name == "x").count(); + assert_eq!(x_count, 1, "expected a single visible x variable"); + + let x = session.variable_by_name("x")?; + assert!(!x.is_constant, "function parameter should shadow constructor constant"); + assert_eq!(session.format_value(&x.type_name, &x.value), "3"); + Ok(()) + }) +} + +#[test] +fn debug_session_exposes_virtual_steps() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Virtuals() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + let first = session.current_location().ok_or("missing first location")?; + assert!(matches!(first.kind, MappingKind::Virtual {})); + let first_pc = session.state().pc; + + let second = session.step_over()?.ok_or("missing second step")?.mapping.ok_or("missing second mapping")?; + assert!(matches!(second.kind, MappingKind::Virtual {})); + assert_eq!(session.state().pc, first_pc, "virtual step should not execute opcodes"); + + let third = session.step_over()?.ok_or("missing third step")?.mapping.ok_or("missing third mapping")?; + assert!(matches!(third.kind, MappingKind::Statement {})); + assert_eq!(session.state().pc, first_pc, "first real statement should still be at same pc boundary"); + Ok(()) + }) +} + +#[test] +fn debug_session_breakpoint_hits_virtual_line() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract VirtualBp() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + assert!(session.add_breakpoint(6), "line with virtual assignment should be a valid breakpoint"); + let hit = session.continue_to_breakpoint()?; + assert!(hit.is_some(), "expected breakpoint on virtual line"); + let span = session.current_span().ok_or("missing span at virtual breakpoint")?; + assert_eq!(span.line, 6); + Ok(()) + }) +} + +#[test] +fn debug_session_inline_stepping_supports_into_over_out() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract InlineStep() { + function add1(int x) : (int) { + int y = x + 1; + require(y > 0); + return(y); + } + + entrypoint function main(int a) { + int seed = a; + (int r) = add1(seed); + require(r > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { + session.run_to_first_executed_statement()?; + let root = session.current_location().ok_or("missing root mapping")?; + assert_eq!(root.call_depth, 0); + + let into = session.step_into()?.ok_or("step into failed")?.mapping.ok_or("missing mapping after step into")?; + assert_eq!(into.call_depth, 1, "step into should enter inline callee"); + assert!( + session.call_stack().iter().any(|name| name == "add1"), + "inline call stack should include callee name" + ); + + let out = session.step_out()?.ok_or("step out failed")?.mapping.ok_or("missing mapping after step out")?; + assert_eq!(out.call_depth, 0, "step out should return to caller depth"); + Ok(()) + })?; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { + session.run_to_first_executed_statement()?; + let over = session.step_over()?.ok_or("step over failed")?.mapping.ok_or("missing mapping after step over")?; + assert_eq!(over.call_depth, 0, "step over should stay in caller depth"); + Ok(()) + }) +} + +#[test] +fn debug_session_inline_params_visible_inside_callee() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract InlineParams() { + function add1(int x) : (int) { + int y = x + 1; + require(y > 0); + return(y); + } + + entrypoint function main(int a) { + int seed = a; + (int r) = add1(seed); + require(r > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { + session.run_to_first_executed_statement()?; + session.step_into()?; + + let x = session.variable_by_name("x")?; + let rendered = session.format_value(&x.type_name, &x.value); + assert_eq!(rendered, "4", "inline param x should be visible inside callee"); + Ok(()) + }) +} + +#[test] +fn debug_session_function_call_assign_resolves_inline_args() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract InlineAssign() { + function inc(int x) : (int) { + return(x + 1); + } + + entrypoint function main(int a) { + int seed = a; + (int r) = inc(seed); + require(r == a + 1); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(5)], |session| { + session.run_to_first_executed_statement()?; + session.step_over()?; + let r = session.variable_by_name("r")?; + let rendered = session.format_value(&r.type_name, &r.value); + assert_eq!(rendered, "6"); + Ok(()) + }) +} diff --git a/silverscript-lang/tests/debugger_cli_tests.rs b/silverscript-lang/tests/debugger_cli_tests.rs new file mode 100644 index 00000000..6818a0cf --- /dev/null +++ b/silverscript-lang/tests/debugger_cli_tests.rs @@ -0,0 +1,50 @@ +use std::io::Write; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +fn example_contract_path() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir.join("tests/examples/if_statement.sil") +} + +#[test] +fn sil_debug_repl_all_commands_smoke() { + let contract_path = example_contract_path(); + assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); + + let mut child = Command::new(env!("CARGO_BIN_EXE_sil-debug")) + .arg(contract_path) + .arg("--function") + .arg("hello") + .arg("--ctor-arg") + .arg("3") + .arg("--ctor-arg") + .arg("10") + .arg("--arg") + .arg("5") + .arg("--arg") + .arg("5") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn sil-debug"); + + let input = b"help\nl\nstack\nb 1\nb 7\nb\nn\nsi\nq\n"; + child.stdin.as_mut().expect("stdin available").write_all(input).expect("write stdin"); + + let output = child.wait_with_output().expect("wait for sil-debug"); + assert!(output.status.success(), "sil-debug exited with status {:?}", output.status.code()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(stderr.is_empty(), "unexpected stderr: {stderr}"); + assert!(stdout.contains("Stepping through"), "missing startup output"); + assert!(stdout.contains("(sdb)"), "missing prompt output"); + assert!(stdout.contains("Commands:"), "missing help output"); + assert!(stdout.contains("Stack:"), "missing stack output"); + assert!(stdout.contains("no statement at line 1"), "missing invalid breakpoint warning"); + assert!(stdout.contains("Breakpoint set at line 7"), "missing breakpoint confirmation"); + assert!(stdout.contains("Breakpoints: 7"), "missing breakpoint listing"); +} From fe32bac658f9d9ced9a2a45b50609a797a28b5ca Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:35:24 +0200 Subject: [PATCH 02/41] fmt --- silverscript-lang/src/compiler.rs | 7 ++--- .../src/compiler/debug_recording.rs | 28 +++-------------- silverscript-lang/src/debug/session.rs | 31 +++++++------------ .../tests/debug_session_tests.rs | 13 ++------ 4 files changed, 20 insertions(+), 59 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 3a16c437..39e22ee3 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1297,10 +1297,9 @@ fn resolve_expr_internal( start: Box::new(resolve_expr_internal(*start, env, visiting, preserve_inline_args)?), end: Box::new(resolve_expr_internal(*end, env, visiting, preserve_inline_args)?), }), - Expr::Introspection { kind, index } => Ok(Expr::Introspection { - kind, - index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), - }), + Expr::Introspection { kind, index } => { + Ok(Expr::Introspection { kind, index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?) }) + } other => Ok(other), } } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 0551f1cf..c5db0e51 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -132,32 +132,12 @@ impl FunctionDebugRecorder { Ok(()) } - pub fn record_inline_call_enter( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - ) -> Option { - self.push_event( - bytecode_offset, - bytecode_offset, - span, - DebugEventKind::InlineCallEnter { callee: callee.to_string() }, - ) + pub fn record_inline_call_enter(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Option { + self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallEnter { callee: callee.to_string() }) } - pub fn record_inline_call_exit( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - ) -> Option { - self.push_event( - bytecode_offset, - bytecode_offset, - span, - DebugEventKind::InlineCallExit { callee: callee.to_string() }, - ) + pub fn record_inline_call_exit(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Option { + self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallExit { callee: callee.to_string() }) } pub fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index a98ac257..6fe1a149 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -232,9 +232,9 @@ impl<'a> DebugSession<'a> { let mut search_from = self.current_step_index; loop { - let Some(target_index) = self.next_steppable_mapping_index(search_from, |mapping| { - predicate(mapping.call_depth, current_depth) - }) else { + let Some(target_index) = + self.next_steppable_mapping_index(search_from, |mapping| predicate(mapping.call_depth, current_depth)) + else { while self.step_opcode()?.is_some() {} return Ok(None); }; @@ -284,9 +284,11 @@ impl<'a> DebugSession<'a> { } let offset = self.current_byte_offset(); if self.engine.is_executing() { - let found = self.source_mappings.iter().enumerate().find(|(_, mapping)| { - self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset) - }); + let found = self + .source_mappings + .iter() + .enumerate() + .find(|(_, mapping)| self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset)); if let Some((index, _)) = found { self.current_step_index = Some(index); return Ok(()); @@ -320,12 +322,7 @@ impl<'a> DebugSession<'a> { pub fn state(&self) -> SessionState { let executed = self.pc.saturating_sub(1); let opcode = self.op_displays.get(executed).cloned(); - SessionState { - pc: self.pc, - opcode, - mapping: self.current_location(), - stack: self.stack(), - } + SessionState { pc: self.pc, opcode, mapping: self.current_location(), stack: self.stack() } } /// Returns true if the script engine is still running. @@ -581,9 +578,7 @@ impl<'a> DebugSession<'a> { /// Returns the debug mapping for the current bytecode position. pub fn current_location(&self) -> Option { - self.current_step_mapping() - .cloned() - .or_else(|| self.mapping_for_offset(self.current_byte_offset()).cloned()) + self.current_step_mapping().cloned().or_else(|| self.mapping_for_offset(self.current_byte_offset()).cloned()) } /// Returns the current bytecode offset in the script. @@ -688,11 +683,7 @@ impl<'a> DebugSession<'a> { matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {}) } - fn next_steppable_mapping_index( - &self, - from: Option, - predicate: impl Fn(&DebugMapping) -> bool, - ) -> Option { + fn next_steppable_mapping_index(&self, from: Option, predicate: impl Fn(&DebugMapping) -> bool) -> Option { let start = from.map(|index| index.saturating_add(1)).unwrap_or(0); for index in start..self.source_mappings.len() { let mapping = self.source_mappings.get(index)?; diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 0e22cfa8..be3af599 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -26,13 +26,7 @@ where assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); let source = fs::read_to_string(&contract_path)?; - with_session_for_source( - &source, - vec![Expr::Int(3), Expr::Int(10)], - "hello", - vec![Expr::Int(5), Expr::Int(5)], - &mut f, - ) + with_session_for_source(&source, vec![Expr::Int(3), Expr::Int(10)], "hello", vec![Expr::Int(5), Expr::Int(5)], &mut f) } // Generic harness that compiles a contract and boots a debugger session for a selected function call. @@ -284,10 +278,7 @@ contract InlineStep() { let into = session.step_into()?.ok_or("step into failed")?.mapping.ok_or("missing mapping after step into")?; assert_eq!(into.call_depth, 1, "step into should enter inline callee"); - assert!( - session.call_stack().iter().any(|name| name == "add1"), - "inline call stack should include callee name" - ); + assert!(session.call_stack().iter().any(|name| name == "add1"), "inline call stack should include callee name"); let out = session.step_out()?.ok_or("step out failed")?.mapping.ok_or("missing mapping after step out")?; assert_eq!(out.call_depth, 0, "step out should return to caller depth"); From 11af7b24f63d89fce3d9a6ac5104cc34bae32919 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:25:30 +0200 Subject: [PATCH 03/41] Fix clippy warning and apply debugger lint fixes --- silverscript-lang/src/compiler.rs | 1 - silverscript-lang/src/debug/session.rs | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 39e22ee3..e943ec71 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -981,7 +981,6 @@ impl<'a> FunctionBodyCompiler<'a> { callee_compiler.compile_statement(stmt, &mut env, ¶ms, &mut types, &mut yields)?; } self.inline_frame_counter = callee_compiler.inline_frame_counter; - drop(callee_compiler); // Remap inline-local sequence numbers and merge events/updates back into // the parent function recorder. self.debug_recorder.merge_inline_events(&debug_recorder); diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 6fe1a149..c4fd3adf 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -259,11 +259,10 @@ impl<'a> DebugSession<'a> { return Ok(false); } - if mapping_matches_offset(&target, offset) { - if self.engine.is_executing() { + if mapping_matches_offset(&target, offset) + && self.engine.is_executing() { return Ok(true); } - } if self.step_opcode()?.is_none() { return Ok(false); @@ -334,8 +333,8 @@ impl<'a> DebugSession<'a> { pub fn stacks_snapshot(&self) -> StackSnapshot { let stacks = self.engine.stacks(); StackSnapshot { - dstack: stacks.dstack.iter().map(|item| hex::encode(item)).collect(), - astack: stacks.astack.iter().map(|item| hex::encode(item)).collect(), + dstack: stacks.dstack.iter().map(hex::encode).collect(), + astack: stacks.astack.iter().map(hex::encode).collect(), } } @@ -391,7 +390,7 @@ impl<'a> DebugSession<'a> { .source_mappings .iter() .filter(|mapping| self.is_steppable_mapping(mapping)) - .any(|mapping| mapping.span.map_or(false, |span| line >= span.line && line <= span.end_line)); + .any(|mapping| mapping.span.is_some_and(|span| line >= span.line && line <= span.end_line)); if valid { self.breakpoints.insert(line); } @@ -704,7 +703,7 @@ impl<'a> DebugSession<'a> { /// Returns the current main stack as hex-encoded strings. pub fn stack(&self) -> Vec { let stacks = self.engine.stacks(); - stacks.dstack.iter().map(|item| hex::encode(item)).collect() + stacks.dstack.iter().map(hex::encode).collect() } fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate) -> Result { From 3dc23d9c33cf54116f9ce8c9e7f07c03a5cb977c Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:43:19 +0200 Subject: [PATCH 04/41] Restore state transition support on debugger branch --- silverscript-lang/src/ast.rs | 151 +++- silverscript-lang/src/compiler.rs | 650 ++++++++++++++---- .../src/compiler/debug_recording.rs | 150 ++-- silverscript-lang/src/debug/session.rs | 7 +- silverscript-lang/src/silverscript.pest | 32 +- silverscript-lang/tests/compiler_tests.rs | 20 + .../tests/examples/covenant_id.sil | 33 + silverscript-lang/tests/examples_tests.rs | 112 ++- 8 files changed, 902 insertions(+), 253 deletions(-) create mode 100644 silverscript-lang/tests/examples/covenant_id.sil diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 71ff84b3..be946b97 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -12,10 +12,19 @@ use chrono::NaiveDateTime; pub struct ContractAst { pub name: String, pub params: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub fields: Vec, pub constants: HashMap, pub functions: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractFieldAst { + pub type_name: String, + pub name: String, + pub expr: Expr, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct SourceSpan { pub line: u32, @@ -49,6 +58,13 @@ pub struct ParamAst { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateBindingAst { + pub field_name: String, + pub type_name: String, + pub name: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Statement { #[serde(skip)] @@ -65,6 +81,7 @@ pub enum StatementKind { ArrayPush { name: String, expr: Expr }, FunctionCall { name: String, args: Vec }, FunctionCallAssign { bindings: Vec, name: String, args: Vec }, + StateFunctionCallAssign { bindings: Vec, name: String, args: Vec }, Assign { name: String, expr: Expr }, TimeOp { tx_var: TimeVar, expr: Expr, message: Option }, Require { expr: Expr, message: Option }, @@ -108,6 +125,13 @@ pub enum Expr { IfElse { condition: Box, then_expr: Box, else_expr: Box }, Nullary(NullaryOp), Introspection { kind: IntrospectionKind, index: Box }, + StateObject(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StateFieldExpr { + pub name: String, + pub expr: Expr, } impl From for Expr { @@ -134,6 +158,17 @@ impl From for Expr { } } +fn normalize_type_name(raw: &str) -> String { + let type_name = raw.trim(); + if type_name == "byte[]" { + return "bytes".to_string(); + } + if let Some(size) = type_name.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok()) { + return format!("bytes{size}"); + } + type_name.to_string() +} + impl From> for Expr { fn from(value: Vec) -> Self { Expr::Array(value.into_iter().map(Expr::Int).collect()) @@ -231,6 +266,7 @@ fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result = HashMap::new(); for item_pair in inner { @@ -243,6 +279,22 @@ fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result { functions.push(parse_function_definition(inner_item)?); } + Rule::contract_field_definition => { + let mut field_inner = inner_item.into_inner(); + let type_name = normalize_type_name( + field_inner + .next() + .ok_or_else(|| CompilerError::Unsupported("missing field type".to_string()))? + .as_str() + .trim(), + ); + let name_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field name".to_string()))?; + validate_user_identifier(name_pair.as_str())?; + let expr_pair = + field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field initializer".to_string()))?; + let expr = parse_expression(expr_pair)?; + fields.push(ContractFieldAst { type_name, name: name_pair.as_str().to_string(), expr }); + } Rule::constant_definition => { let mut const_inner = inner_item.into_inner(); let _type_name = @@ -260,7 +312,7 @@ fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result) -> Result { @@ -306,12 +358,9 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let kind = match pair.as_rule() { Rule::variable_definition => { let mut inner = pair.into_inner(); - let type_name = inner - .next() - .ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))? - .as_str() - .trim() - .to_string(); + let type_name = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))?.as_str().trim(), + ); let mut modifiers = Vec::new(); while let Some(p) = inner.peek() { @@ -328,11 +377,13 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } Rule::tuple_assignment => { let mut inner = pair.into_inner(); - let left_type = - inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()))?.as_str().to_string(); + let left_type = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()))?.as_str(), + ); let left_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple name".to_string()))?; - let right_type = - inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()))?.as_str().to_string(); + let right_type = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()))?.as_str(), + ); let right_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple name".to_string()))?; validate_user_identifier(left_ident.as_str())?; validate_user_identifier(right_ident.as_str())?; @@ -405,26 +456,55 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { for item in pair.into_inner() { if item.as_rule() == Rule::typed_binding { let mut inner = item.into_inner(); - let type_name = inner + let type_name = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?.as_str().trim(), + ); + let name = inner + .next() + .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? + .as_str() + .to_string(); + validate_user_identifier(&name)?; + bindings.push(ParamAst { type_name, name }); + } else if item.as_rule() == Rule::function_call { + call_pair = Some(item); + } + } + let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; + match parse_function_call(call_pair)? { + Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), + } + } + Rule::state_function_call_assignment => { + let mut bindings = Vec::new(); + let mut call_pair = None; + for item in pair.into_inner() { + if item.as_rule() == Rule::state_typed_binding { + let mut inner = item.into_inner(); + let field_name = inner .next() - .ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))? + .ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))? .as_str() - .trim() .to_string(); + validate_user_identifier(&field_name)?; + let type_name = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?.as_str().trim(), + ); let name = inner .next() .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? .as_str() .to_string(); validate_user_identifier(&name)?; - bindings.push(ParamAst { type_name, name }); + bindings.push(StateBindingAst { field_name, type_name, name }); } else if item.as_rule() == Rule::function_call { call_pair = Some(item); } } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, + Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -531,6 +611,7 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result { Rule::NullaryOp => parse_nullary(pair.as_str()), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), + Rule::state_object => parse_state_object(pair), Rule::function_call => parse_function_call(pair), Rule::instantiation => parse_instantiation(pair), Rule::cast => parse_cast(pair), @@ -540,8 +621,8 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result { | Rule::unary_suffix | Rule::StringLiteral | Rule::DateLiteral - | Rule::Bytes - | Rule::type_name => Err(CompilerError::Unsupported(format!("expression not supported: {:?}", pair.as_rule()))), + | Rule::type_name + | Rule::state_entry => Err(CompilerError::Unsupported(format!("expression not supported: {:?}", pair.as_rule()))), _ => Err(CompilerError::Unsupported(format!("unexpected expression: {:?}", pair.as_rule()))), } } @@ -631,8 +712,9 @@ fn parse_typed_parameter_list(pair: Pair<'_, Rule>) -> Result, Com continue; } let mut inner = param.into_inner(); - let type_name = - inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter type".to_string()))?.as_str().trim().to_string(); + let type_name = normalize_type_name( + inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter type".to_string()))?.as_str().trim(), + ); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter name".to_string()))?.as_str().to_string(); validate_user_identifier(&ident)?; params.push(ParamAst { type_name, name: ident }); @@ -644,7 +726,7 @@ fn parse_return_type_list(pair: Pair<'_, Rule>) -> Result, CompilerE let mut types = Vec::new(); for item in pair.into_inner() { if item.as_rule() == Rule::type_name { - types.push(item.as_str().trim().to_string()); + types.push(normalize_type_name(item.as_str().trim())); } } Ok(types) @@ -658,6 +740,7 @@ fn parse_primary(pair: Pair<'_, Rule>) -> Result { Rule::NullaryOp => parse_nullary(pair.as_str()), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), + Rule::state_object => parse_state_object(pair), Rule::function_call => parse_function_call(pair), Rule::instantiation => parse_instantiation(pair), Rule::cast => parse_cast(pair), @@ -666,6 +749,23 @@ fn parse_primary(pair: Pair<'_, Rule>) -> Result { } } +fn parse_state_object(pair: Pair<'_, Rule>) -> Result { + let mut fields = Vec::new(); + for field_pair in pair.into_inner() { + if field_pair.as_rule() != Rule::state_entry { + continue; + } + let mut inner = field_pair.into_inner(); + let name = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))?.as_str().to_string(); + validate_user_identifier(&name)?; + let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field expression".to_string()))?; + let expr = parse_expression(expr_pair)?; + fields.push(StateFieldExpr { name, expr }); + } + Ok(Expr::StateObject(fields)) +} + fn parse_literal(pair: Pair<'_, Rule>) -> Result { match pair.as_rule() { Rule::BooleanLiteral => Ok(Expr::Bool(pair.as_str() == "true")), @@ -739,7 +839,8 @@ fn parse_expression_list(pair: Pair<'_, Rule>) -> Result, CompilerErro fn parse_cast(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); - let type_name = inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?.as_str().to_string(); + let type_name = + normalize_type_name(inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?.as_str()); let args = match inner.next() { Some(list) => parse_expression_list(list)?, None => Vec::new(), @@ -837,7 +938,7 @@ fn parse_string_literal(pair: Pair<'_, Rule>) -> Result { fn parse_nullary(raw: &str) -> Result { let op = match raw { "this.activeInputIndex" => NullaryOp::ActiveInputIndex, - "this.activeBytecode" => NullaryOp::ActiveBytecode, + "this.activeBytecode" | "this.activeScriptPubKey" => NullaryOp::ActiveBytecode, "this.scriptSize" => NullaryOp::ThisScriptSize, "this.scriptSizeDataPrefix" => NullaryOp::ThisScriptSizeDataPrefix, "tx.inputs.length" => NullaryOp::TxInputsLength, @@ -861,13 +962,13 @@ fn parse_introspection(pair: Pair<'_, Rule>) -> Result { let kind = if text.starts_with("tx.inputs") { match field { ".value" => IntrospectionKind::InputValue, - ".lockingBytecode" => IntrospectionKind::InputLockingBytecode, + ".lockingBytecode" | ".scriptPubKey" => IntrospectionKind::InputLockingBytecode, _ => return Err(CompilerError::Unsupported(format!("input field '{field}' not supported"))), } } else if text.starts_with("tx.outputs") { match field { ".value" => IntrospectionKind::OutputValue, - ".lockingBytecode" => IntrospectionKind::OutputLockingBytecode, + ".lockingBytecode" | ".scriptPubKey" => IntrospectionKind::OutputLockingBytecode, _ => return Err(CompilerError::Unsupported(format!("output field '{field}' not supported"))), } } else { diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index e943ec71..288ad208 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - BinaryOp, ConsoleArg, ContractAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, SplitPart, Statement, - StatementKind, TimeVar, UnaryOp, parse_contract_ast, + BinaryOp, ConsoleArg, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, SplitPart, + StateBindingAst, Statement, StatementKind, TimeVar, UnaryOp, parse_contract_ast, }; use crate::debug::DebugInfo; use crate::debug::labels::synthetic; @@ -118,6 +118,10 @@ fn compile_contract_impl( let mut script_size = if uses_script_size { Some(100i64) } else { None }; for _ in 0..32 { + let (contract_fields, field_prolog_script) = compile_contract_fields(&contract.fields, &constants, options, script_size)?; + let mut scoped_constants = constants.clone(); + scoped_constants.extend(contract_fields); + let mut compiled_entrypoints = Vec::new(); // Create a recorder (active/non-active based on compilation options) to collect debug info let mut recorder = DebugSink::new(options.record_debug_infos); @@ -128,7 +132,9 @@ fn compile_contract_impl( compiled_entrypoints.push(compile_function( func, index, - &constants, + &contract.fields, + field_prolog_script.len(), + &scoped_constants, options, &functions_map, &function_order, @@ -138,13 +144,18 @@ fn compile_contract_impl( } let script = if without_selector { + let mut builder = ScriptBuilder::new(); + builder.add_ops(&field_prolog_script)?; let compiled = compiled_entrypoints .first() .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, 0); - compiled.script.clone() + let func_start = builder.script().len(); + builder.add_ops(&compiled.script)?; + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, func_start); + builder.drain() } else { let mut builder = ScriptBuilder::new(); + builder.add_ops(&field_prolog_script)?; let total = compiled_entrypoints.len(); for (index, compiled) in compiled_entrypoints.iter().enumerate() { @@ -223,6 +234,9 @@ fn contract_uses_script_size(contract: &ContractAst) -> bool { if contract.constants.values().any(expr_uses_script_size) { return true; } + if contract.fields.iter().any(|field| expr_uses_script_size(&field.expr)) { + return true; + } contract.functions.iter().any(|func| func.body.iter().any(statement_uses_script_size)) } @@ -231,8 +245,11 @@ fn statement_uses_script_size(stmt: &Statement) -> bool { StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), - StatementKind::FunctionCall { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::FunctionCall { name, args, .. } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::StateFunctionCallAssign { name, args, .. } => { + name == "readInputState" || args.iter().any(expr_uses_script_size) + } StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), StatementKind::Require { expr, .. } => expr_uses_script_size(expr), @@ -269,6 +286,7 @@ fn expr_uses_script_size(expr: &Expr) -> bool { } Expr::Nullary(op) => matches!(op, NullaryOp::ThisScriptSize | NullaryOp::ThisScriptSizeDataPrefix), Expr::Introspection { index, .. } => expr_uses_script_size(index), + Expr::StateObject(fields) => fields.iter().any(|field| expr_uses_script_size(&field.expr)), } } @@ -347,6 +365,109 @@ fn array_element_size(type_name: &str) -> Option { array_element_type(type_name).and_then(fixed_type_size) } +fn compile_contract_fields( + fields: &[ContractFieldAst], + base_constants: &HashMap, + options: CompileOptions, + script_size: Option, +) -> Result<(HashMap, Vec), CompilerError> { + let mut env = base_constants.clone(); + let mut field_values = HashMap::new(); + let mut field_types = HashMap::new(); + let mut builder = ScriptBuilder::new(); + let params = HashMap::new(); + + for field in fields { + if env.contains_key(&field.name) { + return Err(CompilerError::Unsupported(format!("duplicate contract field name: {}", field.name))); + } + + if is_array_type(&field.type_name) && array_element_size(&field.type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", field.type_name))); + } + + let mut resolve_visiting = HashSet::new(); + let resolved = resolve_expr(field.expr.clone(), &env, &mut resolve_visiting)?; + if !expr_matches_type(&resolved, &field.type_name) { + return Err(CompilerError::Unsupported(format!("contract field '{}' expects {}", field.name, field.type_name))); + } + + if field.type_name == "int" { + let Expr::Int(value) = resolved else { + return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); + }; + builder.add_data(&value.to_le_bytes())?; + builder.add_op(OpBin2Num)?; + env.insert(field.name.clone(), Expr::Int(value)); + field_values.insert(field.name.clone(), Expr::Int(value)); + field_types.insert(field.name.clone(), field.type_name.clone()); + continue; + } + + let mut compile_visiting = HashSet::new(); + let mut stack_depth = 0i64; + compile_expr( + &resolved, + &env, + ¶ms, + &field_types, + &mut builder, + options, + &mut compile_visiting, + &mut stack_depth, + script_size, + )?; + + env.insert(field.name.clone(), resolved.clone()); + field_values.insert(field.name.clone(), resolved); + field_types.insert(field.name.clone(), field.type_name.clone()); + } + + Ok((field_values, builder.drain())) +} + +fn fixed_field_byte_len(type_name: &str) -> Option { + match type_name { + "byte" => Some(1), + _ => type_name.strip_prefix("bytes").and_then(|v| v.parse::().ok()), + } +} + +fn encoded_field_chunk_size(field: &ContractFieldAst) -> Result { + if field.type_name == "int" { + return Ok(10); + } + let payload_size = fixed_field_byte_len(&field.type_name) + .ok_or_else(|| CompilerError::Unsupported(format!("readInputState does not support field type {}", field.type_name)))?; + Ok(data_prefix(payload_size).len() + payload_size) +} + +fn read_input_state_binding_expr( + input_idx: &Expr, + field: &ContractFieldAst, + field_chunk_offset: usize, + script_size_value: i64, +) -> Result { + let (field_payload_offset, field_payload_len, decode_int) = if field.type_name == "int" { + (field_chunk_offset + 1, 8usize, true) + } else { + let payload_len = fixed_field_byte_len(&field.type_name) + .ok_or_else(|| CompilerError::Unsupported(format!("readInputState does not support field type {}", field.type_name)))?; + (field_chunk_offset + data_prefix(payload_len).len(), payload_len, false) + }; + + let sig_len = Expr::Call { name: "OpTxInputScriptSigLen".to_string(), args: vec![input_idx.clone()] }; + let start = Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::Binary { op: BinaryOp::Sub, left: Box::new(sig_len), right: Box::new(Expr::Int(script_size_value)) }), + right: Box::new(Expr::Int(field_payload_offset as i64)), + }; + let end = Expr::Binary { op: BinaryOp::Add, left: Box::new(start.clone()), right: Box::new(Expr::Int(field_payload_len as i64)) }; + let substr = Expr::Call { name: "OpTxInputScriptSigSubstr".to_string(), args: vec![input_idx.clone(), start, end] }; + + if decode_int { Ok(Expr::Call { name: "OpBin2Num".to_string(), args: vec![substr] }) } else { Ok(substr) } +} + fn contains_return(stmt: &Statement) -> bool { match &stmt.kind { StatementKind::Return { .. } => true, @@ -369,6 +490,32 @@ fn contains_yield(stmt: &Statement) -> bool { } } +fn validate_function_body(function: &FunctionAst, options: CompileOptions) -> Result { + let has_yield = function.body.iter().any(contains_yield); + if !options.allow_yield && has_yield { + return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); + } + + let has_return = function.body.iter().any(contains_return); + if function.entrypoint && !options.allow_entrypoint_return && has_return { + return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); + } + + if has_return { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body[..function.body.len() - 1].iter().any(contains_return) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if has_yield { + return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + } + } + + Ok(has_return) +} + fn validate_return_types(exprs: &[Expr], return_types: &[String], types: &HashMap) -> Result<(), CompilerError> { if return_types.is_empty() { return Err(CompilerError::Unsupported("return requires function return types".to_string())); @@ -530,6 +677,8 @@ pub fn function_branch_index(contract: &ContractAst, function_name: &str) -> Res fn compile_function( function: &FunctionAst, function_index: usize, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, constants: &HashMap, options: CompileOptions, functions: &HashMap, @@ -543,14 +692,19 @@ fn compile_function( let mut types = HashMap::new(); let mut params = HashMap::new(); + let contract_field_count = contract_fields.len(); let param_count = function.params.len(); for (index, param) in function.params.iter().enumerate() { if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); } - params.insert(param.name.clone(), (param_count - 1 - index) as i64); + params.insert(param.name.clone(), (contract_field_count + (param_count - 1 - index)) as i64); types.insert(param.name.clone(), param.type_name.clone()); } + for (index, field) in contract_fields.iter().enumerate() { + params.insert(field.name.clone(), (contract_field_count - 1 - index) as i64); + types.insert(field.name.clone(), field.type_name.clone()); + } for return_type in &function.return_types { if is_array_type(return_type) && array_element_size(return_type).is_none() { @@ -558,33 +712,15 @@ fn compile_function( } } - if !options.allow_yield && function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); - } + let has_return = validate_function_body(function, options)?; - if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { - return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); - } - - let has_return = function.body.iter().any(contains_return); - if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if function.body[..function.body.len() - 1].iter().any(contains_return) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); - } - } - - let mut yields: Vec = Vec::new(); - { + let yields = { let mut body_compiler = FunctionBodyCompiler { builder: &mut builder, options, debug_recorder: &mut recorder, + contract_fields, + contract_field_prefix_len, contract_constants: constants, functions, function_order, @@ -592,20 +728,8 @@ fn compile_function( script_size, inline_frame_counter: 1, }; - for stmt in &function.body { - if matches!(stmt.kind, StatementKind::Return { .. }) { - let StatementKind::Return { exprs, .. } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); - } - continue; - } - - body_compiler.compile_statement(stmt, &mut env, ¶ms, &mut types, &mut yields)?; - } - } + body_compiler.compile_function_body(function, &mut env, ¶ms, &mut types)? + }; if function.entrypoint { if !has_return && !function.return_types.is_empty() { @@ -613,7 +737,7 @@ fn compile_function( } let yield_count = yields.len(); if yield_count == 0 { - for _ in 0..param_count { + for _ in 0..(param_count + contract_field_count) { builder.add_op(OpDrop)?; } builder.add_op(OpTrue)?; @@ -622,7 +746,7 @@ fn compile_function( for expr in &yields { compile_expr(expr, &env, ¶ms, &types, &mut builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; } - for _ in 0..param_count { + for _ in 0..(param_count + contract_field_count) { builder.add_i64(yield_count as i64)?; builder.add_op(OpRoll)?; builder.add_op(OpDrop)?; @@ -632,10 +756,13 @@ fn compile_function( Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) } + struct FunctionBodyCompiler<'a> { builder: &'a mut ScriptBuilder, options: CompileOptions, debug_recorder: &'a mut FunctionDebugRecorder, + contract_fields: &'a [ContractFieldAst], + contract_field_prefix_len: usize, contract_constants: &'a HashMap, functions: &'a HashMap, function_order: &'a HashMap, @@ -645,6 +772,66 @@ struct FunctionBodyCompiler<'a> { } impl<'a> FunctionBodyCompiler<'a> { + fn compile_function_body( + &mut self, + function: &FunctionAst, + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + ) -> Result, CompilerError> { + let mut yields = Vec::new(); + for stmt in &function.body { + if let StatementKind::Return { exprs, .. } = &stmt.kind { + validate_return_types(exprs, &function.return_types, types)?; + for expr in exprs { + yields.push(resolve_expr(expr.clone(), env, &mut HashSet::new())?); + } + continue; + } + self.compile_statement(stmt, env, params, types, &mut yields)?; + } + Ok(yields) + } + + fn compile_inline_call_and_discard_returns( + &mut self, + name: &str, + args: &[Expr], + params: &HashMap, + types: &mut HashMap, + env: &mut HashMap, + call_span: Option, + ) -> Result<(), CompilerError> { + let returns = self.compile_inline_call(name, args, params, types, env, call_span)?; + self.compile_and_drop_returns(returns, env, params, types) + } + + fn compile_and_drop_returns( + &mut self, + returns: Vec, + env: &HashMap, + params: &HashMap, + types: &HashMap, + ) -> Result<(), CompilerError> { + let mut stack_depth = 0i64; + for expr in returns { + compile_expr( + &expr, + env, + params, + types, + self.builder, + self.options, + &mut HashSet::new(), + &mut stack_depth, + self.script_size, + )?; + self.builder.add_op(OpDrop)?; + stack_depth -= 1; + } + Ok(()) + } + fn compile_statement( &mut self, stmt: &Statement, @@ -759,24 +946,34 @@ impl<'a> FunctionBodyCompiler<'a> { _ => return Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), }, StatementKind::FunctionCall { name, args, .. } => { - let returns = self.compile_inline_call(name, args, params, types, env, stmt.span)?; - if !returns.is_empty() { - let mut stack_depth = 0i64; - for expr in returns { - compile_expr( - &expr, - env, - params, - types, - self.builder, - self.options, - &mut HashSet::new(), - &mut stack_depth, - self.script_size, - )?; - self.builder.add_op(OpDrop)?; - stack_depth -= 1; + if name == "validateOutputState" { + compile_validate_output_state_statement( + args, + env, + params, + types, + self.builder, + self.options, + self.contract_fields, + self.contract_field_prefix_len, + self.script_size, + )?; + } else { + self.compile_inline_call_and_discard_returns(name, args, params, types, env, stmt.span)?; + } + } + StatementKind::StateFunctionCallAssign { bindings, name, args, .. } => { + if name == "readInputState" { + compile_read_input_state_statement(bindings, args, env, types, self.contract_fields, self.script_size)?; + for binding in bindings { + let expr = env.get(&binding.name).cloned().unwrap_or_else(|| Expr::Identifier(binding.name.clone())); + self.debug_recorder.variable_update(env, &mut variables, &binding.name, &binding.type_name, expr)?; } + } else { + return Err(CompilerError::Unsupported(format!( + "state destructuring assignment is only supported for readInputState(), got '{}()'", + name + ))); } } StatementKind::FunctionCallAssign { bindings, name, args, .. } => { @@ -803,10 +1000,7 @@ impl<'a> FunctionBodyCompiler<'a> { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } for (binding, expr) in bindings.iter().zip(returns.into_iter()) { - if self.options.record_debug_infos { - let resolved = resolve_expr_for_debug(expr.clone(), env, &mut HashSet::new())?; - variables.push((binding.name.clone(), binding.type_name.clone(), resolved)); - } + self.debug_recorder.variable_update(env, &mut variables, &binding.name, &binding.type_name, expr.clone())?; env.insert(binding.name.clone(), expr); types.insert(binding.name.clone(), binding.type_name.clone()); } @@ -859,12 +1053,9 @@ impl<'a> FunctionBodyCompiler<'a> { } let end = self.builder.script().len(); - let stmt_seq = self.debug_recorder.record_statement(stmt, start, end - start); // Record updates at the end of the statement so variables reflect post-statement state // when the debugger is paused at the next byte offset. - if let Some(sequence) = stmt_seq { - self.debug_recorder.record_variable_updates(variables, end, stmt.span, sequence); - } + self.debug_recorder.record_statement_updates(stmt, start, end, variables); Ok(()) } @@ -914,29 +1105,31 @@ impl<'a> FunctionBodyCompiler<'a> { caller_types.insert(temp_name, param.type_name.clone()); } - if !self.options.allow_yield && function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); - } + validate_function_body(function, self.options)?; + let yields = self.compile_inline_callee(name, function, callee_index, call_span, caller_params, &mut env, &mut types)?; - if function.entrypoint && !self.options.allow_entrypoint_return && function.body.iter().any(contains_return) { - return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); - } - - let has_return = function.body.iter().any(contains_return); - if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if function.body[..function.body.len() - 1].iter().any(contains_return) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if function.body.iter().any(contains_yield) { - return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + for (name, value) in &env { + if name.starts_with("__arg_") { + if let Some(type_name) = types.get(name) { + caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + caller_env.entry(name.clone()).or_insert_with(|| value.clone()); } } - let mut yields: Vec = Vec::new(); - let params = caller_params.clone(); + Ok(yields) + } + + fn compile_inline_callee( + &mut self, + name: &str, + function: &FunctionAst, + callee_index: usize, + call_span: Option, + caller_params: &HashMap, + env: &mut HashMap, + types: &mut HashMap, + ) -> Result, CompilerError> { let call_offset = self.builder.script().len(); self.debug_recorder.record_inline_call_enter(call_span, call_offset, name); @@ -944,57 +1137,35 @@ impl<'a> FunctionBodyCompiler<'a> { // events/variable updates carry the callee frame id and call depth. let frame_id = self.inline_frame_counter; self.inline_frame_counter = self.inline_frame_counter.saturating_add(1); - let mut debug_recorder = FunctionDebugRecorder::inline( - self.debug_recorder.enabled, - self.debug_recorder.function_name.clone(), - self.debug_recorder.call_depth().saturating_add(1), - frame_id, - ); + let mut debug_recorder = self.debug_recorder.new_inline_child(frame_id); // Inline params are not stack-mapped like normal function params; materialize // them as variable updates at the inline entry virtual step. - debug_recorder.record_inline_param_updates(function, &env, call_span, call_offset)?; - let mut callee_compiler = FunctionBodyCompiler { - builder: &mut *self.builder, - options: self.options, - debug_recorder: &mut debug_recorder, - contract_constants: self.contract_constants, - functions: self.functions, - function_order: self.function_order, - function_index: callee_index, - script_size: self.script_size, - inline_frame_counter: self.inline_frame_counter, + debug_recorder.record_inline_param_updates(function, env, call_span, call_offset)?; + + let (yields, next_inline_frame_counter) = { + let mut callee_compiler = FunctionBodyCompiler { + builder: &mut *self.builder, + options: self.options, + debug_recorder: &mut debug_recorder, + contract_fields: self.contract_fields, + contract_field_prefix_len: self.contract_field_prefix_len, + contract_constants: self.contract_constants, + functions: self.functions, + function_order: self.function_order, + function_index: callee_index, + script_size: self.script_size, + inline_frame_counter: self.inline_frame_counter, + }; + let yields = callee_compiler.compile_function_body(function, env, caller_params, types)?; + (yields, callee_compiler.inline_frame_counter) }; - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - if matches!(stmt.kind, StatementKind::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let StatementKind::Return { exprs, .. } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); - } - continue; - } - callee_compiler.compile_statement(stmt, &mut env, ¶ms, &mut types, &mut yields)?; - } - self.inline_frame_counter = callee_compiler.inline_frame_counter; + + self.inline_frame_counter = next_inline_frame_counter; // Remap inline-local sequence numbers and merge events/updates back into // the parent function recorder. self.debug_recorder.merge_inline_events(&debug_recorder); self.debug_recorder.record_inline_call_exit(call_span, self.builder.script().len(), name); - for (name, value) in &env { - if name.starts_with("__arg_") { - if let Some(type_name) = types.get(name) { - caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); - } - caller_env.entry(name.clone()).or_insert_with(|| value.clone()); - } - } - Ok(yields) } @@ -1080,14 +1251,8 @@ impl<'a> FunctionBodyCompiler<'a> { for value in start..end { let index_expr = Expr::Int(value); env.insert(name.clone(), index_expr.clone()); - if let Some(sequence) = self.debug_recorder.record_virtual_step(span, self.builder.script().len()) { - self.debug_recorder.record_variable_updates( - vec![(name.clone(), "int".to_string(), index_expr)], - self.builder.script().len(), - span, - sequence, - ); - } + let bytecode_offset = self.builder.script().len(); + self.debug_recorder.record_virtual_updates(span, bytecode_offset, vec![(name.clone(), "int".to_string(), index_expr)]); self.compile_block(body, env, params, types, yields)?; } @@ -1112,6 +1277,178 @@ impl<'a> FunctionBodyCompiler<'a> { } } +fn compile_read_input_state_statement( + bindings: &[StateBindingAst], + args: &[Expr], + env: &mut HashMap, + types: &mut HashMap, + contract_fields: &[ContractFieldAst], + script_size: Option, +) -> Result<(), CompilerError> { + if args.len() != 1 { + return Err(CompilerError::Unsupported("readInputState(input_idx) expects 1 argument".to_string())); + } + if contract_fields.is_empty() { + return Err(CompilerError::Unsupported("readInputState requires contract fields".to_string())); + } + let script_size_value = + script_size.ok_or_else(|| CompilerError::Unsupported("readInputState requires this.scriptSize".to_string()))?; + + let mut bindings_by_field: HashMap<&str, &StateBindingAst> = HashMap::new(); + for binding in bindings { + if bindings_by_field.insert(binding.field_name.as_str(), binding).is_some() { + return Err(CompilerError::Unsupported(format!("duplicate state field '{}'", binding.field_name))); + } + } + if bindings_by_field.len() != contract_fields.len() { + return Err(CompilerError::Unsupported("readInputState bindings must include all contract fields exactly once".to_string())); + } + + let input_idx = args[0].clone(); + let mut field_chunk_offset = 0usize; + for field in contract_fields { + let binding = bindings_by_field.get(field.name.as_str()).ok_or_else(|| { + CompilerError::Unsupported("readInputState bindings must include all contract fields exactly once".to_string()) + })?; + + if binding.type_name != field.type_name { + return Err(CompilerError::Unsupported(format!("readInputState binding '{}' expects {}", binding.name, field.type_name))); + } + + let binding_expr = read_input_state_binding_expr(&input_idx, field, field_chunk_offset, script_size_value)?; + env.insert(binding.name.clone(), binding_expr); + types.insert(binding.name.clone(), binding.type_name.clone()); + field_chunk_offset += encoded_field_chunk_size(field)?; + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn compile_validate_output_state_statement( + args: &[Expr], + env: &HashMap, + params: &HashMap, + types: &HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + script_size: Option, +) -> Result<(), CompilerError> { + if args.len() != 2 { + return Err(CompilerError::Unsupported("validateOutputState(output_idx, new_state) expects 2 arguments".to_string())); + } + if contract_fields.is_empty() { + return Err(CompilerError::Unsupported("validateOutputState requires contract fields".to_string())); + } + + let output_idx = &args[0]; + let Expr::StateObject(state_entries) = &args[1] else { + return Err(CompilerError::Unsupported("validateOutputState second argument must be an object literal".to_string())); + }; + + let mut provided = HashMap::new(); + for entry in state_entries { + if provided.insert(entry.name.as_str(), &entry.expr).is_some() { + return Err(CompilerError::Unsupported(format!("duplicate state field '{}'", entry.name))); + } + } + if provided.len() != contract_fields.len() { + return Err(CompilerError::Unsupported("new_state must include all contract fields exactly once".to_string())); + } + + let mut stack_depth = 0i64; + for field in contract_fields { + let Some(new_value) = provided.remove(field.name.as_str()) else { + return Err(CompilerError::Unsupported(format!("missing state field '{}'", field.name))); + }; + + if field.type_name == "int" { + compile_expr(new_value, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + builder.add_i64(8)?; + stack_depth += 1; + builder.add_op(OpNum2Bin)?; + stack_depth -= 1; + builder.add_data(&[0x08])?; + stack_depth += 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_data(&[OpBin2Num])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + continue; + } + + let field_size = fixed_field_byte_len(&field.type_name).ok_or_else(|| { + CompilerError::Unsupported(format!("validateOutputState does not support field type {}", field.type_name)) + })?; + + compile_expr(new_value, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + let prefix = data_prefix(field_size); + builder.add_data(&prefix)?; + stack_depth += 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + stack_depth -= 1; + } + + let script_size_value = + script_size.ok_or_else(|| CompilerError::Unsupported("validateOutputState requires this.scriptSize".to_string()))?; + + builder.add_op(OpTxInputIndex)?; + stack_depth += 1; + builder.add_op(OpDup)?; + stack_depth += 1; + builder.add_op(OpTxInputScriptSigLen)?; + builder.add_op(OpDup)?; + stack_depth += 1; + builder.add_i64(script_size_value)?; + stack_depth += 1; + builder.add_op(OpSub)?; + stack_depth -= 1; + builder.add_i64(contract_field_prefix_len as i64)?; + stack_depth += 1; + builder.add_op(OpAdd)?; + stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpTxInputScriptSigSubstr)?; + stack_depth -= 2; + + for _ in 0..contract_fields.len() { + builder.add_op(OpCat)?; + stack_depth -= 1; + } + + builder.add_op(OpBlake2b)?; + builder.add_data(&[0x00, 0x00])?; + stack_depth += 1; + builder.add_data(&[OpBlake2b])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_data(&[0x20])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_data(&[OpEqual])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + + compile_expr(output_idx, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, Some(script_size_value))?; + builder.add_op(OpTxOutputSpk)?; + builder.add_op(OpEqual)?; + builder.add_op(OpVerify)?; + + Ok(()) +} + fn merge_env_after_if( env: &mut HashMap, original_env: &HashMap, @@ -1296,6 +1633,16 @@ fn resolve_expr_internal( start: Box::new(resolve_expr_internal(*start, env, visiting, preserve_inline_args)?), end: Box::new(resolve_expr_internal(*end, env, visiting, preserve_inline_args)?), }), + Expr::StateObject(fields) => { + let mut resolved = Vec::with_capacity(fields.len()); + for field in fields { + resolved.push(crate::ast::StateFieldExpr { + name: field.name, + expr: resolve_expr_internal(field.expr, env, visiting, preserve_inline_args)?, + }); + } + Ok(Expr::StateObject(resolved)) + } Expr::Introspection { kind, index } => { Ok(Expr::Introspection { kind, index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?) }) } @@ -1339,6 +1686,15 @@ fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { then_expr: Box::new(replace_identifier(then_expr, target, replacement)), else_expr: Box::new(replace_identifier(else_expr, target, replacement)), }, + Expr::StateObject(fields) => Expr::StateObject( + fields + .iter() + .map(|field| crate::ast::StateFieldExpr { + name: field.name.clone(), + expr: replace_identifier(&field.expr, target, replacement), + }) + .collect(), + ), Expr::Introspection { kind, index } => { Expr::Introspection { kind: *kind, index: Box::new(replace_identifier(index, target, replacement)) } } @@ -1418,6 +1774,9 @@ fn compile_expr( Ok(()) } Expr::Array(_) => Err(CompilerError::Unsupported("array literals are only supported in LockingBytecodeNullData".to_string())), + Expr::StateObject(_) => { + Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState()".to_string())) + } Expr::Call { name, args } => match name.as_str() { "OpSha256" => compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpSHA256, script_size), "sha256" => { @@ -1667,7 +2026,7 @@ fn compile_expr( *stack_depth += 1; Ok(()) } - "LockingBytecodeP2PK" => { + "LockingBytecodeP2PK" | "ScriptPubKeyP2PK" => { if args.len() != 1 { return Err(CompilerError::Unsupported("LockingBytecodeP2PK expects a single pubkey argument".to_string())); } @@ -1683,7 +2042,7 @@ fn compile_expr( *stack_depth -= 1; Ok(()) } - "LockingBytecodeP2SH" => { + "LockingBytecodeP2SH" | "ScriptPubKeyP2SH" => { if args.len() != 1 { return Err(CompilerError::Unsupported("LockingBytecodeP2SH expects a single bytes32 argument".to_string())); } @@ -1971,7 +2330,12 @@ fn expr_is_bytes_inner( Expr::Slice { .. } => true, Expr::New { name, .. } => matches!( name.as_str(), - "LockingBytecodeNullData" | "LockingBytecodeP2PK" | "LockingBytecodeP2SH" | "LockingBytecodeP2SHFromRedeemScript" + "LockingBytecodeNullData" + | "LockingBytecodeP2PK" + | "ScriptPubKeyP2PK" + | "LockingBytecodeP2SH" + | "ScriptPubKeyP2SH" + | "LockingBytecodeP2SHFromRedeemScript" ), Expr::Call { name, .. } => { matches!( diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index c5db0e51..bb459542 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -27,11 +27,11 @@ pub(super) fn record_synthetic_range( /// Records params, statements, and variable updates for a single function. #[derive(Debug, Default)] pub struct FunctionDebugRecorder { - pub function_name: String, - pub enabled: bool, - pub events: Vec, - pub variable_updates: Vec, - pub param_mappings: Vec, + function_name: String, + enabled: bool, + events: Vec, + variable_updates: Vec, + param_mappings: Vec, next_seq: u32, call_depth: u32, frame_id: u32, @@ -56,6 +56,10 @@ impl FunctionDebugRecorder { self.call_depth } + pub fn new_inline_child(&self, frame_id: u32) -> Self { + Self::inline(self.enabled, self.function_name.clone(), self.call_depth().saturating_add(1), frame_id) + } + fn next_sequence(&mut self) -> u32 { let seq = self.next_seq; self.next_seq = self.next_seq.saturating_add(1); @@ -109,6 +113,29 @@ impl FunctionDebugRecorder { self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) } + pub fn record_statement_updates( + &mut self, + stmt: &Statement, + bytecode_start: usize, + bytecode_end: usize, + variables: Vec<(String, String, Expr)>, + ) { + if let Some(sequence) = self.record_statement(stmt, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + self.record_variable_updates(variables, bytecode_end, stmt.span, sequence); + } + } + + pub fn record_virtual_updates( + &mut self, + span: Option, + bytecode_offset: usize, + variables: Vec<(String, String, Expr)>, + ) { + if let Some(sequence) = self.record_virtual_step(span, bytecode_offset) { + self.record_variable_updates(variables, bytecode_offset, span, sequence); + } + } + pub fn record_inline_param_updates( &mut self, function: &FunctionAst, @@ -116,19 +143,20 @@ impl FunctionDebugRecorder { span: Option, bytecode_offset: usize, ) -> Result<(), CompilerError> { - if let Some(sequence) = self.record_virtual_step(span, bytecode_offset) { - let mut variables = Vec::new(); - for param in &function.params { - self.variable_update( - env, - &mut variables, - ¶m.name, - ¶m.type_name, - env.get(¶m.name).cloned().unwrap_or(Expr::Identifier(param.name.clone())), - )?; - } - self.record_variable_updates(variables, bytecode_offset, span, sequence); + if !self.enabled { + return Ok(()); } + let mut variables = Vec::with_capacity(function.params.len()); + for param in &function.params { + self.variable_update( + env, + &mut variables, + ¶m.name, + ¶m.type_name, + env.get(¶m.name).cloned().unwrap_or(Expr::Identifier(param.name.clone())), + )?; + } + self.record_virtual_updates(span, bytecode_offset, variables); Ok(()) } @@ -141,7 +169,7 @@ impl FunctionDebugRecorder { } pub fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { - if !self.enabled { + if !self.enabled || inline.events.is_empty() { return; } let mut seq_map: HashMap = HashMap::new(); @@ -222,39 +250,19 @@ impl DebugSink { if enabled { Self::On(DebugRecorder::default()) } else { Self::Off } } - pub fn record(&mut self, event: DebugEvent) { - if let Self::On(rec) = self { - rec.record(event); - } - } - - pub fn record_variable_update(&mut self, update: DebugVariableUpdate) { - if let Self::On(rec) = self { - rec.record_variable_update(update); - } - } - - pub fn record_param(&mut self, param: DebugParamMapping) { - if let Self::On(rec) = self { - rec.record_param(param); - } - } - - pub fn record_function(&mut self, function: DebugFunctionRange) { - if let Self::On(rec) = self { - rec.record_function(function); - } - } - - pub fn record_constant(&mut self, constant: DebugConstantMapping) { - if let Self::On(rec) = self { - rec.record_constant(constant); + fn recorder_mut(&mut self) -> Option<&mut DebugRecorder> { + match self { + Self::Off => None, + Self::On(rec) => Some(rec), } } pub fn record_constructor_constants(&mut self, params: &[ParamAst], values: &[Expr]) { + let Some(rec) = self.recorder_mut() else { + return; + }; for (param, value) in params.iter().zip(values.iter()) { - self.record_constant(DebugConstantMapping { + rec.record_constant(DebugConstantMapping { name: param.name.clone(), type_name: param.type_name.clone(), value: value.clone(), @@ -266,26 +274,30 @@ impl DebugSink { if end <= start { return; } - if let Self::On(rec) = self { - let sequence = rec.next_sequence(); - rec.record(DebugEvent { - bytecode_start: start, - bytecode_end: end, - span: None, - kind: DebugEventKind::Synthetic { label: label.to_string() }, - sequence, - call_depth: 0, - frame_id: 0, - }); - } + let Some(rec) = self.recorder_mut() else { + return; + }; + let sequence = rec.next_sequence(); + rec.record(DebugEvent { + bytecode_start: start, + bytecode_end: end, + span: None, + kind: DebugEventKind::Synthetic { label: label.to_string() }, + sequence, + call_depth: 0, + frame_id: 0, + }); } pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionDebugRecorder, offset: usize) { - let seq_base = if let Self::On(rec) = self { rec.reserve_sequence_block(debug.sequence_count()) } else { 0 }; - emit_events_with_offset(&debug.events, offset, seq_base, self); - emit_variable_updates_with_offset(&debug.variable_updates, offset, seq_base, self); - self.record_function(DebugFunctionRange { name: name.to_string(), bytecode_start: offset, bytecode_end: offset + script_len }); - record_param_mappings(&debug.param_mappings, self); + let Some(rec) = self.recorder_mut() else { + return; + }; + let seq_base = rec.reserve_sequence_block(debug.sequence_count()); + emit_events_with_offset(&debug.events, offset, seq_base, rec); + emit_variable_updates_with_offset(&debug.variable_updates, offset, seq_base, rec); + rec.record_function(DebugFunctionRange { name: name.to_string(), bytecode_start: offset, bytecode_end: offset + script_len }); + record_param_mappings(&debug.param_mappings, rec); } pub fn into_debug_info(self, source: String) -> Option { @@ -296,9 +308,9 @@ impl DebugSink { } } -fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, sink: &mut DebugSink) { +fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, recorder: &mut DebugRecorder) { for event in events { - sink.record(DebugEvent { + recorder.record(DebugEvent { bytecode_start: event.bytecode_start + offset, bytecode_end: event.bytecode_end + offset, span: event.span, @@ -310,9 +322,9 @@ fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, } } -fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: usize, seq_base: u32, sink: &mut DebugSink) { +fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: usize, seq_base: u32, recorder: &mut DebugRecorder) { for update in updates { - sink.record_variable_update(DebugVariableUpdate { + recorder.record_variable_update(DebugVariableUpdate { name: update.name.clone(), type_name: update.type_name.clone(), expr: update.expr.clone(), @@ -325,8 +337,8 @@ fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: us } } -fn record_param_mappings(params: &[DebugParamMapping], sink: &mut DebugSink) { +fn record_param_mappings(params: &[DebugParamMapping], recorder: &mut DebugRecorder) { for param in params { - sink.record_param(param.clone()); + recorder.record_param(param.clone()); } } diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index c4fd3adf..03861ef2 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -259,10 +259,9 @@ impl<'a> DebugSession<'a> { return Ok(false); } - if mapping_matches_offset(&target, offset) - && self.engine.is_executing() { - return Ok(true); - } + if mapping_matches_offset(&target, offset) && self.engine.is_executing() { + return Ok(true); + } if self.step_opcode()?.is_none() { return Ok(false); diff --git a/silverscript-lang/src/silverscript.pest b/silverscript-lang/src/silverscript.pest index 3e9de7a1..a3bfb696 100644 --- a/silverscript-lang/src/silverscript.pest +++ b/silverscript-lang/src/silverscript.pest @@ -7,11 +7,12 @@ version_constraint = { version_operator? ~ VersionLiteral } version_operator = { "^" | "~" | ">=" | ">" | "<" | "<=" | "=" } contract_definition = { "contract" ~ Identifier ~ parameter_list ~ "{" ~ contract_item* ~ "}" } -contract_item = { constant_definition | function_definition } +contract_item = { constant_definition | contract_field_definition | function_definition } entrypoint = { "entrypoint" } function_definition = { entrypoint? ~ "function" ~ Identifier ~ parameter_list ~ return_type_list? ~ "{" ~ statement* ~ "}" } constant_definition = { type_name ~ "constant" ~ Identifier ~ "=" ~ expression ~ ";" } +contract_field_definition = { type_name ~ Identifier ~ "=" ~ expression ~ ";" } parameter_list = { "(" ~ (parameter ~ ("," ~ parameter)* ~ ","?)? ~ ")" } parameter = { type_name ~ Identifier } @@ -23,6 +24,7 @@ statement = _{ variable_definition | tuple_assignment | push_statement + | state_function_call_assignment | function_call_assignment | call_statement | return_statement @@ -39,7 +41,9 @@ variable_definition = { type_name ~ modifier* ~ Identifier ~ ("=" ~ expression)? tuple_assignment = { type_name ~ Identifier ~ "," ~ type_name ~ Identifier ~ "=" ~ expression ~ ";" } push_statement = { Identifier ~ ".push" ~ "(" ~ expression ~ ")" ~ ";" } function_call_assignment = { "(" ~ typed_binding ~ ("," ~ typed_binding)* ~ ","? ~ ")" ~ "=" ~ function_call ~ ";" } +state_function_call_assignment = { "{" ~ state_typed_binding ~ ("," ~ state_typed_binding)* ~ ","? ~ "}" ~ "=" ~ function_call ~ ";" } typed_binding = { type_name ~ Identifier } +state_typed_binding = { Identifier ~ ":" ~ type_name ~ Identifier } call_statement = { function_call ~ ";" } assign_statement = { Identifier ~ "=" ~ expression ~ ";" } return_statement = { "return" ~ expression_list ~ ";" } @@ -100,6 +104,7 @@ primary = _{ | cast | function_call | instantiation + | state_object | introspection | array | NullaryOp @@ -107,6 +112,9 @@ primary = _{ | literal } +state_object = { "{" ~ (state_entry ~ ("," ~ state_entry)* ~ ","?)? ~ "}" } +state_entry = { Identifier ~ ":" ~ expression } + parenthesized = { "(" ~ expression ~ ")" } cast = { type_name ~ "(" ~ expression ~ ("," ~ expression)? ~ ","? ~ ")" } instantiation = { "new" ~ Identifier ~ expression_list } @@ -116,8 +124,8 @@ introspection = { | ("tx.inputs" ~ "[" ~ expression ~ "]" ~ input_field) } -output_field = { "." ~ ("value" | "lockingBytecode" | "tokenCategory" | "nftCommitment" | "tokenAmount") } -input_field = { "." ~ ("value" | "lockingBytecode" | "outpointTransactionHash" | "outpointIndex" | "unlockingBytecode" | "sequenceNumber" | "tokenCategory" | "nftCommitment" | "tokenAmount") } +output_field = { "." ~ ("value" | "scriptPubKey") } +input_field = { "." ~ ("value" | "scriptPubKey" | "outpointTransactionHash" | "outpointIndex" | "sigScript") } array = { "[" ~ (expression ~ ("," ~ expression)* ~ ","?)? ~ "]" } @@ -126,9 +134,14 @@ modifier = { "constant" } literal = { BooleanLiteral | HexLiteral | number_literal | StringLiteral | DateLiteral } number_literal = { NumberLiteral ~ NumberUnit? } -type_name = { base_type ~ array_suffix? } -base_type = { "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | Bytes } -array_suffix = { "[]" } +type_name = { (base_type | legacy_bytes_type) ~ array_suffix* } +base_type = @{ + ("int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte") + ~ !(ASCII_ALPHANUMERIC | "_") +} +legacy_bytes_type = @{ "bytes" ~ ASCII_DIGIT* ~ !(ASCII_ALPHANUMERIC | "_") } +array_suffix = { "[" ~ array_size? ~ "]" } +array_size = { Identifier | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) } VersionLiteral = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ } @@ -140,8 +153,7 @@ NumberLiteral = @{ "-"? ~ NumberPart ~ ExponentPart? } NumberPart = { ASCII_DIGIT+ ~ ("_" ~ ASCII_DIGIT+ )* } ExponentPart = { ("e" | "E") ~ NumberPart } -Bytes = { "bytes" ~ Bound? | "byte" } -Bound = { ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* } + StringLiteral = @{ "\"" ~ ("\\\"" | !("\"" | "\r" | "\n") ~ ANY)* ~ "\"" | "'" ~ ("\\'" | !("'" | "\r" | "\n") ~ ANY)* ~ "'" } @@ -153,7 +165,7 @@ TxVar = { "this.age" | "tx.time" } NullaryOp = { "this.activeInputIndex" - | "this.activeBytecode" + | "this.activeScriptPubKey" | "this.scriptSizeDataPrefix" | "this.scriptSize" | "tx.inputs.length" @@ -170,7 +182,7 @@ keyword = { "pragma" | "silverscript" | "contract" | "entrypoint" | "function" | "if" | "else" | "require" | "for" | "yield" | "return" | "console.log" | "new" | "true" | "false" | "constant" | "date" | "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte" | "bytes" - | "this.age" | "tx.time" | "this.activeInputIndex" | "this.activeBytecode" | "this.scriptSizeDataPrefix" | "this.scriptSize" + | "this.age" | "tx.time" | "this.activeInputIndex" | "this.activeScriptPubKey" | "this.scriptSizeDataPrefix" | "this.scriptSize" | "tx.inputs.length" | "tx.outputs.length" | "tx.version" | "tx.locktime" } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index e35f3571..d4dbd987 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -2461,3 +2461,23 @@ fn compiles_sigscript_reused_inputs_and_fails_on_wrong_value() { let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_err()); } + +#[test] +fn compiles_state_transition_helpers() { + let source = r#" + pragma silverscript ^0.1.0; + + contract Counter(int init_amount) { + int amount = init_amount; + + entrypoint function main(int next_amount) { + {amount: int current_amount} = readInputState(this.activeInputIndex); + validateOutputState(0, {amount: next_amount}); + require(current_amount >= next_amount); + } + } + "#; + + let compiled = compile_contract(source, &[Expr::Int(100)], CompileOptions::default()).expect("compile succeeds"); + assert!(!compiled.script.is_empty()); +} diff --git a/silverscript-lang/tests/examples/covenant_id.sil b/silverscript-lang/tests/examples/covenant_id.sil new file mode 100644 index 00000000..0d493bc7 --- /dev/null +++ b/silverscript-lang/tests/examples/covenant_id.sil @@ -0,0 +1,33 @@ +pragma silverscript ^0.1.0; + +contract CovenantId(int max_ins, int max_outs, int init_amount) { + int amount = init_amount; + entrypoint function main(int[] output_amounts) { + require(output_amounts.length <= max_outs); + byte[32] covid = OpInputCovenantId(this.activeInputIndex); + + int in_count = OpCovInputCount(covid); + require(in_count <= max_ins); + + int in_sum = 0; + for(i,0,max_ins){ + if( i < in_count ){ + int in_idx = OpCovInputIdx(covid, i); + {amount: int in_amount} = readInputState(in_idx); + in_sum = in_sum + in_amount; + } + } + + int out_sum = 0; + for(i,0,max_outs){ + if( i < output_amounts.length ){ + int out_idx = OpCovOutputIdx(covid, i); + int out_amount = output_amounts[i]; + out_sum = out_sum + out_amount; + validateOutputState(out_idx, {amount: out_amount}); + } + } + + require(in_sum >= out_sum); + } +} diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index be8c4803..daaf6926 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -2,10 +2,11 @@ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_consensus_core::hashing::sighash::calc_schnorr_signature_hash; use kaspa_consensus_core::hashing::sighash_type::SIG_HASH_ALL; use kaspa_consensus_core::tx::{ - MutableTransaction, PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionOutpoint, - TransactionOutput, UtxoEntry, VerifiableTransaction, + CovenantBinding, MutableTransaction, PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, + TransactionOutpoint, TransactionOutput, UtxoEntry, VerifiableTransaction, }; use kaspa_txscript::caches::Cache; +use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::opcodes::codes::*; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{EngineCtx, EngineFlags, TxScriptEngine, pay_to_script_hash_script}; @@ -147,6 +148,12 @@ fn script_with_return_checks(script: Vec, expected: &[i64]) -> Vec { builder.drain() } +fn sigscript_push_script(script: &[u8]) -> Vec { + let mut builder = ScriptBuilder::new(); + builder.add_data(script).expect("push script"); + builder.drain() +} + #[test] fn compiles_announcement_example_and_verifies() { let source = load_example_source("announcement.sil"); @@ -1260,6 +1267,107 @@ fn compiles_covenant_mecenas_example_and_verifies() { assert!(result.is_ok(), "covenant mecenas reclaim failed: {}", result.unwrap_err()); } +#[test] +fn compiles_covenant_id_example_and_verifies() { + let source = load_example_source("covenant_id.sil"); + + let max_ins = 2i64; + let max_outs = 2i64; + let covenant_id = kaspa_consensus_core::Hash::from_bytes(*b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + let other_covenant_id = kaspa_consensus_core::Hash::from_bytes(*b"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); + + let execute_case = |out0_amount: i64, out1_amount: i64| { + let active_compiled = + compile_contract(&source, &[max_ins.into(), max_outs.into(), 1_000i64.into()], CompileOptions::default()) + .expect("compile succeeds"); + let input1_compiled = compile_contract(&source, &[max_ins.into(), max_outs.into(), 600i64.into()], CompileOptions::default()) + .expect("compile succeeds"); + let output0_compiled = + compile_contract(&source, &[max_ins.into(), max_outs.into(), out0_amount.into()], CompileOptions::default()) + .expect("compile succeeds"); + let output1_compiled = + compile_contract(&source, &[max_ins.into(), max_outs.into(), out1_amount.into()], CompileOptions::default()) + .expect("compile succeeds"); + + let mut active_sigscript = + active_compiled.build_sig_script("main", vec![vec![out0_amount, out1_amount].into()]).expect("sigscript builds"); + active_sigscript.extend_from_slice(&sigscript_push_script(&active_compiled.script)); + + let input0 = TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::from_bytes([24u8; 32]), index: 0 }, + signature_script: active_sigscript, + sequence: 0, + sig_op_count: 0, + }; + let input1 = TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::from_bytes([25u8; 32]), index: 1 }, + signature_script: sigscript_push_script(&input1_compiled.script), + sequence: 0, + sig_op_count: 0, + }; + let input2 = TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::from_bytes([26u8; 32]), index: 2 }, + signature_script: vec![], + sequence: 0, + sig_op_count: 0, + }; + + let output0 = TransactionOutput { + value: 1, + script_public_key: pay_to_script_hash_script(&output0_compiled.script), + covenant: Some(CovenantBinding { authorizing_input: 0, covenant_id }), + }; + + let output1 = TransactionOutput { + value: 100, + script_public_key: ScriptPublicKey::new(0, vec![OpTrue].into()), + covenant: Some(CovenantBinding { authorizing_input: 2, covenant_id: other_covenant_id }), + }; + + let output2 = TransactionOutput { + value: 1, + script_public_key: pay_to_script_hash_script(&output1_compiled.script), + covenant: Some(CovenantBinding { authorizing_input: 0, covenant_id }), + }; + + let tx = Transaction::new( + 1, + vec![input0.clone(), input1, input2], + vec![output0, output1, output2], + 0, + Default::default(), + 0, + vec![], + ); + + let utxo0 = UtxoEntry::new(1_600, pay_to_script_hash_script(&active_compiled.script), 0, tx.is_coinbase(), Some(covenant_id)); + let utxo1 = UtxoEntry::new(700, pay_to_script_hash_script(&input1_compiled.script), 0, tx.is_coinbase(), Some(covenant_id)); + let utxo2 = UtxoEntry::new(300, ScriptPublicKey::new(0, vec![OpTrue].into()), 0, tx.is_coinbase(), Some(other_covenant_id)); + + let reused_values = SigHashReusedValuesUnsync::new(); + let sig_cache = Cache::new(10_000); + let populated_tx = PopulatedTransaction::new(&tx, vec![utxo0, utxo1, utxo2]); + let cov_ctx = CovenantsContext::from_tx(&populated_tx).expect("covenants context builds"); + + let mut vm = TxScriptEngine::from_transaction_input( + &populated_tx, + &input0, + 0, + populated_tx.utxo(0).expect("utxo entry for input 0"), + EngineCtx::new(&sig_cache).with_reused(&reused_values).with_covenants_ctx(&cov_ctx), + EngineFlags { covenants_enabled: true }, + ); + + vm.execute() + }; + + let result = execute_case(800, 700); + assert!(result.is_ok(), "covenant_id example should pass with in_sum >= out_sum: {}", result.unwrap_err()); + + let result = execute_case(1000, 700); + assert!(result.is_err(), "covenant_id example should fail when out_sum exceeds in_sum"); +} + #[test] fn compiles_bar_example_and_verifies() { let source = load_example_source("bar.sil"); From bd9fcb5ac1ec0ef5e504451912531f66846c223b Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:51:40 +0200 Subject: [PATCH 05/41] Fix AST JSON fixtures for current type_name schema --- silverscript-lang/tests/ast_json/require_test.ast.json | 4 ++-- silverscript-lang/tests/ast_json/yield_test.ast.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/silverscript-lang/tests/ast_json/require_test.ast.json b/silverscript-lang/tests/ast_json/require_test.ast.json index fd35aa8b..a6dbfd05 100644 --- a/silverscript-lang/tests/ast_json/require_test.ast.json +++ b/silverscript-lang/tests/ast_json/require_test.ast.json @@ -7,8 +7,8 @@ "name": "main", "entrypoint": true, "params": [ - { "type_ref": { "base": "int" }, "name": "a" }, - { "type_ref": { "base": "int" }, "name": "b" } + { "type_name": "int", "name": "a" }, + { "type_name": "int", "name": "b" } ], "body": [ { diff --git a/silverscript-lang/tests/ast_json/yield_test.ast.json b/silverscript-lang/tests/ast_json/yield_test.ast.json index 156c0cba..f2bffe51 100644 --- a/silverscript-lang/tests/ast_json/yield_test.ast.json +++ b/silverscript-lang/tests/ast_json/yield_test.ast.json @@ -11,7 +11,7 @@ { "kind": "variable_definition", "data": { - "type_ref": { "base": "int" }, + "type_name": "int", "modifiers": [], "name": "x", "expr": { "kind": "int", "data": 5 } From 0d7a3bb55dfd6d21faedf409fb6bfb2bc3fd42de Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:03:32 +0200 Subject: [PATCH 06/41] Fix length stack cleanup and remove unused target deps --- Cargo.lock | 209 +----------------- silverscript-lang/Cargo.toml | 8 - silverscript-lang/src/compiler.rs | 2 + .../tests/cashc_valid_examples_tests.rs | 8 +- silverscript-lang/tests/examples_tests.rs | 11 +- 5 files changed, 8 insertions(+), 230 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e336876e..5a24b5e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,12 +30,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -368,21 +362,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.55" @@ -418,19 +397,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -449,7 +415,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.2", + "unicode-width", "windows-sys 0.59.0", ] @@ -517,31 +483,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" -dependencies = [ - "bitflags", - "crossterm_winapi", - "libc", - "mio", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -753,12 +694,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "futures" version = "0.3.31" @@ -917,29 +852,12 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -1061,15 +979,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1448,15 +1357,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "mac_address" version = "1.1.8" @@ -1515,18 +1415,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mock_instant" version = "0.6.0" @@ -1696,12 +1584,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pest" version = "2.8.5" @@ -1912,26 +1794,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "ratatui" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" -dependencies = [ - "bitflags", - "cassowary", - "compact_str", - "crossterm", - "itertools 0.12.1", - "lru", - "paste", - "stability", - "strum", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.1.14", -] - [[package]] name = "rayon" version = "1.11.0" @@ -2206,17 +2068,6 @@ dependencies = [ "signal-hook-registry", ] -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2233,9 +2084,7 @@ version = "0.1.0" dependencies = [ "blake2b_simd", "chrono", - "crossterm", "hex", - "js-sys", "kaspa-addresses", "kaspa-consensus-core", "kaspa-txscript", @@ -2243,7 +2092,6 @@ dependencies = [ "pest", "pest_derive", "rand 0.8.5", - "ratatui", "secp256k1", "serde", "serde_json", @@ -2271,44 +2119,6 @@ dependencies = [ "serde", ] -[[package]] -name = "stability" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" -dependencies = [ - "quote", - "syn 2.0.114", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.114", -] - [[package]] name = "syn" version = "1.0.109" @@ -2543,23 +2353,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.2" diff --git a/silverscript-lang/Cargo.toml b/silverscript-lang/Cargo.toml index fbf6409d..49100903 100644 --- a/silverscript-lang/Cargo.toml +++ b/silverscript-lang/Cargo.toml @@ -26,13 +26,5 @@ serde = { version = "1.0", features = ["derive"] } hex = "0.4" serde_json = "1.0" -# Native-only dependencies (not compiled for wasm32) -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ratatui = "0.26" -crossterm = "0.27" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = "0.3" - [dev-dependencies] kaspa-addresses.workspace = true diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 288ad208..5a87f067 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1952,6 +1952,8 @@ fn compile_expr( } compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; builder.add_op(OpSize)?; + builder.add_op(OpSwap)?; + builder.add_op(OpDrop)?; Ok(()) } "int" => { diff --git a/silverscript-lang/tests/cashc_valid_examples_tests.rs b/silverscript-lang/tests/cashc_valid_examples_tests.rs index 43c43b2b..a12f2475 100644 --- a/silverscript-lang/tests/cashc_valid_examples_tests.rs +++ b/silverscript-lang/tests/cashc_valid_examples_tests.rs @@ -918,7 +918,7 @@ fn runs_cashc_valid_examples() { assert!(result.is_err(), "{example} should fail"); } "split_size.sil" => { - // Unsatisfiable in this runtime: `b.length / 2` leaves `b` on the stack, causing invalid substring ranges. + // `length()` now drops the source bytes value after OpSize, so split bounds are computed correctly. let constructor_args = vec![b"abcd".to_vec().into()]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); let selector = selector_for_compiled(&compiled, "spend"); @@ -932,7 +932,7 @@ fn runs_cashc_valid_examples() { ); tx.tx.inputs[0].signature_script = sigscript; let result = execute_tx(tx, utxo, reused); - assert!(result.is_err(), "{example} should fail"); + assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); } "split_typed.sil" => { let constructor_args = vec![b"abcde".to_vec().into()]; @@ -951,7 +951,7 @@ fn runs_cashc_valid_examples() { assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); } "string_concatenation.sil" => { - // Unsatisfiable in this runtime: concatenation leaves an extra stack item (CLEANSTACK). + // String concatenation + length now executes cleanly under CLEANSTACK. let constructor_args = vec![]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); let selector = selector_for_compiled(&compiled, "hello"); @@ -965,7 +965,7 @@ fn runs_cashc_valid_examples() { ); tx.tx.inputs[0].signature_script = sigscript; let result = execute_tx(tx, utxo, reused); - assert!(result.is_err(), "{example} should fail"); + assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); } "string_with_escaped_characters.sil" => { // Unsatisfiable in this runtime: escaped string literals hash differently. diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index daaf6926..087baf06 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -457,16 +457,7 @@ fn runs_everything_example_and_verifies() { sequence: 500, sig_op_count: 1, }; - let checked_script = ScriptBuilder::new() - .add_ops(&compiled.script) - .unwrap() - .add_op(OpDrop) - .unwrap() - .add_op(OpDrop) - .unwrap() - .add_op(OpTrue) - .unwrap() - .drain(); + let checked_script = compiled.script.clone(); let output = TransactionOutput { value: 5_000, script_public_key: ScriptPublicKey::new(0, checked_script.clone().into()), covenant: None }; From cac30ad14a264958c2dedc6bed479dd394eb7cbb Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:16:04 +0200 Subject: [PATCH 07/41] Restore typed return TypeRef model --- silverscript-lang/src/ast.rs | 121 +++++++++++++++++++++++++++++- silverscript-lang/src/compiler.rs | 34 ++++++--- 2 files changed, 143 insertions(+), 12 deletions(-) diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index be946b97..eea1e2b3 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -48,7 +48,7 @@ pub struct FunctionAst { #[serde(default)] pub entrypoint: bool, #[serde(default)] - pub return_types: Vec, + pub return_types: Vec, pub body: Vec, } @@ -65,6 +65,61 @@ pub struct StateBindingAst { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TypeRef { + pub base: TypeBase, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub array_dims: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum TypeBase { + Int, + Bool, + String, + Pubkey, + Sig, + Datasig, + Byte, +} + +impl TypeBase { + pub fn as_str(&self) -> &'static str { + match self { + TypeBase::Int => "int", + TypeBase::Bool => "bool", + TypeBase::String => "string", + TypeBase::Pubkey => "pubkey", + TypeBase::Sig => "sig", + TypeBase::Datasig => "datasig", + TypeBase::Byte => "byte", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", content = "value", rename_all = "snake_case")] +pub enum ArrayDim { + Dynamic, + Fixed(usize), + Constant(String), +} + +impl TypeRef { + pub fn type_name(&self) -> String { + let mut out = self.base.as_str().to_string(); + for dim in &self.array_dims { + match dim { + ArrayDim::Dynamic => out.push_str("[]"), + ArrayDim::Fixed(size) => out.push_str(&format!("[{size}]")), + ArrayDim::Constant(name) => out.push_str(&format!("[{name}]")), + } + } + out + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Statement { #[serde(skip)] @@ -245,6 +300,65 @@ fn validate_user_identifier(name: &str) -> Result<(), CompilerError> { Ok(()) } +pub fn parse_type_ref(type_name: &str) -> Result { + let mut pairs = SilverScriptParser::parse(Rule::type_name, type_name)?; + let pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("missing type name".to_string()))?; + parse_type_name_pair(pair) +} + +fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result { + if pair.as_rule() != Rule::type_name { + return Err(CompilerError::Unsupported("expected type name".to_string())); + } + + let mut inner = pair.clone().into_inner(); + let base_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing base type".to_string()))?; + + let (base, mut array_dims) = match base_pair.as_rule() { + Rule::base_type => { + let base = match base_pair.as_str() { + "int" => TypeBase::Int, + "bool" => TypeBase::Bool, + "string" => TypeBase::String, + "pubkey" => TypeBase::Pubkey, + "sig" => TypeBase::Sig, + "datasig" => TypeBase::Datasig, + "byte" => TypeBase::Byte, + other => return Err(CompilerError::Unsupported(format!("unknown base type: {other}"))), + }; + (base, Vec::new()) + } + Rule::legacy_bytes_type => { + let raw = base_pair.as_str(); + if raw == "bytes" { + (TypeBase::Byte, vec![ArrayDim::Dynamic]) + } else if let Some(size) = raw.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { + (TypeBase::Byte, vec![ArrayDim::Fixed(size)]) + } else { + return Err(CompilerError::Unsupported(format!("invalid bytes type: {raw}"))); + } + } + _ => return Err(CompilerError::Unsupported("invalid type root".to_string())), + }; + + for suffix in inner { + if suffix.as_rule() != Rule::array_suffix { + continue; + } + let mut suffix_inner = suffix.into_inner(); + let dim = match suffix_inner.next() { + None => ArrayDim::Dynamic, + Some(size_pair) => { + let raw = size_pair.as_str().trim(); + if let Ok(size) = raw.parse::() { ArrayDim::Fixed(size) } else { ArrayDim::Constant(raw.to_string()) } + } + }; + array_dims.push(dim); + } + + Ok(TypeRef { base, array_dims }) +} + pub fn parse_contract_ast(source: &str) -> Result { let mut pairs = SilverScriptParser::parse(Rule::source_file, source)?; let source_pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("empty source".to_string()))?; @@ -722,11 +836,12 @@ fn parse_typed_parameter_list(pair: Pair<'_, Rule>) -> Result, Com Ok(params) } -fn parse_return_type_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_return_type_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { let mut types = Vec::new(); for item in pair.into_inner() { if item.as_rule() == Rule::type_name { - types.push(normalize_type_name(item.as_str().trim())); + let normalized = normalize_type_name(item.as_str().trim()); + types.push(parse_type_ref(&normalized)?); } } Ok(types) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 5a87f067..4bd80456 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - BinaryOp, ConsoleArg, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, SplitPart, - StateBindingAst, Statement, StatementKind, TimeVar, UnaryOp, parse_contract_ast, + ArrayDim, BinaryOp, ConsoleArg, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, + SplitPart, StateBindingAst, Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, }; use crate::debug::DebugInfo; use crate::debug::labels::synthetic; @@ -365,6 +365,18 @@ fn array_element_size(type_name: &str) -> Option { array_element_type(type_name).and_then(fixed_type_size) } +fn type_name_from_ref(type_ref: &TypeRef) -> String { + if type_ref.base == TypeBase::Byte { + return match type_ref.array_dims.as_slice() { + [] => "byte".to_string(), + [ArrayDim::Dynamic] => "bytes".to_string(), + [ArrayDim::Fixed(size)] => format!("bytes{size}"), + _ => type_ref.type_name(), + }; + } + type_ref.type_name() +} + fn compile_contract_fields( fields: &[ContractFieldAst], base_constants: &HashMap, @@ -516,15 +528,16 @@ fn validate_function_body(function: &FunctionAst, options: CompileOptions) -> Re Ok(has_return) } -fn validate_return_types(exprs: &[Expr], return_types: &[String], types: &HashMap) -> Result<(), CompilerError> { +fn validate_return_types(exprs: &[Expr], return_types: &[TypeRef], types: &HashMap) -> Result<(), CompilerError> { if return_types.is_empty() { return Err(CompilerError::Unsupported("return requires function return types".to_string())); } if return_types.len() != exprs.len() { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } - for (expr, type_name) in exprs.iter().zip(return_types.iter()) { - if !expr_matches_return_type(expr, type_name, types) { + for (expr, type_ref) in exprs.iter().zip(return_types.iter()) { + let type_name = type_name_from_ref(type_ref); + if !expr_matches_return_type(expr, &type_name, types) { return Err(CompilerError::Unsupported(format!("return value expects {type_name}"))); } } @@ -707,8 +720,11 @@ fn compile_function( } for return_type in &function.return_types { - if is_array_type(return_type) && array_element_size(return_type).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {return_type}"))); + let return_type_name = type_name_from_ref(return_type); + if is_array_type(&return_type_name) && array_element_size(&return_type_name).is_none() { + return Err(CompilerError::Unsupported(format!( + "array element type must have known size: {return_type_name}" + ))); } } @@ -989,11 +1005,11 @@ impl<'a> FunctionBodyCompiler<'a> { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } for (binding, return_type) in bindings.iter().zip(function.return_types.iter()) { - if binding.type_name != *return_type { + if binding.type_name != type_name_from_ref(return_type) { return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); } } - function.return_types.clone() + function.return_types.iter().map(type_name_from_ref).collect::>() }; let returns = self.compile_inline_call(name, args, params, types, env, stmt.span)?; if returns.len() != return_types.len() { From 7e87bc441a066d2a001bcf8e8f8de3e80ec9b77b Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:31:25 +0200 Subject: [PATCH 08/41] Minimize debugger branch against upstream master --- silverscript-lang/src/ast.rs | 280 +- silverscript-lang/src/bin/common/mod.rs | 45 +- silverscript-lang/src/bin/sil-debug.rs | 2 +- silverscript-lang/src/compiler.rs | 3249 +++++++++++------ .../src/compiler/debug_recording.rs | 21 +- silverscript-lang/src/debug.rs | 10 +- silverscript-lang/src/debug/session.rs | 6 +- silverscript-lang/src/silverscript.pest | 10 +- .../tests/ast_json/require_test.ast.json | 4 +- .../tests/ast_json/yield_test.ast.json | 2 +- .../tests/cashc_valid_examples_tests.rs | 8 +- silverscript-lang/tests/compiler_tests.rs | 1309 ++++++- silverscript-lang/tests/date_literal_tests.rs | 8 +- .../tests/debug_session_tests.rs | 306 +- silverscript-lang/tests/debugger_cli_tests.rs | 4 +- .../tests/examples/covenant_id.sil | 2 +- silverscript-lang/tests/examples_tests.rs | 15 +- 17 files changed, 3514 insertions(+), 1767 deletions(-) diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index eea1e2b3..05140b93 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -20,27 +20,11 @@ pub struct ContractAst { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractFieldAst { - pub type_name: String, + pub type_ref: TypeRef, pub name: String, pub expr: Expr, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -pub struct SourceSpan { - pub line: u32, - pub col: u32, - pub end_line: u32, - pub end_col: u32, -} - -impl SourceSpan { - pub fn from_span(span: pest::Span<'_>) -> Self { - let (line, col) = span.start_pos().line_col(); - let (end_line, end_col) = span.end_pos().line_col(); - Self { line: line as u32, col: col as u32, end_line: end_line as u32, end_col: end_col as u32 } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FunctionAst { pub name: String, @@ -54,14 +38,14 @@ pub struct FunctionAst { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ParamAst { - pub type_name: String, + pub type_ref: TypeRef, pub name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateBindingAst { pub field_name: String, - pub type_name: String, + pub type_ref: TypeRef, pub name: String, } @@ -118,21 +102,30 @@ impl TypeRef { } out } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Statement { - #[serde(skip)] - pub span: Option, - #[serde(flatten)] - pub kind: StatementKind, + pub fn is_array(&self) -> bool { + !self.array_dims.is_empty() + } + + pub fn element_type(&self) -> Option { + if self.array_dims.is_empty() { + return None; + } + let mut element = self.clone(); + element.array_dims.pop(); + Some(element) + } + + pub fn array_size(&self) -> Option<&ArrayDim> { + self.array_dims.last() + } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum StatementKind { - VariableDefinition { type_name: String, modifiers: Vec, name: String, expr: Option }, - TupleAssignment { left_type: String, left_name: String, right_type: String, right_name: String, expr: Expr }, +pub enum Statement { + VariableDefinition { type_ref: TypeRef, modifiers: Vec, name: String, expr: Option }, + TupleAssignment { left_type_ref: TypeRef, left_name: String, right_type_ref: TypeRef, right_name: String, expr: Expr }, ArrayPush { name: String, expr: Expr }, FunctionCall { name: String, args: Vec }, FunctionCallAssign { bindings: Vec, name: String, args: Vec }, @@ -166,7 +159,7 @@ pub enum TimeVar { pub enum Expr { Int(i64), Bool(bool), - Bytes(Vec), + Byte(u8), String(String), Identifier(String), Array(Vec), @@ -203,7 +196,7 @@ impl From for Expr { impl From> for Expr { fn from(value: Vec) -> Self { - Expr::Bytes(value) + Expr::Array(value.into_iter().map(Expr::Byte).collect()) } } @@ -213,17 +206,6 @@ impl From for Expr { } } -fn normalize_type_name(raw: &str) -> String { - let type_name = raw.trim(); - if type_name == "byte[]" { - return "bytes".to_string(); - } - if let Some(size) = type_name.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok()) { - return format!("bytes{size}"); - } - type_name.to_string() -} - impl From> for Expr { fn from(value: Vec) -> Self { Expr::Array(value.into_iter().map(Expr::Int).collect()) @@ -232,7 +214,7 @@ impl From> for Expr { impl From>> for Expr { fn from(value: Vec>) -> Self { - Expr::Array(value.into_iter().map(Expr::Bytes).collect()) + Expr::Array(value.into_iter().map(|bytes| Expr::Array(bytes.into_iter().map(Expr::Byte).collect())).collect()) } } @@ -275,7 +257,7 @@ pub enum BinaryOp { #[serde(rename_all = "snake_case")] pub enum NullaryOp { ActiveInputIndex, - ActiveBytecode, + ActiveScriptPubKey, ThisScriptSize, ThisScriptSizeDataPrefix, TxInputsLength, @@ -288,9 +270,10 @@ pub enum NullaryOp { #[serde(rename_all = "snake_case")] pub enum IntrospectionKind { InputValue, - InputLockingBytecode, + InputScriptPubKey, + InputSigScript, OutputValue, - OutputLockingBytecode, + OutputScriptPubKey, } fn validate_user_identifier(name: &str) -> Result<(), CompilerError> { @@ -312,35 +295,18 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result } let mut inner = pair.clone().into_inner(); - let base_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing base type".to_string()))?; - - let (base, mut array_dims) = match base_pair.as_rule() { - Rule::base_type => { - let base = match base_pair.as_str() { - "int" => TypeBase::Int, - "bool" => TypeBase::Bool, - "string" => TypeBase::String, - "pubkey" => TypeBase::Pubkey, - "sig" => TypeBase::Sig, - "datasig" => TypeBase::Datasig, - "byte" => TypeBase::Byte, - other => return Err(CompilerError::Unsupported(format!("unknown base type: {other}"))), - }; - (base, Vec::new()) - } - Rule::legacy_bytes_type => { - let raw = base_pair.as_str(); - if raw == "bytes" { - (TypeBase::Byte, vec![ArrayDim::Dynamic]) - } else if let Some(size) = raw.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { - (TypeBase::Byte, vec![ArrayDim::Fixed(size)]) - } else { - return Err(CompilerError::Unsupported(format!("invalid bytes type: {raw}"))); - } - } - _ => return Err(CompilerError::Unsupported("invalid type root".to_string())), + let base = match inner.next().ok_or_else(|| CompilerError::Unsupported("missing base type".to_string()))?.as_str() { + "int" => TypeBase::Int, + "bool" => TypeBase::Bool, + "string" => TypeBase::String, + "pubkey" => TypeBase::Pubkey, + "sig" => TypeBase::Sig, + "datasig" => TypeBase::Datasig, + "byte" => TypeBase::Byte, + other => return Err(CompilerError::Unsupported(format!("unknown base type: {other}"))), }; + let mut array_dims = Vec::new(); for suffix in inner { if suffix.as_rule() != Rule::array_suffix { continue; @@ -348,10 +314,16 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result let mut suffix_inner = suffix.into_inner(); let dim = match suffix_inner.next() { None => ArrayDim::Dynamic, - Some(size_pair) => { - let raw = size_pair.as_str().trim(); - if let Ok(size) = raw.parse::() { ArrayDim::Fixed(size) } else { ArrayDim::Constant(raw.to_string()) } - } + Some(size_pair) => match size_pair.as_rule() { + Rule::array_size => { + let raw = size_pair.as_str().trim(); + if let Ok(size) = raw.parse::() { ArrayDim::Fixed(size) } else { ArrayDim::Constant(raw.to_string()) } + } + Rule::Identifier => ArrayDim::Constant(size_pair.as_str().to_string()), + _ => { + return Err(CompilerError::Unsupported("invalid array dimension".to_string())); + } + }, }; array_dims.push(dim); } @@ -395,19 +367,14 @@ fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result { let mut field_inner = inner_item.into_inner(); - let type_name = normalize_type_name( - field_inner - .next() - .ok_or_else(|| CompilerError::Unsupported("missing field type".to_string()))? - .as_str() - .trim(), - ); + let type_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field type".to_string()))?; + let type_ref = parse_type_name_pair(type_pair)?; let name_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field name".to_string()))?; validate_user_identifier(name_pair.as_str())?; let expr_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field initializer".to_string()))?; let expr = parse_expression(expr_pair)?; - fields.push(ContractFieldAst { type_name, name: name_pair.as_str().to_string(), expr }); + fields.push(ContractFieldAst { type_ref, name: name_pair.as_str().to_string(), expr }); } Rule::constant_definition => { let mut const_inner = inner_item.into_inner(); @@ -459,22 +426,18 @@ fn parse_function_definition(pair: Pair<'_, Rule>) -> Result) -> Result { - if pair.as_rule() == Rule::statement { - return if let Some(inner) = pair.into_inner().next() { - parse_statement(inner) - } else { - Err(CompilerError::Unsupported("empty statement".to_string())) - }; - } - - let span = Some(SourceSpan::from_span(pair.as_span())); - - let kind = match pair.as_rule() { + match pair.as_rule() { + Rule::statement => { + if let Some(inner) = pair.into_inner().next() { + parse_statement(inner) + } else { + Err(CompilerError::Unsupported("empty statement".to_string())) + } + } Rule::variable_definition => { let mut inner = pair.into_inner(); - let type_name = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))?.as_str().trim(), - ); + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))?; + let type_ref = parse_type_name_pair(type_pair)?; let mut modifiers = Vec::new(); while let Some(p) = inner.peek() { @@ -487,44 +450,42 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()))?; validate_user_identifier(ident.as_str())?; let expr = inner.next().map(parse_expression).transpose()?; - StatementKind::VariableDefinition { type_name, modifiers, name: ident.as_str().to_string(), expr } + Ok(Statement::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr }) } Rule::tuple_assignment => { let mut inner = pair.into_inner(); - let left_type = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()))?.as_str(), - ); + let left_type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()))?; + let left_type_ref = parse_type_name_pair(left_type_pair)?; let left_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple name".to_string()))?; - let right_type = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()))?.as_str(), - ); + let right_type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()))?; + let right_type_ref = parse_type_name_pair(right_type_pair)?; let right_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple name".to_string()))?; validate_user_identifier(left_ident.as_str())?; validate_user_identifier(right_ident.as_str())?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::TupleAssignment { - left_type, + Ok(Statement::TupleAssignment { + left_type_ref, left_name: left_ident.as_str().to_string(), - right_type, + right_type_ref, right_name: right_ident.as_str().to_string(), expr, - } + }) } Rule::push_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::ArrayPush { name: ident.as_str().to_string(), expr } + Ok(Statement::ArrayPush { name: ident.as_str().to_string(), expr }) } Rule::assign_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::Assign { name: ident.as_str().to_string(), expr } + Ok(Statement::Assign { name: ident.as_str().to_string(), expr }) } Rule::time_op_statement => { let mut inner = pair.into_inner(); @@ -538,14 +499,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { "tx.time" => TimeVar::TxTime, other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), }; - StatementKind::TimeOp { tx_var, expr, message } + Ok(Statement::TimeOp { tx_var, expr, message }) } Rule::require_statement => { let mut inner = pair.into_inner(); let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; let message = inner.next().map(parse_require_message).transpose()?; let expr = parse_expression(expr_pair)?; - StatementKind::Require { expr, message } + Ok(Statement::Require { expr, message }) } Rule::if_statement => { let mut inner = pair.into_inner(); @@ -554,14 +515,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let then_block = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()))?; let then_branch = parse_block(then_block)?; let else_branch = inner.next().map(parse_block).transpose()?; - StatementKind::If { condition: cond_expr, then_branch, else_branch } + Ok(Statement::If { condition: cond_expr, then_branch, else_branch }) } Rule::call_statement => { let mut inner = pair.into_inner(); let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => Ok(Statement::FunctionCall { name, args }), + _ => Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::function_call_assignment => { @@ -570,24 +531,23 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { for item in pair.into_inner() { if item.as_rule() == Rule::typed_binding { let mut inner = item.into_inner(); - let type_name = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?.as_str().trim(), - ); + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; + let type_ref = parse_type_name_pair(type_pair)?; let name = inner .next() .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? .as_str() .to_string(); validate_user_identifier(&name)?; - bindings.push(ParamAst { type_name, name }); + bindings.push(ParamAst { type_ref, name }); } else if item.as_rule() == Rule::function_call { call_pair = Some(item); } } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => Ok(Statement::FunctionCallAssign { bindings, name, args }), + _ => Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::state_function_call_assignment => { @@ -601,25 +561,24 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { .ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))? .as_str() .to_string(); - validate_user_identifier(&field_name)?; - let type_name = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?.as_str().trim(), - ); + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; + let type_ref = parse_type_name_pair(type_pair)?; let name = inner .next() .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? .as_str() .to_string(); + validate_user_identifier(&field_name)?; validate_user_identifier(&name)?; - bindings.push(StateBindingAst { field_name, type_name, name }); + bindings.push(StateBindingAst { field_name, type_ref, name }); } else if item.as_rule() == Rule::function_call { call_pair = Some(item); } } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => Ok(Statement::StateFunctionCallAssign { bindings, name, args }), + _ => Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::for_statement => { @@ -634,7 +593,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let end_expr = parse_expression(end_pair)?; let body = parse_block(block_pair)?; - StatementKind::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } + Ok(Statement::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body }) } Rule::yield_statement => { let mut inner = pair.into_inner(); @@ -643,7 +602,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.len() != 1 { return Err(CompilerError::Unsupported("yield() expects a single argument".to_string())); } - StatementKind::Yield { expr: args[0].clone() } + Ok(Statement::Yield { expr: args[0].clone() }) } Rule::return_statement => { let mut inner = pair.into_inner(); @@ -652,18 +611,16 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.is_empty() { return Err(CompilerError::Unsupported("return() expects at least one argument".to_string())); } - StatementKind::Return { exprs: args } + Ok(Statement::Return { exprs: args }) } Rule::console_statement => { let mut inner = pair.into_inner(); let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; let args = parse_console_parameter_list(list_pair)?; - StatementKind::Console { args } + Ok(Statement::Console { args }) } - _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), - }; - - Ok(Statement { span, kind }) + _ => Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), + } } fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { @@ -725,10 +682,10 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result { Rule::NullaryOp => parse_nullary(pair.as_str()), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), - Rule::state_object => parse_state_object(pair), Rule::function_call => parse_function_call(pair), Rule::instantiation => parse_instantiation(pair), Rule::cast => parse_cast(pair), + Rule::state_object => parse_state_object(pair), Rule::split_call | Rule::slice_call | Rule::tuple_index @@ -826,12 +783,11 @@ fn parse_typed_parameter_list(pair: Pair<'_, Rule>) -> Result, Com continue; } let mut inner = param.into_inner(); - let type_name = normalize_type_name( - inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter type".to_string()))?.as_str().trim(), - ); + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter type".to_string()))?; + let type_ref = parse_type_name_pair(type_pair)?; let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter name".to_string()))?.as_str().to_string(); validate_user_identifier(&ident)?; - params.push(ParamAst { type_name, name: ident }); + params.push(ParamAst { type_ref, name: ident }); } Ok(params) } @@ -840,8 +796,8 @@ fn parse_return_type_list(pair: Pair<'_, Rule>) -> Result, Compiler let mut types = Vec::new(); for item in pair.into_inner() { if item.as_rule() == Rule::type_name { - let normalized = normalize_type_name(item.as_str().trim()); - types.push(parse_type_ref(&normalized)?); + let type_ref = parse_type_name_pair(item)?; + types.push(type_ref); } } Ok(types) @@ -855,10 +811,10 @@ fn parse_primary(pair: Pair<'_, Rule>) -> Result { Rule::NullaryOp => parse_nullary(pair.as_str()), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), - Rule::state_object => parse_state_object(pair), Rule::function_call => parse_function_call(pair), Rule::instantiation => parse_instantiation(pair), Rule::cast => parse_cast(pair), + Rule::state_object => parse_state_object(pair), Rule::expression => parse_expression(pair), _ => Err(CompilerError::Unsupported(format!("primary not supported: {:?}", pair.as_rule()))), } @@ -954,8 +910,7 @@ fn parse_expression_list(pair: Pair<'_, Rule>) -> Result, CompilerErro fn parse_cast(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); - let type_name = - normalize_type_name(inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?.as_str()); + let type_name = inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?.as_str().to_string(); let args = match inner.next() { Some(list) => parse_expression_list(list)?, None => Vec::new(), @@ -964,7 +919,7 @@ fn parse_cast(pair: Pair<'_, Rule>) -> Result { return Ok(Expr::Call { name: "bytes".to_string(), args }); } if type_name == "byte" { - return Ok(Expr::Call { name: "bytes1".to_string(), args }); + return Ok(Expr::Call { name: "byte[1]".to_string(), args }); } if type_name == "int" { return Ok(Expr::Call { name: "int".to_string(), args }); @@ -972,8 +927,21 @@ fn parse_cast(pair: Pair<'_, Rule>) -> Result { if matches!(type_name.as_str(), "sig" | "pubkey" | "datasig") { return Ok(Expr::Call { name: type_name, args }); } - if let Some(size) = type_name.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { - return Ok(Expr::Call { name: format!("bytes{size}"), args }); + // Handle single byte cast (duplicate check removed above) + // Support type[N] syntax + if let Some(bracket_pos) = type_name.find('[') { + if type_name.ends_with(']') { + let _base_type = &type_name[..bracket_pos]; + let size_str = &type_name[bracket_pos + 1..type_name.len() - 1]; + // Support both type[N] and type[] (dynamic array) + if size_str.is_empty() { + // Dynamic array cast like byte[] + return Ok(Expr::Call { name: type_name.to_string(), args }); + } else if let Ok(_size) = size_str.parse::() { + // Fixed-size array cast like byte[32] + return Ok(Expr::Call { name: type_name.to_string(), args }); + } + } } Err(CompilerError::Unsupported(format!("cast type not supported: {type_name}"))) } @@ -997,7 +965,8 @@ fn parse_hex_literal(raw: &str) -> Result { .map(|i| u8::from_str_radix(&normalized[i..i + 2], 16)) .collect::, _>>() .map_err(|_| CompilerError::InvalidLiteral(format!("invalid hex literal '{raw}'")))?; - Ok(Expr::Bytes(bytes)) + // Convert Vec to Expr::Array of Expr::Byte + Ok(Expr::Array(bytes.into_iter().map(Expr::Byte).collect())) } fn apply_number_unit(expr: Expr, unit: &str) -> Result { @@ -1053,7 +1022,7 @@ fn parse_string_literal(pair: Pair<'_, Rule>) -> Result { fn parse_nullary(raw: &str) -> Result { let op = match raw { "this.activeInputIndex" => NullaryOp::ActiveInputIndex, - "this.activeBytecode" | "this.activeScriptPubKey" => NullaryOp::ActiveBytecode, + "this.activeScriptPubKey" => NullaryOp::ActiveScriptPubKey, "this.scriptSize" => NullaryOp::ThisScriptSize, "this.scriptSizeDataPrefix" => NullaryOp::ThisScriptSizeDataPrefix, "tx.inputs.length" => NullaryOp::TxInputsLength, @@ -1077,13 +1046,14 @@ fn parse_introspection(pair: Pair<'_, Rule>) -> Result { let kind = if text.starts_with("tx.inputs") { match field { ".value" => IntrospectionKind::InputValue, - ".lockingBytecode" | ".scriptPubKey" => IntrospectionKind::InputLockingBytecode, + ".scriptPubKey" => IntrospectionKind::InputScriptPubKey, + ".sigScript" => IntrospectionKind::InputSigScript, _ => return Err(CompilerError::Unsupported(format!("input field '{field}' not supported"))), } } else if text.starts_with("tx.outputs") { match field { ".value" => IntrospectionKind::OutputValue, - ".lockingBytecode" | ".scriptPubKey" => IntrospectionKind::OutputLockingBytecode, + ".scriptPubKey" => IntrospectionKind::OutputScriptPubKey, _ => return Err(CompilerError::Unsupported(format!("output field '{field}' not supported"))), } } else { diff --git a/silverscript-lang/src/bin/common/mod.rs b/silverscript-lang/src/bin/common/mod.rs index 45447595..915a2469 100644 --- a/silverscript-lang/src/bin/common/mod.rs +++ b/silverscript-lang/src/bin/common/mod.rs @@ -103,6 +103,10 @@ fn parse_hex_bytes(raw: &str) -> Result, Box> { Ok(hex::decode(normalized)?) } +fn bytes_expr(bytes: Vec) -> Expr { + Expr::Array(bytes.into_iter().map(Expr::Byte).collect()) +} + pub fn parse_typed_arg(type_name: &str, raw: &str) -> Result> { // Support array inputs until the LSP exists by allowing: // - JSON arrays: [1,2,3] or ["0x01","0x02"] @@ -123,8 +127,10 @@ pub fn parse_typed_arg(type_name: &str, raw: &str) -> Result Result Err(format!("invalid bool '{raw}' (expected true/false)").into()), }, "string" => Ok(Expr::String(raw.to_string())), - "bytes" | "byte" | "pubkey" | "sig" | "datasig" => Ok(Expr::Bytes(parse_hex_bytes(raw)?)), + "byte" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() == 1 { Ok(Expr::Byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } + } + "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), + "pubkey" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 32 { + return Err(format!("pubkey expects 32 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } + "sig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 65 { + return Err(format!("sig expects 65 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } + "datasig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 64 { + return Err(format!("datasig expects 64 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } other => { - if let Some(size) = other.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { + let size = other + .strip_prefix("bytes") + .and_then(|v| v.parse::().ok()) + .or_else(|| other.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok())); + if let Some(size) = size { let bytes = parse_hex_bytes(raw)?; if bytes.len() != size { return Err(format!("{other} expects {size} bytes, got {}", bytes.len()).into()); } - Ok(Expr::Bytes(bytes)) + Ok(bytes_expr(bytes)) } else { Err(format!("unsupported arg type '{other}'").into()) } diff --git a/silverscript-lang/src/bin/sil-debug.rs b/silverscript-lang/src/bin/sil-debug.rs index b26b487d..cefc655b 100644 --- a/silverscript-lang/src/bin/sil-debug.rs +++ b/silverscript-lang/src/bin/sil-debug.rs @@ -195,7 +195,7 @@ fn main() -> Result<(), Box> { let mut ctor_args = Vec::with_capacity(raw_ctor_args.len()); for (param, raw) in parsed_contract.params.iter().zip(raw_ctor_args.iter()) { - ctor_args.push(common::parse_typed_arg(¶m.type_name, raw)?); + ctor_args.push(common::parse_typed_arg(¶m.type_ref.type_name(), raw)?); } let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 4bd80456..da64adab 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - ArrayDim, BinaryOp, ConsoleArg, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SourceSpan, - SplitPart, StateBindingAst, Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, + ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, + Statement, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; use crate::debug::DebugInfo; use crate::debug::labels::synthetic; @@ -98,8 +98,9 @@ fn compile_contract_impl( } for (param, value) in contract.params.iter().zip(constructor_args.iter()) { - if !expr_matches_type(value, ¶m.type_name) { - return Err(CompilerError::Unsupported(format!("constructor argument '{}' expects {}", param.name, param.type_name))); + let param_type_name = type_name_from_ref(¶m.type_ref); + if !expr_matches_type(value, ¶m_type_name) { + return Err(CompilerError::Unsupported(format!("constructor argument '{}' expects {}", param.name, param_type_name))); } } @@ -115,18 +116,14 @@ fn compile_contract_impl( contract.functions.iter().enumerate().map(|(index, func)| (func.name.clone(), index)).collect::>(); let abi = build_function_abi(contract); let uses_script_size = contract_uses_script_size(contract); - let mut script_size = if uses_script_size { Some(100i64) } else { None }; + let mut script_size = if uses_script_size { Some(100i64) } else { None }; for _ in 0..32 { - let (contract_fields, field_prolog_script) = compile_contract_fields(&contract.fields, &constants, options, script_size)?; - let mut scoped_constants = constants.clone(); - scoped_constants.extend(contract_fields); + let (_contract_fields, field_prolog_script) = compile_contract_fields(&contract.fields, &constants, options, script_size)?; let mut compiled_entrypoints = Vec::new(); - // Create a recorder (active/non-active based on compilation options) to collect debug info let mut recorder = DebugSink::new(options.record_debug_infos); recorder.record_constructor_constants(&contract.params, constructor_args); - for (index, func) in contract.functions.iter().enumerate() { if func.entrypoint { compiled_entrypoints.push(compile_function( @@ -134,7 +131,7 @@ fn compile_contract_impl( index, &contract.fields, field_prolog_script.len(), - &scoped_constants, + &constants, options, &functions_map, &function_order, @@ -149,15 +146,14 @@ fn compile_contract_impl( let compiled = compiled_entrypoints .first() .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; - let func_start = builder.script().len(); + let start = builder.script().len(); builder.add_ops(&compiled.script)?; - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, func_start); + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); builder.drain() } else { let mut builder = ScriptBuilder::new(); builder.add_ops(&field_prolog_script)?; let total = compiled_entrypoints.len(); - for (index, compiled) in compiled_entrypoints.iter().enumerate() { record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_GUARD, |builder| { builder.add_op(OpDup)?; @@ -167,11 +163,9 @@ fn compile_contract_impl( builder.add_op(OpDrop)?; Ok(()) })?; - - let func_start = builder.script().len(); + let start = builder.script().len(); builder.add_ops(&compiled.script)?; - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, func_start); - + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ELSE, |builder| { builder.add_op(OpElse)?; if index == total - 1 { @@ -223,13 +217,6 @@ fn compile_contract_impl( Err(CompilerError::Unsupported("script size did not stabilize".to_string())) } -#[derive(Debug)] -struct CompiledFunction { - name: String, - script: Vec, - debug: FunctionDebugRecorder, -} - fn contract_uses_script_size(contract: &ContractAst) -> bool { if contract.constants.values().any(expr_uses_script_size) { return true; @@ -240,93 +227,199 @@ fn contract_uses_script_size(contract: &ContractAst) -> bool { contract.functions.iter().any(|func| func.body.iter().any(statement_uses_script_size)) } +fn compile_contract_fields( + fields: &[ContractFieldAst], + base_constants: &HashMap, + options: CompileOptions, + script_size: Option, +) -> Result<(HashMap, Vec), CompilerError> { + let mut env = base_constants.clone(); + let mut field_values = HashMap::new(); + let mut field_types = HashMap::new(); + let mut builder = ScriptBuilder::new(); + let params = HashMap::new(); + + for field in fields { + if env.contains_key(&field.name) { + return Err(CompilerError::Unsupported(format!("duplicate contract field name: {}", field.name))); + } + + let type_name = type_name_from_ref(&field.type_ref); + if is_array_type(&type_name) && array_element_size(&type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {type_name}"))); + } + + let mut resolve_visiting = HashSet::new(); + let resolved = resolve_expr(field.expr.clone(), &env, &mut resolve_visiting)?; + if !expr_matches_type_ref(&resolved, &field.type_ref) { + return Err(CompilerError::Unsupported(format!("contract field '{}' expects {}", field.name, type_name))); + } + + let mut compile_visiting = HashSet::new(); + let mut stack_depth = 0i64; + if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { + let Expr::Int(value) = resolved else { + return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); + }; + builder.add_data(&value.to_le_bytes())?; + builder.add_op(OpBin2Num)?; + } else { + compile_expr( + &resolved, + &env, + ¶ms, + &field_types, + &mut builder, + options, + &mut compile_visiting, + &mut stack_depth, + script_size, + &env, + )?; + } + + env.insert(field.name.clone(), resolved.clone()); + field_values.insert(field.name.clone(), resolved); + field_types.insert(field.name.clone(), type_name); + } + + Ok((field_values, builder.drain())) +} + fn statement_uses_script_size(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), - StatementKind::FunctionCall { name, args, .. } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), - StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - StatementKind::StateFunctionCallAssign { name, args, .. } => { - name == "readInputState" || args.iter().any(expr_uses_script_size) - } - StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), - StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), - StatementKind::Require { expr, .. } => expr_uses_script_size(expr), - StatementKind::If { condition, then_branch, else_branch, .. } => { + match stmt { + Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), + Statement::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), + Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + Statement::StateFunctionCallAssign { name, args, .. } => name == "readInputState" || args.iter().any(expr_uses_script_size), + Statement::Assign { expr, .. } => expr_uses_script_size(expr), + Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), + Statement::Require { expr, .. } => expr_uses_script_size(expr), + Statement::If { condition, then_branch, else_branch } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - StatementKind::For { start, end, body, .. } => { + Statement::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - StatementKind::Yield { expr, .. } => expr_uses_script_size(expr), - StatementKind::Return { exprs, .. } => exprs.iter().any(expr_uses_script_size), - StatementKind::Console { args, .. } => { - args.iter().any(|arg| matches!(arg, ConsoleArg::Literal(e) if expr_uses_script_size(e))) - } + Statement::Yield { expr } => expr_uses_script_size(expr), + Statement::Return { exprs } => exprs.iter().any(expr_uses_script_size), + Statement::Console { args } => args.iter().any(|arg| match arg { + crate::ast::ConsoleArg::Identifier(_) => false, + crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), + }), } } fn expr_uses_script_size(expr: &Expr) -> bool { match expr { - Expr::Int(_) | Expr::Bool(_) | Expr::Bytes(_) | Expr::String(_) | Expr::Identifier(_) => false, - Expr::Array(items) => items.iter().any(expr_uses_script_size), - Expr::Call { args, .. } | Expr::New { args, .. } => args.iter().any(expr_uses_script_size), - Expr::Split { source, index, .. } => expr_uses_script_size(source) || expr_uses_script_size(index), - Expr::Slice { source, start, end } => { - expr_uses_script_size(source) || expr_uses_script_size(start) || expr_uses_script_size(end) - } - Expr::ArrayIndex { source, index } => expr_uses_script_size(source) || expr_uses_script_size(index), + Expr::Nullary(NullaryOp::ThisScriptSize) => true, + Expr::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, Expr::Unary { expr, .. } => expr_uses_script_size(expr), Expr::Binary { left, right, .. } => expr_uses_script_size(left) || expr_uses_script_size(right), Expr::IfElse { condition, then_expr, else_expr } => { expr_uses_script_size(condition) || expr_uses_script_size(then_expr) || expr_uses_script_size(else_expr) } - Expr::Nullary(op) => matches!(op, NullaryOp::ThisScriptSize | NullaryOp::ThisScriptSizeDataPrefix), - Expr::Introspection { index, .. } => expr_uses_script_size(index), + Expr::Array(values) => values.iter().any(expr_uses_script_size), Expr::StateObject(fields) => fields.iter().any(|field| expr_uses_script_size(&field.expr)), + Expr::Call { args, .. } => args.iter().any(expr_uses_script_size), + Expr::New { args, .. } => args.iter().any(expr_uses_script_size), + Expr::Split { source, index, .. } => expr_uses_script_size(source) || expr_uses_script_size(index), + Expr::Slice { source, start, end } => { + expr_uses_script_size(source) || expr_uses_script_size(start) || expr_uses_script_size(end) + } + Expr::ArrayIndex { source, index } => expr_uses_script_size(source) || expr_uses_script_size(index), + Expr::Introspection { index, .. } => expr_uses_script_size(index), + Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) | Expr::Identifier(_) => false, + Expr::Nullary(_) => false, } } -fn expr_matches_type(expr: &Expr, type_name: &str) -> bool { - if is_array_type(type_name) { - return matches!(expr, Expr::Bytes(_)) || matches!(expr, Expr::Array(values) if array_literal_matches_type(values, type_name)); +// Helper to check if an expression is an array of bytes +fn is_byte_array(expr: &Expr) -> bool { + match expr { + Expr::Array(values) => values.iter().all(|v| matches!(v, Expr::Byte(_))), + _ => false, } - match type_name { - "int" => matches!(expr, Expr::Int(_)), - "bool" => matches!(expr, Expr::Bool(_)), - "string" => matches!(expr, Expr::String(_)), - "bytes" => matches!(expr, Expr::Bytes(_)), - "byte" => matches!(expr, Expr::Bytes(bytes) if bytes.len() == 1), - "pubkey" => matches!(expr, Expr::Bytes(bytes) if bytes.len() == 32), - "sig" | "datasig" => matches!(expr, Expr::Bytes(bytes) if bytes.len() == 64 || bytes.len() == 65), - _ => { - if let Some(size) = type_name.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { - matches!(expr, Expr::Bytes(bytes) if bytes.len() == size) - } else { - false +} + +// Helper to get the length of a byte array +fn byte_array_len(expr: &Expr) -> Option { + match expr { + Expr::Array(values) if values.iter().all(|v| matches!(v, Expr::Byte(_))) => Some(values.len()), + _ => None, + } +} + +fn expr_matches_type_ref(expr: &Expr, type_ref: &TypeRef) -> bool { + if is_array_type_ref(type_ref) { + // Check for fixed-size array type[N] + if let Some(size) = array_size_ref(type_ref) { + // For fixed-size arrays like byte[4], int[3] + if let Some(element_type) = array_element_type_ref(type_ref) { + if element_type.base == TypeBase::Byte { + // byte[N] should match Expr::Array of Expr::Byte with exact length N + return byte_array_len(expr) == Some(size); + } + // For other fixed-size arrays, match array literal + return matches!(expr, Expr::Array(values) if values.len() == size && array_literal_matches_type_ref(values, type_ref)); } } + // Dynamic arrays type[] + return is_byte_array(expr) || matches!(expr, Expr::Array(values) if array_literal_matches_type_ref(values, type_ref)); + } + match type_ref.base { + TypeBase::Int => matches!(expr, Expr::Int(_)), + TypeBase::Bool => matches!(expr, Expr::Bool(_)), + TypeBase::String => matches!(expr, Expr::String(_)), + TypeBase::Byte => matches!(expr, Expr::Byte(_)), + TypeBase::Pubkey => byte_array_len(expr) == Some(32), + TypeBase::Sig => byte_array_len(expr) == Some(65), + TypeBase::Datasig => byte_array_len(expr) == Some(64), } } -fn array_literal_matches_type(values: &[Expr], type_name: &str) -> bool { - let Some(element_type) = array_element_type(type_name) else { +fn array_literal_matches_type_ref(values: &[Expr], type_ref: &TypeRef) -> bool { + let Some(element_type) = array_element_type_ref(type_ref) else { return false; }; - match element_type { - "int" => values.iter().all(|value| matches!(value, Expr::Int(_))), - "byte" => values.iter().all(|value| matches!(value, Expr::Bytes(bytes) if bytes.len() == 1)), - _ => { - if let Some(size) = element_type.strip_prefix("bytes").and_then(|v| v.parse::().ok()) { - values.iter().all(|value| matches!(value, Expr::Bytes(bytes) if bytes.len() == size)) - } else { - false - } + + // Check if this is a fixed-size array + if let Some(expected_size) = array_size_ref(type_ref) { + if values.len() != expected_size { + return false; + } + } + + values.iter().all(|value| expr_matches_type_ref(value, &element_type)) +} + +fn array_literal_matches_type_with_env_ref( + values: &[Expr], + type_ref: &TypeRef, + types: &HashMap, + constants: &HashMap, +) -> bool { + let Some(element_type) = array_element_type_ref(type_ref) else { + return false; + }; + + if let Some(expected_size) = array_size_with_constants_ref(type_ref, constants) { + if values.len() != expected_size { + return false; } } + + values.iter().all(|value| match value { + Expr::Identifier(name) => types + .get(name) + .and_then(|value_type| parse_type_ref(value_type).ok()) + .is_some_and(|value_type| is_type_assignable_ref(&value_type, &element_type, constants)), + _ => expr_matches_type_ref(value, &element_type), + }) } fn build_function_abi(contract: &ContractAst) -> FunctionAbi { @@ -339,241 +432,297 @@ fn build_function_abi(contract: &ContractAst) -> FunctionAbi { inputs: func .params .iter() - .map(|param| FunctionInputAbi { name: param.name.clone(), type_name: param.type_name.clone() }) + .map(|param| FunctionInputAbi { name: param.name.clone(), type_name: type_name_from_ref(¶m.type_ref) }) .collect(), }) .collect() } -fn is_array_type(type_name: &str) -> bool { - type_name.ends_with("[]") -} - -fn array_element_type(type_name: &str) -> Option<&str> { - type_name.strip_suffix("[]") +fn type_name_from_ref(type_ref: &TypeRef) -> String { + type_ref.type_name() } -fn fixed_type_size(type_name: &str) -> Option { - match type_name { - "int" => Some(8), - "byte" => Some(1), - _ => type_name.strip_prefix("bytes").and_then(|v| v.parse::().ok()), - } +fn is_array_type_ref(type_ref: &TypeRef) -> bool { + type_ref.is_array() } -fn array_element_size(type_name: &str) -> Option { - array_element_type(type_name).and_then(fixed_type_size) +fn array_element_type_ref(type_ref: &TypeRef) -> Option { + type_ref.element_type() } -fn type_name_from_ref(type_ref: &TypeRef) -> String { - if type_ref.base == TypeBase::Byte { - return match type_ref.array_dims.as_slice() { - [] => "byte".to_string(), - [ArrayDim::Dynamic] => "bytes".to_string(), - [ArrayDim::Fixed(size)] => format!("bytes{size}"), - _ => type_ref.type_name(), - }; +fn array_size_ref(type_ref: &TypeRef) -> Option { + match type_ref.array_size()? { + ArrayDim::Fixed(size) => Some(*size), + _ => None, } - type_ref.type_name() } -fn compile_contract_fields( - fields: &[ContractFieldAst], - base_constants: &HashMap, - options: CompileOptions, - script_size: Option, -) -> Result<(HashMap, Vec), CompilerError> { - let mut env = base_constants.clone(); - let mut field_values = HashMap::new(); - let mut field_types = HashMap::new(); - let mut builder = ScriptBuilder::new(); - let params = HashMap::new(); - - for field in fields { - if env.contains_key(&field.name) { - return Err(CompilerError::Unsupported(format!("duplicate contract field name: {}", field.name))); - } - - if is_array_type(&field.type_name) && array_element_size(&field.type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", field.type_name))); - } - - let mut resolve_visiting = HashSet::new(); - let resolved = resolve_expr(field.expr.clone(), &env, &mut resolve_visiting)?; - if !expr_matches_type(&resolved, &field.type_name) { - return Err(CompilerError::Unsupported(format!("contract field '{}' expects {}", field.name, field.type_name))); - } - - if field.type_name == "int" { - let Expr::Int(value) = resolved else { - return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); - }; - builder.add_data(&value.to_le_bytes())?; - builder.add_op(OpBin2Num)?; - env.insert(field.name.clone(), Expr::Int(value)); - field_values.insert(field.name.clone(), Expr::Int(value)); - field_types.insert(field.name.clone(), field.type_name.clone()); - continue; +fn array_size_with_constants_ref(type_ref: &TypeRef, constants: &HashMap) -> Option { + match type_ref.array_size()? { + ArrayDim::Fixed(size) => Some(*size), + ArrayDim::Constant(name) => { + if let Some(Expr::Int(value)) = constants.get(name) { + if *value >= 0 { + return Some(*value as usize); + } + } + None } - - let mut compile_visiting = HashSet::new(); - let mut stack_depth = 0i64; - compile_expr( - &resolved, - &env, - ¶ms, - &field_types, - &mut builder, - options, - &mut compile_visiting, - &mut stack_depth, - script_size, - )?; - - env.insert(field.name.clone(), resolved.clone()); - field_values.insert(field.name.clone(), resolved); - field_types.insert(field.name.clone(), field.type_name.clone()); + ArrayDim::Dynamic => None, } - - Ok((field_values, builder.drain())) } -fn fixed_field_byte_len(type_name: &str) -> Option { - match type_name { - "byte" => Some(1), - _ => type_name.strip_prefix("bytes").and_then(|v| v.parse::().ok()), +fn fixed_type_size_ref(type_ref: &TypeRef) -> Option { + if !type_ref.array_dims.is_empty() { + if let (Some(elem_type), Some(size)) = (array_element_type_ref(type_ref), array_size_ref(type_ref)) { + if elem_type.base == TypeBase::Byte && elem_type.array_dims.is_empty() { + return Some(size as i64); + } + if elem_type.base == TypeBase::Int && elem_type.array_dims.is_empty() { + return Some((size * 8) as i64); + } + } + return None; } -} -fn encoded_field_chunk_size(field: &ContractFieldAst) -> Result { - if field.type_name == "int" { - return Ok(10); + match type_ref.base { + TypeBase::Int => Some(8), + TypeBase::Bool => Some(1), + TypeBase::Byte => Some(1), + TypeBase::Pubkey => Some(32), + TypeBase::Sig => Some(65), + TypeBase::Datasig => Some(64), + TypeBase::String => None, } - let payload_size = fixed_field_byte_len(&field.type_name) - .ok_or_else(|| CompilerError::Unsupported(format!("readInputState does not support field type {}", field.type_name)))?; - Ok(data_prefix(payload_size).len() + payload_size) } -fn read_input_state_binding_expr( - input_idx: &Expr, - field: &ContractFieldAst, - field_chunk_offset: usize, - script_size_value: i64, -) -> Result { - let (field_payload_offset, field_payload_len, decode_int) = if field.type_name == "int" { - (field_chunk_offset + 1, 8usize, true) - } else { - let payload_len = fixed_field_byte_len(&field.type_name) - .ok_or_else(|| CompilerError::Unsupported(format!("readInputState does not support field type {}", field.type_name)))?; - (field_chunk_offset + data_prefix(payload_len).len(), payload_len, false) - }; - - let sig_len = Expr::Call { name: "OpTxInputScriptSigLen".to_string(), args: vec![input_idx.clone()] }; - let start = Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::Binary { op: BinaryOp::Sub, left: Box::new(sig_len), right: Box::new(Expr::Int(script_size_value)) }), - right: Box::new(Expr::Int(field_payload_offset as i64)), - }; - let end = Expr::Binary { op: BinaryOp::Add, left: Box::new(start.clone()), right: Box::new(Expr::Int(field_payload_len as i64)) }; - let substr = Expr::Call { name: "OpTxInputScriptSigSubstr".to_string(), args: vec![input_idx.clone(), start, end] }; - - if decode_int { Ok(Expr::Call { name: "OpBin2Num".to_string(), args: vec![substr] }) } else { Ok(substr) } +fn array_element_size_ref(type_ref: &TypeRef) -> Option { + array_element_type_ref(type_ref).and_then(|element| fixed_type_size_ref(&element)) } fn contains_return(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::Return { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { + match stmt { + Statement::Return { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - StatementKind::For { body, .. } => body.iter().any(contains_return), + Statement::For { body, .. } => body.iter().any(contains_return), _ => false, } } fn contains_yield(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::Yield { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { + match stmt { + Statement::Yield { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - StatementKind::For { body, .. } => body.iter().any(contains_yield), + Statement::For { body, .. } => body.iter().any(contains_yield), _ => false, } } -fn validate_function_body(function: &FunctionAst, options: CompileOptions) -> Result { - let has_yield = function.body.iter().any(contains_yield); - if !options.allow_yield && has_yield { - return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); - } - - let has_return = function.body.iter().any(contains_return); - if function.entrypoint && !options.allow_entrypoint_return && has_return { - return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); - } - - if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if function.body[..function.body.len() - 1].iter().any(contains_return) { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - if has_yield { - return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); - } - } - - Ok(has_return) -} - -fn validate_return_types(exprs: &[Expr], return_types: &[TypeRef], types: &HashMap) -> Result<(), CompilerError> { +fn validate_return_types( + exprs: &[Expr], + return_types: &[TypeRef], + types: &HashMap, + constants: &HashMap, +) -> Result<(), CompilerError> { if return_types.is_empty() { return Err(CompilerError::Unsupported("return requires function return types".to_string())); } if return_types.len() != exprs.len() { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } - for (expr, type_ref) in exprs.iter().zip(return_types.iter()) { - let type_name = type_name_from_ref(type_ref); - if !expr_matches_return_type(expr, &type_name, types) { + for (expr, return_type) in exprs.iter().zip(return_types.iter()) { + if !expr_matches_return_type_ref(expr, return_type, types, constants) { + let type_name = type_name_from_ref(return_type); return Err(CompilerError::Unsupported(format!("return value expects {type_name}"))); } } Ok(()) } -fn expr_matches_type_with_env(expr: &Expr, type_name: &str, types: &HashMap) -> bool { +fn has_explicit_array_size_ref(type_ref: &TypeRef) -> bool { + !matches!(type_ref.array_size(), Some(ArrayDim::Dynamic) | None) +} + +fn is_array_type_assignable_ref(actual: &TypeRef, expected: &TypeRef, constants: &HashMap) -> bool { + if actual == expected { + return true; + } + + if !is_array_type_ref(actual) || !is_array_type_ref(expected) { + return false; + } + + if array_element_type_ref(actual) != array_element_type_ref(expected) { + return false; + } + + if !has_explicit_array_size_ref(expected) { + return true; + } + + match (array_size_with_constants_ref(actual, constants), array_size_with_constants_ref(expected, constants)) { + (Some(actual_size), Some(expected_size)) => actual_size == expected_size, + _ => actual == expected, + } +} + +fn is_type_assignable_ref(actual: &TypeRef, expected: &TypeRef, constants: &HashMap) -> bool { + actual == expected || is_array_type_assignable_ref(actual, expected, constants) +} + +fn expr_matches_type_with_env_ref( + expr: &Expr, + type_ref: &TypeRef, + types: &HashMap, + constants: &HashMap, +) -> bool { match expr { - Expr::Identifier(name) => types.get(name).is_some_and(|t| t == type_name), - Expr::Array(values) => is_array_type(type_name) && array_literal_matches_type(values, type_name), - _ => expr_matches_type(expr, type_name), + Expr::Identifier(name) => { + types.get(name).and_then(|t| parse_type_ref(t).ok()).is_some_and(|t| is_type_assignable_ref(&t, type_ref, constants)) + } + Expr::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), + _ => expr_matches_type_ref(expr, type_ref), } } -fn expr_matches_return_type(expr: &Expr, type_name: &str, types: &HashMap) -> bool { +fn expr_matches_return_type_ref( + expr: &Expr, + type_ref: &TypeRef, + types: &HashMap, + constants: &HashMap, +) -> bool { match expr { - Expr::Identifier(name) => types.get(name).is_some_and(|t| t == type_name), - Expr::Array(values) => is_array_type(type_name) && array_literal_matches_type(values, type_name), - Expr::Int(_) | Expr::Bool(_) | Expr::Bytes(_) | Expr::String(_) => expr_matches_type(expr, type_name), + Expr::Identifier(name) => { + types.get(name).and_then(|t| parse_type_ref(t).ok()).is_some_and(|t| is_type_assignable_ref(&t, type_ref, constants)) + } + Expr::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), + Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) => expr_matches_type_ref(expr, type_ref), _ => true, } } -impl CompiledContract { - pub fn build_sig_script(&self, function_name: &str, args: Vec) -> Result, CompilerError> { - let function = self - .abi - .iter() - .find(|entry| entry.name == function_name) - .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", function_name)))?; +fn infer_fixed_array_type_from_initializer_ref( + declared_type: &TypeRef, + initializer: Option<&Expr>, + types: &HashMap, + constants: &HashMap, +) -> Option { + if !declared_type.array_size().is_some_and(|dim| matches!(dim, ArrayDim::Dynamic)) { + return None; + } - if function.inputs.len() != args.len() { - return Err(CompilerError::Unsupported(format!( - "function '{}' expects {} arguments", - function_name, - function.inputs.len() + let element_type = array_element_type_ref(declared_type)?; + let init = initializer?; + + match init { + Expr::Array(values) => { + let mut inferred = element_type.clone(); + inferred.array_dims.push(ArrayDim::Fixed(values.len())); + if array_literal_matches_type_with_env_ref(values, &inferred, types, constants) { Some(inferred) } else { None } + } + Expr::Identifier(name) => { + let other_type = parse_type_ref(types.get(name)?).ok()?; + if !is_array_type_ref(&other_type) || array_element_type_ref(&other_type) != Some(element_type.clone()) { + return None; + } + let size = array_size_with_constants_ref(&other_type, constants)?; + let mut inferred = element_type; + inferred.array_dims.push(ArrayDim::Fixed(size)); + Some(inferred) + } + _ => None, + } +} + +fn expr_matches_type(expr: &Expr, type_name: &str) -> bool { + parse_type_ref(type_name).is_ok_and(|type_ref| expr_matches_type_ref(expr, &type_ref)) +} + +fn array_literal_matches_type_with_env( + values: &[Expr], + type_name: &str, + types: &HashMap, + constants: &HashMap, +) -> bool { + parse_type_ref(type_name).is_ok_and(|type_ref| array_literal_matches_type_with_env_ref(values, &type_ref, types, constants)) +} + +fn is_array_type(type_name: &str) -> bool { + parse_type_ref(type_name).is_ok_and(|type_ref| is_array_type_ref(&type_ref)) +} + +fn array_element_type(type_name: &str) -> Option { + let type_ref = parse_type_ref(type_name).ok()?; + let element = array_element_type_ref(&type_ref)?; + Some(type_name_from_ref(&element)) +} + +fn array_size(type_name: &str) -> Option { + let type_ref = parse_type_ref(type_name).ok()?; + array_size_ref(&type_ref) +} + +fn array_size_with_constants(type_name: &str, constants: &HashMap) -> Option { + let type_ref = parse_type_ref(type_name).ok()?; + array_size_with_constants_ref(&type_ref, constants) +} + +fn fixed_type_size(type_name: &str) -> Option { + let type_ref = parse_type_ref(type_name).ok()?; + fixed_type_size_ref(&type_ref) +} + +fn array_element_size(type_name: &str) -> Option { + let type_ref = parse_type_ref(type_name).ok()?; + array_element_size_ref(&type_ref) +} + +fn is_type_assignable(actual: &str, expected: &str, constants: &HashMap) -> bool { + let Ok(actual_type) = parse_type_ref(actual) else { + return false; + }; + let Ok(expected_type) = parse_type_ref(expected) else { + return false; + }; + is_type_assignable_ref(&actual_type, &expected_type, constants) +} + +fn expr_matches_type_with_env( + expr: &Expr, + type_name: &str, + types: &HashMap, + constants: &HashMap, +) -> bool { + parse_type_ref(type_name).is_ok_and(|type_ref| expr_matches_type_with_env_ref(expr, &type_ref, types, constants)) +} + +fn infer_fixed_array_type_from_initializer( + declared_type: &str, + initializer: Option<&Expr>, + types: &HashMap, + constants: &HashMap, +) -> Option { + let declared_type = parse_type_ref(declared_type).ok()?; + infer_fixed_array_type_from_initializer_ref(&declared_type, initializer, types, constants).map(|t| type_name_from_ref(&t)) +} + +impl CompiledContract { + pub fn build_sig_script(&self, function_name: &str, args: Vec) -> Result, CompilerError> { + let function = self + .abi + .iter() + .find(|entry| entry.name == function_name) + .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", function_name)))?; + + if function.inputs.len() != args.len() { + return Err(CompilerError::Unsupported(format!( + "function '{}' expects {} arguments", + function_name, + function.inputs.len() ))); } @@ -587,12 +736,18 @@ impl CompiledContract { for (input, arg) in function.inputs.iter().zip(args) { if is_array_type(&input.type_name) { match arg { - Expr::Array(values) => { - let bytes = encode_array_literal(&values, &input.type_name)?; - builder.add_data(&bytes)?; - } - Expr::Bytes(value) => { - builder.add_data(&value)?; + Expr::Array(ref values) => { + // Check if it's a byte array or other array type + if is_byte_array(&arg) { + // Extract bytes from Expr::Byte array + let bytes: Vec = + values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + builder.add_data(&bytes)?; + } else { + // Regular array - encode it + let bytes = encode_array_literal(values, &input.type_name)?; + builder.add_data(&bytes)?; + } } _ => { return Err(CompilerError::Unsupported(format!( @@ -624,8 +779,10 @@ fn push_sigscript_arg(builder: &mut ScriptBuilder, arg: Expr) -> Result<(), Comp Expr::String(value) => { builder.add_data(value.as_bytes())?; } - Expr::Bytes(value) => { - builder.add_data(&value)?; + Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { + // Handle byte arrays + let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + builder.add_data(&bytes)?; } _ => { return Err(CompilerError::Unsupported("signature script arguments must be literals".to_string())); @@ -634,49 +791,120 @@ fn push_sigscript_arg(builder: &mut ScriptBuilder, arg: Expr) -> Result<(), Comp Ok(()) } -fn encode_array_literal(values: &[Expr], type_name: &str) -> Result, CompilerError> { - let element_type = array_element_type(type_name) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - let mut out = Vec::new(); - match element_type { +fn encode_fixed_size_value(value: &Expr, type_name: &str) -> Result, CompilerError> { + match type_name { "int" => { - for value in values { - let Expr::Int(number) = value else { - return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); - }; - out.extend(number.to_le_bytes()); - } + let Expr::Int(number) = value else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + Ok(number.to_le_bytes().to_vec()) + } + "bool" => { + let Expr::Bool(flag) = value else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + Ok(vec![u8::from(*flag)]) } "byte" => { - for value in values { - let Expr::Bytes(bytes) = value else { - return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); - }; - if bytes.len() != 1 { - return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); - } - out.extend(bytes); + let Expr::Byte(byte) = value else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + Ok(vec![*byte]) + } + "pubkey" => { + let Some(len) = byte_array_len(value) else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + if len != 32 { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); } + let Expr::Array(bytes_exprs) = value else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + Ok(bytes_exprs.iter().filter_map(|value| if let Expr::Byte(byte) = value { Some(*byte) } else { None }).collect()) } _ => { - let size = element_type - .strip_prefix("bytes") - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - for value in values { - let Expr::Bytes(bytes) = value else { - return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); - }; - if bytes.len() != size { + // Handle fixed-size byte arrays like byte[N] + if let (Some(inner_type), Some(size)) = (array_element_type(type_name), array_size(type_name)) { + if inner_type == "byte" { + let Some(len) = byte_array_len(value) else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + if len != size { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + } + let Expr::Array(bytes_exprs) = value else { + return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + }; + return Ok(bytes_exprs + .iter() + .filter_map(|value| if let Expr::Byte(byte) = value { Some(*byte) } else { None }) + .collect()); + } + } + + // Handle nested fixed-size arrays with known element sizes. + if let Expr::Array(values) = value { + let element_type = array_element_type(type_name) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let expected_len = array_size(type_name) + .ok_or_else(|| CompilerError::Unsupported("array literal element type mismatch".to_string()))?; + if values.len() != expected_len { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); } - out.extend(bytes); + + let mut encoded = Vec::new(); + for value in values { + encoded.extend(encode_fixed_size_value(value, &element_type)?); + } + return Ok(encoded); } + + Err(CompilerError::Unsupported("array literal element type mismatch".to_string())) } } +} + +fn encode_array_literal(values: &[Expr], type_name: &str) -> Result, CompilerError> { + let element_type = array_element_type(type_name) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let mut out = Vec::new(); + if fixed_type_size(&element_type).is_none() { + return Err(CompilerError::Unsupported("array element type must have known size".to_string())); + } + for value in values { + out.extend(encode_fixed_size_value(value, &element_type)?); + } Ok(out) } +fn infer_fixed_type_from_literal_expr(expr: &Expr) -> Option { + match expr { + Expr::Int(_) => Some("int".to_string()), + Expr::Bool(_) => Some("bool".to_string()), + Expr::Byte(_) => Some("byte".to_string()), + Expr::Array(values) if is_byte_array(expr) => Some(format!("byte[{}]", values.len())), + Expr::Array(values) => { + let nested_type = infer_fixed_array_literal_type(values)?; + Some(nested_type.trim_end_matches("[]").to_string()) + } + _ => None, + } +} + +fn infer_fixed_array_literal_type(values: &[Expr]) -> Option { + if values.is_empty() { + return None; + } + let first_type = infer_fixed_type_from_literal_expr(values.first()?)?; + fixed_type_size(&first_type)?; + if values.iter().skip(1).all(|value| infer_fixed_type_from_literal_expr(value).as_deref() == Some(first_type.as_str())) { + Some(format!("{}[]", first_type)) + } else { + None + } +} + pub fn function_branch_index(contract: &ContractAst, function_name: &str) -> Result { contract .functions @@ -687,6 +915,13 @@ pub fn function_branch_index(contract: &ContractAst, function_name: &str) -> Res .ok_or_else(|| CompilerError::Unsupported(format!("function '{function_name}' not found"))) } +#[derive(Debug)] +struct CompiledFunction { + name: String, + script: Vec, + debug: FunctionDebugRecorder, +} + fn compile_function( function: &FunctionAst, function_index: usize, @@ -698,599 +933,569 @@ fn compile_function( function_order: &HashMap, script_size: Option, ) -> Result { - let mut builder = ScriptBuilder::new(); - let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function); - - let mut env = constants.clone(); - let mut types = HashMap::new(); - let mut params = HashMap::new(); - let contract_field_count = contract_fields.len(); let param_count = function.params.len(); - for (index, param) in function.params.iter().enumerate() { - if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); - } - params.insert(param.name.clone(), (contract_field_count + (param_count - 1 - index)) as i64); - types.insert(param.name.clone(), param.type_name.clone()); - } + let mut params = function + .params + .iter() + .map(|param| param.name.clone()) + .enumerate() + .map(|(index, name)| (name, (contract_field_count + (param_count - 1 - index)) as i64)) + .collect::>(); + for (index, field) in contract_fields.iter().enumerate() { params.insert(field.name.clone(), (contract_field_count - 1 - index) as i64); - types.insert(field.name.clone(), field.type_name.clone()); } + let mut types = + function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); + for field in contract_fields { + types.insert(field.name.clone(), type_name_from_ref(&field.type_ref)); + } + for param in &function.params { + let param_type_name = type_name_from_ref(¶m.type_ref); + if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); + } + } for return_type in &function.return_types { let return_type_name = type_name_from_ref(return_type); if is_array_type(&return_type_name) && array_element_size(&return_type_name).is_none() { - return Err(CompilerError::Unsupported(format!( - "array element type must have known size: {return_type_name}" - ))); + return Err(CompilerError::Unsupported(format!("array element type must have known size: {return_type_name}"))); } } + let mut env: HashMap = constants.clone(); + let mut builder = ScriptBuilder::new(); + let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function); + let mut yields: Vec = Vec::new(); + + if !options.allow_yield && function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); + } - let has_return = validate_function_body(function, options)?; + if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { + return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); + } - let yields = { - let mut body_compiler = FunctionBodyCompiler { - builder: &mut builder, + let has_return = function.body.iter().any(contains_return); + if has_return { + if !matches!(function.body.last(), Some(Statement::Return { .. })) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body[..function.body.len() - 1].iter().any(contains_return) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + } + if function.return_types.is_empty() { + return Err(CompilerError::Unsupported("return requires function return types".to_string())); + } + } + + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = builder.script().len(); + if matches!(stmt, Statement::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let Statement::Return { exprs } = stmt else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + recorder.record_statement_updates(None, start, builder.script().len(), Vec::new()); + continue; + } + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + &mut builder, options, - debug_recorder: &mut recorder, contract_fields, contract_field_prefix_len, - contract_constants: constants, + constants, functions, function_order, function_index, + &mut yields, script_size, - inline_frame_counter: 1, - }; - body_compiler.compile_function_body(function, &mut env, ¶ms, &mut types)? - }; + &mut recorder, + )?; + let end = builder.script().len(); + recorder.record_statement_updates(None, start, end, Vec::new()); + } - if function.entrypoint { - if !has_return && !function.return_types.is_empty() { - return Err(CompilerError::Unsupported("entrypoint function must not have return types unless it returns".to_string())); + let yield_count = yields.len(); + if yield_count == 0 { + for _ in 0..param_count { + builder.add_op(OpDrop)?; } - let yield_count = yields.len(); - if yield_count == 0 { - for _ in 0..(param_count + contract_field_count) { - builder.add_op(OpDrop)?; - } - builder.add_op(OpTrue)?; - } else { - let mut stack_depth = 0i64; - for expr in &yields { - compile_expr(expr, &env, ¶ms, &types, &mut builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; - } - for _ in 0..(param_count + contract_field_count) { - builder.add_i64(yield_count as i64)?; - builder.add_op(OpRoll)?; - builder.add_op(OpDrop)?; - } + for _ in 0..contract_field_count { + builder.add_op(OpDrop)?; + } + builder.add_op(OpTrue)?; + } else { + let mut stack_depth = 0i64; + for expr in &yields { + compile_expr( + expr, + &env, + ¶ms, + &types, + &mut builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + constants, + )?; + } + for _ in 0..param_count { + builder.add_i64(yield_count as i64)?; + builder.add_op(OpRoll)?; + builder.add_op(OpDrop)?; + } + for _ in 0..contract_field_count { + builder.add_i64(yield_count as i64)?; + builder.add_op(OpRoll)?; + builder.add_op(OpDrop)?; } } - Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) } -struct FunctionBodyCompiler<'a> { - builder: &'a mut ScriptBuilder, +#[allow(clippy::too_many_arguments)] +fn compile_statement( + stmt: &Statement, + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + builder: &mut ScriptBuilder, options: CompileOptions, - debug_recorder: &'a mut FunctionDebugRecorder, - contract_fields: &'a [ContractFieldAst], + contract_fields: &[ContractFieldAst], contract_field_prefix_len: usize, - contract_constants: &'a HashMap, - functions: &'a HashMap, - function_order: &'a HashMap, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, function_index: usize, + yields: &mut Vec, script_size: Option, - inline_frame_counter: u32, -} - -impl<'a> FunctionBodyCompiler<'a> { - fn compile_function_body( - &mut self, - function: &FunctionAst, - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - ) -> Result, CompilerError> { - let mut yields = Vec::new(); - for stmt in &function.body { - if let StatementKind::Return { exprs, .. } = &stmt.kind { - validate_return_types(exprs, &function.return_types, types)?; - for expr in exprs { - yields.push(resolve_expr(expr.clone(), env, &mut HashSet::new())?); - } - continue; - } - self.compile_statement(stmt, env, params, types, &mut yields)?; - } - Ok(yields) - } - - fn compile_inline_call_and_discard_returns( - &mut self, - name: &str, - args: &[Expr], - params: &HashMap, - types: &mut HashMap, - env: &mut HashMap, - call_span: Option, - ) -> Result<(), CompilerError> { - let returns = self.compile_inline_call(name, args, params, types, env, call_span)?; - self.compile_and_drop_returns(returns, env, params, types) - } - - fn compile_and_drop_returns( - &mut self, - returns: Vec, - env: &HashMap, - params: &HashMap, - types: &HashMap, - ) -> Result<(), CompilerError> { - let mut stack_depth = 0i64; - for expr in returns { - compile_expr( - &expr, - env, - params, - types, - self.builder, - self.options, - &mut HashSet::new(), - &mut stack_depth, - self.script_size, - )?; - self.builder.add_op(OpDrop)?; - stack_depth -= 1; - } - Ok(()) - } + debug_recorder: &mut FunctionDebugRecorder, +) -> Result<(), CompilerError> { + match stmt { + Statement::VariableDefinition { type_ref, name, expr, .. } => { + let type_name = type_name_from_ref(type_ref); + let effective_type_name = + if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { + infer_fixed_array_type_from_initializer(&type_name, expr.as_ref(), types, contract_constants) + .unwrap_or_else(|| type_name.clone()) + } else { + type_name.clone() + }; - fn compile_statement( - &mut self, - stmt: &Statement, - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - yields: &mut Vec, - ) -> Result<(), CompilerError> { - let start = self.builder.script().len(); - let mut variables = Vec::new(); + // Check if this is a fixed-size array (e.g., byte[N]) or dynamic array (e.g., byte[]) + let is_fixed_size_array = + is_array_type(&effective_type_name) && array_size_with_constants(&effective_type_name, contract_constants).is_some(); + let is_dynamic_array = + is_array_type(&effective_type_name) && array_size_with_constants(&effective_type_name, contract_constants).is_none(); - match &stmt.kind { - StatementKind::VariableDefinition { type_name, name, expr, .. } => { - if is_array_type(type_name) { - if array_element_size(type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {type_name}"))); + if is_dynamic_array { + if array_element_size(&effective_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {effective_type_name}"))); + } + + // For byte[] (dynamic byte arrays), allow initialization from any bytes expression + let is_byte_array_type = effective_type_name.starts_with("byte[") && effective_type_name.ends_with("[]"); + + let initial = match expr { + Some(Expr::Identifier(other)) => match types.get(other) { + Some(other_type) if is_type_assignable(other_type, &effective_type_name, contract_constants) => { + Expr::Identifier(other.clone()) + } + Some(_) => { + return Err(CompilerError::Unsupported("array assignment requires compatible array types".to_string())); + } + None => return Err(CompilerError::UndefinedIdentifier(other.clone())), + }, + Some(e) if is_byte_array_type => { + // byte[] can be initialized from any bytes expression + e.clone() + } + Some(Expr::Array(values)) => { + if !array_literal_matches_type_with_env(values, &effective_type_name, types, contract_constants) { + return Err(CompilerError::Unsupported("array initializer must be another array".to_string())); + } + resolve_expr(Expr::Array(values.clone()), env, &mut HashSet::new())? + } + Some(_) => return Err(CompilerError::Unsupported("array initializer must be another array".to_string())), + None => Expr::Array(Vec::new()), + }; + env.insert(name.clone(), initial); + types.insert(name.clone(), effective_type_name.clone()); + Ok(()) + } else if is_fixed_size_array { + // Fixed-size arrays like byte[N] can be initialized from expressions + let expr = + expr.clone().ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; + + // For array literals, validate that the size matches the declared type + if let Expr::Array(values) = &expr { + if let Some(expected_size) = array_size_with_constants(&effective_type_name, contract_constants) { + if values.len() != expected_size { + return Err(CompilerError::Unsupported(format!( + "array size mismatch: expected {} elements for type {}, got {}", + expected_size, + effective_type_name, + values.len() + ))); + } + } + + // Validate element types match + if !array_literal_matches_type_with_env(values, &effective_type_name, types, contract_constants) { + return Err(CompilerError::Unsupported(format!( + "array element type mismatch for type {}", + effective_type_name + ))); } - let initial = match expr { - Some(Expr::Identifier(other)) => match types.get(other) { - Some(other_type) if other_type == type_name => Expr::Identifier(other.clone()), - Some(_) => { - return Err(CompilerError::Unsupported( - "array assignment requires compatible array types".to_string(), - )); - } - None => return Err(CompilerError::UndefinedIdentifier(other.clone())), - }, - Some(_) => return Err(CompilerError::Unsupported("array initializer must be another array".to_string())), - None => Expr::Bytes(Vec::new()), - }; - self.debug_recorder.variable_update(env, &mut variables, name, type_name, initial.clone())?; - env.insert(name.clone(), initial); - types.insert(name.clone(), type_name.clone()); - } else { - let expr = expr - .clone() - .ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; - self.debug_recorder.variable_update(env, &mut variables, name, type_name, expr.clone())?; - env.insert(name.clone(), expr); - types.insert(name.clone(), type_name.clone()); } + + let stored_expr = if matches!(expr, Expr::Array(_)) { resolve_expr(expr, env, &mut HashSet::new())? } else { expr }; + env.insert(name.clone(), stored_expr); + types.insert(name.clone(), effective_type_name.clone()); + Ok(()) + } else { + let expr = + expr.clone().ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; + env.insert(name.clone(), expr); + types.insert(name.clone(), effective_type_name.clone()); + Ok(()) } - StatementKind::ArrayPush { name, expr, .. } => { - let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; - if !is_array_type(array_type) { - return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); - } - let element_type = array_element_type(array_type) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - let element_size = array_element_size(array_type) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - let element_expr = if element_type == "int" { - Expr::Call { name: "bytes8".to_string(), args: vec![expr.clone()] } - } else if element_type == "byte" { - Expr::Call { name: "bytes1".to_string(), args: vec![expr.clone()] } - } else if element_type.starts_with("bytes") { - if expr_is_bytes(expr, env, types) { - expr.clone() + } + Statement::ArrayPush { name, expr } => { + let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; + if !is_array_type(array_type) { + return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); + } + let element_type = array_element_type(array_type) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let _element_size = array_element_size(array_type) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + let element_expr = if element_type == "int" { + Expr::Call { name: "byte[8]".to_string(), args: vec![expr.clone()] } + } else if element_type == "byte" { + Expr::Call { name: "byte[1]".to_string(), args: vec![expr.clone()] } + } else if element_type.contains('[') && element_type.starts_with("byte") { + // Handle byte[N] type + if expr_is_bytes(expr, env, types) { + expr.clone() + } else { + // Try byte[N] syntax + if let Some(bracket_pos) = element_type.find('[') { + if element_type.ends_with(']') { + let base_type = &element_type[..bracket_pos]; + let size_str = &element_type[bracket_pos + 1..element_type.len() - 1]; + if base_type == "byte" { + if let Ok(_size) = size_str.parse::() { + // Cast expression to byte[N] + Expr::Call { name: element_type.to_string(), args: vec![expr.clone()] } + } else { + return Err(CompilerError::Unsupported("invalid array size".to_string())); + } + } else { + return Err(CompilerError::Unsupported("array element type not supported".to_string())); + } + } else { + return Err(CompilerError::Unsupported("array element type not supported".to_string())); + } } else { - Expr::Call { name: format!("bytes{element_size}"), args: vec![expr.clone()] } + return Err(CompilerError::Unsupported("array element type not supported".to_string())); } - } else { - return Err(CompilerError::Unsupported("array element type not supported".to_string())); - }; + } + } else { + return Err(CompilerError::Unsupported("array element type not supported".to_string())); + }; - let current = env.get(name).cloned().unwrap_or_else(|| Expr::Bytes(Vec::new())); - let updated = Expr::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }; - self.debug_recorder.variable_update(env, &mut variables, name, array_type, updated.clone())?; - env.insert(name.clone(), updated); + let current = env.get(name).cloned().unwrap_or_else(|| Expr::Array(Vec::new())); + let updated = Expr::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }; + env.insert(name.clone(), updated); + Ok(()) + } + Statement::Require { expr, .. } => { + let mut stack_depth = 0i64; + compile_expr( + expr, + env, + params, + types, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpVerify)?; + Ok(()) + } + Statement::TimeOp { tx_var, expr, .. } => { + compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size, contract_constants) + } + Statement::If { condition, then_branch, else_branch } => compile_if_statement( + condition, + then_branch, + else_branch.as_deref(), + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + ), + Statement::For { ident, start, end, body } => compile_for_statement( + ident, + start, + end, + body, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + ), + Statement::Yield { expr } => { + let mut visiting = HashSet::new(); + let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; + yields.push(resolved); + Ok(()) + } + Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), + Statement::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { + Expr::Split { source, index, .. } => { + env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); + env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); + Ok(()) } - StatementKind::Require { expr, .. } => { - let mut stack_depth = 0i64; - compile_expr( - expr, + _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), + }, + Statement::FunctionCall { name, args } => { + if name == "validateOutputState" { + return compile_validate_output_state_statement( + args, env, params, types, - self.builder, - self.options, - &mut HashSet::new(), - &mut stack_depth, - self.script_size, - )?; - self.builder.add_op(OpVerify)?; - } - StatementKind::TimeOp { tx_var, expr, .. } => { - compile_time_op_statement(tx_var, expr, env, params, types, self.builder, self.options, self.script_size)?; - } - StatementKind::If { condition, then_branch, else_branch, .. } => { - self.compile_if_statement(condition, then_branch, else_branch.as_deref(), env, params, types, yields)?; - } - StatementKind::For { ident, start, end, body, .. } => { - self.compile_for_statement(ident, start, end, body, env, params, types, yields, stmt.span)?; - } - StatementKind::Yield { expr, .. } => { - let mut visiting = HashSet::new(); - let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; - yields.push(resolved); - } - StatementKind::Return { .. } => { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + builder, + options, + contract_fields, + contract_field_prefix_len, + script_size, + contract_constants, + ); } - StatementKind::TupleAssignment { left_type, left_name, right_type, right_name, expr, .. } => match expr.clone() { - Expr::Split { source, index, .. } => { - let left_expr = Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }; - let right_expr = Expr::Split { source, index, part: SplitPart::Right }; - self.debug_recorder.variable_update(env, &mut variables, left_name, left_type, left_expr.clone())?; - self.debug_recorder.variable_update(env, &mut variables, right_name, right_type, right_expr.clone())?; - env.insert(left_name.clone(), left_expr); - env.insert(right_name.clone(), right_expr); - } - _ => return Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), - }, - StatementKind::FunctionCall { name, args, .. } => { - if name == "validateOutputState" { - compile_validate_output_state_statement( - args, + let returns = compile_inline_call( + name, + args, + types, + env, + builder, + options, + contract_constants, + functions, + function_order, + function_index, + script_size, + debug_recorder, + )?; + if !returns.is_empty() { + let mut stack_depth = 0i64; + for expr in returns { + compile_expr( + &expr, env, params, types, - self.builder, - self.options, - self.contract_fields, - self.contract_field_prefix_len, - self.script_size, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + contract_constants, )?; - } else { - self.compile_inline_call_and_discard_returns(name, args, params, types, env, stmt.span)?; + builder.add_op(OpDrop)?; + stack_depth -= 1; } } - StatementKind::StateFunctionCallAssign { bindings, name, args, .. } => { - if name == "readInputState" { - compile_read_input_state_statement(bindings, args, env, types, self.contract_fields, self.script_size)?; - for binding in bindings { - let expr = env.get(&binding.name).cloned().unwrap_or_else(|| Expr::Identifier(binding.name.clone())); - self.debug_recorder.variable_update(env, &mut variables, &binding.name, &binding.type_name, expr)?; - } - } else { - return Err(CompilerError::Unsupported(format!( - "state destructuring assignment is only supported for readInputState(), got '{}()'", - name - ))); - } - } - StatementKind::FunctionCallAssign { bindings, name, args, .. } => { - let return_types = { - let function = self - .functions - .get(name) - .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if function.return_types.is_empty() { - return Err(CompilerError::Unsupported("function has no return types".to_string())); - } - if function.return_types.len() != bindings.len() { - return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); - } - for (binding, return_type) in bindings.iter().zip(function.return_types.iter()) { - if binding.type_name != type_name_from_ref(return_type) { - return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); - } - } - function.return_types.iter().map(type_name_from_ref).collect::>() - }; - let returns = self.compile_inline_call(name, args, params, types, env, stmt.span)?; - if returns.len() != return_types.len() { - return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); - } - for (binding, expr) in bindings.iter().zip(returns.into_iter()) { - self.debug_recorder.variable_update(env, &mut variables, &binding.name, &binding.type_name, expr.clone())?; - env.insert(binding.name.clone(), expr); - types.insert(binding.name.clone(), binding.type_name.clone()); + Ok(()) + } + Statement::StateFunctionCallAssign { bindings, name, args } => { + if name == "readInputState" { + return compile_read_input_state_statement( + bindings, + args, + env, + types, + contract_fields, + script_size, + contract_constants, + ); + } + Err(CompilerError::Unsupported(format!( + "state destructuring assignment is only supported for readInputState(), got '{}()'", + name + ))) + } + Statement::FunctionCallAssign { bindings, name, args } => { + let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if function.return_types.is_empty() { + return Err(CompilerError::Unsupported("function has no return types".to_string())); + } + if function.return_types.len() != bindings.len() { + return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); + } + for (binding, return_type) in bindings.iter().zip(function.return_types.iter()) { + let binding_type_name = type_name_from_ref(&binding.type_ref); + let return_type_name = type_name_from_ref(return_type); + if binding_type_name != return_type_name { + return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); } } - StatementKind::Assign { name, expr, .. } => { - if let Some(type_name) = types.get(name) { - if is_array_type(type_name) { - match expr { - Expr::Identifier(other) => match types.get(other) { - Some(other_type) if other_type == type_name => { - self.debug_recorder.variable_update( - env, - &mut variables, - name, - type_name, - Expr::Identifier(other.clone()), - )?; - env.insert(name.clone(), Expr::Identifier(other.clone())); - } - Some(_) => { - return Err(CompilerError::Unsupported( - "array assignment requires compatible array types".to_string(), - )); - } - None => return Err(CompilerError::UndefinedIdentifier(other.clone())), - }, - _ => { + let returns = compile_inline_call( + name, + args, + types, + env, + builder, + options, + contract_constants, + functions, + function_order, + function_index, + script_size, + debug_recorder, + )?; + if returns.len() != bindings.len() { + return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); + } + for (binding, expr) in bindings.iter().zip(returns.into_iter()) { + env.insert(binding.name.clone(), expr); + types.insert(binding.name.clone(), type_name_from_ref(&binding.type_ref)); + } + Ok(()) + } + Statement::Assign { name, expr } => { + if let Some(type_name) = types.get(name) { + if is_array_type(type_name) { + match expr { + Expr::Identifier(other) => match types.get(other) { + Some(other_type) if is_type_assignable(other_type, type_name, contract_constants) => { + env.insert(name.clone(), Expr::Identifier(other.clone())); + return Ok(()); + } + Some(_) => { return Err(CompilerError::Unsupported( - "array assignment only supports array identifiers".to_string(), + "array assignment requires compatible array types".to_string(), )); } - } - } else { - let updated = - if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; - let resolved = resolve_expr(updated, env, &mut HashSet::new())?; - self.debug_recorder.variable_update(env, &mut variables, name, type_name, resolved.clone())?; - env.insert(name.clone(), resolved); + None => return Err(CompilerError::UndefinedIdentifier(other.clone())), + }, + _ => return Err(CompilerError::Unsupported("array assignment only supports array identifiers".to_string())), } - } else { - let updated = - if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; - let resolved = resolve_expr(updated, env, &mut HashSet::new())?; - let type_name = "unknown"; - self.debug_recorder.variable_update(env, &mut variables, name, type_name, resolved.clone())?; - env.insert(name.clone(), resolved); - } - } - StatementKind::Console { .. } => {} - } - - let end = self.builder.script().len(); - // Record updates at the end of the statement so variables reflect post-statement state - // when the debugger is paused at the next byte offset. - self.debug_recorder.record_statement_updates(stmt, start, end, variables); - Ok(()) - } - - fn compile_inline_call( - &mut self, - name: &str, - args: &[Expr], - caller_params: &HashMap, - caller_types: &mut HashMap, - caller_env: &mut HashMap, - call_span: Option, - ) -> Result, CompilerError> { - let function = self.functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - let callee_index = self - .function_order - .get(name) - .copied() - .ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= self.function_index { - return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); - } - - if function.params.len() != args.len() { - return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); - } - for (param, arg) in function.params.iter().zip(args.iter()) { - if !expr_matches_type_with_env(arg, ¶m.type_name, caller_types) { - return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param.type_name))); - } - } - - let mut types = function.params.iter().map(|param| (param.name.clone(), param.type_name.clone())).collect::>(); - for param in &function.params { - if is_array_type(¶m.type_name) && array_element_size(¶m.type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param.type_name))); + } } + let updated = if let Some(previous) = env.get(name) { replace_identifier(expr, name, previous) } else { expr.clone() }; + let resolved = resolve_expr(updated, env, &mut HashSet::new())?; + env.insert(name.clone(), resolved); + Ok(()) } + Statement::Console { .. } => Ok(()), + } +} - let mut env: HashMap = self.contract_constants.clone(); - for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { - let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let temp_name = format!("__arg_{name}_{index}"); - env.insert(temp_name.clone(), resolved.clone()); - types.insert(temp_name.clone(), param.type_name.clone()); - env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); - caller_env.insert(temp_name.clone(), resolved); - caller_types.insert(temp_name, param.type_name.clone()); - } - - validate_function_body(function, self.options)?; - let yields = self.compile_inline_callee(name, function, callee_index, call_span, caller_params, &mut env, &mut types)?; - - for (name, value) in &env { - if name.starts_with("__arg_") { - if let Some(type_name) = types.get(name) { - caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); - } - caller_env.entry(name.clone()).or_insert_with(|| value.clone()); - } - } - - Ok(yields) - } - - fn compile_inline_callee( - &mut self, - name: &str, - function: &FunctionAst, - callee_index: usize, - call_span: Option, - caller_params: &HashMap, - env: &mut HashMap, - types: &mut HashMap, - ) -> Result, CompilerError> { - let call_offset = self.builder.script().len(); - self.debug_recorder.record_inline_call_enter(call_span, call_offset, name); - - // Compile callee statements using an isolated inline debug recorder so emitted - // events/variable updates carry the callee frame id and call depth. - let frame_id = self.inline_frame_counter; - self.inline_frame_counter = self.inline_frame_counter.saturating_add(1); - let mut debug_recorder = self.debug_recorder.new_inline_child(frame_id); - // Inline params are not stack-mapped like normal function params; materialize - // them as variable updates at the inline entry virtual step. - debug_recorder.record_inline_param_updates(function, env, call_span, call_offset)?; - - let (yields, next_inline_frame_counter) = { - let mut callee_compiler = FunctionBodyCompiler { - builder: &mut *self.builder, - options: self.options, - debug_recorder: &mut debug_recorder, - contract_fields: self.contract_fields, - contract_field_prefix_len: self.contract_field_prefix_len, - contract_constants: self.contract_constants, - functions: self.functions, - function_order: self.function_order, - function_index: callee_index, - script_size: self.script_size, - inline_frame_counter: self.inline_frame_counter, - }; - let yields = callee_compiler.compile_function_body(function, env, caller_params, types)?; - (yields, callee_compiler.inline_frame_counter) - }; - - self.inline_frame_counter = next_inline_frame_counter; - // Remap inline-local sequence numbers and merge events/updates back into - // the parent function recorder. - self.debug_recorder.merge_inline_events(&debug_recorder); - self.debug_recorder.record_inline_call_exit(call_span, self.builder.script().len(), name); - - Ok(yields) - } - - fn compile_if_statement( - &mut self, - condition: &Expr, - then_branch: &[Statement], - else_branch: Option<&[Statement]>, - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - yields: &mut Vec, - ) -> Result<(), CompilerError> { - let mut stack_depth = 0i64; - compile_expr( - condition, - env, - params, - types, - self.builder, - self.options, - &mut HashSet::new(), - &mut stack_depth, - self.script_size, - )?; - self.builder.add_op(OpIf)?; - - let original_env = env.clone(); - let mut then_env = original_env.clone(); - let mut then_types = types.clone(); - self.compile_block(then_branch, &mut then_env, params, &mut then_types, yields)?; - - let mut else_env = original_env.clone(); - if let Some(else_branch) = else_branch { - self.builder.add_op(OpElse)?; - let mut else_types = types.clone(); - self.compile_block(else_branch, &mut else_env, params, &mut else_types, yields)?; - } - - self.builder.add_op(OpEndIf)?; - - let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; - merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); - Ok(()) +fn encoded_field_chunk_size(field: &ContractFieldAst, contract_constants: &HashMap) -> Result { + if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { + return Ok(10); } - fn compile_block( - &mut self, - statements: &[Statement], - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - yields: &mut Vec, - ) -> Result<(), CompilerError> { - for stmt in statements { - self.compile_statement(stmt, env, params, types, yields)?; - } - Ok(()) + if field.type_ref.base != TypeBase::Byte { + return Err(CompilerError::Unsupported(format!( + "readInputState does not support field type {}", + type_name_from_ref(&field.type_ref) + ))); } - fn compile_for_statement( - &mut self, - ident: &str, - start_expr: &Expr, - end_expr: &Expr, - body: &[Statement], - env: &mut HashMap, - params: &HashMap, - types: &mut HashMap, - yields: &mut Vec, - span: Option, - ) -> Result<(), CompilerError> { - let start = eval_const_int(start_expr, self.contract_constants)?; - let end = eval_const_int(end_expr, self.contract_constants)?; - if end < start { - return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); - } + let payload_size = if field.type_ref.array_dims.is_empty() { + 1usize + } else { + array_size_with_constants_ref(&field.type_ref, contract_constants).ok_or_else(|| { + CompilerError::Unsupported(format!("readInputState does not support field type {}", type_name_from_ref(&field.type_ref))) + })? + }; - let name = ident.to_string(); - let previous = env.get(&name).cloned(); - let previous_type = types.get(&name).cloned(); - types.insert(name.clone(), "int".to_string()); - for value in start..end { - let index_expr = Expr::Int(value); - env.insert(name.clone(), index_expr.clone()); - let bytecode_offset = self.builder.script().len(); - self.debug_recorder.record_virtual_updates(span, bytecode_offset, vec![(name.clone(), "int".to_string(), index_expr)]); - self.compile_block(body, env, params, types, yields)?; - } + Ok(data_prefix(payload_size).len() + payload_size) +} - match previous { - Some(expr) => { - env.insert(name, expr); - } - None => { - env.remove(&name); - } - } - match previous_type { - Some(type_name) => { - types.insert(ident.to_string(), type_name); - } - None => { - types.remove(ident); - } - } +fn read_input_state_binding_expr( + input_idx: &Expr, + field: &ContractFieldAst, + field_chunk_offset: usize, + script_size_value: i64, + contract_constants: &HashMap, +) -> Result { + let (field_payload_offset, field_payload_len, decode_int) = + if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { + (field_chunk_offset + 1, 8usize, true) + } else if field.type_ref.base == TypeBase::Byte { + let payload_len = if field.type_ref.array_dims.is_empty() { + 1usize + } else { + array_size_with_constants_ref(&field.type_ref, contract_constants).ok_or_else(|| { + CompilerError::Unsupported(format!( + "readInputState does not support field type {}", + type_name_from_ref(&field.type_ref) + )) + })? + }; + (field_chunk_offset + data_prefix(payload_len).len(), payload_len, false) + } else { + return Err(CompilerError::Unsupported(format!( + "readInputState does not support field type {}", + type_name_from_ref(&field.type_ref) + ))); + }; - Ok(()) - } + let sig_len = Expr::Call { name: "OpTxInputScriptSigLen".to_string(), args: vec![input_idx.clone()] }; + let start = Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::Binary { op: BinaryOp::Sub, left: Box::new(sig_len), right: Box::new(Expr::Int(script_size_value)) }), + right: Box::new(Expr::Int(field_payload_offset as i64)), + }; + let end = Expr::Binary { op: BinaryOp::Add, left: Box::new(start.clone()), right: Box::new(Expr::Int(field_payload_len as i64)) }; + let substr = Expr::Call { name: "OpTxInputScriptSigSubstr".to_string(), args: vec![input_idx.clone(), start, end] }; + + if decode_int { Ok(Expr::Call { name: "OpBin2Num".to_string(), args: vec![substr] }) } else { Ok(substr) } } fn compile_read_input_state_statement( @@ -1300,6 +1505,7 @@ fn compile_read_input_state_statement( types: &mut HashMap, contract_fields: &[ContractFieldAst], script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { if args.len() != 1 { return Err(CompilerError::Unsupported("readInputState(input_idx) expects 1 argument".to_string())); @@ -1322,19 +1528,24 @@ fn compile_read_input_state_statement( let input_idx = args[0].clone(); let mut field_chunk_offset = 0usize; + for field in contract_fields { let binding = bindings_by_field.get(field.name.as_str()).ok_or_else(|| { CompilerError::Unsupported("readInputState bindings must include all contract fields exactly once".to_string()) })?; - if binding.type_name != field.type_name { - return Err(CompilerError::Unsupported(format!("readInputState binding '{}' expects {}", binding.name, field.type_name))); + let binding_type = type_name_from_ref(&binding.type_ref); + let field_type = type_name_from_ref(&field.type_ref); + if binding_type != field_type { + return Err(CompilerError::Unsupported(format!("readInputState binding '{}' expects {}", binding.name, field_type))); } - let binding_expr = read_input_state_binding_expr(&input_idx, field, field_chunk_offset, script_size_value)?; + let binding_expr = + read_input_state_binding_expr(&input_idx, field, field_chunk_offset, script_size_value, contract_constants)?; env.insert(binding.name.clone(), binding_expr); - types.insert(binding.name.clone(), binding.type_name.clone()); - field_chunk_offset += encoded_field_chunk_size(field)?; + types.insert(binding.name.clone(), binding_type); + + field_chunk_offset += encoded_field_chunk_size(field, contract_constants)?; } Ok(()) @@ -1351,6 +1562,7 @@ fn compile_validate_output_state_statement( contract_fields: &[ContractFieldAst], contract_field_prefix_len: usize, script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { if args.len() != 2 { return Err(CompilerError::Unsupported("validateOutputState(output_idx, new_state) expects 2 arguments".to_string())); @@ -1380,8 +1592,19 @@ fn compile_validate_output_state_statement( return Err(CompilerError::Unsupported(format!("missing state field '{}'", field.name))); }; - if field.type_name == "int" { - compile_expr(new_value, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { + compile_expr( + new_value, + env, + params, + types, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + contract_constants, + )?; builder.add_i64(8)?; stack_depth += 1; builder.add_op(OpNum2Bin)?; @@ -1398,11 +1621,35 @@ fn compile_validate_output_state_statement( continue; } - let field_size = fixed_field_byte_len(&field.type_name).ok_or_else(|| { - CompilerError::Unsupported(format!("validateOutputState does not support field type {}", field.type_name)) - })?; + let field_size = if field.type_ref.base == TypeBase::Byte { + if field.type_ref.array_dims.is_empty() { + Some(1usize) + } else { + array_size_with_constants_ref(&field.type_ref, contract_constants) + } + } else { + None + }; + + let Some(field_size) = field_size else { + return Err(CompilerError::Unsupported(format!( + "validateOutputState does not support field type {}", + type_name_from_ref(&field.type_ref) + ))); + }; - compile_expr(new_value, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + compile_expr( + new_value, + env, + params, + types, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + contract_constants, + )?; let prefix = data_prefix(field_size); builder.add_data(&prefix)?; stack_depth += 1; @@ -1438,30 +1685,252 @@ fn compile_validate_output_state_statement( stack_depth -= 1; } - builder.add_op(OpBlake2b)?; - builder.add_data(&[0x00, 0x00])?; - stack_depth += 1; - builder.add_data(&[OpBlake2b])?; - stack_depth += 1; - builder.add_op(OpCat)?; - stack_depth -= 1; - builder.add_data(&[0x20])?; - stack_depth += 1; - builder.add_op(OpCat)?; - stack_depth -= 1; - builder.add_op(OpSwap)?; - builder.add_op(OpCat)?; - stack_depth -= 1; - builder.add_data(&[OpEqual])?; - stack_depth += 1; - builder.add_op(OpCat)?; - stack_depth -= 1; - - compile_expr(output_idx, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, Some(script_size_value))?; - builder.add_op(OpTxOutputSpk)?; - builder.add_op(OpEqual)?; - builder.add_op(OpVerify)?; + builder.add_op(OpBlake2b)?; + builder.add_data(&[0x00, 0x00])?; + stack_depth += 1; + builder.add_data(&[OpBlake2b])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_data(&[0x20])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + stack_depth -= 1; + builder.add_data(&[OpEqual])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; + + compile_expr( + output_idx, + env, + params, + types, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + Some(script_size_value), + contract_constants, + )?; + builder.add_op(OpTxOutputSpk)?; + builder.add_op(OpEqual)?; + builder.add_op(OpVerify)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn compile_inline_call( + name: &str, + args: &[Expr], + caller_types: &mut HashMap, + caller_env: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + caller_index: usize, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result, CompilerError> { + let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let callee_index = + function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= caller_index { + return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); + } + + if function.params.len() != args.len() { + return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); + } + for (param, arg) in function.params.iter().zip(args.iter()) { + let param_type_name = type_name_from_ref(¶m.type_ref); + if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, contract_constants) { + return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); + } + } + + let mut types = + function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); + for param in &function.params { + let param_type_name = type_name_from_ref(¶m.type_ref); + if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); + } + } + + let mut env: HashMap = contract_constants.clone(); + for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { + let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; + let temp_name = format!("__arg_{name}_{index}"); + let param_type_name = type_name_from_ref(¶m.type_ref); + env.insert(temp_name.clone(), resolved.clone()); + types.insert(temp_name.clone(), param_type_name.clone()); + env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); + caller_env.insert(temp_name.clone(), resolved); + caller_types.insert(temp_name, param_type_name); + } + + if !options.allow_yield && function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); + } + + if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { + return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); + } + + let has_return = function.body.iter().any(contains_return); + if has_return { + if !matches!(function.body.last(), Some(Statement::Return { .. })) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body[..function.body.len() - 1].iter().any(contains_return) { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + if function.body.iter().any(contains_yield) { + return Err(CompilerError::Unsupported("return cannot be combined with yield".to_string())); + } + } + + let mut yields: Vec = Vec::new(); + let params = HashMap::new(); + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = builder.script().len(); + if matches!(stmt, Statement::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let Statement::Return { exprs } = stmt else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, contract_constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + debug_recorder.record_statement_updates(None, start, builder.script().len(), Vec::new()); + continue; + } + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + builder, + options, + &[], + 0, + contract_constants, + functions, + function_order, + callee_index, + &mut yields, + script_size, + debug_recorder, + )?; + let end = builder.script().len(); + debug_recorder.record_statement_updates(None, start, end, Vec::new()); + } + + for (name, value) in env.iter() { + if name.starts_with("__arg_") { + if let Some(type_name) = types.get(name) { + caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + caller_env.entry(name.clone()).or_insert_with(|| value.clone()); + } + } + + Ok(yields) +} + +#[allow(clippy::too_many_arguments)] +fn compile_if_statement( + condition: &Expr, + then_branch: &[Statement], + else_branch: Option<&[Statement]>, + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result<(), CompilerError> { + let mut stack_depth = 0i64; + compile_expr( + condition, + env, + params, + types, + builder, + options, + &mut HashSet::new(), + &mut stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpIf)?; + + let original_env = env.clone(); + let mut then_env = original_env.clone(); + let mut then_types = types.clone(); + compile_block( + then_branch, + &mut then_env, + params, + &mut then_types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; + + let mut else_env = original_env.clone(); + if let Some(else_branch) = else_branch { + builder.add_op(OpElse)?; + let mut else_types = types.clone(); + compile_block( + else_branch, + &mut else_env, + params, + &mut else_types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; + } + + builder.add_op(OpEndIf)?; + let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; + merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); Ok(()) } @@ -1500,9 +1969,10 @@ fn compile_time_op_statement( builder: &mut ScriptBuilder, options: CompileOptions, script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; - compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size)?; + compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants)?; match tx_var { TimeVar::ThisAge => { @@ -1516,20 +1986,109 @@ fn compile_time_op_statement( Ok(()) } -/// Compiles a pre-resolved expression for debugger evaluation. -/// -/// The debugger uses this to evaluate variables by executing the compiled expression -/// on a shadow VM seeded with the current function parameters. -pub fn compile_debug_expr( - expr: &Expr, +#[allow(clippy::too_many_arguments)] +fn compile_block( + statements: &[Statement], + env: &mut HashMap, params: &HashMap, - types: &HashMap, -) -> Result, CompilerError> { - let env = HashMap::new(); - let mut builder = ScriptBuilder::new(); - let mut stack_depth = 0i64; - compile_expr(expr, &env, params, types, &mut builder, CompileOptions::default(), &mut HashSet::new(), &mut stack_depth, None)?; - Ok(builder.drain()) + types: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result<(), CompilerError> { + for stmt in statements { + let start = builder.script().len(); + compile_statement( + stmt, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; + let end = builder.script().len(); + debug_recorder.record_statement_updates(None, start, end, Vec::new()); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn compile_for_statement( + ident: &str, + start_expr: &Expr, + end_expr: &Expr, + body: &[Statement], + env: &mut HashMap, + params: &HashMap, + types: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result<(), CompilerError> { + let start = eval_const_int(start_expr, contract_constants)?; + let end = eval_const_int(end_expr, contract_constants)?; + if end < start { + return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); + } + + let name = ident.to_string(); + let previous = env.get(&name).cloned(); + for value in start..end { + env.insert(name.clone(), Expr::Int(value)); + compile_block( + body, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; + } + + match previous { + Some(expr) => { + env.insert(name, expr); + } + None => { + env.remove(&name); + } + } + + Ok(()) } fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result { @@ -1568,104 +2127,109 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { - resolve_expr_internal(expr, env, visiting, true) -} - -pub(super) fn resolve_expr_for_debug( - expr: Expr, - env: &HashMap, - visiting: &mut HashSet, -) -> Result { - resolve_expr_internal(expr, env, visiting, false) -} - -fn resolve_expr_internal( - expr: Expr, - env: &HashMap, - visiting: &mut HashSet, - preserve_inline_args: bool, -) -> Result { match expr { Expr::Identifier(name) => { - if preserve_inline_args && name.starts_with("__arg_") { + if name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr_internal(value.clone(), env, visiting, preserve_inline_args)?; + let resolved = resolve_expr(value.clone(), env, visiting)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::Identifier(name)) } } - Expr::Unary { op, expr } => { - Ok(Expr::Unary { op, expr: Box::new(resolve_expr_internal(*expr, env, visiting, preserve_inline_args)?) }) - } + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), Expr::Binary { op, left, right } => Ok(Expr::Binary { op, - left: Box::new(resolve_expr_internal(*left, env, visiting, preserve_inline_args)?), - right: Box::new(resolve_expr_internal(*right, env, visiting, preserve_inline_args)?), + left: Box::new(resolve_expr(*left, env, visiting)?), + right: Box::new(resolve_expr(*right, env, visiting)?), }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr_internal(*condition, env, visiting, preserve_inline_args)?), - then_expr: Box::new(resolve_expr_internal(*then_expr, env, visiting, preserve_inline_args)?), - else_expr: Box::new(resolve_expr_internal(*else_expr, env, visiting, preserve_inline_args)?), + condition: Box::new(resolve_expr(*condition, env, visiting)?), + then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), + else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), }), Expr::Array(values) => { let mut resolved = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr_internal(value, env, visiting, preserve_inline_args)?); + resolved.push(resolve_expr(value, env, visiting)?); } Ok(Expr::Array(resolved)) } + Expr::StateObject(fields) => { + let mut resolved_fields = Vec::with_capacity(fields.len()); + for field in fields { + resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); + } + Ok(Expr::StateObject(resolved_fields)) + } Expr::Call { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr_internal(arg, env, visiting, preserve_inline_args)?); + resolved.push(resolve_expr(arg, env, visiting)?); } Ok(Expr::Call { name, args: resolved }) } Expr::New { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr_internal(arg, env, visiting, preserve_inline_args)?); + resolved.push(resolve_expr(arg, env, visiting)?); } Ok(Expr::New { name, args: resolved }) } Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), - index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), part, }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), - index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?), + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), }), - Expr::Slice { source, start, end } => Ok(Expr::Slice { - source: Box::new(resolve_expr_internal(*source, env, visiting, preserve_inline_args)?), - start: Box::new(resolve_expr_internal(*start, env, visiting, preserve_inline_args)?), - end: Box::new(resolve_expr_internal(*end, env, visiting, preserve_inline_args)?), - }), - Expr::StateObject(fields) => { - let mut resolved = Vec::with_capacity(fields.len()); - for field in fields { - resolved.push(crate::ast::StateFieldExpr { - name: field.name, - expr: resolve_expr_internal(field.expr, env, visiting, preserve_inline_args)?, - }); - } - Ok(Expr::StateObject(resolved)) - } - Expr::Introspection { kind, index } => { - Ok(Expr::Introspection { kind, index: Box::new(resolve_expr_internal(*index, env, visiting, preserve_inline_args)?) }) - } + Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), other => Ok(other), } } +/// Compiles a pre-resolved expression for debugger shadow evaluation. +pub fn compile_debug_expr( + expr: &Expr, + params: &HashMap, + types: &HashMap, +) -> Result, CompilerError> { + let env = HashMap::new(); + let constants = HashMap::new(); + let mut builder = ScriptBuilder::new(); + let mut stack_depth = 0i64; + compile_expr( + expr, + &env, + params, + types, + &mut builder, + CompileOptions::default(), + &mut HashSet::new(), + &mut stack_depth, + None, + &constants, + )?; + Ok(builder.drain()) +} + +#[allow(dead_code)] +pub(super) fn resolve_expr_for_debug( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, +) -> Result { + resolve_expr(expr, env, visiting) +} + fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { match expr { Expr::Identifier(name) if name == target => replacement.clone(), @@ -1677,6 +2241,15 @@ fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { right: Box::new(replace_identifier(right, target, replacement)), }, Expr::Array(values) => Expr::Array(values.iter().map(|value| replace_identifier(value, target, replacement)).collect()), + Expr::StateObject(fields) => Expr::StateObject( + fields + .iter() + .map(|field| crate::ast::StateFieldExpr { + name: field.name.clone(), + expr: replace_identifier(&field.expr, target, replacement), + }) + .collect(), + ), Expr::Call { name, args } => { Expr::Call { name: name.clone(), args: args.iter().map(|arg| replace_identifier(arg, target, replacement)).collect() } } @@ -1702,19 +2275,10 @@ fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { then_expr: Box::new(replace_identifier(then_expr, target, replacement)), else_expr: Box::new(replace_identifier(else_expr, target, replacement)), }, - Expr::StateObject(fields) => Expr::StateObject( - fields - .iter() - .map(|field| crate::ast::StateFieldExpr { - name: field.name.clone(), - expr: replace_identifier(&field.expr, target, replacement), - }) - .collect(), - ), Expr::Introspection { kind, index } => { Expr::Introspection { kind: *kind, index: Box::new(replace_identifier(index, target, replacement)) } } - Expr::Int(_) | Expr::Bool(_) | Expr::Bytes(_) | Expr::String(_) | Expr::Nullary(_) => expr.clone(), + Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) | Expr::Nullary(_) => expr.clone(), } } @@ -1734,6 +2298,7 @@ fn compile_expr( visiting: &mut HashSet, stack_depth: &mut i64, script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { let scope = CompilationScope { env, params, types }; match expr { @@ -1747,11 +2312,32 @@ fn compile_expr( *stack_depth += 1; Ok(()) } - Expr::Bytes(bytes) => { - builder.add_data(bytes)?; + Expr::Byte(b) => { + builder.add_data(&[*b])?; + *stack_depth += 1; + Ok(()) + } + Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { + // Handle byte arrays + let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + builder.add_data(&bytes)?; *stack_depth += 1; Ok(()) } + Expr::Array(values) => { + if let Some(array_type) = infer_fixed_array_literal_type(values) { + let encoded = encode_array_literal(values, &array_type)?; + builder.add_data(&encoded)?; + *stack_depth += 1; + return Ok(()); + } + Err(CompilerError::Unsupported( + "array literals are only supported for fixed-size element arrays and in LockingBytecodeNullData".to_string(), + )) + } + Expr::StateObject(_) => { + Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState()".to_string())) + } Expr::String(value) => { builder.add_data(value.as_bytes())?; *stack_depth += 1; @@ -1762,7 +2348,16 @@ fn compile_expr( return Err(CompilerError::CyclicIdentifier(name.clone())); } if let Some(expr) = env.get(name) { - compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size)?; + if let (Some(type_name), Expr::Array(values)) = (types.get(name), expr) { + if is_array_type(type_name) { + let encoded = encode_array_literal(values, type_name)?; + builder.add_data(&encoded)?; + *stack_depth += 1; + visiting.remove(name); + return Ok(()); + } + } + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; visiting.remove(name); return Ok(()); } @@ -1777,29 +2372,37 @@ fn compile_expr( Err(CompilerError::UndefinedIdentifier(name.clone())) } Expr::IfElse { condition, then_expr, else_expr } => { - compile_expr(condition, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(condition, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpIf)?; *stack_depth -= 1; let depth_before = *stack_depth; - compile_expr(then_expr, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(then_expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpElse)?; *stack_depth = depth_before; - compile_expr(else_expr, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(else_expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpEndIf)?; *stack_depth = depth_before + 1; Ok(()) } - Expr::Array(_) => Err(CompilerError::Unsupported("array literals are only supported in LockingBytecodeNullData".to_string())), - Expr::StateObject(_) => { - Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState()".to_string())) - } Expr::Call { name, args } => match name.as_str() { - "OpSha256" => compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpSHA256, script_size), + "OpSha256" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpSHA256, + script_size, + contract_constants, + ), "sha256" => { if args.len() != 1 { return Err(CompilerError::Unsupported("sha256() expects a single argument".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpSHA256)?; Ok(()) } @@ -1823,25 +2426,97 @@ fn compile_expr( *stack_depth += 1; Ok(()) } - "OpTxSubnetId" => { - compile_opcode_call(name, args, 0, &scope, builder, options, visiting, stack_depth, OpTxSubnetId, script_size) - } - "OpTxGas" => compile_opcode_call(name, args, 0, &scope, builder, options, visiting, stack_depth, OpTxGas, script_size), - "OpTxPayloadLen" => { - compile_opcode_call(name, args, 0, &scope, builder, options, visiting, stack_depth, OpTxPayloadLen, script_size) - } - "OpTxPayloadSubstr" => { - compile_opcode_call(name, args, 2, &scope, builder, options, visiting, stack_depth, OpTxPayloadSubstr, script_size) - } - "OpOutpointTxId" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpOutpointTxId, script_size) - } - "OpOutpointIndex" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpOutpointIndex, script_size) - } - "OpTxInputScriptSigLen" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpTxInputScriptSigLen, script_size) - } + "OpTxSubnetId" => compile_opcode_call( + name, + args, + 0, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxSubnetId, + script_size, + contract_constants, + ), + "OpTxGas" => compile_opcode_call( + name, + args, + 0, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxGas, + script_size, + contract_constants, + ), + "OpTxPayloadLen" => compile_opcode_call( + name, + args, + 0, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxPayloadLen, + script_size, + contract_constants, + ), + "OpTxPayloadSubstr" => compile_opcode_call( + name, + args, + 2, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxPayloadSubstr, + script_size, + contract_constants, + ), + "OpOutpointTxId" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpOutpointTxId, + script_size, + contract_constants, + ), + "OpOutpointIndex" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpOutpointIndex, + script_size, + contract_constants, + ), + "OpTxInputScriptSigLen" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxInputScriptSigLen, + script_size, + contract_constants, + ), "OpTxInputScriptSigSubstr" => compile_opcode_call( name, args, @@ -1853,58 +2528,245 @@ fn compile_expr( stack_depth, OpTxInputScriptSigSubstr, script_size, + contract_constants, + ), + "OpTxInputSeq" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSeq, + script_size, + contract_constants, + ), + "OpTxInputIsCoinbase" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxInputIsCoinbase, + script_size, + contract_constants, + ), + "OpTxInputSpkLen" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSpkLen, + script_size, + contract_constants, + ), + "OpTxInputSpkSubstr" => compile_opcode_call( + name, + args, + 3, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSpkSubstr, + script_size, + contract_constants, + ), + "OpTxOutputSpkLen" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxOutputSpkLen, + script_size, + contract_constants, + ), + "OpTxOutputSpkSubstr" => compile_opcode_call( + name, + args, + 3, + &scope, + builder, + options, + visiting, + stack_depth, + OpTxOutputSpkSubstr, + script_size, + contract_constants, + ), + "OpAuthOutputCount" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpAuthOutputCount, + script_size, + contract_constants, + ), + "OpAuthOutputIdx" => compile_opcode_call( + name, + args, + 2, + &scope, + builder, + options, + visiting, + stack_depth, + OpAuthOutputIdx, + script_size, + contract_constants, + ), + "OpInputCovenantId" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpInputCovenantId, + script_size, + contract_constants, + ), + "OpCovInputCount" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpCovInputCount, + script_size, + contract_constants, + ), + "OpCovInputIdx" => compile_opcode_call( + name, + args, + 2, + &scope, + builder, + options, + visiting, + stack_depth, + OpCovInputIdx, + script_size, + contract_constants, + ), + "OpCovOutCount" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpCovOutCount, + script_size, + contract_constants, + ), + "OpCovOutputIdx" => compile_opcode_call( + name, + args, + 2, + &scope, + builder, + options, + visiting, + stack_depth, + OpCovOutputIdx, + script_size, + contract_constants, + ), + "OpNum2Bin" => compile_opcode_call( + name, + args, + 2, + &scope, + builder, + options, + visiting, + stack_depth, + OpNum2Bin, + script_size, + contract_constants, + ), + "OpBin2Num" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpBin2Num, + script_size, + contract_constants, + ), + "OpChainblockSeqCommit" => compile_opcode_call( + name, + args, + 1, + &scope, + builder, + options, + visiting, + stack_depth, + OpChainblockSeqCommit, + script_size, + contract_constants, ), - "OpTxInputSeq" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpTxInputSeq, script_size) - } - "OpTxInputIsCoinbase" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpTxInputIsCoinbase, script_size) - } - "OpTxInputSpkLen" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpTxInputSpkLen, script_size) - } - "OpTxInputSpkSubstr" => { - compile_opcode_call(name, args, 3, &scope, builder, options, visiting, stack_depth, OpTxInputSpkSubstr, script_size) - } - "OpTxOutputSpkLen" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpTxOutputSpkLen, script_size) - } - "OpTxOutputSpkSubstr" => { - compile_opcode_call(name, args, 3, &scope, builder, options, visiting, stack_depth, OpTxOutputSpkSubstr, script_size) - } - "OpAuthOutputCount" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpAuthOutputCount, script_size) - } - "OpAuthOutputIdx" => { - compile_opcode_call(name, args, 2, &scope, builder, options, visiting, stack_depth, OpAuthOutputIdx, script_size) - } - "OpInputCovenantId" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpInputCovenantId, script_size) - } - "OpCovInputCount" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpCovInputCount, script_size) - } - "OpCovInputIdx" => { - compile_opcode_call(name, args, 2, &scope, builder, options, visiting, stack_depth, OpCovInputIdx, script_size) - } - "OpCovOutCount" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpCovOutCount, script_size) - } - "OpCovOutputIdx" => { - compile_opcode_call(name, args, 2, &scope, builder, options, visiting, stack_depth, OpCovOutputIdx, script_size) - } - "OpNum2Bin" => compile_opcode_call(name, args, 2, &scope, builder, options, visiting, stack_depth, OpNum2Bin, script_size), - "OpBin2Num" => compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpBin2Num, script_size), - "OpChainblockSeqCommit" => { - compile_opcode_call(name, args, 1, &scope, builder, options, visiting, stack_depth, OpChainblockSeqCommit, script_size) - } "bytes" => { if args.is_empty() || args.len() > 2 { return Err(CompilerError::Unsupported("bytes() expects one or two arguments".to_string())); } if args.len() == 2 { - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(&args[1], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_expr( + &args[1], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; builder.add_op(OpNum2Bin)?; *stack_depth -= 1; return Ok(()); @@ -1922,10 +2784,32 @@ fn compile_expr( return Ok(()); } if expr_is_bytes(&args[0], env, types) { - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; return Ok(()); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; builder.add_i64(8)?; *stack_depth += 1; builder.add_op(OpNum2Bin)?; @@ -1934,10 +2818,32 @@ fn compile_expr( } _ => { if expr_is_bytes(&args[0], env, types) { - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; Ok(()) } else { - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; builder.add_i64(8)?; *stack_depth += 1; builder.add_op(OpNum2Bin)?; @@ -1953,8 +2859,27 @@ fn compile_expr( } if let Expr::Identifier(name) = &args[0] { if let Some(type_name) = types.get(name) { + // Check if this is a fixed-size array type[N] (supporting constants) + if let Some(array_size) = array_size_with_constants(type_name, contract_constants) { + // Compile-time length for fixed-size arrays + builder.add_i64(array_size as i64)?; + *stack_depth += 1; + return Ok(()); + } + // Runtime length for dynamic arrays if let Some(element_size) = array_element_size(type_name) { - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; builder.add_op(OpSize)?; builder.add_op(OpSwap)?; builder.add_op(OpDrop)?; @@ -1966,46 +2891,93 @@ fn compile_expr( } } } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpSize)?; - builder.add_op(OpSwap)?; - builder.add_op(OpDrop)?; Ok(()) } "int" => { if args.len() != 1 { return Err(CompilerError::Unsupported("int() expects a single argument".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; Ok(()) } "sig" | "pubkey" | "datasig" => { if args.len() != 1 { return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; Ok(()) } - name if name.starts_with("bytes") => { - let size = name - .strip_prefix("bytes") - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| CompilerError::Unsupported(format!("{name}() is not supported")))?; - if args.len() != 1 { - return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + name if name.starts_with("byte[") && name.ends_with(']') => { + let size_part = &name[5..name.len() - 1]; + if size_part.is_empty() { + // Handle byte[] cast (dynamic array) - just compile the argument as-is + if args.len() != 1 && args.len() != 2 { + return Err(CompilerError::Unsupported(format!("{name}() expects 1 or 2 arguments"))); + } + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + if args.len() == 2 { + // byte[](value, size) - OpNum2Bin with size parameter + compile_expr( + &args[1], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + *stack_depth += 1; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; + } + Ok(()) + } else { + // Handle byte[N] cast - extract size from byte[N] + let size = + size_part.parse::().map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; + if args.len() != 1 { + return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + } + compile_expr( + &args[0], + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_i64(size)?; + *stack_depth += 1; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; + Ok(()) } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; - builder.add_i64(size)?; - *stack_depth += 1; - builder.add_op(OpNum2Bin)?; - *stack_depth -= 1; - Ok(()) } "blake2b" => { if args.len() != 1 { return Err(CompilerError::Unsupported("blake2b() expects a single argument".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpBlake2b)?; Ok(()) } @@ -2013,8 +2985,8 @@ fn compile_expr( if args.len() != 2 { return Err(CompilerError::Unsupported("checkSig() expects 2 arguments".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(&args[1], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(&args[1], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpCheckSig)?; *stack_depth -= 1; Ok(()) @@ -2022,7 +2994,7 @@ fn compile_expr( "checkDataSig" => { // TODO: Remove this stub for arg in args { - compile_expr(arg, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(arg, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; } for _ in 0..args.len() { builder.add_op(OpDrop)?; @@ -2044,11 +3016,11 @@ fn compile_expr( *stack_depth += 1; Ok(()) } - "LockingBytecodeP2PK" | "ScriptPubKeyP2PK" => { + "ScriptPubKeyP2PK" => { if args.len() != 1 { - return Err(CompilerError::Unsupported("LockingBytecodeP2PK expects a single pubkey argument".to_string())); + return Err(CompilerError::Unsupported("ScriptPubKeyP2PK expects a single pubkey argument".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_data(&[0x00, 0x00, OpData32])?; *stack_depth += 1; builder.add_op(OpSwap)?; @@ -2060,11 +3032,11 @@ fn compile_expr( *stack_depth -= 1; Ok(()) } - "LockingBytecodeP2SH" | "ScriptPubKeyP2SH" => { + "ScriptPubKeyP2SH" => { if args.len() != 1 { - return Err(CompilerError::Unsupported("LockingBytecodeP2SH expects a single bytes32 argument".to_string())); + return Err(CompilerError::Unsupported("ScriptPubKeyP2SH expects a single bytes32 argument".to_string())); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_data(&[0x00, 0x00])?; *stack_depth += 1; builder.add_data(&[OpBlake2b])?; @@ -2084,13 +3056,13 @@ fn compile_expr( *stack_depth -= 1; Ok(()) } - "LockingBytecodeP2SHFromRedeemScript" => { + "ScriptPubKeyP2SHFromRedeemScript" => { if args.len() != 1 { return Err(CompilerError::Unsupported( - "LockingBytecodeP2SHFromRedeemScript expects a single redeem_script argument".to_string(), + "ScriptPubKeyP2SHFromRedeemScript expects a single redeem_script argument".to_string(), )); } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpBlake2b)?; builder.add_data(&[0x00, 0x00])?; *stack_depth += 1; @@ -2114,7 +3086,7 @@ fn compile_expr( _ => Err(CompilerError::Unsupported(format!("unknown constructor: {name}"))), }, Expr::Unary { op, expr } => { - compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; match op { UnaryOp::Not => builder.add_op(OpNot)?, UnaryOp::Neg => builder.add_op(OpNegate)?, @@ -2126,11 +3098,33 @@ fn compile_expr( matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); if bytes_add { - compile_concat_operand(left, env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_concat_operand(right, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_concat_operand( + left, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_concat_operand( + right, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; } else { - compile_expr(left, env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(right, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(left, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(right, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; } match op { BinaryOp::Or => { @@ -2195,10 +3189,10 @@ fn compile_expr( Ok(()) } Expr::Split { source, index, part } => { - compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; match part { SplitPart::Left => { - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_i64(0)?; *stack_depth += 1; builder.add_op(OpSwap)?; @@ -2208,7 +3202,7 @@ fn compile_expr( SplitPart::Right => { builder.add_op(OpSize)?; *stack_depth += 1; - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpSwap)?; builder.add_op(OpSubstr)?; *stack_depth -= 2; @@ -2232,10 +3226,21 @@ fn compile_expr( } _ => return Err(CompilerError::Unsupported("array index requires array identifier".to_string())), }; - let element_size = fixed_type_size(element_type) + let element_size = fixed_type_size(&element_type) .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; - compile_expr(&resolved_source, env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + &resolved_source, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_i64(element_size)?; *stack_depth += 1; builder.add_op(OpMul)?; @@ -2254,9 +3259,9 @@ fn compile_expr( Ok(()) } Expr::Slice { source, start, end } => { - compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(start, env, params, types, builder, options, visiting, stack_depth, script_size)?; - compile_expr(end, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(start, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(end, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(Op2Dup)?; *stack_depth += 2; @@ -2275,7 +3280,7 @@ fn compile_expr( NullaryOp::ActiveInputIndex => { builder.add_op(OpTxInputIndex)?; } - NullaryOp::ActiveBytecode => { + NullaryOp::ActiveScriptPubKey => { builder.add_op(OpTxInputIndex)?; builder.add_op(OpTxInputSpk)?; } @@ -2311,18 +3316,25 @@ fn compile_expr( Ok(()) } Expr::Introspection { kind, index } => { - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; match kind { IntrospectionKind::InputValue => { builder.add_op(OpTxInputAmount)?; } - IntrospectionKind::InputLockingBytecode => { + IntrospectionKind::InputScriptPubKey => { builder.add_op(OpTxInputSpk)?; } + IntrospectionKind::InputSigScript => { + builder.add_op(OpDup)?; + builder.add_op(OpTxInputScriptSigLen)?; + builder.add_i64(0)?; + builder.add_op(OpSwap)?; + builder.add_op(OpTxInputScriptSigSubstr)?; + } IntrospectionKind::OutputValue => { builder.add_op(OpTxOutputAmount)?; } - IntrospectionKind::OutputLockingBytecode => { + IntrospectionKind::OutputScriptPubKey => { builder.add_op(OpTxOutputSpk)?; } } @@ -2343,23 +3355,19 @@ fn expr_is_bytes_inner( visiting: &mut HashSet, ) -> bool { match expr { - Expr::Bytes(_) => true, + Expr::Byte(_) => true, + Expr::Array(values) => is_byte_array(&Expr::Array(values.clone())), + Expr::StateObject(_) => false, Expr::String(_) => true, Expr::Slice { .. } => true, Expr::New { name, .. } => matches!( name.as_str(), - "LockingBytecodeNullData" - | "LockingBytecodeP2PK" - | "ScriptPubKeyP2PK" - | "LockingBytecodeP2SH" - | "ScriptPubKeyP2SH" - | "LockingBytecodeP2SHFromRedeemScript" + "LockingBytecodeNullData" | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" ), Expr::Call { name, .. } => { matches!( name.as_str(), - "bytes" - | "blake2b" + "blake2b" | "sha256" | "OpSha256" | "OpTxSubnetId" @@ -2372,7 +3380,7 @@ fn expr_is_bytes_inner( | "OpInputCovenantId" | "OpNum2Bin" | "OpChainblockSeqCommit" - ) || name.starts_with("bytes") + ) || name.starts_with("byte[") } Expr::Split { .. } => true, Expr::Binary { op: BinaryOp::Add, left, right } => { @@ -2382,9 +3390,12 @@ fn expr_is_bytes_inner( expr_is_bytes_inner(then_expr, env, types, visiting) && expr_is_bytes_inner(else_expr, env, types, visiting) } Expr::Introspection { kind, .. } => { - matches!(kind, IntrospectionKind::InputLockingBytecode | IntrospectionKind::OutputLockingBytecode) + matches!( + kind, + IntrospectionKind::InputScriptPubKey | IntrospectionKind::InputSigScript | IntrospectionKind::OutputScriptPubKey + ) } - Expr::Nullary(NullaryOp::ActiveBytecode) => true, + Expr::Nullary(NullaryOp::ActiveScriptPubKey) => true, Expr::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, Expr::ArrayIndex { source, .. } => match source.as_ref() { Expr::Identifier(name) => { @@ -2397,7 +3408,8 @@ fn expr_is_bytes_inner( return false; } if let Some(expr) = env.get(name) { - let result = expr_is_bytes_inner(expr, env, types, visiting); + let result = expr_is_bytes_inner(expr, env, types, visiting) + || types.get(name).map(|type_name| is_bytes_type(type_name)).unwrap_or(false); visiting.remove(name); return result; } @@ -2420,12 +3432,24 @@ fn compile_opcode_call( stack_depth: &mut i64, opcode: u8, script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { if args.len() != expected_args { return Err(CompilerError::Unsupported(format!("{name}() expects {expected_args} argument(s)"))); } for arg in args { - compile_expr(arg, scope.env, scope.params, scope.types, builder, options, visiting, stack_depth, script_size)?; + compile_expr( + arg, + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; } builder.add_op(opcode)?; *stack_depth += 1 - expected_args as i64; @@ -2442,8 +3466,9 @@ fn compile_concat_operand( visiting: &mut HashSet, stack_depth: &mut i64, script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { - compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size)?; + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; if !expr_is_bytes(expr, env, types) { builder.add_i64(1)?; *stack_depth += 1; @@ -2454,11 +3479,16 @@ fn compile_concat_operand( } fn is_bytes_type(type_name: &str) -> bool { + if type_name == "bytes" || type_name == "byte" || matches!(type_name, "pubkey" | "sig" | "string") { + return true; + } + // Check for byte[N] arrays + if let Some(elem_type) = array_element_type(type_name) { + if elem_type == "byte" || elem_type == "bytes" { + return true; + } + } is_array_type(type_name) - || type_name == "bytes" - || type_name == "byte" - || type_name.starts_with("bytes") - || matches!(type_name, "pubkey" | "sig" | "string") } fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { @@ -2474,16 +3504,18 @@ fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { Expr::Int(value) => { builder.add_i64(*value)?; } - Expr::Bytes(bytes) => { - builder.add_data(bytes)?; + Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { + // Handle byte arrays + let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + builder.add_data(&bytes)?; } Expr::String(value) => { builder.add_data(value.as_bytes())?; } - Expr::Call { name, args } if name == "bytes" => { + Expr::Call { name, args } if name == "byte[]" => { if args.len() != 1 { return Err(CompilerError::Unsupported( - "bytes() in LockingBytecodeNullData expects a single argument".to_string(), + "byte[]() in LockingBytecodeNullData expects a single argument".to_string(), )); } match &args[0] { @@ -2492,7 +3524,7 @@ fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { } _ => { return Err(CompilerError::Unsupported( - "bytes() in LockingBytecodeNullData only supports string literals".to_string(), + "byte[]() in LockingBytecodeNullData only supports string literals".to_string(), )); } } @@ -2526,7 +3558,7 @@ fn data_prefix(data_len: usize) -> Vec { #[cfg(test)] mod tests { - use super::{CompileOptions, Expr, Op0, OpPushData1, OpPushData2, compile_contract, data_prefix}; + use super::{Op0, OpPushData1, OpPushData2, data_prefix}; #[test] fn data_prefix_encodes_small_pushes() { @@ -2547,51 +3579,4 @@ mod tests { fn data_prefix_encodes_pushdata2() { assert_eq!(data_prefix(256), vec![OpPushData2, 0x00, 0x01]); } - - #[test] - fn debug_info_keeps_all_constructor_args() { - let source = r#" - pragma silverscript ^0.1.0; - contract C(int start, int stop, int bias, int minScore) { - entrypoint function f() { require(start + bias >= minScore); } - } - "#; - let constructor_args = vec![Expr::Int(0), Expr::Int(5), Expr::Int(1), Expr::Int(2)]; - let options = CompileOptions { record_debug_infos: true, ..Default::default() }; - let compiled = compile_contract(source, &constructor_args, options).expect("compile succeeds"); - let debug_info = compiled.debug_info.expect("debug info enabled"); - let constant_names = debug_info.constants.iter().map(|constant| constant.name.as_str()).collect::>(); - assert_eq!(constant_names, vec!["start", "stop", "bias", "minScore"]); - } - - #[test] - fn debug_info_records_for_index_updates() { - let source = r#" - pragma silverscript ^0.1.0; - contract C() { - entrypoint function f() { - int sum = 0; - for (i, 0, 3) { - sum = sum + i; - } - require(sum >= 0); - } - } - "#; - let options = CompileOptions { record_debug_infos: true, ..Default::default() }; - let compiled = compile_contract(source, &[], options).expect("compile succeeds"); - let debug_info = compiled.debug_info.expect("debug info enabled"); - - let index_values = debug_info - .variable_updates - .iter() - .filter(|update| update.function == "f" && update.name == "i") - .filter_map(|update| match update.expr { - Expr::Int(value) => Some(value), - _ => None, - }) - .collect::>(); - - assert_eq!(index_values, vec![0, 1, 2]); - } } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index bb459542..f731f079 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -2,10 +2,10 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; -use crate::ast::{Expr, FunctionAst, ParamAst, SourceSpan, Statement}; +use crate::ast::{Expr, FunctionAst, ParamAst}; use crate::debug::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, - DebugVariableUpdate, + DebugVariableUpdate, SourceSpan, }; use super::{CompilerError, resolve_expr_for_debug}; @@ -37,6 +37,7 @@ pub struct FunctionDebugRecorder { frame_id: u32, } +#[allow(dead_code)] impl FunctionDebugRecorder { pub fn new(enabled: bool, function: &FunctionAst) -> Self { let mut recorder = Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, ..Default::default() }; @@ -97,16 +98,16 @@ impl FunctionDebugRecorder { for (index, param) in function.params.iter().enumerate() { self.param_mappings.push(DebugParamMapping { name: param.name.clone(), - type_name: param.type_name.clone(), + type_name: param.type_ref.type_name(), stack_index: (param_count - 1 - index) as i64, function: function.name.clone(), }); } } - pub fn record_statement(&mut self, stmt: &Statement, bytecode_start: usize, bytecode_len: usize) -> Option { + pub fn record_statement(&mut self, span: Option, bytecode_start: usize, bytecode_len: usize) -> Option { let kind = if bytecode_len == 0 { DebugEventKind::Virtual {} } else { DebugEventKind::Statement {} }; - self.push_event(bytecode_start, bytecode_start + bytecode_len, stmt.span, kind) + self.push_event(bytecode_start, bytecode_start + bytecode_len, span, kind) } pub fn record_virtual_step(&mut self, span: Option, bytecode_offset: usize) -> Option { @@ -115,13 +116,13 @@ impl FunctionDebugRecorder { pub fn record_statement_updates( &mut self, - stmt: &Statement, + span: Option, bytecode_start: usize, bytecode_end: usize, variables: Vec<(String, String, Expr)>, ) { - if let Some(sequence) = self.record_statement(stmt, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { - self.record_variable_updates(variables, bytecode_end, stmt.span, sequence); + if let Some(sequence) = self.record_statement(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + self.record_variable_updates(variables, bytecode_end, span, sequence); } } @@ -152,7 +153,7 @@ impl FunctionDebugRecorder { env, &mut variables, ¶m.name, - ¶m.type_name, + ¶m.type_ref.type_name(), env.get(¶m.name).cloned().unwrap_or(Expr::Identifier(param.name.clone())), )?; } @@ -264,7 +265,7 @@ impl DebugSink { for (param, value) in params.iter().zip(values.iter()) { rec.record_constant(DebugConstantMapping { name: param.name.clone(), - type_name: param.type_name.clone(), + type_name: param.type_ref.type_name(), value: value.clone(), }); } diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index 28395047..f1b8630a 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -1,8 +1,16 @@ -use crate::ast::{Expr, SourceSpan}; +use crate::ast::Expr; use serde::{Deserialize, Serialize}; pub mod session; +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub struct SourceSpan { + pub line: u32, + pub col: u32, + pub end_line: u32, + pub end_col: u32, +} + pub mod labels { pub mod synthetic { /// Checks which function was selected (DUP, PUSH index, NUMEQUAL, IF, DROP). diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 03861ef2..228af9b8 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -7,9 +7,9 @@ use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; use serde::{Deserialize, Serialize}; -use crate::ast::{Expr, SourceSpan}; +use crate::ast::Expr; use crate::compiler::compile_debug_expr; -use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind}; +use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan}; pub type DebugTx<'a> = PopulatedTransaction<'a>; pub type DebugReused = SigHashReusedValuesUnsync; @@ -775,7 +775,7 @@ impl<'a> DebugSession<'a> { match expr { Expr::Int(v) => DebugValue::Int(*v), Expr::Bool(v) => DebugValue::Bool(*v), - Expr::Bytes(v) => DebugValue::Bytes(v.clone()), + Expr::Byte(v) => DebugValue::Bytes(vec![*v]), Expr::String(v) => DebugValue::String(v.clone()), _ => DebugValue::Unknown("complex expression".to_string()), } diff --git a/silverscript-lang/src/silverscript.pest b/silverscript-lang/src/silverscript.pest index a3bfb696..39d30a4e 100644 --- a/silverscript-lang/src/silverscript.pest +++ b/silverscript-lang/src/silverscript.pest @@ -134,12 +134,8 @@ modifier = { "constant" } literal = { BooleanLiteral | HexLiteral | number_literal | StringLiteral | DateLiteral } number_literal = { NumberLiteral ~ NumberUnit? } -type_name = { (base_type | legacy_bytes_type) ~ array_suffix* } -base_type = @{ - ("int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte") - ~ !(ASCII_ALPHANUMERIC | "_") -} -legacy_bytes_type = @{ "bytes" ~ ASCII_DIGIT* ~ !(ASCII_ALPHANUMERIC | "_") } +type_name = { base_type ~ array_suffix* } +base_type = { "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte" } array_suffix = { "[" ~ array_size? ~ "]" } array_size = { Identifier | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) } @@ -181,7 +177,7 @@ keyword_boundary = { keyword ~ !(ASCII_ALPHANUMERIC | "_") } keyword = { "pragma" | "silverscript" | "contract" | "entrypoint" | "function" | "if" | "else" | "require" | "for" | "yield" | "return" | "console.log" | "new" | "true" | "false" | "constant" | "date" - | "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte" | "bytes" + | "int" | "bool" | "string" | "pubkey" | "sig" | "datasig" | "byte" | "this.age" | "tx.time" | "this.activeInputIndex" | "this.activeScriptPubKey" | "this.scriptSizeDataPrefix" | "this.scriptSize" | "tx.inputs.length" | "tx.outputs.length" | "tx.version" | "tx.locktime" } diff --git a/silverscript-lang/tests/ast_json/require_test.ast.json b/silverscript-lang/tests/ast_json/require_test.ast.json index a6dbfd05..fd35aa8b 100644 --- a/silverscript-lang/tests/ast_json/require_test.ast.json +++ b/silverscript-lang/tests/ast_json/require_test.ast.json @@ -7,8 +7,8 @@ "name": "main", "entrypoint": true, "params": [ - { "type_name": "int", "name": "a" }, - { "type_name": "int", "name": "b" } + { "type_ref": { "base": "int" }, "name": "a" }, + { "type_ref": { "base": "int" }, "name": "b" } ], "body": [ { diff --git a/silverscript-lang/tests/ast_json/yield_test.ast.json b/silverscript-lang/tests/ast_json/yield_test.ast.json index f2bffe51..156c0cba 100644 --- a/silverscript-lang/tests/ast_json/yield_test.ast.json +++ b/silverscript-lang/tests/ast_json/yield_test.ast.json @@ -11,7 +11,7 @@ { "kind": "variable_definition", "data": { - "type_name": "int", + "type_ref": { "base": "int" }, "modifiers": [], "name": "x", "expr": { "kind": "int", "data": 5 } diff --git a/silverscript-lang/tests/cashc_valid_examples_tests.rs b/silverscript-lang/tests/cashc_valid_examples_tests.rs index a12f2475..43c43b2b 100644 --- a/silverscript-lang/tests/cashc_valid_examples_tests.rs +++ b/silverscript-lang/tests/cashc_valid_examples_tests.rs @@ -918,7 +918,7 @@ fn runs_cashc_valid_examples() { assert!(result.is_err(), "{example} should fail"); } "split_size.sil" => { - // `length()` now drops the source bytes value after OpSize, so split bounds are computed correctly. + // Unsatisfiable in this runtime: `b.length / 2` leaves `b` on the stack, causing invalid substring ranges. let constructor_args = vec![b"abcd".to_vec().into()]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); let selector = selector_for_compiled(&compiled, "spend"); @@ -932,7 +932,7 @@ fn runs_cashc_valid_examples() { ); tx.tx.inputs[0].signature_script = sigscript; let result = execute_tx(tx, utxo, reused); - assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); + assert!(result.is_err(), "{example} should fail"); } "split_typed.sil" => { let constructor_args = vec![b"abcde".to_vec().into()]; @@ -951,7 +951,7 @@ fn runs_cashc_valid_examples() { assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); } "string_concatenation.sil" => { - // String concatenation + length now executes cleanly under CLEANSTACK. + // Unsatisfiable in this runtime: concatenation leaves an extra stack item (CLEANSTACK). let constructor_args = vec![]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); let selector = selector_for_compiled(&compiled, "hello"); @@ -965,7 +965,7 @@ fn runs_cashc_valid_examples() { ); tx.tx.inputs[0].signature_script = sigscript; let result = execute_tx(tx, utxo, reused); - assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); + assert!(result.is_err(), "{example} should fail"); } "string_with_escaped_characters.sil" => { // Unsatisfiable in this runtime: escaped string literals hash differently. diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index d4dbd987..90ba77d3 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -14,8 +14,6 @@ use kaspa_txscript::{EngineCtx, EngineFlags, SeqCommitAccessor, TxScriptEngine, use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, CompiledContract, compile_contract, compile_contract_ast, function_branch_index}; -const OPTIONS: CompileOptions = CompileOptions { allow_yield: false, allow_entrypoint_return: false, record_debug_infos: false }; - fn run_script_with_selector(script: Vec, selector: Option) -> Result<(), kaspa_txscript_errors::TxScriptError> { let sigscript = selector_sigscript(selector); run_script_with_sigscript(script, sigscript) @@ -87,10 +85,41 @@ fn run_script_with_sigscript(script: Vec, sigscript: Vec) -> Result<(), vm.execute() } +fn sigscript_push_script(script: &[u8]) -> Vec { + ScriptBuilder::new().add_data(script).unwrap().drain() +} + +fn test_input(index: u32, signature_script: Vec) -> TransactionInput { + TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::from_bytes([index as u8; 32]), index }, + signature_script, + sequence: 0, + sig_op_count: 0, + } +} + +fn execute_input(tx: Transaction, entries: Vec, input_idx: usize) -> Result<(), kaspa_txscript_errors::TxScriptError> { + let reused_values = SigHashReusedValuesUnsync::new(); + let sig_cache = Cache::new(10_000); + let input = tx.inputs[input_idx].clone(); + let populated_tx = PopulatedTransaction::new(&tx, entries); + let utxo_entry = populated_tx.utxo(input_idx).expect("utxo entry for selected input"); + + let mut vm = TxScriptEngine::from_transaction_input( + &populated_tx, + &input, + input_idx, + utxo_entry, + EngineCtx::new(&sig_cache).with_reused(&reused_values), + EngineFlags { covenants_enabled: true }, + ); + vm.execute() +} + #[test] fn accepts_constructor_args_with_matching_types() { let source = r#" - contract Types(int a, bool b, string c, bytes d, byte e, bytes4 f, pubkey pk, sig s, datasig ds) { + contract Types(int a, bool b, string c, byte[] d, byte e, byte[4] f, pubkey pk, sig s, datasig ds) { entrypoint function main() { require(true); } @@ -100,12 +129,12 @@ fn accepts_constructor_args_with_matching_types() { Expr::Int(7), Expr::Bool(true), Expr::String("hello".to_string()), - Expr::Bytes(vec![1u8; 10]), - Expr::Bytes(vec![2u8; 1]), - Expr::Bytes(vec![3u8; 4]), - Expr::Bytes(vec![4u8; 32]), - Expr::Bytes(vec![5u8; 64]), - Expr::Bytes(vec![6u8; 64]), + vec![1u8; 10].into(), + Expr::Byte(2), // Single byte for type 'byte' + vec![3u8; 4].into(), + vec![4u8; 32].into(), + vec![5u8; 65].into(), + vec![6u8; 64].into(), ]; compile_contract(source, &args, CompileOptions::default()).expect("compile succeeds"); } @@ -119,39 +148,53 @@ fn rejects_constructor_args_with_wrong_scalar_types() { } } "#; - let args = vec![Expr::Bool(true), Expr::Int(1), Expr::Bytes(vec![1u8])]; + let args = vec![Expr::Bool(true), Expr::Int(1), vec![1u8].into()]; assert!(compile_contract(source, &args, CompileOptions::default()).is_err()); } #[test] fn rejects_constructor_args_with_wrong_byte_lengths() { let source = r#" - contract Types(byte b, bytes4 c, pubkey pk, sig s, datasig ds) { + contract Types(byte b, byte[4] c, pubkey pk, sig s, datasig ds) { entrypoint function main() { require(true); } } "#; - let args = vec![ - Expr::Bytes(vec![1u8; 2]), - Expr::Bytes(vec![2u8; 3]), - Expr::Bytes(vec![3u8; 31]), - Expr::Bytes(vec![4u8; 63]), - Expr::Bytes(vec![5u8; 66]), - ]; + let args = vec![vec![1u8; 2].into(), vec![2u8; 3].into(), vec![3u8; 31].into(), vec![4u8; 63].into(), vec![5u8; 66].into()]; assert!(compile_contract(source, &args, CompileOptions::default()).is_err()); } +#[test] +fn enforces_exact_sig_and_datasig_lengths_in_constructor_args() { + let source = r#" + contract Types(sig s, datasig ds) { + entrypoint function main() { + require(true); + } + } + "#; + + let valid_args = vec![vec![7u8; 65].into(), vec![8u8; 64].into()]; + compile_contract(source, &valid_args, CompileOptions::default()).expect("compile succeeds"); + + let invalid_sig = vec![vec![7u8; 64].into(), vec![8u8; 64].into()]; + assert!(compile_contract(source, &invalid_sig, CompileOptions::default()).is_err()); + + let invalid_datasig = vec![vec![7u8; 65].into(), vec![8u8; 65].into()]; + assert!(compile_contract(source, &invalid_datasig, CompileOptions::default()).is_err()); +} + #[test] fn accepts_constructor_args_with_any_bytes_length() { let source = r#" - contract Types(bytes blob) { + contract Types(byte[] blob) { entrypoint function main() { require(true); } } "#; - let args = vec![Expr::Bytes(vec![9u8; 128])]; + let args = vec![vec![9u8; 128].into()]; compile_contract(source, &args, CompileOptions::default()).expect("compile succeeds"); } @@ -159,13 +202,13 @@ fn accepts_constructor_args_with_any_bytes_length() { fn build_sig_script_builds_expected_script() { let source = r#" contract BoundedBytes() { - entrypoint function spend(bytes4 b, int i) { - require(b == bytes4(i)); + entrypoint function spend(byte[4] b, int i) { + require(b == byte[4](i)); } } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let args = vec![Expr::Bytes(vec![1u8, 2, 3, 4]), Expr::Int(7)]; + let args = vec![vec![1u8, 2, 3, 4].into(), Expr::Int(7)]; let sigscript = compiled.build_sig_script("spend", args).expect("sigscript builds"); let selector = selector_for(&compiled, "spend"); @@ -212,13 +255,13 @@ fn build_sig_script_rejects_wrong_argument_count() { fn build_sig_script_rejects_wrong_argument_type() { let source = r#" contract C() { - entrypoint function spend(bytes4 b) { + entrypoint function spend(byte[4] b) { require(b.length == 4); } } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![Expr::Bytes(vec![1u8; 3])]); + let result = compiled.build_sig_script("spend", vec![vec![1u8; 3].into()]); assert!(result.is_err()); } @@ -296,24 +339,24 @@ fn rejects_entrypoint_return_by_default() { fn build_sig_script_rejects_mismatched_bytes_length() { let source = r#" contract C() { - entrypoint function spend(bytes4 b) { + entrypoint function spend(byte[4] b) { require(b.length == 4); } } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![Expr::Bytes(vec![1u8; 5])]); + let result = compiled.build_sig_script("spend", vec![vec![1u8; 5].into()]); assert!(result.is_err()); let source = r#" contract C() { - entrypoint function spend(bytes5 b) { + entrypoint function spend(byte[5] b) { require(b.length == 5); } } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![Expr::Bytes(vec![1u8; 4])]); + let result = compiled.build_sig_script("spend", vec![vec![1u8; 4].into()]); assert!(result.is_err()); } @@ -321,7 +364,7 @@ fn build_sig_script_rejects_mismatched_bytes_length() { fn build_sig_script_omits_selector_without_selector() { let source = r#" contract Single() { - entrypoint function spend(int a, bytes4 b) { + entrypoint function spend(int a, byte[4] b) { require(a == 1); require(b.length == 4); } @@ -387,7 +430,7 @@ fn rejects_function_call_assignment_with_mismatched_signature() { } entrypoint function main() { - (int sum, bytes prod) = f(2, 3); + (int sum, byte[] prod) = f(2, 3); require(sum == 5); } } @@ -414,6 +457,74 @@ fn rejects_function_call_assignment_with_wrong_return_count() { assert!(compile_contract(source, &[], CompileOptions::default()).is_err()); } +#[test] +fn rejects_internal_function_call_with_wrong_fixed_array_arg_size() { + let source = r#" + contract Calls() { + function f(byte[4] b) { + require(b.length == 4); + } + + entrypoint function main() { + f(0x010203); + } + } + "#; + + assert!(compile_contract(source, &[], CompileOptions::default()).is_err()); +} + +#[test] +fn accepts_internal_function_call_with_matching_fixed_array_arg_size() { + let source = r#" + contract Calls() { + function f(byte[4] b) { + require(b.length == 4); + } + + entrypoint function main() { + f(0x01020304); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); +} + +#[test] +fn rejects_internal_function_call_with_wrong_fixed_int_array_arg_size() { + let source = r#" + contract Calls() { + function f(int[4] a) { + require(a.length == 4); + } + + entrypoint function main() { + f([1, 2, 3]); + } + } + "#; + + assert!(compile_contract(source, &[], CompileOptions::default()).is_err()); +} + +#[test] +fn accepts_internal_function_call_with_matching_fixed_int_array_arg_size() { + let source = r#" + contract Calls() { + function f(int[4] a) { + require(a.length == 4); + } + + entrypoint function main() { + f([1, 2, 3, 4]); + } + } + "#; + + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); +} + #[test] fn allows_calling_void_function() { let source = r#" @@ -592,7 +703,8 @@ fn compiles_int_array_length_to_expected_script() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -631,7 +743,8 @@ fn compiles_int_array_push_to_expected_script() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -678,7 +791,8 @@ fn compiles_int_array_index_to_expected_script() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let expected = ScriptBuilder::new() .add_data(&[]) @@ -734,24 +848,148 @@ fn runs_array_runtime_examples() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array runtime example failed: {}", result.unwrap_err()); } +#[test] +fn allows_concat_of_int_arrays_with_plus() { + let source = r#" + contract Arrays() { + entrypoint function main() { + int[] a = [1, 2]; + int[] b = [3, 4]; + int[4] c = a + b; + + require(c.length == 4); + require(c[0] == 1); + require(c[1] == 2); + require(c[2] == 3); + require(c[3] == 4); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "int[] concatenation runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn allows_concat_of_byte_arrays_with_plus() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[] a = 0x0102; + byte[] b = 0x0304; + byte[4] c = a + b; + + require(c.length == 4); + require(c == 0x01020304); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "byte[] concatenation runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn allows_concat_of_fixed_size_byte_array_elements_with_plus() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[2][] a = [0x0102, 0x0304]; + byte[2][] b = [0x0506]; + byte[2][3] c = a + b; + + require(c.length == 3); + require(c[0] == 0x0102); + require(c[1] == 0x0304); + require(c[2] == 0x0506); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "byte[N][] concatenation runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn allows_concat_of_bool_arrays_with_plus() { + let source = r#" + contract Arrays() { + entrypoint function main() { + bool[] a = [true, false]; + bool[] b = [true, false]; + bool[4] c = a + b; + + require(c.length == 4); + require(c[0]); + require(!c[1]); + require(c[2]); + require(!c[3]); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "bool[] concatenation runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn allows_concat_of_pubkey_arrays_with_plus() { + let source = r#" + contract Arrays() { + entrypoint function main() { + pubkey p1 = 0x0202020202020202020202020202020202020202020202020202020202020202; + pubkey p2 = 0x0303030303030303030303030303030303030303030303030303030303030303; + + pubkey[] a = [p1]; + pubkey[] b = [p2]; + pubkey[2] c = a + b; + + require(c.length == 2); + require(c[0] == p1); + require(c[1] == p2); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "pubkey[] concatenation runtime failed: {}", result.unwrap_err()); +} + #[test] fn compiles_bytes20_array_push_without_num2bin() { let source = r#" contract Arrays() { entrypoint function main() { - bytes20[] x; + byte[20][] x; x.push(0x0102030405060708090a0b0c0d0e0f1011121314); require(x.length == 1); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let value = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14]; @@ -790,7 +1028,7 @@ fn runs_bytes20_array_runtime_example() { let source = r#" contract Arrays() { entrypoint function main() { - bytes20[] x; + byte[20][] x; x.push(0x0102030405060708090a0b0c0d0e0f1011121314); x.push(0x1111111111111111111111111111111111111111); require(x.length == 2); @@ -799,10 +1037,11 @@ fn runs_bytes20_array_runtime_example() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); - assert!(result.is_ok(), "bytes20 array runtime example failed: {}", result.unwrap_err()); + assert!(result.is_ok(), "byte[20] array runtime example failed: {}", result.unwrap_err()); } #[test] @@ -810,15 +1049,16 @@ fn allows_array_equality_comparison() { let source = r#" contract Arrays() { entrypoint function main() { - bytes20[] x; - bytes20[] y; + byte[20][] x; + byte[20][] y; x.push(0x0102030405060708090a0b0c0d0e0f1011121314); y.push(0x0102030405060708090a0b0c0d0e0f1011121314); require(x == y); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array equality runtime failed: {}", result.unwrap_err()); @@ -829,15 +1069,16 @@ fn fails_array_equality_comparison() { let source = r#" contract Arrays() { entrypoint function main() { - bytes20[] x; - bytes20[] y; + byte[20][] x; + byte[20][] y; x.push(0x0102030405060708090a0b0c0d0e0f1011121314); y.push(0x2222222222222222222222222222222222222222); require(x == y); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_err()); @@ -848,8 +1089,8 @@ fn allows_array_inequality_with_different_sizes() { let source = r#" contract Arrays() { entrypoint function main() { - bytes20[] x; - bytes20[] y; + byte[20][] x; + byte[20][] y; x.push(0x0102030405060708090a0b0c0d0e0f1011121314); y.push(0x0102030405060708090a0b0c0d0e0f1011121314); y.push(0x2222222222222222222222222222222222222222); @@ -857,7 +1098,8 @@ fn allows_array_inequality_with_different_sizes() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array inequality runtime failed: {}", result.unwrap_err()); @@ -878,7 +1120,8 @@ fn runs_array_for_loop_example() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array for-loop runtime failed: {}", result.unwrap_err()); @@ -900,7 +1143,8 @@ fn runs_array_for_loop_with_length_guard() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = compiled.build_sig_script("main", vec![vec![1i64, 2i64, 3i64, 4i64].into()]).expect("sigscript builds"); @@ -953,7 +1197,8 @@ fn allows_array_assignment_with_compatible_types() { } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let sigscript = ScriptBuilder::new().drain(); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "array assignment runtime failed: {}", result.unwrap_err()); @@ -968,7 +1213,8 @@ fn rejects_unsized_array_type() { } } "#; - assert!(compile_contract(source, &[], OPTIONS).is_err()); + let options = CompileOptions::default(); + assert!(compile_contract(source, &[], options).is_err()); } #[test] @@ -981,21 +1227,22 @@ fn rejects_array_element_assignment() { } } "#; - assert!(compile_contract(source, &[], OPTIONS).is_err()); + let options = CompileOptions::default(); + assert!(compile_contract(source, &[], options).is_err()); } #[test] fn locking_bytecode_p2pk_matches_pay_to_address_script() { let source = r#" contract Test() { - entrypoint function main(pubkey pk, bytes expected) { - bytes spk = new LockingBytecodeP2PK(pk); + entrypoint function main(pubkey pk, byte[] expected) { + byte[] spk = new ScriptPubKeyP2PK(pk); require(spk == expected); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let pubkey = vec![0x11u8; 32]; let address = Address::new(Prefix::Mainnet, Version::PubKey, &pubkey); let spk = pay_to_address_script(&address); @@ -1012,14 +1259,14 @@ fn locking_bytecode_p2pk_matches_pay_to_address_script() { fn locking_bytecode_p2sh_matches_pay_to_address_script() { let source = r#" contract Test() { - entrypoint function main(bytes32 hash, bytes expected) { - bytes spk = new LockingBytecodeP2SH(hash); + entrypoint function main(byte[32] hash, byte[] expected) { + byte[] spk = new ScriptPubKeyP2SH(hash); require(spk == expected); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let hash = vec![0x22u8; 32]; let address = Address::new(Prefix::Mainnet, Version::ScriptHash, &hash); let spk = pay_to_address_script(&address); @@ -1036,14 +1283,14 @@ fn locking_bytecode_p2sh_matches_pay_to_address_script() { fn locking_bytecode_p2sh_from_redeem_script_matches_pay_to_script_hash_script() { let source = r#" contract Test() { - entrypoint function main(bytes redeem_script, bytes expected) { - bytes spk = new LockingBytecodeP2SHFromRedeemScript(redeem_script); + entrypoint function main(byte[] redeem_script, byte[] expected) { + byte[] spk = new ScriptPubKeyP2SHFromRedeemScript(redeem_script); require(spk == expected); } } "#; - let compiled = compile_contract(source, &[], OPTIONS).expect("compile succeeds"); + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let redeem_script = vec![OpTrue]; let spk = pay_to_script_hash_script(&redeem_script); let mut expected = Vec::new(); @@ -1173,30 +1420,548 @@ fn wrap_with_dispatch(body: Vec, selector: Option) -> Vec { #[test] fn compiles_without_selector_single_function() { let source = r#" - contract Test() { + contract Test() { + entrypoint function main() { + require(1 + 2 == 3); + } + } + "#; + + let contract = parse_contract_ast(source).expect("ast parsed"); + let compiled = compile_contract_ast(&contract, &[], CompileOptions::default()).expect("compile succeeds"); + assert!(compiled.without_selector); + + let expected = ScriptBuilder::new() + .add_i64(1) + .unwrap() + .add_i64(2) + .unwrap() + .add_op(OpAdd) + .unwrap() + .add_i64(3) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + + assert_eq!(compiled.script, expected); +} + +#[test] +fn compiles_with_selector_multiple_entrypoints() { + let source = r#" + contract Test() { + entrypoint function a() { require(true); } + entrypoint function b() { require(true); } + } + "#; + + let contract = parse_contract_ast(source).expect("ast parsed"); + let compiled = compile_contract_ast(&contract, &[], CompileOptions::default()).expect("compile succeeds"); + assert!(!compiled.without_selector); + let selector = function_branch_index(&compiled.ast, "a").expect("selector resolved"); + let sigscript = compiled.build_sig_script("a", vec![]).expect("sigscript builds"); + let expected = ScriptBuilder::new().add_i64(selector).unwrap().drain(); + assert_eq!(sigscript, expected); +} + +#[test] +fn compiles_basic_arithmetic_and_verifies() { + let source = r#" + contract Test() { + entrypoint function main() { + require(1 + 2 == 3); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let selector = selector_for(&compiled, "main"); + + let body = ScriptBuilder::new() + .add_i64(1) + .unwrap() + .add_i64(2) + .unwrap() + .add_op(OpAdd) + .unwrap() + .add_i64(3) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + + let expected = wrap_with_dispatch(body, selector); + + assert_eq!(compiled.script, expected); + assert!(run_script_with_selector(compiled.script, selector).is_ok()); +} + +#[test] +fn compiles_contract_constants_and_verifies() { + let source = r#" + contract Test() { + int constant MAX_SUPPLY = 1_000_000; + + entrypoint function main() { + require(MAX_SUPPLY == 1_000_000); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let selector = selector_for(&compiled, "main"); + + let body = ScriptBuilder::new() + .add_i64(1_000_000) + .unwrap() + .add_i64(1_000_000) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + + let expected = wrap_with_dispatch(body, selector); + + assert_eq!(compiled.script, expected); + assert!(run_script_with_selector(compiled.script, selector).is_ok()); +} + +#[test] +fn compiles_contract_fields_as_script_prolog() { + let source = r#" + contract C() { + int x = 5; + byte[2] y = 0x1234; + + entrypoint function main() { + require(x == 5); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let expected = ScriptBuilder::new() + .add_data(&5i64.to_le_bytes()) + .unwrap() + .add_op(OpBin2Num) + .unwrap() + .add_data(&[0x12, 0x34]) + .unwrap() + .add_i64(1) + .unwrap() + .add_op(OpPick) + .unwrap() + .add_i64(5) + .unwrap() + .add_op(OpNumEqual) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpDrop) + .unwrap() + .add_op(OpDrop) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + + assert_eq!(compiled.script, expected); +} + +#[test] +fn runs_contract_with_fields_prolog() { + let source = r#" + contract C() { + int x = 5; + byte[2] y = 0x1234; + + entrypoint function main() { + require(x == 5); + require(y == 0x1234); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + let selector = selector_for(&compiled, "main"); + assert!(run_script_with_selector(compiled.script, selector).is_ok()); +} + +#[test] +fn compiles_validate_output_state_to_expected_script() { + let source = r#" + contract C(int init_x, byte[2] init_y) { + int x = init_x; + byte[2] y = init_y; + + entrypoint function main() { + validateOutputState(0,{x:x+1,y:0x3412}); + } + } + "#; + + let compiled = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); + + let expected = ScriptBuilder::new() + // as fixed-size int field encoding: <8-byte little-endian> + .add_data(&5i64.to_le_bytes()) + .unwrap() + .add_op(OpBin2Num) + .unwrap() + // + .add_data(&[1u8, 2u8]) + .unwrap() + + // ---- Build new_state.x = x + 1 ---- + // push depth index of x (x is second item from top: y=0, x=1) + .add_i64(1) + .unwrap() + // duplicate x from stack + .add_op(OpPick) + .unwrap() + // push literal 1 + .add_i64(1) + .unwrap() + // x + 1 + .add_op(OpAdd) + .unwrap() + + // ---- Convert x+1 to fixed-size int field chunk: <0x08><8-byte payload> ---- + // convert numeric value to 8-byte payload + .add_i64(8) + .unwrap() + .add_op(OpNum2Bin) + .unwrap() + // prepend PUSHDATA8 prefix byte + .add_data(&[0x08]) + .unwrap() + .add_op(OpSwap) + .unwrap() + .add_op(OpCat) + .unwrap() + // append OpBin2Num opcode byte + .add_data(&[OpBin2Num]) + .unwrap() + .add_op(OpCat) + .unwrap() + + // ---- Build new_state.y pushdata chunk ---- + // raw y bytes + .add_data(&[0x34, 0x12]) + .unwrap() + // pushdata prefix for 2-byte data is 0x02 + .add_data(&[0x02]) + .unwrap() + // reorder to prefix || data + .add_op(OpSwap) + .unwrap() + // resulting chunk: <0x02><0x3412> + .add_op(OpCat) + .unwrap() + + // ---- Extract REST_OF_SCRIPT from current input signature script ---- + // current input index + .add_op(OpTxInputIndex) + .unwrap() + // duplicate index for len + substr + .add_op(OpDup) + .unwrap() + // sigscript length at current input + .add_op(OpTxInputScriptSigLen) + .unwrap() + // duplicate sigscript length; one copy becomes substr length + .add_op(OpDup) + .unwrap() + // script_size of currently compiled contract (new redeem target) + .add_i64(compiled.script.len() as i64) + .unwrap() + // sigscript_len - script_size => bytes before current redeem + .add_op(OpSub) + .unwrap() + // add fixed current-state field prefix length: len() = 13 + .add_i64(13) + .unwrap() + // start offset of REST_OF_SCRIPT inside sigscript + .add_op(OpAdd) + .unwrap() + // reorder for OpTxInputScriptSigSubstr(index, start, length) + .add_op(OpSwap) + .unwrap() + // read REST_OF_SCRIPT from current input sigscript + .add_op(OpTxInputScriptSigSubstr) + .unwrap() + + // ---- new_redeem_script = ---- + // concatenate y_chunk with rest + .add_op(OpCat) + .unwrap() + // prepend x_chunk + .add_op(OpCat) + .unwrap() + + // ---- Build expected P2SH scriptPubKey bytes for new_redeem_script ---- + // hash160-equivalent in this system: blake2b(redeem) + .add_op(OpBlake2b) + .unwrap() + // version bytes + .add_data(&[0x00, 0x00]) + .unwrap() + // locking opcode prefix OP_BLAKE2B + .add_data(&[OpBlake2b]) + .unwrap() + // version || OP_BLAKE2B + .add_op(OpCat) + .unwrap() + // pushdata-length byte for 32-byte hash + .add_data(&[0x20]) + .unwrap() + // version || OP_BLAKE2B || push32 + .add_op(OpCat) + .unwrap() + // bring hash to top + .add_op(OpSwap) + .unwrap() + // append hash bytes + .add_op(OpCat) + .unwrap() + // trailing OP_EQUAL + .add_data(&[OpEqual]) + .unwrap() + // final expected output scriptPubKey bytes + .add_op(OpCat) + .unwrap() + + // ---- Compare against tx.outputs[0].scriptPubKey ---- + // output index argument + .add_i64(0) + .unwrap() + // fetch tx.outputs[0].scriptPubKey + .add_op(OpTxOutputSpk) + .unwrap() + // expected == actual + .add_op(OpEqual) + .unwrap() + // enforce match + .add_op(OpVerify) + .unwrap() + + // ---- Entrypoint epilogue cleanup for original state fields ---- + // drop original y + .add_op(OpDrop) + .unwrap() + // drop original x + .add_op(OpDrop) + .unwrap() + // final success value + .add_op(OpTrue) + .unwrap() + .drain(); + + assert_eq!(compiled.script, expected); +} + +#[test] +fn runs_validate_output_state() { + let source = r#" + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + + entrypoint function main() { + validateOutputState(0,{x:x+1,y:0x3412}); + } + } + "#; + + let input_compiled = + compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); + + let input = test_input(0, sigscript_push_script(&input_compiled.script)); + + let output_compiled = + compile_contract(source, &[6.into(), vec![0x34u8, 0x12u8].into()], CompileOptions::default()).expect("compile succeeds"); + let input_spk = pay_to_script_hash_script(&input_compiled.script); + let output_spk = pay_to_script_hash_script(&output_compiled.script); + let output = TransactionOutput { value: 1000, script_public_key: output_spk, covenant: None }; + let tx = Transaction::new(1, vec![input], vec![output.clone()], 0, Default::default(), 0, vec![]); + let utxo_entry = UtxoEntry::new(output.value, input_spk, 0, tx.is_coinbase(), None); + + let result = execute_input(tx, vec![utxo_entry], 0); + assert!(result.is_ok(), "validateOutputState runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn compiles_read_input_state_to_expected_script() { + let source = r#" + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + entrypoint function main() { - require(1 + 2 == 3); + {x: int in1_x, y: byte[2] in1_y} = readInputState(1); + require(in1_x > 7); + require(in1_y == 0x3412); } } "#; - let contract = parse_contract_ast(source).expect("ast parsed"); - let compiled = compile_contract_ast(&contract, &[], CompileOptions::default()).expect("compile succeeds"); - assert!(compiled.without_selector); + let compiled = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); let expected = ScriptBuilder::new() + // ---- Prolog state on active input: x=5, y=0x0102 ---- + // push x payload (8-byte LE) + .add_data(&5i64.to_le_bytes()) + .unwrap() + // decode x to numeric form + .add_op(OpBin2Num) + .unwrap() + // push y payload bytes + .add_data(&[1u8, 2u8]) + .unwrap() + + // ---- in1_x = readInputState(1).x ---- + // input index for start computation + .add_i64(1) + .unwrap() + // same input index for scriptSig length + .add_i64(1) + .unwrap() + // len(sigScript of input 1) + .add_op(OpTxInputScriptSigLen) + .unwrap() + // this.scriptSize + .add_i64(compiled.script.len() as i64) + .unwrap() + // base = sig_len - script_size + .add_op(OpSub) + .unwrap() + // skip int pushdata prefix byte (0x08) + .add_i64(1) + .unwrap() + // start_x = base + 1 + .add_op(OpAdd) + .unwrap() + + // input index for end computation + .add_i64(1) + .unwrap() + // len(sigScript of input 1) + .add_op(OpTxInputScriptSigLen) + .unwrap() + // this.scriptSize + .add_i64(compiled.script.len() as i64) + .unwrap() + // base = sig_len - script_size + .add_op(OpSub) + .unwrap() + // skip int prefix + .add_i64(1) + .unwrap() + // start_x = base + 1 + .add_op(OpAdd) + .unwrap() + // int payload length + .add_i64(8) + .unwrap() + // end_x = start_x + 8 + .add_op(OpAdd) + .unwrap() + // bytes = sigScriptSubstr(input=1, start_x, end_x) + .add_op(OpTxInputScriptSigSubstr) + .unwrap() + // decode bytes -> int + .add_op(OpBin2Num) + .unwrap() + // literal threshold + .add_i64(7) + .unwrap() + // in1_x > 7 + .add_op(OpGreaterThan) + .unwrap() + // enforce require(in1_x > 7) + .add_op(OpVerify) + .unwrap() + + // ---- in1_y = readInputState(1).y ---- + // input index for y start computation + .add_i64(1) + .unwrap() + // same input index for scriptSig length + .add_i64(1) + .unwrap() + // len(sigScript of input 1) + .add_op(OpTxInputScriptSigLen) + .unwrap() + // this.scriptSize + .add_i64(compiled.script.len() as i64) + .unwrap() + // base = sig_len - script_size + .add_op(OpSub) + .unwrap() + // skip x encoded chunk (10 bytes) + y pushdata prefix (1 byte) + .add_i64(11) + .unwrap() + // start_y = base + 11 + .add_op(OpAdd) + .unwrap() + + // input index for y end computation .add_i64(1) .unwrap() + // len(sigScript of input 1) + .add_op(OpTxInputScriptSigLen) + .unwrap() + // this.scriptSize + .add_i64(compiled.script.len() as i64) + .unwrap() + // base = sig_len - script_size + .add_op(OpSub) + .unwrap() + // skip x chunk + y prefix + .add_i64(11) + .unwrap() + // start_y = base + 11 + .add_op(OpAdd) + .unwrap() + // y payload length .add_i64(2) .unwrap() + // end_y = start_y + 2 .add_op(OpAdd) .unwrap() - .add_i64(3) + // bytes = sigScriptSubstr(input=1, start_y, end_y) + .add_op(OpTxInputScriptSigSubstr) .unwrap() - .add_op(OpNumEqual) + // expected y bytes + .add_data(&[0x34, 0x12]) .unwrap() + // in1_y == 0x3412 + .add_op(OpEqual) + .unwrap() + // enforce require(in1_y == 0x3412) .add_op(OpVerify) .unwrap() + + // drop original y field from active-input state prolog + .add_op(OpDrop) + .unwrap() + // drop original x field from active-input state prolog + .add_op(OpDrop) + .unwrap() + // success .add_op(OpTrue) .unwrap() .drain(); @@ -1205,91 +1970,155 @@ fn compiles_without_selector_single_function() { } #[test] -fn compiles_with_selector_multiple_entrypoints() { +fn runs_read_input_state() { let source = r#" - contract Test() { - entrypoint function a() { require(true); } - entrypoint function b() { require(true); } + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + + entrypoint function main() { + {x: int in1_x, y: byte[2] in1_y} = readInputState(1); + require(in1_x > 7); + require(in1_y == 0x3412); + } } "#; - let contract = parse_contract_ast(source).expect("ast parsed"); - let compiled = compile_contract_ast(&contract, &[], CompileOptions::default()).expect("compile succeeds"); - assert!(!compiled.without_selector); - let selector = function_branch_index(&compiled.ast, "a").expect("selector resolved"); - let sigscript = compiled.build_sig_script("a", vec![]).expect("sigscript builds"); - let expected = ScriptBuilder::new().add_i64(selector).unwrap().drain(); - assert_eq!(sigscript, expected); + let active_compiled = + compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); + let input1_compiled = + compile_contract(source, &[8.into(), vec![0x34u8, 0x12u8].into()], CompileOptions::default()).expect("compile succeeds"); + + let input0 = test_input(0, vec![]); + let input1 = test_input(1, sigscript_push_script(&input1_compiled.script)); + + let output = TransactionOutput { + value: 1000, + script_public_key: ScriptPublicKey::new(0, active_compiled.script.clone().into()), + covenant: None, + }; + let tx = Transaction::new(1, vec![input0.clone(), input1], vec![output.clone()], 0, Default::default(), 0, vec![]); + let utxo0 = UtxoEntry::new(output.value, output.script_public_key.clone(), 0, tx.is_coinbase(), None); + let utxo1 = UtxoEntry::new(1000, ScriptPublicKey::new(0, vec![OpTrue].into()), 0, tx.is_coinbase(), None); + let result = execute_input(tx, vec![utxo0, utxo1], 0); + assert!(result.is_ok(), "readInputState runtime failed: {}", result.unwrap_err()); } #[test] -fn compiles_basic_arithmetic_and_verifies() { +fn fails_validate_output_state_with_wrong_output_index() { let source = r#" - contract Test() { + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + entrypoint function main() { - require(1 + 2 == 3); + validateOutputState(0,{x:x+1,y:0x3412}); } } "#; - let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let selector = selector_for(&compiled, "main"); + let input_compiled = + compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); + let expected_output_state = + compile_contract(source, &[6.into(), vec![0x34u8, 0x12u8].into()], CompileOptions::default()).expect("compile succeeds"); - let body = ScriptBuilder::new() - .add_i64(1) - .unwrap() - .add_i64(2) - .unwrap() - .add_op(OpAdd) - .unwrap() - .add_i64(3) - .unwrap() - .add_op(OpNumEqual) - .unwrap() - .add_op(OpVerify) - .unwrap() - .add_op(OpTrue) - .unwrap() - .drain(); + let input = test_input(0, sigscript_push_script(&input_compiled.script)); - let expected = wrap_with_dispatch(body, selector); + let input_spk = pay_to_script_hash_script(&input_compiled.script); + let matching_spk = pay_to_script_hash_script(&expected_output_state.script); + let wrong_spk = pay_to_script_hash_script(&input_compiled.script); - assert_eq!(compiled.script, expected); - assert!(run_script_with_selector(compiled.script, selector).is_ok()); + let output0 = TransactionOutput { value: 1000, script_public_key: wrong_spk, covenant: None }; + let output1 = TransactionOutput { value: 1000, script_public_key: matching_spk, covenant: None }; + let tx = Transaction::new(1, vec![input], vec![output0, output1], 0, Default::default(), 0, vec![]); + let utxo_entry = UtxoEntry::new(1000, input_spk, 0, tx.is_coinbase(), None); + + let result = execute_input(tx, vec![utxo_entry], 0); + assert!(result.is_err()); } #[test] -fn compiles_contract_constants_and_verifies() { +fn fails_validate_output_state_with_mismatched_next_state_fields() { let source = r#" - contract Test() { - int constant MAX_SUPPLY = 1_000_000; + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; entrypoint function main() { - require(MAX_SUPPLY == 1_000_000); + validateOutputState(0,{x:x+1,y:0x3412}); } } "#; - let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let selector = selector_for(&compiled, "main"); + let input_compiled = + compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); + let wrong_output_state = + compile_contract(source, &[7.into(), vec![0x34u8, 0x12u8].into()], CompileOptions::default()).expect("compile succeeds"); - let body = ScriptBuilder::new() - .add_i64(1_000_000) - .unwrap() - .add_i64(1_000_000) - .unwrap() - .add_op(OpNumEqual) - .unwrap() - .add_op(OpVerify) - .unwrap() - .add_op(OpTrue) - .unwrap() - .drain(); + let input = test_input(0, sigscript_push_script(&input_compiled.script)); - let expected = wrap_with_dispatch(body, selector); + let input_spk = pay_to_script_hash_script(&input_compiled.script); + let wrong_output_spk = pay_to_script_hash_script(&wrong_output_state.script); + let output = TransactionOutput { value: 1000, script_public_key: wrong_output_spk, covenant: None }; + let tx = Transaction::new(1, vec![input], vec![output], 0, Default::default(), 0, vec![]); + let utxo_entry = UtxoEntry::new(1000, input_spk, 0, tx.is_coinbase(), None); - assert_eq!(compiled.script, expected); - assert!(run_script_with_selector(compiled.script, selector).is_ok()); + let result = execute_input(tx, vec![utxo_entry], 0); + assert!(result.is_err()); +} + +#[test] +fn rejects_validate_output_state_with_malformed_state_object() { + let source = r#" + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + + entrypoint function main() { + validateOutputState(0,{x:x+1}); + } + } + "#; + + let err = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()) + .expect_err("state object missing fields should fail"); + assert!(err.to_string().contains("new_state must include all contract fields exactly once"), "unexpected error: {err}"); +} + +#[test] +fn rejects_validate_output_state_with_duplicate_state_field() { + let source = r#" + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + + entrypoint function main() { + validateOutputState(0,{x:x+1,y:0x3412,x:x+2}); + } + } + "#; + + let err = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()) + .expect_err("state object duplicate fields should fail"); + assert!(err.to_string().contains("duplicate state field 'x'"), "unexpected error: {err}"); +} + +#[test] +fn rejects_validate_output_state_with_unknown_state_field() { + let source = r#" + contract C(int initX, byte[2] initY) { + int x = initX; + byte[2] y = initY; + + entrypoint function main() { + validateOutputState(0,{x:x+1,y:0x3412,z:1}); + } + } + "#; + + let err = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()) + .expect_err("state object with unknown field should fail"); + assert!(err.to_string().contains("new_state must include all contract fields exactly once"), "unexpected error: {err}"); } fn assert_compiled_body(source: &str, body: Vec) { @@ -2335,7 +3164,7 @@ fn data_prefix_for_size(data_len: usize) -> Vec { fn compiles_script_size_data_prefix_small_script() { let source = r#" contract PrefixSmall() { - entrypoint function main(bytes expected_data_prefix) { + entrypoint function main(byte[] expected_data_prefix) { require(expected_data_prefix == this.scriptSizeDataPrefix); require(true); } @@ -2344,7 +3173,7 @@ fn compiles_script_size_data_prefix_small_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![Expr::Bytes(expected_prefix)]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix small failed: {}", result.unwrap_err()); @@ -2354,7 +3183,7 @@ fn compiles_script_size_data_prefix_small_script() { fn compiles_script_size_data_prefix_medium_script() { let source = r#" contract PrefixMedium() { - entrypoint function main(bytes expected_data_prefix) { + entrypoint function main(byte[] expected_data_prefix) { require(expected_data_prefix == this.scriptSizeDataPrefix); for (i, 0, 100) { require(true); @@ -2365,7 +3194,7 @@ fn compiles_script_size_data_prefix_medium_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![Expr::Bytes(expected_prefix)]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix medium failed: {}", result.unwrap_err()); @@ -2375,7 +3204,7 @@ fn compiles_script_size_data_prefix_medium_script() { fn compiles_script_size_data_prefix_large_script() { let source = r#" contract PrefixLarge() { - entrypoint function main(bytes expected_data_prefix) { + entrypoint function main(byte[] expected_data_prefix) { require(expected_data_prefix == this.scriptSizeDataPrefix); for (i, 0, 300) { require(true); @@ -2386,7 +3215,7 @@ fn compiles_script_size_data_prefix_large_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![Expr::Bytes(expected_prefix)]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix large failed: {}", result.unwrap_err()); @@ -2463,21 +3292,217 @@ fn compiles_sigscript_reused_inputs_and_fails_on_wrong_value() { } #[test] -fn compiles_state_transition_helpers() { +fn compile_time_length_for_fixed_size_int_array() { + let source = r#" + contract Test() { + entrypoint function test() { + int[5] nums = [1, 2, 3, 4, 5]; + require(nums.length == 5); + } + } + "#; + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + + // Expected script for compile-time length: + // The nums.length should be replaced with a compile-time constant 5 + // require(nums.length == 5) becomes: <5> <5> OP_NUMEQUALVERIFY, then OP_TRUE for entrypoint return + let expected_script = vec![ + 0x55, // OP_5 (push 5 for nums.length) + 0x55, // OP_5 (push 5 for comparison) + 0x9c, // OP_NUMEQUALVERIFY (combined OP_NUMEQUAL + OP_VERIFY) + 0x69, // OP_VERIFY + 0x51, // OP_TRUE (entrypoint return value) + ]; + + assert_eq!( + compiled.script, expected_script, + "Script should use compile-time length. Expected: {:?}, Got: {:?}", + expected_script, compiled.script + ); +} + +#[test] +fn compile_time_length_for_fixed_size_byte_array() { + let source = r#" + contract Test() { + entrypoint function test() { + byte[3] data = 0x010203; + require(data.length == 3); + } + } + "#; + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + + // Expected script for compile-time length: + // data.length should be replaced with a compile-time constant 3 + // require(data.length == 3) becomes: <3> <3> OP_NUMEQUALVERIFY, then OP_TRUE for entrypoint return + let expected_script = vec![ + 0x53, // OP_3 (push 3 for data.length) + 0x53, // OP_3 (push 3 for comparison) + 0x9c, // OP_NUMEQUALVERIFY (combined OP_NUMEQUAL + OP_VERIFY) + 0x69, // OP_VERIFY + 0x51, // OP_TRUE (entrypoint return value) + ]; + + assert_eq!( + compiled.script, expected_script, + "Script should use compile-time length. Expected: {:?}, Got: {:?}", + expected_script, compiled.script + ); +} + +#[test] +fn compile_time_length_for_inferred_array_sizes() { let source = r#" - pragma silverscript ^0.1.0; + contract Test() { + entrypoint function test() { + byte[] data = 0x1234abcd; + int[] nums = [1, 2, 3]; + require(data.length == 4); + require(nums.length == 3); + } + } + "#; + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + + // Both lengths should be compile-time constants (no OP_SIZE path): + // require(data.length == 4) -> OP_4 OP_4 OP_NUMEQUALVERIFY + // require(nums.length == 3) -> OP_3 OP_3 OP_NUMEQUALVERIFY + let expected_script = vec![ + 0x54, // OP_4 (data.length) + 0x54, // OP_4 + 0x9c, // OP_NUMEQUALVERIFY + 0x69, // OP_VERIFY + 0x53, // OP_3 (nums.length) + 0x53, // OP_3 + 0x9c, // OP_NUMEQUALVERIFY + 0x69, // OP_VERIFY + 0x51, // OP_TRUE + ]; + + assert_eq!( + compiled.script, expected_script, + "Script should use compile-time inferred lengths. Expected: {:?}, Got: {:?}", + expected_script, compiled.script + ); +} + +#[test] +fn accepts_fixed_size_array_init_with_correct_size() { + let source = r#" + contract Test() { + entrypoint function test() { + int[4] nums = [1, 2, 3, 4]; + byte[3] data = 0x010203; + require(nums.length == 4); + require(data.length == 3); + } + } + "#; + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); +} + +#[test] +fn rejects_fixed_size_array_init_with_too_few_elements() { + let source = r#" + contract Test() { + entrypoint function test() { + int[4] nums = [1, 2, 3]; // Too few + } + } + "#; + let result = compile_contract(source, &[], CompileOptions::default()); + assert!(result.is_err(), "Should reject array with too few elements"); + let err_msg = format!("{:?}", result.unwrap_err()); + assert!(err_msg.contains("type mismatch") || err_msg.contains("size mismatch"), "Error should mention type or size mismatch"); +} + +#[test] +fn rejects_fixed_size_array_init_with_too_many_elements() { + let source = r#" + contract Test() { + entrypoint function test() { + int[3] nums = [1, 2, 3, 4, 5]; // Too many + } + } + "#; + let result = compile_contract(source, &[], CompileOptions::default()); + assert!(result.is_err(), "Should reject array with too many elements"); + let err_msg = format!("{:?}", result.unwrap_err()); + assert!(err_msg.contains("type mismatch") || err_msg.contains("size mismatch"), "Error should mention type or size mismatch"); +} + +#[test] +fn accepts_fixed_size_byte_array_init() { + let source = r#" + contract Test() { + entrypoint function test() { + byte[32] hash = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f; + require(hash.length == 32); + } + } + "#; + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); +} - contract Counter(int init_amount) { - int amount = init_amount; +#[test] +fn accepts_array_type_with_constant_size() { + // Test that constants can be used in array type declarations like int[SIZE] + let source = r#" + contract Test() { + int constant SIZE = 4; + entrypoint function test() { + int[SIZE] nums = [1, 2, 3, 4]; + require(nums.length == SIZE); + } + } + "#; + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds with int[SIZE]"); +} - entrypoint function main(int next_amount) { - {amount: int current_amount} = readInputState(this.activeInputIndex); - validateOutputState(0, {amount: next_amount}); - require(current_amount >= next_amount); +#[test] +fn compile_time_length_with_constant_size() { + // Test that array.length is computed at compile-time for arrays with constant sizes + let source = r#" + contract Test() { + int constant SIZE = 5; + entrypoint function test() { + int[SIZE] nums = [1, 2, 3, 4, 5]; + require(nums.length == SIZE); } } "#; + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + + // Expected script for compile-time length with constant size: + // nums.length should be replaced with compile-time constant 5 (from SIZE) + // SIZE constant should also be replaced with 5 + // require(nums.length == SIZE) becomes: <5> <5> OP_NUMEQUALVERIFY + let expected_script = vec![ + 0x55, // OP_5 (push 5 for nums.length) + 0x55, // OP_5 (push 5 for SIZE constant) + 0x9c, // OP_NUMEQUALVERIFY + 0x69, // OP_VERIFY + 0x51, // OP_TRUE (entrypoint return value) + ]; + + assert_eq!( + compiled.script, expected_script, + "Script should use compile-time length with constant. Expected: {:?}, Got: {:?}", + expected_script, compiled.script + ); +} - let compiled = compile_contract(source, &[Expr::Int(100)], CompileOptions::default()).expect("compile succeeds"); - assert!(!compiled.script.is_empty()); +#[test] +fn accepts_byte_array_with_constant_size() { + // Test that constants work with byte arrays too + let source = r#" + contract Test() { + int constant HASH_SIZE = 32; + entrypoint function test(byte[HASH_SIZE] hash) { + require(hash.length == HASH_SIZE); + } + } + "#; + compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds with byte[HASH_SIZE]"); } diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index e0606afc..1fef2c33 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; +use silverscript_lang::ast::{Expr, Statement, parse_contract_ast}; fn extract_first_expr(source: &str) -> Expr { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; - match &statement.kind { - StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - StatementKind::Require { expr, .. } => expr.clone(), + match statement { + Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + Statement::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index be3af599..5aee58b3 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -7,9 +7,8 @@ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_txscript::caches::Cache; use kaspa_txscript::{EngineCtx, EngineFlags}; -use silverscript_lang::ast::{Expr, parse_contract_ast}; +use silverscript_lang::ast::Expr; use silverscript_lang::compiler::{CompileOptions, compile_contract}; -use silverscript_lang::debug::MappingKind; use silverscript_lang::debug::session::DebugSession; fn example_contract_path() -> PathBuf { @@ -17,335 +16,56 @@ fn example_contract_path() -> PathBuf { manifest_dir.join("tests/examples/if_statement.sil") } -// Convenience harness for the canonical example contract used by baseline session tests. fn with_session(mut f: F) -> Result<(), Box> where F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, { let contract_path = example_contract_path(); - assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); - let source = fs::read_to_string(&contract_path)?; - with_session_for_source(&source, vec![Expr::Int(3), Expr::Int(10)], "hello", vec![Expr::Int(5), Expr::Int(5)], &mut f) -} -// Generic harness that compiles a contract and boots a debugger session for a selected function call. -fn with_session_for_source( - source: &str, - ctor_args: Vec, - function_name: &str, - function_args: Vec, - mut f: F, -) -> Result<(), Box> -where - F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, -{ - let parsed_contract = parse_contract_ast(source)?; - assert_eq!(parsed_contract.params.len(), ctor_args.len()); - - // Compile with debug metadata enabled so line mappings and variable updates are available. let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; - let compiled = compile_contract(source, &ctor_args, compile_opts)?; - let debug_info = compiled.debug_info.clone(); + let compiled = compile_contract(&source, &[Expr::Int(3), Expr::Int(10)], compile_opts)?; let sig_cache = Cache::new(10_000); let reused_values = SigHashReusedValuesUnsync::new(); let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); - let flags = EngineFlags { covenants_enabled: true }; let engine = silverscript_lang::debug::session::DebugEngine::new(ctx, flags); - let entry = compiled - .abi - .iter() - .find(|entry| entry.name == function_name) - .ok_or_else(|| format!("function '{function_name}' not found"))?; - - assert_eq!(entry.inputs.len(), function_args.len()); - - // Seed stack with sigscript args and then execute the lockscript in debug mode. - let sigscript = compiled.build_sig_script(function_name, function_args)?; - let mut session = DebugSession::full(&sigscript, &compiled.script, source, debug_info, engine)?; + let sigscript = compiled.build_sig_script("hello", vec![Expr::Int(5), Expr::Int(5)])?; + let mut session = DebugSession::full(&sigscript, &compiled.script, &source, compiled.debug_info.clone(), engine)?; f(&mut session) } #[test] -fn debug_session_provides_source_context_and_vars() -> Result<(), Box> { +fn debug_session_lists_entrypoint_params() -> Result<(), Box> { with_session(|session| { - // Skip dispatcher setup and land on first user statement. session.run_to_first_executed_statement()?; - let context = session.source_context(); - assert!(context.is_some(), "expected source context"); - let vars = session.list_variables().expect("variables available"); let names = vars.iter().map(|var| var.name.as_str()).collect::>(); - assert!(names.contains("a"), "expected param 'a' in variables"); - assert!(names.contains("b"), "expected param 'b' in variables"); - + assert!(names.contains("a")); + assert!(names.contains("b")); Ok(()) }) } #[test] -fn debug_session_steps_forward() -> Result<(), Box> { +fn debug_session_can_step_mappings() -> Result<(), Box> { with_session(|session| { session.run_to_first_executed_statement()?; - let before = session.state().pc; - let before_span = session.current_span(); - session.step_statement()?; - let after = session.state().pc; - let after_span = session.current_span(); - assert!(after > before || after_span != before_span, "expected statement step to make source progress"); + let stepped = session.step_statement()?; + assert!(stepped.is_some(), "expected at least one statement step"); Ok(()) }) } #[test] -fn debug_session_breakpoint_management() -> Result<(), Box> { +fn debug_session_breakpoint_requires_source_spans() -> Result<(), Box> { with_session(|session| { session.run_to_first_executed_statement()?; - let span = session.current_span().ok_or("no current span")?; - let line = span.line; - - session.add_breakpoint(line); - assert!(session.breakpoints().contains(&line)); - - session.clear_breakpoint(line); - assert!(!session.breakpoints().contains(&line)); - Ok(()) - }) -} - -#[test] -fn debug_session_tracks_array_assignment_updates() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract Arr() { - entrypoint function main() { - int[] a; - int[] b; - b.push(1); - a = b; - require(length(a) == 1); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![], |session| { - session.run_to_first_executed_statement()?; - assert!(session.add_breakpoint(9), "require line should accept breakpoints"); - session.continue_to_breakpoint()?; - - let a = session.variable_by_name("a")?; - assert_eq!(session.format_value(&a.type_name, &a.value), "[1]"); - Ok(()) - }) -} - -#[test] -fn debug_session_hits_multiline_breakpoints() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract BP() { - entrypoint function main(int a) { - require(a == 1); - require(a == 1); - require( - a == 1 - ); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(1)], |session| { - session.run_to_first_executed_statement()?; - // Line 8 is inside a multiline `require(...)` span and should still be hit. - assert!(session.add_breakpoint(8), "expected breakpoint line to be valid"); - - let hit = session.continue_to_breakpoint()?; - assert!(hit.is_some(), "expected to stop at multiline statement breakpoint"); - - let span = session.current_span().ok_or("expected source span at breakpoint")?; - assert!((span.line..=span.end_line).contains(&8)); - Ok(()) - }) -} - -#[test] -fn debug_session_dedupes_shadowed_constructor_constants() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract Shadow(int x) { - entrypoint function main(int x) { - require(x == x); - } -} -"#; - - with_session_for_source(source, vec![Expr::Int(7)], "main", vec![Expr::Int(3)], |session| { - session.run_to_first_executed_statement()?; - - // Function param `x` should shadow constructor constant `x` in visible debugger variables. - let vars = session.list_variables()?; - let x_count = vars.iter().filter(|var| var.name == "x").count(); - assert_eq!(x_count, 1, "expected a single visible x variable"); - - let x = session.variable_by_name("x")?; - assert!(!x.is_constant, "function parameter should shadow constructor constant"); - assert_eq!(session.format_value(&x.type_name, &x.value), "3"); - Ok(()) - }) -} - -#[test] -fn debug_session_exposes_virtual_steps() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract Virtuals() { - entrypoint function main(int a) { - int x = a + 1; - x = x + 2; - require(x > 0); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { - session.run_to_first_executed_statement()?; - let first = session.current_location().ok_or("missing first location")?; - assert!(matches!(first.kind, MappingKind::Virtual {})); - let first_pc = session.state().pc; - - let second = session.step_over()?.ok_or("missing second step")?.mapping.ok_or("missing second mapping")?; - assert!(matches!(second.kind, MappingKind::Virtual {})); - assert_eq!(session.state().pc, first_pc, "virtual step should not execute opcodes"); - - let third = session.step_over()?.ok_or("missing third step")?.mapping.ok_or("missing third mapping")?; - assert!(matches!(third.kind, MappingKind::Statement {})); - assert_eq!(session.state().pc, first_pc, "first real statement should still be at same pc boundary"); - Ok(()) - }) -} - -#[test] -fn debug_session_breakpoint_hits_virtual_line() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract VirtualBp() { - entrypoint function main(int a) { - int x = a + 1; - x = x + 2; - require(x > 0); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { - session.run_to_first_executed_statement()?; - assert!(session.add_breakpoint(6), "line with virtual assignment should be a valid breakpoint"); - let hit = session.continue_to_breakpoint()?; - assert!(hit.is_some(), "expected breakpoint on virtual line"); - let span = session.current_span().ok_or("missing span at virtual breakpoint")?; - assert_eq!(span.line, 6); - Ok(()) - }) -} - -#[test] -fn debug_session_inline_stepping_supports_into_over_out() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract InlineStep() { - function add1(int x) : (int) { - int y = x + 1; - require(y > 0); - return(y); - } - - entrypoint function main(int a) { - int seed = a; - (int r) = add1(seed); - require(r > 0); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { - session.run_to_first_executed_statement()?; - let root = session.current_location().ok_or("missing root mapping")?; - assert_eq!(root.call_depth, 0); - - let into = session.step_into()?.ok_or("step into failed")?.mapping.ok_or("missing mapping after step into")?; - assert_eq!(into.call_depth, 1, "step into should enter inline callee"); - assert!(session.call_stack().iter().any(|name| name == "add1"), "inline call stack should include callee name"); - - let out = session.step_out()?.ok_or("step out failed")?.mapping.ok_or("missing mapping after step out")?; - assert_eq!(out.call_depth, 0, "step out should return to caller depth"); - Ok(()) - })?; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { - session.run_to_first_executed_statement()?; - let over = session.step_over()?.ok_or("step over failed")?.mapping.ok_or("missing mapping after step over")?; - assert_eq!(over.call_depth, 0, "step over should stay in caller depth"); - Ok(()) - }) -} - -#[test] -fn debug_session_inline_params_visible_inside_callee() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract InlineParams() { - function add1(int x) : (int) { - int y = x + 1; - require(y > 0); - return(y); - } - - entrypoint function main(int a) { - int seed = a; - (int r) = add1(seed); - require(r > 0); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { - session.run_to_first_executed_statement()?; - session.step_into()?; - - let x = session.variable_by_name("x")?; - let rendered = session.format_value(&x.type_name, &x.value); - assert_eq!(rendered, "4", "inline param x should be visible inside callee"); - Ok(()) - }) -} - -#[test] -fn debug_session_function_call_assign_resolves_inline_args() -> Result<(), Box> { - let source = r#"pragma silverscript ^0.1.0; - -contract InlineAssign() { - function inc(int x) : (int) { - return(x + 1); - } - - entrypoint function main(int a) { - int seed = a; - (int r) = inc(seed); - require(r == a + 1); - } -} -"#; - - with_session_for_source(source, vec![], "main", vec![Expr::Int(5)], |session| { - session.run_to_first_executed_statement()?; - session.step_over()?; - let r = session.variable_by_name("r")?; - let rendered = session.format_value(&r.type_name, &r.value); - assert_eq!(rendered, "6"); + assert!(!session.add_breakpoint(7), "line breakpoints should be rejected without span mappings"); + assert!(session.breakpoints().is_empty()); Ok(()) }) } diff --git a/silverscript-lang/tests/debugger_cli_tests.rs b/silverscript-lang/tests/debugger_cli_tests.rs index 6818a0cf..e0cd2021 100644 --- a/silverscript-lang/tests/debugger_cli_tests.rs +++ b/silverscript-lang/tests/debugger_cli_tests.rs @@ -45,6 +45,6 @@ fn sil_debug_repl_all_commands_smoke() { assert!(stdout.contains("Commands:"), "missing help output"); assert!(stdout.contains("Stack:"), "missing stack output"); assert!(stdout.contains("no statement at line 1"), "missing invalid breakpoint warning"); - assert!(stdout.contains("Breakpoint set at line 7"), "missing breakpoint confirmation"); - assert!(stdout.contains("Breakpoints: 7"), "missing breakpoint listing"); + assert!(stdout.contains("no statement at line 7"), "missing line-7 breakpoint warning"); + assert!(stdout.contains("No breakpoints set."), "missing breakpoint listing"); } diff --git a/silverscript-lang/tests/examples/covenant_id.sil b/silverscript-lang/tests/examples/covenant_id.sil index 0d493bc7..c0ea1b05 100644 --- a/silverscript-lang/tests/examples/covenant_id.sil +++ b/silverscript-lang/tests/examples/covenant_id.sil @@ -5,7 +5,7 @@ contract CovenantId(int max_ins, int max_outs, int init_amount) { entrypoint function main(int[] output_amounts) { require(output_amounts.length <= max_outs); byte[32] covid = OpInputCovenantId(this.activeInputIndex); - + int in_count = OpCovInputCount(covid); require(in_count <= max_ins); diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index 087baf06..91845575 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -149,9 +149,7 @@ fn script_with_return_checks(script: Vec, expected: &[i64]) -> Vec { } fn sigscript_push_script(script: &[u8]) -> Vec { - let mut builder = ScriptBuilder::new(); - builder.add_data(script).expect("push script"); - builder.drain() + ScriptBuilder::new().add_data(script).unwrap().drain() } #[test] @@ -457,7 +455,16 @@ fn runs_everything_example_and_verifies() { sequence: 500, sig_op_count: 1, }; - let checked_script = compiled.script.clone(); + let checked_script = ScriptBuilder::new() + .add_ops(&compiled.script) + .unwrap() + .add_op(OpDrop) + .unwrap() + .add_op(OpDrop) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); let output = TransactionOutput { value: 5_000, script_public_key: ScriptPublicKey::new(0, checked_script.clone().into()), covenant: None }; From 926343a1e475f770727f9d98c70489f2912f02db Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:37:54 +0200 Subject: [PATCH 09/41] Refactor sil-debug CLI to clap and use faster-hex --- Cargo.lock | 190 ++++++++++++++++-- silverscript-lang/Cargo.toml | 3 +- silverscript-lang/src/ast.rs | 68 ++++--- silverscript-lang/src/bin/common/mod.rs | 186 ----------------- silverscript-lang/src/bin/sil-debug.rs | 127 +++++++++++- silverscript-lang/src/compiler.rs | 103 +++++----- .../src/compiler/debug_recording.rs | 31 ++- silverscript-lang/src/debug.rs | 9 + silverscript-lang/src/debug/session.rs | 20 +- silverscript-lang/tests/date_literal_tests.rs | 8 +- .../tests/debug_session_tests.rs | 188 +++++++++++++++-- silverscript-lang/tests/debugger_cli_tests.rs | 4 +- 12 files changed, 621 insertions(+), 316 deletions(-) delete mode 100644 silverscript-lang/src/bin/common/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5a24b5e1..7fe4380b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -324,6 +374,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" @@ -397,6 +453,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -666,6 +768,16 @@ dependencies = [ "serde", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -843,6 +955,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -858,6 +979,22 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -873,12 +1010,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hexplay" version = "0.3.0" @@ -970,6 +1101,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.11.0" @@ -1029,7 +1166,7 @@ dependencies = [ "bitflags", "borsh", "cfg-if", - "faster-hex", + "faster-hex 0.9.0", "futures-util", "getrandom 0.2.17", "itertools 0.13.0", @@ -1096,7 +1233,7 @@ dependencies = [ "blake3", "borsh", "cc", - "faster-hex", + "faster-hex 0.9.0", "js-sys", "kaspa-utils", "keccak", @@ -1113,7 +1250,7 @@ version = "1.1.0-rc.2" source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ "borsh", - "faster-hex", + "faster-hex 0.9.0", "js-sys", "kaspa-utils", "malachite-base", @@ -1199,7 +1336,7 @@ dependencies = [ "cfg-if", "duct", "event-listener 2.5.3", - "faster-hex", + "faster-hex 0.9.0", "ipnet", "itertools 0.13.0", "log", @@ -1223,7 +1360,7 @@ name = "kaspa-wasm-core" version = "1.1.0-rc.2" source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#2aec19dbbb9579474a2338192c0294197266eab5" dependencies = [ - "faster-hex", + "faster-hex 0.9.0", "hexplay", "js-sys", "wasm-bindgen", @@ -1510,6 +1647,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2084,7 +2227,8 @@ version = "0.1.0" dependencies = [ "blake2b_simd", "chrono", - "hex", + "clap", + "faster-hex 0.10.0", "kaspa-addresses", "kaspa-consensus-core", "kaspa-txscript", @@ -2119,6 +2263,18 @@ dependencies = [ "serde", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -2380,6 +2536,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "0.8.2" @@ -2908,7 +3070,7 @@ dependencies = [ "cfg-if", "chrono", "dirs", - "faster-hex", + "faster-hex 0.9.0", "futures", "getrandom 0.2.17", "instant", @@ -3003,7 +3165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e5fbf266e0fffb5c24d6103735eb2b94bb31f93b664b91eaaf63b4f959804" dependencies = [ "cfg-if", - "faster-hex", + "faster-hex 0.9.0", "futures", "js-sys", "serde", diff --git a/silverscript-lang/Cargo.toml b/silverscript-lang/Cargo.toml index 49100903..746eb26d 100644 --- a/silverscript-lang/Cargo.toml +++ b/silverscript-lang/Cargo.toml @@ -23,7 +23,8 @@ rand.workspace = true secp256k1.workspace = true thiserror.workspace = true serde = { version = "1.0", features = ["derive"] } -hex = "0.4" +clap = { version = "4.5", features = ["derive"] } +faster-hex = "0.10" serde_json = "1.0" [dev-dependencies] diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 05140b93..07795ce3 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -5,6 +5,7 @@ use pest::iterators::Pair; use serde::{Deserialize, Serialize}; use crate::compiler::CompilerError; +use crate::debug::SourceSpan; use crate::parser::{Rule, SilverScriptParser}; use chrono::NaiveDateTime; @@ -121,9 +122,17 @@ impl TypeRef { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Statement { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub span: Option, + #[serde(flatten)] + pub kind: StatementKind, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum Statement { +pub enum StatementKind { VariableDefinition { type_ref: TypeRef, modifiers: Vec, name: String, expr: Option }, TupleAssignment { left_type_ref: TypeRef, left_name: String, right_type_ref: TypeRef, right_name: String, expr: Expr }, ArrayPush { name: String, expr: Expr }, @@ -426,14 +435,15 @@ fn parse_function_definition(pair: Pair<'_, Rule>) -> Result) -> Result { - match pair.as_rule() { - Rule::statement => { - if let Some(inner) = pair.into_inner().next() { - parse_statement(inner) - } else { - Err(CompilerError::Unsupported("empty statement".to_string())) - } + if pair.as_rule() == Rule::statement { + if let Some(inner) = pair.into_inner().next() { + return parse_statement(inner); } + return Err(CompilerError::Unsupported("empty statement".to_string())); + } + + let span = Some(SourceSpan::from(pair.as_span())); + let kind = match pair.as_rule() { Rule::variable_definition => { let mut inner = pair.into_inner(); let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))?; @@ -450,7 +460,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()))?; validate_user_identifier(ident.as_str())?; let expr = inner.next().map(parse_expression).transpose()?; - Ok(Statement::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr }) + StatementKind::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr } } Rule::tuple_assignment => { let mut inner = pair.into_inner(); @@ -465,27 +475,27 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::TupleAssignment { + StatementKind::TupleAssignment { left_type_ref, left_name: left_ident.as_str().to_string(), right_type_ref, right_name: right_ident.as_str().to_string(), expr, - }) + } } Rule::push_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::ArrayPush { name: ident.as_str().to_string(), expr }) + StatementKind::ArrayPush { name: ident.as_str().to_string(), expr } } Rule::assign_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Ok(Statement::Assign { name: ident.as_str().to_string(), expr }) + StatementKind::Assign { name: ident.as_str().to_string(), expr } } Rule::time_op_statement => { let mut inner = pair.into_inner(); @@ -499,14 +509,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { "tx.time" => TimeVar::TxTime, other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), }; - Ok(Statement::TimeOp { tx_var, expr, message }) + StatementKind::TimeOp { tx_var, expr, message } } Rule::require_statement => { let mut inner = pair.into_inner(); let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; let message = inner.next().map(parse_require_message).transpose()?; let expr = parse_expression(expr_pair)?; - Ok(Statement::Require { expr, message }) + StatementKind::Require { expr, message } } Rule::if_statement => { let mut inner = pair.into_inner(); @@ -515,14 +525,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let then_block = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()))?; let then_branch = parse_block(then_block)?; let else_branch = inner.next().map(parse_block).transpose()?; - Ok(Statement::If { condition: cond_expr, then_branch, else_branch }) + StatementKind::If { condition: cond_expr, then_branch, else_branch } } Rule::call_statement => { let mut inner = pair.into_inner(); let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Ok(Statement::FunctionCall { name, args }), - _ => Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::function_call_assignment => { @@ -546,8 +556,8 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Ok(Statement::FunctionCallAssign { bindings, name, args }), - _ => Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::state_function_call_assignment => { @@ -577,8 +587,8 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Ok(Statement::StateFunctionCallAssign { bindings, name, args }), - _ => Err(CompilerError::Unsupported("function call expected".to_string())), + Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, + _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } Rule::for_statement => { @@ -593,7 +603,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let end_expr = parse_expression(end_pair)?; let body = parse_block(block_pair)?; - Ok(Statement::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body }) + StatementKind::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } } Rule::yield_statement => { let mut inner = pair.into_inner(); @@ -602,7 +612,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.len() != 1 { return Err(CompilerError::Unsupported("yield() expects a single argument".to_string())); } - Ok(Statement::Yield { expr: args[0].clone() }) + StatementKind::Yield { expr: args[0].clone() } } Rule::return_statement => { let mut inner = pair.into_inner(); @@ -611,16 +621,18 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.is_empty() { return Err(CompilerError::Unsupported("return() expects at least one argument".to_string())); } - Ok(Statement::Return { exprs: args }) + StatementKind::Return { exprs: args } } Rule::console_statement => { let mut inner = pair.into_inner(); let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; let args = parse_console_parameter_list(list_pair)?; - Ok(Statement::Console { args }) + StatementKind::Console { args } } - _ => Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), - } + _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), + }; + + Ok(Statement { span, kind }) } fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { diff --git a/silverscript-lang/src/bin/common/mod.rs b/silverscript-lang/src/bin/common/mod.rs deleted file mode 100644 index 915a2469..00000000 --- a/silverscript-lang/src/bin/common/mod.rs +++ /dev/null @@ -1,186 +0,0 @@ -#![allow(dead_code)] - -use std::env; -use std::error::Error; - -use silverscript_lang::ast::Expr; - -pub struct DebugCliArgs { - pub script_path: String, - pub without_selector: bool, - pub function_name: Option, - pub raw_ctor_args: Vec, - pub raw_args: Vec, -} - -pub fn print_usage(bin_name: &str) { - eprintln!( - "Usage: {bin_name} [--no-selector] [--function ] [--ctor-arg ...] [--arg ...]\n\n --ctor-arg is typed by the contract constructor params.\n --arg is typed by the selected function ABI.\n\nExamples:\n # constructor (int x, int y), function hello(int a, int b)\n {bin_name} if_statement.sil --function hello --ctor-arg 3 --ctor-arg 10 --arg 1 --arg 2\n\nValue formats:\n int: 123 (or 0x7b)\n bool: true|false\n string: hello (shell quoting handles spaces)\n bytes*: 0xdeadbeef\n" - ); -} - -pub fn parse_cli_args_or_help(bin_name: &str) -> Result, Box> { - parse_cli_args_or_help_from(bin_name, env::args().skip(1)) -} - -fn parse_cli_args_or_help_from( - bin_name: &str, - mut args: impl Iterator, -) -> Result, Box> { - let mut script_path: Option = None; - let mut without_selector = false; - let mut function_name: Option = None; - let mut raw_ctor_args: Vec = Vec::new(); - let mut raw_args: Vec = Vec::new(); - - while let Some(arg) = args.next() { - match arg.as_str() { - "--no-selector" => without_selector = true, - "--function" | "-f" => { - function_name = args.next(); - if function_name.is_none() { - print_usage(bin_name); - return Err("missing function name".into()); - } - } - "--ctor-arg" => { - let value = args.next(); - if value.is_none() { - print_usage(bin_name); - return Err("missing --ctor-arg value".into()); - } - raw_ctor_args.push(value.expect("checked")); - } - "--arg" | "-a" => { - let value = args.next(); - if value.is_none() { - print_usage(bin_name); - return Err("missing --arg value".into()); - } - raw_args.push(value.expect("checked")); - } - "-h" | "--help" => { - print_usage(bin_name); - return Ok(None); - } - _ => { - if script_path.is_some() { - print_usage(bin_name); - return Err("unexpected extra argument".into()); - } - script_path = Some(arg); - } - } - } - - let script_path = match script_path { - Some(path) => path, - None => { - print_usage(bin_name); - return Err("missing contract path".into()); - } - }; - - Ok(Some(DebugCliArgs { script_path, without_selector, function_name, raw_ctor_args, raw_args })) -} - -fn parse_int_arg(raw: &str) -> Result> { - let cleaned = raw.replace('_', ""); - if let Some(hex) = cleaned.strip_prefix("0x").or_else(|| cleaned.strip_prefix("0X")) { - return Ok(i64::from_str_radix(hex, 16)?); - } - Ok(cleaned.parse::()?) -} - -fn parse_hex_bytes(raw: &str) -> Result, Box> { - let trimmed = raw.trim(); - let hex_str = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")).unwrap_or(trimmed); - if hex_str.is_empty() { - return Ok(vec![]); - } - // Allow odd length by implicitly left-padding with 0 - let normalized = if hex_str.len() % 2 != 0 { format!("0{hex_str}") } else { hex_str.to_string() }; - Ok(hex::decode(normalized)?) -} - -fn bytes_expr(bytes: Vec) -> Expr { - Expr::Array(bytes.into_iter().map(Expr::Byte).collect()) -} - -pub fn parse_typed_arg(type_name: &str, raw: &str) -> Result> { - // Support array inputs until the LSP exists by allowing: - // - JSON arrays: [1,2,3] or ["0x01","0x02"] - // - raw hex bytes: 0x... (treated as encoded array bytes) - if let Some(element_type) = type_name.strip_suffix("[]") { - let trimmed = raw.trim(); - if trimmed.starts_with('[') { - let values = serde_json::from_str::>(trimmed)?; - let mut out = Vec::with_capacity(values.len()); - for v in values { - let expr = match v { - serde_json::Value::Number(n) => Expr::Int(n.as_i64().ok_or("invalid int in array")?), - serde_json::Value::Bool(b) => Expr::Bool(b), - serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, - _ => return Err("unsupported array element (expected number/bool/string)".into()), - }; - out.push(expr); - } - return Ok(Expr::Array(out)); - } - if element_type == "byte" { - return Ok(bytes_expr(parse_hex_bytes(trimmed)?)); - } - return Err(format!("unsupported array literal format for '{type_name}'").into()); - } - - match type_name { - "int" => Ok(Expr::Int(parse_int_arg(raw)?)), - "bool" => match raw { - "true" => Ok(Expr::Bool(true)), - "false" => Ok(Expr::Bool(false)), - _ => Err(format!("invalid bool '{raw}' (expected true/false)").into()), - }, - "string" => Ok(Expr::String(raw.to_string())), - "byte" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() == 1 { Ok(Expr::Byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } - } - "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), - "pubkey" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 32 { - return Err(format!("pubkey expects 32 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - "sig" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 65 { - return Err(format!("sig expects 65 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - "datasig" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 64 { - return Err(format!("datasig expects 64 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - other => { - let size = other - .strip_prefix("bytes") - .and_then(|v| v.parse::().ok()) - .or_else(|| other.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok())); - if let Some(size) = size { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != size { - return Err(format!("{other} expects {size} bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } else { - Err(format!("unsupported arg type '{other}'").into()) - } - } - } -} diff --git a/silverscript-lang/src/bin/sil-debug.rs b/silverscript-lang/src/bin/sil-debug.rs index cefc655b..b13531cb 100644 --- a/silverscript-lang/src/bin/sil-debug.rs +++ b/silverscript-lang/src/bin/sil-debug.rs @@ -1,18 +1,131 @@ use std::fs; use std::io::{self, BufRead, Write}; +use clap::Parser; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_txscript::caches::Cache; use kaspa_txscript::{EngineCtx, EngineFlags}; -use silverscript_lang::ast::parse_contract_ast; +use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; use silverscript_lang::debug::session::{DebugEngine, DebugSession}; -mod common; - const PROMPT: &str = "(sdb) "; +#[derive(Debug, Parser)] +#[command(name = "sil-debug", about = "SilverScript debugger")] +struct CliArgs { + script_path: String, + #[arg(long = "no-selector")] + without_selector: bool, + #[arg(long = "function", short = 'f')] + function_name: Option, + #[arg(long = "ctor-arg")] + raw_ctor_args: Vec, + #[arg(long = "arg", short = 'a')] + raw_args: Vec, +} + +fn parse_int_arg(raw: &str) -> Result> { + let cleaned = raw.replace('_', ""); + if let Some(hex) = cleaned.strip_prefix("0x").or_else(|| cleaned.strip_prefix("0X")) { + return Ok(i64::from_str_radix(hex, 16)?); + } + Ok(cleaned.parse::()?) +} + +fn parse_hex_bytes(raw: &str) -> Result, Box> { + let trimmed = raw.trim(); + let hex_str = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")).unwrap_or(trimmed); + if hex_str.is_empty() { + return Ok(vec![]); + } + // Allow odd length by implicitly left-padding with 0. + let normalized = if hex_str.len() % 2 != 0 { format!("0{hex_str}") } else { hex_str.to_string() }; + let mut out = vec![0u8; normalized.len() / 2]; + faster_hex::hex_decode(normalized.as_bytes(), &mut out)?; + Ok(out) +} + +fn bytes_expr(bytes: Vec) -> Expr { + Expr::Array(bytes.into_iter().map(Expr::Byte).collect()) +} + +fn parse_typed_arg(type_name: &str, raw: &str) -> Result> { + if let Some(element_type) = type_name.strip_suffix("[]") { + let trimmed = raw.trim(); + if trimmed.starts_with('[') { + let values = serde_json::from_str::>(trimmed)?; + let mut out = Vec::with_capacity(values.len()); + for value in values { + let expr = match value { + serde_json::Value::Number(n) => Expr::Int(n.as_i64().ok_or("invalid int in array")?), + serde_json::Value::Bool(b) => Expr::Bool(b), + serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, + _ => return Err("unsupported array element (expected number/bool/string)".into()), + }; + out.push(expr); + } + return Ok(Expr::Array(out)); + } + if element_type == "byte" { + return Ok(bytes_expr(parse_hex_bytes(trimmed)?)); + } + return Err(format!("unsupported array literal format for '{type_name}'").into()); + } + + match type_name { + "int" => Ok(Expr::Int(parse_int_arg(raw)?)), + "bool" => match raw { + "true" => Ok(Expr::Bool(true)), + "false" => Ok(Expr::Bool(false)), + _ => Err(format!("invalid bool '{raw}' (expected true/false)").into()), + }, + "string" => Ok(Expr::String(raw.to_string())), + "byte" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() == 1 { Ok(Expr::Byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } + } + "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), + "pubkey" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 32 { + return Err(format!("pubkey expects 32 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } + "sig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 65 { + return Err(format!("sig expects 65 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } + "datasig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 64 { + return Err(format!("datasig expects 64 bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } + other => { + let size = other + .strip_prefix("bytes") + .and_then(|v| v.parse::().ok()) + .or_else(|| other.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok())); + if let Some(size) = size { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != size { + return Err(format!("{other} expects {size} bytes, got {}", bytes.len()).into()); + } + Ok(bytes_expr(bytes)) + } else { + Err(format!("unsupported arg type '{other}'").into()) + } + } + } +} + fn show_stack(session: &DebugSession<'_>) { println!("Stack:"); let stack = session.stack(); @@ -172,9 +285,7 @@ fn run_repl(session: &mut DebugSession<'_>) -> Result<(), kaspa_txscript_errors: } fn main() -> Result<(), Box> { - let Some(cli) = common::parse_cli_args_or_help("sil-debug")? else { - return Ok(()); - }; + let cli = CliArgs::parse(); let script_path = cli.script_path; let without_selector = cli.without_selector; let function_name = cli.function_name; @@ -195,7 +306,7 @@ fn main() -> Result<(), Box> { let mut ctor_args = Vec::with_capacity(raw_ctor_args.len()); for (param, raw) in parsed_contract.params.iter().zip(raw_ctor_args.iter()) { - ctor_args.push(common::parse_typed_arg(¶m.type_ref.type_name(), raw)?); + ctor_args.push(parse_typed_arg(¶m.type_ref.type_name(), raw)?); } let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; @@ -224,7 +335,7 @@ fn main() -> Result<(), Box> { let mut typed_args = Vec::with_capacity(raw_args.len()); for (input, raw) in entry.inputs.iter().zip(raw_args.iter()) { - typed_args.push(common::parse_typed_arg(&input.type_name, raw)?); + typed_args.push(parse_typed_arg(&input.type_name, raw)?); } // Always seed: even in --no-selector mode the function params must be pushed. diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index da64adab..89058032 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -7,7 +7,7 @@ use thiserror::Error; use crate::ast::{ ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, - Statement, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, + Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; use crate::debug::DebugInfo; use crate::debug::labels::synthetic; @@ -287,27 +287,29 @@ fn compile_contract_fields( } fn statement_uses_script_size(stmt: &Statement) -> bool { - match stmt { - Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), - Statement::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), - Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - Statement::StateFunctionCallAssign { name, args, .. } => name == "readInputState" || args.iter().any(expr_uses_script_size), - Statement::Assign { expr, .. } => expr_uses_script_size(expr), - Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), - Statement::Require { expr, .. } => expr_uses_script_size(expr), - Statement::If { condition, then_branch, else_branch } => { + match &stmt.kind { + StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), + StatementKind::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), + StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::StateFunctionCallAssign { name, args, .. } => { + name == "readInputState" || args.iter().any(expr_uses_script_size) + } + StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), + StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), + StatementKind::Require { expr, .. } => expr_uses_script_size(expr), + StatementKind::If { condition, then_branch, else_branch } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - Statement::For { start, end, body, .. } => { + StatementKind::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - Statement::Yield { expr } => expr_uses_script_size(expr), - Statement::Return { exprs } => exprs.iter().any(expr_uses_script_size), - Statement::Console { args } => args.iter().any(|arg| match arg { + StatementKind::Yield { expr } => expr_uses_script_size(expr), + StatementKind::Return { exprs } => exprs.iter().any(expr_uses_script_size), + StatementKind::Console { args } => args.iter().any(|arg| match arg { crate::ast::ConsoleArg::Identifier(_) => false, crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), }), @@ -501,23 +503,23 @@ fn array_element_size_ref(type_ref: &TypeRef) -> Option { } fn contains_return(stmt: &Statement) -> bool { - match stmt { - Statement::Return { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + match &stmt.kind { + StatementKind::Return { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - Statement::For { body, .. } => body.iter().any(contains_return), + StatementKind::For { body, .. } => body.iter().any(contains_return), _ => false, } } fn contains_yield(stmt: &Statement) -> bool { - match stmt { - Statement::Yield { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + match &stmt.kind { + StatementKind::Yield { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - Statement::For { body, .. } => body.iter().any(contains_yield), + StatementKind::For { body, .. } => body.iter().any(contains_yield), _ => false, } } @@ -979,7 +981,7 @@ fn compile_function( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement::Return { .. })) { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -996,17 +998,17 @@ fn compile_function( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - if matches!(stmt, Statement::Return { .. }) { + if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - let Statement::Return { exprs } = stmt else { unreachable!() }; + let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; validate_return_types(exprs, &function.return_types, &types, constants)?; for expr in exprs { let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - recorder.record_statement_updates(None, start, builder.script().len(), Vec::new()); + recorder.record_statement_updates(stmt, start, builder.script().len(), Vec::new()); continue; } compile_statement( @@ -1027,7 +1029,7 @@ fn compile_function( &mut recorder, )?; let end = builder.script().len(); - recorder.record_statement_updates(None, start, end, Vec::new()); + recorder.record_statement_updates(stmt, start, end, Vec::new()); } let yield_count = yields.len(); @@ -1087,8 +1089,8 @@ fn compile_statement( script_size: Option, debug_recorder: &mut FunctionDebugRecorder, ) -> Result<(), CompilerError> { - match stmt { - Statement::VariableDefinition { type_ref, name, expr, .. } => { + match &stmt.kind { + StatementKind::VariableDefinition { type_ref, name, expr, .. } => { let type_name = type_name_from_ref(type_ref); let effective_type_name = if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { @@ -1177,7 +1179,7 @@ fn compile_statement( Ok(()) } } - Statement::ArrayPush { name, expr } => { + StatementKind::ArrayPush { name, expr } => { let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; if !is_array_type(array_type) { return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); @@ -1226,7 +1228,7 @@ fn compile_statement( env.insert(name.clone(), updated); Ok(()) } - Statement::Require { expr, .. } => { + StatementKind::Require { expr, .. } => { let mut stack_depth = 0i64; compile_expr( expr, @@ -1243,10 +1245,10 @@ fn compile_statement( builder.add_op(OpVerify)?; Ok(()) } - Statement::TimeOp { tx_var, expr, .. } => { + StatementKind::TimeOp { tx_var, expr, .. } => { compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size, contract_constants) } - Statement::If { condition, then_branch, else_branch } => compile_if_statement( + StatementKind::If { condition, then_branch, else_branch } => compile_if_statement( condition, then_branch, else_branch.as_deref(), @@ -1265,7 +1267,7 @@ fn compile_statement( script_size, debug_recorder, ), - Statement::For { ident, start, end, body } => compile_for_statement( + StatementKind::For { ident, start, end, body } => compile_for_statement( ident, start, end, @@ -1285,14 +1287,14 @@ fn compile_statement( script_size, debug_recorder, ), - Statement::Yield { expr } => { + StatementKind::Yield { expr } => { let mut visiting = HashSet::new(); let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; yields.push(resolved); Ok(()) } - Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), - Statement::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { + StatementKind::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), + StatementKind::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { Expr::Split { source, index, .. } => { env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); @@ -1300,7 +1302,7 @@ fn compile_statement( } _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), }, - Statement::FunctionCall { name, args } => { + StatementKind::FunctionCall { name, args } => { if name == "validateOutputState" { return compile_validate_output_state_statement( args, @@ -1350,7 +1352,7 @@ fn compile_statement( } Ok(()) } - Statement::StateFunctionCallAssign { bindings, name, args } => { + StatementKind::StateFunctionCallAssign { bindings, name, args } => { if name == "readInputState" { return compile_read_input_state_statement( bindings, @@ -1367,7 +1369,7 @@ fn compile_statement( name ))) } - Statement::FunctionCallAssign { bindings, name, args } => { + StatementKind::FunctionCallAssign { bindings, name, args } => { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; if function.return_types.is_empty() { return Err(CompilerError::Unsupported("function has no return types".to_string())); @@ -1405,7 +1407,7 @@ fn compile_statement( } Ok(()) } - Statement::Assign { name, expr } => { + StatementKind::Assign { name, expr } => { if let Some(type_name) = types.get(name) { if is_array_type(type_name) { match expr { @@ -1430,7 +1432,7 @@ fn compile_statement( env.insert(name.clone(), resolved); Ok(()) } - Statement::Console { .. } => Ok(()), + StatementKind::Console { .. } => Ok(()), } } @@ -1786,7 +1788,7 @@ fn compile_inline_call( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement::Return { .. })) { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1802,17 +1804,17 @@ fn compile_inline_call( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - if matches!(stmt, Statement::Return { .. }) { + if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - let Statement::Return { exprs } = stmt else { unreachable!() }; + let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; validate_return_types(exprs, &function.return_types, &types, contract_constants)?; for expr in exprs { let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - debug_recorder.record_statement_updates(None, start, builder.script().len(), Vec::new()); + debug_recorder.record_statement_updates(stmt, start, builder.script().len(), Vec::new()); continue; } compile_statement( @@ -1833,7 +1835,7 @@ fn compile_inline_call( debug_recorder, )?; let end = builder.script().len(); - debug_recorder.record_statement_updates(None, start, end, Vec::new()); + debug_recorder.record_statement_updates(stmt, start, end, Vec::new()); } for (name, value) in env.iter() { @@ -2024,7 +2026,7 @@ fn compile_block( debug_recorder, )?; let end = builder.script().len(); - debug_recorder.record_statement_updates(None, start, end, Vec::new()); + debug_recorder.record_statement_updates(stmt, start, end, Vec::new()); } Ok(()) } @@ -2221,7 +2223,6 @@ pub fn compile_debug_expr( Ok(builder.drain()) } -#[allow(dead_code)] pub(super) fn resolve_expr_for_debug( expr: Expr, env: &HashMap, diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index f731f079..524809c2 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; -use crate::ast::{Expr, FunctionAst, ParamAst}; +use crate::ast::{Expr, FunctionAst, ParamAst, Statement}; use crate::debug::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugVariableUpdate, SourceSpan, @@ -105,23 +105,48 @@ impl FunctionDebugRecorder { } } - pub fn record_statement(&mut self, span: Option, bytecode_start: usize, bytecode_len: usize) -> Option { + fn record_statement_span(&mut self, span: Option, bytecode_start: usize, bytecode_len: usize) -> Option { let kind = if bytecode_len == 0 { DebugEventKind::Virtual {} } else { DebugEventKind::Statement {} }; self.push_event(bytecode_start, bytecode_start + bytecode_len, span, kind) } + pub fn record_statement(&mut self, stmt: &Statement, bytecode_start: usize, bytecode_len: usize) -> Option { + self.record_statement_span(stmt.span, bytecode_start, bytecode_len) + } + + pub fn record_statement_with_span( + &mut self, + span: Option, + bytecode_start: usize, + bytecode_len: usize, + ) -> Option { + self.record_statement_span(span, bytecode_start, bytecode_len) + } + pub fn record_virtual_step(&mut self, span: Option, bytecode_offset: usize) -> Option { self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) } pub fn record_statement_updates( + &mut self, + stmt: &Statement, + bytecode_start: usize, + bytecode_end: usize, + variables: Vec<(String, String, Expr)>, + ) { + if let Some(sequence) = self.record_statement(stmt, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + self.record_variable_updates(variables, bytecode_end, stmt.span, sequence); + } + } + + pub fn record_statement_updates_with_span( &mut self, span: Option, bytecode_start: usize, bytecode_end: usize, variables: Vec<(String, String, Expr)>, ) { - if let Some(sequence) = self.record_statement(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + if let Some(sequence) = self.record_statement_with_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { self.record_variable_updates(variables, bytecode_end, span, sequence); } } diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index f1b8630a..2aa23b53 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -1,4 +1,5 @@ use crate::ast::Expr; +use pest::Span; use serde::{Deserialize, Serialize}; pub mod session; @@ -11,6 +12,14 @@ pub struct SourceSpan { pub end_col: u32, } +impl<'a> From> for SourceSpan { + fn from(span: Span<'a>) -> Self { + let (line, col) = span.start_pos().line_col(); + let (end_line, end_col) = span.end_pos().line_col(); + Self { line: line as u32, col: col as u32, end_line: end_line as u32, end_col: end_col as u32 } + } +} + pub mod labels { pub mod synthetic { /// Checks which function was selected (DUP, PUSH index, NUMEQUAL, IF, DROP). diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 228af9b8..d69b0fe7 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -332,8 +332,8 @@ impl<'a> DebugSession<'a> { pub fn stacks_snapshot(&self) -> StackSnapshot { let stacks = self.engine.stacks(); StackSnapshot { - dstack: stacks.dstack.iter().map(hex::encode).collect(), - astack: stacks.astack.iter().map(hex::encode).collect(), + dstack: stacks.dstack.iter().map(|item| encode_hex(item)).collect(), + astack: stacks.astack.iter().map(|item| encode_hex(item)).collect(), } } @@ -546,10 +546,10 @@ impl<'a> DebugSession<'a> { (_, DebugValue::Bytes(bytes)) if element_type.is_some() => { let element_type = element_type.expect("checked"); let Some(element_size) = array_element_size(element_type) else { - return format!("0x{}", hex::encode(bytes)); + return format!("0x{}", encode_hex(bytes)); }; if element_size == 0 || bytes.len() % element_size != 0 { - return format!("0x{}", hex::encode(bytes)); + return format!("0x{}", encode_hex(bytes)); } let mut values: Vec = Vec::new(); @@ -563,7 +563,7 @@ impl<'a> DebugSession<'a> { } format!("[{}]", values.join(", ")) } - (_, DebugValue::Bytes(bytes)) => format!("0x{}", hex::encode(bytes)), + (_, DebugValue::Bytes(bytes)) => format!("0x{}", encode_hex(bytes)), (_, DebugValue::Int(number)) => number.to_string(), (_, DebugValue::Bool(value)) => value.to_string(), (_, DebugValue::String(value)) => value.clone(), @@ -702,7 +702,7 @@ impl<'a> DebugSession<'a> { /// Returns the current main stack as hex-encoded strings. pub fn stack(&self) -> Vec { let stacks = self.engine.stacks(); - stacks.dstack.iter().map(hex::encode).collect() + stacks.dstack.iter().map(|item| encode_hex(item)).collect() } fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate) -> Result { @@ -885,6 +885,14 @@ fn mapping_matches_offset(mapping: &DebugMapping, offset: usize) -> bool { } } +fn encode_hex(bytes: &[u8]) -> String { + let mut out = vec![0u8; bytes.len() * 2]; + if faster_hex::hex_encode(bytes, &mut out).is_err() { + return String::new(); + } + String::from_utf8(out).unwrap_or_default() +} + #[cfg(test)] mod tests { use super::*; diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index 1fef2c33..e0606afc 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, Statement, parse_contract_ast}; +use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; fn extract_first_expr(source: &str) -> Expr { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; - match statement { - Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - Statement::Require { expr, .. } => expr.clone(), + match &statement.kind { + StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + StatementKind::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 5aee58b3..642a5741 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -7,8 +7,9 @@ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_txscript::caches::Cache; use kaspa_txscript::{EngineCtx, EngineFlags}; -use silverscript_lang::ast::Expr; +use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; +use silverscript_lang::debug::MappingKind; use silverscript_lang::debug::session::DebugSession; fn example_contract_path() -> PathBuf { @@ -16,56 +17,217 @@ fn example_contract_path() -> PathBuf { manifest_dir.join("tests/examples/if_statement.sil") } +// Convenience harness for the canonical example contract used by baseline session tests. fn with_session(mut f: F) -> Result<(), Box> where F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, { let contract_path = example_contract_path(); + assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); + let source = fs::read_to_string(&contract_path)?; + with_session_for_source( + &source, + vec![Expr::Int(3), Expr::Int(10)], + "hello", + vec![Expr::Int(5), Expr::Int(5)], + &mut f, + ) +} +// Generic harness that compiles a contract and boots a debugger session for a selected function call. +fn with_session_for_source( + source: &str, + ctor_args: Vec, + function_name: &str, + function_args: Vec, + mut f: F, +) -> Result<(), Box> +where + F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, +{ + let parsed_contract = parse_contract_ast(source)?; + assert_eq!(parsed_contract.params.len(), ctor_args.len()); + + // Compile with debug metadata enabled so line mappings and variable updates are available. let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; - let compiled = compile_contract(&source, &[Expr::Int(3), Expr::Int(10)], compile_opts)?; + let compiled = compile_contract(source, &ctor_args, compile_opts)?; + let debug_info = compiled.debug_info.clone(); let sig_cache = Cache::new(10_000); let reused_values = SigHashReusedValuesUnsync::new(); let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); + let flags = EngineFlags { covenants_enabled: true }; let engine = silverscript_lang::debug::session::DebugEngine::new(ctx, flags); - let sigscript = compiled.build_sig_script("hello", vec![Expr::Int(5), Expr::Int(5)])?; - let mut session = DebugSession::full(&sigscript, &compiled.script, &source, compiled.debug_info.clone(), engine)?; + let entry = compiled + .abi + .iter() + .find(|entry| entry.name == function_name) + .ok_or_else(|| format!("function '{function_name}' not found"))?; + + assert_eq!(entry.inputs.len(), function_args.len()); + + // Seed stack with sigscript args and then execute the lockscript in debug mode. + let sigscript = compiled.build_sig_script(function_name, function_args)?; + let mut session = DebugSession::full(&sigscript, &compiled.script, source, debug_info, engine)?; f(&mut session) } #[test] -fn debug_session_lists_entrypoint_params() -> Result<(), Box> { +fn debug_session_provides_source_context_and_vars() -> Result<(), Box> { with_session(|session| { + // Skip dispatcher setup and land on first user statement. session.run_to_first_executed_statement()?; + let context = session.source_context(); + assert!(context.is_some(), "expected source context"); + let vars = session.list_variables().expect("variables available"); let names = vars.iter().map(|var| var.name.as_str()).collect::>(); - assert!(names.contains("a")); - assert!(names.contains("b")); + assert!(names.contains("a"), "expected param 'a' in variables"); + assert!(names.contains("b"), "expected param 'b' in variables"); + Ok(()) }) } #[test] -fn debug_session_can_step_mappings() -> Result<(), Box> { +fn debug_session_steps_forward() -> Result<(), Box> { with_session(|session| { session.run_to_first_executed_statement()?; - let stepped = session.step_statement()?; - assert!(stepped.is_some(), "expected at least one statement step"); + let before = session.state().pc; + let before_span = session.current_span(); + session.step_statement()?; + let after = session.state().pc; + let after_span = session.current_span(); + assert!(after > before || after_span != before_span, "expected statement step to make source progress"); Ok(()) }) } #[test] -fn debug_session_breakpoint_requires_source_spans() -> Result<(), Box> { +fn debug_session_breakpoint_management() -> Result<(), Box> { with_session(|session| { session.run_to_first_executed_statement()?; - assert!(!session.add_breakpoint(7), "line breakpoints should be rejected without span mappings"); - assert!(session.breakpoints().is_empty()); + let span = session.current_span().ok_or("no current span")?; + let line = span.line; + + session.add_breakpoint(line); + assert!(session.breakpoints().contains(&line)); + + session.clear_breakpoint(line); + assert!(!session.breakpoints().contains(&line)); + Ok(()) + }) +} + +#[test] +fn debug_session_hits_multiline_breakpoints() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract BP() { + entrypoint function main(int a) { + require(a == 1); + require(a == 1); + require( + a == 1 + ); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(1)], |session| { + session.run_to_first_executed_statement()?; + // Line 8 is inside a multiline `require(...)` span and should still be hit. + assert!(session.add_breakpoint(8), "expected breakpoint line to be valid"); + + let hit = session.continue_to_breakpoint()?; + assert!(hit.is_some(), "expected to stop at multiline statement breakpoint"); + + let span = session.current_span().ok_or("expected source span at breakpoint")?; + assert!((span.line..=span.end_line).contains(&8)); + Ok(()) + }) +} + +#[test] +fn debug_session_dedupes_shadowed_constructor_constants() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Shadow(int x) { + entrypoint function main(int x) { + require(x == x); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(7)], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + + // Function param `x` should shadow constructor constant `x` in visible debugger variables. + let vars = session.list_variables()?; + let x_count = vars.iter().filter(|var| var.name == "x").count(); + assert_eq!(x_count, 1, "expected a single visible x variable"); + + let x = session.variable_by_name("x")?; + assert!(!x.is_constant, "function parameter should shadow constructor constant"); + assert_eq!(session.format_value(&x.type_name, &x.value), "3"); + Ok(()) + }) +} + +#[test] +fn debug_session_exposes_virtual_steps() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Virtuals() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + let first = session.current_location().ok_or("missing first location")?; + assert!(matches!(first.kind, MappingKind::Virtual {})); + let first_pc = session.state().pc; + + let second = session.step_over()?.ok_or("missing second step")?.mapping.ok_or("missing second mapping")?; + assert!(matches!(second.kind, MappingKind::Virtual {})); + assert_eq!(session.state().pc, first_pc, "virtual step should not execute opcodes"); + + let third = session.step_over()?.ok_or("missing third step")?.mapping.ok_or("missing third mapping")?; + assert!(matches!(third.kind, MappingKind::Statement {})); + assert_eq!(session.state().pc, first_pc, "first real statement should still be at same pc boundary"); + Ok(()) + }) +} + +#[test] +fn debug_session_breakpoint_hits_virtual_line() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract VirtualBp() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + assert!(session.add_breakpoint(6), "line with virtual assignment should be a valid breakpoint"); + let hit = session.continue_to_breakpoint()?; + assert!(hit.is_some(), "expected breakpoint on virtual line"); + let span = session.current_span().ok_or("missing span at virtual breakpoint")?; + assert_eq!(span.line, 6); Ok(()) }) } diff --git a/silverscript-lang/tests/debugger_cli_tests.rs b/silverscript-lang/tests/debugger_cli_tests.rs index e0cd2021..ba7d99ab 100644 --- a/silverscript-lang/tests/debugger_cli_tests.rs +++ b/silverscript-lang/tests/debugger_cli_tests.rs @@ -45,6 +45,6 @@ fn sil_debug_repl_all_commands_smoke() { assert!(stdout.contains("Commands:"), "missing help output"); assert!(stdout.contains("Stack:"), "missing stack output"); assert!(stdout.contains("no statement at line 1"), "missing invalid breakpoint warning"); - assert!(stdout.contains("no statement at line 7"), "missing line-7 breakpoint warning"); - assert!(stdout.contains("No breakpoints set."), "missing breakpoint listing"); + assert!(stdout.contains("Breakpoint set at line 7"), "missing line-7 breakpoint success"); + assert!(stdout.contains("Breakpoints: 7"), "missing breakpoint listing"); } From 70115b276f57c06ad6b48cd99f9749c1d20c94f4 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:09:44 +0200 Subject: [PATCH 10/41] Fix inline call param resolution and improve debug event tracking --- silverscript-lang/src/compiler.rs | 90 +++++++++++++++++-- .../src/compiler/debug_recording.rs | 32 +++++-- silverscript-lang/src/debug/session.rs | 39 ++++++-- .../tests/debug_session_tests.rs | 88 ++++++++++++++++++ 4 files changed, 230 insertions(+), 19 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 89058032..b5fbdb87 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -9,7 +9,7 @@ use crate::ast::{ ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; -use crate::debug::DebugInfo; +use crate::debug::{DebugInfo, SourceSpan}; use crate::debug::labels::synthetic; use crate::parser::Rule; use chrono::NaiveDateTime; @@ -524,6 +524,39 @@ fn contains_yield(stmt: &Statement) -> bool { } } +fn collect_debug_variable_updates( + before_env: &HashMap, + after_env: &HashMap, + types: &HashMap, + recorder: &FunctionDebugRecorder, +) -> Result, CompilerError> { + if !recorder.is_enabled() { + return Ok(Vec::new()); + } + + let mut names: Vec = after_env.keys().cloned().collect(); + names.sort_unstable(); + + let mut updates = Vec::new(); + for name in names { + if name.starts_with("__arg_") { + continue; + } + let Some(after_expr) = after_env.get(&name) else { + continue; + }; + if before_env.get(&name).is_some_and(|before_expr| before_expr == after_expr) { + continue; + } + let Some(type_name) = types.get(&name) else { + continue; + }; + recorder.variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; + } + + Ok(updates) +} + fn validate_return_types( exprs: &[Expr], return_types: &[TypeRef], @@ -998,6 +1031,7 @@ fn compile_function( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); + let env_before = recorder.is_enabled().then(|| env.clone()); if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1008,7 +1042,12 @@ fn compile_function( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - recorder.record_statement_updates(stmt, start, builder.script().len(), Vec::new()); + let updates = if let Some(before_env) = env_before.as_ref() { + collect_debug_variable_updates(before_env, &env, &types, &recorder)? + } else { + Vec::new() + }; + recorder.record_statement_updates(stmt, start, builder.script().len(), updates); continue; } compile_statement( @@ -1029,7 +1068,12 @@ fn compile_function( &mut recorder, )?; let end = builder.script().len(); - recorder.record_statement_updates(stmt, start, end, Vec::new()); + let updates = if let Some(before_env) = env_before.as_ref() { + collect_debug_variable_updates(before_env, &env, &types, &recorder)? + } else { + Vec::new() + }; + recorder.record_statement_updates(stmt, start, end, updates); } let yield_count = yields.len(); @@ -1320,6 +1364,8 @@ fn compile_statement( let returns = compile_inline_call( name, args, + stmt.span, + params, types, env, builder, @@ -1387,6 +1433,8 @@ fn compile_statement( let returns = compile_inline_call( name, args, + stmt.span, + params, types, env, builder, @@ -1729,6 +1777,8 @@ fn compile_validate_output_state_statement( fn compile_inline_call( name: &str, args: &[Expr], + call_span: Option, + caller_params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap, builder: &mut ScriptBuilder, @@ -1799,11 +1849,16 @@ fn compile_inline_call( } } + let call_start = builder.script().len(); + debug_recorder.record_inline_call_enter(call_span, call_start, name); + let mut inline_recorder = debug_recorder.new_inline_child(); + let mut yields: Vec = Vec::new(); - let params = HashMap::new(); + let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); + let env_before = inline_recorder.is_enabled().then(|| env.clone()); if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1814,7 +1869,12 @@ fn compile_inline_call( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - debug_recorder.record_statement_updates(stmt, start, builder.script().len(), Vec::new()); + let updates = if let Some(before_env) = env_before.as_ref() { + collect_debug_variable_updates(before_env, &env, &types, &inline_recorder)? + } else { + Vec::new() + }; + inline_recorder.record_statement_updates(stmt, start, builder.script().len(), updates); continue; } compile_statement( @@ -1832,12 +1892,20 @@ fn compile_inline_call( callee_index, &mut yields, script_size, - debug_recorder, + &mut inline_recorder, )?; let end = builder.script().len(); - debug_recorder.record_statement_updates(stmt, start, end, Vec::new()); + let updates = if let Some(before_env) = env_before.as_ref() { + collect_debug_variable_updates(before_env, &env, &types, &inline_recorder)? + } else { + Vec::new() + }; + inline_recorder.record_statement_updates(stmt, start, end, updates); } + debug_recorder.merge_inline_events(&inline_recorder); + debug_recorder.record_inline_call_exit(call_span, builder.script().len(), name); + for (name, value) in env.iter() { if name.starts_with("__arg_") { if let Some(type_name) = types.get(name) { @@ -2008,6 +2076,7 @@ fn compile_block( ) -> Result<(), CompilerError> { for stmt in statements { let start = builder.script().len(); + let env_before = debug_recorder.is_enabled().then(|| env.clone()); compile_statement( stmt, env, @@ -2026,7 +2095,12 @@ fn compile_block( debug_recorder, )?; let end = builder.script().len(); - debug_recorder.record_statement_updates(stmt, start, end, Vec::new()); + let updates = if let Some(before_env) = env_before.as_ref() { + collect_debug_variable_updates(before_env, env, types, debug_recorder)? + } else { + Vec::new() + }; + debug_recorder.record_statement_updates(stmt, start, end, updates); } Ok(()) } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 524809c2..b2369477 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -35,30 +35,50 @@ pub struct FunctionDebugRecorder { next_seq: u32, call_depth: u32, frame_id: u32, + next_frame_id: u32, } #[allow(dead_code)] impl FunctionDebugRecorder { pub fn new(enabled: bool, function: &FunctionAst) -> Self { - let mut recorder = Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, ..Default::default() }; + let mut recorder = Self { + function_name: function.name.clone(), + enabled, + call_depth: 0, + frame_id: 0, + next_frame_id: 1, + ..Default::default() + }; recorder.record_params(function); recorder } - pub fn inline(enabled: bool, function_name: String, call_depth: u32, frame_id: u32) -> Self { - Self { function_name, enabled, call_depth, frame_id, ..Default::default() } + pub fn inline(enabled: bool, function_name: String, call_depth: u32, frame_id: u32, next_frame_id: u32) -> Self { + Self { function_name, enabled, call_depth, frame_id, next_frame_id, ..Default::default() } } pub fn sequence_count(&self) -> u32 { self.next_seq } + pub fn is_enabled(&self) -> bool { + self.enabled + } + pub fn call_depth(&self) -> u32 { self.call_depth } - pub fn new_inline_child(&self, frame_id: u32) -> Self { - Self::inline(self.enabled, self.function_name.clone(), self.call_depth().saturating_add(1), frame_id) + pub fn new_inline_child(&mut self) -> Self { + let frame_id = self.next_frame_id; + self.next_frame_id = self.next_frame_id.saturating_add(1); + Self::inline( + self.enabled, + self.function_name.clone(), + self.call_depth().saturating_add(1), + frame_id, + self.next_frame_id, + ) } fn next_sequence(&mut self) -> u32 { @@ -196,6 +216,7 @@ impl FunctionDebugRecorder { pub fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { if !self.enabled || inline.events.is_empty() { + self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); return; } let mut seq_map: HashMap = HashMap::new(); @@ -218,6 +239,7 @@ impl FunctionDebugRecorder { self.variable_updates.push(update); } } + self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); } pub(super) fn record_variable_updates( diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index d69b0fe7..3ae4b6d1 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -100,6 +100,7 @@ pub struct DebugSession<'a> { uses_sequence_order: bool, source_lines: Vec, breakpoints: HashSet, + executed_sequences: HashSet, } struct ShadowParamValue { @@ -166,11 +167,15 @@ impl<'a> DebugSession<'a> { }) .cloned() .collect(); - if uses_sequence_order { - source_mappings.sort_by_key(|mapping| (mapping.sequence, mapping.bytecode_start, mapping.bytecode_end)); - } else { - source_mappings.sort_by_key(|mapping| (mapping.bytecode_start, mapping.bytecode_end)); - } + source_mappings.sort_by_key(|mapping| { + ( + mapping.bytecode_start, + mapping.bytecode_end, + mapping_kind_order(&mapping.kind), + mapping.call_depth, + if uses_sequence_order { mapping.sequence } else { 0 }, + ) + }); Ok(Self { engine, @@ -185,6 +190,7 @@ impl<'a> DebugSession<'a> { uses_sequence_order, source_lines, breakpoints: HashSet::new(), + executed_sequences: HashSet::new(), }) } @@ -241,6 +247,7 @@ impl<'a> DebugSession<'a> { if self.advance_to_mapping(target_index)? { self.current_step_index = Some(target_index); + self.mark_mapping_executed(target_index); return Ok(Some(self.state())); } @@ -289,6 +296,7 @@ impl<'a> DebugSession<'a> { .find(|(_, mapping)| self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset)); if let Some((index, _)) = found { self.current_step_index = Some(index); + self.mark_mapping_executed(index); return Ok(()); } } @@ -629,7 +637,10 @@ impl<'a> DebugSession<'a> { return false; } if self.uses_sequence_order { - update.frame_id == frame_id && update.sequence <= sequence + update.frame_id == frame_id + && self.executed_sequences.contains(&update.sequence) + && update.sequence < sequence + && update.bytecode_offset <= offset } else { update.bytecode_offset <= offset } @@ -677,6 +688,12 @@ impl<'a> DebugSession<'a> { self.current_step_mapping().map(|mapping| (mapping.sequence, mapping.frame_id)).unwrap_or((0, 0)) } + fn mark_mapping_executed(&mut self, mapping_index: usize) { + if let Some(mapping) = self.source_mappings.get(mapping_index) { + self.executed_sequences.insert(mapping.sequence); + } + } + fn is_steppable_mapping(&self, mapping: &DebugMapping) -> bool { matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {}) } @@ -877,6 +894,16 @@ fn build_opcode_offsets(opcodes: &[Option>]) -> (Vec, usi (offsets, offset) } +fn mapping_kind_order(kind: &MappingKind) -> u8 { + match kind { + MappingKind::InlineCallEnter { .. } => 0, + MappingKind::Virtual {} => 1, + MappingKind::Statement {} => 2, + MappingKind::InlineCallExit { .. } => 3, + MappingKind::Synthetic { .. } => 4, + } +} + fn mapping_matches_offset(mapping: &DebugMapping, offset: usize) -> bool { if mapping.bytecode_start == mapping.bytecode_end { offset == mapping.bytecode_start diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 642a5741..5eff2022 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -231,3 +231,91 @@ contract VirtualBp() { Ok(()) }) } + +#[test] +fn debug_session_tracks_local_variable_updates() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract LocalVars() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + assert!(session.variable_by_name("x").is_err(), "x should not exist before its statement executes"); + + session.step_over()?; + let x_after_init = session.variable_by_name("x")?; + assert_eq!(session.format_value(&x_after_init.type_name, &x_after_init.value), "4"); + + session.step_over()?; + let x_after_assign = session.variable_by_name("x")?; + assert_eq!(session.format_value(&x_after_assign.type_name, &x_after_assign.value), "6"); + Ok(()) + }) +} + +#[test] +fn debug_session_hits_if_header_breakpoint() -> Result<(), Box> { + with_session(|session| { + session.run_to_first_executed_statement()?; + assert!(session.add_breakpoint(7), "expected if-header line to accept breakpoints"); + + let hit = session.continue_to_breakpoint()?; + assert!(hit.is_some(), "expected to stop at if-header breakpoint"); + + let span = session.current_span().ok_or("missing span at breakpoint")?; + assert!((span.line..=span.end_line).contains(&7), "breakpoint should resolve to line 7 span"); + Ok(()) + }) +} + +#[test] +fn debug_session_step_over_and_out_handle_inline_calls() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract InlineCalls() { + function addOne(int x) : (int) { + int y = x + 1; + return(y); + } + + entrypoint function main(int a) { + (int b) = addOne(a); + require(b == a + 1); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + let start = session.current_span().ok_or("missing start span")?; + assert_eq!(start.line, 10); + + session.step_over()?; + let after_over = session.current_span().ok_or("missing span after step_over")?; + assert_eq!(after_over.line, 11, "step_over should stay in caller and move past inline call"); + Ok(()) + })?; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + session.step_into()?; + let in_callee = session.current_span().ok_or("missing span in callee")?; + assert_eq!(in_callee.line, 5, "step_into should enter callee body"); + assert_eq!(session.call_stack(), vec!["addOne".to_string()]); + + session.step_out()?; + let after_out = session.current_span().ok_or("missing span after step_out")?; + assert_eq!(after_out.line, 11, "step_out should return to caller after inline call"); + assert!(session.call_stack().is_empty(), "call stack should unwind after step_out"); + Ok(()) + })?; + + Ok(()) +} From a21a38a4985ea46645d07cc69404128695b38193 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:42:56 +0200 Subject: [PATCH 11/41] Finalize debugger inline resolution and cleanup --- silverscript-lang/src/compiler.rs | 73 ++++++++++++----- .../src/compiler/debug_recording.rs | 82 ++----------------- .../tests/debug_session_tests.rs | 10 +-- 3 files changed, 62 insertions(+), 103 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index b5fbdb87..d4a268d2 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -9,8 +9,8 @@ use crate::ast::{ ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; -use crate::debug::{DebugInfo, SourceSpan}; use crate::debug::labels::synthetic; +use crate::debug::{DebugInfo, SourceSpan}; use crate::parser::Rule; use chrono::NaiveDateTime; @@ -1821,6 +1821,9 @@ fn compile_inline_call( let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; let temp_name = format!("__arg_{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); + // Inline calls bind each callee parameter to a synthetic identifier so + // callee expressions keep a stable name while still pointing at the + // caller-provided argument expression. env.insert(temp_name.clone(), resolved.clone()); types.insert(temp_name.clone(), param_type_name.clone()); env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); @@ -1854,6 +1857,8 @@ fn compile_inline_call( let mut inline_recorder = debug_recorder.new_inline_child(); let mut yields: Vec = Vec::new(); + // Use caller parameter stack indexes while compiling callee bytecode so + // identifier resolution can still pick values from the caller frame. let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { @@ -2203,71 +2208,101 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { + resolve_expr_internal(expr, env, visiting, ResolveMode::Compile) +} + +/// Controls identifier expansion semantics for shared expression resolution. +/// Compile mode preserves synthetic inline arg placeholders (`__arg_*`) so +/// generated scripts still pick arguments from the caller stack frame. Debug +/// mode expands them into caller-visible expressions for shadow evaluation. +#[derive(Clone, Copy)] +enum ResolveMode { + Compile, + Debug, +} + +impl ResolveMode { + fn preserve_inline_args(self) -> bool { + matches!(self, Self::Compile) + } +} + +fn resolve_expr_internal( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, + mode: ResolveMode, +) -> Result { match expr { Expr::Identifier(name) => { - if name.starts_with("__arg_") { + if mode.preserve_inline_args() && name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr(value.clone(), env, visiting)?; + let resolved = resolve_expr_internal(value.clone(), env, visiting, mode)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::Identifier(name)) } } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr_internal(*expr, env, visiting, mode)?) }), Expr::Binary { op, left, right } => Ok(Expr::Binary { op, - left: Box::new(resolve_expr(*left, env, visiting)?), - right: Box::new(resolve_expr(*right, env, visiting)?), + left: Box::new(resolve_expr_internal(*left, env, visiting, mode)?), + right: Box::new(resolve_expr_internal(*right, env, visiting, mode)?), }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr(*condition, env, visiting)?), - then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), - else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + condition: Box::new(resolve_expr_internal(*condition, env, visiting, mode)?), + then_expr: Box::new(resolve_expr_internal(*then_expr, env, visiting, mode)?), + else_expr: Box::new(resolve_expr_internal(*else_expr, env, visiting, mode)?), }), Expr::Array(values) => { let mut resolved = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr(value, env, visiting)?); + resolved.push(resolve_expr_internal(value, env, visiting, mode)?); } Ok(Expr::Array(resolved)) } Expr::StateObject(fields) => { let mut resolved_fields = Vec::with_capacity(fields.len()); for field in fields { - resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); + resolved_fields.push(crate::ast::StateFieldExpr { + name: field.name, + expr: resolve_expr_internal(field.expr, env, visiting, mode)?, + }); } Ok(Expr::StateObject(resolved_fields)) } Expr::Call { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + resolved.push(resolve_expr_internal(arg, env, visiting, mode)?); } Ok(Expr::Call { name, args: resolved }) } Expr::New { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + resolved.push(resolve_expr_internal(arg, env, visiting, mode)?); } Ok(Expr::New { name, args: resolved }) } Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), + source: Box::new(resolve_expr_internal(*source, env, visiting, mode)?), + index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?), part, }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), + source: Box::new(resolve_expr_internal(*source, env, visiting, mode)?), + index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?), }), - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), + Expr::Introspection { kind, index } => { + Ok(Expr::Introspection { kind, index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?) }) + } other => Ok(other), } } @@ -2302,7 +2337,7 @@ pub(super) fn resolve_expr_for_debug( env: &HashMap, visiting: &mut HashSet, ) -> Result { - resolve_expr(expr, env, visiting) + resolve_expr_internal(expr, env, visiting, ResolveMode::Debug) } fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index b2369477..08f1207b 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -38,17 +38,10 @@ pub struct FunctionDebugRecorder { next_frame_id: u32, } -#[allow(dead_code)] impl FunctionDebugRecorder { pub fn new(enabled: bool, function: &FunctionAst) -> Self { - let mut recorder = Self { - function_name: function.name.clone(), - enabled, - call_depth: 0, - frame_id: 0, - next_frame_id: 1, - ..Default::default() - }; + let mut recorder = + Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; recorder.record_params(function); recorder } @@ -72,13 +65,7 @@ impl FunctionDebugRecorder { pub fn new_inline_child(&mut self) -> Self { let frame_id = self.next_frame_id; self.next_frame_id = self.next_frame_id.saturating_add(1); - Self::inline( - self.enabled, - self.function_name.clone(), - self.call_depth().saturating_add(1), - frame_id, - self.next_frame_id, - ) + Self::inline(self.enabled, self.function_name.clone(), self.call_depth().saturating_add(1), frame_id, self.next_frame_id) } fn next_sequence(&mut self) -> u32 { @@ -134,19 +121,6 @@ impl FunctionDebugRecorder { self.record_statement_span(stmt.span, bytecode_start, bytecode_len) } - pub fn record_statement_with_span( - &mut self, - span: Option, - bytecode_start: usize, - bytecode_len: usize, - ) -> Option { - self.record_statement_span(span, bytecode_start, bytecode_len) - } - - pub fn record_virtual_step(&mut self, span: Option, bytecode_offset: usize) -> Option { - self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) - } - pub fn record_statement_updates( &mut self, stmt: &Statement, @@ -159,53 +133,6 @@ impl FunctionDebugRecorder { } } - pub fn record_statement_updates_with_span( - &mut self, - span: Option, - bytecode_start: usize, - bytecode_end: usize, - variables: Vec<(String, String, Expr)>, - ) { - if let Some(sequence) = self.record_statement_with_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { - self.record_variable_updates(variables, bytecode_end, span, sequence); - } - } - - pub fn record_virtual_updates( - &mut self, - span: Option, - bytecode_offset: usize, - variables: Vec<(String, String, Expr)>, - ) { - if let Some(sequence) = self.record_virtual_step(span, bytecode_offset) { - self.record_variable_updates(variables, bytecode_offset, span, sequence); - } - } - - pub fn record_inline_param_updates( - &mut self, - function: &FunctionAst, - env: &HashMap, - span: Option, - bytecode_offset: usize, - ) -> Result<(), CompilerError> { - if !self.enabled { - return Ok(()); - } - let mut variables = Vec::with_capacity(function.params.len()); - for param in &function.params { - self.variable_update( - env, - &mut variables, - ¶m.name, - ¶m.type_ref.type_name(), - env.get(¶m.name).cloned().unwrap_or(Expr::Identifier(param.name.clone())), - )?; - } - self.record_virtual_updates(span, bytecode_offset, variables); - Ok(()) - } - pub fn record_inline_call_enter(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Option { self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallEnter { callee: callee.to_string() }) } @@ -267,7 +194,8 @@ impl FunctionDebugRecorder { } /// Records a variable update by resolving its expression against the current environment. - /// This expands all local variable references inline, leaving only param identifiers. + /// This expands locals and synthetic inline placeholders (`__arg_*`) into + /// caller-visible expressions, leaving only real param identifiers. /// The resolved expression is what enables shadow VM evaluation at debug time. pub(super) fn variable_update( &self, diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 5eff2022..c6e6640f 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -26,13 +26,7 @@ where assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); let source = fs::read_to_string(&contract_path)?; - with_session_for_source( - &source, - vec![Expr::Int(3), Expr::Int(10)], - "hello", - vec![Expr::Int(5), Expr::Int(5)], - &mut f, - ) + with_session_for_source(&source, vec![Expr::Int(3), Expr::Int(10)], "hello", vec![Expr::Int(5), Expr::Int(5)], &mut f) } // Generic harness that compiles a contract and boots a debugger session for a selected function call. @@ -300,6 +294,8 @@ contract InlineCalls() { session.step_over()?; let after_over = session.current_span().ok_or("missing span after step_over")?; assert_eq!(after_over.line, 11, "step_over should stay in caller and move past inline call"); + let b = session.variable_by_name("b")?; + assert_eq!(session.format_value(&b.type_name, &b.value), "4", "inline return should resolve against caller params"); Ok(()) })?; From fa6ee6c69b1c01ac70e7ae7fe4a1e682feecc5d9 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:57:35 +0200 Subject: [PATCH 12/41] Fix param shadowing over constructor constants --- silverscript-lang/src/compiler.rs | 6 ++++ .../tests/debug_session_tests.rs | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index d4a268d2..a5a4df78 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1000,6 +1000,12 @@ fn compile_function( } } let mut env: HashMap = constants.clone(); + // Function parameters shadow constructor constants with the same name. + // Keep constants in env for non-shadowed names, but drop colliding entries + // so identifier resolution inside this function picks stack params. + for param in &function.params { + env.remove(¶m.name); + } let mut builder = ScriptBuilder::new(); let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function); let mut yields: Vec = Vec::new(); diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index c6e6640f..98fc12d5 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -172,6 +172,37 @@ contract Shadow(int x) { }) } +#[test] +fn debug_session_prefers_function_param_value_over_shadowed_constructor_constant() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract ShadowMath(int fee) { + entrypoint function main(int fee) { + int local = fee + 1; + local = local + fee; + require(local > 0); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + + session.step_over()?; + let local_after_init = session.variable_by_name("local")?; + assert_eq!(session.format_value(&local_after_init.type_name, &local_after_init.value), "4"); + + session.step_over()?; + let local_after_update = session.variable_by_name("local")?; + assert_eq!(session.format_value(&local_after_update.type_name, &local_after_update.value), "7"); + + let fee = session.variable_by_name("fee")?; + assert!(!fee.is_constant); + assert_eq!(session.format_value(&fee.type_name, &fee.value), "3"); + Ok(()) + }) +} + #[test] fn debug_session_exposes_virtual_steps() -> Result<(), Box> { let source = r#"pragma silverscript ^0.1.0; From 9f6f6b6e761c7360e346697b0b927ab4c235d06f Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:28:35 +0200 Subject: [PATCH 13/41] Refine debugger recorder flow and simplify compiler call sites --- silverscript-lang/src/compiler.rs | 78 ++--------- .../src/compiler/debug_recording.rs | 121 ++++++++++++++---- 2 files changed, 105 insertions(+), 94 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index a5a4df78..bdaee31f 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -524,39 +524,6 @@ fn contains_yield(stmt: &Statement) -> bool { } } -fn collect_debug_variable_updates( - before_env: &HashMap, - after_env: &HashMap, - types: &HashMap, - recorder: &FunctionDebugRecorder, -) -> Result, CompilerError> { - if !recorder.is_enabled() { - return Ok(Vec::new()); - } - - let mut names: Vec = after_env.keys().cloned().collect(); - names.sort_unstable(); - - let mut updates = Vec::new(); - for name in names { - if name.starts_with("__arg_") { - continue; - } - let Some(after_expr) = after_env.get(&name) else { - continue; - }; - if before_env.get(&name).is_some_and(|before_expr| before_expr == after_expr) { - continue; - } - let Some(type_name) = types.get(&name) else { - continue; - }; - recorder.variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; - } - - Ok(updates) -} - fn validate_return_types( exprs: &[Expr], return_types: &[TypeRef], @@ -1037,6 +1004,7 @@ fn compile_function( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = recorder.is_enabled().then(|| env.clone()); if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { @@ -1048,12 +1016,7 @@ fn compile_function( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - let updates = if let Some(before_env) = env_before.as_ref() { - collect_debug_variable_updates(before_env, &env, &types, &recorder)? - } else { - Vec::new() - }; - recorder.record_statement_updates(stmt, start, builder.script().len(), updates); + recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; continue; } compile_statement( @@ -1074,12 +1037,7 @@ fn compile_function( &mut recorder, )?; let end = builder.script().len(); - let updates = if let Some(before_env) = env_before.as_ref() { - collect_debug_variable_updates(before_env, &env, &types, &recorder)? - } else { - Vec::new() - }; - recorder.record_statement_updates(stmt, start, end, updates); + recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } let yield_count = yields.len(); @@ -1859,8 +1817,8 @@ fn compile_inline_call( } let call_start = builder.script().len(); - debug_recorder.record_inline_call_enter(call_span, call_start, name); - let mut inline_recorder = debug_recorder.new_inline_child(); + // Record call boundary on caller frame and collect callee events in a child frame. + let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); let mut yields: Vec = Vec::new(); // Use caller parameter stack indexes while compiling callee bytecode so @@ -1869,6 +1827,7 @@ fn compile_inline_call( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = inline_recorder.is_enabled().then(|| env.clone()); if matches!(stmt.kind, StatementKind::Return { .. }) { if index != body_len - 1 { @@ -1880,12 +1839,7 @@ fn compile_inline_call( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; yields.push(resolved); } - let updates = if let Some(before_env) = env_before.as_ref() { - collect_debug_variable_updates(before_env, &env, &types, &inline_recorder)? - } else { - Vec::new() - }; - inline_recorder.record_statement_updates(stmt, start, builder.script().len(), updates); + inline_recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; continue; } compile_statement( @@ -1906,16 +1860,10 @@ fn compile_inline_call( &mut inline_recorder, )?; let end = builder.script().len(); - let updates = if let Some(before_env) = env_before.as_ref() { - collect_debug_variable_updates(before_env, &env, &types, &inline_recorder)? - } else { - Vec::new() - }; - inline_recorder.record_statement_updates(stmt, start, end, updates); + inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } - debug_recorder.merge_inline_events(&inline_recorder); - debug_recorder.record_inline_call_exit(call_span, builder.script().len(), name); + debug_recorder.finish_inline_call_recording(call_span, builder.script().len(), name, &inline_recorder); for (name, value) in env.iter() { if name.starts_with("__arg_") { @@ -2087,6 +2035,7 @@ fn compile_block( ) -> Result<(), CompilerError> { for stmt in statements { let start = builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = debug_recorder.is_enabled().then(|| env.clone()); compile_statement( stmt, @@ -2106,12 +2055,7 @@ fn compile_block( debug_recorder, )?; let end = builder.script().len(); - let updates = if let Some(before_env) = env_before.as_ref() { - collect_debug_variable_updates(before_env, env, types, debug_recorder)? - } else { - Vec::new() - }; - debug_recorder.record_statement_updates(stmt, start, end, updates); + debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; } Ok(()) } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 08f1207b..57a623f4 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -10,6 +10,8 @@ use crate::debug::{ use super::{CompilerError, resolve_expr_for_debug}; +type ResolvedVariableUpdate = (String, String, Expr); + pub(super) fn record_synthetic_range( builder: &mut ScriptBuilder, recorder: &mut DebugSink, @@ -46,11 +48,7 @@ impl FunctionDebugRecorder { recorder } - pub fn inline(enabled: bool, function_name: String, call_depth: u32, frame_id: u32, next_frame_id: u32) -> Self { - Self { function_name, enabled, call_depth, frame_id, next_frame_id, ..Default::default() } - } - - pub fn sequence_count(&self) -> u32 { + fn sequence_count(&self) -> u32 { self.next_seq } @@ -58,14 +56,17 @@ impl FunctionDebugRecorder { self.enabled } - pub fn call_depth(&self) -> u32 { - self.call_depth - } - - pub fn new_inline_child(&mut self) -> Self { + fn new_inline_child(&mut self) -> Self { let frame_id = self.next_frame_id; self.next_frame_id = self.next_frame_id.saturating_add(1); - Self::inline(self.enabled, self.function_name.clone(), self.call_depth().saturating_add(1), frame_id, self.next_frame_id) + Self { + function_name: self.function_name.clone(), + enabled: self.enabled, + call_depth: self.call_depth.saturating_add(1), + frame_id, + next_frame_id: self.next_frame_id, + ..Default::default() + } } fn next_sequence(&mut self) -> u32 { @@ -117,31 +118,56 @@ impl FunctionDebugRecorder { self.push_event(bytecode_start, bytecode_start + bytecode_len, span, kind) } - pub fn record_statement(&mut self, stmt: &Statement, bytecode_start: usize, bytecode_len: usize) -> Option { - self.record_statement_span(stmt.span, bytecode_start, bytecode_len) - } - - pub fn record_statement_updates( + fn record_statement_updates( &mut self, stmt: &Statement, bytecode_start: usize, bytecode_end: usize, - variables: Vec<(String, String, Expr)>, + variables: Vec, ) { - if let Some(sequence) = self.record_statement(stmt, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + if let Some(sequence) = self.record_statement_span(stmt.span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { self.record_variable_updates(variables, bytecode_end, stmt.span, sequence); } } - pub fn record_inline_call_enter(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Option { - self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallEnter { callee: callee.to_string() }) + /// Records one source step for `stmt` and emits variable updates for names + /// whose expressions changed between `before_env` and `after_env`. + /// Stored expressions are resolved against `after_env` so debugger shadow + /// evaluation can compute values from the current state. + pub fn record_statement_with_env_diff( + &mut self, + stmt: &Statement, + bytecode_start: usize, + bytecode_end: usize, + before_env: Option<&HashMap>, + after_env: &HashMap, + types: &HashMap, + ) -> Result<(), CompilerError> { + let updates = self.collect_variable_updates(before_env, after_env, types)?; + self.record_statement_updates(stmt, bytecode_start, bytecode_end, updates); + Ok(()) } - pub fn record_inline_call_exit(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Option { - self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallExit { callee: callee.to_string() }) + /// Starts an inline call recording session and returns a child recorder for + /// callee body statements. + pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { + self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallEnter { callee: callee.to_string() }); + self.new_inline_child() } - pub fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { + /// Merges recorded callee events and emits the inline exit marker. + pub fn finish_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + inline: &FunctionDebugRecorder, + ) { + self.merge_inline_events(inline); + self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallExit { callee: callee.to_string() }); + } + + fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { if !self.enabled || inline.events.is_empty() { self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); return; @@ -169,9 +195,9 @@ impl FunctionDebugRecorder { self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); } - pub(super) fn record_variable_updates( + fn record_variable_updates( &mut self, - variables: Vec<(String, String, Expr)>, + variables: Vec, bytecode_offset: usize, span: Option, sequence: u32, @@ -193,14 +219,51 @@ impl FunctionDebugRecorder { } } + fn collect_variable_updates( + &self, + before_env: Option<&HashMap>, + after_env: &HashMap, + types: &HashMap, + ) -> Result, CompilerError> { + if !self.enabled { + return Ok(Vec::new()); + } + let Some(before_env) = before_env else { + return Ok(Vec::new()); + }; + + // Stable ordering keeps debug metadata deterministic across runs. + let mut names: Vec = after_env.keys().cloned().collect(); + names.sort_unstable(); + + let mut updates = Vec::new(); + for name in names { + // Inline synthetic args are plumbing, not user-facing variables. + if is_inline_synthetic_name(&name) { + continue; + } + let Some(after_expr) = after_env.get(&name) else { + continue; + }; + if before_env.get(&name).is_some_and(|before_expr| before_expr == after_expr) { + continue; + } + let Some(type_name) = types.get(&name) else { + continue; + }; + self.variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; + } + Ok(updates) + } + /// Records a variable update by resolving its expression against the current environment. /// This expands locals and synthetic inline placeholders (`__arg_*`) into /// caller-visible expressions, leaving only real param identifiers. /// The resolved expression is what enables shadow VM evaluation at debug time. - pub(super) fn variable_update( + fn variable_update( &self, env: &HashMap, - variables: &mut Vec<(String, String, Expr)>, + variables: &mut Vec, name: &str, type_name: &str, expr: Expr, @@ -214,6 +277,10 @@ impl FunctionDebugRecorder { } } +fn is_inline_synthetic_name(name: &str) -> bool { + name.starts_with("__arg_") +} + /// Global debug recording sink that can be enabled or disabled. /// When Off, all recording calls become no-ops with zero overhead. pub enum DebugSink { From 44cbf555dcbb789e1ae9b8f0d63737cc830e0d95 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:53:26 +0200 Subject: [PATCH 14/41] Fix debugger stack bindings and opcode cursor sync --- silverscript-lang/src/compiler.rs | 2 +- .../src/compiler/debug_recording.rs | 23 +++-- silverscript-lang/src/debug/session.rs | 27 ++++-- .../tests/debug_session_tests.rs | 84 +++++++++++++++++++ 4 files changed, 124 insertions(+), 12 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index bdaee31f..eb7b1372 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -974,7 +974,7 @@ fn compile_function( env.remove(¶m.name); } let mut builder = ScriptBuilder::new(); - let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function); + let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function, contract_fields); let mut yields: Vec = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 57a623f4..3cce4525 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; -use crate::ast::{Expr, FunctionAst, ParamAst, Statement}; +use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; use crate::debug::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugVariableUpdate, SourceSpan, @@ -41,10 +41,10 @@ pub struct FunctionDebugRecorder { } impl FunctionDebugRecorder { - pub fn new(enabled: bool, function: &FunctionAst) -> Self { + pub fn new(enabled: bool, function: &FunctionAst, contract_fields: &[ContractFieldAst]) -> Self { let mut recorder = Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; - recorder.record_params(function); + recorder.record_stack_bindings(function, contract_fields); recorder } @@ -98,16 +98,29 @@ impl FunctionDebugRecorder { Some(sequence) } - fn record_params(&mut self, function: &FunctionAst) { + fn record_stack_bindings(&mut self, function: &FunctionAst, contract_fields: &[ContractFieldAst]) { if !self.enabled { return; } let param_count = function.params.len(); + let field_count = contract_fields.len(); + // Runtime stack layout at function entry is: + // top -> contract fields (reverse declaration order), then function args. + // Keep debug stack indexes aligned with that layout so shadow evaluation + // reads the same values as normal execution. for (index, param) in function.params.iter().enumerate() { self.param_mappings.push(DebugParamMapping { name: param.name.clone(), type_name: param.type_ref.type_name(), - stack_index: (param_count - 1 - index) as i64, + stack_index: (field_count + (param_count - 1 - index)) as i64, + function: function.name.clone(), + }); + } + for (index, field) in contract_fields.iter().enumerate() { + self.param_mappings.push(DebugParamMapping { + name: field.name.clone(), + type_name: field.type_ref.type_name(), + stack_index: (field_count - 1 - index) as i64, function: function.name.clone(), }); } diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 3ae4b6d1..73d9d5a1 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -203,6 +203,7 @@ impl<'a> DebugSession<'a> { let opcode = self.opcodes[self.pc].take().expect("opcode already executed"); self.engine.execute_opcode(opcode)?; self.pc += 1; + self.sync_step_cursor_to_current_offset(); Ok(Some(self.state())) } @@ -289,12 +290,7 @@ impl<'a> DebugSession<'a> { } let offset = self.current_byte_offset(); if self.engine.is_executing() { - let found = self - .source_mappings - .iter() - .enumerate() - .find(|(_, mapping)| self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset)); - if let Some((index, _)) = found { + if let Some(index) = self.steppable_mapping_index_for_offset(offset) { self.current_step_index = Some(index); self.mark_mapping_executed(index); return Ok(()); @@ -694,10 +690,29 @@ impl<'a> DebugSession<'a> { } } + fn sync_step_cursor_to_current_offset(&mut self) { + let offset = self.current_byte_offset(); + if let Some(index) = self.steppable_mapping_index_for_offset(offset) { + // `si` executes raw opcodes; keep statement cursor in sync so later + // source-level steps (`next`/`step`/`finish`) start from the real + // current mapping instead of an old one. + self.current_step_index = Some(index); + self.mark_mapping_executed(index); + } + } + fn is_steppable_mapping(&self, mapping: &DebugMapping) -> bool { matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {}) } + fn steppable_mapping_index_for_offset(&self, offset: usize) -> Option { + self.source_mappings + .iter() + .enumerate() + .find(|(_, mapping)| self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset)) + .map(|(index, _)| index) + } + fn next_steppable_mapping_index(&self, from: Option, predicate: impl Fn(&DebugMapping) -> bool) -> Option { let start = from.map(|index| index.saturating_add(1)).unwrap_or(0); for index in start..self.source_mappings.len() { diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 98fc12d5..f4382117 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -203,6 +203,62 @@ contract ShadowMath(int fee) { }) } +#[test] +fn debug_session_offsets_param_indexes_when_contract_has_fields() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract FieldOffset(int c) { + int x = 7; + + entrypoint function main(int a) { + require(a > 0); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(5)], |session| { + session.run_to_first_executed_statement()?; + + let a = session.variable_by_name("a")?; + assert_eq!(session.format_value(&a.type_name, &a.value), "5"); + + let x = session.variable_by_name("x")?; + assert_eq!(session.format_value(&x.type_name, &x.value), "7"); + Ok(()) + }) +} + +#[test] +fn debug_session_resolves_updates_that_reference_contract_fields() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract FieldMath(int c) { + int x = 7; + + entrypoint function main(int a) { + int z = a + x + c; + require(z > 0); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(5)], |session| { + session.run_to_first_executed_statement()?; + + for _ in 0..4 { + if let Ok(z) = session.variable_by_name("z") { + assert_eq!(session.format_value(&z.type_name, &z.value), "14"); + return Ok(()); + } + if session.step_over()?.is_none() { + break; + } + } + + Err("expected z to become visible after assignment".into()) + }) +} + #[test] fn debug_session_exposes_virtual_steps() -> Result<(), Box> { let source = r#"pragma silverscript ^0.1.0; @@ -233,6 +289,34 @@ contract Virtuals() { }) } +#[test] +fn debug_session_step_opcode_advances_statement_cursor() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract OpcodeCursor() { + entrypoint function main(int a) { + int x = a + 1; + x = x + 2; + require(x > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + session.run_to_first_executed_statement()?; + let start = session.current_span().ok_or("missing start span")?; + assert_eq!(start.line, 5); + + session.step_opcode()?.ok_or("expected si to execute one opcode")?; + let after_si = session.current_span().ok_or("missing span after si")?; + assert_ne!(after_si.line, start.line, "si should refresh statement cursor"); + + let x = session.variable_by_name("x")?; + assert_eq!(session.format_value(&x.type_name, &x.value), "1"); + Ok(()) + }) +} + #[test] fn debug_session_breakpoint_hits_virtual_line() -> Result<(), Box> { let source = r#"pragma silverscript ^0.1.0; From 3202bad2f3f87630d83b03adfc936de3b8f3a0c3 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:17:53 +0200 Subject: [PATCH 15/41] Clarify debugger sequence and stepping documentation --- silverscript-lang/src/compiler.rs | 6 +++--- silverscript-lang/src/debug.rs | 9 +++++++++ silverscript-lang/src/debug/session.rs | 21 +++++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index eb7b1372..60231794 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -967,9 +967,9 @@ fn compile_function( } } let mut env: HashMap = constants.clone(); - // Function parameters shadow constructor constants with the same name. - // Keep constants in env for non-shadowed names, but drop colliding entries - // so identifier resolution inside this function picks stack params. + // `env` is checked before `params` during identifier compilation. + // Remove any constructor-constant names that collide with function params, + // otherwise the compiler would inline the constant and ignore the runtime arg. for param in &function.params { env.remove(¶m.name); } diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index 2aa23b53..b62cbe2d 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -48,6 +48,8 @@ pub struct DebugEvent { pub bytecode_end: usize, pub span: Option, pub kind: DebugEventKind, + /// Monotonic event order assigned by the compiler recorder. + /// Used to preserve source step order when bytecode ranges overlap. #[serde(default)] pub sequence: u32, #[serde(default)] @@ -90,12 +92,16 @@ impl DebugRecorder { self.constants.push(constant); } + /// Returns the next global sequence id for one emitted debug event. pub fn next_sequence(&mut self) -> u32 { let sequence = self.next_sequence; self.next_sequence = self.next_sequence.saturating_add(1); sequence } + /// Reserves a contiguous sequence block and returns its base id. + /// Callers use this when merging per-function debug data into contract-level + /// metadata so each function keeps local order while remaining globally ordered. pub fn reserve_sequence_block(&mut self, count: u32) -> u32 { let base = self.next_sequence; self.next_sequence = self.next_sequence.saturating_add(count); @@ -153,6 +159,8 @@ pub struct DebugVariableUpdate { pub bytecode_offset: usize, pub span: Option, pub function: String, + /// Sequence of the statement/virtual mapping that produced this update. + /// The debugger uses this to show locals only after that step executes. #[serde(default)] pub sequence: u32, #[serde(default)] @@ -193,6 +201,7 @@ pub struct DebugMapping { pub bytecode_end: usize, pub span: Option, pub kind: MappingKind, + /// Global event order used as a stable tiebreak for overlapping mappings. #[serde(default)] pub sequence: u32, #[serde(default)] diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 73d9d5a1..48a59398 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -97,9 +97,12 @@ pub struct DebugSession<'a> { debug_info: DebugInfo, source_mappings: Vec, current_step_index: Option, + // When sequence metadata exists, prefer sequence/frame semantics for step order + // and variable visibility (handles overlapping mapping ranges deterministically). uses_sequence_order: bool, source_lines: Vec, breakpoints: HashSet, + // Sequence ids of steppable mappings that were already visited in this session. executed_sequences: HashSet, } @@ -167,6 +170,8 @@ impl<'a> DebugSession<'a> { }) .cloned() .collect(); + // Sort by bytecode range first, then by event kind/depth. If sequence is + // available, use it as the final tie-breaker for overlapping events. source_mappings.sort_by_key(|mapping| { ( mapping.bytecode_start, @@ -227,6 +232,11 @@ impl<'a> DebugSession<'a> { self.step_over() } + /// Shared stepping loop for `step_into`, `step_over`, and `step_out`. + /// Picks the next steppable mapping whose call depth satisfies `predicate`, + /// executes opcodes until that mapping becomes active, and skips candidates + /// that are already behind the current byte offset (for example, non-taken + /// branch mappings). fn step_with_depth_predicate( &mut self, predicate: impl Fn(u32, u32) -> bool, @@ -633,6 +643,8 @@ impl<'a> DebugSession<'a> { return false; } if self.uses_sequence_order { + // Sequence-aware mode: stay in the active inline frame and only + // consider updates from steps already executed in this session. update.frame_id == frame_id && self.executed_sequences.contains(&update.sequence) && update.sequence < sequence @@ -660,7 +672,9 @@ impl<'a> DebugSession<'a> { latest } - /// Best mapping = smallest bytecode span containing `offset`. + /// Returns the most specific mapping for `offset`. + /// Multiple mappings may overlap; choosing the narrowest bytecode span makes + /// location lookups prefer inner statement/inline ranges over broader ranges. fn mapping_for_offset(&self, offset: usize) -> Option<&DebugMapping> { let mut best: Option<&DebugMapping> = None; let mut best_len = usize::MAX; @@ -681,6 +695,7 @@ impl<'a> DebugSession<'a> { } fn current_step_sequence_and_frame(&self) -> (u32, u32) { + // Sequence/frame identify the statement context used for variable filtering. self.current_step_mapping().map(|mapping| (mapping.sequence, mapping.frame_id)).unwrap_or((0, 0)) } @@ -871,7 +886,9 @@ fn concise_reason(reason: &str) -> String { } } -/// Decode a sign-magnitude little-endian integer +/// Decodes a txscript script number (little-endian sign-magnitude, max 8 bytes). +/// Mirrors txscript's internal numeric decode logic; kept local because txscript +/// exposes this helper only as crate-private internals today. fn decode_i64(bytes: &[u8]) -> Result { if bytes.is_empty() { return Ok(0); From 9fc3da32ee57fb62cf96c768404942246d7d8329 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:50:20 +0200 Subject: [PATCH 16/41] Simplify inline resolution and debugger variable plumbing --- silverscript-lang/src/compiler.rs | 268 ++++++++++++------ .../src/compiler/debug_recording.rs | 6 +- silverscript-lang/src/debug/session.rs | 203 +++++++------ 3 files changed, 283 insertions(+), 194 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 60231794..12b902ac 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1737,30 +1737,25 @@ fn compile_validate_output_state_statement( Ok(()) } -#[allow(clippy::too_many_arguments)] -fn compile_inline_call( +const INLINE_SYNTHETIC_ARG_PREFIX: &str = "__arg_"; + +pub(super) fn is_inline_synthetic_name(name: &str) -> bool { + name.starts_with(INLINE_SYNTHETIC_ARG_PREFIX) +} + +fn make_inline_synthetic_name(callee: &str, index: usize) -> String { + format!("{INLINE_SYNTHETIC_ARG_PREFIX}{callee}_{index}") +} + +type InlineScope = (HashMap, HashMap); + +fn validate_inline_call_signature( name: &str, + function: &FunctionAst, args: &[Expr], - call_span: Option, - caller_params: &HashMap, - caller_types: &mut HashMap, - caller_env: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, + caller_types: &HashMap, contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - caller_index: usize, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, -) -> Result, CompilerError> { - let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - let callee_index = - function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= caller_index { - return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); - } - +) -> Result<(), CompilerError> { if function.params.len() != args.len() { return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); } @@ -1770,31 +1765,88 @@ fn compile_inline_call( return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); } } - - let mut types = - function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); for param in &function.params { let param_type_name = type_name_from_ref(¶m.type_ref); if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); } } + Ok(()) +} +fn build_inline_scope( + callee_name: &str, + function: &FunctionAst, + args: &[Expr], + caller_env: &mut HashMap, + caller_types: &mut HashMap, + contract_constants: &HashMap, +) -> Result { + let mut types = + function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); + for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let temp_name = format!("__arg_{name}_{index}"); + let synthetic_name = make_inline_synthetic_name(callee_name, index); let param_type_name = type_name_from_ref(¶m.type_ref); // Inline calls bind each callee parameter to a synthetic identifier so // callee expressions keep a stable name while still pointing at the // caller-provided argument expression. - env.insert(temp_name.clone(), resolved.clone()); - types.insert(temp_name.clone(), param_type_name.clone()); - env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); - caller_env.insert(temp_name.clone(), resolved); - caller_types.insert(temp_name, param_type_name); + env.insert(synthetic_name.clone(), resolved.clone()); + types.insert(synthetic_name.clone(), param_type_name.clone()); + env.insert(param.name.clone(), Expr::Identifier(synthetic_name.clone())); + caller_env.insert(synthetic_name.clone(), resolved); + caller_types.insert(synthetic_name, param_type_name); + } + + Ok((env, types)) +} + +fn sync_inline_synthetic_bindings_back_to_caller( + env: &HashMap, + types: &HashMap, + caller_env: &mut HashMap, + caller_types: &mut HashMap, +) { + for (name, value) in env { + if !is_inline_synthetic_name(name) { + continue; + } + if let Some(type_name) = types.get(name) { + caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + caller_env.entry(name.clone()).or_insert_with(|| value.clone()); + } +} + +#[allow(clippy::too_many_arguments)] +fn compile_inline_call( + name: &str, + args: &[Expr], + call_span: Option, + caller_params: &HashMap, + caller_types: &mut HashMap, + caller_env: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + caller_index: usize, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result, CompilerError> { + let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let callee_index = + function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= caller_index { + return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); } + validate_inline_call_signature(name, function, args, caller_types, contract_constants)?; + let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, contract_constants)?; + if !options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); } @@ -1865,14 +1917,7 @@ fn compile_inline_call( debug_recorder.finish_inline_call_recording(call_span, builder.script().len(), name, &inline_recorder); - for (name, value) in env.iter() { - if name.starts_with("__arg_") { - if let Some(type_name) = types.get(name) { - caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); - } - caller_env.entry(name.clone()).or_insert_with(|| value.clone()); - } - } + sync_inline_synthetic_bindings_back_to_caller(&env, &types, caller_env, caller_types); Ok(yields) } @@ -2158,101 +2203,73 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { - resolve_expr_internal(expr, env, visiting, ResolveMode::Compile) -} - -/// Controls identifier expansion semantics for shared expression resolution. -/// Compile mode preserves synthetic inline arg placeholders (`__arg_*`) so -/// generated scripts still pick arguments from the caller stack frame. Debug -/// mode expands them into caller-visible expressions for shadow evaluation. -#[derive(Clone, Copy)] -enum ResolveMode { - Compile, - Debug, -} - -impl ResolveMode { - fn preserve_inline_args(self) -> bool { - matches!(self, Self::Compile) - } -} - -fn resolve_expr_internal( - expr: Expr, - env: &HashMap, - visiting: &mut HashSet, - mode: ResolveMode, -) -> Result { match expr { Expr::Identifier(name) => { - if mode.preserve_inline_args() && name.starts_with("__arg_") { + // Keep synthetic inline args unresolved in compile mode so generated + // bytecode still reads them from caller stack bindings. + if is_inline_synthetic_name(&name) { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr_internal(value.clone(), env, visiting, mode)?; + let resolved = resolve_expr(value.clone(), env, visiting)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::Identifier(name)) } } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr_internal(*expr, env, visiting, mode)?) }), + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), Expr::Binary { op, left, right } => Ok(Expr::Binary { op, - left: Box::new(resolve_expr_internal(*left, env, visiting, mode)?), - right: Box::new(resolve_expr_internal(*right, env, visiting, mode)?), + left: Box::new(resolve_expr(*left, env, visiting)?), + right: Box::new(resolve_expr(*right, env, visiting)?), }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr_internal(*condition, env, visiting, mode)?), - then_expr: Box::new(resolve_expr_internal(*then_expr, env, visiting, mode)?), - else_expr: Box::new(resolve_expr_internal(*else_expr, env, visiting, mode)?), + condition: Box::new(resolve_expr(*condition, env, visiting)?), + then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), + else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), }), Expr::Array(values) => { let mut resolved = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr_internal(value, env, visiting, mode)?); + resolved.push(resolve_expr(value, env, visiting)?); } Ok(Expr::Array(resolved)) } Expr::StateObject(fields) => { let mut resolved_fields = Vec::with_capacity(fields.len()); for field in fields { - resolved_fields.push(crate::ast::StateFieldExpr { - name: field.name, - expr: resolve_expr_internal(field.expr, env, visiting, mode)?, - }); + resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); } Ok(Expr::StateObject(resolved_fields)) } Expr::Call { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr_internal(arg, env, visiting, mode)?); + resolved.push(resolve_expr(arg, env, visiting)?); } Ok(Expr::Call { name, args: resolved }) } Expr::New { name, args } => { let mut resolved = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr_internal(arg, env, visiting, mode)?); + resolved.push(resolve_expr(arg, env, visiting)?); } Ok(Expr::New { name, args: resolved }) } Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr_internal(*source, env, visiting, mode)?), - index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?), + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), part, }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr_internal(*source, env, visiting, mode)?), - index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?), + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), }), - Expr::Introspection { kind, index } => { - Ok(Expr::Introspection { kind, index: Box::new(resolve_expr_internal(*index, env, visiting, mode)?) }) - } + Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), other => Ok(other), } } @@ -2287,7 +2304,86 @@ pub(super) fn resolve_expr_for_debug( env: &HashMap, visiting: &mut HashSet, ) -> Result { - resolve_expr_internal(expr, env, visiting, ResolveMode::Debug) + let resolved = resolve_expr(expr, env, visiting)?; + expand_inline_arg_placeholders(resolved, env, &mut HashSet::new()) +} + +fn expand_inline_arg_placeholders( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, +) -> Result { + match expr { + Expr::Identifier(name) => { + if !is_inline_synthetic_name(&name) { + return Ok(Expr::Identifier(name)); + } + let Some(value) = env.get(&name).cloned() else { + return Ok(Expr::Identifier(name)); + }; + if !visiting.insert(name.clone()) { + return Err(CompilerError::CyclicIdentifier(name)); + } + let expanded = expand_inline_arg_placeholders(value, env, visiting)?; + visiting.remove(&name); + Ok(expanded) + } + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }), + Expr::Binary { op, left, right } => Ok(Expr::Binary { + op, + left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), + right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), + }), + Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { + condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), + then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), + else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), + }), + Expr::Array(values) => { + let mut expanded = Vec::with_capacity(values.len()); + for value in values { + expanded.push(expand_inline_arg_placeholders(value, env, visiting)?); + } + Ok(Expr::Array(expanded)) + } + Expr::StateObject(fields) => { + let mut expanded_fields = Vec::with_capacity(fields.len()); + for field in fields { + expanded_fields.push(crate::ast::StateFieldExpr { + name: field.name, + expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, + }); + } + Ok(Expr::StateObject(expanded_fields)) + } + Expr::Call { name, args } => { + let mut expanded = Vec::with_capacity(args.len()); + for arg in args { + expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::Call { name, args: expanded }) + } + Expr::New { name, args } => { + let mut expanded = Vec::with_capacity(args.len()); + for arg in args { + expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::New { name, args: expanded }) + } + Expr::Split { source, index, part } => Ok(Expr::Split { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + part, + }), + Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + }), + Expr::Introspection { kind, index } => { + Ok(Expr::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?) }) + } + other => Ok(other), + } } fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 3cce4525..a92201cb 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -8,7 +8,7 @@ use crate::debug::{ DebugVariableUpdate, SourceSpan, }; -use super::{CompilerError, resolve_expr_for_debug}; +use super::{CompilerError, is_inline_synthetic_name, resolve_expr_for_debug}; type ResolvedVariableUpdate = (String, String, Expr); @@ -290,10 +290,6 @@ impl FunctionDebugRecorder { } } -fn is_inline_synthetic_name(name: &str) -> bool { - name.starts_with("__arg_") -} - /// Global debug recording sink that can be enabled or disabled. /// When Off, all recording calls become no-ops with zero overhead. pub enum DebugSink { diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 48a59398..f823380d 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -113,6 +113,13 @@ struct ShadowParamValue { value: Vec, } +struct VariableContext<'a> { + function_name: &'a str, + offset: usize, + sequence: u32, + frame_id: u32, +} + impl<'a> DebugSession<'a> { // --- Session construction + stepping --- @@ -437,55 +444,8 @@ impl<'a> DebugSession<'a> { } fn collect_variables(&self, sequence: u32, frame_id: u32) -> Result, String> { - let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; - let offset = self.current_byte_offset(); - let var_updates = self.current_variable_updates(function_name, offset, sequence, frame_id); - - let mut variables: Vec = Vec::new(); - let mut seen_names: HashSet = HashSet::new(); - - for (name, update) in &var_updates { - let value = self.evaluate_update_with_shadow_vm(function_name, update).unwrap_or_else(DebugValue::Unknown); - variables.push(Variable { - name: name.clone(), - type_name: update.type_name.clone(), - value, - is_constant: false, - origin: VariableOrigin::Local, - }); - seen_names.insert(name.clone()); - } - - for param in self.debug_info.params.iter().filter(|param| param.function == function_name) { - if seen_names.contains(¶m.name) { - continue; - } - let value = self.read_param_value(param)?; - variables.push(Variable { - name: param.name.clone(), - type_name: param.type_name.clone(), - value, - is_constant: false, - origin: VariableOrigin::Param, - }); - seen_names.insert(param.name.clone()); - } - - for constant in &self.debug_info.constants { - if seen_names.contains(&constant.name) { - continue; - } - let value = self.evaluate_constant(&constant.value); - variables.push(Variable { - name: constant.name.clone(), - type_name: constant.type_name.clone(), - value, - is_constant: true, - origin: VariableOrigin::Constant, - }); - seen_names.insert(constant.name.clone()); - } - + let context = self.current_variable_context(sequence, frame_id)?; + let mut variables = self.collect_variables_map(&context)?.into_values().collect::>(); variables.sort_by(|a, b| a.name.cmp(&b.name)); Ok(variables) } @@ -493,46 +453,10 @@ impl<'a> DebugSession<'a> { /// Returns a specific variable by name, or error if not in scope. /// Retrieves a specific variable by name with its current value. pub fn variable_by_name(&self, name: &str) -> Result { - let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; - let offset = self.current_byte_offset(); let (sequence, frame_id) = self.current_step_sequence_and_frame(); - let var_updates = self.current_variable_updates(function_name, offset, sequence, frame_id); - - if let Some(update) = var_updates.get(name) { - let value = self.evaluate_update_with_shadow_vm(function_name, update).unwrap_or_else(DebugValue::Unknown); - return Ok(Variable { - name: name.to_string(), - type_name: update.type_name.clone(), - value, - is_constant: false, - origin: VariableOrigin::Local, - }); - } - - if let Some(param) = self.debug_info.params.iter().find(|param| param.function == function_name && param.name == name) { - let value = self.read_param_value(param)?; - return Ok(Variable { - name: name.to_string(), - type_name: param.type_name.clone(), - value, - is_constant: false, - origin: VariableOrigin::Param, - }); - } - - // Check constructor constants - if let Some(constant) = self.debug_info.constants.iter().find(|c| c.name == name) { - let value = self.evaluate_constant(&constant.value); - return Ok(Variable { - name: name.to_string(), - type_name: constant.type_name.clone(), - value, - is_constant: true, - origin: VariableOrigin::Constant, - }); - } - - Err(format!("unknown variable '{name}'")) + let context = self.current_variable_context(sequence, frame_id)?; + let variables = self.collect_variables_map(&context)?; + variables.get(name).cloned().ok_or_else(|| format!("unknown variable '{name}'")) } // --- DebugValue formatting --- @@ -638,21 +562,12 @@ impl<'a> DebugSession<'a> { frame_id: u32, ) -> HashMap { let mut latest: HashMap = HashMap::new(); - for update in self.debug_info.variable_updates.iter().filter(|update| { - if update.function != function_name { - return false; - } - if self.uses_sequence_order { - // Sequence-aware mode: stay in the active inline frame and only - // consider updates from steps already executed in this session. - update.frame_id == frame_id - && self.executed_sequences.contains(&update.sequence) - && update.sequence < sequence - && update.bytecode_offset <= offset - } else { - update.bytecode_offset <= offset - } - }) { + for update in self + .debug_info + .variable_updates + .iter() + .filter(|update| self.update_is_visible(update, function_name, offset, sequence, frame_id)) + { if self.uses_sequence_order { match latest.get(&update.name) { Some(existing) if existing.sequence > update.sequence => {} @@ -672,6 +587,88 @@ impl<'a> DebugSession<'a> { latest } + fn current_variable_context(&self, sequence: u32, frame_id: u32) -> Result, String> { + let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; + Ok(VariableContext { function_name, offset: self.current_byte_offset(), sequence, frame_id }) + } + + fn collect_variables_map(&self, context: &VariableContext<'_>) -> Result, String> { + let mut variables: HashMap = HashMap::new(); + let var_updates = self.current_variable_updates(context.function_name, context.offset, context.sequence, context.frame_id); + + for (name, update) in &var_updates { + let value = self.evaluate_update_with_shadow_vm(context.function_name, update).unwrap_or_else(DebugValue::Unknown); + variables.insert( + name.clone(), + Variable { + name: name.clone(), + type_name: update.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Local, + }, + ); + } + + for param in self.debug_info.params.iter().filter(|param| param.function == context.function_name) { + if variables.contains_key(¶m.name) { + continue; + } + let value = self.read_param_value(param)?; + variables.insert( + param.name.clone(), + Variable { + name: param.name.clone(), + type_name: param.type_name.clone(), + value, + is_constant: false, + origin: VariableOrigin::Param, + }, + ); + } + + for constant in &self.debug_info.constants { + if variables.contains_key(&constant.name) { + continue; + } + variables.insert( + constant.name.clone(), + Variable { + name: constant.name.clone(), + type_name: constant.type_name.clone(), + value: self.evaluate_constant(&constant.value), + is_constant: true, + origin: VariableOrigin::Constant, + }, + ); + } + + Ok(variables) + } + + fn update_is_visible( + &self, + update: &DebugVariableUpdate, + function_name: &str, + offset: usize, + sequence: u32, + frame_id: u32, + ) -> bool { + if update.function != function_name { + return false; + } + if self.uses_sequence_order { + // Sequence-aware mode: stay in the active inline frame and only + // consider updates from steps already executed in this session. + update.frame_id == frame_id + && self.executed_sequences.contains(&update.sequence) + && update.sequence < sequence + && update.bytecode_offset <= offset + } else { + update.bytecode_offset <= offset + } + } + /// Returns the most specific mapping for `offset`. /// Multiple mappings may overlap; choosing the narrowest bytecode span makes /// location lookups prefer inner statement/inline ranges over broader ranges. From c22963d811bf2d17cf93bd2eddb44897ab0f31fa Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:20:59 +0200 Subject: [PATCH 17/41] CompileCtx --- silverscript-lang/src/ast.rs | 46 +- silverscript-lang/src/compiler.rs | 685 +++++++----------- .../src/compiler/debug_recording.rs | 13 +- silverscript-lang/tests/date_literal_tests.rs | 6 +- 4 files changed, 292 insertions(+), 458 deletions(-) diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 07795ce3..51be788b 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -34,7 +34,7 @@ pub struct FunctionAst { pub entrypoint: bool, #[serde(default)] pub return_types: Vec, - pub body: Vec, + pub body: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -123,16 +123,16 @@ impl TypeRef { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Statement { +pub struct SpannedStatement { #[serde(default, skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(flatten)] - pub kind: StatementKind, + pub kind: Statement, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum StatementKind { +pub enum Statement { VariableDefinition { type_ref: TypeRef, modifiers: Vec, name: String, expr: Option }, TupleAssignment { left_type_ref: TypeRef, left_name: String, right_type_ref: TypeRef, right_name: String, expr: Expr }, ArrayPush { name: String, expr: Expr }, @@ -142,8 +142,8 @@ pub enum StatementKind { Assign { name: String, expr: Expr }, TimeOp { tx_var: TimeVar, expr: Expr, message: Option }, Require { expr: Expr, message: Option }, - If { condition: Expr, then_branch: Vec, else_branch: Option> }, - For { ident: String, start: Expr, end: Expr, body: Vec }, + If { condition: Expr, then_branch: Vec, else_branch: Option> }, + For { ident: String, start: Expr, end: Expr, body: Vec }, Yield { expr: Expr }, Return { exprs: Vec }, Console { args: Vec }, @@ -434,7 +434,7 @@ fn parse_function_definition(pair: Pair<'_, Rule>) -> Result) -> Result { +fn parse_statement(pair: Pair<'_, Rule>) -> Result { if pair.as_rule() == Rule::statement { if let Some(inner) = pair.into_inner().next() { return parse_statement(inner); @@ -460,7 +460,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()))?; validate_user_identifier(ident.as_str())?; let expr = inner.next().map(parse_expression).transpose()?; - StatementKind::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr } + Statement::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr } } Rule::tuple_assignment => { let mut inner = pair.into_inner(); @@ -475,7 +475,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::TupleAssignment { + Statement::TupleAssignment { left_type_ref, left_name: left_ident.as_str().to_string(), right_type_ref, @@ -488,14 +488,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::ArrayPush { name: ident.as_str().to_string(), expr } + Statement::ArrayPush { name: ident.as_str().to_string(), expr } } Rule::assign_statement => { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; let expr = parse_expression(expr_pair)?; - StatementKind::Assign { name: ident.as_str().to_string(), expr } + Statement::Assign { name: ident.as_str().to_string(), expr } } Rule::time_op_statement => { let mut inner = pair.into_inner(); @@ -509,14 +509,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { "tx.time" => TimeVar::TxTime, other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), }; - StatementKind::TimeOp { tx_var, expr, message } + Statement::TimeOp { tx_var, expr, message } } Rule::require_statement => { let mut inner = pair.into_inner(); let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; let message = inner.next().map(parse_require_message).transpose()?; let expr = parse_expression(expr_pair)?; - StatementKind::Require { expr, message } + Statement::Require { expr, message } } Rule::if_statement => { let mut inner = pair.into_inner(); @@ -525,13 +525,13 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let then_block = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()))?; let then_branch = parse_block(then_block)?; let else_branch = inner.next().map(parse_block).transpose()?; - StatementKind::If { condition: cond_expr, then_branch, else_branch } + Statement::If { condition: cond_expr, then_branch, else_branch } } Rule::call_statement => { let mut inner = pair.into_inner(); let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, + Expr::Call { name, args } => Statement::FunctionCall { name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -556,7 +556,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, + Expr::Call { name, args } => Statement::FunctionCallAssign { bindings, name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -587,7 +587,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { } let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, + Expr::Call { name, args } => Statement::StateFunctionCallAssign { bindings, name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -603,7 +603,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let end_expr = parse_expression(end_pair)?; let body = parse_block(block_pair)?; - StatementKind::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } + Statement::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } } Rule::yield_statement => { let mut inner = pair.into_inner(); @@ -612,7 +612,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.len() != 1 { return Err(CompilerError::Unsupported("yield() expects a single argument".to_string())); } - StatementKind::Yield { expr: args[0].clone() } + Statement::Yield { expr: args[0].clone() } } Rule::return_statement => { let mut inner = pair.into_inner(); @@ -621,21 +621,21 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { if args.is_empty() { return Err(CompilerError::Unsupported("return() expects at least one argument".to_string())); } - StatementKind::Return { exprs: args } + Statement::Return { exprs: args } } Rule::console_statement => { let mut inner = pair.into_inner(); let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; let args = parse_console_parameter_list(list_pair)?; - StatementKind::Console { args } + Statement::Console { args } } _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), }; - Ok(Statement { span, kind }) + Ok(SpannedStatement { span, kind }) } -fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { match pair.as_rule() { Rule::block => { let mut statements = Vec::new(); diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 12b902ac..d707a9c0 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, - Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, + ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SpannedStatement, SplitPart, + StateBindingAst, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; use crate::debug::labels::synthetic; use crate::debug::{DebugInfo, SourceSpan}; @@ -286,30 +286,28 @@ fn compile_contract_fields( Ok((field_values, builder.drain())) } -fn statement_uses_script_size(stmt: &Statement) -> bool { +fn statement_uses_script_size(stmt: &SpannedStatement) -> bool { match &stmt.kind { - StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), - StatementKind::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), - StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - StatementKind::StateFunctionCallAssign { name, args, .. } => { - name == "readInputState" || args.iter().any(expr_uses_script_size) - } - StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), - StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), - StatementKind::Require { expr, .. } => expr_uses_script_size(expr), - StatementKind::If { condition, then_branch, else_branch } => { + Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), + Statement::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), + Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + Statement::StateFunctionCallAssign { name, args, .. } => name == "readInputState" || args.iter().any(expr_uses_script_size), + Statement::Assign { expr, .. } => expr_uses_script_size(expr), + Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), + Statement::Require { expr, .. } => expr_uses_script_size(expr), + Statement::If { condition, then_branch, else_branch } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - StatementKind::For { start, end, body, .. } => { + Statement::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - StatementKind::Yield { expr } => expr_uses_script_size(expr), - StatementKind::Return { exprs } => exprs.iter().any(expr_uses_script_size), - StatementKind::Console { args } => args.iter().any(|arg| match arg { + Statement::Yield { expr } => expr_uses_script_size(expr), + Statement::Return { exprs } => exprs.iter().any(expr_uses_script_size), + Statement::Console { args } => args.iter().any(|arg| match arg { crate::ast::ConsoleArg::Identifier(_) => false, crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), }), @@ -502,24 +500,24 @@ fn array_element_size_ref(type_ref: &TypeRef) -> Option { array_element_type_ref(type_ref).and_then(|element| fixed_type_size_ref(&element)) } -fn contains_return(stmt: &Statement) -> bool { +fn contains_return(stmt: &SpannedStatement) -> bool { match &stmt.kind { - StatementKind::Return { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { + Statement::Return { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - StatementKind::For { body, .. } => body.iter().any(contains_return), + Statement::For { body, .. } => body.iter().any(contains_return), _ => false, } } -fn contains_yield(stmt: &Statement) -> bool { +fn contains_yield(stmt: &SpannedStatement) -> bool { match &stmt.kind { - StatementKind::Yield { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { + Statement::Yield { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - StatementKind::For { body, .. } => body.iter().any(contains_yield), + Statement::For { body, .. } => body.iter().any(contains_yield), _ => false, } } @@ -924,6 +922,21 @@ struct CompiledFunction { debug: FunctionDebugRecorder, } +struct CompileCtx<'a> { + params: &'a HashMap, + builder: &'a mut ScriptBuilder, + options: CompileOptions, + contract_fields: &'a [ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &'a HashMap, + functions: &'a HashMap, + function_order: &'a HashMap, + function_index: usize, + yields: &'a mut Vec, + script_size: Option, + debug_recorder: &'a mut FunctionDebugRecorder, +} + fn compile_function( function: &FunctionAst, function_index: usize, @@ -987,7 +1000,7 @@ fn compile_function( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(SpannedStatement { kind: Statement::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1001,43 +1014,51 @@ fn compile_function( } } - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - let start = builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, StatementKind::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types, constants)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); - } - recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; - continue; - } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - &mut builder, + { + let mut ctx = CompileCtx { + params: ¶ms, + builder: &mut builder, options, contract_fields, contract_field_prefix_len, - constants, + contract_constants: constants, functions, function_order, function_index, - &mut yields, + yields: &mut yields, script_size, - &mut recorder, - )?; - let end = builder.script().len(); - recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; + debug_recorder: &mut recorder, + }; + + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = ctx.builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. + let env_before = ctx.debug_recorder.is_enabled().then(|| env.clone()); + if matches!(stmt.kind, Statement::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let Statement::Return { exprs } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + ctx.yields.push(resolved); + } + ctx.debug_recorder.record_statement_with_env_diff( + stmt, + start, + ctx.builder.script().len(), + env_before.as_ref(), + &env, + &types, + )?; + continue; + } + compile_statement(stmt, &mut env, &mut types, &mut ctx)?; + let end = ctx.builder.script().len(); + ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; + } } let yield_count = yields.len(); @@ -1079,26 +1100,22 @@ fn compile_function( Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) } -#[allow(clippy::too_many_arguments)] fn compile_statement( - stmt: &Statement, + stmt: &SpannedStatement, env: &mut HashMap, - params: &HashMap, types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_fields: &[ContractFieldAst], - contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + ctx: &mut CompileCtx<'_>, ) -> Result<(), CompilerError> { + let params = ctx.params; + let options = ctx.options; + let contract_fields = ctx.contract_fields; + let contract_field_prefix_len = ctx.contract_field_prefix_len; + let contract_constants = ctx.contract_constants; + let functions = ctx.functions; + let script_size = ctx.script_size; + match &stmt.kind { - StatementKind::VariableDefinition { type_ref, name, expr, .. } => { + Statement::VariableDefinition { type_ref, name, expr, .. } => { let type_name = type_name_from_ref(type_ref); let effective_type_name = if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { @@ -1187,7 +1204,7 @@ fn compile_statement( Ok(()) } } - StatementKind::ArrayPush { name, expr } => { + Statement::ArrayPush { name, expr } => { let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; if !is_array_type(array_type) { return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); @@ -1236,73 +1253,36 @@ fn compile_statement( env.insert(name.clone(), updated); Ok(()) } - StatementKind::Require { expr, .. } => { + Statement::Require { expr, .. } => { let mut stack_depth = 0i64; compile_expr( expr, env, params, types, - builder, + ctx.builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants, )?; - builder.add_op(OpVerify)?; + ctx.builder.add_op(OpVerify)?; Ok(()) } - StatementKind::TimeOp { tx_var, expr, .. } => { - compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size, contract_constants) + Statement::TimeOp { tx_var, expr, .. } => compile_time_op_statement(tx_var, expr, env, types, ctx), + Statement::If { condition, then_branch, else_branch } => { + compile_if_statement(condition, then_branch, else_branch.as_deref(), env, types, ctx) } - StatementKind::If { condition, then_branch, else_branch } => compile_if_statement( - condition, - then_branch, - else_branch.as_deref(), - env, - params, - types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - ), - StatementKind::For { ident, start, end, body } => compile_for_statement( - ident, - start, - end, - body, - env, - params, - types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - ), - StatementKind::Yield { expr } => { + Statement::For { ident, start, end, body } => compile_for_statement(ident, start, end, body, env, types, ctx), + Statement::Yield { expr } => { let mut visiting = HashSet::new(); let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; - yields.push(resolved); + ctx.yields.push(resolved); Ok(()) } - StatementKind::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), - StatementKind::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { + Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), + Statement::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { Expr::Split { source, index, .. } => { env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); @@ -1310,14 +1290,14 @@ fn compile_statement( } _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), }, - StatementKind::FunctionCall { name, args } => { + Statement::FunctionCall { name, args } => { if name == "validateOutputState" { return compile_validate_output_state_statement( args, env, params, types, - builder, + ctx.builder, options, contract_fields, contract_field_prefix_len, @@ -1325,22 +1305,7 @@ fn compile_statement( contract_constants, ); } - let returns = compile_inline_call( - name, - args, - stmt.span, - params, - types, - env, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - script_size, - debug_recorder, - )?; + let returns = compile_inline_call(name, args, stmt.span, types, env, ctx)?; if !returns.is_empty() { let mut stack_depth = 0i64; for expr in returns { @@ -1349,20 +1314,20 @@ fn compile_statement( env, params, types, - builder, + ctx.builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants, )?; - builder.add_op(OpDrop)?; + ctx.builder.add_op(OpDrop)?; stack_depth -= 1; } } Ok(()) } - StatementKind::StateFunctionCallAssign { bindings, name, args } => { + Statement::StateFunctionCallAssign { bindings, name, args } => { if name == "readInputState" { return compile_read_input_state_statement( bindings, @@ -1379,7 +1344,7 @@ fn compile_statement( name ))) } - StatementKind::FunctionCallAssign { bindings, name, args } => { + Statement::FunctionCallAssign { bindings, name, args } => { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; if function.return_types.is_empty() { return Err(CompilerError::Unsupported("function has no return types".to_string())); @@ -1394,22 +1359,7 @@ fn compile_statement( return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); } } - let returns = compile_inline_call( - name, - args, - stmt.span, - params, - types, - env, - builder, - options, - contract_constants, - functions, - function_order, - function_index, - script_size, - debug_recorder, - )?; + let returns = compile_inline_call(name, args, stmt.span, types, env, ctx)?; if returns.len() != bindings.len() { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } @@ -1419,7 +1369,7 @@ fn compile_statement( } Ok(()) } - StatementKind::Assign { name, expr } => { + Statement::Assign { name, expr } => { if let Some(type_name) = types.get(name) { if is_array_type(type_name) { match expr { @@ -1444,7 +1394,7 @@ fn compile_statement( env.insert(name.clone(), resolved); Ok(()) } - StatementKind::Console { .. } => Ok(()), + Statement::Console { .. } => Ok(()), } } @@ -1737,43 +1687,6 @@ fn compile_validate_output_state_statement( Ok(()) } -const INLINE_SYNTHETIC_ARG_PREFIX: &str = "__arg_"; - -pub(super) fn is_inline_synthetic_name(name: &str) -> bool { - name.starts_with(INLINE_SYNTHETIC_ARG_PREFIX) -} - -fn make_inline_synthetic_name(callee: &str, index: usize) -> String { - format!("{INLINE_SYNTHETIC_ARG_PREFIX}{callee}_{index}") -} - -type InlineScope = (HashMap, HashMap); - -fn validate_inline_call_signature( - name: &str, - function: &FunctionAst, - args: &[Expr], - caller_types: &HashMap, - contract_constants: &HashMap, -) -> Result<(), CompilerError> { - if function.params.len() != args.len() { - return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); - } - for (param, arg) in function.params.iter().zip(args.iter()) { - let param_type_name = type_name_from_ref(¶m.type_ref); - if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, contract_constants) { - return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); - } - } - for param in &function.params { - let param_type_name = type_name_from_ref(¶m.type_ref); - if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); - } - } - Ok(()) -} - fn build_inline_scope( callee_name: &str, function: &FunctionAst, @@ -1781,14 +1694,14 @@ fn build_inline_scope( caller_env: &mut HashMap, caller_types: &mut HashMap, contract_constants: &HashMap, -) -> Result { +) -> Result<(HashMap, HashMap), CompilerError> { let mut types = function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let synthetic_name = make_inline_synthetic_name(callee_name, index); + let synthetic_name = format!("__arg_{callee_name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); // Inline calls bind each callee parameter to a synthetic identifier so // callee expressions keep a stable name while still pointing at the @@ -1810,7 +1723,7 @@ fn sync_inline_synthetic_bindings_back_to_caller( caller_types: &mut HashMap, ) { for (name, value) in env { - if !is_inline_synthetic_name(name) { + if !name.starts_with("__arg_") { continue; } if let Some(type_name) = types.get(name) { @@ -1820,44 +1733,50 @@ fn sync_inline_synthetic_bindings_back_to_caller( } } -#[allow(clippy::too_many_arguments)] fn compile_inline_call( name: &str, args: &[Expr], call_span: Option, - caller_params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - caller_index: usize, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + ctx: &mut CompileCtx<'_>, ) -> Result, CompilerError> { - let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let function = ctx.functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = - function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= caller_index { + ctx.function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= ctx.function_index { return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); } - validate_inline_call_signature(name, function, args, caller_types, contract_constants)?; - let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, contract_constants)?; + if function.params.len() != args.len() { + return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); + } + for (param, arg) in function.params.iter().zip(args.iter()) { + let param_type_name = type_name_from_ref(¶m.type_ref); + if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, ctx.contract_constants) { + return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); + } + } + for param in &function.params { + let param_type_name = type_name_from_ref(¶m.type_ref); + if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); + } + } - if !options.allow_yield && function.body.iter().any(contains_yield) { + let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, ctx.contract_constants)?; + + if !ctx.options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); } - if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { + if function.entrypoint && !ctx.options.allow_entrypoint_return && function.body.iter().any(contains_return) { return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); } let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(SpannedStatement { kind: Statement::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1868,140 +1787,104 @@ fn compile_inline_call( } } - let call_start = builder.script().len(); + let call_start = ctx.builder.script().len(); // Record call boundary on caller frame and collect callee events in a child frame. - let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); + let mut inline_recorder = ctx.debug_recorder.start_inline_call_recording(call_span, call_start, name); let mut yields: Vec = Vec::new(); // Use caller parameter stack indexes while compiling callee bytecode so // identifier resolution can still pick values from the caller frame. - let params = caller_params.clone(); - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - let start = builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = inline_recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, StatementKind::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types, contract_constants)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - yields.push(resolved); - } - inline_recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; - continue; + let params = ctx.params.clone(); + { + let mut inline_ctx = CompileCtx { + params: ¶ms, + builder: &mut *ctx.builder, + options: ctx.options, + contract_fields: &[], + contract_field_prefix_len: 0, + contract_constants: ctx.contract_constants, + functions: ctx.functions, + function_order: ctx.function_order, + function_index: callee_index, + yields: &mut yields, + script_size: ctx.script_size, + debug_recorder: &mut inline_recorder, + }; + + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = inline_ctx.builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. + let env_before = inline_ctx.debug_recorder.is_enabled().then(|| env.clone()); + if matches!(stmt.kind, Statement::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let Statement::Return { exprs } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, inline_ctx.contract_constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + inline_ctx.yields.push(resolved); + } + inline_ctx.debug_recorder.record_statement_with_env_diff( + stmt, + start, + inline_ctx.builder.script().len(), + env_before.as_ref(), + &env, + &types, + )?; + continue; + } + compile_statement(stmt, &mut env, &mut types, &mut inline_ctx)?; + let end = inline_ctx.builder.script().len(); + inline_ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - builder, - options, - &[], - 0, - contract_constants, - functions, - function_order, - callee_index, - &mut yields, - script_size, - &mut inline_recorder, - )?; - let end = builder.script().len(); - inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } - debug_recorder.finish_inline_call_recording(call_span, builder.script().len(), name, &inline_recorder); + ctx.debug_recorder.finish_inline_call_recording(call_span, ctx.builder.script().len(), name, &inline_recorder); sync_inline_synthetic_bindings_back_to_caller(&env, &types, caller_env, caller_types); Ok(yields) } -#[allow(clippy::too_many_arguments)] fn compile_if_statement( condition: &Expr, - then_branch: &[Statement], - else_branch: Option<&[Statement]>, + then_branch: &[SpannedStatement], + else_branch: Option<&[SpannedStatement]>, env: &mut HashMap, - params: &HashMap, types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_fields: &[ContractFieldAst], - contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + ctx: &mut CompileCtx<'_>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( condition, env, - params, + ctx.params, types, - builder, - options, + ctx.builder, + ctx.options, &mut HashSet::new(), &mut stack_depth, - script_size, - contract_constants, + ctx.script_size, + ctx.contract_constants, )?; - builder.add_op(OpIf)?; + ctx.builder.add_op(OpIf)?; let original_env = env.clone(); let mut then_env = original_env.clone(); let mut then_types = types.clone(); - compile_block( - then_branch, - &mut then_env, - params, - &mut then_types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - )?; + compile_block(then_branch, &mut then_env, &mut then_types, ctx)?; let mut else_env = original_env.clone(); if let Some(else_branch) = else_branch { - builder.add_op(OpElse)?; + ctx.builder.add_op(OpElse)?; let mut else_types = types.clone(); - compile_block( - else_branch, - &mut else_env, - params, - &mut else_types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - )?; + compile_block(else_branch, &mut else_env, &mut else_types, ctx)?; } - builder.add_op(OpEndIf)?; + ctx.builder.add_op(OpEndIf)?; let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); @@ -2038,96 +1921,63 @@ fn compile_time_op_statement( tx_var: &TimeVar, expr: &Expr, env: &mut HashMap, - params: &HashMap, types: &HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - script_size: Option, - contract_constants: &HashMap, + ctx: &mut CompileCtx<'_>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; - compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants)?; + compile_expr( + expr, + env, + ctx.params, + types, + ctx.builder, + ctx.options, + &mut HashSet::new(), + &mut stack_depth, + ctx.script_size, + ctx.contract_constants, + )?; match tx_var { TimeVar::ThisAge => { - builder.add_op(OpCheckSequenceVerify)?; + ctx.builder.add_op(OpCheckSequenceVerify)?; } TimeVar::TxTime => { - builder.add_op(OpCheckLockTimeVerify)?; + ctx.builder.add_op(OpCheckLockTimeVerify)?; } } Ok(()) } -#[allow(clippy::too_many_arguments)] fn compile_block( - statements: &[Statement], + statements: &[SpannedStatement], env: &mut HashMap, - params: &HashMap, types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_fields: &[ContractFieldAst], - contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + ctx: &mut CompileCtx<'_>, ) -> Result<(), CompilerError> { for stmt in statements { - let start = builder.script().len(); + let start = ctx.builder.script().len(); // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = debug_recorder.is_enabled().then(|| env.clone()); - compile_statement( - stmt, - env, - params, - types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - )?; - let end = builder.script().len(); - debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; + let env_before = ctx.debug_recorder.is_enabled().then(|| env.clone()); + compile_statement(stmt, env, types, ctx)?; + let end = ctx.builder.script().len(); + ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; } Ok(()) } -#[allow(clippy::too_many_arguments)] fn compile_for_statement( ident: &str, start_expr: &Expr, end_expr: &Expr, - body: &[Statement], + body: &[SpannedStatement], env: &mut HashMap, - params: &HashMap, types: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_fields: &[ContractFieldAst], - contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - function_index: usize, - yields: &mut Vec, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + ctx: &mut CompileCtx<'_>, ) -> Result<(), CompilerError> { - let start = eval_const_int(start_expr, contract_constants)?; - let end = eval_const_int(end_expr, contract_constants)?; + let start = eval_const_int(start_expr, ctx.contract_constants)?; + let end = eval_const_int(end_expr, ctx.contract_constants)?; if end < start { return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); } @@ -2136,23 +1986,7 @@ fn compile_for_statement( let previous = env.get(&name).cloned(); for value in start..end { env.insert(name.clone(), Expr::Int(value)); - compile_block( - body, - env, - params, - types, - builder, - options, - contract_fields, - contract_field_prefix_len, - contract_constants, - functions, - function_order, - function_index, - yields, - script_size, - debug_recorder, - )?; + compile_block(body, env, types, ctx)?; } match previous { @@ -2205,9 +2039,9 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { match expr { Expr::Identifier(name) => { - // Keep synthetic inline args unresolved in compile mode so generated - // bytecode still reads them from caller stack bindings. - if is_inline_synthetic_name(&name) { + // Preserve synthetic inline placeholders in compile mode so + // generated bytecode keeps reading caller stack arguments. + if name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { @@ -2265,6 +2099,11 @@ fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet< index: Box::new(resolve_expr(*index, env, visiting)?), part, }), + Expr::Slice { source, start, end } => Ok(Expr::Slice { + source: Box::new(resolve_expr(*source, env, visiting)?), + start: Box::new(resolve_expr(*start, env, visiting)?), + end: Box::new(resolve_expr(*end, env, visiting)?), + }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { source: Box::new(resolve_expr(*source, env, visiting)?), index: Box::new(resolve_expr(*index, env, visiting)?), @@ -2299,23 +2138,14 @@ pub fn compile_debug_expr( Ok(builder.drain()) } -pub(super) fn resolve_expr_for_debug( - expr: Expr, - env: &HashMap, - visiting: &mut HashSet, -) -> Result { - let resolved = resolve_expr(expr, env, visiting)?; - expand_inline_arg_placeholders(resolved, env, &mut HashSet::new()) -} - -fn expand_inline_arg_placeholders( +pub(super) fn expand_inline_args( expr: Expr, env: &HashMap, visiting: &mut HashSet, ) -> Result { match expr { Expr::Identifier(name) => { - if !is_inline_synthetic_name(&name) { + if !name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } let Some(value) = env.get(&name).cloned() else { @@ -2324,63 +2154,66 @@ fn expand_inline_arg_placeholders( if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let expanded = expand_inline_arg_placeholders(value, env, visiting)?; + let expanded = expand_inline_args(value, env, visiting)?; visiting.remove(&name); Ok(expanded) } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }), + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_args(*expr, env, visiting)?) }), Expr::Binary { op, left, right } => Ok(Expr::Binary { op, - left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), - right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), + left: Box::new(expand_inline_args(*left, env, visiting)?), + right: Box::new(expand_inline_args(*right, env, visiting)?), }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), - then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), - else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), + condition: Box::new(expand_inline_args(*condition, env, visiting)?), + then_expr: Box::new(expand_inline_args(*then_expr, env, visiting)?), + else_expr: Box::new(expand_inline_args(*else_expr, env, visiting)?), }), Expr::Array(values) => { let mut expanded = Vec::with_capacity(values.len()); for value in values { - expanded.push(expand_inline_arg_placeholders(value, env, visiting)?); + expanded.push(expand_inline_args(value, env, visiting)?); } Ok(Expr::Array(expanded)) } Expr::StateObject(fields) => { let mut expanded_fields = Vec::with_capacity(fields.len()); for field in fields { - expanded_fields.push(crate::ast::StateFieldExpr { - name: field.name, - expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, - }); + expanded_fields + .push(crate::ast::StateFieldExpr { name: field.name, expr: expand_inline_args(field.expr, env, visiting)? }); } Ok(Expr::StateObject(expanded_fields)) } Expr::Call { name, args } => { let mut expanded = Vec::with_capacity(args.len()); for arg in args { - expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + expanded.push(expand_inline_args(arg, env, visiting)?); } Ok(Expr::Call { name, args: expanded }) } Expr::New { name, args } => { let mut expanded = Vec::with_capacity(args.len()); for arg in args { - expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + expanded.push(expand_inline_args(arg, env, visiting)?); } Ok(Expr::New { name, args: expanded }) } Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + source: Box::new(expand_inline_args(*source, env, visiting)?), + index: Box::new(expand_inline_args(*index, env, visiting)?), part, }), + Expr::Slice { source, start, end } => Ok(Expr::Slice { + source: Box::new(expand_inline_args(*source, env, visiting)?), + start: Box::new(expand_inline_args(*start, env, visiting)?), + end: Box::new(expand_inline_args(*end, env, visiting)?), + }), Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + source: Box::new(expand_inline_args(*source, env, visiting)?), + index: Box::new(expand_inline_args(*index, env, visiting)?), }), Expr::Introspection { kind, index } => { - Ok(Expr::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?) }) + Ok(Expr::Introspection { kind, index: Box::new(expand_inline_args(*index, env, visiting)?) }) } other => Ok(other), } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index a92201cb..d875cd8a 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -2,13 +2,13 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; -use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; +use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, SpannedStatement}; use crate::debug::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugVariableUpdate, SourceSpan, }; -use super::{CompilerError, is_inline_synthetic_name, resolve_expr_for_debug}; +use super::{CompilerError, expand_inline_args, resolve_expr}; type ResolvedVariableUpdate = (String, String, Expr); @@ -133,7 +133,7 @@ impl FunctionDebugRecorder { fn record_statement_updates( &mut self, - stmt: &Statement, + stmt: &SpannedStatement, bytecode_start: usize, bytecode_end: usize, variables: Vec, @@ -149,7 +149,7 @@ impl FunctionDebugRecorder { /// evaluation can compute values from the current state. pub fn record_statement_with_env_diff( &mut self, - stmt: &Statement, + stmt: &SpannedStatement, bytecode_start: usize, bytecode_end: usize, before_env: Option<&HashMap>, @@ -252,7 +252,7 @@ impl FunctionDebugRecorder { let mut updates = Vec::new(); for name in names { // Inline synthetic args are plumbing, not user-facing variables. - if is_inline_synthetic_name(&name) { + if name.starts_with("__arg_") { continue; } let Some(after_expr) = after_env.get(&name) else { @@ -284,7 +284,8 @@ impl FunctionDebugRecorder { if !self.enabled { return Ok(()); } - let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; + let resolved = resolve_expr(expr, env, &mut HashSet::new())?; + let resolved = expand_inline_args(resolved, env, &mut HashSet::new())?; variables.push((name.to_string(), type_name.to_string(), resolved)); Ok(()) } diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index e0606afc..803988fe 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; +use silverscript_lang::ast::{Expr, Statement, parse_contract_ast}; fn extract_first_expr(source: &str) -> Expr { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; match &statement.kind { - StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - StatementKind::Require { expr, .. } => expr.clone(), + Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + Statement::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } From f1966536d47664799c9deaed673d5fd76a8d51f4 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:55:17 +0200 Subject: [PATCH 18/41] Restore pre-refactor staged debugger snapshot --- silverscript-lang/src/compiler.rs | 169 +++++++------------- silverscript-lang/src/debug.rs | 1 + silverscript-lang/src/debug/presentation.rs | 136 ++++++++++++++++ silverscript-lang/src/debug/session.rs | 110 +------------ 4 files changed, 202 insertions(+), 214 deletions(-) create mode 100644 silverscript-lang/src/debug/presentation.rs diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index d707a9c0..6c2804f1 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1687,6 +1687,8 @@ fn compile_validate_output_state_statement( Ok(()) } +type InlineScopeMaps = (HashMap, HashMap); + fn build_inline_scope( callee_name: &str, function: &FunctionAst, @@ -1694,7 +1696,7 @@ fn build_inline_scope( caller_env: &mut HashMap, caller_types: &mut HashMap, contract_constants: &HashMap, -) -> Result<(HashMap, HashMap), CompilerError> { +) -> Result { let mut types = function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); @@ -2036,83 +2038,87 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { +fn rewrite_expr_children( + expr: Expr, + mut rewrite_child: impl FnMut(Expr) -> Result, +) -> Result { match expr { - Expr::Identifier(name) => { - // Preserve synthetic inline placeholders in compile mode so - // generated bytecode keeps reading caller stack arguments. - if name.starts_with("__arg_") { - return Ok(Expr::Identifier(name)); - } - if let Some(value) = env.get(&name) { - if !visiting.insert(name.clone()) { - return Err(CompilerError::CyclicIdentifier(name)); - } - let resolved = resolve_expr(value.clone(), env, visiting)?; - visiting.remove(&name); - Ok(resolved) - } else { - Ok(Expr::Identifier(name)) - } + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(rewrite_child(*expr)?) }), + Expr::Binary { op, left, right } => { + Ok(Expr::Binary { op, left: Box::new(rewrite_child(*left)?), right: Box::new(rewrite_child(*right)?) }) } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), - Expr::Binary { op, left, right } => Ok(Expr::Binary { - op, - left: Box::new(resolve_expr(*left, env, visiting)?), - right: Box::new(resolve_expr(*right, env, visiting)?), - }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr(*condition, env, visiting)?), - then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), - else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + condition: Box::new(rewrite_child(*condition)?), + then_expr: Box::new(rewrite_child(*then_expr)?), + else_expr: Box::new(rewrite_child(*else_expr)?), }), Expr::Array(values) => { - let mut resolved = Vec::with_capacity(values.len()); + let mut rewritten = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr(value, env, visiting)?); + rewritten.push(rewrite_child(value)?); } - Ok(Expr::Array(resolved)) + Ok(Expr::Array(rewritten)) } Expr::StateObject(fields) => { - let mut resolved_fields = Vec::with_capacity(fields.len()); + let mut rewritten_fields = Vec::with_capacity(fields.len()); for field in fields { - resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); + rewritten_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: rewrite_child(field.expr)? }); } - Ok(Expr::StateObject(resolved_fields)) + Ok(Expr::StateObject(rewritten_fields)) } Expr::Call { name, args } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(rewrite_child(arg)?); } - Ok(Expr::Call { name, args: resolved }) + Ok(Expr::Call { name, args: rewritten }) } Expr::New { name, args } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(rewrite_child(arg)?); } - Ok(Expr::New { name, args: resolved }) + Ok(Expr::New { name, args: rewritten }) + } + Expr::Split { source, index, part } => { + Ok(Expr::Split { source: Box::new(rewrite_child(*source)?), index: Box::new(rewrite_child(*index)?), part }) } - Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), - part, - }), Expr::Slice { source, start, end } => Ok(Expr::Slice { - source: Box::new(resolve_expr(*source, env, visiting)?), - start: Box::new(resolve_expr(*start, env, visiting)?), - end: Box::new(resolve_expr(*end, env, visiting)?), - }), - Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), + source: Box::new(rewrite_child(*source)?), + start: Box::new(rewrite_child(*start)?), + end: Box::new(rewrite_child(*end)?), }), - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), + Expr::ArrayIndex { source, index } => { + Ok(Expr::ArrayIndex { source: Box::new(rewrite_child(*source)?), index: Box::new(rewrite_child(*index)?) }) + } + Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(rewrite_child(*index)?) }), other => Ok(other), } } +fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet) -> Result { + match expr { + Expr::Identifier(name) => { + // Preserve synthetic inline placeholders in compile mode so + // generated bytecode keeps reading caller stack arguments. + if name.starts_with("__arg_") { + return Ok(Expr::Identifier(name)); + } + if let Some(value) = env.get(&name) { + if !visiting.insert(name.clone()) { + return Err(CompilerError::CyclicIdentifier(name)); + } + let resolved = resolve_expr(value.clone(), env, visiting)?; + visiting.remove(&name); + Ok(resolved) + } else { + Ok(Expr::Identifier(name)) + } + } + other => rewrite_expr_children(other, |child| resolve_expr(child, env, visiting)), + } +} + /// Compiles a pre-resolved expression for debugger shadow evaluation. pub fn compile_debug_expr( expr: &Expr, @@ -2158,64 +2164,7 @@ pub(super) fn expand_inline_args( visiting.remove(&name); Ok(expanded) } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_args(*expr, env, visiting)?) }), - Expr::Binary { op, left, right } => Ok(Expr::Binary { - op, - left: Box::new(expand_inline_args(*left, env, visiting)?), - right: Box::new(expand_inline_args(*right, env, visiting)?), - }), - Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(expand_inline_args(*condition, env, visiting)?), - then_expr: Box::new(expand_inline_args(*then_expr, env, visiting)?), - else_expr: Box::new(expand_inline_args(*else_expr, env, visiting)?), - }), - Expr::Array(values) => { - let mut expanded = Vec::with_capacity(values.len()); - for value in values { - expanded.push(expand_inline_args(value, env, visiting)?); - } - Ok(Expr::Array(expanded)) - } - Expr::StateObject(fields) => { - let mut expanded_fields = Vec::with_capacity(fields.len()); - for field in fields { - expanded_fields - .push(crate::ast::StateFieldExpr { name: field.name, expr: expand_inline_args(field.expr, env, visiting)? }); - } - Ok(Expr::StateObject(expanded_fields)) - } - Expr::Call { name, args } => { - let mut expanded = Vec::with_capacity(args.len()); - for arg in args { - expanded.push(expand_inline_args(arg, env, visiting)?); - } - Ok(Expr::Call { name, args: expanded }) - } - Expr::New { name, args } => { - let mut expanded = Vec::with_capacity(args.len()); - for arg in args { - expanded.push(expand_inline_args(arg, env, visiting)?); - } - Ok(Expr::New { name, args: expanded }) - } - Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(expand_inline_args(*source, env, visiting)?), - index: Box::new(expand_inline_args(*index, env, visiting)?), - part, - }), - Expr::Slice { source, start, end } => Ok(Expr::Slice { - source: Box::new(expand_inline_args(*source, env, visiting)?), - start: Box::new(expand_inline_args(*start, env, visiting)?), - end: Box::new(expand_inline_args(*end, env, visiting)?), - }), - Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(expand_inline_args(*source, env, visiting)?), - index: Box::new(expand_inline_args(*index, env, visiting)?), - }), - Expr::Introspection { kind, index } => { - Ok(Expr::Introspection { kind, index: Box::new(expand_inline_args(*index, env, visiting)?) }) - } - other => Ok(other), + other => rewrite_expr_children(other, |child| expand_inline_args(child, env, visiting)), } } diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index b62cbe2d..a30972f0 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -2,6 +2,7 @@ use crate::ast::Expr; use pest::Span; use serde::{Deserialize, Serialize}; +pub mod presentation; pub mod session; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] diff --git a/silverscript-lang/src/debug/presentation.rs b/silverscript-lang/src/debug/presentation.rs new file mode 100644 index 00000000..3b052e62 --- /dev/null +++ b/silverscript-lang/src/debug/presentation.rs @@ -0,0 +1,136 @@ +use crate::debug::SourceSpan; +use crate::debug::session::DebugValue; + +#[derive(Debug, Clone)] +pub struct SourceContextLine { + pub line: u32, + pub text: String, + pub is_active: bool, +} + +#[derive(Debug, Clone)] +pub struct SourceContext { + pub lines: Vec, +} + +pub fn build_source_context(source_lines: &[String], span: SourceSpan, radius: usize) -> SourceContext { + let line = span.line.saturating_sub(1) as usize; + let start = line.saturating_sub(radius); + let end = (line + radius).min(source_lines.len().saturating_sub(1)); + + let mut lines = Vec::new(); + for idx in start..=end { + let display_line = idx + 1; + let content = source_lines.get(idx).map(String::as_str).unwrap_or(""); + lines.push(SourceContextLine { line: display_line as u32, text: content.to_string(), is_active: idx == line }); + } + + SourceContext { lines } +} + +pub fn format_value(type_name: &str, value: &DebugValue) -> String { + let element_type = type_name.strip_suffix("[]"); + match (type_name, value) { + ("int", DebugValue::Int(number)) => number.to_string(), + ("bool", DebugValue::Bool(value)) => value.to_string(), + ("string", DebugValue::String(value)) => value.clone(), + (_, DebugValue::Unknown(reason)) => unavailable_reason(reason), + (_, DebugValue::Bytes(bytes)) if element_type.is_some() => { + let element_type = element_type.expect("checked"); + let Some(element_size) = array_element_size(element_type) else { + return format!("0x{}", encode_hex(bytes)); + }; + if element_size == 0 || bytes.len() % element_size != 0 { + return format!("0x{}", encode_hex(bytes)); + } + + let mut values: Vec = Vec::new(); + for chunk in bytes.chunks(element_size) { + let decoded = match element_type { + "int" => DebugValue::Int(decode_i64(chunk).unwrap_or(0)), + "bool" => DebugValue::Bool(decode_i64(chunk).unwrap_or(0) != 0), + _ => DebugValue::Bytes(chunk.to_vec()), + }; + values.push(format_value(element_type, &decoded)); + } + format!("[{}]", values.join(", ")) + } + (_, DebugValue::Bytes(bytes)) => format!("0x{}", encode_hex(bytes)), + (_, DebugValue::Int(number)) => number.to_string(), + (_, DebugValue::Bool(value)) => value.to_string(), + (_, DebugValue::String(value)) => value.clone(), + (_, DebugValue::Array(values)) => { + let value_type = element_type.unwrap_or(type_name); + format!("[{}]", values.iter().map(|v| format_value(value_type, v)).collect::>().join(", ")) + } + } +} + +fn unavailable_reason(reason: &str) -> String { + if reason.trim().is_empty() { + "".to_string() + } else if reason.contains("failed to compile debug expression") + || reason.contains("undefined identifier") + || reason.contains("__arg_") + { + "".to_string() + } else if reason.contains("failed to execute shadow script") { + "".to_string() + } else { + format!("", concise_reason(reason)) + } +} + +/// Truncates error messages to 96 chars for display in debugger UI. +fn concise_reason(reason: &str) -> String { + let trimmed = reason.trim(); + if trimmed.is_empty() { + return "unknown".to_string(); + } + let first_line = trimmed.lines().next().unwrap_or(trimmed); + const MAX_CHARS: usize = 96; + if first_line.chars().count() <= MAX_CHARS { + first_line.to_string() + } else { + let mut out = String::new(); + for ch in first_line.chars().take(MAX_CHARS) { + out.push(ch); + } + out.push_str("..."); + out + } +} + +fn array_element_size(element_type: &str) -> Option { + match element_type { + "int" => Some(8), + "bool" => Some(1), + "byte" => Some(1), + other => other.strip_prefix("bytes").and_then(|v| v.parse::().ok()), + } +} + +fn decode_i64(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(0); + } + if bytes.len() > 8 { + return Err("numeric value is longer than 8 bytes".to_string()); + } + let msb = bytes[bytes.len() - 1]; + let sign = 1 - 2 * ((msb >> 7) as i64); + let first_byte = (msb & 0x7f) as i64; + let mut value = first_byte; + for byte in bytes[..bytes.len() - 1].iter().rev() { + value = (value << 8) + (*byte as i64); + } + Ok(value * sign) +} + +fn encode_hex(bytes: &[u8]) -> String { + let mut out = vec![0u8; bytes.len() * 2]; + if faster_hex::hex_encode(bytes, &mut out).is_err() { + return String::new(); + } + String::from_utf8(out).unwrap_or_default() +} diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index f823380d..6beca9c6 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -9,8 +9,11 @@ use serde::{Deserialize, Serialize}; use crate::ast::Expr; use crate::compiler::compile_debug_expr; +use crate::debug::presentation::{build_source_context, format_value as format_debug_value}; use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan}; +pub use crate::debug::presentation::{SourceContext, SourceContextLine}; + pub type DebugTx<'a> = PopulatedTransaction<'a>; pub type DebugReused = SigHashReusedValuesUnsync; pub type DebugOpcode<'a> = DynOpcodeImplementation, DebugReused>; @@ -23,7 +26,7 @@ pub enum DebugValue { Bytes(Vec), String(String), Array(Vec), - /// Value could not be evaluated (e.g., from inline function return) + /// Value could not be evaluated (for example unresolved identifiers or shadow VM failures). Unknown(std::string::String), } @@ -53,18 +56,6 @@ pub struct Variable { pub origin: VariableOrigin, } -#[derive(Debug, Clone)] -pub struct SourceContextLine { - pub line: u32, - pub text: String, - pub is_active: bool, -} - -#[derive(Debug, Clone)] -pub struct SourceContext { - pub lines: Vec, -} - #[derive(Debug, Clone)] pub struct SessionState { pub pc: usize, @@ -389,19 +380,7 @@ impl<'a> DebugSession<'a> { /// Returns surrounding source lines with the current line highlighted. pub fn source_context(&self) -> Option { let span = self.current_span()?; - let line = span.line.saturating_sub(1) as usize; - let radius = 6; - let start = line.saturating_sub(radius); - let end = (line + radius).min(self.source_lines.len().saturating_sub(1)); - - let mut lines = Vec::new(); - for idx in start..=end { - let display_line = idx + 1; - let content = self.source_lines.get(idx).map(String::as_str).unwrap_or(""); - lines.push(SourceContextLine { line: display_line as u32, text: content.to_string(), is_active: idx == line }); - } - - Some(SourceContext { lines }) + Some(build_source_context(&self.source_lines, span, 6)) } /// Adds a breakpoint at the given line number. Returns true if added. @@ -462,54 +441,7 @@ impl<'a> DebugSession<'a> { // --- DebugValue formatting --- /// Formats a debug value for display based on its type. pub fn format_value(&self, type_name: &str, value: &DebugValue) -> String { - let element_type = type_name.strip_suffix("[]"); - match (type_name, value) { - ("int", DebugValue::Int(number)) => number.to_string(), - ("bool", DebugValue::Bool(value)) => value.to_string(), - ("string", DebugValue::String(value)) => value.clone(), - (_, DebugValue::Unknown(reason)) => { - if reason.trim().is_empty() { - "".to_string() - } else if reason.contains("failed to compile debug expression") - || reason.contains("undefined identifier") - || reason.contains("__arg_") - { - "".to_string() - } else if reason.contains("failed to execute shadow script") { - "".to_string() - } else { - format!("", concise_reason(reason)) - } - } - (_, DebugValue::Bytes(bytes)) if element_type.is_some() => { - let element_type = element_type.expect("checked"); - let Some(element_size) = array_element_size(element_type) else { - return format!("0x{}", encode_hex(bytes)); - }; - if element_size == 0 || bytes.len() % element_size != 0 { - return format!("0x{}", encode_hex(bytes)); - } - - let mut values: Vec = Vec::new(); - for chunk in bytes.chunks(element_size) { - let decoded = match element_type { - "int" => DebugValue::Int(decode_i64(chunk).unwrap_or(0)), - "bool" => DebugValue::Bool(decode_i64(chunk).unwrap_or(0) != 0), - _ => DebugValue::Bytes(chunk.to_vec()), - }; - values.push(self.format_value(element_type, &decoded)); - } - format!("[{}]", values.join(", ")) - } - (_, DebugValue::Bytes(bytes)) => format!("0x{}", encode_hex(bytes)), - (_, DebugValue::Int(number)) => number.to_string(), - (_, DebugValue::Bool(value)) => value.to_string(), - (_, DebugValue::String(value)) => value.clone(), - (_, DebugValue::Array(values)) => { - let value_type = element_type.unwrap_or(type_name); - format!("[{}]", values.iter().map(|v| self.format_value(value_type, v)).collect::>().join(", ")) - } - } + format_debug_value(type_name, value) } /// Returns the debug mapping for the current bytecode position. @@ -840,16 +772,6 @@ impl<'a> DebugSession<'a> { } } -/// Returns byte size for fixed-size array elements (e.g., bytes32 → 32), or None for variable-size. -fn array_element_size(element_type: &str) -> Option { - match element_type { - "int" => Some(8), - "bool" => Some(1), - "byte" => Some(1), - other => other.strip_prefix("bytes").and_then(|v| v.parse::().ok()), - } -} - /// Decodes raw bytes into a typed debug value based on the type name. fn decode_value_by_type(type_name: &str, bytes: Vec) -> Result { match type_name { @@ -863,26 +785,6 @@ fn decode_value_by_type(type_name: &str, bytes: Vec) -> Result String { - let trimmed = reason.trim(); - if trimmed.is_empty() { - return "unknown".to_string(); - } - let first_line = trimmed.lines().next().unwrap_or(trimmed); - const MAX_CHARS: usize = 96; - if first_line.chars().count() <= MAX_CHARS { - first_line.to_string() - } else { - let mut out = String::new(); - for ch in first_line.chars().take(MAX_CHARS) { - out.push(ch); - } - out.push_str("..."); - out - } -} - /// Decodes a txscript script number (little-endian sign-magnitude, max 8 bytes). /// Mirrors txscript's internal numeric decode logic; kept local because txscript /// exposes this helper only as crate-private internals today. From 2cd252bafe9722a9a6cce02b4678ff5dbe5b6d3a Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:57:57 +0200 Subject: [PATCH 19/41] Minimize compiler footprint by dropping CompileCtx refactor --- silverscript-lang/src/ast.rs | 46 +- silverscript-lang/src/compiler.rs | 794 +++++++++++------- .../src/compiler/debug_recording.rs | 13 +- silverscript-lang/tests/date_literal_tests.rs | 6 +- 4 files changed, 538 insertions(+), 321 deletions(-) diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 51be788b..07795ce3 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -34,7 +34,7 @@ pub struct FunctionAst { pub entrypoint: bool, #[serde(default)] pub return_types: Vec, - pub body: Vec, + pub body: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -123,16 +123,16 @@ impl TypeRef { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SpannedStatement { +pub struct Statement { #[serde(default, skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(flatten)] - pub kind: Statement, + pub kind: StatementKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum Statement { +pub enum StatementKind { VariableDefinition { type_ref: TypeRef, modifiers: Vec, name: String, expr: Option }, TupleAssignment { left_type_ref: TypeRef, left_name: String, right_type_ref: TypeRef, right_name: String, expr: Expr }, ArrayPush { name: String, expr: Expr }, @@ -142,8 +142,8 @@ pub enum Statement { Assign { name: String, expr: Expr }, TimeOp { tx_var: TimeVar, expr: Expr, message: Option }, Require { expr: Expr, message: Option }, - If { condition: Expr, then_branch: Vec, else_branch: Option> }, - For { ident: String, start: Expr, end: Expr, body: Vec }, + If { condition: Expr, then_branch: Vec, else_branch: Option> }, + For { ident: String, start: Expr, end: Expr, body: Vec }, Yield { expr: Expr }, Return { exprs: Vec }, Console { args: Vec }, @@ -434,7 +434,7 @@ fn parse_function_definition(pair: Pair<'_, Rule>) -> Result) -> Result { +fn parse_statement(pair: Pair<'_, Rule>) -> Result { if pair.as_rule() == Rule::statement { if let Some(inner) = pair.into_inner().next() { return parse_statement(inner); @@ -460,7 +460,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); @@ -475,7 +475,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result) -> Result { let mut inner = pair.into_inner(); let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; let expr = parse_expression(expr_pair)?; - Statement::Assign { name: ident.as_str().to_string(), expr } + StatementKind::Assign { name: ident.as_str().to_string(), expr } } Rule::time_op_statement => { let mut inner = pair.into_inner(); @@ -509,14 +509,14 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result TimeVar::TxTime, other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), }; - Statement::TimeOp { tx_var, expr, message } + StatementKind::TimeOp { tx_var, expr, message } } Rule::require_statement => { let mut inner = pair.into_inner(); let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; let message = inner.next().map(parse_require_message).transpose()?; let expr = parse_expression(expr_pair)?; - Statement::Require { expr, message } + StatementKind::Require { expr, message } } Rule::if_statement => { let mut inner = pair.into_inner(); @@ -525,13 +525,13 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; match parse_function_call(call_pair)? { - Expr::Call { name, args } => Statement::FunctionCall { name, args }, + Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -556,7 +556,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result Statement::FunctionCallAssign { bindings, name, args }, + Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -587,7 +587,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result Statement::StateFunctionCallAssign { bindings, name, args }, + Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, _ => return Err(CompilerError::Unsupported("function call expected".to_string())), } } @@ -603,7 +603,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); @@ -612,7 +612,7 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); @@ -621,21 +621,21 @@ fn parse_statement(pair: Pair<'_, Rule>) -> Result { let mut inner = pair.into_inner(); let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; let args = parse_console_parameter_list(list_pair)?; - Statement::Console { args } + StatementKind::Console { args } } _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), }; - Ok(SpannedStatement { span, kind }) + Ok(Statement { span, kind }) } -fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { match pair.as_rule() { Rule::block => { let mut statements = Vec::new(); diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 6c2804f1..12b902ac 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ast::{ - ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SpannedStatement, SplitPart, - StateBindingAst, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, + ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, + Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, }; use crate::debug::labels::synthetic; use crate::debug::{DebugInfo, SourceSpan}; @@ -286,28 +286,30 @@ fn compile_contract_fields( Ok((field_values, builder.drain())) } -fn statement_uses_script_size(stmt: &SpannedStatement) -> bool { +fn statement_uses_script_size(stmt: &Statement) -> bool { match &stmt.kind { - Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), - Statement::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), - Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - Statement::StateFunctionCallAssign { name, args, .. } => name == "readInputState" || args.iter().any(expr_uses_script_size), - Statement::Assign { expr, .. } => expr_uses_script_size(expr), - Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), - Statement::Require { expr, .. } => expr_uses_script_size(expr), - Statement::If { condition, then_branch, else_branch } => { + StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), + StatementKind::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), + StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + StatementKind::StateFunctionCallAssign { name, args, .. } => { + name == "readInputState" || args.iter().any(expr_uses_script_size) + } + StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), + StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), + StatementKind::Require { expr, .. } => expr_uses_script_size(expr), + StatementKind::If { condition, then_branch, else_branch } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - Statement::For { start, end, body, .. } => { + StatementKind::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - Statement::Yield { expr } => expr_uses_script_size(expr), - Statement::Return { exprs } => exprs.iter().any(expr_uses_script_size), - Statement::Console { args } => args.iter().any(|arg| match arg { + StatementKind::Yield { expr } => expr_uses_script_size(expr), + StatementKind::Return { exprs } => exprs.iter().any(expr_uses_script_size), + StatementKind::Console { args } => args.iter().any(|arg| match arg { crate::ast::ConsoleArg::Identifier(_) => false, crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), }), @@ -500,24 +502,24 @@ fn array_element_size_ref(type_ref: &TypeRef) -> Option { array_element_type_ref(type_ref).and_then(|element| fixed_type_size_ref(&element)) } -fn contains_return(stmt: &SpannedStatement) -> bool { +fn contains_return(stmt: &Statement) -> bool { match &stmt.kind { - Statement::Return { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + StatementKind::Return { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - Statement::For { body, .. } => body.iter().any(contains_return), + StatementKind::For { body, .. } => body.iter().any(contains_return), _ => false, } } -fn contains_yield(stmt: &SpannedStatement) -> bool { +fn contains_yield(stmt: &Statement) -> bool { match &stmt.kind { - Statement::Yield { .. } => true, - Statement::If { then_branch, else_branch, .. } => { + StatementKind::Yield { .. } => true, + StatementKind::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - Statement::For { body, .. } => body.iter().any(contains_yield), + StatementKind::For { body, .. } => body.iter().any(contains_yield), _ => false, } } @@ -922,21 +924,6 @@ struct CompiledFunction { debug: FunctionDebugRecorder, } -struct CompileCtx<'a> { - params: &'a HashMap, - builder: &'a mut ScriptBuilder, - options: CompileOptions, - contract_fields: &'a [ContractFieldAst], - contract_field_prefix_len: usize, - contract_constants: &'a HashMap, - functions: &'a HashMap, - function_order: &'a HashMap, - function_index: usize, - yields: &'a mut Vec, - script_size: Option, - debug_recorder: &'a mut FunctionDebugRecorder, -} - fn compile_function( function: &FunctionAst, function_index: usize, @@ -1000,7 +987,7 @@ fn compile_function( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(SpannedStatement { kind: Statement::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1014,51 +1001,43 @@ fn compile_function( } } - { - let mut ctx = CompileCtx { - params: ¶ms, - builder: &mut builder, + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. + let env_before = recorder.is_enabled().then(|| env.clone()); + if matches!(stmt.kind, StatementKind::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; + continue; + } + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + &mut builder, options, contract_fields, contract_field_prefix_len, - contract_constants: constants, + constants, functions, function_order, function_index, - yields: &mut yields, + &mut yields, script_size, - debug_recorder: &mut recorder, - }; - - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - let start = ctx.builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = ctx.debug_recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, Statement::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let Statement::Return { exprs } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types, constants)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - ctx.yields.push(resolved); - } - ctx.debug_recorder.record_statement_with_env_diff( - stmt, - start, - ctx.builder.script().len(), - env_before.as_ref(), - &env, - &types, - )?; - continue; - } - compile_statement(stmt, &mut env, &mut types, &mut ctx)?; - let end = ctx.builder.script().len(); - ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; - } + &mut recorder, + )?; + let end = builder.script().len(); + recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } let yield_count = yields.len(); @@ -1100,22 +1079,26 @@ fn compile_function( Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) } +#[allow(clippy::too_many_arguments)] fn compile_statement( - stmt: &SpannedStatement, + stmt: &Statement, env: &mut HashMap, + params: &HashMap, types: &mut HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, ) -> Result<(), CompilerError> { - let params = ctx.params; - let options = ctx.options; - let contract_fields = ctx.contract_fields; - let contract_field_prefix_len = ctx.contract_field_prefix_len; - let contract_constants = ctx.contract_constants; - let functions = ctx.functions; - let script_size = ctx.script_size; - match &stmt.kind { - Statement::VariableDefinition { type_ref, name, expr, .. } => { + StatementKind::VariableDefinition { type_ref, name, expr, .. } => { let type_name = type_name_from_ref(type_ref); let effective_type_name = if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { @@ -1204,7 +1187,7 @@ fn compile_statement( Ok(()) } } - Statement::ArrayPush { name, expr } => { + StatementKind::ArrayPush { name, expr } => { let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; if !is_array_type(array_type) { return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); @@ -1253,36 +1236,73 @@ fn compile_statement( env.insert(name.clone(), updated); Ok(()) } - Statement::Require { expr, .. } => { + StatementKind::Require { expr, .. } => { let mut stack_depth = 0i64; compile_expr( expr, env, params, types, - ctx.builder, + builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants, )?; - ctx.builder.add_op(OpVerify)?; + builder.add_op(OpVerify)?; Ok(()) } - Statement::TimeOp { tx_var, expr, .. } => compile_time_op_statement(tx_var, expr, env, types, ctx), - Statement::If { condition, then_branch, else_branch } => { - compile_if_statement(condition, then_branch, else_branch.as_deref(), env, types, ctx) + StatementKind::TimeOp { tx_var, expr, .. } => { + compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size, contract_constants) } - Statement::For { ident, start, end, body } => compile_for_statement(ident, start, end, body, env, types, ctx), - Statement::Yield { expr } => { + StatementKind::If { condition, then_branch, else_branch } => compile_if_statement( + condition, + then_branch, + else_branch.as_deref(), + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + ), + StatementKind::For { ident, start, end, body } => compile_for_statement( + ident, + start, + end, + body, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + ), + StatementKind::Yield { expr } => { let mut visiting = HashSet::new(); let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; - ctx.yields.push(resolved); + yields.push(resolved); Ok(()) } - Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), - Statement::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { + StatementKind::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), + StatementKind::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { Expr::Split { source, index, .. } => { env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); @@ -1290,14 +1310,14 @@ fn compile_statement( } _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), }, - Statement::FunctionCall { name, args } => { + StatementKind::FunctionCall { name, args } => { if name == "validateOutputState" { return compile_validate_output_state_statement( args, env, params, types, - ctx.builder, + builder, options, contract_fields, contract_field_prefix_len, @@ -1305,7 +1325,22 @@ fn compile_statement( contract_constants, ); } - let returns = compile_inline_call(name, args, stmt.span, types, env, ctx)?; + let returns = compile_inline_call( + name, + args, + stmt.span, + params, + types, + env, + builder, + options, + contract_constants, + functions, + function_order, + function_index, + script_size, + debug_recorder, + )?; if !returns.is_empty() { let mut stack_depth = 0i64; for expr in returns { @@ -1314,20 +1349,20 @@ fn compile_statement( env, params, types, - ctx.builder, + builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants, )?; - ctx.builder.add_op(OpDrop)?; + builder.add_op(OpDrop)?; stack_depth -= 1; } } Ok(()) } - Statement::StateFunctionCallAssign { bindings, name, args } => { + StatementKind::StateFunctionCallAssign { bindings, name, args } => { if name == "readInputState" { return compile_read_input_state_statement( bindings, @@ -1344,7 +1379,7 @@ fn compile_statement( name ))) } - Statement::FunctionCallAssign { bindings, name, args } => { + StatementKind::FunctionCallAssign { bindings, name, args } => { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; if function.return_types.is_empty() { return Err(CompilerError::Unsupported("function has no return types".to_string())); @@ -1359,7 +1394,22 @@ fn compile_statement( return Err(CompilerError::Unsupported("function return types must match binding types".to_string())); } } - let returns = compile_inline_call(name, args, stmt.span, types, env, ctx)?; + let returns = compile_inline_call( + name, + args, + stmt.span, + params, + types, + env, + builder, + options, + contract_constants, + functions, + function_order, + function_index, + script_size, + debug_recorder, + )?; if returns.len() != bindings.len() { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); } @@ -1369,7 +1419,7 @@ fn compile_statement( } Ok(()) } - Statement::Assign { name, expr } => { + StatementKind::Assign { name, expr } => { if let Some(type_name) = types.get(name) { if is_array_type(type_name) { match expr { @@ -1394,7 +1444,7 @@ fn compile_statement( env.insert(name.clone(), resolved); Ok(()) } - Statement::Console { .. } => Ok(()), + StatementKind::Console { .. } => Ok(()), } } @@ -1687,7 +1737,42 @@ fn compile_validate_output_state_statement( Ok(()) } -type InlineScopeMaps = (HashMap, HashMap); +const INLINE_SYNTHETIC_ARG_PREFIX: &str = "__arg_"; + +pub(super) fn is_inline_synthetic_name(name: &str) -> bool { + name.starts_with(INLINE_SYNTHETIC_ARG_PREFIX) +} + +fn make_inline_synthetic_name(callee: &str, index: usize) -> String { + format!("{INLINE_SYNTHETIC_ARG_PREFIX}{callee}_{index}") +} + +type InlineScope = (HashMap, HashMap); + +fn validate_inline_call_signature( + name: &str, + function: &FunctionAst, + args: &[Expr], + caller_types: &HashMap, + contract_constants: &HashMap, +) -> Result<(), CompilerError> { + if function.params.len() != args.len() { + return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); + } + for (param, arg) in function.params.iter().zip(args.iter()) { + let param_type_name = type_name_from_ref(¶m.type_ref); + if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, contract_constants) { + return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); + } + } + for param in &function.params { + let param_type_name = type_name_from_ref(¶m.type_ref); + if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); + } + } + Ok(()) +} fn build_inline_scope( callee_name: &str, @@ -1696,14 +1781,14 @@ fn build_inline_scope( caller_env: &mut HashMap, caller_types: &mut HashMap, contract_constants: &HashMap, -) -> Result { +) -> Result { let mut types = function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let synthetic_name = format!("__arg_{callee_name}_{index}"); + let synthetic_name = make_inline_synthetic_name(callee_name, index); let param_type_name = type_name_from_ref(¶m.type_ref); // Inline calls bind each callee parameter to a synthetic identifier so // callee expressions keep a stable name while still pointing at the @@ -1725,7 +1810,7 @@ fn sync_inline_synthetic_bindings_back_to_caller( caller_types: &mut HashMap, ) { for (name, value) in env { - if !name.starts_with("__arg_") { + if !is_inline_synthetic_name(name) { continue; } if let Some(type_name) = types.get(name) { @@ -1735,50 +1820,44 @@ fn sync_inline_synthetic_bindings_back_to_caller( } } +#[allow(clippy::too_many_arguments)] fn compile_inline_call( name: &str, args: &[Expr], call_span: Option, + caller_params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + caller_index: usize, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, ) -> Result, CompilerError> { - let function = ctx.functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = - ctx.function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= ctx.function_index { + function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= caller_index { return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); } - if function.params.len() != args.len() { - return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); - } - for (param, arg) in function.params.iter().zip(args.iter()) { - let param_type_name = type_name_from_ref(¶m.type_ref); - if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, ctx.contract_constants) { - return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); - } - } - for param in &function.params { - let param_type_name = type_name_from_ref(¶m.type_ref); - if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); - } - } - - let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, ctx.contract_constants)?; + validate_inline_call_signature(name, function, args, caller_types, contract_constants)?; + let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, contract_constants)?; - if !ctx.options.allow_yield && function.body.iter().any(contains_yield) { + if !options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); } - if function.entrypoint && !ctx.options.allow_entrypoint_return && function.body.iter().any(contains_return) { + if function.entrypoint && !options.allow_entrypoint_return && function.body.iter().any(contains_return) { return Err(CompilerError::Unsupported("entrypoint return requires allow_entrypoint_return=true".to_string())); } let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(SpannedStatement { kind: Statement::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1789,104 +1868,140 @@ fn compile_inline_call( } } - let call_start = ctx.builder.script().len(); + let call_start = builder.script().len(); // Record call boundary on caller frame and collect callee events in a child frame. - let mut inline_recorder = ctx.debug_recorder.start_inline_call_recording(call_span, call_start, name); + let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); let mut yields: Vec = Vec::new(); // Use caller parameter stack indexes while compiling callee bytecode so // identifier resolution can still pick values from the caller frame. - let params = ctx.params.clone(); - { - let mut inline_ctx = CompileCtx { - params: ¶ms, - builder: &mut *ctx.builder, - options: ctx.options, - contract_fields: &[], - contract_field_prefix_len: 0, - contract_constants: ctx.contract_constants, - functions: ctx.functions, - function_order: ctx.function_order, - function_index: callee_index, - yields: &mut yields, - script_size: ctx.script_size, - debug_recorder: &mut inline_recorder, - }; - - let body_len = function.body.len(); - for (index, stmt) in function.body.iter().enumerate() { - let start = inline_ctx.builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = inline_ctx.debug_recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, Statement::Return { .. }) { - if index != body_len - 1 { - return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); - } - let Statement::Return { exprs } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types, inline_ctx.contract_constants)?; - for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; - inline_ctx.yields.push(resolved); - } - inline_ctx.debug_recorder.record_statement_with_env_diff( - stmt, - start, - inline_ctx.builder.script().len(), - env_before.as_ref(), - &env, - &types, - )?; - continue; - } - compile_statement(stmt, &mut env, &mut types, &mut inline_ctx)?; - let end = inline_ctx.builder.script().len(); - inline_ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; + let params = caller_params.clone(); + let body_len = function.body.len(); + for (index, stmt) in function.body.iter().enumerate() { + let start = builder.script().len(); + // Snapshot only when debug is enabled; used to derive per-statement var updates. + let env_before = inline_recorder.is_enabled().then(|| env.clone()); + if matches!(stmt.kind, StatementKind::Return { .. }) { + if index != body_len - 1 { + return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); + } + let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; + validate_return_types(exprs, &function.return_types, &types, contract_constants)?; + for expr in exprs { + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + yields.push(resolved); + } + inline_recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; + continue; } + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + builder, + options, + &[], + 0, + contract_constants, + functions, + function_order, + callee_index, + &mut yields, + script_size, + &mut inline_recorder, + )?; + let end = builder.script().len(); + inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } - ctx.debug_recorder.finish_inline_call_recording(call_span, ctx.builder.script().len(), name, &inline_recorder); + debug_recorder.finish_inline_call_recording(call_span, builder.script().len(), name, &inline_recorder); sync_inline_synthetic_bindings_back_to_caller(&env, &types, caller_env, caller_types); Ok(yields) } +#[allow(clippy::too_many_arguments)] fn compile_if_statement( condition: &Expr, - then_branch: &[SpannedStatement], - else_branch: Option<&[SpannedStatement]>, + then_branch: &[Statement], + else_branch: Option<&[Statement]>, env: &mut HashMap, + params: &HashMap, types: &mut HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( condition, env, - ctx.params, + params, types, - ctx.builder, - ctx.options, + builder, + options, &mut HashSet::new(), &mut stack_depth, - ctx.script_size, - ctx.contract_constants, + script_size, + contract_constants, )?; - ctx.builder.add_op(OpIf)?; + builder.add_op(OpIf)?; let original_env = env.clone(); let mut then_env = original_env.clone(); let mut then_types = types.clone(); - compile_block(then_branch, &mut then_env, &mut then_types, ctx)?; + compile_block( + then_branch, + &mut then_env, + params, + &mut then_types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; let mut else_env = original_env.clone(); if let Some(else_branch) = else_branch { - ctx.builder.add_op(OpElse)?; + builder.add_op(OpElse)?; let mut else_types = types.clone(); - compile_block(else_branch, &mut else_env, &mut else_types, ctx)?; + compile_block( + else_branch, + &mut else_env, + params, + &mut else_types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; } - ctx.builder.add_op(OpEndIf)?; + builder.add_op(OpEndIf)?; let resolved_condition = resolve_expr(condition.clone(), &original_env, &mut HashSet::new())?; merge_env_after_if(env, &original_env, &then_env, &else_env, &resolved_condition); @@ -1923,63 +2038,96 @@ fn compile_time_op_statement( tx_var: &TimeVar, expr: &Expr, env: &mut HashMap, + params: &HashMap, types: &HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + script_size: Option, + contract_constants: &HashMap, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; - compile_expr( - expr, - env, - ctx.params, - types, - ctx.builder, - ctx.options, - &mut HashSet::new(), - &mut stack_depth, - ctx.script_size, - ctx.contract_constants, - )?; + compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants)?; match tx_var { TimeVar::ThisAge => { - ctx.builder.add_op(OpCheckSequenceVerify)?; + builder.add_op(OpCheckSequenceVerify)?; } TimeVar::TxTime => { - ctx.builder.add_op(OpCheckLockTimeVerify)?; + builder.add_op(OpCheckLockTimeVerify)?; } } Ok(()) } +#[allow(clippy::too_many_arguments)] fn compile_block( - statements: &[SpannedStatement], + statements: &[Statement], env: &mut HashMap, + params: &HashMap, types: &mut HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, ) -> Result<(), CompilerError> { for stmt in statements { - let start = ctx.builder.script().len(); + let start = builder.script().len(); // Snapshot only when debug is enabled; used to derive per-statement var updates. - let env_before = ctx.debug_recorder.is_enabled().then(|| env.clone()); - compile_statement(stmt, env, types, ctx)?; - let end = ctx.builder.script().len(); - ctx.debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; + let env_before = debug_recorder.is_enabled().then(|| env.clone()); + compile_statement( + stmt, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; + let end = builder.script().len(); + debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; } Ok(()) } +#[allow(clippy::too_many_arguments)] fn compile_for_statement( ident: &str, start_expr: &Expr, end_expr: &Expr, - body: &[SpannedStatement], + body: &[Statement], env: &mut HashMap, + params: &HashMap, types: &mut HashMap, - ctx: &mut CompileCtx<'_>, + builder: &mut ScriptBuilder, + options: CompileOptions, + contract_fields: &[ContractFieldAst], + contract_field_prefix_len: usize, + contract_constants: &HashMap, + functions: &HashMap, + function_order: &HashMap, + function_index: usize, + yields: &mut Vec, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, ) -> Result<(), CompilerError> { - let start = eval_const_int(start_expr, ctx.contract_constants)?; - let end = eval_const_int(end_expr, ctx.contract_constants)?; + let start = eval_const_int(start_expr, contract_constants)?; + let end = eval_const_int(end_expr, contract_constants)?; if end < start { return Err(CompilerError::Unsupported("for loop end must be >= start".to_string())); } @@ -1988,7 +2136,23 @@ fn compile_for_statement( let previous = env.get(&name).cloned(); for value in start..end { env.insert(name.clone(), Expr::Int(value)); - compile_block(body, env, types, ctx)?; + compile_block( + body, + env, + params, + types, + builder, + options, + contract_fields, + contract_field_prefix_len, + contract_constants, + functions, + function_order, + function_index, + yields, + script_size, + debug_recorder, + )?; } match previous { @@ -2038,87 +2202,78 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result Result, -) -> Result { +fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet) -> Result { match expr { - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(rewrite_child(*expr)?) }), - Expr::Binary { op, left, right } => { - Ok(Expr::Binary { op, left: Box::new(rewrite_child(*left)?), right: Box::new(rewrite_child(*right)?) }) + Expr::Identifier(name) => { + // Keep synthetic inline args unresolved in compile mode so generated + // bytecode still reads them from caller stack bindings. + if is_inline_synthetic_name(&name) { + return Ok(Expr::Identifier(name)); + } + if let Some(value) = env.get(&name) { + if !visiting.insert(name.clone()) { + return Err(CompilerError::CyclicIdentifier(name)); + } + let resolved = resolve_expr(value.clone(), env, visiting)?; + visiting.remove(&name); + Ok(resolved) + } else { + Ok(Expr::Identifier(name)) + } } + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), + Expr::Binary { op, left, right } => Ok(Expr::Binary { + op, + left: Box::new(resolve_expr(*left, env, visiting)?), + right: Box::new(resolve_expr(*right, env, visiting)?), + }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(rewrite_child(*condition)?), - then_expr: Box::new(rewrite_child(*then_expr)?), - else_expr: Box::new(rewrite_child(*else_expr)?), + condition: Box::new(resolve_expr(*condition, env, visiting)?), + then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), + else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), }), Expr::Array(values) => { - let mut rewritten = Vec::with_capacity(values.len()); + let mut resolved = Vec::with_capacity(values.len()); for value in values { - rewritten.push(rewrite_child(value)?); + resolved.push(resolve_expr(value, env, visiting)?); } - Ok(Expr::Array(rewritten)) + Ok(Expr::Array(resolved)) } Expr::StateObject(fields) => { - let mut rewritten_fields = Vec::with_capacity(fields.len()); + let mut resolved_fields = Vec::with_capacity(fields.len()); for field in fields { - rewritten_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: rewrite_child(field.expr)? }); + resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); } - Ok(Expr::StateObject(rewritten_fields)) + Ok(Expr::StateObject(resolved_fields)) } Expr::Call { name, args } => { - let mut rewritten = Vec::with_capacity(args.len()); + let mut resolved = Vec::with_capacity(args.len()); for arg in args { - rewritten.push(rewrite_child(arg)?); + resolved.push(resolve_expr(arg, env, visiting)?); } - Ok(Expr::Call { name, args: rewritten }) + Ok(Expr::Call { name, args: resolved }) } Expr::New { name, args } => { - let mut rewritten = Vec::with_capacity(args.len()); + let mut resolved = Vec::with_capacity(args.len()); for arg in args { - rewritten.push(rewrite_child(arg)?); + resolved.push(resolve_expr(arg, env, visiting)?); } - Ok(Expr::New { name, args: rewritten }) - } - Expr::Split { source, index, part } => { - Ok(Expr::Split { source: Box::new(rewrite_child(*source)?), index: Box::new(rewrite_child(*index)?), part }) + Ok(Expr::New { name, args: resolved }) } - Expr::Slice { source, start, end } => Ok(Expr::Slice { - source: Box::new(rewrite_child(*source)?), - start: Box::new(rewrite_child(*start)?), - end: Box::new(rewrite_child(*end)?), + Expr::Split { source, index, part } => Ok(Expr::Split { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + part, }), - Expr::ArrayIndex { source, index } => { - Ok(Expr::ArrayIndex { source: Box::new(rewrite_child(*source)?), index: Box::new(rewrite_child(*index)?) }) - } - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(rewrite_child(*index)?) }), + Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + }), + Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), other => Ok(other), } } -fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet) -> Result { - match expr { - Expr::Identifier(name) => { - // Preserve synthetic inline placeholders in compile mode so - // generated bytecode keeps reading caller stack arguments. - if name.starts_with("__arg_") { - return Ok(Expr::Identifier(name)); - } - if let Some(value) = env.get(&name) { - if !visiting.insert(name.clone()) { - return Err(CompilerError::CyclicIdentifier(name)); - } - let resolved = resolve_expr(value.clone(), env, visiting)?; - visiting.remove(&name); - Ok(resolved) - } else { - Ok(Expr::Identifier(name)) - } - } - other => rewrite_expr_children(other, |child| resolve_expr(child, env, visiting)), - } -} - /// Compiles a pre-resolved expression for debugger shadow evaluation. pub fn compile_debug_expr( expr: &Expr, @@ -2144,14 +2299,23 @@ pub fn compile_debug_expr( Ok(builder.drain()) } -pub(super) fn expand_inline_args( +pub(super) fn resolve_expr_for_debug( + expr: Expr, + env: &HashMap, + visiting: &mut HashSet, +) -> Result { + let resolved = resolve_expr(expr, env, visiting)?; + expand_inline_arg_placeholders(resolved, env, &mut HashSet::new()) +} + +fn expand_inline_arg_placeholders( expr: Expr, env: &HashMap, visiting: &mut HashSet, ) -> Result { match expr { Expr::Identifier(name) => { - if !name.starts_with("__arg_") { + if !is_inline_synthetic_name(&name) { return Ok(Expr::Identifier(name)); } let Some(value) = env.get(&name).cloned() else { @@ -2160,11 +2324,65 @@ pub(super) fn expand_inline_args( if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let expanded = expand_inline_args(value, env, visiting)?; + let expanded = expand_inline_arg_placeholders(value, env, visiting)?; visiting.remove(&name); Ok(expanded) } - other => rewrite_expr_children(other, |child| expand_inline_args(child, env, visiting)), + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }), + Expr::Binary { op, left, right } => Ok(Expr::Binary { + op, + left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), + right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), + }), + Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { + condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), + then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), + else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), + }), + Expr::Array(values) => { + let mut expanded = Vec::with_capacity(values.len()); + for value in values { + expanded.push(expand_inline_arg_placeholders(value, env, visiting)?); + } + Ok(Expr::Array(expanded)) + } + Expr::StateObject(fields) => { + let mut expanded_fields = Vec::with_capacity(fields.len()); + for field in fields { + expanded_fields.push(crate::ast::StateFieldExpr { + name: field.name, + expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, + }); + } + Ok(Expr::StateObject(expanded_fields)) + } + Expr::Call { name, args } => { + let mut expanded = Vec::with_capacity(args.len()); + for arg in args { + expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::Call { name, args: expanded }) + } + Expr::New { name, args } => { + let mut expanded = Vec::with_capacity(args.len()); + for arg in args { + expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::New { name, args: expanded }) + } + Expr::Split { source, index, part } => Ok(Expr::Split { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + part, + }), + Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + }), + Expr::Introspection { kind, index } => { + Ok(Expr::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?) }) + } + other => Ok(other), } } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index d875cd8a..a92201cb 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -2,13 +2,13 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; -use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, SpannedStatement}; +use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; use crate::debug::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugVariableUpdate, SourceSpan, }; -use super::{CompilerError, expand_inline_args, resolve_expr}; +use super::{CompilerError, is_inline_synthetic_name, resolve_expr_for_debug}; type ResolvedVariableUpdate = (String, String, Expr); @@ -133,7 +133,7 @@ impl FunctionDebugRecorder { fn record_statement_updates( &mut self, - stmt: &SpannedStatement, + stmt: &Statement, bytecode_start: usize, bytecode_end: usize, variables: Vec, @@ -149,7 +149,7 @@ impl FunctionDebugRecorder { /// evaluation can compute values from the current state. pub fn record_statement_with_env_diff( &mut self, - stmt: &SpannedStatement, + stmt: &Statement, bytecode_start: usize, bytecode_end: usize, before_env: Option<&HashMap>, @@ -252,7 +252,7 @@ impl FunctionDebugRecorder { let mut updates = Vec::new(); for name in names { // Inline synthetic args are plumbing, not user-facing variables. - if name.starts_with("__arg_") { + if is_inline_synthetic_name(&name) { continue; } let Some(after_expr) = after_env.get(&name) else { @@ -284,8 +284,7 @@ impl FunctionDebugRecorder { if !self.enabled { return Ok(()); } - let resolved = resolve_expr(expr, env, &mut HashSet::new())?; - let resolved = expand_inline_args(resolved, env, &mut HashSet::new())?; + let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; variables.push((name.to_string(), type_name.to_string(), resolved)); Ok(()) } diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index 803988fe..e0606afc 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, Statement, parse_contract_ast}; +use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; fn extract_first_expr(source: &str) -> Expr { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; match &statement.kind { - Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - Statement::Require { expr, .. } => expr.clone(), + StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + StatementKind::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } From f01e17161f7530231aeae2ee9efdf4eba2ce0206 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:19:09 +0200 Subject: [PATCH 20/41] Deduplicate expression recursion in resolve and inline arg expansion --- silverscript-lang/src/compiler.rs | 156 ++++++++++-------------------- 1 file changed, 53 insertions(+), 103 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 12b902ac..32b85a51 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2202,78 +2202,82 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result, visiting: &mut HashSet) -> Result { +fn rewrite_expr_children(expr: Expr, mut recurse: impl FnMut(Expr) -> Result) -> Result { match expr { - Expr::Identifier(name) => { - // Keep synthetic inline args unresolved in compile mode so generated - // bytecode still reads them from caller stack bindings. - if is_inline_synthetic_name(&name) { - return Ok(Expr::Identifier(name)); - } - if let Some(value) = env.get(&name) { - if !visiting.insert(name.clone()) { - return Err(CompilerError::CyclicIdentifier(name)); - } - let resolved = resolve_expr(value.clone(), env, visiting)?; - visiting.remove(&name); - Ok(resolved) - } else { - Ok(Expr::Identifier(name)) - } + Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(recurse(*expr)?) }), + Expr::Binary { op, left, right } => { + Ok(Expr::Binary { op, left: Box::new(recurse(*left)?), right: Box::new(recurse(*right)?) }) } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }), - Expr::Binary { op, left, right } => Ok(Expr::Binary { - op, - left: Box::new(resolve_expr(*left, env, visiting)?), - right: Box::new(resolve_expr(*right, env, visiting)?), - }), Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(resolve_expr(*condition, env, visiting)?), - then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), - else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + condition: Box::new(recurse(*condition)?), + then_expr: Box::new(recurse(*then_expr)?), + else_expr: Box::new(recurse(*else_expr)?), }), Expr::Array(values) => { - let mut resolved = Vec::with_capacity(values.len()); + let mut rewritten = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr(value, env, visiting)?); + rewritten.push(recurse(value)?); } - Ok(Expr::Array(resolved)) + Ok(Expr::Array(rewritten)) } Expr::StateObject(fields) => { - let mut resolved_fields = Vec::with_capacity(fields.len()); + let mut rewritten_fields = Vec::with_capacity(fields.len()); for field in fields { - resolved_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: resolve_expr(field.expr, env, visiting)? }); + rewritten_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: recurse(field.expr)? }); } - Ok(Expr::StateObject(resolved_fields)) + Ok(Expr::StateObject(rewritten_fields)) } Expr::Call { name, args } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(recurse(arg)?); } - Ok(Expr::Call { name, args: resolved }) + Ok(Expr::Call { name, args: rewritten }) } Expr::New { name, args } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(recurse(arg)?); } - Ok(Expr::New { name, args: resolved }) + Ok(Expr::New { name, args: rewritten }) } - Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), - part, - }), - Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), - }), - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?) }), + Expr::Split { source, index, part } => { + Ok(Expr::Split { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?), part }) + } + Expr::Slice { source, start, end } => { + Ok(Expr::Slice { source: Box::new(recurse(*source)?), start: Box::new(recurse(*start)?), end: Box::new(recurse(*end)?) }) + } + Expr::ArrayIndex { source, index } => { + Ok(Expr::ArrayIndex { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?) }) + } + Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(recurse(*index)?) }), other => Ok(other), } } +fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet) -> Result { + match expr { + Expr::Identifier(name) => { + // Keep synthetic inline args unresolved in compile mode so generated + // bytecode still reads them from caller stack bindings. + if is_inline_synthetic_name(&name) { + return Ok(Expr::Identifier(name)); + } + if let Some(value) = env.get(&name) { + if !visiting.insert(name.clone()) { + return Err(CompilerError::CyclicIdentifier(name)); + } + let resolved = resolve_expr(value.clone(), env, visiting)?; + visiting.remove(&name); + Ok(resolved) + } else { + Ok(Expr::Identifier(name)) + } + } + other => rewrite_expr_children(other, |child| resolve_expr(child, env, visiting)), + } +} + /// Compiles a pre-resolved expression for debugger shadow evaluation. pub fn compile_debug_expr( expr: &Expr, @@ -2328,61 +2332,7 @@ fn expand_inline_arg_placeholders( visiting.remove(&name); Ok(expanded) } - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }), - Expr::Binary { op, left, right } => Ok(Expr::Binary { - op, - left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), - right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), - }), - Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), - then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), - else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), - }), - Expr::Array(values) => { - let mut expanded = Vec::with_capacity(values.len()); - for value in values { - expanded.push(expand_inline_arg_placeholders(value, env, visiting)?); - } - Ok(Expr::Array(expanded)) - } - Expr::StateObject(fields) => { - let mut expanded_fields = Vec::with_capacity(fields.len()); - for field in fields { - expanded_fields.push(crate::ast::StateFieldExpr { - name: field.name, - expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, - }); - } - Ok(Expr::StateObject(expanded_fields)) - } - Expr::Call { name, args } => { - let mut expanded = Vec::with_capacity(args.len()); - for arg in args { - expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); - } - Ok(Expr::Call { name, args: expanded }) - } - Expr::New { name, args } => { - let mut expanded = Vec::with_capacity(args.len()); - for arg in args { - expanded.push(expand_inline_arg_placeholders(arg, env, visiting)?); - } - Ok(Expr::New { name, args: expanded }) - } - Expr::Split { source, index, part } => Ok(Expr::Split { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), - part, - }), - Expr::ArrayIndex { source, index } => Ok(Expr::ArrayIndex { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), - }), - Expr::Introspection { kind, index } => { - Ok(Expr::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?) }) - } - other => Ok(other), + other => rewrite_expr_children(other, |child| expand_inline_arg_placeholders(child, env, visiting)), } } From 3337f9c1a3156841b216ddd875533638e9029a7d Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:18:47 +0200 Subject: [PATCH 21/41] Simplify inline call flow and remove synthetic arg helpers --- silverscript-lang/src/compiler.rs | 112 +++++------------- .../src/compiler/debug_recording.rs | 4 +- 2 files changed, 34 insertions(+), 82 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 32b85a51..99090617 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1737,25 +1737,30 @@ fn compile_validate_output_state_statement( Ok(()) } -const INLINE_SYNTHETIC_ARG_PREFIX: &str = "__arg_"; - -pub(super) fn is_inline_synthetic_name(name: &str) -> bool { - name.starts_with(INLINE_SYNTHETIC_ARG_PREFIX) -} - -fn make_inline_synthetic_name(callee: &str, index: usize) -> String { - format!("{INLINE_SYNTHETIC_ARG_PREFIX}{callee}_{index}") -} - -type InlineScope = (HashMap, HashMap); - -fn validate_inline_call_signature( +#[allow(clippy::too_many_arguments)] +fn compile_inline_call( name: &str, - function: &FunctionAst, args: &[Expr], - caller_types: &HashMap, + call_span: Option, + caller_params: &HashMap, + caller_types: &mut HashMap, + caller_env: &mut HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, contract_constants: &HashMap, -) -> Result<(), CompilerError> { + functions: &HashMap, + function_order: &HashMap, + caller_index: usize, + script_size: Option, + debug_recorder: &mut FunctionDebugRecorder, +) -> Result, CompilerError> { + let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + let callee_index = + function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; + if callee_index >= caller_index { + return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); + } + if function.params.len() != args.len() { return Err(CompilerError::Unsupported(format!("function '{}' expects {} arguments", name, function.params.len()))); } @@ -1764,31 +1769,18 @@ fn validate_inline_call_signature( if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, contract_constants) { return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); } - } - for param in &function.params { - let param_type_name = type_name_from_ref(¶m.type_ref); if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); } } - Ok(()) -} -fn build_inline_scope( - callee_name: &str, - function: &FunctionAst, - args: &[Expr], - caller_env: &mut HashMap, - caller_types: &mut HashMap, - contract_constants: &HashMap, -) -> Result { let mut types = function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let synthetic_name = make_inline_synthetic_name(callee_name, index); + let synthetic_name = format!("__arg_{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); // Inline calls bind each callee parameter to a synthetic identifier so // callee expressions keep a stable name while still pointing at the @@ -1800,53 +1792,6 @@ fn build_inline_scope( caller_types.insert(synthetic_name, param_type_name); } - Ok((env, types)) -} - -fn sync_inline_synthetic_bindings_back_to_caller( - env: &HashMap, - types: &HashMap, - caller_env: &mut HashMap, - caller_types: &mut HashMap, -) { - for (name, value) in env { - if !is_inline_synthetic_name(name) { - continue; - } - if let Some(type_name) = types.get(name) { - caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); - } - caller_env.entry(name.clone()).or_insert_with(|| value.clone()); - } -} - -#[allow(clippy::too_many_arguments)] -fn compile_inline_call( - name: &str, - args: &[Expr], - call_span: Option, - caller_params: &HashMap, - caller_types: &mut HashMap, - caller_env: &mut HashMap, - builder: &mut ScriptBuilder, - options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, - function_order: &HashMap, - caller_index: usize, - script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, -) -> Result, CompilerError> { - let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - let callee_index = - function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; - if callee_index >= caller_index { - return Err(CompilerError::Unsupported("functions may only call earlier-defined functions".to_string())); - } - - validate_inline_call_signature(name, function, args, caller_types, contract_constants)?; - let (mut env, mut types) = build_inline_scope(name, function, args, caller_env, caller_types, contract_constants)?; - if !options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); } @@ -1917,7 +1862,14 @@ fn compile_inline_call( debug_recorder.finish_inline_call_recording(call_span, builder.script().len(), name, &inline_recorder); - sync_inline_synthetic_bindings_back_to_caller(&env, &types, caller_env, caller_types); + for (name, value) in &env { + if name.starts_with("__arg_") { + if let Some(type_name) = types.get(name) { + caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + caller_env.entry(name.clone()).or_insert_with(|| value.clone()); + } + } Ok(yields) } @@ -2260,7 +2212,7 @@ fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet< Expr::Identifier(name) => { // Keep synthetic inline args unresolved in compile mode so generated // bytecode still reads them from caller stack bindings. - if is_inline_synthetic_name(&name) { + if name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } if let Some(value) = env.get(&name) { @@ -2319,7 +2271,7 @@ fn expand_inline_arg_placeholders( ) -> Result { match expr { Expr::Identifier(name) => { - if !is_inline_synthetic_name(&name) { + if !name.starts_with("__arg_") { return Ok(Expr::Identifier(name)); } let Some(value) = env.get(&name).cloned() else { diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index a92201cb..573b3584 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -8,7 +8,7 @@ use crate::debug::{ DebugVariableUpdate, SourceSpan, }; -use super::{CompilerError, is_inline_synthetic_name, resolve_expr_for_debug}; +use super::{CompilerError, resolve_expr_for_debug}; type ResolvedVariableUpdate = (String, String, Expr); @@ -252,7 +252,7 @@ impl FunctionDebugRecorder { let mut updates = Vec::new(); for name in names { // Inline synthetic args are plumbing, not user-facing variables. - if is_inline_synthetic_name(&name) { + if name.starts_with("__arg_") { continue; } let Some(after_expr) = after_env.get(&name) else { From 0008fb5c99ca4e4fd066eacf7b7bbce8939a5e6e Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:09:22 +0200 Subject: [PATCH 22/41] Fix inline call bug --- silverscript-lang/src/compiler.rs | 12 + .../src/compiler/debug_recording.rs | 39 +++ silverscript-lang/src/debug/session.rs | 69 +++-- .../tests/debug_session_tests.rs | 255 +++++++++++++++++- 4 files changed, 334 insertions(+), 41 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 99090617..12af9066 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1778,6 +1778,17 @@ fn compile_inline_call( function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); let mut env: HashMap = contract_constants.clone(); + // Preserve caller synthetic inline bindings so nested inline calls can + // continue resolving chains like __arg_inner_0 -> __arg_outer_0. + for (name, value) in caller_env.iter() { + if name.starts_with("__arg_") { + env.entry(name.clone()).or_insert_with(|| value.clone()); + if let Some(type_name) = caller_types.get(name) { + types.entry(name.clone()).or_insert_with(|| type_name.clone()); + } + } + } + for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; let synthetic_name = format!("__arg_{name}_{index}"); @@ -1816,6 +1827,7 @@ fn compile_inline_call( let call_start = builder.script().len(); // Record call boundary on caller frame and collect callee events in a child frame. let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); + inline_recorder.record_inline_param_updates(function, &env, call_span, call_start)?; let mut yields: Vec = Vec::new(); // Use caller parameter stack indexes while compiling callee bytecode so diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 573b3584..badda453 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -59,6 +59,9 @@ impl FunctionDebugRecorder { fn new_inline_child(&mut self) -> Self { let frame_id = self.next_frame_id; self.next_frame_id = self.next_frame_id.saturating_add(1); + // Child starts allocating from the parent's current frontier. + // Parent frontier is reconciled back in `merge_inline_events` after the + // child returns, so sibling inline calls never reuse frame ids. Self { function_name: self.function_name.clone(), enabled: self.enabled, @@ -161,6 +164,38 @@ impl FunctionDebugRecorder { Ok(()) } + /// Emits explicit inline-callee param updates on the callee's next + /// statement sequence, so args are visible after stepping into the call + /// without adding an extra source step at the call-site. + pub fn record_inline_param_updates( + &mut self, + function: &FunctionAst, + env: &HashMap, + span: Option, + bytecode_offset: usize, + ) -> Result<(), CompilerError> { + if !self.enabled { + return Ok(()); + } + // Anchor inline param updates to the next callee statement sequence. + // We intentionally "peek" (do not consume) so these updates align with + // the first real callee statement event sequence. + // without creating an extra steppable mapping at the call-site span. + let sequence = self.next_seq; + let mut variables = Vec::new(); + for param in &function.params { + self.variable_update( + env, + &mut variables, + ¶m.name, + ¶m.type_ref.type_name(), + env.get(¶m.name).cloned().unwrap_or_else(|| Expr::Identifier(param.name.clone())), + )?; + } + self.record_variable_updates(variables, bytecode_offset, span, sequence); + Ok(()) + } + /// Starts an inline call recording session and returns a child recorder for /// callee body statements. pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { @@ -182,6 +217,8 @@ impl FunctionDebugRecorder { fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { if !self.enabled || inline.events.is_empty() { + // Keep frame-id frontier monotonic even if the inline call recorded + // no events; this preserves uniqueness for later sibling calls. self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); return; } @@ -205,6 +242,8 @@ impl FunctionDebugRecorder { self.variable_updates.push(update); } } + // Child may allocate nested frame ids; advance parent frontier so later + // sibling inline calls start after the whole child subtree. self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); } diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 6beca9c6..5365dec0 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -88,9 +88,6 @@ pub struct DebugSession<'a> { debug_info: DebugInfo, source_mappings: Vec, current_step_index: Option, - // When sequence metadata exists, prefer sequence/frame semantics for step order - // and variable visibility (handles overlapping mapping ranges deterministically). - uses_sequence_order: bool, source_lines: Vec, breakpoints: HashSet, // Sequence ids of steppable mappings that were already visited in this session. @@ -152,8 +149,6 @@ impl<'a> DebugSession<'a> { let source_lines: Vec = source.lines().map(String::from).collect(); let (opcode_offsets, script_len) = build_opcode_offsets(&opcodes); - let uses_sequence_order = debug_info.mappings.iter().any(|mapping| mapping.sequence != 0) - || debug_info.variable_updates.iter().any(|update| update.sequence != 0); let mut source_mappings: Vec = debug_info .mappings .iter() @@ -168,15 +163,16 @@ impl<'a> DebugSession<'a> { }) .cloned() .collect(); - // Sort by bytecode range first, then by event kind/depth. If sequence is - // available, use it as the final tie-breaker for overlapping events. + // Overlapping inline ranges can share the same bytecode offsets; keep + // compiler emission order via sequence before comparing range width. source_mappings.sort_by_key(|mapping| { ( mapping.bytecode_start, - mapping.bytecode_end, + mapping.sequence, mapping_kind_order(&mapping.kind), mapping.call_depth, - if uses_sequence_order { mapping.sequence } else { 0 }, + mapping.bytecode_end, + mapping.frame_id, ) }); @@ -190,7 +186,6 @@ impl<'a> DebugSession<'a> { debug_info, source_mappings, current_step_index: None, - uses_sequence_order, source_lines, breakpoints: HashSet::new(), executed_sequences: HashSet::new(), @@ -430,7 +425,6 @@ impl<'a> DebugSession<'a> { } /// Returns a specific variable by name, or error if not in scope. - /// Retrieves a specific variable by name with its current value. pub fn variable_by_name(&self, name: &str) -> Result { let (sequence, frame_id) = self.current_step_sequence_and_frame(); let context = self.current_variable_context(sequence, frame_id)?; @@ -500,19 +494,10 @@ impl<'a> DebugSession<'a> { .iter() .filter(|update| self.update_is_visible(update, function_name, offset, sequence, frame_id)) { - if self.uses_sequence_order { - match latest.get(&update.name) { - Some(existing) if existing.sequence > update.sequence => {} - _ => { - latest.insert(update.name.clone(), update); - } - } - } else { - match latest.get(&update.name) { - Some(existing) if existing.bytecode_offset > update.bytecode_offset => {} - _ => { - latest.insert(update.name.clone(), update); - } + match latest.get(&update.name) { + Some(existing) if existing.sequence > update.sequence => {} + _ => { + latest.insert(update.name.clone(), update); } } } @@ -559,6 +544,8 @@ impl<'a> DebugSession<'a> { ); } + // Contract constants are contract-scoped, not frame-scoped, so they + // remain visible while stepping through inline callee frames. for constant in &self.debug_info.constants { if variables.contains_key(&constant.name) { continue; @@ -589,16 +576,12 @@ impl<'a> DebugSession<'a> { if update.function != function_name { return false; } - if self.uses_sequence_order { - // Sequence-aware mode: stay in the active inline frame and only - // consider updates from steps already executed in this session. - update.frame_id == frame_id - && self.executed_sequences.contains(&update.sequence) - && update.sequence < sequence - && update.bytecode_offset <= offset - } else { - update.bytecode_offset <= offset - } + // Stay in the active inline frame and only consider updates from + // steps already executed in this session. + update.frame_id == frame_id + && self.executed_sequences.contains(&update.sequence) + && update.sequence < sequence + && update.bytecode_offset <= offset } /// Returns the most specific mapping for `offset`. @@ -637,6 +620,12 @@ impl<'a> DebugSession<'a> { fn sync_step_cursor_to_current_offset(&mut self) { let offset = self.current_byte_offset(); if let Some(index) = self.steppable_mapping_index_for_offset(offset) { + if self.current_step_index.is_some_and(|current| index < current) { + // In sequence mode multiple steps may map to the same byte offset. + // Keep cursor monotonic and avoid snapping backward to an earlier + // mapping for that offset. + return; + } // `si` executes raw opcodes; keep statement cursor in sync so later // source-level steps (`next`/`step`/`finish`) start from the real // current mapping instead of an old one. @@ -646,7 +635,10 @@ impl<'a> DebugSession<'a> { } fn is_steppable_mapping(&self, mapping: &DebugMapping) -> bool { - matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {}) + // InlineCallEnter is steppable so `step_into` can land on a call + // boundary and build call-stack transitions. InlineCallExit is not + // steppable to avoid synthetic extra stops while unwinding. + matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {} | MappingKind::InlineCallEnter { .. }) } fn steppable_mapping_index_for_offset(&self, offset: usize) -> Option { @@ -923,7 +915,7 @@ mod tests { sig_builder.add_i64(5).unwrap(); let sigscript = sig_builder.drain(); - let session = make_session( + let mut session = make_session( vec![DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }], vec![DebugVariableUpdate { name: "x".to_string(), @@ -939,7 +931,10 @@ mod tests { ) .unwrap(); - let vars = session.list_variables().unwrap(); + session.executed_sequences.insert(0); + // In sequence-only mode, query visibility at an explicit sequence that + // is after the update's sequence. + let vars = session.list_variables_at_sequence(1, 0).unwrap(); let x = vars.into_iter().find(|var| var.name == "x").expect("x variable"); assert!(matches!(x.value, DebugValue::Unknown(_))); } diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index f4382117..95a2b5f7 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -407,8 +407,14 @@ contract InlineCalls() { assert_eq!(start.line, 10); session.step_over()?; - let after_over = session.current_span().ok_or("missing span after step_over")?; - assert_eq!(after_over.line, 11, "step_over should stay in caller and move past inline call"); + let mut after_over = session.current_span().ok_or("missing span after step_over")?; + if after_over.line == 10 { + // In simplified inline stepping mode we may stop once on the call-site + // boundary before advancing past the call. + session.step_over()?; + after_over = session.current_span().ok_or("missing span after second step_over")?; + } + assert_eq!(after_over.line, 11, "step_over should eventually move past inline call"); let b = session.variable_by_name("b")?; assert_eq!(session.format_value(&b.type_name, &b.value), "4", "inline return should resolve against caller params"); Ok(()) @@ -417,12 +423,21 @@ contract InlineCalls() { with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { session.run_to_first_executed_statement()?; session.step_into()?; - let in_callee = session.current_span().ok_or("missing span in callee")?; + let mut in_callee = session.current_span().ok_or("missing span in callee")?; + if in_callee.line == 10 { + // First stop can be the inline enter boundary on the caller line. + session.step_into()?; + in_callee = session.current_span().ok_or("missing span in callee after second step_into")?; + } assert_eq!(in_callee.line, 5, "step_into should enter callee body"); assert_eq!(session.call_stack(), vec!["addOne".to_string()]); session.step_out()?; - let after_out = session.current_span().ok_or("missing span after step_out")?; + let mut after_out = session.current_span().ok_or("missing span after step_out")?; + if after_out.line == 10 { + session.step_over()?; + after_out = session.current_span().ok_or("missing span after post-step_out step_over")?; + } assert_eq!(after_out.line, 11, "step_out should return to caller after inline call"); assert!(session.call_stack().is_empty(), "call stack should unwind after step_out"); Ok(()) @@ -430,3 +445,235 @@ contract InlineCalls() { Ok(()) } + +#[test] +fn debug_session_run_to_first_statement_starts_in_caller_for_inline_entry() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Repeat() { + function inc(int x) { + int y = x + 1; + require(y > 0); + } + + entrypoint function main(int a) { + inc(a); + inc(a); + require(a >= 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + session.run_to_first_executed_statement()?; + let start = session.current_span().ok_or("missing start span")?; + assert_eq!(start.line, 10, "first source step should be caller line, not callee internals"); + Ok(()) + }) +} + +#[test] +fn debug_session_step_into_repeated_inline_calls_preserves_order_and_stack() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract Repeat() { + function inc(int x) { + int y = x + 1; + require(y > 0); + } + + entrypoint function main(int a) { + inc(a); + inc(a); + require(a >= 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + session.run_to_first_executed_statement()?; + + let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; + let mut max_depth = session.call_stack().len(); + while let Some(_) = session.step_into()? { + lines.push(session.current_span().ok_or("missing span while stepping")?.line); + max_depth = max_depth.max(session.call_stack().len()); + } + + assert_eq!(max_depth, 1, "repeated inline calls should not nest call frames"); + let count_10 = lines.iter().filter(|&&line| line == 10).count(); + assert!(count_10 >= 2, "expected duplicate call-site stops for first call"); + assert!(lines.windows(2).any(|window| window == [5, 6]), "expected callee body stepping"); + assert_eq!(lines.last().copied(), Some(12), "final step should reach caller require"); + assert!(session.call_stack().is_empty(), "call stack should be empty after execution"); + Ok(()) + }) +} + +#[test] +fn debug_session_step_into_nested_inline_calls_preserves_execution_order() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract NestedNoArgs() { + function inner() { + int y = 1; + require(y > 0); + } + + function outer() { + inner(); + require(1 == 1); + } + + entrypoint function main() { + outer(); + require(1 == 1); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![], |session| { + session.run_to_first_executed_statement()?; + let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; + + for _ in 0..5 { + session.step_into()?.ok_or("expected additional source step")?; + lines.push(session.current_span().ok_or("missing span while stepping")?.line); + } + + assert_eq!(lines, vec![15, 10, 5, 6, 10, 15], "nested inline stepping order regressed"); + Ok(()) + }) +} + +#[test] +fn debug_session_inline_source_sequences_are_monotonic() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract DebugPoC(int const) { + function bump(int x) { + int y = x + 1; + require(y > 0); + } + + function check_pair(int leftInput, int rightInput) { + int left = leftInput + rightInput; + int right = left * 2; + require(right >= left); + } + + entrypoint function main(int a, int b) { + int seed = a + const; + check_pair(a, b); + bump(seed); + require(seed >= const); + require(b >= 0); + } +} +"#; + + with_session_for_source(source, vec![Expr::Int(0)], "main", vec![Expr::Int(0), Expr::Int(0)], |session| { + session.run_to_first_executed_statement()?; + + let initial = session.current_location().ok_or("missing initial location")?; + let mut prev_sequence = initial.sequence; + let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; + + while session.step_into()?.is_some() { + let loc = session.current_location().ok_or("missing location after step_into")?; + assert!( + loc.sequence >= prev_sequence, + "source sequence rewound from {} to {} (lines {:?})", + prev_sequence, + loc.sequence, + lines + ); + prev_sequence = loc.sequence; + lines.push(session.current_span().ok_or("missing span after step_into")?.line); + } + + assert!(lines.starts_with(&[16, 17, 10, 11, 12, 17, 18, 5]), "unexpected inline stepping prefix: {:?}", lines); + Ok(()) + }) +} + +#[test] +fn debug_session_inline_params_visible_inside_callee() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract InlineParams() { + function add1(int x) : (int) { + int y = x + 1; + require(y > 0); + return(y); + } + + entrypoint function main(int a) { + int seed = a; + (int r) = add1(seed); + require(r > 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { + session.run_to_first_executed_statement()?; + + let mut saw_inline_param = false; + for _ in 0..8 { + let in_callee = session.call_stack().iter().any(|name| name == "add1"); + if in_callee { + if let Ok(x) = session.variable_by_name("x") { + let rendered = session.format_value(&x.type_name, &x.value); + assert_eq!(rendered, "4", "inline param x should reflect caller-provided value"); + saw_inline_param = true; + break; + } + } + if session.step_into()?.is_none() { + break; + } + } + + assert!(saw_inline_param, "expected inline param x to be visible while inside add1"); + Ok(()) + }) +} + +#[test] +fn debug_session_nested_inline_calls_with_args_compile_and_step() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract NestedArgs() { + function inner(int x) { + int y = x + 1; + require(y > 0); + } + + function outer(int v) { + inner(v); + require(v >= 0); + } + + entrypoint function main(int a) { + outer(a); + require(a >= 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + session.run_to_first_executed_statement()?; + let start = session.current_span().ok_or("missing start span")?; + assert_eq!(start.line, 15); + + session.step_over()?; + let mut after_over = session.current_span().ok_or("missing span after step_over")?; + if after_over.line == 15 { + session.step_over()?; + after_over = session.current_span().ok_or("missing span after second step_over")?; + } + assert_eq!(after_over.line, 16, "step_over should move past nested inline call in caller"); + Ok(()) + }) +} From fbdfb92b1cb89cc7bbcdbacededfb1daef13236d Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:29:09 +0200 Subject: [PATCH 23/41] Fix clippy redundant pattern matching in debug session test --- silverscript-lang/tests/debug_session_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 95a2b5f7..594fd3e3 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -495,7 +495,7 @@ contract Repeat() { let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; let mut max_depth = session.call_stack().len(); - while let Some(_) = session.step_into()? { + while (session.step_into()?).is_some() { lines.push(session.current_span().ok_or("missing span while stepping")?.line); max_depth = max_depth.max(session.call_stack().len()); } From e89386178345db853946c5b6c73660731f589e31 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:44:17 +0200 Subject: [PATCH 24/41] Rename changes --- silverscript-lang/src/compiler.rs | 37 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 12af9066..6f6c3a86 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -140,19 +140,14 @@ fn compile_contract_impl( } } - let script = if without_selector { - let mut builder = ScriptBuilder::new(); - builder.add_ops(&field_prolog_script)?; + let entrypoint_script = if without_selector { let compiled = compiled_entrypoints .first() .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; - let start = builder.script().len(); - builder.add_ops(&compiled.script)?; - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); - builder.drain() + recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, field_prolog_script.len()); + compiled.script.clone() } else { let mut builder = ScriptBuilder::new(); - builder.add_ops(&field_prolog_script)?; let total = compiled_entrypoints.len(); for (index, compiled) in compiled_entrypoints.iter().enumerate() { record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_GUARD, |builder| { @@ -163,7 +158,7 @@ fn compile_contract_impl( builder.add_op(OpDrop)?; Ok(()) })?; - let start = builder.script().len(); + let start = field_prolog_script.len() + builder.script().len(); builder.add_ops(&compiled.script)?; recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ELSE, |builder| { @@ -187,6 +182,9 @@ fn compile_contract_impl( builder.drain() }; + let mut script = field_prolog_script.clone(); + script.extend(entrypoint_script); + if !uses_script_size { let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); return Ok(CompiledContract { @@ -1769,13 +1767,16 @@ fn compile_inline_call( if !expr_matches_type_with_env(arg, ¶m_type_name, caller_types, contract_constants) { return Err(CompilerError::Unsupported(format!("function argument '{}' expects {}", param.name, param_type_name))); } - if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { - return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); - } } let mut types = function.params.iter().map(|param| (param.name.clone(), type_name_from_ref(¶m.type_ref))).collect::>(); + for param in &function.params { + let param_type_name = type_name_from_ref(¶m.type_ref); + if is_array_type(¶m_type_name) && array_element_size(¶m_type_name).is_none() { + return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); + } + } let mut env: HashMap = contract_constants.clone(); // Preserve caller synthetic inline bindings so nested inline calls can @@ -1791,16 +1792,16 @@ fn compile_inline_call( for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let synthetic_name = format!("__arg_{name}_{index}"); + let temp_name = format!("__arg_{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); // Inline calls bind each callee parameter to a synthetic identifier so // callee expressions keep a stable name while still pointing at the // caller-provided argument expression. - env.insert(synthetic_name.clone(), resolved.clone()); - types.insert(synthetic_name.clone(), param_type_name.clone()); - env.insert(param.name.clone(), Expr::Identifier(synthetic_name.clone())); - caller_env.insert(synthetic_name.clone(), resolved); - caller_types.insert(synthetic_name, param_type_name); + env.insert(temp_name.clone(), resolved.clone()); + types.insert(temp_name.clone(), param_type_name.clone()); + env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); + caller_env.insert(temp_name.clone(), resolved); + caller_types.insert(temp_name, param_type_name); } if !options.allow_yield && function.body.iter().any(contains_yield) { From d3d6469ce352762e3bd3967bc4472c6b8dfee52c Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:24:18 +0200 Subject: [PATCH 25/41] feat(debugger): port debugger onto izio spanned-ast core --- silverscript-lang/Cargo.toml | 4 +- silverscript-lang/src/ast.rs | 1376 ++++--- silverscript-lang/src/bin/sil-debug.rs | 35 +- silverscript-lang/src/bin/silverc.rs | 140 +- silverscript-lang/src/compiler.rs | 3395 +++++++++-------- .../src/compiler/debug_recording.rs | 92 +- silverscript-lang/src/debug.rs | 36 +- silverscript-lang/src/debug/session.rs | 66 +- silverscript-lang/src/diagnostic/mod.rs | 5 + silverscript-lang/src/diagnostic/parse.rs | 217 ++ .../src/diagnostic/parse_diagnostics.rs | 178 + silverscript-lang/src/errors.rs | 52 + silverscript-lang/src/lib.rs | 3 + silverscript-lang/src/parser.rs | 18 +- silverscript-lang/src/span.rs | 81 + .../tests/ast_json/require_test.ast.json | 2 +- .../tests/ast_json/yield_test.ast.json | 2 +- silverscript-lang/tests/ast_json_tests.rs | 2 +- silverscript-lang/tests/ast_spans_tests.rs | 60 + .../tests/cashc_valid_examples_tests.rs | 4 +- silverscript-lang/tests/compiler_tests.rs | 83 +- silverscript-lang/tests/date_literal_tests.rs | 14 +- .../tests/debug_session_tests.rs | 42 +- .../tests/parse_diagnostics_tests.rs | 53 + silverscript-lang/tests/parser_tests.rs | 22 + silverscript-lang/tests/silverc_tests.rs | 111 +- .../tests/tutorial_rust_examples_tests.rs | 2 +- 27 files changed, 3897 insertions(+), 2198 deletions(-) create mode 100644 silverscript-lang/src/diagnostic/mod.rs create mode 100644 silverscript-lang/src/diagnostic/parse.rs create mode 100644 silverscript-lang/src/diagnostic/parse_diagnostics.rs create mode 100644 silverscript-lang/src/errors.rs create mode 100644 silverscript-lang/src/span.rs create mode 100644 silverscript-lang/tests/ast_spans_tests.rs create mode 100644 silverscript-lang/tests/parse_diagnostics_tests.rs diff --git a/silverscript-lang/Cargo.toml b/silverscript-lang/Cargo.toml index 746eb26d..02623350 100644 --- a/silverscript-lang/Cargo.toml +++ b/silverscript-lang/Cargo.toml @@ -23,9 +23,9 @@ rand.workspace = true secp256k1.workspace = true thiserror.workspace = true serde = { version = "1.0", features = ["derive"] } -clap = { version = "4.5", features = ["derive"] } -faster-hex = "0.10" serde_json = "1.0" +clap = { version = "4.5.60", features = ["derive"] } +faster-hex = "0.10" [dev-dependencies] kaspa-addresses.workspace = true diff --git a/silverscript-lang/src/ast.rs b/silverscript-lang/src/ast.rs index 07795ce3..d3768eb2 100644 --- a/silverscript-lang/src/ast.rs +++ b/silverscript-lang/src/ast.rs @@ -1,53 +1,96 @@ -use std::collections::HashMap; +use std::fmt; -use pest::Parser; +use chrono::NaiveDateTime; use pest::iterators::Pair; use serde::{Deserialize, Serialize}; -use crate::compiler::CompilerError; -use crate::debug::SourceSpan; -use crate::parser::{Rule, SilverScriptParser}; -use chrono::NaiveDateTime; +use crate::errors::CompilerError; +use crate::parser::{Rule, parse_source_file, parse_type_name as parse_type_name_rule}; +pub use crate::span::{Span, SpanUtils}; + +#[derive(Debug, Clone)] +struct Identifier<'i> { + name: String, + span: Span<'i>, +} #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractAst { +pub struct ContractAst<'i> { pub name: String, - pub params: Vec, + pub params: Vec>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub fields: Vec, - pub constants: HashMap, - pub functions: Vec, + pub fields: Vec>, + pub constants: Vec>, + pub functions: Vec>, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, +} + +impl<'i> fmt::Display for ContractAst<'i> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let pretty = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; + f.write_str(&pretty) + } } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractFieldAst { +pub struct ContractFieldAst<'i> { pub type_ref: TypeRef, pub name: String, - pub expr: Expr, + pub expr: Expr<'i>, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub type_span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionAst { +pub struct FunctionAst<'i> { pub name: String, - pub params: Vec, - #[serde(default)] + pub params: Vec>, pub entrypoint: bool, #[serde(default)] pub return_types: Vec, - pub body: Vec, + pub body: Vec>, + #[serde(skip_deserializing)] + pub return_type_spans: Vec>, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, + #[serde(skip_deserializing)] + pub body_span: Span<'i>, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ParamAst { +pub struct ParamAst<'i> { pub type_ref: TypeRef, pub name: String, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub type_span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StateBindingAst { +pub struct StateBindingAst<'i> { pub field_name: String, pub type_ref: TypeRef, pub name: String, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub field_span: Span<'i>, + #[serde(skip_deserializing)] + pub type_span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -122,38 +165,168 @@ impl TypeRef { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Statement { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub span: Option, - #[serde(flatten)] - pub kind: StatementKind, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum StatementKind { - VariableDefinition { type_ref: TypeRef, modifiers: Vec, name: String, expr: Option }, - TupleAssignment { left_type_ref: TypeRef, left_name: String, right_type_ref: TypeRef, right_name: String, expr: Expr }, - ArrayPush { name: String, expr: Expr }, - FunctionCall { name: String, args: Vec }, - FunctionCallAssign { bindings: Vec, name: String, args: Vec }, - StateFunctionCallAssign { bindings: Vec, name: String, args: Vec }, - Assign { name: String, expr: Expr }, - TimeOp { tx_var: TimeVar, expr: Expr, message: Option }, - Require { expr: Expr, message: Option }, - If { condition: Expr, then_branch: Vec, else_branch: Option> }, - For { ident: String, start: Expr, end: Expr, body: Vec }, - Yield { expr: Expr }, - Return { exprs: Vec }, - Console { args: Vec }, +pub enum Statement<'i> { + VariableDefinition { + type_ref: TypeRef, + #[serde(default)] + modifiers: Vec, + name: String, + expr: Option>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + type_span: Span<'i>, + #[serde(skip_deserializing)] + modifier_spans: Vec>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + TupleAssignment { + left_type_ref: TypeRef, + left_name: String, + right_type_ref: TypeRef, + right_name: String, + expr: Expr<'i>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + left_type_span: Span<'i>, + #[serde(skip_deserializing)] + left_name_span: Span<'i>, + #[serde(skip_deserializing)] + right_type_span: Span<'i>, + #[serde(skip_deserializing)] + right_name_span: Span<'i>, + }, + ArrayPush { + name: String, + expr: Expr<'i>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + FunctionCall { + name: String, + args: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + FunctionCallAssign { + bindings: Vec>, + name: String, + args: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + StateFunctionCallAssign { + bindings: Vec>, + name: String, + args: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + Assign { + name: String, + expr: Expr<'i>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + TimeOp { + tx_var: TimeVar, + expr: Expr<'i>, + message: Option, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + tx_var_span: Span<'i>, + #[serde(skip_deserializing)] + message_span: Option>, + }, + Require { + expr: Expr<'i>, + message: Option, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + message_span: Option>, + }, + If { + condition: Expr<'i>, + then_branch: Vec>, + else_branch: Option>>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + then_span: Span<'i>, + #[serde(skip_deserializing)] + else_span: Option>, + }, + For { + ident: String, + start: Expr<'i>, + end: Expr<'i>, + body: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + #[serde(skip_deserializing)] + ident_span: Span<'i>, + #[serde(skip_deserializing)] + body_span: Span<'i>, + }, + Yield { + expr: Expr<'i>, + #[serde(skip_deserializing)] + span: Span<'i>, + }, + Return { + exprs: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + }, + Console { + args: Vec>, + #[serde(skip_deserializing)] + span: Span<'i>, + }, +} + +impl<'i> Statement<'i> { + pub fn span(&self) -> Span<'i> { + match self { + Statement::VariableDefinition { span, .. } + | Statement::TupleAssignment { span, .. } + | Statement::ArrayPush { span, .. } + | Statement::FunctionCall { span, .. } + | Statement::FunctionCallAssign { span, .. } + | Statement::StateFunctionCallAssign { span, .. } + | Statement::Assign { span, .. } + | Statement::Return { span, .. } + | Statement::TimeOp { span, .. } + | Statement::Require { span, .. } + | Statement::If { span, .. } + | Statement::For { span, .. } + | Statement::Yield { span, .. } + | Statement::Console { span, .. } => *span, + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum ConsoleArg { - Identifier(String), - Literal(Expr), +pub enum ConsoleArg<'i> { + Identifier(String, #[serde(skip_deserializing)] Span<'i>), + Literal(Expr<'i>), } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -163,85 +336,200 @@ pub enum TimeVar { TxTime, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(tag = "kind", content = "data", rename_all = "snake_case")] -pub enum Expr { - Int(i64), - Bool(bool), - Byte(u8), - String(String), - Identifier(String), - Array(Vec), - Call { name: String, args: Vec }, - New { name: String, args: Vec }, - Split { source: Box, index: Box, part: SplitPart }, - Slice { source: Box, start: Box, end: Box }, - ArrayIndex { source: Box, index: Box }, - Unary { op: UnaryOp, expr: Box }, - Binary { op: BinaryOp, left: Box, right: Box }, - IfElse { condition: Box, then_expr: Box, else_expr: Box }, - Nullary(NullaryOp), - Introspection { kind: IntrospectionKind, index: Box }, - StateObject(Vec), +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Expr<'i> { + // TODO: evaluate splitting kind in two: + // - actual Expressions + // - user defined primitive Values + #[serde(flatten)] + pub kind: ExprKind<'i>, + #[serde(skip_deserializing)] + pub span: Span<'i>, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct StateFieldExpr { - pub name: String, - pub expr: Expr, +impl<'i> Expr<'i> { + pub fn new(kind: ExprKind<'i>, span: Span<'i>) -> Self { + Self { kind, span } + } + + pub fn int(value: i64) -> Self { + Self::new(ExprKind::Int(value), Span::default()) + } + + pub fn bool(value: bool) -> Self { + Self::new(ExprKind::Bool(value), Span::default()) + } + + pub fn byte(value: u8) -> Self { + Self::new(ExprKind::Byte(value), Span::default()) + } + + pub fn bytes(value: Vec) -> Self { + Self::new(ExprKind::Array(value.into_iter().map(Expr::byte).collect()), Span::default()) + } + + pub fn string(value: impl Into) -> Self { + Self::new(ExprKind::String(value.into()), Span::default()) + } + + pub fn identifier(value: impl Into) -> Self { + Self::new(ExprKind::Identifier(value.into()), Span::default()) + } + + pub fn call(name: impl Into, args: Vec>) -> Self { + Self::new(ExprKind::Call { name: name.into(), args, name_span: Span::default() }, Span::default()) + } } -impl From for Expr { +impl<'i> From for Expr<'i> { fn from(value: i64) -> Self { - Expr::Int(value) + Expr::int(value) } } -impl From for Expr { +impl<'i> From for Expr<'i> { fn from(value: bool) -> Self { - Expr::Bool(value) + Expr::bool(value) } } -impl From> for Expr { +impl<'i> From> for Expr<'i> { fn from(value: Vec) -> Self { - Expr::Array(value.into_iter().map(Expr::Byte).collect()) + Expr::bytes(value) } } -impl From for Expr { +impl<'i> From for Expr<'i> { fn from(value: String) -> Self { - Expr::String(value) + Expr::string(value) + } +} + +impl<'i> From<&str> for Expr<'i> { + fn from(value: &str) -> Self { + Expr::string(value) + } +} + +impl<'i> From> for Expr<'i> { + fn from(values: Vec) -> Self { + let exprs = values.into_iter().map(Expr::int).collect(); + Expr::new(ExprKind::Array(exprs), Span::default()) } } -impl From> for Expr { - fn from(value: Vec) -> Self { - Expr::Array(value.into_iter().map(Expr::Int).collect()) +impl<'i> From>> for Expr<'i> { + fn from(values: Vec>) -> Self { + Expr::new(ExprKind::Array(values), Span::default()) } } -impl From>> for Expr { - fn from(value: Vec>) -> Self { - Expr::Array(value.into_iter().map(|bytes| Expr::Array(bytes.into_iter().map(Expr::Byte).collect())).collect()) +impl<'i> From>> for Expr<'i> { + fn from(values: Vec>) -> Self { + let exprs = values.into_iter().map(Expr::bytes).collect(); + Expr::new(ExprKind::Array(exprs), Span::default()) } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", content = "data", rename_all = "snake_case")] +pub enum ExprKind<'i> { + Int(i64), + Bool(bool), + Byte(u8), + String(String), + DateLiteral(i64), + Identifier(String), + Array(Vec>), + Call { + name: String, + args: Vec>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + New { + name: String, + args: Vec>, + #[serde(skip_deserializing)] + name_span: Span<'i>, + }, + Split { + source: Box>, + index: Box>, + part: SplitPart, + #[serde(skip_deserializing)] + span: Span<'i>, + }, + Slice { + source: Box>, + start: Box>, + end: Box>, + #[serde(skip_deserializing)] + span: Span<'i>, + }, + ArrayIndex { + source: Box>, + index: Box>, + }, + Unary { + op: UnaryOp, + expr: Box>, + }, + Binary { + op: BinaryOp, + left: Box>, + right: Box>, + }, + IfElse { + condition: Box>, + then_expr: Box>, + else_expr: Box>, + }, + Nullary(NullaryOp), + Introspection { + kind: IntrospectionKind, + index: Box>, + #[serde(skip_deserializing)] + field_span: Span<'i>, + }, + StateObject(Vec>), + NumberWithUnit { + value: i64, + unit: String, + }, + UnarySuffix { + source: Box>, + kind: UnarySuffixKind, + #[serde(skip_deserializing)] + span: Span<'i>, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StateFieldExpr<'i> { + pub name: String, + pub expr: Expr<'i>, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SplitPart { Left, Right, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum UnaryOp { Not, Neg, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BinaryOp { Or, @@ -262,7 +550,7 @@ pub enum BinaryOp { Mod, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum NullaryOp { ActiveInputIndex, @@ -275,25 +563,44 @@ pub enum NullaryOp { TxLockTime, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum IntrospectionKind { InputValue, InputScriptPubKey, InputSigScript, + /// TODO: not supported yet + InputOutpointTransactionHash, + /// TODO: not supported yet + InputOutpointIndex, + /// TODO: not supported yet + InputSequenceNumber, OutputValue, OutputScriptPubKey, } -fn validate_user_identifier(name: &str) -> Result<(), CompilerError> { - if name.starts_with("__") { - return Err(CompilerError::Unsupported("identifier cannot start with '__'".to_string())); - } - Ok(()) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConstantAst<'i> { + pub type_ref: TypeRef, + pub name: String, + pub expr: Expr<'i>, + #[serde(skip_deserializing)] + pub span: Span<'i>, + #[serde(skip_deserializing)] + pub type_span: Span<'i>, + #[serde(skip_deserializing)] + pub name_span: Span<'i>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UnarySuffixKind { + Reverse, + Length, } pub fn parse_type_ref(type_name: &str) -> Result { - let mut pairs = SilverScriptParser::parse(Rule::type_name, type_name)?; + let mut pairs = parse_type_name_rule(type_name)?; let pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("missing type name".to_string()))?; parse_type_name_pair(pair) } @@ -303,7 +610,7 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result return Err(CompilerError::Unsupported("expected type name".to_string())); } - let mut inner = pair.clone().into_inner(); + let mut inner = pair.into_inner(); let base = match inner.next().ok_or_else(|| CompilerError::Unsupported("missing base type".to_string()))?.as_str() { "int" => TypeBase::Int, "bool" => TypeBase::Bool, @@ -329,9 +636,7 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result if let Ok(size) = raw.parse::() { ArrayDim::Fixed(size) } else { ArrayDim::Constant(raw.to_string()) } } Rule::Identifier => ArrayDim::Constant(size_pair.as_str().to_string()), - _ => { - return Err(CompilerError::Unsupported("invalid array dimension".to_string())); - } + _ => return Err(CompilerError::Unsupported("invalid array dimension".to_string())), }, }; array_dims.push(dim); @@ -340,8 +645,8 @@ fn parse_type_name_pair(pair: Pair<'_, Rule>) -> Result Ok(TypeRef { base, array_dims }) } -pub fn parse_contract_ast(source: &str) -> Result { - let mut pairs = SilverScriptParser::parse(Rule::source_file, source)?; +pub fn parse_contract_ast<'i>(source: &'i str) -> Result, CompilerError> { + let mut pairs = parse_source_file(source)?; let source_pair = pairs.next().ok_or_else(|| CompilerError::Unsupported("empty source".to_string()))?; let mut contract = None; @@ -354,15 +659,19 @@ pub fn parse_contract_ast(source: &str) -> Result { contract.ok_or_else(|| CompilerError::Unsupported("no contract definition".to_string())) } -fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result { +fn parse_contract_definition<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing contract name".to_string()))?; let params_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing contract parameters".to_string()))?; + + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; let params = parse_typed_parameter_list(params_pair)?; let mut functions = Vec::new(); let mut fields = Vec::new(); - let mut constants: HashMap = HashMap::new(); + let mut constants = Vec::new(); for item_pair in inner { if item_pair.as_rule() != Rule::contract_item { @@ -371,289 +680,354 @@ fn parse_contract_definition(pair: Pair<'_, Rule>) -> Result { - functions.push(parse_function_definition(inner_item)?); - } - Rule::contract_field_definition => { - let mut field_inner = inner_item.into_inner(); - let type_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field type".to_string()))?; - let type_ref = parse_type_name_pair(type_pair)?; - let name_pair = field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field name".to_string()))?; - validate_user_identifier(name_pair.as_str())?; - let expr_pair = - field_inner.next().ok_or_else(|| CompilerError::Unsupported("missing field initializer".to_string()))?; - let expr = parse_expression(expr_pair)?; - fields.push(ContractFieldAst { type_ref, name: name_pair.as_str().to_string(), expr }); - } - Rule::constant_definition => { - let mut const_inner = inner_item.into_inner(); - let _type_name = - const_inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant type".to_string()))?; - let name_pair = - const_inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant name".to_string()))?; - validate_user_identifier(name_pair.as_str())?; - let expr_pair = - const_inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant initializer".to_string()))?; - let expr = parse_expression(expr_pair)?; - constants.insert(name_pair.as_str().to_string(), expr); - } + Rule::function_definition => functions.push(parse_function_definition(inner_item)?), + Rule::contract_field_definition => fields.push(parse_contract_field_definition(inner_item)?), + Rule::constant_definition => constants.push(parse_constant_definition(inner_item)?), _ => {} } } } - Ok(ContractAst { name: name_pair.as_str().to_string(), params, fields, constants, functions }) + Ok(ContractAst { name, params, fields, constants, functions, span, name_span }) } -fn parse_function_definition(pair: Pair<'_, Rule>) -> Result { +fn parse_function_definition<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); - let mut entrypoint = false; - let name_pair = match inner.next() { - Some(pair) if pair.as_rule() == Rule::entrypoint => { - entrypoint = true; - inner.next().ok_or_else(|| CompilerError::Unsupported("missing function name".to_string()))? - } - Some(pair) => pair, - None => return Err(CompilerError::Unsupported("missing function name".to_string())), + + let first = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function name".to_string()))?; + let (entrypoint, name_pair) = if first.as_rule() == Rule::entrypoint { + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function name".to_string()))?; + (true, name_pair) + } else { + (false, first) }; + let params_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function parameters".to_string()))?; let params = parse_typed_parameter_list(params_pair)?; + let mut return_types = Vec::new(); + let mut return_type_spans = Vec::new(); if let Some(next) = inner.peek() { if next.as_rule() == Rule::return_type_list { let return_pair = inner.next().expect("checked"); - return_types = parse_return_type_list(return_pair)?; + let (types, spans) = parse_return_type_list(return_pair)?; + return_types = types; + return_type_spans = spans; } } + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; + let mut body = Vec::new(); - for stmt in inner { - body.push(parse_statement(stmt)?); + let mut body_span: Option> = None; + for stmt_pair in inner { + let stmt = parse_statement(stmt_pair)?; + let stmt_span = stmt.span(); + body_span = Some(match body_span { + None => stmt_span, + Some(prev) => prev.join(&stmt_span), + }); + body.push(stmt); } + let body_span = body_span.unwrap_or(span); - Ok(FunctionAst { name: name_pair.as_str().to_string(), params, entrypoint, return_types, body }) + Ok(FunctionAst { name, entrypoint, params, return_types, return_type_spans, body, span, name_span, body_span }) } -fn parse_statement(pair: Pair<'_, Rule>) -> Result { - if pair.as_rule() == Rule::statement { - if let Some(inner) = pair.into_inner().next() { - return parse_statement(inner); - } - return Err(CompilerError::Unsupported("empty statement".to_string())); - } +fn parse_constant_definition<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); + + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant type".to_string()))?; + let type_span = Span::from(type_pair.as_span()); + let type_ref = parse_type_name_pair(type_pair)?; + + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant name".to_string()))?; + let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing constant initializer".to_string()))?; - let span = Some(SourceSpan::from(pair.as_span())); - let kind = match pair.as_rule() { + let expr = parse_expression(expr_pair)?; + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; + + Ok(ConstantAst { type_ref, name, expr, span, type_span, name_span }) +} + +fn parse_contract_field_definition<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); + + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing field type".to_string()))?; + let type_span = Span::from(type_pair.as_span()); + let type_ref = parse_type_name_pair(type_pair)?; + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing field name".to_string()))?; + let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing field initializer".to_string()))?; + + let expr = parse_expression(expr_pair)?; + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; + + Ok(ContractFieldAst { type_ref, name, expr, span, type_span, name_span }) +} + +fn parse_statement<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + match pair.as_rule() { + Rule::statement => { + if let Some(inner) = pair.into_inner().next() { + parse_statement(inner) + } else { + Err(CompilerError::Unsupported("empty statement".to_string()).with_span(&span)) + } + } Rule::variable_definition => { let mut inner = pair.into_inner(); - let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()))?; - let type_ref = parse_type_name_pair(type_pair)?; + let type_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable type".to_string()).with_span(&span))?; + let type_span = Span::from(type_pair.as_span()); + let type_ref = parse_type_name_pair(type_pair).map_err(|err| err.with_span(&span))?; let mut modifiers = Vec::new(); + let mut modifier_spans = Vec::new(); while let Some(p) = inner.peek() { if p.as_rule() != Rule::modifier { break; } - modifiers.push(inner.next().expect("checked").as_str().to_string()); + let modifier = inner.next().expect("checked"); + modifiers.push(modifier.as_str().to_string()); + modifier_spans.push(Span::from(modifier.as_span())); } - let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()))?; - validate_user_identifier(ident.as_str())?; - let expr = inner.next().map(parse_expression).transpose()?; - StatementKind::VariableDefinition { type_ref, modifiers, name: ident.as_str().to_string(), expr } + let ident = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing variable name".to_string()).with_span(&span))?; + let Identifier { name, span: name_span } = parse_identifier(ident).map_err(|err| err.with_span(&span))?; + let expr = match inner.next() { + Some(expr_pair) => Some(parse_expression(expr_pair).map_err(|err| err.with_span(&span))?), + None => None, + }; + Ok(Statement::VariableDefinition { type_ref, modifiers, name, expr, span, type_span, modifier_spans, name_span }) } Rule::tuple_assignment => { let mut inner = pair.into_inner(); - let left_type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()))?; - let left_type_ref = parse_type_name_pair(left_type_pair)?; - let left_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple name".to_string()))?; - let right_type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()))?; - let right_type_ref = parse_type_name_pair(right_type_pair)?; - let right_ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple name".to_string()))?; - validate_user_identifier(left_ident.as_str())?; - validate_user_identifier(right_ident.as_str())?; - let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()))?; - - let expr = parse_expression(expr_pair)?; - StatementKind::TupleAssignment { + let left_type_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple type".to_string()).with_span(&span))?; + let left_ident = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing left tuple name".to_string()).with_span(&span))?; + let right_type_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple type".to_string()).with_span(&span))?; + let right_ident = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing right tuple name".to_string()).with_span(&span))?; + let expr_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple expression".to_string()).with_span(&span))?; + + let Identifier { name: left_name, span: left_name_span } = + parse_identifier(left_ident).map_err(|err| err.with_span(&span))?; + let Identifier { name: right_name, span: right_name_span } = + parse_identifier(right_ident).map_err(|err| err.with_span(&span))?; + + let right_type_span = Span::from(right_type_pair.as_span()); + let right_type_ref = parse_type_name_pair(right_type_pair).map_err(|err| err.with_span(&span))?; + + let left_type_span = Span::from(left_type_pair.as_span()); + let left_type_ref = parse_type_name_pair(left_type_pair).map_err(|err| err.with_span(&span))?; + + let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::TupleAssignment { left_type_ref, - left_name: left_ident.as_str().to_string(), + left_name, right_type_ref, - right_name: right_ident.as_str().to_string(), + right_name, expr, - } + span, + left_type_span, + left_name_span, + right_type_span, + right_name_span, + }) } Rule::push_statement => { let mut inner = pair.into_inner(); - let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()))?; - let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()))?; - let expr = parse_expression(expr_pair)?; - StatementKind::ArrayPush { name: ident.as_str().to_string(), expr } + let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing push target".to_string()).with_span(&span))?; + let expr_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing push expression".to_string()).with_span(&span))?; + let Identifier { name, span: name_span } = parse_identifier(ident).map_err(|err| err.with_span(&span))?; + let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::ArrayPush { name, expr, span, name_span }) + } + Rule::function_call_assignment => { + let mut inner = pair.into_inner(); + let mut bindings = Vec::new(); + while let Some(p) = inner.peek() { + if p.as_rule() != Rule::typed_binding { + break; + } + let binding = inner.next().expect("checked"); + bindings.push(parse_typed_binding(binding).map_err(|err| err.with_span(&span))?); + } + let call_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()).with_span(&span))?; + let (Identifier { name, span: name_span }, args) = + parse_function_call_parts(call_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::FunctionCallAssign { bindings, name, args, span, name_span }) + } + Rule::state_function_call_assignment => { + let mut inner = pair.into_inner(); + let mut bindings = Vec::new(); + while let Some(p) = inner.peek() { + if p.as_rule() != Rule::state_typed_binding { + break; + } + let binding = inner.next().expect("checked"); + bindings.push(parse_state_typed_binding(binding).map_err(|err| err.with_span(&span))?); + } + let call_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()).with_span(&span))?; + let (Identifier { name, span: name_span }, args) = + parse_function_call_parts(call_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::StateFunctionCallAssign { bindings, name, args, span, name_span }) + } + Rule::call_statement => { + let mut inner = pair.into_inner(); + let call_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing call statement".to_string()).with_span(&span))?; + let (Identifier { name, span: name_span }, args) = + parse_function_call_parts(call_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::FunctionCall { name, args, span, name_span }) } Rule::assign_statement => { let mut inner = pair.into_inner(); - let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()))?; - let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()))?; - let expr = parse_expression(expr_pair)?; - StatementKind::Assign { name: ident.as_str().to_string(), expr } + let ident = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing assignment name".to_string()).with_span(&span))?; + let expr_pair = inner + .next() + .ok_or_else(|| CompilerError::Unsupported("missing assignment expression".to_string()).with_span(&span))?; + let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; + let Identifier { name, span: name_span } = parse_identifier(ident).map_err(|err| err.with_span(&span))?; + Ok(Statement::Assign { name, expr, span, name_span }) + } + Rule::return_statement => { + let mut inner = pair.into_inner(); + let list_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing return values".to_string()).with_span(&span))?; + let exprs = parse_expression_list(list_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::Return { exprs, span }) } Rule::time_op_statement => { let mut inner = pair.into_inner(); - let tx_var = inner.next().ok_or_else(|| CompilerError::Unsupported("missing time op variable".to_string()))?; - let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing time op expression".to_string()))?; - let message = inner.next().map(parse_require_message).transpose()?; - - let expr = parse_expression(expr_pair)?; - let tx_var = match tx_var.as_str() { + let tx_var = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing time op variable".to_string()).with_span(&span))?; + let expr_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing time op expression".to_string()).with_span(&span))?; + let message = inner.next().map(parse_require_message).transpose().map_err(|err| err.with_span(&span))?; + + let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; + let tx_var_span = Span::from(tx_var.as_span()); + let tx_var_value = match tx_var.as_str() { "this.age" => TimeVar::ThisAge, "tx.time" => TimeVar::TxTime, - other => return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}"))), + other => { + return Err(CompilerError::Unsupported(format!("unsupported time variable: {other}")).with_span(&tx_var_span)); + } }; - StatementKind::TimeOp { tx_var, expr, message } + let (message, message_span) = message.unzip(); + Ok(Statement::TimeOp { tx_var: tx_var_value, expr, message, span, tx_var_span, message_span }) } Rule::require_statement => { let mut inner = pair.into_inner(); - let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()))?; - let message = inner.next().map(parse_require_message).transpose()?; - let expr = parse_expression(expr_pair)?; - StatementKind::Require { expr, message } + let expr_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing require expression".to_string()).with_span(&span))?; + let message = inner.next().map(parse_require_message).transpose().map_err(|err| err.with_span(&span))?; + let expr = parse_expression(expr_pair).map_err(|err| err.with_span(&span))?; + let (message, message_span) = message.unzip(); + Ok(Statement::Require { expr, message, span, message_span }) } Rule::if_statement => { let mut inner = pair.into_inner(); - let cond_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if condition".to_string()))?; - let cond_expr = parse_expression(cond_pair)?; - let then_block = inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()))?; - let then_branch = parse_block(then_block)?; - let else_branch = inner.next().map(parse_block).transpose()?; - StatementKind::If { condition: cond_expr, then_branch, else_branch } - } - Rule::call_statement => { - let mut inner = pair.into_inner(); - let call_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; - match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCall { name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), - } - } - Rule::function_call_assignment => { - let mut bindings = Vec::new(); - let mut call_pair = None; - for item in pair.into_inner() { - if item.as_rule() == Rule::typed_binding { - let mut inner = item.into_inner(); - let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; - let type_ref = parse_type_name_pair(type_pair)?; - let name = inner - .next() - .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? - .as_str() - .to_string(); - validate_user_identifier(&name)?; - bindings.push(ParamAst { type_ref, name }); - } else if item.as_rule() == Rule::function_call { - call_pair = Some(item); - } - } - let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; - match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::FunctionCallAssign { bindings, name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), - } - } - Rule::state_function_call_assignment => { - let mut bindings = Vec::new(); - let mut call_pair = None; - for item in pair.into_inner() { - if item.as_rule() == Rule::state_typed_binding { - let mut inner = item.into_inner(); - let field_name = inner - .next() - .ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))? - .as_str() - .to_string(); - let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; - let type_ref = parse_type_name_pair(type_pair)?; - let name = inner - .next() - .ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))? - .as_str() - .to_string(); - validate_user_identifier(&field_name)?; - validate_user_identifier(&name)?; - bindings.push(StateBindingAst { field_name, type_ref, name }); - } else if item.as_rule() == Rule::function_call { - call_pair = Some(item); - } - } - let call_pair = call_pair.ok_or_else(|| CompilerError::Unsupported("missing function call".to_string()))?; - match parse_function_call(call_pair)? { - Expr::Call { name, args } => StatementKind::StateFunctionCallAssign { bindings, name, args }, - _ => return Err(CompilerError::Unsupported("function call expected".to_string())), - } + let cond_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing if condition".to_string()).with_span(&span))?; + let cond_expr = parse_expression(cond_pair).map_err(|err| err.with_span(&span))?; + let then_block = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing if block".to_string()).with_span(&span))?; + let (then_branch, then_span) = parse_block(then_block).map_err(|err| err.with_span(&span))?; + let else_data = inner.next().map(parse_block).transpose().map_err(|err| err.with_span(&span))?; + let (else_branch, else_span) = match else_data { + Some((branch, span)) => (Some(branch), Some(span)), + None => (None, None), + }; + Ok(Statement::If { condition: cond_expr, then_branch, else_branch, span, then_span, else_span }) } Rule::for_statement => { let mut inner = pair.into_inner(); - let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop identifier".to_string()))?; - validate_user_identifier(ident.as_str())?; - let start_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop start".to_string()))?; - let end_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop end".to_string()))?; - let block_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop body".to_string()))?; - - let start_expr = parse_expression(start_pair)?; - let end_expr = parse_expression(end_pair)?; - let body = parse_block(block_pair)?; - - StatementKind::For { ident: ident.as_str().to_string(), start: start_expr, end: end_expr, body } + let ident = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop identifier".to_string()).with_span(&span))?; + let start_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop start".to_string()).with_span(&span))?; + let end_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop end".to_string()).with_span(&span))?; + let block_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing for loop body".to_string()).with_span(&span))?; + + let start_expr = parse_expression(start_pair).map_err(|err| err.with_span(&span))?; + let end_expr = parse_expression(end_pair).map_err(|err| err.with_span(&span))?; + let (body, body_span) = parse_block(block_pair).map_err(|err| err.with_span(&span))?; + let Identifier { name: ident, span: ident_span } = parse_identifier(ident).map_err(|err| err.with_span(&span))?; + + Ok(Statement::For { ident, start: start_expr, end: end_expr, body, span, ident_span, body_span }) } Rule::yield_statement => { let mut inner = pair.into_inner(); - let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing yield arguments".to_string()))?; - let args = parse_expression_list(list_pair)?; + let list_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing yield arguments".to_string()).with_span(&span))?; + let args = parse_expression_list(list_pair).map_err(|err| err.with_span(&span))?; if args.len() != 1 { - return Err(CompilerError::Unsupported("yield() expects a single argument".to_string())); + return Err(CompilerError::Unsupported("yield() expects a single argument".to_string()).with_span(&span)); } - StatementKind::Yield { expr: args[0].clone() } - } - Rule::return_statement => { - let mut inner = pair.into_inner(); - let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing return arguments".to_string()))?; - let args = parse_expression_list(list_pair)?; - if args.is_empty() { - return Err(CompilerError::Unsupported("return() expects at least one argument".to_string())); - } - StatementKind::Return { exprs: args } + Ok(Statement::Yield { expr: args[0].clone(), span }) } Rule::console_statement => { let mut inner = pair.into_inner(); - let list_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()))?; - let args = parse_console_parameter_list(list_pair)?; - StatementKind::Console { args } + let list_pair = + inner.next().ok_or_else(|| CompilerError::Unsupported("missing console arguments".to_string()).with_span(&span))?; + let args = parse_console_parameter_list(list_pair).map_err(|err| err.with_span(&span))?; + Ok(Statement::Console { args, span }) } - _ => return Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule()))), - }; - - Ok(Statement { span, kind }) + _ => Err(CompilerError::Unsupported(format!("unexpected statement: {:?}", pair.as_rule())).with_span(&span)), + } } -fn parse_block(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_block<'i>(pair: Pair<'i, Rule>) -> Result<(Vec>, Span<'i>), CompilerError> { + let span = Span::from(pair.as_span()); match pair.as_rule() { Rule::block => { let mut statements = Vec::new(); - for stmt in pair.into_inner() { - statements.push(parse_statement(stmt)?); + let mut block_span: Option> = None; + for stmt_pair in pair.into_inner() { + let stmt = parse_statement(stmt_pair)?; + let stmt_span = stmt.span(); + block_span = Some(match block_span { + None => stmt_span, + Some(prev) => prev.join(&stmt_span), + }); + statements.push(stmt); } - Ok(statements) + Ok((statements, block_span.unwrap_or(span))) + } + _ => { + let stmt = parse_statement(pair)?; + let stmt_span = stmt.span(); + Ok((vec![stmt], stmt_span)) } - _ => Ok(vec![parse_statement(pair)?]), } } -fn parse_console_parameter_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_console_parameter_list<'i>(pair: Pair<'i, Rule>) -> Result>, CompilerError> { let mut args = Vec::new(); + for param in pair.into_inner() { let value = if param.as_rule() == Rule::console_parameter { single_inner(param)? } else { param }; match value.as_rule() { - Rule::Identifier => args.push(ConsoleArg::Identifier(value.as_str().to_string())), + Rule::Identifier => { + let Identifier { name, span } = parse_identifier(value)?; + args.push(ConsoleArg::Identifier(name, span)); + } Rule::literal => args.push(ConsoleArg::Literal(parse_literal(single_inner(value)?)?)), _ => return Err(CompilerError::Unsupported("console.log arguments not supported".to_string())), } @@ -661,15 +1035,45 @@ fn parse_console_parameter_list(pair: Pair<'_, Rule>) -> Result, Ok(args) } -fn parse_require_message(pair: Pair<'_, Rule>) -> Result { +fn parse_typed_binding<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); + + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; + let ident_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))?; + + let type_span = Span::from(type_pair.as_span()); + let type_ref = parse_type_name_pair(type_pair)?; + let Identifier { name, span: name_span } = parse_identifier(ident_pair)?; + + Ok(ParamAst { type_ref, name, span, type_span, name_span }) +} + +fn parse_require_message<'i>(pair: Pair<'i, Rule>) -> Result<(String, Span<'i>), CompilerError> { let inner = single_inner(pair)?; match parse_string_literal(inner)? { - Expr::String(value) => Ok(value), + Expr { kind: ExprKind::String(value), span } => Ok((value, span)), _ => Err(CompilerError::Unsupported("require message must be a string literal".to_string())), } } -fn parse_expression(pair: Pair<'_, Rule>) -> Result { +fn parse_state_typed_binding<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); + + let field_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))?; + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding type".to_string()))?; + let ident_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing binding name".to_string()))?; + + let Identifier { name: field_name, span: field_span } = parse_identifier(field_pair)?; + let type_span = Span::from(type_pair.as_span()); + let type_ref = parse_type_name_pair(type_pair)?; + let Identifier { name, span: name_span } = parse_identifier(ident_pair)?; + + Ok(StateBindingAst { field_name, type_ref, name, span, field_span, type_span, name_span }) +} + +fn parse_expression<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { match pair.as_rule() { Rule::expression => parse_expression(single_inner(pair)?), Rule::logical_or => parse_infix(pair, parse_expression, map_logical_or), @@ -687,11 +1091,14 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result { Rule::parenthesized => parse_expression(single_inner(pair)?), Rule::literal => parse_literal(single_inner(pair)?), Rule::number_literal => parse_number_literal(pair), - Rule::NumberLiteral => parse_number(pair.as_str()), - Rule::BooleanLiteral => Ok(Expr::Bool(pair.as_str() == "true")), - Rule::HexLiteral => parse_hex_literal(pair.as_str()), - Rule::Identifier => Ok(Expr::Identifier(pair.as_str().to_string())), - Rule::NullaryOp => parse_nullary(pair.as_str()), + Rule::NumberLiteral => parse_number_expr(pair), + Rule::BooleanLiteral => Ok(Expr::new(ExprKind::Bool(pair.as_str() == "true"), Span::from(pair.as_span()))), + Rule::HexLiteral => parse_hex_literal(pair), + Rule::Identifier => { + let Identifier { name, span } = parse_identifier(pair)?; + Ok(Expr::new(ExprKind::Identifier(name), span)) + } + Rule::NullaryOp => parse_nullary(pair.as_str(), Span::from(pair.as_span())), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), Rule::function_call => parse_function_call(pair), @@ -710,7 +1117,8 @@ fn parse_expression(pair: Pair<'_, Rule>) -> Result { } } -fn parse_unary(pair: Pair<'_, Rule>) -> Result { +fn parse_unary<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); let mut ops = Vec::new(); while let Some(op) = inner.peek() { @@ -728,22 +1136,24 @@ fn parse_unary(pair: Pair<'_, Rule>) -> Result { let mut expr = parse_expression(inner.next().ok_or_else(|| CompilerError::Unsupported("missing unary operand".to_string()))?)?; for op in ops.into_iter().rev() { - expr = Expr::Unary { op, expr: Box::new(expr) }; + expr = Expr::new(ExprKind::Unary { op, expr: Box::new(expr) }, span); } Ok(expr) } -fn parse_postfix(pair: Pair<'_, Rule>) -> Result { +fn parse_postfix<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { let mut inner = pair.into_inner(); let primary = inner.next().ok_or_else(|| CompilerError::Unsupported("missing primary in postfix".to_string()))?; let mut expr = parse_primary(primary)?; for postfix in inner { + let postfix_span = Span::from(postfix.as_span()); match postfix.as_rule() { Rule::split_call => { let mut split_inner = postfix.into_inner(); let index_expr = split_inner.next().ok_or_else(|| CompilerError::Unsupported("missing split index".to_string()))?; let index = Box::new(parse_expression(index_expr)?); - expr = Expr::Split { source: Box::new(expr), index, part: SplitPart::Left }; + let span = expr.span.join(&postfix_span); + expr = Expr::new(ExprKind::Split { source: Box::new(expr), index, part: SplitPart::Left, span: postfix_span }, span); } Rule::slice_call => { let mut slice_inner = postfix.into_inner(); @@ -751,34 +1161,39 @@ fn parse_postfix(pair: Pair<'_, Rule>) -> Result { let end_expr = slice_inner.next().ok_or_else(|| CompilerError::Unsupported("missing slice end".to_string()))?; let start = Box::new(parse_expression(start_expr)?); let end = Box::new(parse_expression(end_expr)?); - expr = Expr::Slice { source: Box::new(expr), start, end }; + let span = expr.span.join(&postfix_span); + expr = Expr::new(ExprKind::Slice { source: Box::new(expr), start, end, span: postfix_span }, span); } Rule::tuple_index => { let mut index_inner = postfix.into_inner(); - let index_expr = index_inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple index".to_string()))?; - let index = parse_expression(index_expr)?; - match (&expr, &index) { - (Expr::Split { source, index: split_index, .. }, Expr::Int(0)) => { - expr = Expr::Split { source: source.clone(), index: split_index.clone(), part: SplitPart::Left }; - } - (Expr::Split { source, index: split_index, .. }, Expr::Int(1)) => { - expr = Expr::Split { source: source.clone(), index: split_index.clone(), part: SplitPart::Right }; - } - (Expr::Split { .. }, _) => { - return Err(CompilerError::Unsupported("tuple index must be 0 or 1".to_string())); - } - _ => { - expr = Expr::ArrayIndex { source: Box::new(expr), index: Box::new(index) }; - } + let index_pair = index_inner.next().ok_or_else(|| CompilerError::Unsupported("missing tuple index".to_string()))?; + let index_expr = parse_expression(index_pair)?; + let index_span = index_expr.span; + let span = expr.span.join(&postfix_span); + if let ExprKind::Split { source, index: split_index, span: split_span, .. } = &expr.kind { + let part = match index_expr.kind { + ExprKind::Int(0) => SplitPart::Left, + ExprKind::Int(1) => SplitPart::Right, + _ => { + return Err(CompilerError::Unsupported("split() index must be 0 or 1".to_string()).with_span(&index_span)); + } + }; + expr = Expr::new( + ExprKind::Split { source: source.clone(), index: split_index.clone(), part, span: *split_span }, + span, + ); + } else { + expr = Expr::new(ExprKind::ArrayIndex { source: Box::new(expr), index: Box::new(index_expr) }, span); } } Rule::unary_suffix => { - let text = postfix.as_str(); - if text.ends_with("length") { - expr = Expr::Call { name: "length".to_string(), args: vec![expr] }; - } else { - return Err(CompilerError::Unsupported("postfix operators are not supported".to_string())); - } + let kind = match postfix.as_str() { + ".reverse()" => UnarySuffixKind::Reverse, + ".length" => UnarySuffixKind::Length, + other => return Err(CompilerError::Unsupported(format!("unknown unary suffix '{other}'"))), + }; + let span = expr.span.join(&postfix_span); + expr = Expr::new(ExprKind::UnarySuffix { source: Box::new(expr), kind, span: postfix_span }, span); } _ => { return Err(CompilerError::Unsupported("postfix operators are not supported".to_string())); @@ -788,39 +1203,51 @@ fn parse_postfix(pair: Pair<'_, Rule>) -> Result { Ok(expr) } -fn parse_typed_parameter_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_typed_parameter_list<'i>(pair: Pair<'i, Rule>) -> Result>, CompilerError> { let mut params = Vec::new(); for param in pair.into_inner() { if param.as_rule() != Rule::parameter { continue; } + + let param_span = Span::from(param.as_span()); let mut inner = param.into_inner(); + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter type".to_string()))?; + let ident_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter name".to_string()))?; + + let Identifier { name, span: name_span } = parse_identifier(ident_pair)?; + let type_span = Span::from(type_pair.as_span()); let type_ref = parse_type_name_pair(type_pair)?; - let ident = inner.next().ok_or_else(|| CompilerError::Unsupported("missing parameter name".to_string()))?.as_str().to_string(); - validate_user_identifier(&ident)?; - params.push(ParamAst { type_ref, name: ident }); + + params.push(ParamAst { type_ref, name, span: param_span, type_span, name_span }); } Ok(params) } -fn parse_return_type_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { - let mut types = Vec::new(); - for item in pair.into_inner() { - if item.as_rule() == Rule::type_name { - let type_ref = parse_type_name_pair(item)?; - types.push(type_ref); +fn parse_return_type_list<'i>(pair: Pair<'i, Rule>) -> Result<(Vec, Vec>), CompilerError> { + let mut return_types = Vec::new(); + let mut return_spans = Vec::new(); + for user_type in pair.into_inner() { + if user_type.as_rule() != Rule::type_name { + continue; } + let type_span = Span::from(user_type.as_span()); + return_types.push(parse_type_name_pair(user_type)?); + return_spans.push(type_span); } - Ok(types) + Ok((return_types, return_spans)) } -fn parse_primary(pair: Pair<'_, Rule>) -> Result { +fn parse_primary<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { match pair.as_rule() { Rule::parenthesized => parse_expression(single_inner(pair)?), Rule::literal => parse_literal(single_inner(pair)?), - Rule::Identifier => Ok(Expr::Identifier(pair.as_str().to_string())), - Rule::NullaryOp => parse_nullary(pair.as_str()), + Rule::Identifier => { + let Identifier { name, span } = parse_identifier(pair)?; + Ok(Expr::new(ExprKind::Identifier(name), span)) + } + Rule::NullaryOp => parse_nullary(pair.as_str(), Span::from(pair.as_span())), Rule::introspection => parse_introspection(pair), Rule::array => parse_array(pair), Rule::function_call => parse_function_call(pair), @@ -832,87 +1259,115 @@ fn parse_primary(pair: Pair<'_, Rule>) -> Result { } } -fn parse_state_object(pair: Pair<'_, Rule>) -> Result { +fn parse_state_object<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut fields = Vec::new(); for field_pair in pair.into_inner() { if field_pair.as_rule() != Rule::state_entry { continue; } + let field_span = Span::from(field_pair.as_span()); let mut inner = field_pair.into_inner(); - let name = - inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))?.as_str().to_string(); - validate_user_identifier(&name)?; + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field name".to_string()))?; let expr_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing state field expression".to_string()))?; + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; let expr = parse_expression(expr_pair)?; - fields.push(StateFieldExpr { name, expr }); + fields.push(StateFieldExpr { name, expr, span: field_span, name_span }); } - Ok(Expr::StateObject(fields)) + Ok(Expr::new(ExprKind::StateObject(fields), span)) } -fn parse_literal(pair: Pair<'_, Rule>) -> Result { +fn parse_literal<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { match pair.as_rule() { - Rule::BooleanLiteral => Ok(Expr::Bool(pair.as_str() == "true")), + Rule::BooleanLiteral => Ok(Expr::new(ExprKind::Bool(pair.as_str() == "true"), Span::from(pair.as_span()))), Rule::number_literal => parse_number_literal(pair), - Rule::NumberLiteral => parse_number(pair.as_str()), - Rule::HexLiteral => parse_hex_literal(pair.as_str()), + Rule::NumberLiteral => parse_number_expr(pair), + Rule::HexLiteral => parse_hex_literal(pair), Rule::StringLiteral => parse_string_literal(pair), Rule::DateLiteral => parse_date_literal(pair), _ => Err(CompilerError::Unsupported(format!("literal not supported: {:?}", pair.as_rule()))), } } -fn parse_number(raw: &str) -> Result { - let cleaned = raw.replace('_', ""); - if let Some((base_str, exp_str)) = cleaned.split_once('e').or_else(|| cleaned.split_once('E')) { - if exp_str.is_empty() { - return Err(CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'"))); - } - let base: i64 = base_str.parse().map_err(|_| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; - let exp: i64 = exp_str.parse().map_err(|_| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; - if exp < 0 { - return Err(CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'"))); - } - let pow = 10i128.pow(exp as u32); - let value = - (base as i128).checked_mul(pow).ok_or_else(|| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; - if value > i64::MAX as i128 || value < i64::MIN as i128 { - return Err(CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'"))); - } - return Ok(Expr::Int(value as i64)); +fn parse_number_expr<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let value = parse_number(pair.as_str())?; + Ok(Expr::new(ExprKind::Int(value), span)) +} + +fn parse_number(raw: &str) -> Result { + let raw = raw.trim(); + let mut parts = raw.split(['e', 'E']); + let base_raw = parts.next().unwrap_or(""); + let exp_raw = parts.next(); + + // nothing allowed after having the exponent + if parts.next().is_some() { + return Err(CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'"))); } - let value: i64 = cleaned.parse().map_err(|_| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; - Ok(Expr::Int(value)) + + let base_clean = base_raw.replace('_', ""); + if base_clean.is_empty() || base_clean == "-" { + return Err(CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'"))); + } + let mut value = + base_clean.parse::().map_err(|_| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; + + if let Some(exp_raw) = exp_raw { + let exp_clean = exp_raw.replace('_', ""); + + // rejects negative exponent + let exp = exp_clean.parse::().map_err(|_| CompilerError::InvalidLiteral(format!("invalid number literal '{raw}'")))?; + let pow = 10i128.checked_pow(exp).ok_or_else(|| CompilerError::InvalidLiteral(format!("number literal overflow '{raw}'")))?; + value = value.checked_mul(pow).ok_or_else(|| CompilerError::InvalidLiteral(format!("number literal overflow '{raw}'")))?; + } + + if value < i64::MIN as i128 || value > i64::MAX as i128 { + return Err(CompilerError::InvalidLiteral(format!("number literal overflow '{raw}'"))); + } + + Ok(value as i64) } -fn parse_array(pair: Pair<'_, Rule>) -> Result { +fn parse_array<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut values = Vec::new(); for expr_pair in pair.into_inner() { values.push(parse_expression(expr_pair)?); } - Ok(Expr::Array(values)) + Ok(Expr::new(ExprKind::Array(values), span)) } -fn parse_function_call(pair: Pair<'_, Rule>) -> Result { +fn parse_function_call_parts<'i>(pair: Pair<'i, Rule>) -> Result<(Identifier<'i>, Vec>), CompilerError> { let mut inner = pair.into_inner(); - let name = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function name".to_string()))?.as_str().to_string(); + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing function name".to_string()))?; let args = match inner.next() { Some(list) => parse_expression_list(list)?, None => Vec::new(), }; - Ok(Expr::Call { name, args }) + let name = parse_identifier(name_pair)?; + Ok((name, args)) +} + +fn parse_function_call<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let (Identifier { name, span: name_span }, args) = parse_function_call_parts(pair)?; + Ok(Expr::new(ExprKind::Call { name, args, name_span }, span)) } -fn parse_instantiation(pair: Pair<'_, Rule>) -> Result { +fn parse_instantiation<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); - let name = inner.next().ok_or_else(|| CompilerError::Unsupported("missing constructor name".to_string()))?.as_str().to_string(); + let name_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing constructor name".to_string()))?; let args = match inner.next() { Some(list) => parse_expression_list(list)?, None => Vec::new(), }; - Ok(Expr::New { name, args }) + let Identifier { name, span: name_span } = parse_identifier(name_pair)?; + Ok(Expr::new(ExprKind::New { name, args, name_span }, span)) } -fn parse_expression_list(pair: Pair<'_, Rule>) -> Result, CompilerError> { +fn parse_expression_list<'i>(pair: Pair<'i, Rule>) -> Result>, CompilerError> { let mut args = Vec::new(); for expr_pair in pair.into_inner() { args.push(parse_expression(expr_pair)?); @@ -920,56 +1375,64 @@ fn parse_expression_list(pair: Pair<'_, Rule>) -> Result, CompilerErro Ok(args) } -fn parse_cast(pair: Pair<'_, Rule>) -> Result { +fn parse_cast<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); - let type_name = inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?.as_str().to_string(); - let args = match inner.next() { - Some(list) => parse_expression_list(list)?, - None => Vec::new(), - }; + + let type_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing cast type".to_string()))?; + let type_name = type_pair.as_str().trim().to_string(); + let type_span = Span::from(type_pair.as_span()); + + let mut args = Vec::new(); + for part in inner { + args.push(parse_expression(part)?); + } + if type_name == "bytes" { - return Ok(Expr::Call { name: "bytes".to_string(), args }); + return Ok(Expr::new(ExprKind::Call { name: "bytes".to_string(), args, name_span: type_span }, span)); } + if type_name == "byte" { - return Ok(Expr::Call { name: "byte[1]".to_string(), args }); + return Ok(Expr::new(ExprKind::Call { name: "byte[1]".to_string(), args, name_span: type_span }, span)); } + if type_name == "int" { - return Ok(Expr::Call { name: "int".to_string(), args }); + return Ok(Expr::new(ExprKind::Call { name: "int".to_string(), args, name_span: type_span }, span)); } + if matches!(type_name.as_str(), "sig" | "pubkey" | "datasig") { - return Ok(Expr::Call { name: type_name, args }); + return Ok(Expr::new(ExprKind::Call { name: type_name, args, name_span: type_span }, span)); } + // Handle single byte cast (duplicate check removed above) // Support type[N] syntax if let Some(bracket_pos) = type_name.find('[') { if type_name.ends_with(']') { - let _base_type = &type_name[..bracket_pos]; let size_str = &type_name[bracket_pos + 1..type_name.len() - 1]; - // Support both type[N] and type[] (dynamic array) - if size_str.is_empty() { - // Dynamic array cast like byte[] - return Ok(Expr::Call { name: type_name.to_string(), args }); - } else if let Ok(_size) = size_str.parse::() { - // Fixed-size array cast like byte[32] - return Ok(Expr::Call { name: type_name.to_string(), args }); + if size_str.is_empty() || size_str.parse::().is_ok() { + return Ok(Expr::new(ExprKind::Call { name: type_name, args, name_span: type_span }, span)); } } } + Err(CompilerError::Unsupported(format!("cast type not supported: {type_name}"))) } -fn parse_number_literal(pair: Pair<'_, Rule>) -> Result { +fn parse_number_literal<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let mut inner = pair.into_inner(); let number = inner.next().ok_or_else(|| CompilerError::InvalidLiteral("missing number literal".to_string()))?; let value = parse_number(number.as_str())?; - if let Some(unit_pair) = inner.next() { - let unit = unit_pair.as_str(); - return apply_number_unit(value, unit); + let expr = Expr::new(ExprKind::Int(value), span); + if let Some(unit) = inner.next() { + return apply_number_unit(expr, unit.as_str()); } - Ok(value) + Ok(expr) } -fn parse_hex_literal(raw: &str) -> Result { +fn parse_hex_literal<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let raw = pair.as_str(); let trimmed = raw.trim_start_matches("0x").trim_start_matches("0X"); let normalized = if trimmed.len() % 2 != 0 { format!("0{trimmed}") } else { trimmed.to_string() }; let bytes = (0..normalized.len()) @@ -977,13 +1440,13 @@ fn parse_hex_literal(raw: &str) -> Result { .map(|i| u8::from_str_radix(&normalized[i..i + 2], 16)) .collect::, _>>() .map_err(|_| CompilerError::InvalidLiteral(format!("invalid hex literal '{raw}'")))?; - // Convert Vec to Expr::Array of Expr::Byte - Ok(Expr::Array(bytes.into_iter().map(Expr::Byte).collect())) + Ok(Expr::new(ExprKind::Array(bytes.into_iter().map(|byte| Expr::new(ExprKind::Byte(byte), span)).collect()), span)) } -fn apply_number_unit(expr: Expr, unit: &str) -> Result { - let value = match expr { - Expr::Int(value) => value, +fn apply_number_unit<'i>(expr: Expr<'i>, unit: &str) -> Result, CompilerError> { + let span = expr.span; + let value = match expr.kind { + ExprKind::Int(value) => value, _ => return Err(CompilerError::InvalidLiteral("number literal is not an int".to_string())), }; let multiplier = match unit { @@ -997,41 +1460,37 @@ fn apply_number_unit(expr: Expr, unit: &str) -> Result { "kas" => 100_000_000, _ => return Err(CompilerError::Unsupported(format!("number unit '{unit}' not supported"))), }; - Ok(Expr::Int(value.saturating_mul(multiplier))) + Ok(Expr::new(ExprKind::Int(value.saturating_mul(multiplier)), span)) } -fn parse_date_literal(pair: Pair<'_, Rule>) -> Result { - let raw = pair.as_str(); - let start = raw - .find('"') - .or_else(|| raw.find('\'')) - .ok_or_else(|| CompilerError::InvalidLiteral("date literal missing quotes".to_string()))?; - let quote = raw.as_bytes()[start] as char; - let end = raw[start + 1..] - .find(quote) - .map(|idx| idx + start + 1) - .ok_or_else(|| CompilerError::InvalidLiteral("date literal missing closing quote".to_string()))?; - let value = &raw[start + 1..end]; - - let timestamp = NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S") - .map_err(|_| CompilerError::InvalidLiteral("invalid date literal".to_string()))? +fn parse_date_literal<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let mut inner = pair.into_inner(); + let string_pair = inner.next().ok_or_else(|| CompilerError::InvalidLiteral("missing date literal".to_string()))?; + let value = match parse_string_literal(string_pair)? { + Expr { kind: ExprKind::String(value), .. } => value, + _ => return Err(CompilerError::InvalidLiteral("invalid date literal".to_string())), + }; + let timestamp = NaiveDateTime::parse_from_str(&value, "%Y-%m-%dT%H:%M:%S") + .map_err(|_| CompilerError::InvalidLiteral(format!("invalid date literal '{value}'")))? .and_utc() .timestamp(); - Ok(Expr::Int(timestamp)) + Ok(Expr::new(ExprKind::DateLiteral(timestamp), span)) } -fn parse_string_literal(pair: Pair<'_, Rule>) -> Result { +fn parse_string_literal<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let raw = pair.as_str(); - let unquoted = if raw.starts_with('"') && raw.ends_with('"') || raw.starts_with('\'') && raw.ends_with('\'') { + let unquoted = if (raw.starts_with('"') && raw.ends_with('"')) || (raw.starts_with('\'') && raw.ends_with('\'')) { &raw[1..raw.len() - 1] } else { raw }; let unescaped = unquoted.replace("\\\"", "\"").replace("\\'", "'"); - Ok(Expr::String(unescaped)) + Ok(Expr::new(ExprKind::String(unescaped), span)) } -fn parse_nullary(raw: &str) -> Result { +fn parse_nullary<'i>(raw: &str, span: Span<'i>) -> Result, CompilerError> { let op = match raw { "this.activeInputIndex" => NullaryOp::ActiveInputIndex, "this.activeScriptPubKey" => NullaryOp::ActiveScriptPubKey, @@ -1043,45 +1502,49 @@ fn parse_nullary(raw: &str) -> Result { "tx.locktime" => NullaryOp::TxLockTime, _ => return Err(CompilerError::Unsupported(format!("unknown nullary op: {raw}"))), }; - Ok(Expr::Nullary(op)) + Ok(Expr::new(ExprKind::Nullary(op), span)) } -fn parse_introspection(pair: Pair<'_, Rule>) -> Result { +fn parse_introspection<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); let text = pair.as_str(); let mut inner = pair.into_inner(); let index_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing introspection index".to_string()))?; let field_pair = inner.next().ok_or_else(|| CompilerError::Unsupported("missing introspection field".to_string()))?; let index = Box::new(parse_expression(index_pair)?); - let field = field_pair.as_str(); - + let field_raw = field_pair.as_str(); let kind = if text.starts_with("tx.inputs") { - match field { + match field_raw { ".value" => IntrospectionKind::InputValue, ".scriptPubKey" => IntrospectionKind::InputScriptPubKey, ".sigScript" => IntrospectionKind::InputSigScript, - _ => return Err(CompilerError::Unsupported(format!("input field '{field}' not supported"))), + // TODO: support this + ".outpointTransactionHash" => IntrospectionKind::InputOutpointTransactionHash, + ".outpointIndex" => IntrospectionKind::InputOutpointIndex, + ".sequenceNumber" => IntrospectionKind::InputSequenceNumber, + _ => return Err(CompilerError::Unsupported(format!("input field '{field_raw}' not supported"))), } } else if text.starts_with("tx.outputs") { - match field { + match field_raw { ".value" => IntrospectionKind::OutputValue, ".scriptPubKey" => IntrospectionKind::OutputScriptPubKey, - _ => return Err(CompilerError::Unsupported(format!("output field '{field}' not supported"))), + _ => return Err(CompilerError::Unsupported(format!("output field '{field_raw}' not supported"))), } } else { return Err(CompilerError::Unsupported("unknown introspection root".to_string())); }; - Ok(Expr::Introspection { kind, index }) + Ok(Expr::new(ExprKind::Introspection { kind, index, field_span: Span::from(field_pair.as_span()) }, span)) } fn single_inner(pair: Pair<'_, Rule>) -> Result, CompilerError> { pair.into_inner().next().ok_or_else(|| CompilerError::Unsupported("expected inner pair".to_string())) } -fn parse_infix(pair: Pair<'_, Rule>, mut parse_operand: F, mut map_op: G) -> Result +fn parse_infix<'i, F, G>(pair: Pair<'i, Rule>, mut parse_operand: F, mut map_op: G) -> Result, CompilerError> where - F: FnMut(Pair<'_, Rule>) -> Result, + F: FnMut(Pair<'i, Rule>) -> Result, CompilerError>, G: FnMut(Pair<'_, Rule>) -> Result, { let mut inner = pair.into_inner(); @@ -1092,7 +1555,8 @@ where let rhs = inner.next().ok_or_else(|| CompilerError::Unsupported("missing infix rhs".to_string()))?; let op = map_op(op_pair)?; let rhs_expr = parse_operand(rhs)?; - expr = Expr::Binary { op, left: Box::new(expr), right: Box::new(rhs_expr) }; + let span = expr.span.join(&rhs_expr.span); + expr = Expr::new(ExprKind::Binary { op, left: Box::new(expr), right: Box::new(rhs_expr) }, span); } Ok(expr) @@ -1179,3 +1643,15 @@ fn map_factor(pair: Pair<'_, Rule>) -> Result { _ => Err(CompilerError::Unsupported("unexpected factor operator".to_string())), } } + +// validate user input +fn parse_identifier<'i>(pair: Pair<'i, Rule>) -> Result, CompilerError> { + let span = Span::from(pair.as_span()); + let value = pair.as_str().to_string(); + + if value.starts_with("__") { + return Err(CompilerError::Unsupported("identifiers starting with '__' are reserved".to_string())); + } + + Ok(Identifier { name: value, span }) +} diff --git a/silverscript-lang/src/bin/sil-debug.rs b/silverscript-lang/src/bin/sil-debug.rs index b13531cb..8fcd602f 100644 --- a/silverscript-lang/src/bin/sil-debug.rs +++ b/silverscript-lang/src/bin/sil-debug.rs @@ -6,9 +6,10 @@ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_txscript::caches::Cache; use kaspa_txscript::{EngineCtx, EngineFlags}; -use silverscript_lang::ast::{Expr, parse_contract_ast}; +use silverscript_lang::ast::{Expr, ExprKind, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; use silverscript_lang::debug::session::{DebugEngine, DebugSession}; +use silverscript_lang::span; const PROMPT: &str = "(sdb) "; @@ -47,11 +48,11 @@ fn parse_hex_bytes(raw: &str) -> Result, Box> { Ok(out) } -fn bytes_expr(bytes: Vec) -> Expr { - Expr::Array(bytes.into_iter().map(Expr::Byte).collect()) +fn bytes_expr(bytes: Vec) -> Expr<'static> { + Expr::new(ExprKind::Array(bytes.into_iter().map(Expr::byte).collect()), span::Span::default()) } -fn parse_typed_arg(type_name: &str, raw: &str) -> Result> { +fn parse_typed_arg(type_name: &str, raw: &str) -> Result, Box> { if let Some(element_type) = type_name.strip_suffix("[]") { let trimmed = raw.trim(); if trimmed.starts_with('[') { @@ -59,14 +60,14 @@ fn parse_typed_arg(type_name: &str, raw: &str) -> Result Expr::Int(n.as_i64().ok_or("invalid int in array")?), - serde_json::Value::Bool(b) => Expr::Bool(b), + serde_json::Value::Number(n) => Expr::int(n.as_i64().ok_or("invalid int in array")?), + serde_json::Value::Bool(b) => Expr::bool(b), serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, _ => return Err("unsupported array element (expected number/bool/string)".into()), }; out.push(expr); } - return Ok(Expr::Array(out)); + return Ok(Expr::new(ExprKind::Array(out), span::Span::default())); } if element_type == "byte" { return Ok(bytes_expr(parse_hex_bytes(trimmed)?)); @@ -75,16 +76,16 @@ fn parse_typed_arg(type_name: &str, raw: &str) -> Result Ok(Expr::Int(parse_int_arg(raw)?)), + "int" => Ok(Expr::int(parse_int_arg(raw)?)), "bool" => match raw { - "true" => Ok(Expr::Bool(true)), - "false" => Ok(Expr::Bool(false)), + "true" => Ok(Expr::bool(true)), + "false" => Ok(Expr::bool(false)), _ => Err(format!("invalid bool '{raw}' (expected true/false)").into()), }, - "string" => Ok(Expr::String(raw.to_string())), + "string" => Ok(Expr::string(raw.to_string())), "byte" => { let bytes = parse_hex_bytes(raw)?; - if bytes.len() == 1 { Ok(Expr::Byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } + if bytes.len() == 1 { Ok(Expr::byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } } "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), "pubkey" => { @@ -126,7 +127,7 @@ fn parse_typed_arg(type_name: &str, raw: &str) -> Result) { +fn show_stack(session: &DebugSession<'_, '_>) { println!("Stack:"); let stack = session.stack(); for (i, item) in stack.iter().enumerate().rev() { @@ -134,7 +135,7 @@ fn show_stack(session: &DebugSession<'_>) { } } -fn show_source_context(session: &DebugSession<'_>) { +fn show_source_context(session: &DebugSession<'_, '_>) { let Some(context) = session.source_context() else { println!("No source context available."); return; @@ -146,7 +147,7 @@ fn show_source_context(session: &DebugSession<'_>) { } } -fn show_vars(session: &DebugSession<'_>) { +fn show_vars(session: &DebugSession<'_, '_>) { match session.list_variables() { Ok(variables) => { if variables.is_empty() { @@ -168,12 +169,12 @@ fn show_vars(session: &DebugSession<'_>) { } } -fn show_step_view(session: &DebugSession<'_>) { +fn show_step_view(session: &DebugSession<'_, '_>) { show_source_context(session); show_vars(session); } -fn run_repl(session: &mut DebugSession<'_>) -> Result<(), kaspa_txscript_errors::TxScriptError> { +fn run_repl(session: &mut DebugSession<'_, '_>) -> Result<(), kaspa_txscript_errors::TxScriptError> { let stdin = io::stdin(); loop { print!("{PROMPT}"); diff --git a/silverscript-lang/src/bin/silverc.rs b/silverscript-lang/src/bin/silverc.rs index fbd89538..557988c0 100644 --- a/silverscript-lang/src/bin/silverc.rs +++ b/silverscript-lang/src/bin/silverc.rs @@ -1,10 +1,44 @@ -use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use silverscript_lang::ast::Expr; +use clap::Parser; +use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; +#[derive(Debug, Parser)] +#[command( + name = "silverc", + about = "Compile SilverScript contracts into JSON artifacts", + long_about = "Compile a SilverScript source file into a compiled JSON artifact, or parse only and emit AST JSON.\n\ +\n\ +Destination precedence:\n\ +1) -c, --stdout -> write JSON to stdout\n\ +2) -o -> write JSON to the specified file\n\ +3) compile default -> .json", + after_help = "Examples:\n\ + silverc contract.sil\n\ + silverc contract.sil -o artifact.json\n\ + silverc contract.sil -c", + next_line_help = true +)] +struct Cli { + /// Source SilverScript file (e.g. contract.sil) + #[arg(value_name = "SOURCE.sil")] + src: PathBuf, + /// Path to JSON constructor arguments + #[arg(visible_alias = "ctor", long = "constructor-args", value_name = "CTOR.json")] + constructor_args: Option, + /// Output file path for JSON output + #[arg(short = 'o', long = "output", value_name = "FILE.json")] + out: Option, + /// Write JSON output to stdout + #[arg(short = 'c', long = "stdout")] + stdout: bool, + /// Parse source and emit AST JSON without compiling + #[arg(long = "ast-only")] + ast_only: bool, +} + fn main() { if let Err(err) = run() { eprintln!("{err}"); @@ -13,47 +47,29 @@ fn main() { } fn run() -> Result<(), String> { - let args = env::args().skip(1).collect::>(); - if args.is_empty() { - return Err("usage: silverc [--constructor-args ctor.json] [-o dst.json]".to_string()); - } + let cli = match Cli::try_parse() { + Ok(cli) => cli, + Err(err) if err.use_stderr() => return Err(err.to_string()), + Err(err) => { + err.print().map_err(|print_err| format!("failed to print clap output: {print_err}"))?; + return Ok(()); + } + }; - let mut src: Option = None; - let mut ctor_args_path: Option = None; - let mut out_path: Option = None; + let source = fs::read_to_string(&cli.src).map_err(|err| format!("failed to read {}: {err}", cli.src.display()))?; - let mut i = 0; - while i < args.len() { - match args[i].as_str() { - "--constructor-args" => { - let value = args.get(i + 1).ok_or_else(|| "--constructor-args requires a path".to_string())?; - ctor_args_path = Some(value.clone()); - i += 2; - } - "-o" => { - let value = args.get(i + 1).ok_or_else(|| "-o requires a path".to_string())?; - out_path = Some(value.clone()); - i += 2; - } - value if value.starts_with('-') => { - return Err(format!("unknown option: {value}")); - } - value => { - if src.is_some() { - return Err("only one source file is supported".to_string()); - } - src = Some(value.to_string()); - i += 1; - } - } + if cli.ast_only { + let ast = parse_contract_ast(&source).map_err(|err| format!("parse error: {err}"))?; + let rendered = ast.to_string(); + let target = resolve_output_target(&cli, &cli.src, true); + emit_output(&rendered, target)?; + return Ok(()); } - let src = src.ok_or_else(|| "missing source file".to_string())?; - let source = fs::read_to_string(&src).map_err(|err| format!("failed to read {src}: {err}"))?; - - let constructor_args = if let Some(path) = ctor_args_path { - let json = fs::read_to_string(&path).map_err(|err| format!("failed to read {path}: {err}"))?; - serde_json::from_str::>(&json).map_err(|err| format!("failed to parse constructor args {path}: {err}"))? + let constructor_args = if let Some(path) = &cli.constructor_args { + let json = fs::read_to_string(path).map_err(|err| format!("failed to read {}: {err}", path.display()))?; + serde_json::from_str::>(&json) + .map_err(|err| format!("failed to parse constructor args {}: {err}", path.display()))? } else { Vec::new() }; @@ -61,21 +77,43 @@ fn run() -> Result<(), String> { let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).map_err(|err| format!("compile error: {err}"))?; - let output_path = match out_path { - Some(path) => PathBuf::from(path), - None => default_output_path(&src), - }; - let json = serde_json::to_string_pretty(&compiled).map_err(|err| format!("failed to serialize output: {err}"))?; - fs::write(&output_path, json).map_err(|err| format!("failed to write {}: {err}", output_path.display()))?; + let target = resolve_output_target(&cli, &cli.src, false); + emit_output(&json, target)?; Ok(()) } -fn default_output_path(src: &str) -> PathBuf { - if let Some(stripped) = src.strip_suffix(".sil") { - PathBuf::from(format!("{stripped}.json")) - } else { - PathBuf::from(format!("{src}.json")) +enum OutputTarget { + Stdout, + File(PathBuf), +} + +fn resolve_output_target(cli: &Cli, src: &Path, ast_only: bool) -> OutputTarget { + if cli.stdout { + return OutputTarget::Stdout; + } + if let Some(path) = &cli.out { + return OutputTarget::File(path.clone()); + } + if ast_only { OutputTarget::Stdout } else { OutputTarget::File(default_output_path(src)) } +} + +fn emit_output(content: &str, target: OutputTarget) -> Result<(), String> { + match target { + OutputTarget::Stdout => { + println!("{content}"); + Ok(()) + } + OutputTarget::File(path) => { + fs::write(&path, content).map_err(|err| format!("failed to write {}: {err}", path.display()))?; + Ok(()) + } } } + +fn default_output_path(src: &Path) -> PathBuf { + let mut output_path = src.to_path_buf(); + output_path.set_extension("json"); + output_path +} diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 6f6c3a86..9b47b384 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1,39 +1,23 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::opcodes::codes::*; -use kaspa_txscript::script_builder::{ScriptBuilder, ScriptBuilderError}; +use kaspa_txscript::script_builder::ScriptBuilder; use serde::{Deserialize, Serialize}; -use thiserror::Error; use crate::ast::{ - ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, StateBindingAst, - Statement, StatementKind, TimeVar, TypeBase, TypeRef, UnaryOp, parse_contract_ast, parse_type_ref, + ArrayDim, BinaryOp, ContractAst, ContractFieldAst, Expr, ExprKind, FunctionAst, IntrospectionKind, NullaryOp, SplitPart, + StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, parse_contract_ast, + parse_type_ref, }; +pub use crate::errors::{CompilerError, ErrorSpan}; use crate::debug::labels::synthetic; use crate::debug::{DebugInfo, SourceSpan}; -use crate::parser::Rule; -use chrono::NaiveDateTime; +use crate::span; mod debug_recording; use debug_recording::{DebugSink, FunctionDebugRecorder, record_synthetic_range}; -#[derive(Debug, Error)] -pub enum CompilerError { - #[error("parse error: {0}")] - Parse(#[from] pest::error::Error), - #[error("unsupported feature: {0}")] - Unsupported(String), - #[error("invalid literal: {0}")] - InvalidLiteral(String), - #[error("undefined identifier: {0}")] - UndefinedIdentifier(String), - #[error("cyclic identifier reference: {0}")] - CyclicIdentifier(String), - #[error("script build error: {0}")] - ScriptBuild(#[from] ScriptBuilderError), -} - #[derive(Debug, Clone, Copy, Default)] pub struct CompileOptions { pub allow_yield: bool, @@ -53,42 +37,44 @@ pub struct FunctionAbiEntry { pub inputs: Vec, } -pub type FunctionAbi = Vec; - #[derive(Debug, Serialize, Deserialize)] -pub struct CompiledContract { +pub struct CompiledContract<'i> { pub contract_name: String, pub script: Vec, - pub ast: ContractAst, - pub abi: FunctionAbi, + pub ast: ContractAst<'i>, + pub abi: Vec, pub without_selector: bool, - pub debug_info: Option, + pub debug_info: Option>, } -pub fn compile_contract(source: &str, constructor_args: &[Expr], options: CompileOptions) -> Result { +pub fn compile_contract<'i>( + source: &'i str, + constructor_args: &[Expr<'i>], + options: CompileOptions, +) -> Result, CompilerError> { let contract = parse_contract_ast(source)?; compile_contract_impl(&contract, constructor_args, options, Some(source)) } -pub fn compile_contract_ast( - contract: &ContractAst, - constructor_args: &[Expr], +pub fn compile_contract_ast<'i>( + contract: &ContractAst<'i>, + constructor_args: &[Expr<'i>], options: CompileOptions, -) -> Result { +) -> Result, CompilerError> { compile_contract_impl(contract, constructor_args, options, None) } -fn compile_contract_impl( - contract: &ContractAst, - constructor_args: &[Expr], +fn compile_contract_impl<'i>( + contract: &ContractAst<'i>, + constructor_args: &[Expr<'i>], options: CompileOptions, - source: Option<&str>, -) -> Result { + source: Option<&'i str>, +) -> Result, CompilerError> { if contract.functions.is_empty() { return Err(CompilerError::Unsupported("contract has no functions".to_string())); } - let entrypoint_functions: Vec<&FunctionAst> = contract.functions.iter().filter(|func| func.entrypoint).collect(); + let entrypoint_functions: Vec<&FunctionAst<'i>> = contract.functions.iter().filter(|func| func.entrypoint).collect(); if entrypoint_functions.is_empty() { return Err(CompilerError::Unsupported("contract has no entrypoint functions".to_string())); } @@ -106,7 +92,8 @@ fn compile_contract_impl( let without_selector = entrypoint_functions.len() == 1; - let mut constants = contract.constants.clone(); + let mut constants: HashMap> = + contract.constants.iter().map(|constant| (constant.name.clone(), constant.expr.clone())).collect(); for (param, value) in contract.params.iter().zip(constructor_args.iter()) { constants.insert(param.name.clone(), value.clone()); } @@ -114,7 +101,7 @@ fn compile_contract_impl( let functions_map = contract.functions.iter().cloned().map(|func| (func.name.clone(), func)).collect::>(); let function_order = contract.functions.iter().enumerate().map(|(index, func)| (func.name.clone(), index)).collect::>(); - let abi = build_function_abi(contract); + let function_abi_entries = build_function_abi_entries(contract); let uses_script_size = contract_uses_script_size(contract); let mut script_size = if uses_script_size { Some(100i64) } else { None }; @@ -191,7 +178,7 @@ fn compile_contract_impl( contract_name: contract.name.clone(), script, ast: contract.clone(), - abi, + abi: function_abi_entries, without_selector, debug_info, }); @@ -204,7 +191,7 @@ fn compile_contract_impl( contract_name: contract.name.clone(), script, ast: contract.clone(), - abi, + abi: function_abi_entries, without_selector, debug_info, }); @@ -215,8 +202,8 @@ fn compile_contract_impl( Err(CompilerError::Unsupported("script size did not stabilize".to_string())) } -fn contract_uses_script_size(contract: &ContractAst) -> bool { - if contract.constants.values().any(expr_uses_script_size) { +fn contract_uses_script_size<'i>(contract: &ContractAst<'i>) -> bool { + if contract.constants.iter().any(|constant| expr_uses_script_size(&constant.expr)) { return true; } if contract.fields.iter().any(|field| expr_uses_script_size(&field.expr)) { @@ -225,12 +212,12 @@ fn contract_uses_script_size(contract: &ContractAst) -> bool { contract.functions.iter().any(|func| func.body.iter().any(statement_uses_script_size)) } -fn compile_contract_fields( - fields: &[ContractFieldAst], - base_constants: &HashMap, +fn compile_contract_fields<'i>( + fields: &[ContractFieldAst<'i>], + base_constants: &HashMap>, options: CompileOptions, script_size: Option, -) -> Result<(HashMap, Vec), CompilerError> { +) -> Result<(HashMap>, Vec), CompilerError> { let mut env = base_constants.clone(); let mut field_values = HashMap::new(); let mut field_types = HashMap::new(); @@ -256,7 +243,7 @@ fn compile_contract_fields( let mut compile_visiting = HashSet::new(); let mut stack_depth = 0i64; if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { - let Expr::Int(value) = resolved else { + let ExprKind::Int(value) = &resolved.kind else { return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); }; builder.add_data(&value.to_le_bytes())?; @@ -284,110 +271,109 @@ fn compile_contract_fields( Ok((field_values, builder.drain())) } -fn statement_uses_script_size(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), - StatementKind::TupleAssignment { expr, .. } => expr_uses_script_size(expr), - StatementKind::ArrayPush { expr, .. } => expr_uses_script_size(expr), - StatementKind::FunctionCall { name, args } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), - StatementKind::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), - StatementKind::StateFunctionCallAssign { name, args, .. } => { - name == "readInputState" || args.iter().any(expr_uses_script_size) - } - StatementKind::Assign { expr, .. } => expr_uses_script_size(expr), - StatementKind::TimeOp { expr, .. } => expr_uses_script_size(expr), - StatementKind::Require { expr, .. } => expr_uses_script_size(expr), - StatementKind::If { condition, then_branch, else_branch } => { +fn statement_uses_script_size(stmt: &Statement<'_>) -> bool { + match stmt { + Statement::VariableDefinition { expr, .. } => expr.as_ref().is_some_and(expr_uses_script_size), + Statement::TupleAssignment { expr, .. } => expr_uses_script_size(expr), + Statement::ArrayPush { expr, .. } => expr_uses_script_size(expr), + Statement::FunctionCall { name, args, .. } => name == "validateOutputState" || args.iter().any(expr_uses_script_size), + Statement::FunctionCallAssign { args, .. } => args.iter().any(expr_uses_script_size), + Statement::StateFunctionCallAssign { name, args, .. } => name == "readInputState" || args.iter().any(expr_uses_script_size), + Statement::Assign { expr, .. } => expr_uses_script_size(expr), + Statement::TimeOp { expr, .. } => expr_uses_script_size(expr), + Statement::Require { expr, .. } => expr_uses_script_size(expr), + Statement::If { condition, then_branch, else_branch, .. } => { expr_uses_script_size(condition) || then_branch.iter().any(statement_uses_script_size) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(statement_uses_script_size)) } - StatementKind::For { start, end, body, .. } => { + Statement::For { start, end, body, .. } => { expr_uses_script_size(start) || expr_uses_script_size(end) || body.iter().any(statement_uses_script_size) } - StatementKind::Yield { expr } => expr_uses_script_size(expr), - StatementKind::Return { exprs } => exprs.iter().any(expr_uses_script_size), - StatementKind::Console { args } => args.iter().any(|arg| match arg { - crate::ast::ConsoleArg::Identifier(_) => false, + Statement::Yield { expr, .. } => expr_uses_script_size(expr), + Statement::Return { exprs, .. } => exprs.iter().any(expr_uses_script_size), + Statement::Console { args, .. } => args.iter().any(|arg| match arg { + crate::ast::ConsoleArg::Identifier(_, _) => false, crate::ast::ConsoleArg::Literal(expr) => expr_uses_script_size(expr), }), } } -fn expr_uses_script_size(expr: &Expr) -> bool { - match expr { - Expr::Nullary(NullaryOp::ThisScriptSize) => true, - Expr::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, - Expr::Unary { expr, .. } => expr_uses_script_size(expr), - Expr::Binary { left, right, .. } => expr_uses_script_size(left) || expr_uses_script_size(right), - Expr::IfElse { condition, then_expr, else_expr } => { +fn expr_uses_script_size<'i>(expr: &Expr<'i>) -> bool { + match &expr.kind { + ExprKind::Nullary(NullaryOp::ThisScriptSize) => true, + ExprKind::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, + ExprKind::Unary { expr, .. } => expr_uses_script_size(expr), + ExprKind::Binary { left, right, .. } => expr_uses_script_size(left) || expr_uses_script_size(right), + ExprKind::IfElse { condition, then_expr, else_expr } => { expr_uses_script_size(condition) || expr_uses_script_size(then_expr) || expr_uses_script_size(else_expr) } - Expr::Array(values) => values.iter().any(expr_uses_script_size), - Expr::StateObject(fields) => fields.iter().any(|field| expr_uses_script_size(&field.expr)), - Expr::Call { args, .. } => args.iter().any(expr_uses_script_size), - Expr::New { args, .. } => args.iter().any(expr_uses_script_size), - Expr::Split { source, index, .. } => expr_uses_script_size(source) || expr_uses_script_size(index), - Expr::Slice { source, start, end } => { + ExprKind::Array(values) => values.iter().any(expr_uses_script_size), + ExprKind::StateObject(fields) => fields.iter().any(|field| expr_uses_script_size(&field.expr)), + ExprKind::Call { args, .. } => args.iter().any(expr_uses_script_size), + ExprKind::New { args, .. } => args.iter().any(expr_uses_script_size), + ExprKind::Split { source, index, .. } => expr_uses_script_size(source) || expr_uses_script_size(index), + ExprKind::Slice { source, start, end, .. } => { expr_uses_script_size(source) || expr_uses_script_size(start) || expr_uses_script_size(end) } - Expr::ArrayIndex { source, index } => expr_uses_script_size(source) || expr_uses_script_size(index), - Expr::Introspection { index, .. } => expr_uses_script_size(index), - Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) | Expr::Identifier(_) => false, - Expr::Nullary(_) => false, + ExprKind::UnarySuffix { source, .. } => expr_uses_script_size(source), + ExprKind::ArrayIndex { source, index } => expr_uses_script_size(source) || expr_uses_script_size(index), + ExprKind::Introspection { index, .. } => expr_uses_script_size(index), + ExprKind::Int(_) + | ExprKind::Bool(_) + | ExprKind::Byte(_) + | ExprKind::String(_) + | ExprKind::Identifier(_) + | ExprKind::DateLiteral(_) + | ExprKind::NumberWithUnit { .. } + | ExprKind::Nullary(_) => false, } } -// Helper to check if an expression is an array of bytes -fn is_byte_array(expr: &Expr) -> bool { - match expr { - Expr::Array(values) => values.iter().all(|v| matches!(v, Expr::Byte(_))), - _ => false, - } +fn is_byte_array<'i>(expr: &Expr<'i>) -> bool { + byte_array_len(expr).is_some() } -// Helper to get the length of a byte array -fn byte_array_len(expr: &Expr) -> Option { - match expr { - Expr::Array(values) if values.iter().all(|v| matches!(v, Expr::Byte(_))) => Some(values.len()), +fn byte_array_len<'i>(expr: &Expr<'i>) -> Option { + match &expr.kind { + ExprKind::Array(values) if values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))) => Some(values.len()), _ => None, } } -fn expr_matches_type_ref(expr: &Expr, type_ref: &TypeRef) -> bool { +/// Does the expression match the expected type passed as a secondary argument. +/// +/// If type is a fixed-size array (known at parsing time), it also verifies the array length. +fn expr_matches_type_ref<'i>(expr: &Expr<'i>, type_ref: &TypeRef) -> bool { if is_array_type_ref(type_ref) { - // Check for fixed-size array type[N] if let Some(size) = array_size_ref(type_ref) { - // For fixed-size arrays like byte[4], int[3] if let Some(element_type) = array_element_type_ref(type_ref) { if element_type.base == TypeBase::Byte { - // byte[N] should match Expr::Array of Expr::Byte with exact length N return byte_array_len(expr) == Some(size); } - // For other fixed-size arrays, match array literal - return matches!(expr, Expr::Array(values) if values.len() == size && array_literal_matches_type_ref(values, type_ref)); + return matches!(&expr.kind, ExprKind::Array(values) if values.len() == size && array_literal_matches_type_ref(values, type_ref)); } } - // Dynamic arrays type[] - return is_byte_array(expr) || matches!(expr, Expr::Array(values) if array_literal_matches_type_ref(values, type_ref)); + return is_byte_array(expr) + || matches!(&expr.kind, ExprKind::Array(values) if array_literal_matches_type_ref(values, type_ref)); } + match type_ref.base { - TypeBase::Int => matches!(expr, Expr::Int(_)), - TypeBase::Bool => matches!(expr, Expr::Bool(_)), - TypeBase::String => matches!(expr, Expr::String(_)), - TypeBase::Byte => matches!(expr, Expr::Byte(_)), + TypeBase::Int => matches!(&expr.kind, ExprKind::Int(_) | ExprKind::DateLiteral(_)), + TypeBase::Bool => matches!(&expr.kind, ExprKind::Bool(_)), + TypeBase::String => matches!(&expr.kind, ExprKind::String(_)), + TypeBase::Byte => matches!(&expr.kind, ExprKind::Byte(_)), TypeBase::Pubkey => byte_array_len(expr) == Some(32), TypeBase::Sig => byte_array_len(expr) == Some(65), TypeBase::Datasig => byte_array_len(expr) == Some(64), } } -fn array_literal_matches_type_ref(values: &[Expr], type_ref: &TypeRef) -> bool { +fn array_literal_matches_type_ref<'i>(values: &[Expr<'i>], type_ref: &TypeRef) -> bool { let Some(element_type) = array_element_type_ref(type_ref) else { return false; }; - // Check if this is a fixed-size array if let Some(expected_size) = array_size_ref(type_ref) { if values.len() != expected_size { return false; @@ -397,11 +383,11 @@ fn array_literal_matches_type_ref(values: &[Expr], type_ref: &TypeRef) -> bool { values.iter().all(|value| expr_matches_type_ref(value, &element_type)) } -fn array_literal_matches_type_with_env_ref( - values: &[Expr], +fn array_literal_matches_type_with_env_ref<'i>( + values: &[Expr<'i>], type_ref: &TypeRef, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> bool { let Some(element_type) = array_element_type_ref(type_ref) else { return false; @@ -413,8 +399,8 @@ fn array_literal_matches_type_with_env_ref( } } - values.iter().all(|value| match value { - Expr::Identifier(name) => types + values.iter().all(|value| match &value.kind { + ExprKind::Identifier(name) => types .get(name) .and_then(|value_type| parse_type_ref(value_type).ok()) .is_some_and(|value_type| is_type_assignable_ref(&value_type, &element_type, constants)), @@ -422,7 +408,7 @@ fn array_literal_matches_type_with_env_ref( }) } -fn build_function_abi(contract: &ContractAst) -> FunctionAbi { +fn build_function_abi_entries<'i>(contract: &ContractAst<'i>) -> Vec { contract .functions .iter() @@ -457,11 +443,11 @@ fn array_size_ref(type_ref: &TypeRef) -> Option { } } -fn array_size_with_constants_ref(type_ref: &TypeRef, constants: &HashMap) -> Option { +fn array_size_with_constants_ref<'i>(type_ref: &TypeRef, constants: &HashMap>) -> Option { match type_ref.array_size()? { ArrayDim::Fixed(size) => Some(*size), ArrayDim::Constant(name) => { - if let Some(Expr::Int(value)) = constants.get(name) { + if let Some(Expr { kind: ExprKind::Int(value), .. }) = constants.get(name) { if *value >= 0 { return Some(*value as usize); } @@ -500,33 +486,33 @@ fn array_element_size_ref(type_ref: &TypeRef) -> Option { array_element_type_ref(type_ref).and_then(|element| fixed_type_size_ref(&element)) } -fn contains_return(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::Return { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { +fn contains_return(stmt: &Statement<'_>) -> bool { + match stmt { + Statement::Return { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_return) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_return)) } - StatementKind::For { body, .. } => body.iter().any(contains_return), + Statement::For { body, .. } => body.iter().any(contains_return), _ => false, } } -fn contains_yield(stmt: &Statement) -> bool { - match &stmt.kind { - StatementKind::Yield { .. } => true, - StatementKind::If { then_branch, else_branch, .. } => { +fn contains_yield(stmt: &Statement<'_>) -> bool { + match stmt { + Statement::Yield { .. } => true, + Statement::If { then_branch, else_branch, .. } => { then_branch.iter().any(contains_yield) || else_branch.as_ref().is_some_and(|branch| branch.iter().any(contains_yield)) } - StatementKind::For { body, .. } => body.iter().any(contains_yield), + Statement::For { body, .. } => body.iter().any(contains_yield), _ => false, } } -fn validate_return_types( - exprs: &[Expr], +fn validate_return_types<'i>( + exprs: &[Expr<'i>], return_types: &[TypeRef], types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> Result<(), CompilerError> { if return_types.is_empty() { return Err(CompilerError::Unsupported("return requires function return types".to_string())); @@ -547,7 +533,7 @@ fn has_explicit_array_size_ref(type_ref: &TypeRef) -> bool { !matches!(type_ref.array_size(), Some(ArrayDim::Dynamic) | None) } -fn is_array_type_assignable_ref(actual: &TypeRef, expected: &TypeRef, constants: &HashMap) -> bool { +fn is_array_type_assignable_ref<'i>(actual: &TypeRef, expected: &TypeRef, constants: &HashMap>) -> bool { if actual == expected { return true; } @@ -570,46 +556,48 @@ fn is_array_type_assignable_ref(actual: &TypeRef, expected: &TypeRef, constants: } } -fn is_type_assignable_ref(actual: &TypeRef, expected: &TypeRef, constants: &HashMap) -> bool { +fn is_type_assignable_ref<'i>(actual: &TypeRef, expected: &TypeRef, constants: &HashMap>) -> bool { actual == expected || is_array_type_assignable_ref(actual, expected, constants) } -fn expr_matches_type_with_env_ref( - expr: &Expr, +fn expr_matches_type_with_env_ref<'i>( + expr: &Expr<'i>, type_ref: &TypeRef, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> bool { - match expr { - Expr::Identifier(name) => { + match &expr.kind { + ExprKind::Identifier(name) => { types.get(name).and_then(|t| parse_type_ref(t).ok()).is_some_and(|t| is_type_assignable_ref(&t, type_ref, constants)) } - Expr::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), + ExprKind::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), _ => expr_matches_type_ref(expr, type_ref), } } -fn expr_matches_return_type_ref( - expr: &Expr, +fn expr_matches_return_type_ref<'i>( + expr: &Expr<'i>, type_ref: &TypeRef, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> bool { - match expr { - Expr::Identifier(name) => { + match &expr.kind { + ExprKind::Identifier(name) => { types.get(name).and_then(|t| parse_type_ref(t).ok()).is_some_and(|t| is_type_assignable_ref(&t, type_ref, constants)) } - Expr::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), - Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) => expr_matches_type_ref(expr, type_ref), + ExprKind::Array(values) => is_array_type_ref(type_ref) && array_literal_matches_type_ref(values, type_ref), + ExprKind::Int(_) | ExprKind::DateLiteral(_) | ExprKind::Bool(_) | ExprKind::Byte(_) | ExprKind::String(_) => { + expr_matches_type_ref(expr, type_ref) + } _ => true, } } -fn infer_fixed_array_type_from_initializer_ref( +fn infer_fixed_array_type_from_initializer_ref<'i>( declared_type: &TypeRef, - initializer: Option<&Expr>, + initializer: Option<&Expr<'i>>, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> Option { if !declared_type.array_size().is_some_and(|dim| matches!(dim, ArrayDim::Dynamic)) { return None; @@ -618,13 +606,13 @@ fn infer_fixed_array_type_from_initializer_ref( let element_type = array_element_type_ref(declared_type)?; let init = initializer?; - match init { - Expr::Array(values) => { + match &init.kind { + ExprKind::Array(values) => { let mut inferred = element_type.clone(); inferred.array_dims.push(ArrayDim::Fixed(values.len())); if array_literal_matches_type_with_env_ref(values, &inferred, types, constants) { Some(inferred) } else { None } } - Expr::Identifier(name) => { + ExprKind::Identifier(name) => { let other_type = parse_type_ref(types.get(name)?).ok()?; if !is_array_type_ref(&other_type) || array_element_type_ref(&other_type) != Some(element_type.clone()) { return None; @@ -638,15 +626,15 @@ fn infer_fixed_array_type_from_initializer_ref( } } -fn expr_matches_type(expr: &Expr, type_name: &str) -> bool { +fn expr_matches_type<'i>(expr: &Expr<'i>, type_name: &str) -> bool { parse_type_ref(type_name).is_ok_and(|type_ref| expr_matches_type_ref(expr, &type_ref)) } -fn array_literal_matches_type_with_env( - values: &[Expr], +fn array_literal_matches_type_with_env<'i>( + values: &[Expr<'i>], type_name: &str, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> bool { parse_type_ref(type_name).is_ok_and(|type_ref| array_literal_matches_type_with_env_ref(values, &type_ref, types, constants)) } @@ -666,7 +654,7 @@ fn array_size(type_name: &str) -> Option { array_size_ref(&type_ref) } -fn array_size_with_constants(type_name: &str, constants: &HashMap) -> Option { +fn array_size_with_constants<'i>(type_name: &str, constants: &HashMap>) -> Option { let type_ref = parse_type_ref(type_name).ok()?; array_size_with_constants_ref(&type_ref, constants) } @@ -681,7 +669,7 @@ fn array_element_size(type_name: &str) -> Option { array_element_size_ref(&type_ref) } -fn is_type_assignable(actual: &str, expected: &str, constants: &HashMap) -> bool { +fn is_type_assignable<'i>(actual: &str, expected: &str, constants: &HashMap>) -> bool { let Ok(actual_type) = parse_type_ref(actual) else { return false; }; @@ -691,27 +679,27 @@ fn is_type_assignable(actual: &str, expected: &str, constants: &HashMap( + expr: &Expr<'i>, type_name: &str, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> bool { parse_type_ref(type_name).is_ok_and(|type_ref| expr_matches_type_with_env_ref(expr, &type_ref, types, constants)) } -fn infer_fixed_array_type_from_initializer( +fn infer_fixed_array_type_from_initializer<'i>( declared_type: &str, - initializer: Option<&Expr>, + initializer: Option<&Expr<'i>>, types: &HashMap, - constants: &HashMap, + constants: &HashMap>, ) -> Option { let declared_type = parse_type_ref(declared_type).ok()?; infer_fixed_array_type_from_initializer_ref(&declared_type, initializer, types, constants).map(|t| type_name_from_ref(&t)) } -impl CompiledContract { - pub fn build_sig_script(&self, function_name: &str, args: Vec) -> Result, CompilerError> { +impl<'i> CompiledContract<'i> { + pub fn build_sig_script(&self, function_name: &str, args: Vec>) -> Result, CompilerError> { let function = self .abi .iter() @@ -735,17 +723,17 @@ impl CompiledContract { let mut builder = ScriptBuilder::new(); for (input, arg) in function.inputs.iter().zip(args) { if is_array_type(&input.type_name) { - match arg { - Expr::Array(ref values) => { - // Check if it's a byte array or other array type + let kind = arg.kind.clone(); + match kind { + ExprKind::Array(values) => { if is_byte_array(&arg) { - // Extract bytes from Expr::Byte array - let bytes: Vec = - values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + let bytes: Vec = values + .iter() + .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) + .collect(); builder.add_data(&bytes)?; } else { - // Regular array - encode it - let bytes = encode_array_literal(values, &input.type_name)?; + let bytes = encode_array_literal(&values, &input.type_name)?; builder.add_data(&bytes)?; } } @@ -768,22 +756,28 @@ impl CompiledContract { } } -fn push_sigscript_arg(builder: &mut ScriptBuilder, arg: Expr) -> Result<(), CompilerError> { - match arg { - Expr::Int(value) => { +fn push_sigscript_arg<'i>(builder: &mut ScriptBuilder, arg: Expr<'i>) -> Result<(), CompilerError> { + match arg.kind { + ExprKind::Int(value) => { builder.add_i64(value)?; } - Expr::Bool(value) => { + ExprKind::Bool(value) => { builder.add_i64(if value { 1 } else { 0 })?; } - Expr::String(value) => { + ExprKind::String(value) => { builder.add_data(value.as_bytes())?; } - Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { - // Handle byte arrays - let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + ExprKind::Byte(value) => { + builder.add_data(&[value])?; + } + ExprKind::Array(values) if values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))) => { + let bytes: Vec = + values.iter().filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }).collect(); builder.add_data(&bytes)?; } + ExprKind::DateLiteral(value) => { + builder.add_i64(value)?; + } _ => { return Err(CompilerError::Unsupported("signature script arguments must be literals".to_string())); } @@ -791,22 +785,23 @@ fn push_sigscript_arg(builder: &mut ScriptBuilder, arg: Expr) -> Result<(), Comp Ok(()) } -fn encode_fixed_size_value(value: &Expr, type_name: &str) -> Result, CompilerError> { +fn encode_fixed_size_value<'i>(value: &Expr<'i>, type_name: &str) -> Result, CompilerError> { match type_name { "int" => { - let Expr::Int(number) = value else { - return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); + let number = match &value.kind { + ExprKind::Int(number) | ExprKind::DateLiteral(number) => *number, + _ => return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())), }; Ok(number.to_le_bytes().to_vec()) } "bool" => { - let Expr::Bool(flag) = value else { + let ExprKind::Bool(flag) = &value.kind else { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); }; Ok(vec![u8::from(*flag)]) } "byte" => { - let Expr::Byte(byte) = value else { + let ExprKind::Byte(byte) = &value.kind else { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); }; Ok(vec![*byte]) @@ -818,10 +813,13 @@ fn encode_fixed_size_value(value: &Expr, type_name: &str) -> Result, Com if len != 32 { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); } - let Expr::Array(bytes_exprs) = value else { + let ExprKind::Array(bytes_exprs) = &value.kind else { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); }; - Ok(bytes_exprs.iter().filter_map(|value| if let Expr::Byte(byte) = value { Some(*byte) } else { None }).collect()) + Ok(bytes_exprs + .iter() + .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) + .collect()) } _ => { // Handle fixed-size byte arrays like byte[N] @@ -833,18 +831,18 @@ fn encode_fixed_size_value(value: &Expr, type_name: &str) -> Result, Com if len != size { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); } - let Expr::Array(bytes_exprs) = value else { + let ExprKind::Array(bytes_exprs) = &value.kind else { return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())); }; return Ok(bytes_exprs .iter() - .filter_map(|value| if let Expr::Byte(byte) = value { Some(*byte) } else { None }) + .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) .collect()); } } // Handle nested fixed-size arrays with known element sizes. - if let Expr::Array(values) = value { + if let ExprKind::Array(values) = &value.kind { let element_type = array_element_type(type_name) .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; let expected_len = array_size(type_name) @@ -865,7 +863,7 @@ fn encode_fixed_size_value(value: &Expr, type_name: &str) -> Result, Com } } -fn encode_array_literal(values: &[Expr], type_name: &str) -> Result, CompilerError> { +fn encode_array_literal<'i>(values: &[Expr<'i>], type_name: &str) -> Result, CompilerError> { let element_type = array_element_type(type_name) .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; let mut out = Vec::new(); @@ -878,13 +876,13 @@ fn encode_array_literal(values: &[Expr], type_name: &str) -> Result, Com Ok(out) } -fn infer_fixed_type_from_literal_expr(expr: &Expr) -> Option { - match expr { - Expr::Int(_) => Some("int".to_string()), - Expr::Bool(_) => Some("bool".to_string()), - Expr::Byte(_) => Some("byte".to_string()), - Expr::Array(values) if is_byte_array(expr) => Some(format!("byte[{}]", values.len())), - Expr::Array(values) => { +fn infer_fixed_type_from_literal_expr<'i>(expr: &Expr<'i>) -> Option { + match &expr.kind { + ExprKind::Int(_) | ExprKind::DateLiteral(_) => Some("int".to_string()), + ExprKind::Bool(_) => Some("bool".to_string()), + ExprKind::Byte(_) => Some("byte".to_string()), + ExprKind::Array(values) if is_byte_array(expr) => Some(format!("byte[{}]", values.len())), + ExprKind::Array(values) => { let nested_type = infer_fixed_array_literal_type(values)?; Some(nested_type.trim_end_matches("[]").to_string()) } @@ -892,7 +890,7 @@ fn infer_fixed_type_from_literal_expr(expr: &Expr) -> Option { } } -fn infer_fixed_array_literal_type(values: &[Expr]) -> Option { +fn infer_fixed_array_literal_type<'i>(values: &[Expr<'i>]) -> Option { if values.is_empty() { return None; } @@ -905,7 +903,7 @@ fn infer_fixed_array_literal_type(values: &[Expr]) -> Option { } } -pub fn function_branch_index(contract: &ContractAst, function_name: &str) -> Result { +pub fn function_branch_index<'i>(contract: &ContractAst<'i>, function_name: &str) -> Result { contract .functions .iter() @@ -916,23 +914,23 @@ pub fn function_branch_index(contract: &ContractAst, function_name: &str) -> Res } #[derive(Debug)] -struct CompiledFunction { +struct CompiledFunction<'i> { name: String, script: Vec, - debug: FunctionDebugRecorder, + debug: FunctionDebugRecorder<'i>, } -fn compile_function( - function: &FunctionAst, +fn compile_function<'i>( + function: &FunctionAst<'i>, function_index: usize, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], contract_field_prefix_len: usize, - constants: &HashMap, + constants: &HashMap>, options: CompileOptions, - functions: &HashMap, + functions: &HashMap>, function_order: &HashMap, script_size: Option, -) -> Result { +) -> Result, CompilerError> { let contract_field_count = contract_fields.len(); let param_count = function.params.len(); let mut params = function @@ -964,16 +962,15 @@ fn compile_function( return Err(CompilerError::Unsupported(format!("array element type must have known size: {return_type_name}"))); } } - let mut env: HashMap = constants.clone(); + let mut env: HashMap> = constants.clone(); // `env` is checked before `params` during identifier compilation. - // Remove any constructor-constant names that collide with function params, - // otherwise the compiler would inline the constant and ignore the runtime arg. + // Remove any constructor-constant names that collide with function params. for param in &function.params { env.remove(¶m.name); } let mut builder = ScriptBuilder::new(); let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function, contract_fields); - let mut yields: Vec = Vec::new(); + let mut yields: Vec> = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { return Err(CompilerError::Unsupported("yield requires allow_yield=true".to_string())); @@ -985,7 +982,7 @@ fn compile_function( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(Statement::Return { .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1002,16 +999,14 @@ fn compile_function( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, StatementKind::Return { .. }) { + if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; validate_return_types(exprs, &function.return_types, &types, constants)?; for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new()).map_err(|err| err.with_span(&expr.span))?; yields.push(resolved); } recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; @@ -1033,7 +1028,8 @@ fn compile_function( &mut yields, script_size, &mut recorder, - )?; + ) + .map_err(|err| err.with_span(&stmt.span()))?; let end = builder.script().len(); recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } @@ -1078,25 +1074,25 @@ fn compile_function( } #[allow(clippy::too_many_arguments)] -fn compile_statement( - stmt: &Statement, - env: &mut HashMap, +fn compile_statement<'i>( + stmt: &Statement<'i>, + env: &mut HashMap>, params: &HashMap, types: &mut HashMap, builder: &mut ScriptBuilder, options: CompileOptions, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, + contract_constants: &HashMap>, + functions: &HashMap>, function_order: &HashMap, function_index: usize, - yields: &mut Vec, + yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + debug_recorder: &mut FunctionDebugRecorder<'i>, ) -> Result<(), CompilerError> { - match &stmt.kind { - StatementKind::VariableDefinition { type_ref, name, expr, .. } => { + match stmt { + Statement::VariableDefinition { type_ref, name, expr, .. } => { let type_name = type_name_from_ref(type_ref); let effective_type_name = if is_array_type(&type_name) && array_size_with_constants(&type_name, contract_constants).is_none() { @@ -1121,9 +1117,9 @@ fn compile_statement( let is_byte_array_type = effective_type_name.starts_with("byte[") && effective_type_name.ends_with("[]"); let initial = match expr { - Some(Expr::Identifier(other)) => match types.get(other) { + Some(Expr { kind: ExprKind::Identifier(other), .. }) => match types.get(other) { Some(other_type) if is_type_assignable(other_type, &effective_type_name, contract_constants) => { - Expr::Identifier(other.clone()) + Expr::new(ExprKind::Identifier(other.clone()), span::Span::default()) } Some(_) => { return Err(CompilerError::Unsupported("array assignment requires compatible array types".to_string())); @@ -1134,14 +1130,14 @@ fn compile_statement( // byte[] can be initialized from any bytes expression e.clone() } - Some(Expr::Array(values)) => { + Some(e @ Expr { kind: ExprKind::Array(values), .. }) => { if !array_literal_matches_type_with_env(values, &effective_type_name, types, contract_constants) { return Err(CompilerError::Unsupported("array initializer must be another array".to_string())); } - resolve_expr(Expr::Array(values.clone()), env, &mut HashSet::new())? + resolve_expr(Expr::new(ExprKind::Array(values.clone()), e.span), env, &mut HashSet::new())? } Some(_) => return Err(CompilerError::Unsupported("array initializer must be another array".to_string())), - None => Expr::Array(Vec::new()), + None => Expr::new(ExprKind::Array(Vec::new()), span::Span::default()), }; env.insert(name.clone(), initial); types.insert(name.clone(), effective_type_name.clone()); @@ -1152,7 +1148,7 @@ fn compile_statement( expr.clone().ok_or_else(|| CompilerError::Unsupported("variable definition requires initializer".to_string()))?; // For array literals, validate that the size matches the declared type - if let Expr::Array(values) = &expr { + if let ExprKind::Array(values) = &expr.kind { if let Some(expected_size) = array_size_with_constants(&effective_type_name, contract_constants) { if values.len() != expected_size { return Err(CompilerError::Unsupported(format!( @@ -1173,7 +1169,8 @@ fn compile_statement( } } - let stored_expr = if matches!(expr, Expr::Array(_)) { resolve_expr(expr, env, &mut HashSet::new())? } else { expr }; + let stored_expr = + if matches!(&expr.kind, ExprKind::Array(_)) { resolve_expr(expr, env, &mut HashSet::new())? } else { expr }; env.insert(name.clone(), stored_expr); types.insert(name.clone(), effective_type_name.clone()); Ok(()) @@ -1185,7 +1182,7 @@ fn compile_statement( Ok(()) } } - StatementKind::ArrayPush { name, expr } => { + Statement::ArrayPush { name, expr, .. } => { let array_type = types.get(name).ok_or_else(|| CompilerError::UndefinedIdentifier(name.clone()))?; if !is_array_type(array_type) { return Err(CompilerError::Unsupported("push() only supported on arrays".to_string())); @@ -1195,9 +1192,15 @@ fn compile_statement( let _element_size = array_element_size(array_type) .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; let element_expr = if element_type == "int" { - Expr::Call { name: "byte[8]".to_string(), args: vec![expr.clone()] } + Expr::new( + ExprKind::Call { name: "byte[8]".to_string(), args: vec![expr.clone()], name_span: span::Span::default() }, + span::Span::default(), + ) } else if element_type == "byte" { - Expr::Call { name: "byte[1]".to_string(), args: vec![expr.clone()] } + Expr::new( + ExprKind::Call { name: "byte[1]".to_string(), args: vec![expr.clone()], name_span: span::Span::default() }, + span::Span::default(), + ) } else if element_type.contains('[') && element_type.starts_with("byte") { // Handle byte[N] type if expr_is_bytes(expr, env, types) { @@ -1211,7 +1214,14 @@ fn compile_statement( if base_type == "byte" { if let Ok(_size) = size_str.parse::() { // Cast expression to byte[N] - Expr::Call { name: element_type.to_string(), args: vec![expr.clone()] } + Expr::new( + ExprKind::Call { + name: element_type.to_string(), + args: vec![expr.clone()], + name_span: span::Span::default(), + }, + span::Span::default(), + ) } else { return Err(CompilerError::Unsupported("invalid array size".to_string())); } @@ -1229,12 +1239,15 @@ fn compile_statement( return Err(CompilerError::Unsupported("array element type not supported".to_string())); }; - let current = env.get(name).cloned().unwrap_or_else(|| Expr::Array(Vec::new())); - let updated = Expr::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }; + let current = env.get(name).cloned().unwrap_or_else(|| Expr::new(ExprKind::Array(Vec::new()), span::Span::default())); + let updated = Expr::new( + ExprKind::Binary { op: BinaryOp::Add, left: Box::new(current), right: Box::new(element_expr) }, + span::Span::default(), + ); env.insert(name.clone(), updated); Ok(()) } - StatementKind::Require { expr, .. } => { + Statement::Require { expr, .. } => { let mut stack_depth = 0i64; compile_expr( expr, @@ -1251,10 +1264,10 @@ fn compile_statement( builder.add_op(OpVerify)?; Ok(()) } - StatementKind::TimeOp { tx_var, expr, .. } => { + Statement::TimeOp { tx_var, expr, .. } => { compile_time_op_statement(tx_var, expr, env, params, types, builder, options, script_size, contract_constants) } - StatementKind::If { condition, then_branch, else_branch } => compile_if_statement( + Statement::If { condition, then_branch, else_branch, .. } => compile_if_statement( condition, then_branch, else_branch.as_deref(), @@ -1273,7 +1286,7 @@ fn compile_statement( script_size, debug_recorder, ), - StatementKind::For { ident, start, end, body } => compile_for_statement( + Statement::For { ident, start, end, body, .. } => compile_for_statement( ident, start, end, @@ -1293,22 +1306,30 @@ fn compile_statement( script_size, debug_recorder, ), - StatementKind::Yield { expr } => { + Statement::Yield { expr, .. } => { let mut visiting = HashSet::new(); let resolved = resolve_expr(expr.clone(), env, &mut visiting)?; yields.push(resolved); Ok(()) } - StatementKind::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), - StatementKind::TupleAssignment { left_name, right_name, expr, .. } => match expr.clone() { - Expr::Split { source, index, .. } => { - env.insert(left_name.clone(), Expr::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left }); - env.insert(right_name.clone(), Expr::Split { source, index, part: SplitPart::Right }); + Statement::Return { .. } => Err(CompilerError::Unsupported("return statement must be the last statement".to_string())), + Statement::TupleAssignment { left_name, right_name, expr, .. } => match &expr.kind { + ExprKind::Split { source, index, span: split_span, .. } => { + let left_expr = Expr::new( + ExprKind::Split { source: source.clone(), index: index.clone(), part: SplitPart::Left, span: *split_span }, + span::Span::default(), + ); + let right_expr = Expr::new( + ExprKind::Split { source: source.clone(), index: index.clone(), part: SplitPart::Right, span: *split_span }, + span::Span::default(), + ); + env.insert(left_name.clone(), left_expr); + env.insert(right_name.clone(), right_expr); Ok(()) } _ => Err(CompilerError::Unsupported("tuple assignment only supports split()".to_string())), }, - StatementKind::FunctionCall { name, args } => { + Statement::FunctionCall { name, args, .. } => { if name == "validateOutputState" { return compile_validate_output_state_statement( args, @@ -1326,7 +1347,7 @@ fn compile_statement( let returns = compile_inline_call( name, args, - stmt.span, + Some(SourceSpan::from(stmt.span())), params, types, env, @@ -1360,7 +1381,7 @@ fn compile_statement( } Ok(()) } - StatementKind::StateFunctionCallAssign { bindings, name, args } => { + Statement::StateFunctionCallAssign { bindings, name, args, .. } => { if name == "readInputState" { return compile_read_input_state_statement( bindings, @@ -1377,7 +1398,7 @@ fn compile_statement( name ))) } - StatementKind::FunctionCallAssign { bindings, name, args } => { + Statement::FunctionCallAssign { bindings, name, args, .. } => { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; if function.return_types.is_empty() { return Err(CompilerError::Unsupported("function has no return types".to_string())); @@ -1395,7 +1416,7 @@ fn compile_statement( let returns = compile_inline_call( name, args, - stmt.span, + Some(SourceSpan::from(stmt.span())), params, types, env, @@ -1417,13 +1438,13 @@ fn compile_statement( } Ok(()) } - StatementKind::Assign { name, expr } => { + Statement::Assign { name, expr, .. } => { if let Some(type_name) = types.get(name) { if is_array_type(type_name) { - match expr { - Expr::Identifier(other) => match types.get(other) { + match &expr.kind { + ExprKind::Identifier(other) => match types.get(other) { Some(other_type) if is_type_assignable(other_type, type_name, contract_constants) => { - env.insert(name.clone(), Expr::Identifier(other.clone())); + env.insert(name.clone(), Expr::new(ExprKind::Identifier(other.clone()), span::Span::default())); return Ok(()); } Some(_) => { @@ -1433,7 +1454,9 @@ fn compile_statement( } None => return Err(CompilerError::UndefinedIdentifier(other.clone())), }, - _ => return Err(CompilerError::Unsupported("array assignment only supports array identifiers".to_string())), + _ => { + return Err(CompilerError::Unsupported("array assignment only supports array identifiers".to_string())); + } } } } @@ -1442,11 +1465,14 @@ fn compile_statement( env.insert(name.clone(), resolved); Ok(()) } - StatementKind::Console { .. } => Ok(()), + Statement::Console { .. } => Ok(()), } } -fn encoded_field_chunk_size(field: &ContractFieldAst, contract_constants: &HashMap) -> Result { +fn encoded_field_chunk_size<'i>( + field: &ContractFieldAst<'i>, + contract_constants: &HashMap>, +) -> Result { if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { return Ok(10); } @@ -1469,13 +1495,13 @@ fn encoded_field_chunk_size(field: &ContractFieldAst, contract_constants: &HashM Ok(data_prefix(payload_size).len() + payload_size) } -fn read_input_state_binding_expr( - input_idx: &Expr, - field: &ContractFieldAst, +fn read_input_state_binding_expr<'i>( + input_idx: &Expr<'i>, + field: &ContractFieldAst<'i>, field_chunk_offset: usize, script_size_value: i64, - contract_constants: &HashMap, -) -> Result { + contract_constants: &HashMap>, +) -> Result, CompilerError> { let (field_payload_offset, field_payload_len, decode_int) = if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { (field_chunk_offset + 1, 8usize, true) @@ -1498,26 +1524,35 @@ fn read_input_state_binding_expr( ))); }; - let sig_len = Expr::Call { name: "OpTxInputScriptSigLen".to_string(), args: vec![input_idx.clone()] }; - let start = Expr::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::Binary { op: BinaryOp::Sub, left: Box::new(sig_len), right: Box::new(Expr::Int(script_size_value)) }), - right: Box::new(Expr::Int(field_payload_offset as i64)), - }; - let end = Expr::Binary { op: BinaryOp::Add, left: Box::new(start.clone()), right: Box::new(Expr::Int(field_payload_len as i64)) }; - let substr = Expr::Call { name: "OpTxInputScriptSigSubstr".to_string(), args: vec![input_idx.clone(), start, end] }; + let sig_len = Expr::call("OpTxInputScriptSigLen", vec![input_idx.clone()]); + let start = Expr::new( + ExprKind::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::new( + ExprKind::Binary { op: BinaryOp::Sub, left: Box::new(sig_len), right: Box::new(Expr::int(script_size_value)) }, + span::Span::default(), + )), + right: Box::new(Expr::int(field_payload_offset as i64)), + }, + span::Span::default(), + ); + let end = Expr::new( + ExprKind::Binary { op: BinaryOp::Add, left: Box::new(start.clone()), right: Box::new(Expr::int(field_payload_len as i64)) }, + span::Span::default(), + ); + let substr = Expr::call("OpTxInputScriptSigSubstr", vec![input_idx.clone(), start, end]); - if decode_int { Ok(Expr::Call { name: "OpBin2Num".to_string(), args: vec![substr] }) } else { Ok(substr) } + if decode_int { Ok(Expr::call("OpBin2Num", vec![substr])) } else { Ok(substr) } } -fn compile_read_input_state_statement( - bindings: &[StateBindingAst], - args: &[Expr], - env: &mut HashMap, +fn compile_read_input_state_statement<'i>( + bindings: &[StateBindingAst<'i>], + args: &[Expr<'i>], + env: &mut HashMap>, types: &mut HashMap, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { if args.len() != 1 { return Err(CompilerError::Unsupported("readInputState(input_idx) expects 1 argument".to_string())); @@ -1528,7 +1563,7 @@ fn compile_read_input_state_statement( let script_size_value = script_size.ok_or_else(|| CompilerError::Unsupported("readInputState requires this.scriptSize".to_string()))?; - let mut bindings_by_field: HashMap<&str, &StateBindingAst> = HashMap::new(); + let mut bindings_by_field: HashMap<&str, &StateBindingAst<'i>> = HashMap::new(); for binding in bindings { if bindings_by_field.insert(binding.field_name.as_str(), binding).is_some() { return Err(CompilerError::Unsupported(format!("duplicate state field '{}'", binding.field_name))); @@ -1565,16 +1600,16 @@ fn compile_read_input_state_statement( #[allow(clippy::too_many_arguments)] fn compile_validate_output_state_statement( - args: &[Expr], - env: &HashMap, + args: &[Expr<'_>], + env: &HashMap>, params: &HashMap, types: &HashMap, builder: &mut ScriptBuilder, options: CompileOptions, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'_>], contract_field_prefix_len: usize, script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { if args.len() != 2 { return Err(CompilerError::Unsupported("validateOutputState(output_idx, new_state) expects 2 arguments".to_string())); @@ -1584,7 +1619,7 @@ fn compile_validate_output_state_statement( } let output_idx = &args[0]; - let Expr::StateObject(state_entries) = &args[1] else { + let ExprKind::StateObject(state_entries) = &args[1].kind else { return Err(CompilerError::Unsupported("validateOutputState second argument must be an object literal".to_string())); }; @@ -1736,22 +1771,22 @@ fn compile_validate_output_state_statement( } #[allow(clippy::too_many_arguments)] -fn compile_inline_call( +fn compile_inline_call<'i>( name: &str, - args: &[Expr], + args: &[Expr<'i>], call_span: Option, caller_params: &HashMap, caller_types: &mut HashMap, - caller_env: &mut HashMap, + caller_env: &mut HashMap>, builder: &mut ScriptBuilder, options: CompileOptions, - contract_constants: &HashMap, - functions: &HashMap, + contract_constants: &HashMap>, + functions: &HashMap>, function_order: &HashMap, caller_index: usize, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, -) -> Result, CompilerError> { + debug_recorder: &mut FunctionDebugRecorder<'i>, +) -> Result>, CompilerError> { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = function_order.get(name).copied().ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; @@ -1777,8 +1812,8 @@ fn compile_inline_call( return Err(CompilerError::Unsupported(format!("array element type must have known size: {}", param_type_name))); } } - let mut env: HashMap = contract_constants.clone(); + let mut env: HashMap> = contract_constants.clone(); // Preserve caller synthetic inline bindings so nested inline calls can // continue resolving chains like __arg_inner_0 -> __arg_outer_0. for (name, value) in caller_env.iter() { @@ -1794,12 +1829,11 @@ fn compile_inline_call( let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; let temp_name = format!("__arg_{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); - // Inline calls bind each callee parameter to a synthetic identifier so - // callee expressions keep a stable name while still pointing at the - // caller-provided argument expression. + // Bind each callee parameter to a synthetic identifier so callee + // expressions keep stable names while pointing to caller expressions. env.insert(temp_name.clone(), resolved.clone()); types.insert(temp_name.clone(), param_type_name.clone()); - env.insert(param.name.clone(), Expr::Identifier(temp_name.clone())); + env.insert(param.name.clone(), Expr::new(ExprKind::Identifier(temp_name.clone()), span::Span::default())); caller_env.insert(temp_name.clone(), resolved); caller_types.insert(temp_name, param_type_name); } @@ -1814,7 +1848,7 @@ fn compile_inline_call( let has_return = function.body.iter().any(contains_return); if has_return { - if !matches!(function.body.last(), Some(Statement { kind: StatementKind::Return { .. }, .. })) { + if !matches!(function.body.last(), Some(Statement::Return { .. })) { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } if function.body[..function.body.len() - 1].iter().any(contains_return) { @@ -1826,27 +1860,25 @@ fn compile_inline_call( } let call_start = builder.script().len(); - // Record call boundary on caller frame and collect callee events in a child frame. let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); inline_recorder.record_inline_param_updates(function, &env, call_span, call_start)?; - let mut yields: Vec = Vec::new(); - // Use caller parameter stack indexes while compiling callee bytecode so - // identifier resolution can still pick values from the caller frame. + let mut yields: Vec> = Vec::new(); + // Use caller parameter indexes while compiling callee bytecode so + // resolution can still pick values from the caller frame. let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = inline_recorder.is_enabled().then(|| env.clone()); - if matches!(stmt.kind, StatementKind::Return { .. }) { + if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); } - let StatementKind::Return { exprs } = &stmt.kind else { unreachable!() }; - validate_return_types(exprs, &function.return_types, &types, contract_constants)?; + validate_return_types(exprs, &function.return_types, &types, contract_constants) + .map_err(|err| err.with_span(&stmt.span()))?; for expr in exprs { - let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new())?; + let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new()).map_err(|err| err.with_span(&expr.span))?; yields.push(resolved); } inline_recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; @@ -1868,7 +1900,8 @@ fn compile_inline_call( &mut yields, script_size, &mut inline_recorder, - )?; + ) + .map_err(|err| err.with_span(&stmt.span()))?; let end = builder.script().len(); inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } @@ -1888,24 +1921,24 @@ fn compile_inline_call( } #[allow(clippy::too_many_arguments)] -fn compile_if_statement( - condition: &Expr, - then_branch: &[Statement], - else_branch: Option<&[Statement]>, - env: &mut HashMap, +fn compile_if_statement<'i>( + condition: &Expr<'i>, + then_branch: &[Statement<'i>], + else_branch: Option<&[Statement<'i>]>, + env: &mut HashMap>, params: &HashMap, types: &mut HashMap, builder: &mut ScriptBuilder, options: CompileOptions, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, + contract_constants: &HashMap>, + functions: &HashMap>, function_order: &HashMap, function_index: usize, - yields: &mut Vec, + yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + debug_recorder: &mut FunctionDebugRecorder<'i>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( @@ -1973,12 +2006,12 @@ fn compile_if_statement( Ok(()) } -fn merge_env_after_if( - env: &mut HashMap, - original_env: &HashMap, - then_env: &HashMap, - else_env: &HashMap, - condition: &Expr, +fn merge_env_after_if<'i>( + env: &mut HashMap>, + original_env: &HashMap>, + then_env: &HashMap>, + else_env: &HashMap>, + condition: &Expr<'i>, ) { for (name, original_expr) in original_env { let then_expr = then_env.get(name).unwrap_or(original_expr); @@ -1989,26 +2022,29 @@ fn merge_env_after_if( } else { env.insert( name.clone(), - Expr::IfElse { - condition: Box::new(condition.clone()), - then_expr: Box::new(then_expr.clone()), - else_expr: Box::new(else_expr.clone()), - }, + Expr::new( + ExprKind::IfElse { + condition: Box::new(condition.clone()), + then_expr: Box::new(then_expr.clone()), + else_expr: Box::new(else_expr.clone()), + }, + span::Span::default(), + ), ); } } } -fn compile_time_op_statement( +fn compile_time_op_statement<'i>( tx_var: &TimeVar, - expr: &Expr, - env: &mut HashMap, + expr: &Expr<'i>, + env: &mut HashMap>, params: &HashMap, types: &HashMap, builder: &mut ScriptBuilder, options: CompileOptions, script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr(expr, env, params, types, builder, options, &mut HashSet::new(), &mut stack_depth, script_size, contract_constants)?; @@ -2026,26 +2062,25 @@ fn compile_time_op_statement( } #[allow(clippy::too_many_arguments)] -fn compile_block( - statements: &[Statement], - env: &mut HashMap, +fn compile_block<'i>( + statements: &[Statement<'i>], + env: &mut HashMap>, params: &HashMap, types: &mut HashMap, builder: &mut ScriptBuilder, options: CompileOptions, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, + contract_constants: &HashMap>, + functions: &HashMap>, function_order: &HashMap, function_index: usize, - yields: &mut Vec, + yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + debug_recorder: &mut FunctionDebugRecorder<'i>, ) -> Result<(), CompilerError> { for stmt in statements { let start = builder.script().len(); - // Snapshot only when debug is enabled; used to derive per-statement var updates. let env_before = debug_recorder.is_enabled().then(|| env.clone()); compile_statement( stmt, @@ -2063,7 +2098,8 @@ fn compile_block( yields, script_size, debug_recorder, - )?; + ) + .map_err(|err| err.with_span(&stmt.span()))?; let end = builder.script().len(); debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; } @@ -2071,25 +2107,25 @@ fn compile_block( } #[allow(clippy::too_many_arguments)] -fn compile_for_statement( +fn compile_for_statement<'i>( ident: &str, - start_expr: &Expr, - end_expr: &Expr, - body: &[Statement], - env: &mut HashMap, + start_expr: &Expr<'i>, + end_expr: &Expr<'i>, + body: &[Statement<'i>], + env: &mut HashMap>, params: &HashMap, types: &mut HashMap, builder: &mut ScriptBuilder, options: CompileOptions, - contract_fields: &[ContractFieldAst], + contract_fields: &[ContractFieldAst<'i>], contract_field_prefix_len: usize, - contract_constants: &HashMap, - functions: &HashMap, + contract_constants: &HashMap>, + functions: &HashMap>, function_order: &HashMap, function_index: usize, - yields: &mut Vec, + yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder, + debug_recorder: &mut FunctionDebugRecorder<'i>, ) -> Result<(), CompilerError> { let start = eval_const_int(start_expr, contract_constants)?; let end = eval_const_int(end_expr, contract_constants)?; @@ -2100,7 +2136,7 @@ fn compile_for_statement( let name = ident.to_string(); let previous = env.get(&name).cloned(); for value in start..end { - env.insert(name.clone(), Expr::Int(value)); + env.insert(name.clone(), Expr::int(value)); compile_block( body, env, @@ -2132,16 +2168,17 @@ fn compile_for_statement( Ok(()) } -fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result { - match expr { - Expr::Int(value) => Ok(*value), - Expr::Identifier(name) => match constants.get(name) { +fn eval_const_int<'i>(expr: &Expr<'i>, constants: &HashMap>) -> Result { + match &expr.kind { + ExprKind::Int(value) => Ok(*value), + ExprKind::DateLiteral(value) => Ok(*value), + ExprKind::Identifier(name) => match constants.get(name) { Some(value) => eval_const_int(value, constants), None => Err(CompilerError::Unsupported("for loop bounds must be constant integers".to_string())), }, - Expr::Unary { op: UnaryOp::Neg, expr } => Ok(-eval_const_int(expr, constants)?), - Expr::Unary { .. } => Err(CompilerError::Unsupported("for loop bounds must be constant integers".to_string())), - Expr::Binary { op, left, right } => { + ExprKind::Unary { op: UnaryOp::Neg, expr } => Ok(-eval_const_int(expr, constants)?), + ExprKind::Unary { .. } => Err(CompilerError::Unsupported("for loop bounds must be constant integers".to_string())), + ExprKind::Binary { op, left, right } => { let lhs = eval_const_int(left, constants)?; let rhs = eval_const_int(right, constants)?; match op { @@ -2167,66 +2204,16 @@ fn eval_const_int(expr: &Expr, constants: &HashMap) -> Result Result) -> Result { - match expr { - Expr::Unary { op, expr } => Ok(Expr::Unary { op, expr: Box::new(recurse(*expr)?) }), - Expr::Binary { op, left, right } => { - Ok(Expr::Binary { op, left: Box::new(recurse(*left)?), right: Box::new(recurse(*right)?) }) - } - Expr::IfElse { condition, then_expr, else_expr } => Ok(Expr::IfElse { - condition: Box::new(recurse(*condition)?), - then_expr: Box::new(recurse(*then_expr)?), - else_expr: Box::new(recurse(*else_expr)?), - }), - Expr::Array(values) => { - let mut rewritten = Vec::with_capacity(values.len()); - for value in values { - rewritten.push(recurse(value)?); - } - Ok(Expr::Array(rewritten)) - } - Expr::StateObject(fields) => { - let mut rewritten_fields = Vec::with_capacity(fields.len()); - for field in fields { - rewritten_fields.push(crate::ast::StateFieldExpr { name: field.name, expr: recurse(field.expr)? }); - } - Ok(Expr::StateObject(rewritten_fields)) - } - Expr::Call { name, args } => { - let mut rewritten = Vec::with_capacity(args.len()); - for arg in args { - rewritten.push(recurse(arg)?); - } - Ok(Expr::Call { name, args: rewritten }) - } - Expr::New { name, args } => { - let mut rewritten = Vec::with_capacity(args.len()); - for arg in args { - rewritten.push(recurse(arg)?); - } - Ok(Expr::New { name, args: rewritten }) - } - Expr::Split { source, index, part } => { - Ok(Expr::Split { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?), part }) - } - Expr::Slice { source, start, end } => { - Ok(Expr::Slice { source: Box::new(recurse(*source)?), start: Box::new(recurse(*start)?), end: Box::new(recurse(*end)?) }) - } - Expr::ArrayIndex { source, index } => { - Ok(Expr::ArrayIndex { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?) }) - } - Expr::Introspection { kind, index } => Ok(Expr::Introspection { kind, index: Box::new(recurse(*index)?) }), - other => Ok(other), - } -} - -fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet) -> Result { - match expr { - Expr::Identifier(name) => { - // Keep synthetic inline args unresolved in compile mode so generated - // bytecode still reads them from caller stack bindings. +fn resolve_expr<'i>( + expr: Expr<'i>, + env: &HashMap>, + visiting: &mut HashSet, +) -> Result, CompilerError> { + let Expr { kind, span } = expr; + match kind { + ExprKind::Identifier(name) => { if name.starts_with("__arg_") { - return Ok(Expr::Identifier(name)); + return Ok(Expr::new(ExprKind::Identifier(name), span)); } if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { @@ -2236,16 +2223,100 @@ fn resolve_expr(expr: Expr, env: &HashMap, visiting: &mut HashSet< visiting.remove(&name); Ok(resolved) } else { - Ok(Expr::Identifier(name)) + Ok(Expr::new(ExprKind::Identifier(name), span)) + } + } + ExprKind::Unary { op, expr } => { + Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }, span)) + } + ExprKind::Binary { op, left, right } => Ok(Expr::new( + ExprKind::Binary { + op, + left: Box::new(resolve_expr(*left, env, visiting)?), + right: Box::new(resolve_expr(*right, env, visiting)?), + }, + span, + )), + ExprKind::IfElse { condition, then_expr, else_expr } => Ok(Expr::new( + ExprKind::IfElse { + condition: Box::new(resolve_expr(*condition, env, visiting)?), + then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), + else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + }, + span, + )), + ExprKind::Array(values) => { + let mut resolved = Vec::with_capacity(values.len()); + for value in values { + resolved.push(resolve_expr(value, env, visiting)?); } + Ok(Expr::new(ExprKind::Array(resolved), span)) } - other => rewrite_expr_children(other, |child| resolve_expr(child, env, visiting)), + ExprKind::StateObject(fields) => { + let mut resolved_fields = Vec::with_capacity(fields.len()); + for field in fields { + resolved_fields.push(StateFieldExpr { + name: field.name, + expr: resolve_expr(field.expr, env, visiting)?, + span: field.span, + name_span: field.name_span, + }); + } + Ok(Expr::new(ExprKind::StateObject(resolved_fields), span)) + } + ExprKind::Call { name, args, name_span } => { + let mut resolved = Vec::with_capacity(args.len()); + for arg in args { + resolved.push(resolve_expr(arg, env, visiting)?); + } + Ok(Expr::new(ExprKind::Call { name, args: resolved, name_span }, span)) + } + ExprKind::New { name, args, name_span } => { + let mut resolved = Vec::with_capacity(args.len()); + for arg in args { + resolved.push(resolve_expr(arg, env, visiting)?); + } + Ok(Expr::new(ExprKind::New { name, args: resolved, name_span }, span)) + } + ExprKind::Split { source, index, part, span: split_span } => Ok(Expr::new( + ExprKind::Split { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + part, + span: split_span, + }, + span, + )), + ExprKind::ArrayIndex { source, index } => Ok(Expr::new( + ExprKind::ArrayIndex { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + }, + span, + )), + ExprKind::Introspection { kind, index, field_span } => { + Ok(Expr::new(ExprKind::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?), field_span }, span)) + } + ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( + ExprKind::UnarySuffix { source: Box::new(resolve_expr(*source, env, visiting)?), kind, span: suffix_span }, + span, + )), + ExprKind::Slice { source, start, end, span: slice_span } => Ok(Expr::new( + ExprKind::Slice { + source: Box::new(resolve_expr(*source, env, visiting)?), + start: Box::new(resolve_expr(*start, env, visiting)?), + end: Box::new(resolve_expr(*end, env, visiting)?), + span: slice_span, + }, + span, + )), + other => Ok(Expr::new(other, span)), } } /// Compiles a pre-resolved expression for debugger shadow evaluation. -pub fn compile_debug_expr( - expr: &Expr, +pub fn compile_debug_expr<'i>( + expr: &Expr<'i>, params: &HashMap, types: &HashMap, ) -> Result, CompilerError> { @@ -2268,27 +2339,28 @@ pub fn compile_debug_expr( Ok(builder.drain()) } -pub(super) fn resolve_expr_for_debug( - expr: Expr, - env: &HashMap, +pub(super) fn resolve_expr_for_debug<'i>( + expr: Expr<'i>, + env: &HashMap>, visiting: &mut HashSet, -) -> Result { +) -> Result, CompilerError> { let resolved = resolve_expr(expr, env, visiting)?; expand_inline_arg_placeholders(resolved, env, &mut HashSet::new()) } -fn expand_inline_arg_placeholders( - expr: Expr, - env: &HashMap, +fn expand_inline_arg_placeholders<'i>( + expr: Expr<'i>, + env: &HashMap>, visiting: &mut HashSet, -) -> Result { - match expr { - Expr::Identifier(name) => { +) -> Result, CompilerError> { + let Expr { kind, span } = expr; + match kind { + ExprKind::Identifier(name) => { if !name.starts_with("__arg_") { - return Ok(Expr::Identifier(name)); + return Ok(Expr::new(ExprKind::Identifier(name), span)); } let Some(value) = env.get(&name).cloned() else { - return Ok(Expr::Identifier(name)); + return Ok(Expr::new(ExprKind::Identifier(name), span)); }; if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); @@ -2297,71 +2369,225 @@ fn expand_inline_arg_placeholders( visiting.remove(&name); Ok(expanded) } - other => rewrite_expr_children(other, |child| expand_inline_arg_placeholders(child, env, visiting)), - } -} - -fn replace_identifier(expr: &Expr, target: &str, replacement: &Expr) -> Expr { - match expr { - Expr::Identifier(name) if name == target => replacement.clone(), - Expr::Identifier(_) => expr.clone(), - Expr::Unary { op, expr: inner } => Expr::Unary { op: *op, expr: Box::new(replace_identifier(inner, target, replacement)) }, - Expr::Binary { op, left, right } => Expr::Binary { - op: *op, - left: Box::new(replace_identifier(left, target, replacement)), - right: Box::new(replace_identifier(right, target, replacement)), - }, - Expr::Array(values) => Expr::Array(values.iter().map(|value| replace_identifier(value, target, replacement)).collect()), - Expr::StateObject(fields) => Expr::StateObject( - fields - .iter() - .map(|field| crate::ast::StateFieldExpr { - name: field.name.clone(), - expr: replace_identifier(&field.expr, target, replacement), - }) - .collect(), - ), - Expr::Call { name, args } => { - Expr::Call { name: name.clone(), args: args.iter().map(|arg| replace_identifier(arg, target, replacement)).collect() } + ExprKind::Unary { op, expr } => Ok(Expr::new( + ExprKind::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }, + span, + )), + ExprKind::Binary { op, left, right } => Ok(Expr::new( + ExprKind::Binary { + op, + left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), + right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), + }, + span, + )), + ExprKind::IfElse { condition, then_expr, else_expr } => Ok(Expr::new( + ExprKind::IfElse { + condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), + then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), + else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), + }, + span, + )), + ExprKind::Array(values) => { + let mut rewritten = Vec::with_capacity(values.len()); + for value in values { + rewritten.push(expand_inline_arg_placeholders(value, env, visiting)?); + } + Ok(Expr::new(ExprKind::Array(rewritten), span)) } - Expr::New { name, args } => { - Expr::New { name: name.clone(), args: args.iter().map(|arg| replace_identifier(arg, target, replacement)).collect() } + ExprKind::StateObject(fields) => { + let mut rewritten = Vec::with_capacity(fields.len()); + for field in fields { + rewritten.push(StateFieldExpr { + name: field.name, + expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, + span: field.span, + name_span: field.name_span, + }); + } + Ok(Expr::new(ExprKind::StateObject(rewritten), span)) } - Expr::Split { source, index, part } => Expr::Split { - source: Box::new(replace_identifier(source, target, replacement)), - index: Box::new(replace_identifier(index, target, replacement)), - part: *part, - }, - Expr::Slice { source, start, end } => Expr::Slice { - source: Box::new(replace_identifier(source, target, replacement)), - start: Box::new(replace_identifier(start, target, replacement)), - end: Box::new(replace_identifier(end, target, replacement)), - }, - Expr::ArrayIndex { source, index } => Expr::ArrayIndex { - source: Box::new(replace_identifier(source, target, replacement)), - index: Box::new(replace_identifier(index, target, replacement)), - }, - Expr::IfElse { condition, then_expr, else_expr } => Expr::IfElse { - condition: Box::new(replace_identifier(condition, target, replacement)), - then_expr: Box::new(replace_identifier(then_expr, target, replacement)), - else_expr: Box::new(replace_identifier(else_expr, target, replacement)), - }, - Expr::Introspection { kind, index } => { - Expr::Introspection { kind: *kind, index: Box::new(replace_identifier(index, target, replacement)) } + ExprKind::Call { name, args, name_span } => { + let mut rewritten = Vec::with_capacity(args.len()); + for arg in args { + rewritten.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::new(ExprKind::Call { name, args: rewritten, name_span }, span)) } - Expr::Int(_) | Expr::Bool(_) | Expr::Byte(_) | Expr::String(_) | Expr::Nullary(_) => expr.clone(), + ExprKind::New { name, args, name_span } => { + let mut rewritten = Vec::with_capacity(args.len()); + for arg in args { + rewritten.push(expand_inline_arg_placeholders(arg, env, visiting)?); + } + Ok(Expr::new(ExprKind::New { name, args: rewritten, name_span }, span)) + } + ExprKind::Split { source, index, part, span: split_span } => Ok(Expr::new( + ExprKind::Split { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + part, + span: split_span, + }, + span, + )), + ExprKind::ArrayIndex { source, index } => Ok(Expr::new( + ExprKind::ArrayIndex { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + }, + span, + )), + ExprKind::Introspection { kind, index, field_span } => Ok(Expr::new( + ExprKind::Introspection { + kind, + index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), + field_span, + }, + span, + )), + ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( + ExprKind::UnarySuffix { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + kind, + span: suffix_span, + }, + span, + )), + ExprKind::Slice { source, start, end, span: slice_span } => Ok(Expr::new( + ExprKind::Slice { + source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), + start: Box::new(expand_inline_arg_placeholders(*start, env, visiting)?), + end: Box::new(expand_inline_arg_placeholders(*end, env, visiting)?), + span: slice_span, + }, + span, + )), + other => Ok(Expr::new(other, span)), + } +} + +/// Replace `target` identifiers in `expr` with `replacement`. +/// +/// Example: for `x = x + 1`, this rewrites the right side to +/// ` + 1` before `resolve_expr` runs. +fn replace_identifier<'i>(expr: &Expr<'i>, target: &str, replacement: &Expr<'i>) -> Expr<'i> { + let span = expr.span; + match &expr.kind { + ExprKind::Identifier(name) if name == target => replacement.clone(), + ExprKind::Identifier(_) => expr.clone(), + ExprKind::Unary { op, expr: inner } => { + Expr::new(ExprKind::Unary { op: *op, expr: Box::new(replace_identifier(inner, target, replacement)) }, span) + } + ExprKind::Binary { op, left, right } => Expr::new( + ExprKind::Binary { + op: *op, + left: Box::new(replace_identifier(left, target, replacement)), + right: Box::new(replace_identifier(right, target, replacement)), + }, + span, + ), + ExprKind::Array(values) => { + Expr::new(ExprKind::Array(values.iter().map(|value| replace_identifier(value, target, replacement)).collect()), span) + } + ExprKind::StateObject(fields) => Expr::new( + ExprKind::StateObject( + fields + .iter() + .map(|field| StateFieldExpr { + name: field.name.clone(), + expr: replace_identifier(&field.expr, target, replacement), + span: field.span, + name_span: field.name_span, + }) + .collect(), + ), + span, + ), + ExprKind::Call { name, args, name_span } => Expr::new( + ExprKind::Call { + name: name.clone(), + args: args.iter().map(|arg| replace_identifier(arg, target, replacement)).collect(), + name_span: *name_span, + }, + span, + ), + ExprKind::New { name, args, name_span } => Expr::new( + ExprKind::New { + name: name.clone(), + args: args.iter().map(|arg| replace_identifier(arg, target, replacement)).collect(), + name_span: *name_span, + }, + span, + ), + ExprKind::Split { source, index, part, span: split_span } => Expr::new( + ExprKind::Split { + source: Box::new(replace_identifier(source, target, replacement)), + index: Box::new(replace_identifier(index, target, replacement)), + part: *part, + span: *split_span, + }, + span, + ), + ExprKind::Slice { source, start, end, span: slice_span } => Expr::new( + ExprKind::Slice { + source: Box::new(replace_identifier(source, target, replacement)), + start: Box::new(replace_identifier(start, target, replacement)), + end: Box::new(replace_identifier(end, target, replacement)), + span: *slice_span, + }, + span, + ), + ExprKind::ArrayIndex { source, index } => Expr::new( + ExprKind::ArrayIndex { + source: Box::new(replace_identifier(source, target, replacement)), + index: Box::new(replace_identifier(index, target, replacement)), + }, + span, + ), + ExprKind::IfElse { condition, then_expr, else_expr } => Expr::new( + ExprKind::IfElse { + condition: Box::new(replace_identifier(condition, target, replacement)), + then_expr: Box::new(replace_identifier(then_expr, target, replacement)), + else_expr: Box::new(replace_identifier(else_expr, target, replacement)), + }, + span, + ), + ExprKind::Introspection { kind, index, field_span } => Expr::new( + ExprKind::Introspection { + kind: *kind, + index: Box::new(replace_identifier(index, target, replacement)), + field_span: *field_span, + }, + span, + ), + ExprKind::UnarySuffix { source, kind, span: suffix_span } => Expr::new( + ExprKind::UnarySuffix { + source: Box::new(replace_identifier(source, target, replacement)), + kind: *kind, + span: *suffix_span, + }, + span, + ), + ExprKind::Int(_) + | ExprKind::Bool(_) + | ExprKind::Byte(_) + | ExprKind::String(_) + | ExprKind::DateLiteral(_) + | ExprKind::NumberWithUnit { .. } + | ExprKind::Nullary(_) => expr.clone(), } } -struct CompilationScope<'a> { - env: &'a HashMap, +struct CompilationScope<'a, 'i> { + env: &'a HashMap>, params: &'a HashMap, types: &'a HashMap, } -fn compile_expr( - expr: &Expr, - env: &HashMap, +fn compile_expr<'i>( + expr: &Expr<'i>, + env: &HashMap>, params: &HashMap, types: &HashMap, builder: &mut ScriptBuilder, @@ -2369,63 +2595,60 @@ fn compile_expr( visiting: &mut HashSet, stack_depth: &mut i64, script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { let scope = CompilationScope { env, params, types }; - match expr { - Expr::Int(value) => { + match &expr.kind { + ExprKind::Int(value) => { builder.add_i64(*value)?; *stack_depth += 1; Ok(()) } - Expr::Bool(value) => { + ExprKind::Bool(value) => { builder.add_op(if *value { OpTrue } else { OpFalse })?; *stack_depth += 1; Ok(()) } - Expr::Byte(b) => { - builder.add_data(&[*b])?; - *stack_depth += 1; - Ok(()) - } - Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { - // Handle byte arrays - let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); - builder.add_data(&bytes)?; + ExprKind::Byte(byte) => { + builder.add_data(&[*byte])?; *stack_depth += 1; Ok(()) } - Expr::Array(values) => { - if let Some(array_type) = infer_fixed_array_literal_type(values) { - let encoded = encode_array_literal(values, &array_type)?; - builder.add_data(&encoded)?; + ExprKind::Array(values) => { + if values.is_empty() { + builder.add_data(&[])?; *stack_depth += 1; return Ok(()); } - Err(CompilerError::Unsupported( - "array literals are only supported for fixed-size element arrays and in LockingBytecodeNullData".to_string(), - )) + let inferred_type = infer_fixed_array_literal_type(values) + .ok_or_else(|| CompilerError::Unsupported("array literal type cannot be inferred".to_string()))?; + let encoded = encode_array_literal(values, &inferred_type)?; + builder.add_data(&encoded)?; + *stack_depth += 1; + Ok(()) } - Expr::StateObject(_) => { - Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState()".to_string())) + ExprKind::StateObject(_) => { + Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState".to_string())) } - Expr::String(value) => { + ExprKind::String(value) => { builder.add_data(value.as_bytes())?; *stack_depth += 1; Ok(()) } - Expr::Identifier(name) => { + ExprKind::Identifier(name) => { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name.clone())); } if let Some(expr) = env.get(name) { - if let (Some(type_name), Expr::Array(values)) = (types.get(name), expr) { - if is_array_type(type_name) { - let encoded = encode_array_literal(values, type_name)?; - builder.add_data(&encoded)?; - *stack_depth += 1; - visiting.remove(name); - return Ok(()); + if let Some(type_name) = types.get(name) { + if let ExprKind::Array(values) = &expr.kind { + if is_array_type(type_name) { + let encoded = encode_array_literal(values, type_name)?; + builder.add_data(&encoded)?; + *stack_depth += 1; + visiting.remove(name); + return Ok(()); + } } } compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; @@ -2442,7 +2665,7 @@ fn compile_expr( visiting.remove(name); Err(CompilerError::UndefinedIdentifier(name.clone())) } - Expr::IfElse { condition, then_expr, else_expr } => { + ExprKind::IfElse { condition, then_expr, else_expr } => { compile_expr(condition, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpIf)?; *stack_depth -= 1; @@ -2455,557 +2678,942 @@ fn compile_expr( *stack_depth = depth_before + 1; Ok(()) } - Expr::Call { name, args } => match name.as_str() { - "OpSha256" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpSHA256, - script_size, - contract_constants, - ), - "sha256" => { + ExprKind::Call { name, args, .. } => { + compile_call_expr(name.as_str(), args, &scope, builder, options, visiting, stack_depth, script_size, contract_constants) + } + ExprKind::New { name, args, .. } => match name.as_str() { + "LockingBytecodeNullData" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported("LockingBytecodeNullData expects a single array argument".to_string())); + } + let script = build_null_data_script(&args[0])?; + builder.add_data(&script)?; + *stack_depth += 1; + Ok(()) + } + "ScriptPubKeyP2PK" => { if args.len() != 1 { - return Err(CompilerError::Unsupported("sha256() expects a single argument".to_string())); + return Err(CompilerError::Unsupported("ScriptPubKeyP2PK expects a single pubkey argument".to_string())); } compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_op(OpSHA256)?; + builder.add_data(&[0x00, 0x00, OpData32])?; + *stack_depth += 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_data(&[OpCheckSig])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; Ok(()) } - "date" => { + "ScriptPubKeyP2SH" => { if args.len() != 1 { - return Err(CompilerError::Unsupported("date() expects a single argument".to_string())); + return Err(CompilerError::Unsupported("ScriptPubKeyP2SH expects a single bytes32 argument".to_string())); } - let value = match &args[0] { - Expr::String(value) => value.as_str(), - Expr::Identifier(name) => { - if let Some(Expr::String(value)) = env.get(name) { - value.as_str() - } else { - return Err(CompilerError::Unsupported("date() expects a string literal".to_string())); - } - } - _ => return Err(CompilerError::Unsupported("date() expects a string literal".to_string())), - }; - let timestamp = parse_date_value(value)?; - builder.add_i64(timestamp)?; + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_data(&[0x00, 0x00])?; + *stack_depth += 1; + builder.add_data(&[OpBlake2b])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_data(&[0x20])?; *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_data(&[OpEqual])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; Ok(()) } - "OpTxSubnetId" => compile_opcode_call( - name, - args, - 0, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxSubnetId, - script_size, - contract_constants, - ), - "OpTxGas" => compile_opcode_call( - name, - args, - 0, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxGas, - script_size, - contract_constants, - ), - "OpTxPayloadLen" => compile_opcode_call( - name, - args, - 0, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxPayloadLen, - script_size, - contract_constants, - ), - "OpTxPayloadSubstr" => compile_opcode_call( - name, - args, - 2, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxPayloadSubstr, - script_size, - contract_constants, - ), - "OpOutpointTxId" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpOutpointTxId, - script_size, - contract_constants, - ), - "OpOutpointIndex" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpOutpointIndex, - script_size, - contract_constants, - ), - "OpTxInputScriptSigLen" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputScriptSigLen, - script_size, - contract_constants, - ), - "OpTxInputScriptSigSubstr" => compile_opcode_call( - name, - args, - 3, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputScriptSigSubstr, - script_size, - contract_constants, - ), - "OpTxInputSeq" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputSeq, - script_size, - contract_constants, - ), - "OpTxInputIsCoinbase" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputIsCoinbase, - script_size, - contract_constants, - ), - "OpTxInputSpkLen" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputSpkLen, - script_size, - contract_constants, - ), - "OpTxInputSpkSubstr" => compile_opcode_call( - name, - args, - 3, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxInputSpkSubstr, - script_size, - contract_constants, - ), - "OpTxOutputSpkLen" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxOutputSpkLen, - script_size, - contract_constants, - ), - "OpTxOutputSpkSubstr" => compile_opcode_call( - name, - args, - 3, - &scope, - builder, - options, - visiting, - stack_depth, - OpTxOutputSpkSubstr, - script_size, - contract_constants, - ), - "OpAuthOutputCount" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpAuthOutputCount, - script_size, - contract_constants, - ), - "OpAuthOutputIdx" => compile_opcode_call( - name, - args, - 2, - &scope, - builder, - options, - visiting, - stack_depth, - OpAuthOutputIdx, - script_size, - contract_constants, - ), - "OpInputCovenantId" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpInputCovenantId, - script_size, - contract_constants, - ), - "OpCovInputCount" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpCovInputCount, - script_size, - contract_constants, - ), - "OpCovInputIdx" => compile_opcode_call( - name, - args, - 2, - &scope, - builder, - options, - visiting, - stack_depth, - OpCovInputIdx, - script_size, - contract_constants, - ), - "OpCovOutCount" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpCovOutCount, - script_size, - contract_constants, - ), - "OpCovOutputIdx" => compile_opcode_call( - name, - args, - 2, - &scope, - builder, - options, - visiting, - stack_depth, - OpCovOutputIdx, - script_size, - contract_constants, - ), - "OpNum2Bin" => compile_opcode_call( - name, - args, - 2, - &scope, + "ScriptPubKeyP2SHFromRedeemScript" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported( + "ScriptPubKeyP2SHFromRedeemScript expects a single redeem_script argument".to_string(), + )); + } + compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_op(OpBlake2b)?; + builder.add_data(&[0x00, 0x00])?; + *stack_depth += 1; + builder.add_data(&[OpBlake2b])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_data(&[0x20])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpCat)?; + *stack_depth -= 1; + builder.add_data(&[OpEqual])?; + *stack_depth += 1; + builder.add_op(OpCat)?; + *stack_depth -= 1; + Ok(()) + } + name => Err(CompilerError::Unsupported(format!("unknown constructor: {name}"))), + }, + ExprKind::Unary { op, expr } => { + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + match op { + UnaryOp::Not => builder.add_op(OpNot)?, + UnaryOp::Neg => builder.add_op(OpNegate)?, + }; + Ok(()) + } + ExprKind::Binary { op, left, right } => { + let bytes_eq = + matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); + let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); + if bytes_add { + compile_concat_operand( + left, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_concat_operand( + right, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + } else { + compile_expr(left, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(right, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + } + match op { + BinaryOp::Or => { + builder.add_op(OpBoolOr)?; + } + BinaryOp::And => { + builder.add_op(OpBoolAnd)?; + } + BinaryOp::BitOr => { + builder.add_op(OpOr)?; + } + BinaryOp::BitXor => { + builder.add_op(OpXor)?; + } + BinaryOp::BitAnd => { + builder.add_op(OpAnd)?; + } + BinaryOp::Eq => { + builder.add_op(if bytes_eq { OpEqual } else { OpNumEqual })?; + } + BinaryOp::Ne => { + if bytes_eq { + builder.add_op(OpEqual)?; + builder.add_op(OpNot)?; + } else { + builder.add_op(OpNumNotEqual)?; + } + } + BinaryOp::Lt => { + builder.add_op(OpLessThan)?; + } + BinaryOp::Le => { + builder.add_op(OpLessThanOrEqual)?; + } + BinaryOp::Gt => { + builder.add_op(OpGreaterThan)?; + } + BinaryOp::Ge => { + builder.add_op(OpGreaterThanOrEqual)?; + } + BinaryOp::Add => { + if bytes_add { + builder.add_op(OpCat)?; + } else { + builder.add_op(OpAdd)?; + } + } + BinaryOp::Sub => { + builder.add_op(OpSub)?; + } + BinaryOp::Mul => { + builder.add_op(OpMul)?; + } + BinaryOp::Div => { + builder.add_op(OpDiv)?; + } + BinaryOp::Mod => { + builder.add_op(OpMod)?; + } + } + *stack_depth -= 1; + Ok(()) + } + ExprKind::Split { source, index, part, .. } => compile_split_part( + source, + index, + *part, + env, + params, + types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + ), + ExprKind::UnarySuffix { source, kind, .. } => match kind { + UnarySuffixKind::Length => compile_length_expr( + source, + env, + params, + types, builder, options, visiting, stack_depth, - OpNum2Bin, script_size, contract_constants, ), - "OpBin2Num" => compile_opcode_call( - name, - args, - 1, - &scope, + UnarySuffixKind::Reverse => Err(CompilerError::Unsupported("reverse() is not supported".to_string())), + }, + ExprKind::ArrayIndex { source, index } => { + let resolved_source = match source.as_ref() { + Expr { kind: ExprKind::Identifier(_), .. } => source.as_ref().clone(), + _ => resolve_expr(*source.clone(), env, visiting)?, + }; + let element_type = match &resolved_source.kind { + ExprKind::Identifier(name) => { + let type_name = types.get(name).or_else(|| { + env.get(name).and_then(|value| match &value.kind { + ExprKind::Identifier(inner) => types.get(inner), + _ => None, + }) + }); + type_name + .and_then(|t| array_element_type(t)) + .ok_or_else(|| CompilerError::Unsupported(format!("array index requires array identifier: {name}")))? + } + _ => return Err(CompilerError::Unsupported("array index requires array identifier".to_string())), + }; + let element_size = fixed_type_size(&element_type) + .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + compile_expr( + &resolved_source, + env, + params, + types, builder, options, visiting, stack_depth, - OpBin2Num, script_size, contract_constants, - ), - "OpChainblockSeqCommit" => compile_opcode_call( - name, - args, - 1, - &scope, - builder, - options, - visiting, - stack_depth, - OpChainblockSeqCommit, - script_size, - contract_constants, - ), - "bytes" => { - if args.is_empty() || args.len() > 2 { - return Err(CompilerError::Unsupported("bytes() expects one or two arguments".to_string())); + )?; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_i64(element_size)?; + *stack_depth += 1; + builder.add_op(OpMul)?; + *stack_depth -= 1; + builder.add_op(OpDup)?; + *stack_depth += 1; + builder.add_i64(element_size)?; + *stack_depth += 1; + builder.add_op(OpAdd)?; + *stack_depth -= 1; + builder.add_op(OpSubstr)?; + *stack_depth -= 2; + if element_type == "int" { + builder.add_op(OpBin2Num)?; + } + Ok(()) + } + ExprKind::Slice { source, start, end, .. } => { + compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(start, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + compile_expr(end, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + + builder.add_op(Op2Dup)?; + *stack_depth += 2; + builder.add_op(OpSwap)?; + builder.add_op(OpSub)?; + *stack_depth -= 1; + builder.add_op(OpSwap)?; + builder.add_op(OpDrop)?; + *stack_depth -= 1; + builder.add_op(OpSubstr)?; + *stack_depth -= 2; + Ok(()) + } + ExprKind::Nullary(op) => { + match op { + NullaryOp::ActiveInputIndex => { + builder.add_op(OpTxInputIndex)?; } - if args.len() == 2 { - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - compile_expr( - &args[1], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - builder.add_op(OpNum2Bin)?; - *stack_depth -= 1; - return Ok(()); + NullaryOp::ActiveScriptPubKey => { + builder.add_op(OpTxInputIndex)?; + builder.add_op(OpTxInputSpk)?; } - match &args[0] { - Expr::String(value) => { - builder.add_data(value.as_bytes())?; - *stack_depth += 1; - Ok(()) - } - Expr::Identifier(name) => { - if let Some(Expr::String(value)) = env.get(name) { - builder.add_data(value.as_bytes())?; - *stack_depth += 1; - return Ok(()); - } - if expr_is_bytes(&args[0], env, types) { - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - return Ok(()); - } - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - builder.add_i64(8)?; - *stack_depth += 1; - builder.add_op(OpNum2Bin)?; - *stack_depth -= 1; - Ok(()) - } - _ => { - if expr_is_bytes(&args[0], env, types) { - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - Ok(()) - } else { - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - builder.add_i64(8)?; - *stack_depth += 1; - builder.add_op(OpNum2Bin)?; - *stack_depth -= 1; - Ok(()) - } - } + NullaryOp::ThisScriptSize => { + let size = script_size + .ok_or_else(|| CompilerError::Unsupported("this.scriptSize is only available at compile time".to_string()))?; + builder.add_i64(size)?; + } + NullaryOp::ThisScriptSizeDataPrefix => { + let size = script_size.ok_or_else(|| { + CompilerError::Unsupported("this.scriptSizeDataPrefix is only available at compile time".to_string()) + })?; + let size: usize = size.try_into().map_err(|_| { + CompilerError::Unsupported("this.scriptSizeDataPrefix requires a non-negative script size".to_string()) + })?; + let prefix = data_prefix(size); + builder.add_data(&prefix)?; + } + NullaryOp::TxInputsLength => { + builder.add_op(OpTxInputCount)?; + } + NullaryOp::TxOutputsLength => { + builder.add_op(OpTxOutputCount)?; + } + NullaryOp::TxVersion => { + builder.add_op(OpTxVersion)?; + } + NullaryOp::TxLockTime => { + builder.add_op(OpTxLockTime)?; } } - "length" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("length() expects a single argument".to_string())); + *stack_depth += 1; + Ok(()) + } + ExprKind::Introspection { kind, index, .. } => { + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + match kind { + IntrospectionKind::InputValue => { + builder.add_op(OpTxInputAmount)?; } - if let Expr::Identifier(name) = &args[0] { - if let Some(type_name) = types.get(name) { - // Check if this is a fixed-size array type[N] (supporting constants) - if let Some(array_size) = array_size_with_constants(type_name, contract_constants) { - // Compile-time length for fixed-size arrays - builder.add_i64(array_size as i64)?; - *stack_depth += 1; - return Ok(()); - } - // Runtime length for dynamic arrays - if let Some(element_size) = array_element_size(type_name) { - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - builder.add_op(OpSize)?; - builder.add_op(OpSwap)?; - builder.add_op(OpDrop)?; - builder.add_i64(element_size)?; - *stack_depth += 1; - builder.add_op(OpDiv)?; - *stack_depth -= 1; - return Ok(()); - } - } + IntrospectionKind::InputScriptPubKey => { + builder.add_op(OpTxInputSpk)?; } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + IntrospectionKind::InputSigScript => { + builder.add_op(OpDup)?; + builder.add_op(OpTxInputScriptSigLen)?; + builder.add_i64(0)?; + builder.add_op(OpSwap)?; + builder.add_op(OpTxInputScriptSigSubstr)?; + } + IntrospectionKind::InputOutpointTransactionHash => { + builder.add_op(OpOutpointTxId)?; + } + IntrospectionKind::InputOutpointIndex => { + builder.add_op(OpOutpointIndex)?; + } + IntrospectionKind::InputSequenceNumber => { + builder.add_op(OpTxInputSeq)?; + } + IntrospectionKind::OutputValue => { + builder.add_op(OpTxOutputAmount)?; + } + IntrospectionKind::OutputScriptPubKey => { + builder.add_op(OpTxOutputSpk)?; + } + } + Ok(()) + } + ExprKind::DateLiteral(value) => { + builder.add_i64(*value)?; + *stack_depth += 1; + Ok(()) + } + ExprKind::NumberWithUnit { .. } => { + Err(CompilerError::Unsupported("number units must be normalized during parsing".to_string())) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn compile_split_part<'i>( + source: &Expr<'i>, + index: &Expr<'i>, + part: SplitPart, + env: &HashMap>, + params: &HashMap, + types: &HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + visiting: &mut HashSet, + stack_depth: &mut i64, + script_size: Option, + contract_constants: &HashMap>, +) -> Result<(), CompilerError> { + compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + match part { + SplitPart::Left => { + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_i64(0)?; + *stack_depth += 1; + builder.add_op(OpSwap)?; + builder.add_op(OpSubstr)?; + *stack_depth -= 2; + Ok(()) + } + SplitPart::Right => { + builder.add_op(OpSize)?; + *stack_depth += 1; + compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_op(OpSwap)?; + builder.add_op(OpSubstr)?; + *stack_depth -= 2; + Ok(()) + } + } +} + +fn expr_is_bytes<'i>(expr: &Expr<'i>, env: &HashMap>, types: &HashMap) -> bool { + let mut visiting = HashSet::new(); + expr_is_bytes_inner(expr, env, types, &mut visiting) +} + +fn expr_is_bytes_inner<'i>( + expr: &Expr<'i>, + env: &HashMap>, + types: &HashMap, + visiting: &mut HashSet, +) -> bool { + match &expr.kind { + ExprKind::Byte(_) => true, + ExprKind::String(_) => true, + ExprKind::Array(values) => values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))), + ExprKind::Slice { .. } => true, + ExprKind::New { name, .. } => matches!( + name.as_str(), + "LockingBytecodeNullData" | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" + ), + ExprKind::Call { name, .. } => { + let name = name.as_str(); + matches!( + name, + "bytes" + | "blake2b" + | "sha256" + | "OpSha256" + | "OpTxSubnetId" + | "OpTxPayloadSubstr" + | "OpOutpointTxId" + | "OpTxInputScriptSigSubstr" + | "OpTxInputSeq" + | "OpTxInputSpkSubstr" + | "OpTxOutputSpkSubstr" + | "OpInputCovenantId" + | "OpNum2Bin" + | "OpChainblockSeqCommit" + ) || name.starts_with("byte[") + } + ExprKind::Split { .. } => true, + ExprKind::Binary { op: BinaryOp::Add, left, right } => { + expr_is_bytes_inner(left, env, types, visiting) || expr_is_bytes_inner(right, env, types, visiting) + } + ExprKind::IfElse { condition: _, then_expr, else_expr } => { + expr_is_bytes_inner(then_expr, env, types, visiting) && expr_is_bytes_inner(else_expr, env, types, visiting) + } + ExprKind::Introspection { kind, .. } => matches!( + kind, + IntrospectionKind::InputScriptPubKey + | IntrospectionKind::InputSigScript + | IntrospectionKind::InputOutpointTransactionHash + | IntrospectionKind::OutputScriptPubKey + ), + ExprKind::Nullary(NullaryOp::ActiveScriptPubKey) => true, + ExprKind::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, + ExprKind::ArrayIndex { source, .. } => match &source.kind { + ExprKind::Identifier(name) => { + types.get(name).and_then(|type_name| array_element_type(type_name)).map(|element| element != "int").unwrap_or(false) + } + _ => false, + }, + ExprKind::Identifier(name) => { + if !visiting.insert(name.clone()) { + return false; + } + if let Some(expr) = env.get(name) { + let result = expr_is_bytes_inner(expr, env, types, visiting) + || types.get(name).map(|type_name| is_bytes_type(type_name)).unwrap_or(false); + visiting.remove(name); + return result; + } + visiting.remove(name); + types.get(name).map(|type_name| is_bytes_type(type_name)).unwrap_or(false) + } + ExprKind::UnarySuffix { kind, .. } => matches!(kind, UnarySuffixKind::Reverse), + _ => false, + } +} + +fn compile_length_expr<'i>( + expr: &Expr<'i>, + env: &HashMap>, + params: &HashMap, + types: &HashMap, + builder: &mut ScriptBuilder, + options: CompileOptions, + visiting: &mut HashSet, + stack_depth: &mut i64, + script_size: Option, + contract_constants: &HashMap>, +) -> Result<(), CompilerError> { + if let ExprKind::Identifier(name) = &expr.kind { + if let Some(type_name) = types.get(name) { + if let Some(size) = array_size_with_constants(type_name, contract_constants) { + builder.add_i64(size as i64)?; + *stack_depth += 1; + return Ok(()); + } + if let Some(element_size) = array_element_size(type_name) { + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; builder.add_op(OpSize)?; - Ok(()) + builder.add_op(OpSwap)?; + builder.add_op(OpDrop)?; + builder.add_i64(element_size)?; + *stack_depth += 1; + builder.add_op(OpDiv)?; + *stack_depth -= 1; + return Ok(()); } - "int" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("int() expects a single argument".to_string())); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - Ok(()) + } + } + if let ExprKind::Array(values) = &expr.kind { + builder.add_i64(values.len() as i64)?; + *stack_depth += 1; + return Ok(()); + } + compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; + builder.add_op(OpSize)?; + Ok(()) +} + +fn compile_call_expr<'i>( + name: &str, + args: &[Expr<'i>], + scope: &CompilationScope<'_, 'i>, + builder: &mut ScriptBuilder, + options: CompileOptions, + visiting: &mut HashSet, + stack_depth: &mut i64, + script_size: Option, + contract_constants: &HashMap>, +) -> Result<(), CompilerError> { + match name { + "OpSha256" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpSHA256, + script_size, + contract_constants, + ), + "sha256" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported("sha256() expects a single argument".to_string())); } - "sig" | "pubkey" | "datasig" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - Ok(()) + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpSHA256)?; + Ok(()) + } + "OpTxSubnetId" => compile_opcode_call( + name, + args, + 0, + scope, + builder, + options, + visiting, + stack_depth, + OpTxSubnetId, + script_size, + contract_constants, + ), + "OpTxGas" => compile_opcode_call( + name, + args, + 0, + scope, + builder, + options, + visiting, + stack_depth, + OpTxGas, + script_size, + contract_constants, + ), + "OpTxPayloadLen" => compile_opcode_call( + name, + args, + 0, + scope, + builder, + options, + visiting, + stack_depth, + OpTxPayloadLen, + script_size, + contract_constants, + ), + "OpTxPayloadSubstr" => compile_opcode_call( + name, + args, + 2, + scope, + builder, + options, + visiting, + stack_depth, + OpTxPayloadSubstr, + script_size, + contract_constants, + ), + "OpOutpointTxId" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpOutpointTxId, + script_size, + contract_constants, + ), + "OpOutpointIndex" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpOutpointIndex, + script_size, + contract_constants, + ), + "OpTxInputScriptSigLen" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputScriptSigLen, + script_size, + contract_constants, + ), + "OpTxInputScriptSigSubstr" => compile_opcode_call( + name, + args, + 3, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputScriptSigSubstr, + script_size, + contract_constants, + ), + "OpTxInputSeq" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSeq, + script_size, + contract_constants, + ), + "OpTxInputIsCoinbase" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputIsCoinbase, + script_size, + contract_constants, + ), + "OpTxInputSpkLen" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSpkLen, + script_size, + contract_constants, + ), + "OpTxInputSpkSubstr" => compile_opcode_call( + name, + args, + 3, + scope, + builder, + options, + visiting, + stack_depth, + OpTxInputSpkSubstr, + script_size, + contract_constants, + ), + "OpTxOutputSpkLen" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpTxOutputSpkLen, + script_size, + contract_constants, + ), + "OpTxOutputSpkSubstr" => compile_opcode_call( + name, + args, + 3, + scope, + builder, + options, + visiting, + stack_depth, + OpTxOutputSpkSubstr, + script_size, + contract_constants, + ), + "OpAuthOutputCount" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpAuthOutputCount, + script_size, + contract_constants, + ), + "OpAuthOutputIdx" => compile_opcode_call( + name, + args, + 2, + scope, + builder, + options, + visiting, + stack_depth, + OpAuthOutputIdx, + script_size, + contract_constants, + ), + "OpInputCovenantId" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpInputCovenantId, + script_size, + contract_constants, + ), + "OpCovInputCount" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpCovInputCount, + script_size, + contract_constants, + ), + "OpCovInputIdx" => compile_opcode_call( + name, + args, + 2, + scope, + builder, + options, + visiting, + stack_depth, + OpCovInputIdx, + script_size, + contract_constants, + ), + "OpCovOutCount" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpCovOutCount, + script_size, + contract_constants, + ), + "OpCovOutputIdx" => compile_opcode_call( + name, + args, + 2, + scope, + builder, + options, + visiting, + stack_depth, + OpCovOutputIdx, + script_size, + contract_constants, + ), + "OpNum2Bin" => compile_opcode_call( + name, + args, + 2, + scope, + builder, + options, + visiting, + stack_depth, + OpNum2Bin, + script_size, + contract_constants, + ), + "OpBin2Num" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpBin2Num, + script_size, + contract_constants, + ), + "OpChainblockSeqCommit" => compile_opcode_call( + name, + args, + 1, + scope, + builder, + options, + visiting, + stack_depth, + OpChainblockSeqCommit, + script_size, + contract_constants, + ), + "bytes" => { + if args.is_empty() || args.len() > 2 { + return Err(CompilerError::Unsupported("bytes() expects one or two arguments".to_string())); + } + if args.len() == 2 { + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_expr( + &args[1], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; + return Ok(()); } - name if name.starts_with("byte[") && name.ends_with(']') => { - let size_part = &name[5..name.len() - 1]; - if size_part.is_empty() { - // Handle byte[] cast (dynamic array) - just compile the argument as-is - if args.len() != 1 && args.len() != 2 { - return Err(CompilerError::Unsupported(format!("{name}() expects 1 or 2 arguments"))); + match &args[0].kind { + ExprKind::String(value) => { + builder.add_data(value.as_bytes())?; + *stack_depth += 1; + Ok(()) + } + ExprKind::Identifier(name) => { + if let Some(expr) = scope.env.get(name) { + if let ExprKind::String(value) = &expr.kind { + builder.add_data(value.as_bytes())?; + *stack_depth += 1; + return Ok(()); + } } - compile_expr( - &args[0], - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - if args.len() == 2 { - // byte[](value, size) - OpNum2Bin with size parameter + if expr_is_bytes(&args[0], scope.env, scope.types) { compile_expr( - &args[1], - env, - params, - types, + &args[0], + scope.env, + scope.params, + scope.types, builder, options, visiting, @@ -3013,23 +3621,13 @@ fn compile_expr( script_size, contract_constants, )?; - *stack_depth += 1; - builder.add_op(OpNum2Bin)?; - *stack_depth -= 1; - } - Ok(()) - } else { - // Handle byte[N] cast - extract size from byte[N] - let size = - size_part.parse::().map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; - if args.len() != 1 { - return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + return Ok(()); } compile_expr( &args[0], - env, - params, - types, + scope.env, + scope.params, + scope.types, builder, options, visiting, @@ -3037,273 +3635,75 @@ fn compile_expr( script_size, contract_constants, )?; - builder.add_i64(size)?; + builder.add_i64(8)?; *stack_depth += 1; builder.add_op(OpNum2Bin)?; *stack_depth -= 1; Ok(()) } - } - "blake2b" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("blake2b() expects a single argument".to_string())); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_op(OpBlake2b)?; - Ok(()) - } - "checkSig" => { - if args.len() != 2 { - return Err(CompilerError::Unsupported("checkSig() expects 2 arguments".to_string())); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - compile_expr(&args[1], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_op(OpCheckSig)?; - *stack_depth -= 1; - Ok(()) - } - "checkDataSig" => { - // TODO: Remove this stub - for arg in args { - compile_expr(arg, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - } - for _ in 0..args.len() { - builder.add_op(OpDrop)?; - *stack_depth -= 1; - } - builder.add_op(OpTrue)?; - *stack_depth += 1; - Ok(()) - } - _ => Err(CompilerError::Unsupported(format!("unknown function call: {name}"))), - }, - Expr::New { name, args } => match name.as_str() { - "LockingBytecodeNullData" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("LockingBytecodeNullData expects a single array argument".to_string())); - } - let script = build_null_data_script(&args[0])?; - builder.add_data(&script)?; - *stack_depth += 1; - Ok(()) - } - "ScriptPubKeyP2PK" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("ScriptPubKeyP2PK expects a single pubkey argument".to_string())); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_data(&[0x00, 0x00, OpData32])?; - *stack_depth += 1; - builder.add_op(OpSwap)?; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_data(&[OpCheckSig])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - Ok(()) - } - "ScriptPubKeyP2SH" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported("ScriptPubKeyP2SH expects a single bytes32 argument".to_string())); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_data(&[0x00, 0x00])?; - *stack_depth += 1; - builder.add_data(&[OpBlake2b])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_data(&[0x20])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_op(OpSwap)?; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_data(&[OpEqual])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - Ok(()) - } - "ScriptPubKeyP2SHFromRedeemScript" => { - if args.len() != 1 { - return Err(CompilerError::Unsupported( - "ScriptPubKeyP2SHFromRedeemScript expects a single redeem_script argument".to_string(), - )); - } - compile_expr(&args[0], env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_op(OpBlake2b)?; - builder.add_data(&[0x00, 0x00])?; - *stack_depth += 1; - builder.add_data(&[OpBlake2b])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_data(&[0x20])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_op(OpSwap)?; - builder.add_op(OpCat)?; - *stack_depth -= 1; - builder.add_data(&[OpEqual])?; - *stack_depth += 1; - builder.add_op(OpCat)?; - *stack_depth -= 1; - Ok(()) - } - _ => Err(CompilerError::Unsupported(format!("unknown constructor: {name}"))), - }, - Expr::Unary { op, expr } => { - compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - match op { - UnaryOp::Not => builder.add_op(OpNot)?, - UnaryOp::Neg => builder.add_op(OpNegate)?, - }; - Ok(()) - } - Expr::Binary { op, left, right } => { - let bytes_eq = - matches!(op, BinaryOp::Eq | BinaryOp::Ne) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); - let bytes_add = matches!(op, BinaryOp::Add) && (expr_is_bytes(left, env, types) || expr_is_bytes(right, env, types)); - if bytes_add { - compile_concat_operand( - left, - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - compile_concat_operand( - right, - env, - params, - types, - builder, - options, - visiting, - stack_depth, - script_size, - contract_constants, - )?; - } else { - compile_expr(left, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - compile_expr(right, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - } - match op { - BinaryOp::Or => { - builder.add_op(OpBoolOr)?; - } - BinaryOp::And => { - builder.add_op(OpBoolAnd)?; - } - BinaryOp::BitOr => { - builder.add_op(OpOr)?; - } - BinaryOp::BitXor => { - builder.add_op(OpXor)?; - } - BinaryOp::BitAnd => { - builder.add_op(OpAnd)?; - } - BinaryOp::Eq => { - builder.add_op(if bytes_eq { OpEqual } else { OpNumEqual })?; - } - BinaryOp::Ne => { - if bytes_eq { - builder.add_op(OpEqual)?; - builder.add_op(OpNot)?; - } else { - builder.add_op(OpNumNotEqual)?; - } - } - BinaryOp::Lt => { - builder.add_op(OpLessThan)?; - } - BinaryOp::Le => { - builder.add_op(OpLessThanOrEqual)?; - } - BinaryOp::Gt => { - builder.add_op(OpGreaterThan)?; - } - BinaryOp::Ge => { - builder.add_op(OpGreaterThanOrEqual)?; - } - BinaryOp::Add => { - if bytes_add { - builder.add_op(OpCat)?; + _ => { + if expr_is_bytes(&args[0], scope.env, scope.types) { + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + Ok(()) } else { - builder.add_op(OpAdd)?; - } - } - BinaryOp::Sub => { - builder.add_op(OpSub)?; - } - BinaryOp::Mul => { - builder.add_op(OpMul)?; - } - BinaryOp::Div => { - builder.add_op(OpDiv)?; - } - BinaryOp::Mod => { - builder.add_op(OpMod)?; - } - } - *stack_depth -= 1; - Ok(()) - } - Expr::Split { source, index, part } => { - compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - match part { - SplitPart::Left => { - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_i64(0)?; - *stack_depth += 1; - builder.add_op(OpSwap)?; - builder.add_op(OpSubstr)?; - *stack_depth -= 2; - } - SplitPart::Right => { - builder.add_op(OpSize)?; - *stack_depth += 1; - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_op(OpSwap)?; - builder.add_op(OpSubstr)?; - *stack_depth -= 2; + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_i64(8)?; + *stack_depth += 1; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; + Ok(()) + } } } - Ok(()) } - Expr::ArrayIndex { source, index } => { - let resolved_source = match source.as_ref() { - Expr::Identifier(_) => source.as_ref().clone(), - _ => resolve_expr(*source.clone(), env, visiting)?, - }; - let element_type = match &resolved_source { - Expr::Identifier(name) => { - let type_name = types.get(name).or_else(|| { - env.get(name).and_then(|value| if let Expr::Identifier(inner) = value { types.get(inner) } else { None }) - }); - type_name - .and_then(|t| array_element_type(t)) - .ok_or_else(|| CompilerError::Unsupported(format!("array index requires array identifier: {name}")))? - } - _ => return Err(CompilerError::Unsupported("array index requires array identifier".to_string())), - }; - let element_size = fixed_type_size(&element_type) - .ok_or_else(|| CompilerError::Unsupported("array element type must have known size".to_string()))?; + "length" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported("length() expects a single argument".to_string())); + } + compile_length_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + ) + } + "int" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported("int() expects a single argument".to_string())); + } compile_expr( - &resolved_source, - env, - params, - types, + &args[0], + scope.env, + scope.params, + scope.types, builder, options, visiting, @@ -3311,199 +3711,181 @@ fn compile_expr( script_size, contract_constants, )?; - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - builder.add_i64(element_size)?; - *stack_depth += 1; - builder.add_op(OpMul)?; - *stack_depth -= 1; - builder.add_op(OpDup)?; - *stack_depth += 1; - builder.add_i64(element_size)?; - *stack_depth += 1; - builder.add_op(OpAdd)?; - *stack_depth -= 1; - builder.add_op(OpSubstr)?; - *stack_depth -= 2; - if element_type == "int" { - builder.add_op(OpBin2Num)?; - } Ok(()) } - Expr::Slice { source, start, end } => { - compile_expr(source, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - compile_expr(start, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - compile_expr(end, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - - builder.add_op(Op2Dup)?; - *stack_depth += 2; - builder.add_op(OpSwap)?; - builder.add_op(OpSub)?; - *stack_depth -= 1; - builder.add_op(OpSwap)?; - builder.add_op(OpDrop)?; - *stack_depth -= 1; - builder.add_op(OpSubstr)?; - *stack_depth -= 2; + "sig" | "pubkey" | "datasig" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); + } + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; Ok(()) } - Expr::Nullary(op) => { - match op { - NullaryOp::ActiveInputIndex => { - builder.add_op(OpTxInputIndex)?; - } - NullaryOp::ActiveScriptPubKey => { - builder.add_op(OpTxInputIndex)?; - builder.add_op(OpTxInputSpk)?; - } - NullaryOp::ThisScriptSize => { - let size = script_size - .ok_or_else(|| CompilerError::Unsupported("this.scriptSize is only available at compile time".to_string()))?; - builder.add_i64(size)?; - } - NullaryOp::ThisScriptSizeDataPrefix => { - let size = script_size.ok_or_else(|| { - CompilerError::Unsupported("this.scriptSizeDataPrefix is only available at compile time".to_string()) - })?; - let size: usize = size.try_into().map_err(|_| { - CompilerError::Unsupported("this.scriptSizeDataPrefix requires a non-negative script size".to_string()) - })?; - let prefix = data_prefix(size); - builder.add_data(&prefix)?; - } - NullaryOp::TxInputsLength => { - builder.add_op(OpTxInputCount)?; - } - NullaryOp::TxOutputsLength => { - builder.add_op(OpTxOutputCount)?; - } - NullaryOp::TxVersion => { - builder.add_op(OpTxVersion)?; + name if name.starts_with("byte[") && name.ends_with(']') => { + let size_part = &name[5..name.len() - 1]; + if size_part.is_empty() { + // Handle byte[] cast (dynamic array) - just compile the argument as-is + if args.len() != 1 && args.len() != 2 { + return Err(CompilerError::Unsupported(format!("{name}() expects 1 or 2 arguments"))); + } + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + if args.len() == 2 { + // byte[](value, size) - OpNum2Bin with size parameter + compile_expr( + &args[1], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + *stack_depth += 1; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; } - NullaryOp::TxLockTime => { - builder.add_op(OpTxLockTime)?; + Ok(()) + } else { + // Handle byte[N] cast - extract size from byte[N] + let size = size_part.parse::().map_err(|_| CompilerError::Unsupported(format!("{name}() is not supported")))?; + if args.len() != 1 { + return Err(CompilerError::Unsupported(format!("{name}() expects a single argument"))); } + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_i64(size)?; + *stack_depth += 1; + builder.add_op(OpNum2Bin)?; + *stack_depth -= 1; + Ok(()) } - *stack_depth += 1; - Ok(()) } - Expr::Introspection { kind, index } => { - compile_expr(index, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; - match kind { - IntrospectionKind::InputValue => { - builder.add_op(OpTxInputAmount)?; - } - IntrospectionKind::InputScriptPubKey => { - builder.add_op(OpTxInputSpk)?; - } - IntrospectionKind::InputSigScript => { - builder.add_op(OpDup)?; - builder.add_op(OpTxInputScriptSigLen)?; - builder.add_i64(0)?; - builder.add_op(OpSwap)?; - builder.add_op(OpTxInputScriptSigSubstr)?; - } - IntrospectionKind::OutputValue => { - builder.add_op(OpTxOutputAmount)?; - } - IntrospectionKind::OutputScriptPubKey => { - builder.add_op(OpTxOutputSpk)?; - } + "blake2b" => { + if args.len() != 1 { + return Err(CompilerError::Unsupported("blake2b() expects a single argument".to_string())); } + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpBlake2b)?; Ok(()) } - } -} - -fn expr_is_bytes(expr: &Expr, env: &HashMap, types: &HashMap) -> bool { - let mut visiting = HashSet::new(); - expr_is_bytes_inner(expr, env, types, &mut visiting) -} - -fn expr_is_bytes_inner( - expr: &Expr, - env: &HashMap, - types: &HashMap, - visiting: &mut HashSet, -) -> bool { - match expr { - Expr::Byte(_) => true, - Expr::Array(values) => is_byte_array(&Expr::Array(values.clone())), - Expr::StateObject(_) => false, - Expr::String(_) => true, - Expr::Slice { .. } => true, - Expr::New { name, .. } => matches!( - name.as_str(), - "LockingBytecodeNullData" | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" - ), - Expr::Call { name, .. } => { - matches!( - name.as_str(), - "blake2b" - | "sha256" - | "OpSha256" - | "OpTxSubnetId" - | "OpTxPayloadSubstr" - | "OpOutpointTxId" - | "OpTxInputScriptSigSubstr" - | "OpTxInputSeq" - | "OpTxInputSpkSubstr" - | "OpTxOutputSpkSubstr" - | "OpInputCovenantId" - | "OpNum2Bin" - | "OpChainblockSeqCommit" - ) || name.starts_with("byte[") - } - Expr::Split { .. } => true, - Expr::Binary { op: BinaryOp::Add, left, right } => { - expr_is_bytes_inner(left, env, types, visiting) || expr_is_bytes_inner(right, env, types, visiting) - } - Expr::IfElse { condition: _, then_expr, else_expr } => { - expr_is_bytes_inner(then_expr, env, types, visiting) && expr_is_bytes_inner(else_expr, env, types, visiting) - } - Expr::Introspection { kind, .. } => { - matches!( - kind, - IntrospectionKind::InputScriptPubKey | IntrospectionKind::InputSigScript | IntrospectionKind::OutputScriptPubKey - ) - } - Expr::Nullary(NullaryOp::ActiveScriptPubKey) => true, - Expr::Nullary(NullaryOp::ThisScriptSizeDataPrefix) => true, - Expr::ArrayIndex { source, .. } => match source.as_ref() { - Expr::Identifier(name) => { - types.get(name).and_then(|type_name| array_element_type(type_name)).map(|element| element != "int").unwrap_or(false) + "checkSig" => { + if args.len() != 2 { + return Err(CompilerError::Unsupported("checkSig() expects 2 arguments".to_string())); } - _ => false, - }, - Expr::Identifier(name) => { - if !visiting.insert(name.clone()) { - return false; + compile_expr( + &args[0], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + compile_expr( + &args[1], + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; + builder.add_op(OpCheckSig)?; + *stack_depth -= 1; + Ok(()) + } + "checkDataSig" => { + // TODO: Remove this stub + for arg in args { + compile_expr( + arg, + scope.env, + scope.params, + scope.types, + builder, + options, + visiting, + stack_depth, + script_size, + contract_constants, + )?; } - if let Some(expr) = env.get(name) { - let result = expr_is_bytes_inner(expr, env, types, visiting) - || types.get(name).map(|type_name| is_bytes_type(type_name)).unwrap_or(false); - visiting.remove(name); - return result; + for _ in 0..args.len() { + builder.add_op(OpDrop)?; + *stack_depth -= 1; } - visiting.remove(name); - types.get(name).map(|type_name| is_bytes_type(type_name)).unwrap_or(false) + builder.add_op(OpTrue)?; + *stack_depth += 1; + Ok(()) } - _ => false, + _ => Err(CompilerError::Unsupported(format!("unknown function call: {name}"))), } } #[allow(clippy::too_many_arguments)] -fn compile_opcode_call( +fn compile_opcode_call<'i>( name: &str, - args: &[Expr], + args: &[Expr<'i>], expected_args: usize, - scope: &CompilationScope, + scope: &CompilationScope<'_, 'i>, builder: &mut ScriptBuilder, options: CompileOptions, visiting: &mut HashSet, stack_depth: &mut i64, opcode: u8, script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { if args.len() != expected_args { return Err(CompilerError::Unsupported(format!("{name}() expects {expected_args} argument(s)"))); @@ -3527,9 +3909,9 @@ fn compile_opcode_call( Ok(()) } -fn compile_concat_operand( - expr: &Expr, - env: &HashMap, +fn compile_concat_operand<'i>( + expr: &Expr<'i>, + env: &HashMap>, params: &HashMap, types: &HashMap, builder: &mut ScriptBuilder, @@ -3537,7 +3919,7 @@ fn compile_concat_operand( visiting: &mut HashSet, stack_depth: &mut i64, script_size: Option, - contract_constants: &HashMap, + contract_constants: &HashMap>, ) -> Result<(), CompilerError> { compile_expr(expr, env, params, types, builder, options, visiting, stack_depth, script_size, contract_constants)?; if !expr_is_bytes(expr, env, types) { @@ -3562,35 +3944,40 @@ fn is_bytes_type(type_name: &str) -> bool { is_array_type(type_name) } -fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { - let elements = match arg { - Expr::Array(items) => items, +fn build_null_data_script<'i>(arg: &Expr<'i>) -> Result, CompilerError> { + let elements = match &arg.kind { + ExprKind::Array(items) => items, _ => return Err(CompilerError::Unsupported("LockingBytecodeNullData expects an array literal".to_string())), }; let mut builder = ScriptBuilder::new(); builder.add_op(OpReturn)?; for item in elements { - match item { - Expr::Int(value) => { + match &item.kind { + ExprKind::Int(value) => { + builder.add_i64(*value)?; + } + ExprKind::DateLiteral(value) => { builder.add_i64(*value)?; } - Expr::Array(values) if is_byte_array(&Expr::Array(values.clone())) => { - // Handle byte arrays - let bytes: Vec = values.iter().filter_map(|v| if let Expr::Byte(b) = v { Some(*b) } else { None }).collect(); + ExprKind::Array(values) if values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))) => { + let bytes: Vec = values + .iter() + .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) + .collect(); builder.add_data(&bytes)?; } - Expr::String(value) => { + ExprKind::String(value) => { builder.add_data(value.as_bytes())?; } - Expr::Call { name, args } if name == "byte[]" => { + ExprKind::Call { name, args, .. } if name == "bytes" || name == "byte[]" => { if args.len() != 1 { return Err(CompilerError::Unsupported( "byte[]() in LockingBytecodeNullData expects a single argument".to_string(), )); } - match &args[0] { - Expr::String(value) => { + match &args[0].kind { + ExprKind::String(value) => { builder.add_data(value.as_bytes())?; } _ => { @@ -3600,7 +3987,9 @@ fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { } } } - _ => return Err(CompilerError::Unsupported("LockingBytecodeNullData only supports int or bytes literals".to_string())), + _ => { + return Err(CompilerError::Unsupported("LockingBytecodeNullData only supports int or bytes literals".to_string())); + } } } @@ -3611,14 +4000,6 @@ fn build_null_data_script(arg: &Expr) -> Result, CompilerError> { Ok(spk_bytes) } -fn parse_date_value(value: &str) -> Result { - let timestamp = NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S") - .map_err(|_| CompilerError::InvalidLiteral("invalid date literal".to_string()))? - .and_utc() - .timestamp(); - Ok(timestamp) -} - fn data_prefix(data_len: usize) -> Vec { let dummy_data = vec![0u8; data_len]; let mut builder = ScriptBuilder::new(); diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index badda453..ff84dd62 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -10,11 +10,11 @@ use crate::debug::{ use super::{CompilerError, resolve_expr_for_debug}; -type ResolvedVariableUpdate = (String, String, Expr); +type ResolvedVariableUpdate<'i> = (String, String, Expr<'i>); -pub(super) fn record_synthetic_range( +pub(super) fn record_synthetic_range<'i>( builder: &mut ScriptBuilder, - recorder: &mut DebugSink, + recorder: &mut DebugSink<'i>, label: &'static str, f: impl FnOnce(&mut ScriptBuilder) -> Result<(), CompilerError>, ) -> Result<(), CompilerError> { @@ -28,11 +28,11 @@ pub(super) fn record_synthetic_range( /// Per-function debug recorder active during function compilation. /// Records params, statements, and variable updates for a single function. #[derive(Debug, Default)] -pub struct FunctionDebugRecorder { +pub struct FunctionDebugRecorder<'i> { function_name: String, enabled: bool, events: Vec, - variable_updates: Vec, + variable_updates: Vec>, param_mappings: Vec, next_seq: u32, call_depth: u32, @@ -40,8 +40,8 @@ pub struct FunctionDebugRecorder { next_frame_id: u32, } -impl FunctionDebugRecorder { - pub fn new(enabled: bool, function: &FunctionAst, contract_fields: &[ContractFieldAst]) -> Self { +impl<'i> FunctionDebugRecorder<'i> { + pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { let mut recorder = Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; recorder.record_stack_bindings(function, contract_fields); @@ -101,7 +101,7 @@ impl FunctionDebugRecorder { Some(sequence) } - fn record_stack_bindings(&mut self, function: &FunctionAst, contract_fields: &[ContractFieldAst]) { + fn record_stack_bindings(&mut self, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { if !self.enabled { return; } @@ -129,20 +129,21 @@ impl FunctionDebugRecorder { } } - fn record_statement_span(&mut self, span: Option, bytecode_start: usize, bytecode_len: usize) -> Option { + fn record_statement_span(&mut self, span: SourceSpan, bytecode_start: usize, bytecode_len: usize) -> Option { let kind = if bytecode_len == 0 { DebugEventKind::Virtual {} } else { DebugEventKind::Statement {} }; - self.push_event(bytecode_start, bytecode_start + bytecode_len, span, kind) + self.push_event(bytecode_start, bytecode_start + bytecode_len, Some(span), kind) } fn record_statement_updates( &mut self, - stmt: &Statement, + stmt: &Statement<'i>, bytecode_start: usize, bytecode_end: usize, - variables: Vec, + variables: Vec>, ) { - if let Some(sequence) = self.record_statement_span(stmt.span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { - self.record_variable_updates(variables, bytecode_end, stmt.span, sequence); + let span = SourceSpan::from(stmt.span()); + if let Some(sequence) = self.record_statement_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { + self.record_variable_updates(variables, bytecode_end, Some(span), sequence); } } @@ -152,11 +153,11 @@ impl FunctionDebugRecorder { /// evaluation can compute values from the current state. pub fn record_statement_with_env_diff( &mut self, - stmt: &Statement, + stmt: &Statement<'i>, bytecode_start: usize, bytecode_end: usize, - before_env: Option<&HashMap>, - after_env: &HashMap, + before_env: Option<&HashMap>>, + after_env: &HashMap>, types: &HashMap, ) -> Result<(), CompilerError> { let updates = self.collect_variable_updates(before_env, after_env, types)?; @@ -169,8 +170,8 @@ impl FunctionDebugRecorder { /// without adding an extra source step at the call-site. pub fn record_inline_param_updates( &mut self, - function: &FunctionAst, - env: &HashMap, + function: &FunctionAst<'i>, + env: &HashMap>, span: Option, bytecode_offset: usize, ) -> Result<(), CompilerError> { @@ -180,7 +181,6 @@ impl FunctionDebugRecorder { // Anchor inline param updates to the next callee statement sequence. // We intentionally "peek" (do not consume) so these updates align with // the first real callee statement event sequence. - // without creating an extra steppable mapping at the call-site span. let sequence = self.next_seq; let mut variables = Vec::new(); for param in &function.params { @@ -189,7 +189,7 @@ impl FunctionDebugRecorder { &mut variables, ¶m.name, ¶m.type_ref.type_name(), - env.get(¶m.name).cloned().unwrap_or_else(|| Expr::Identifier(param.name.clone())), + env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), )?; } self.record_variable_updates(variables, bytecode_offset, span, sequence); @@ -209,13 +209,13 @@ impl FunctionDebugRecorder { span: Option, bytecode_offset: usize, callee: &str, - inline: &FunctionDebugRecorder, + inline: &FunctionDebugRecorder<'i>, ) { self.merge_inline_events(inline); self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallExit { callee: callee.to_string() }); } - fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder) { + fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder<'i>) { if !self.enabled || inline.events.is_empty() { // Keep frame-id frontier monotonic even if the inline call recorded // no events; this preserves uniqueness for later sibling calls. @@ -249,7 +249,7 @@ impl FunctionDebugRecorder { fn record_variable_updates( &mut self, - variables: Vec, + variables: Vec>, bytecode_offset: usize, span: Option, sequence: u32, @@ -273,10 +273,10 @@ impl FunctionDebugRecorder { fn collect_variable_updates( &self, - before_env: Option<&HashMap>, - after_env: &HashMap, + before_env: Option<&HashMap>>, + after_env: &HashMap>, types: &HashMap, - ) -> Result, CompilerError> { + ) -> Result>, CompilerError> { if !self.enabled { return Ok(Vec::new()); } @@ -311,14 +311,13 @@ impl FunctionDebugRecorder { /// Records a variable update by resolving its expression against the current environment. /// This expands locals and synthetic inline placeholders (`__arg_*`) into /// caller-visible expressions, leaving only real param identifiers. - /// The resolved expression is what enables shadow VM evaluation at debug time. fn variable_update( &self, - env: &HashMap, - variables: &mut Vec, + env: &HashMap>, + variables: &mut Vec>, name: &str, type_name: &str, - expr: Expr, + expr: Expr<'i>, ) -> Result<(), CompilerError> { if !self.enabled { return Ok(()); @@ -331,24 +330,24 @@ impl FunctionDebugRecorder { /// Global debug recording sink that can be enabled or disabled. /// When Off, all recording calls become no-ops with zero overhead. -pub enum DebugSink { +pub enum DebugSink<'i> { Off, - On(DebugRecorder), + On(DebugRecorder<'i>), } -impl DebugSink { +impl<'i> DebugSink<'i> { pub fn new(enabled: bool) -> Self { if enabled { Self::On(DebugRecorder::default()) } else { Self::Off } } - fn recorder_mut(&mut self) -> Option<&mut DebugRecorder> { + fn recorder_mut(&mut self) -> Option<&mut DebugRecorder<'i>> { match self { Self::Off => None, Self::On(rec) => Some(rec), } } - pub fn record_constructor_constants(&mut self, params: &[ParamAst], values: &[Expr]) { + pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { let Some(rec) = self.recorder_mut() else { return; }; @@ -380,7 +379,13 @@ impl DebugSink { }); } - pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionDebugRecorder, offset: usize) { + pub fn record_compiled_function( + &mut self, + name: &str, + script_len: usize, + debug: &FunctionDebugRecorder<'i>, + offset: usize, + ) { let Some(rec) = self.recorder_mut() else { return; }; @@ -391,7 +396,7 @@ impl DebugSink { record_param_mappings(&debug.param_mappings, rec); } - pub fn into_debug_info(self, source: String) -> Option { + pub fn into_debug_info(self, source: String) -> Option> { match self { Self::Off => None, Self::On(rec) => Some(rec.into_debug_info(source)), @@ -399,7 +404,7 @@ impl DebugSink { } } -fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, recorder: &mut DebugRecorder) { +fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'_>) { for event in events { recorder.record(DebugEvent { bytecode_start: event.bytecode_start + offset, @@ -413,7 +418,12 @@ fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, } } -fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: usize, seq_base: u32, recorder: &mut DebugRecorder) { +fn emit_variable_updates_with_offset<'i>( + updates: &[DebugVariableUpdate<'i>], + offset: usize, + seq_base: u32, + recorder: &mut DebugRecorder<'i>, +) { for update in updates { recorder.record_variable_update(DebugVariableUpdate { name: update.name.clone(), @@ -428,7 +438,7 @@ fn emit_variable_updates_with_offset(updates: &[DebugVariableUpdate], offset: us } } -fn record_param_mappings(params: &[DebugParamMapping], recorder: &mut DebugRecorder) { +fn record_param_mappings(params: &[DebugParamMapping], recorder: &mut DebugRecorder<'_>) { for param in params { recorder.record_param(param.clone()); } diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index a30972f0..020ed760 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -1,6 +1,6 @@ use crate::ast::Expr; -use pest::Span; use serde::{Deserialize, Serialize}; +use crate::span; pub mod presentation; pub mod session; @@ -13,8 +13,8 @@ pub struct SourceSpan { pub end_col: u32, } -impl<'a> From> for SourceSpan { - fn from(span: Span<'a>) -> Self { +impl<'a> From> for SourceSpan { + fn from(span: span::Span<'a>) -> Self { let (line, col) = span.start_pos().line_col(); let (end_line, end_col) = span.end_pos().line_col(); Self { line: line as u32, col: col as u32, end_line: end_line as u32, end_col: end_col as u32 } @@ -63,21 +63,21 @@ pub struct DebugEvent { /// Collects events, variable updates, param mappings, function ranges, and constants. /// Converted to `DebugInfo` after compilation completes. #[derive(Debug, Default)] -pub struct DebugRecorder { +pub struct DebugRecorder<'i> { events: Vec, - variable_updates: Vec, + variable_updates: Vec>, params: Vec, functions: Vec, - constants: Vec, + constants: Vec>, next_sequence: u32, } -impl DebugRecorder { +impl<'i> DebugRecorder<'i> { pub fn record(&mut self, event: DebugEvent) { self.events.push(event); } - pub fn record_variable_update(&mut self, update: DebugVariableUpdate) { + pub fn record_variable_update(&mut self, update: DebugVariableUpdate<'i>) { self.variable_updates.push(update); } @@ -89,7 +89,7 @@ impl DebugRecorder { self.functions.push(function); } - pub fn record_constant(&mut self, constant: DebugConstantMapping) { + pub fn record_constant(&mut self, constant: DebugConstantMapping<'i>) { self.constants.push(constant); } @@ -113,7 +113,7 @@ impl DebugRecorder { self.events } - pub fn into_debug_info(self, source: String) -> DebugInfo { + pub fn into_debug_info(self, source: String) -> DebugInfo<'i> { DebugInfo { source, mappings: self.events.into_iter().map(DebugMapping::from).collect(), @@ -128,16 +128,16 @@ impl DebugRecorder { /// Complete debug metadata attached to compiled contract. /// Contains everything needed to map bytecode execution back to source and evaluate variables. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DebugInfo { +pub struct DebugInfo<'i> { pub source: String, pub mappings: Vec, - pub variable_updates: Vec, + pub variable_updates: Vec>, pub params: Vec, pub functions: Vec, - pub constants: Vec, + pub constants: Vec>, } -impl DebugInfo { +impl<'i> DebugInfo<'i> { pub fn empty() -> Self { Self { source: String::new(), @@ -151,12 +151,12 @@ impl DebugInfo { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DebugVariableUpdate { +pub struct DebugVariableUpdate<'i> { pub name: String, pub type_name: String, /// Pre-resolved expression with all local variable references expanded inline. /// Only function parameter Identifiers remain. Enables shadow VM evaluation. - pub expr: Expr, + pub expr: Expr<'i>, pub bytecode_offset: usize, pub span: Option, pub function: String, @@ -190,10 +190,10 @@ pub struct DebugFunctionRange { /// Constructor constant (contract instantiation parameter). /// Recorded for display in debugger variable list. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DebugConstantMapping { +pub struct DebugConstantMapping<'i> { pub name: String, pub type_name: String, - pub value: Expr, + pub value: Expr<'i>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 5365dec0..780613b1 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -7,7 +7,7 @@ use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; use serde::{Deserialize, Serialize}; -use crate::ast::Expr; +use crate::ast::{Expr, ExprKind}; use crate::compiler::compile_debug_expr; use crate::debug::presentation::{build_source_context, format_value as format_debug_value}; use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan}; @@ -78,14 +78,14 @@ pub struct OpcodeMeta { pub mapping: Option, } -pub struct DebugSession<'a> { +pub struct DebugSession<'a, 'i> { engine: DebugEngine<'a>, opcodes: Vec>>, op_displays: Vec, opcode_offsets: Vec, script_len: usize, pc: usize, - debug_info: DebugInfo, + debug_info: DebugInfo<'i>, source_mappings: Vec, current_step_index: Option, source_lines: Vec, @@ -108,7 +108,7 @@ struct VariableContext<'a> { frame_id: u32, } -impl<'a> DebugSession<'a> { +impl<'a, 'i> DebugSession<'a, 'i> { // --- Session construction + stepping --- /// Creates a debug session for lockscript-only execution. @@ -116,7 +116,7 @@ impl<'a> DebugSession<'a> { pub fn lockscript_only( script: &[u8], source: &str, - debug_info: Option, + debug_info: Option>, engine: DebugEngine<'a>, ) -> Result { Self::from_scripts(script, source, debug_info, engine) @@ -128,7 +128,7 @@ impl<'a> DebugSession<'a> { sigscript: &[u8], lockscript: &[u8], source: &str, - debug_info: Option, + debug_info: Option>, mut engine: DebugEngine<'a>, ) -> Result { seed_engine_with_sigscript(&mut engine, sigscript)?; @@ -139,7 +139,7 @@ impl<'a> DebugSession<'a> { pub fn from_scripts( script: &[u8], source: &str, - debug_info: Option, + debug_info: Option>, engine: DebugEngine<'a>, ) -> Result { let debug_info = debug_info.unwrap_or_else(DebugInfo::empty); @@ -364,7 +364,7 @@ impl<'a> DebugSession<'a> { self.op_displays.len() } - pub fn debug_info(&self) -> &DebugInfo { + pub fn debug_info(&self) -> &DebugInfo<'i> { &self.debug_info } @@ -486,8 +486,8 @@ impl<'a> DebugSession<'a> { offset: usize, sequence: u32, frame_id: u32, - ) -> HashMap { - let mut latest: HashMap = HashMap::new(); + ) -> HashMap> { + let mut latest: HashMap> = HashMap::new(); for update in self .debug_info .variable_updates @@ -567,7 +567,7 @@ impl<'a> DebugSession<'a> { fn update_is_visible( &self, - update: &DebugVariableUpdate, + update: &DebugVariableUpdate<'i>, function_name: &str, offset: usize, sequence: u32, @@ -673,7 +673,11 @@ impl<'a> DebugSession<'a> { stacks.dstack.iter().map(|item| encode_hex(item)).collect() } - fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate) -> Result { + fn evaluate_update_with_shadow_vm( + &self, + function_name: &str, + update: &DebugVariableUpdate<'i>, + ) -> Result { self.evaluate_expr_with_shadow_vm(function_name, &update.type_name, &update.expr) } @@ -683,7 +687,12 @@ impl<'a> DebugSession<'a> { /// that pushes current param values then executes the bytecode, run on fresh VM, /// read result from top of stack. This guarantees debugger sees same semantics as /// real execution without duplicating evaluation logic. - fn evaluate_expr_with_shadow_vm(&self, function_name: &str, type_name: &str, expr: &Expr) -> Result { + fn evaluate_expr_with_shadow_vm( + &self, + function_name: &str, + type_name: &str, + expr: &Expr<'i>, + ) -> Result { let params = self.shadow_param_values(function_name)?; let mut param_indexes = HashMap::new(); let mut param_types = HashMap::new(); @@ -739,12 +748,12 @@ impl<'a> DebugSession<'a> { decode_value_by_type(¶m.type_name, bytes) } - fn evaluate_constant(&self, expr: &Expr) -> DebugValue { - match expr { - Expr::Int(v) => DebugValue::Int(*v), - Expr::Bool(v) => DebugValue::Bool(*v), - Expr::Byte(v) => DebugValue::Bytes(vec![*v]), - Expr::String(v) => DebugValue::String(v.clone()), + fn evaluate_constant(&self, expr: &Expr<'i>) -> DebugValue { + match &expr.kind { + ExprKind::Int(v) => DebugValue::Int(*v), + ExprKind::Bool(v) => DebugValue::Bool(*v), + ExprKind::Byte(v) => DebugValue::Bytes(vec![*v]), + ExprKind::String(v) => DebugValue::String(v.clone()), _ => DebugValue::Unknown("complex expression".to_string()), } } @@ -847,14 +856,15 @@ fn encode_hex(bytes: &[u8]) -> String { mod tests { use super::*; - use crate::ast::{BinaryOp, Expr}; + use crate::ast::{BinaryOp, Expr, ExprKind}; use crate::debug::{DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugVariableUpdate}; + use crate::span; fn make_session( params: Vec, - updates: Vec, + updates: Vec>, sigscript: &[u8], - ) -> Result, kaspa_txscript_errors::TxScriptError> { + ) -> Result, kaspa_txscript_errors::TxScriptError> { let sig_cache = Box::leak(Box::new(Cache::new(10_000))); let reused_values: &'static SigHashReusedValuesUnsync = Box::leak(Box::new(SigHashReusedValuesUnsync::new())); let engine: DebugEngine<'static> = @@ -865,7 +875,7 @@ mod tests { variable_updates: updates, params, functions: vec![DebugFunctionRange { name: "f".to_string(), bytecode_start: 0, bytecode_end: 1 }], - constants: vec![DebugConstantMapping { name: "K".to_string(), type_name: "int".to_string(), value: Expr::Int(7) }], + constants: vec![DebugConstantMapping { name: "K".to_string(), type_name: "int".to_string(), value: Expr::int(7) }], }; DebugSession::full(sigscript, &[], "", Some(debug_info), engine) } @@ -899,11 +909,11 @@ mod tests { .evaluate_expr_with_shadow_vm( "f", "int", - &Expr::Binary { + &Expr::new(ExprKind::Binary { op: BinaryOp::Add, - left: Box::new(Expr::Identifier("a".to_string())), - right: Box::new(Expr::Identifier("b".to_string())), - }, + left: Box::new(Expr::identifier("a")), + right: Box::new(Expr::identifier("b")), + }, span::Span::default()), ) .unwrap(); assert!(matches!(value, DebugValue::Int(12))); @@ -920,7 +930,7 @@ mod tests { vec![DebugVariableUpdate { name: "x".to_string(), type_name: "int".to_string(), - expr: Expr::Identifier("missing".to_string()), + expr: Expr::identifier("missing"), bytecode_offset: 0, span: None, function: "f".to_string(), diff --git a/silverscript-lang/src/diagnostic/mod.rs b/silverscript-lang/src/diagnostic/mod.rs new file mode 100644 index 00000000..88e1e5b4 --- /dev/null +++ b/silverscript-lang/src/diagnostic/mod.rs @@ -0,0 +1,5 @@ +mod parse; +mod parse_diagnostics; + +pub use parse::{ErrorSpan, ParseDiagnostic, ParseDiagnosticLabel, ParseDisplayLocation, ParseErrorInterpretation}; +pub(crate) use parse_diagnostics::interpret_parse_error; diff --git a/silverscript-lang/src/diagnostic/parse.rs b/silverscript-lang/src/diagnostic/parse.rs new file mode 100644 index 00000000..731a0551 --- /dev/null +++ b/silverscript-lang/src/diagnostic/parse.rs @@ -0,0 +1,217 @@ +use std::fmt; + +use pest::Position; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ErrorSpan { + pub start: usize, + pub end: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ParseErrorInterpretation { + MissingSemicolon, + Unclassified, +} + +impl ParseErrorInterpretation { + pub const fn code(self) -> &'static str { + match self { + Self::MissingSemicolon => "missing_semicolon", + Self::Unclassified => "parse_error", + } + } + + pub fn from_code(code: &str) -> Option { + match code { + "missing_semicolon" => Some(Self::MissingSemicolon), + "parse_error" => Some(Self::Unclassified), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseDiagnosticLabel { + span: ErrorSpan, + message: String, +} + +impl ParseDiagnosticLabel { + pub fn new(span: ErrorSpan, message: impl Into) -> Self { + Self { span, message: message.into() } + } + + pub fn span(&self) -> ErrorSpan { + self.span + } + + pub fn message(&self) -> &str { + &self.message + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseDisplayLocation { + line: usize, + column: usize, + line_text: String, +} + +impl ParseDisplayLocation { + pub fn new(line: usize, column: usize, line_text: impl Into) -> Self { + Self { line, column, line_text: line_text.into() } + } + + pub fn line(&self) -> usize { + self.line + } + + pub fn column(&self) -> usize { + self.column + } + + pub fn line_text(&self) -> &str { + &self.line_text + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParseDiagnostic { + interpretation: ParseErrorInterpretation, + span: ErrorSpan, + primary_message: String, + expected_tokens: Vec, + labels: Vec, + help: Option, + notes: Vec, + source_text: Box, +} + +impl ParseDiagnostic { + pub(crate) fn new( + interpretation: ParseErrorInterpretation, + span: ErrorSpan, + source_text: &str, + primary_message: impl Into, + ) -> Self { + Self { + interpretation, + span, + primary_message: primary_message.into(), + expected_tokens: Vec::new(), + labels: Vec::new(), + help: None, + notes: Vec::new(), + source_text: source_text.to_owned().into_boxed_str(), + } + } + + pub(crate) fn with_expected_tokens(mut self, expected_tokens: Vec) -> Self { + self.expected_tokens = expected_tokens; + self + } + + pub(crate) fn with_labels(mut self, labels: Vec) -> Self { + self.labels = labels; + self + } + + pub(crate) fn with_help(mut self, help: impl Into) -> Self { + self.help = Some(help.into()); + self + } + + pub(crate) fn with_notes(mut self, notes: Vec) -> Self { + self.notes = notes; + self + } + + pub fn code(&self) -> &'static str { + self.interpretation.code() + } + + pub fn interpretation(&self) -> ParseErrorInterpretation { + self.interpretation + } + + pub fn span(&self) -> ErrorSpan { + self.span + } + + pub fn primary_message(&self) -> &str { + &self.primary_message + } + + pub fn expected_tokens(&self) -> &[String] { + &self.expected_tokens + } + + pub fn labels(&self) -> &[ParseDiagnosticLabel] { + &self.labels + } + + pub fn help(&self) -> Option<&str> { + self.help.as_deref() + } + + pub fn notes(&self) -> &[String] { + &self.notes + } + + pub fn source_text(&self) -> &str { + &self.source_text + } + + pub fn display_location(&self) -> ParseDisplayLocation { + if self.source_text.is_empty() { + return ParseDisplayLocation::new(1, 1, String::new()); + } + let pos = self.span.start.min(self.source_text.len()); + let position = Position::new(&self.source_text, pos).unwrap_or_else(|| Position::from_start(&self.source_text)); + let (line, column) = position.line_col(); + let line_text = position.line_of().lines().next().unwrap_or_default().to_owned(); + ParseDisplayLocation::new(line, column, line_text) + } +} + +// TODO: make the display dumb and: +// * CLI: adapt diagnostic to miette diagnostic +// * LSP: adapt to LSP diagnostic (tower-lsp) +impl fmt::Display for ParseDiagnostic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let location = self.display_location(); + let line_digits = location.line().to_string().len(); + let spacing = " ".repeat(line_digits); + let underline_pad = " ".repeat(location.column().saturating_sub(1)); + writeln!(f, "{spacing}--> {}:{}", location.line(), location.column())?; + writeln!(f, "{spacing} |")?; + writeln!(f, "{} | {}", location.line(), location.line_text())?; + writeln!(f, "{spacing} | {underline_pad}^---")?; + writeln!(f, "{spacing} |")?; + writeln!(f, "{spacing} = error: {}", self.primary_message)?; + + if !self.expected_tokens.is_empty() { + let expected_tokens = self.expected_tokens.iter().map(|token| format_expected_token(token)).collect::>().join(", "); + writeln!(f, "{spacing} note: expected one of tokens: {expected_tokens}")?; + } + for note in &self.notes { + writeln!(f, "{spacing} note: {note}")?; + } + if let Some(help) = &self.help { + writeln!(f, "{spacing} help: {help}")?; + } + + Ok(()) + } +} + +fn format_expected_token(token: &str) -> String { + match token { + "WHITESPACE" => "WHITESPACE".to_owned(), + _ => format!("`{token}`"), + } +} + +impl std::error::Error for ParseDiagnostic {} diff --git a/silverscript-lang/src/diagnostic/parse_diagnostics.rs b/silverscript-lang/src/diagnostic/parse_diagnostics.rs new file mode 100644 index 00000000..e24aa641 --- /dev/null +++ b/silverscript-lang/src/diagnostic/parse_diagnostics.rs @@ -0,0 +1,178 @@ +use std::collections::BTreeSet; + +use super::parse::{ErrorSpan, ParseDiagnostic, ParseDiagnosticLabel, ParseErrorInterpretation}; +use crate::parser::Rule; + +const MISSING_SEMICOLON_EXPECTED_TOKENS: &[&str] = &["WHITESPACE", "/*", "//", ";"]; + +#[derive(Clone, Copy)] +enum SpanStrategy { + AtFailure, + PreviousNonWhitespaceOrFailure, +} + +impl SpanStrategy { + fn resolve(self, input: &str, failure_pos: usize) -> ErrorSpan { + let failure_pos = failure_pos.min(input.len()); + let start = match self { + Self::AtFailure => failure_pos, + Self::PreviousNonWhitespaceOrFailure => focused_error_start(input, failure_pos), + }; + ErrorSpan { start, end: start } + } +} + +#[derive(Clone, Copy)] +struct InterpretationSpec { + interpretation: ParseErrorInterpretation, + span_strategy: SpanStrategy, + expected_tokens_override: Option<&'static [&'static str]>, + help: Option<&'static str>, + primary_label: Option<&'static str>, + notes: &'static [&'static str], +} + +const UNCLASSIFIED_SPEC: InterpretationSpec = InterpretationSpec { + interpretation: ParseErrorInterpretation::Unclassified, + span_strategy: SpanStrategy::AtFailure, + expected_tokens_override: None, + help: None, + primary_label: None, + notes: &[], +}; + +const INTERPRETATION_SPECS: &[InterpretationSpec] = &[ + InterpretationSpec { + interpretation: ParseErrorInterpretation::MissingSemicolon, + span_strategy: SpanStrategy::PreviousNonWhitespaceOrFailure, + expected_tokens_override: Some(MISSING_SEMICOLON_EXPECTED_TOKENS), + help: Some("statements must end with ';'"), + primary_label: Some("expected ';' to terminate statement"), + notes: &[], + }, + UNCLASSIFIED_SPEC, +]; + +#[derive(Clone, Copy)] +struct InterpretationHeuristic { + interpretation: ParseErrorInterpretation, + matches: fn(&ParseAttemptData) -> bool, +} + +const INTERPRETATION_HEURISTICS: &[InterpretationHeuristic] = + &[InterpretationHeuristic { interpretation: ParseErrorInterpretation::MissingSemicolon, matches: expects_semicolon }]; + +#[derive(Default)] +struct ParseAttemptData { + expected_tokens: Vec, + rules: Vec, +} + +impl ParseAttemptData { + fn from_error(err: &pest::error::Error) -> Self { + let Some(attempts) = err.parse_attempts() else { + return Self::default(); + }; + + let expected_tokens = attempts.expected_tokens().into_iter().map(|token| token.to_string()).collect::>(); + + let mut rules = Vec::new(); + for stack in attempts.call_stacks() { + if let Some(rule) = stack.deepest.get_rule() { + rules.push(*rule); + } + if let Some(rule) = stack.parent { + rules.push(rule); + } + } + + Self { expected_tokens, rules } + } + + fn expects_token(&self, token: &str) -> bool { + self.expected_tokens.iter().any(|candidate| candidate == token) + } + + // remove once used by one of the heuristic matcher + #[allow(dead_code)] + fn includes_rule(&self, rule: Rule) -> bool { + self.rules.contains(&rule) + } +} + +pub(crate) fn interpret_parse_error(input: &str, err: &pest::error::Error) -> ParseDiagnostic { + let failure_pos = error_start_offset(err); + let attempt_data = ParseAttemptData::from_error(err); + let interpretation = classify_interpretation(&attempt_data); + let spec = interpretation_spec(interpretation); + let span = spec.span_strategy.resolve(input, failure_pos); + let primary_message = match interpretation { + ParseErrorInterpretation::Unclassified => err.variant.message().into_owned(), + _ => "parsing error occurred.".to_owned(), + }; + + let mut diagnostic = ParseDiagnostic::new(interpretation, span, input, primary_message) + .with_expected_tokens(normalize_expected_tokens(&attempt_data.expected_tokens, spec)) + .with_labels(primary_labels(spec, span)) + .with_notes(spec.notes.iter().map(|note| (*note).to_owned()).collect()); + if let Some(help) = spec.help { + diagnostic = diagnostic.with_help(help); + } + diagnostic +} + +fn classify_interpretation(attempt_data: &ParseAttemptData) -> ParseErrorInterpretation { + INTERPRETATION_HEURISTICS + .iter() + .find(|heuristic| (heuristic.matches)(attempt_data)) + .map(|heuristic| heuristic.interpretation) + .unwrap_or(ParseErrorInterpretation::Unclassified) +} + +fn expects_semicolon(attempt_data: &ParseAttemptData) -> bool { + attempt_data.expects_token(";") +} + +fn interpretation_spec(interpretation: ParseErrorInterpretation) -> &'static InterpretationSpec { + INTERPRETATION_SPECS.iter().find(|spec| spec.interpretation == interpretation).unwrap_or(&UNCLASSIFIED_SPEC) +} + +fn normalize_expected_tokens(expected_tokens: &[String], spec: &InterpretationSpec) -> Vec { + if let Some(tokens) = spec.expected_tokens_override { + return tokens.iter().map(|token| (*token).to_owned()).collect(); + } + + expected_tokens.iter().map(|token| normalize_token(token)).collect::>().into_iter().collect() +} + +fn primary_labels(spec: &InterpretationSpec, span: ErrorSpan) -> Vec { + spec.primary_label.map(|label| vec![ParseDiagnosticLabel::new(span, label)]).unwrap_or_default() +} + +fn normalize_token(token: &str) -> String { + if token.chars().all(char::is_whitespace) { "WHITESPACE".to_string() } else { token.to_string() } +} + +fn error_start_offset(err: &pest::error::Error) -> usize { + match err.location { + pest::error::InputLocation::Pos(pos) => pos, + pest::error::InputLocation::Span((start, _)) => start, + } +} + +fn focused_error_start(input: &str, failure_pos: usize) -> usize { + if is_closing_delimiter_at(input, failure_pos) { + failure_pos + } else { + previous_non_whitespace_offset(input, failure_pos).unwrap_or(failure_pos) + } +} + +fn previous_non_whitespace_offset(input: &str, pos: usize) -> Option { + let prefix = input.get(..pos)?; + prefix.char_indices().rev().find_map(|(idx, ch)| if ch.is_whitespace() { None } else { Some(idx) }) +} + +fn is_closing_delimiter_at(input: &str, pos: usize) -> bool { + matches!(input.as_bytes().get(pos), Some(b')' | b']' | b'}')) +} diff --git a/silverscript-lang/src/errors.rs b/silverscript-lang/src/errors.rs new file mode 100644 index 00000000..caab450f --- /dev/null +++ b/silverscript-lang/src/errors.rs @@ -0,0 +1,52 @@ +use kaspa_txscript::script_builder::ScriptBuilderError; +use thiserror::Error; + +pub use crate::diagnostic::{ErrorSpan, ParseDiagnostic, ParseDiagnosticLabel, ParseDisplayLocation, ParseErrorInterpretation}; +use crate::span; + +#[derive(Debug, Error)] +pub enum CompilerError { + #[error("parse error: {0}")] + Parse(#[from] ParseDiagnostic), + #[error("unsupported feature: {0}")] + Unsupported(String), + #[error("invalid literal: {0}")] + InvalidLiteral(String), + #[error("undefined identifier: {0}")] + UndefinedIdentifier(String), + #[error("cyclic identifier reference: {0}")] + CyclicIdentifier(String), + #[error("script build error: {0}")] + ScriptBuild(#[from] ScriptBuilderError), + // QUESTION: not entierly sure about this pattern + #[error("{source}")] + Context { + #[source] + source: Box, + span: ErrorSpan, + }, +} + +impl CompilerError { + pub fn root(&self) -> &CompilerError { + let mut current = self; + while let Self::Context { source, .. } = current { + current = source; + } + current + } + + pub fn span(&self) -> Option { + match self { + Self::Context { span, .. } => Some(*span), + _ => None, + } + } + + pub fn with_span(self, span: &span::Span<'_>) -> Self { + if self.span().is_some() || matches!(self.root(), Self::Parse(_)) { + return self; + } + Self::Context { source: Box::new(self), span: ErrorSpan { start: span.start(), end: span.end() } } + } +} diff --git a/silverscript-lang/src/lib.rs b/silverscript-lang/src/lib.rs index 3ace8479..cdd68dfb 100644 --- a/silverscript-lang/src/lib.rs +++ b/silverscript-lang/src/lib.rs @@ -1,4 +1,7 @@ pub mod ast; pub mod compiler; pub mod debug; +pub mod diagnostic; +pub mod errors; pub mod parser; +pub mod span; diff --git a/silverscript-lang/src/parser.rs b/silverscript-lang/src/parser.rs index 0ba10fa9..e00bd2ec 100644 --- a/silverscript-lang/src/parser.rs +++ b/silverscript-lang/src/parser.rs @@ -1,16 +1,24 @@ use pest::Parser; -use pest::error::Error; use pest::iterators::Pairs; use pest_derive::Parser; +use crate::errors::ParseDiagnostic; + #[derive(Parser)] #[grammar = "silverscript.pest"] pub struct SilverScriptParser; -pub fn parse_source_file(input: &str) -> Result, Error> { - SilverScriptParser::parse(Rule::source_file, input) +pub fn parse_source_file(input: &str) -> Result, ParseDiagnostic> { + pest::set_error_detail(true); + SilverScriptParser::parse(Rule::source_file, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) +} + +pub fn parse_expression(input: &str) -> Result, ParseDiagnostic> { + pest::set_error_detail(true); + SilverScriptParser::parse(Rule::expression, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) } -pub fn parse_expression(input: &str) -> Result, Error> { - SilverScriptParser::parse(Rule::expression, input) +pub fn parse_type_name(input: &str) -> Result, ParseDiagnostic> { + pest::set_error_detail(true); + SilverScriptParser::parse(Rule::type_name, input).map_err(|err| crate::diagnostic::interpret_parse_error(input, &err)) } diff --git a/silverscript-lang/src/span.rs b/silverscript-lang/src/span.rs new file mode 100644 index 00000000..00e415d9 --- /dev/null +++ b/silverscript-lang/src/span.rs @@ -0,0 +1,81 @@ +use std::fmt; +use std::ops::Deref; + +use pest::Span as PestSpan; +use serde::Serialize; +use serde::Serializer; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Span<'i>(PestSpan<'i>); + +impl<'i> Span<'i> { + pub fn new(input: &'i str, start: usize, end: usize) -> Option { + PestSpan::new(input, start, end).map(Span) + } + + pub fn join(&self, other: &Span<'i>) -> Span<'i> { + let input = self.get_input(); + let start = self.start().min(other.start()); + let end = self.end().max(other.end()); + Span::new(input, start, end).unwrap_or(*self) + } +} + +impl<'i> Default for Span<'i> { + fn default() -> Self { + Span(PestSpan::new("", 0, 0).expect("synthetic span")) + } +} + +impl<'i> From> for Span<'i> { + fn from(span: PestSpan<'i>) -> Self { + Span(span) + } +} + +impl<'i> Deref for Span<'i> { + type Target = PestSpan<'i>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'i> fmt::Display for Span<'i> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let source = self.as_str(); + if source.is_empty() { f.write_str("") } else { f.write_str(source) } + } +} + +// serde serialize becomes display +impl<'i> Serialize for Span<'i> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +pub trait SpanUtils { + fn len(&self) -> usize; + fn is_empty(&self) -> bool { + self.len() == 0 + } + fn contains(&self, offset: usize) -> bool; +} + +impl<'i> SpanUtils for Span<'i> { + fn len(&self) -> usize { + self.end().saturating_sub(self.start()) + } + + fn contains(&self, offset: usize) -> bool { + offset >= self.start() && offset < self.end() + } +} + +pub fn join<'i>(left: &Span<'i>, right: &Span<'i>) -> Span<'i> { + left.join(right) +} diff --git a/silverscript-lang/tests/ast_json/require_test.ast.json b/silverscript-lang/tests/ast_json/require_test.ast.json index fd35aa8b..0ead523d 100644 --- a/silverscript-lang/tests/ast_json/require_test.ast.json +++ b/silverscript-lang/tests/ast_json/require_test.ast.json @@ -1,7 +1,7 @@ { "name": "Test", "params": [], - "constants": {}, + "constants": [], "functions": [ { "name": "main", diff --git a/silverscript-lang/tests/ast_json/yield_test.ast.json b/silverscript-lang/tests/ast_json/yield_test.ast.json index 156c0cba..b93d70f0 100644 --- a/silverscript-lang/tests/ast_json/yield_test.ast.json +++ b/silverscript-lang/tests/ast_json/yield_test.ast.json @@ -1,7 +1,7 @@ { "name": "YieldTest", "params": [], - "constants": {}, + "constants": [], "functions": [ { "name": "main", diff --git a/silverscript-lang/tests/ast_json_tests.rs b/silverscript-lang/tests/ast_json_tests.rs index 115224c4..7d4dea28 100644 --- a/silverscript-lang/tests/ast_json_tests.rs +++ b/silverscript-lang/tests/ast_json_tests.rs @@ -3,7 +3,7 @@ use std::fs; use silverscript_lang::ast::ContractAst; use silverscript_lang::compiler::{CompileOptions, compile_contract, compile_contract_ast}; -fn load_ast(name: &str) -> ContractAst { +fn load_ast(name: &str) -> ContractAst<'_> { let path = format!("{}/tests/ast_json/{name}", env!("CARGO_MANIFEST_DIR")); let json = fs::read_to_string(&path).unwrap_or_else(|err| panic!("failed to read {path}: {err}")); serde_json::from_str(&json).unwrap_or_else(|err| panic!("failed to parse {path}: {err}")) diff --git a/silverscript-lang/tests/ast_spans_tests.rs b/silverscript-lang/tests/ast_spans_tests.rs new file mode 100644 index 00000000..0dca9faf --- /dev/null +++ b/silverscript-lang/tests/ast_spans_tests.rs @@ -0,0 +1,60 @@ +use silverscript_lang::ast::{ExprKind, Statement, parse_contract_ast}; + +fn assert_span_text(source: &str, actual: &str, expected: &str) { + let start = source.find(expected).expect("expected text must exist in source"); + let end = start + expected.len(); + assert_eq!(actual, expected); + assert_eq!(&source[start..end], expected); +} + +#[test] +fn populates_contract_function_and_statement_spans() { + let source = r#" + contract Foo(int a) { + function bar(int b):(int) { + int x = a + b; + return(x); + } + } + "#; + let contract = parse_contract_ast(source).expect("contract should parse"); + + assert_span_text(source, contract.name_span.as_str(), "Foo"); + assert_span_text(source, contract.functions[0].name_span.as_str(), "bar"); + assert_span_text(source, contract.functions[0].body_span.as_str(), "int x = a + b;\n return(x);"); + + let first_stmt = &contract.functions[0].body[0]; + let Statement::VariableDefinition { span, .. } = first_stmt else { + panic!("expected first statement to be a variable definition"); + }; + assert_span_text(source, span.as_str(), "int x = a + b;"); +} + +#[test] +fn populates_slice_expression_spans() { + let source = r#" + contract SliceTest() { + function main(byte[] data) { + byte[] part = data.slice(1, 3); + } + } + "#; + let contract = parse_contract_ast(source).expect("contract should parse"); + let stmt = &contract.functions[0].body[0]; + + let Statement::VariableDefinition { expr: Some(expr), .. } = stmt else { + panic!("expected a variable definition with expression"); + }; + let ExprKind::Slice { source: base, start, end, span } = &expr.kind else { + panic!("expected slice expression"); + }; + let ExprKind::Identifier(_) = &base.kind else { + panic!("slice source should be an identifier"); + }; + + assert_span_text(source, expr.span.as_str(), "data.slice(1, 3)"); + assert_span_text(source, span.as_str(), ".slice(1, 3)"); + assert_span_text(source, base.span.as_str(), "data"); + assert_span_text(source, start.span.as_str(), "1"); + assert_span_text(source, end.span.as_str(), "3"); +} diff --git a/silverscript-lang/tests/cashc_valid_examples_tests.rs b/silverscript-lang/tests/cashc_valid_examples_tests.rs index 43c43b2b..60e2b8b6 100644 --- a/silverscript-lang/tests/cashc_valid_examples_tests.rs +++ b/silverscript-lang/tests/cashc_valid_examples_tests.rs @@ -41,7 +41,7 @@ fn parse_contract_param_types(source: &str) -> Vec { result } -fn dummy_expr_for_type(type_name: &str) -> Expr { +fn dummy_expr_for_type(type_name: &str) -> Expr<'static> { if type_name == "int" { return 0i64.into(); } @@ -125,7 +125,7 @@ fn build_sigscript(args: &[ArgValue], selector: Option) -> Vec { builder.drain() } -fn selector_for_compiled(compiled: &CompiledContract, function_name: &str) -> Option { +fn selector_for_compiled(compiled: &CompiledContract<'_>, function_name: &str) -> Option { if compiled.without_selector { None } else { diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 90ba77d3..2729bb80 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -126,15 +126,15 @@ fn accepts_constructor_args_with_matching_types() { } "#; let args = vec![ - Expr::Int(7), - Expr::Bool(true), - Expr::String("hello".to_string()), - vec![1u8; 10].into(), - Expr::Byte(2), // Single byte for type 'byte' - vec![3u8; 4].into(), - vec![4u8; 32].into(), - vec![5u8; 65].into(), - vec![6u8; 64].into(), + Expr::int(7), + Expr::bool(true), + Expr::string("hello".to_string()), + Expr::bytes(vec![1u8; 10]), + Expr::byte(2), + Expr::bytes(vec![3u8; 4]), + Expr::bytes(vec![4u8; 32]), + Expr::bytes(vec![5u8; 65]), + Expr::bytes(vec![6u8; 64]), ]; compile_contract(source, &args, CompileOptions::default()).expect("compile succeeds"); } @@ -148,7 +148,7 @@ fn rejects_constructor_args_with_wrong_scalar_types() { } } "#; - let args = vec![Expr::Bool(true), Expr::Int(1), vec![1u8].into()]; + let args = vec![Expr::bool(true), Expr::int(1), Expr::bytes(vec![1u8])]; assert!(compile_contract(source, &args, CompileOptions::default()).is_err()); } @@ -161,7 +161,13 @@ fn rejects_constructor_args_with_wrong_byte_lengths() { } } "#; - let args = vec![vec![1u8; 2].into(), vec![2u8; 3].into(), vec![3u8; 31].into(), vec![4u8; 63].into(), vec![5u8; 66].into()]; + let args = vec![ + Expr::bytes(vec![1u8; 2]), + Expr::bytes(vec![2u8; 3]), + Expr::bytes(vec![3u8; 31]), + Expr::bytes(vec![4u8; 63]), + Expr::bytes(vec![5u8; 66]), + ]; assert!(compile_contract(source, &args, CompileOptions::default()).is_err()); } @@ -194,7 +200,7 @@ fn accepts_constructor_args_with_any_bytes_length() { } } "#; - let args = vec![vec![9u8; 128].into()]; + let args = vec![Expr::bytes(vec![9u8; 128])]; compile_contract(source, &args, CompileOptions::default()).expect("compile succeeds"); } @@ -208,7 +214,7 @@ fn build_sig_script_builds_expected_script() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let args = vec![vec![1u8, 2, 3, 4].into(), Expr::Int(7)]; + let args = vec![Expr::bytes(vec![1u8, 2, 3, 4]), Expr::int(7)]; let sigscript = compiled.build_sig_script("spend", args).expect("sigscript builds"); let selector = selector_for(&compiled, "spend"); @@ -233,7 +239,7 @@ fn build_sig_script_rejects_unknown_function() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("missing", vec![Expr::Int(1)]); + let result = compiled.build_sig_script("missing", vec![Expr::int(1)]); assert!(result.is_err()); } @@ -247,7 +253,7 @@ fn build_sig_script_rejects_wrong_argument_count() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![Expr::Int(1)]); + let result = compiled.build_sig_script("spend", vec![Expr::int(1)]); assert!(result.is_err()); } @@ -261,7 +267,7 @@ fn build_sig_script_rejects_wrong_argument_type() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![vec![1u8; 3].into()]); + let result = compiled.build_sig_script("spend", vec![Expr::bytes(vec![1u8; 3])]); assert!(result.is_err()); } @@ -317,7 +323,7 @@ fn rejects_external_call_without_entrypoint() { "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("helper", vec![Expr::Int(1)]); + let result = compiled.build_sig_script("helper", vec![Expr::int(1)]); assert!(result.is_err()); } @@ -345,7 +351,7 @@ fn build_sig_script_rejects_mismatched_bytes_length() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![vec![1u8; 5].into()]); + let result = compiled.build_sig_script("spend", vec![Expr::bytes(vec![1u8; 5])]); assert!(result.is_err()); let source = r#" @@ -356,7 +362,7 @@ fn build_sig_script_rejects_mismatched_bytes_length() { } "#; let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); - let result = compiled.build_sig_script("spend", vec![vec![1u8; 4].into()]); + let result = compiled.build_sig_script("spend", vec![Expr::bytes(vec![1u8; 4])]); assert!(result.is_err()); } @@ -1389,7 +1395,7 @@ fn build_covenant_opcode_tx(sigscript: Vec, covenant_id_a: Hash, covenant_id (tx, entries) } -fn selector_for(compiled: &CompiledContract, function_name: &str) -> Option { +fn selector_for(compiled: &CompiledContract<'_>, function_name: &str) -> Option { if compiled.without_selector { None } else { @@ -3146,7 +3152,7 @@ fn compiles_script_size_and_runs_sum_array() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_size = compiled.script.len() as i64; - let sigscript = compiled.build_sig_script("main", vec![Expr::Int(expected_size)]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![Expr::int(expected_size)]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "script size contract failed: {}", result.unwrap_err()); @@ -3173,7 +3179,7 @@ fn compiles_script_size_data_prefix_small_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![Expr::bytes(expected_prefix)]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix small failed: {}", result.unwrap_err()); @@ -3194,7 +3200,7 @@ fn compiles_script_size_data_prefix_medium_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![Expr::bytes(expected_prefix)]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix medium failed: {}", result.unwrap_err()); @@ -3215,7 +3221,7 @@ fn compiles_script_size_data_prefix_large_script() { let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); let expected_prefix = data_prefix_for_size(compiled.script.len()); - let sigscript = compiled.build_sig_script("main", vec![expected_prefix.into()]).expect("sigscript builds"); + let sigscript = compiled.build_sig_script("main", vec![Expr::bytes(expected_prefix)]).expect("sigscript builds"); let result = run_script_with_sigscript(compiled.script, sigscript); assert!(result.is_ok(), "scriptSizeDataPrefix large failed: {}", result.unwrap_err()); @@ -3506,3 +3512,32 @@ fn accepts_byte_array_with_constant_size() { "#; compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds with byte[HASH_SIZE]"); } + +#[test] +fn blake2b_int_and_byte_cast_forms_compile_to_identical_script() { + let source_plain = r#" + contract Test() { + entrypoint function test() { + int x = 5; + require(blake2b(x).length == 32); + } + } + "#; + + let source_cast = r#" + contract Test() { + entrypoint function test() { + int x = 5; + require(blake2b(byte[](x)).length == 32); + } + } + "#; + + let compiled_plain = compile_contract(source_plain, &[], CompileOptions::default()).expect("plain form compiles"); + let compiled_cast = compile_contract(source_cast, &[], CompileOptions::default()).expect("byte-cast form compiles"); + + assert_eq!( + compiled_plain.script, compiled_cast.script, + "blake2b(x) and blake2b(byte[](x)) should currently compile to identical scripts" + ); +} diff --git a/silverscript-lang/tests/date_literal_tests.rs b/silverscript-lang/tests/date_literal_tests.rs index e0606afc..6b86d1db 100644 --- a/silverscript-lang/tests/date_literal_tests.rs +++ b/silverscript-lang/tests/date_literal_tests.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use silverscript_lang::ast::{Expr, StatementKind, parse_contract_ast}; +use silverscript_lang::ast::{Expr, ExprKind, Statement, parse_contract_ast}; -fn extract_first_expr(source: &str) -> Expr { +fn extract_first_expr<'i>(source: &'i str) -> Expr<'i> { let ast = parse_contract_ast(source).expect("parse succeeds"); let function = &ast.functions[0]; let statement = &function.body[0]; - match &statement.kind { - StatementKind::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), - StatementKind::Require { expr, .. } => expr.clone(), + match statement { + Statement::VariableDefinition { expr, .. } => expr.clone().expect("missing initializer"), + Statement::Require { expr, .. } => expr.clone(), _ => panic!("unexpected statement"), } } @@ -23,8 +23,8 @@ fn parses_date_literal_basic_iso() { } "#; let expr = extract_first_expr(source); - let Expr::Int(parsed) = expr else { - panic!("expected int literal"); + let Expr { kind: ExprKind::DateLiteral(parsed), .. } = expr else { + panic!("expected date literal"); }; let expected = NaiveDateTime::parse_from_str("2021-02-17T01:30:00", "%Y-%m-%dT%H:%M:%S").unwrap().and_utc().timestamp(); assert_eq!(parsed, expected); diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 594fd3e3..81aea52f 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -20,25 +20,25 @@ fn example_contract_path() -> PathBuf { // Convenience harness for the canonical example contract used by baseline session tests. fn with_session(mut f: F) -> Result<(), Box> where - F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, + F: FnMut(&mut DebugSession<'_, '_>) -> Result<(), Box>, { let contract_path = example_contract_path(); assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); let source = fs::read_to_string(&contract_path)?; - with_session_for_source(&source, vec![Expr::Int(3), Expr::Int(10)], "hello", vec![Expr::Int(5), Expr::Int(5)], &mut f) + with_session_for_source(&source, vec![Expr::int(3), Expr::int(10)], "hello", vec![Expr::int(5), Expr::int(5)], &mut f) } // Generic harness that compiles a contract and boots a debugger session for a selected function call. fn with_session_for_source( source: &str, - ctor_args: Vec, + ctor_args: Vec>, function_name: &str, - function_args: Vec, + function_args: Vec>, mut f: F, ) -> Result<(), Box> where - F: FnMut(&mut DebugSession<'_>) -> Result<(), Box>, + F: FnMut(&mut DebugSession<'_, '_>) -> Result<(), Box>, { let parsed_contract = parse_contract_ast(source)?; assert_eq!(parsed_contract.params.len(), ctor_args.len()); @@ -132,7 +132,7 @@ contract BP() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(1)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(1)], |session| { session.run_to_first_executed_statement()?; // Line 8 is inside a multiline `require(...)` span and should still be hit. assert!(session.add_breakpoint(8), "expected breakpoint line to be valid"); @@ -157,7 +157,7 @@ contract Shadow(int x) { } "#; - with_session_for_source(source, vec![Expr::Int(7)], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![Expr::int(7)], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; // Function param `x` should shadow constructor constant `x` in visible debugger variables. @@ -185,7 +185,7 @@ contract ShadowMath(int fee) { } "#; - with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![Expr::int(2)], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; session.step_over()?; @@ -216,7 +216,7 @@ contract FieldOffset(int c) { } "#; - with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(5)], |session| { + with_session_for_source(source, vec![Expr::int(2)], "main", vec![Expr::int(5)], |session| { session.run_to_first_executed_statement()?; let a = session.variable_by_name("a")?; @@ -242,7 +242,7 @@ contract FieldMath(int c) { } "#; - with_session_for_source(source, vec![Expr::Int(2)], "main", vec![Expr::Int(5)], |session| { + with_session_for_source(source, vec![Expr::int(2)], "main", vec![Expr::int(5)], |session| { session.run_to_first_executed_statement()?; for _ in 0..4 { @@ -272,7 +272,7 @@ contract Virtuals() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; let first = session.current_location().ok_or("missing first location")?; assert!(matches!(first.kind, MappingKind::Virtual {})); @@ -302,7 +302,7 @@ contract OpcodeCursor() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; let start = session.current_span().ok_or("missing start span")?; assert_eq!(start.line, 5); @@ -330,7 +330,7 @@ contract VirtualBp() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; assert!(session.add_breakpoint(6), "line with virtual assignment should be a valid breakpoint"); let hit = session.continue_to_breakpoint()?; @@ -354,7 +354,7 @@ contract LocalVars() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; assert!(session.variable_by_name("x").is_err(), "x should not exist before its statement executes"); @@ -401,7 +401,7 @@ contract InlineCalls() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; let start = session.current_span().ok_or("missing start span")?; assert_eq!(start.line, 10); @@ -420,7 +420,7 @@ contract InlineCalls() { Ok(()) })?; - with_session_for_source(source, vec![], "main", vec![Expr::Int(3)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; session.step_into()?; let mut in_callee = session.current_span().ok_or("missing span in callee")?; @@ -464,7 +464,7 @@ contract Repeat() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(0)], |session| { session.run_to_first_executed_statement()?; let start = session.current_span().ok_or("missing start span")?; assert_eq!(start.line, 10, "first source step should be caller line, not callee internals"); @@ -490,7 +490,7 @@ contract Repeat() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(0)], |session| { session.run_to_first_executed_statement()?; let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; @@ -572,7 +572,7 @@ contract DebugPoC(int const) { } "#; - with_session_for_source(source, vec![Expr::Int(0)], "main", vec![Expr::Int(0), Expr::Int(0)], |session| { + with_session_for_source(source, vec![Expr::int(0)], "main", vec![Expr::int(0), Expr::int(0)], |session| { session.run_to_first_executed_statement()?; let initial = session.current_location().ok_or("missing initial location")?; @@ -616,7 +616,7 @@ contract InlineParams() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(4)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(4)], |session| { session.run_to_first_executed_statement()?; let mut saw_inline_param = false; @@ -662,7 +662,7 @@ contract NestedArgs() { } "#; - with_session_for_source(source, vec![], "main", vec![Expr::Int(0)], |session| { + with_session_for_source(source, vec![], "main", vec![Expr::int(0)], |session| { session.run_to_first_executed_statement()?; let start = session.current_span().ok_or("missing start span")?; assert_eq!(start.line, 15); diff --git a/silverscript-lang/tests/parse_diagnostics_tests.rs b/silverscript-lang/tests/parse_diagnostics_tests.rs new file mode 100644 index 00000000..a0adf65a --- /dev/null +++ b/silverscript-lang/tests/parse_diagnostics_tests.rs @@ -0,0 +1,53 @@ +use silverscript_lang::ast::parse_contract_ast; +use silverscript_lang::errors::{CompilerError, ParseErrorInterpretation}; + +#[test] +fn full_diagnostic_from_missing_semicolon() { + let source = r#" + contract Foo() { + function bar(byte[] data) { + int x = a + b + int t = x + a; + } + } + "#; + let err = parse_contract_ast(source).expect_err("source without semicolon must fail parsing"); + let CompilerError::Parse(diagnostic) = err else { + panic!("expected parse error"); + }; + assert_eq!(diagnostic.interpretation(), ParseErrorInterpretation::MissingSemicolon); + assert_eq!(diagnostic.code(), "missing_semicolon"); + assert_eq!(diagnostic.expected_tokens(), ["WHITESPACE", "/*", "//", ";"]); + assert_eq!(diagnostic.primary_message(), "parsing error occurred."); + assert_eq!(diagnostic.help(), Some("statements must end with ';'")); + assert_eq!(diagnostic.labels().len(), 1); + + let span = diagnostic.span(); + assert_eq!(span.start, span.end); + assert_eq!(&source[span.start..span.start + 1], "b"); + + let location = diagnostic.display_location(); + assert!(location.line() > 0); + assert!(location.column() > 0); + assert!(location.line_text().contains("int x = a + b")); +} + +#[test] +fn unclassified_diagnostic_preserves_pest_message() { + let source = r#" + pragma silverscript ^0.1.0; + + contract Foo() { + ??? + } + "#; + + let err = parse_contract_ast(source).expect_err("invalid token must fail parsing"); + let CompilerError::Parse(diagnostic) = err else { + panic!("expected parse error"); + }; + + assert_eq!(diagnostic.interpretation(), ParseErrorInterpretation::Unclassified); + assert_ne!(diagnostic.primary_message(), "parsing error occurred."); + assert!(diagnostic.primary_message().contains("expected")); +} diff --git a/silverscript-lang/tests/parser_tests.rs b/silverscript-lang/tests/parser_tests.rs index 63cde999..f63ce1aa 100644 --- a/silverscript-lang/tests/parser_tests.rs +++ b/silverscript-lang/tests/parser_tests.rs @@ -50,3 +50,25 @@ fn parses_arrays_and_introspection() { panic!("{}", err); } } + +#[test] +fn parses_input_sigscript_and_rejects_output_sigscript() { + let input_ok = r#" + contract SigScriptCheck() { + function verify(int idx) { + require(tx.inputs[idx].sigScript.length >= 0); + } + } + "#; + assert!(parse_source_file(input_ok).is_ok()); + + let input_bad = r#" + contract SigScriptCheck() { + function verify(int idx) { + // outputs don't have a sigScript field, so parsing is expected to fail + require(tx.outputs[idx].sigScript.length >= 0); + } + } + "#; + assert!(parse_source_file(input_bad).is_err()); +} diff --git a/silverscript-lang/tests/silverc_tests.rs b/silverscript-lang/tests/silverc_tests.rs index 5f7ba4ef..dd0d2ff0 100644 --- a/silverscript-lang/tests/silverc_tests.rs +++ b/silverscript-lang/tests/silverc_tests.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Command; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; @@ -11,9 +11,38 @@ use kaspa_txscript::caches::Cache; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{EngineCtx, EngineFlags, TxScriptEngine}; use rand::RngCore; -use silverscript_lang::ast::Expr; +use silverscript_lang::ast::{ContractAst, Expr}; use silverscript_lang::compiler::{CompiledContract, function_branch_index}; +const BASIC_CONTRACT_SOURCE: &str = r#" + contract Basic() { + entrypoint function main() { + require(true); + } + } + "#; + +const WITH_CTOR_SOURCE: &str = r#" + contract WithCtor(int a) { + entrypoint function main() { + require(a == 7); + } + } + "#; + +fn silverc() -> Command { + Command::new(env!("CARGO_BIN_EXE_silverc")) +} + +fn write_basic_contract(path: &Path) { + fs::write(path, BASIC_CONTRACT_SOURCE).expect("write source"); +} + +fn write_with_ctor_contract(path: &Path) { + fs::write(path, WITH_CTOR_SOURCE).expect("write source"); +} + +// TODO: move to tempfile crate or manually delete as a test tear down fn temp_dir(name: &str) -> PathBuf { let mut rng = rand::thread_rng(); let dir = std::env::temp_dir().join(format!("silverc_test_{name}_{}", rng.next_u64())); @@ -56,16 +85,9 @@ fn run_script_with_selector(script: Vec, selector: Option) -> Result<() fn silverc_defaults_output_path_and_empty_ctor_args() { let dir = temp_dir("default"); let src_path = dir.join("basic.sil"); - let source = r#" - contract Basic() { - entrypoint function main() { - require(true); - } - } - "#; - fs::write(&src_path, source).expect("write source"); + write_basic_contract(&src_path); - let status = Command::new(env!("CARGO_BIN_EXE_silverc")).arg(src_path.to_str().unwrap()).status().expect("run silverc"); + let status = silverc().arg(src_path.to_str().unwrap()).status().expect("run silverc"); assert!(status.success()); let out_path = dir.join("basic.json"); @@ -74,24 +96,34 @@ fn silverc_defaults_output_path_and_empty_ctor_args() { assert_eq!(compiled.contract_name, "Basic"); } +#[test] +fn silverc_stdout_flag_overrides_output_file() { + let dir = temp_dir("compile_stdout"); + let src_path = dir.join("basic.sil"); + let out_path = dir.join("compiled.json"); + write_basic_contract(&src_path); + + let output = + silverc().arg(src_path.to_str().unwrap()).arg("-o").arg(out_path.to_str().unwrap()).arg("-c").output().expect("run silverc"); + assert!(output.status.success()); + + let stdout = String::from_utf8(output.stdout).expect("decode stdout"); + let compiled: CompiledContract = serde_json::from_str(&stdout).expect("parse compiled json"); + assert_eq!(compiled.contract_name, "Basic"); + assert!(!out_path.exists()); +} + #[test] fn silverc_accepts_constructor_args_and_output_flag() { let dir = temp_dir("ctor"); let src_path = dir.join("with_ctor.sil"); let out_path = dir.join("out.json"); let ctor_path = dir.join("ctor.json"); - let source = r#" - contract WithCtor(int a) { - entrypoint function main() { - require(a == 7); - } - } - "#; - fs::write(&src_path, source).expect("write source"); - let ctor_args = vec![Expr::Int(7)]; + write_with_ctor_contract(&src_path); + let ctor_args = vec![Expr::int(7)]; fs::write(&ctor_path, serde_json::to_string(&ctor_args).expect("serialize ctor args")).expect("write ctor args"); - let status = Command::new(env!("CARGO_BIN_EXE_silverc")) + let status = silverc() .arg(src_path.to_str().unwrap()) .arg("--constructor-args") .arg(ctor_path.to_str().unwrap()) @@ -108,3 +140,40 @@ fn silverc_accepts_constructor_args_and_output_flag() { if compiled.without_selector { None } else { Some(function_branch_index(&compiled.ast, "main").expect("selector resolved")) }; assert!(run_script_with_selector(compiled.script, selector).is_ok()); } + +#[test] +fn silverc_ast_only_defaults_to_stdout() { + let dir = temp_dir("ast_stdout"); + let src_path = dir.join("basic.sil"); + write_basic_contract(&src_path); + + let output = silverc().arg(src_path.to_str().unwrap()).arg("--ast-only").output().expect("run silverc"); + assert!(output.status.success()); + + let stdout = String::from_utf8(output.stdout).expect("decode stdout"); + let ast: ContractAst<'static> = serde_json::from_str(&stdout).expect("parse ast json"); + assert_eq!(ast.name, "Basic"); + assert!(!dir.join("basic.json").exists()); +} + +#[test] +fn silverc_ast_only_writes_file_with_output_flag() { + let dir = temp_dir("ast_file"); + let src_path = dir.join("basic.sil"); + let out_path = dir.join("basic.ast.json"); + write_basic_contract(&src_path); + + let output = silverc() + .arg(src_path.to_str().unwrap()) + .arg("--ast-only") + .arg("-o") + .arg(out_path.to_str().unwrap()) + .output() + .expect("run silverc"); + assert!(output.status.success()); + assert!(output.stdout.is_empty()); + + let ast_json = fs::read_to_string(&out_path).expect("read ast output"); + let ast: ContractAst<'static> = serde_json::from_str(&ast_json).expect("parse ast json"); + assert_eq!(ast.name, "Basic"); +} diff --git a/silverscript-lang/tests/tutorial_rust_examples_tests.rs b/silverscript-lang/tests/tutorial_rust_examples_tests.rs index 146a14b1..2a8d2252 100644 --- a/silverscript-lang/tests/tutorial_rust_examples_tests.rs +++ b/silverscript-lang/tests/tutorial_rust_examples_tests.rs @@ -13,7 +13,7 @@ fn tutorial_rust_programmatic_compilation_example() { } "#; - let constructor_args = vec![Expr::Int(100)]; + let constructor_args = vec![Expr::int(100)]; let compiled = compile_contract(source, &constructor_args, CompileOptions::default()) .expect("programmatic compilation example should compile"); From 4ecc23c4dbe57238a5c549d81110e1fc3c320c97 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:44:33 +0200 Subject: [PATCH 26/41] fmt --- silverscript-lang/src/compiler.rs | 15 ++++------- .../src/compiler/debug_recording.rs | 8 +----- silverscript-lang/src/debug.rs | 2 +- silverscript-lang/src/debug/session.rs | 26 +++++++------------ 4 files changed, 17 insertions(+), 34 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 9b47b384..284df8c8 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -9,9 +9,9 @@ use crate::ast::{ StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, parse_contract_ast, parse_type_ref, }; -pub use crate::errors::{CompilerError, ErrorSpan}; use crate::debug::labels::synthetic; use crate::debug::{DebugInfo, SourceSpan}; +pub use crate::errors::{CompilerError, ErrorSpan}; use crate::span; mod debug_recording; @@ -2369,10 +2369,9 @@ fn expand_inline_arg_placeholders<'i>( visiting.remove(&name); Ok(expanded) } - ExprKind::Unary { op, expr } => Ok(Expr::new( - ExprKind::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }, - span, - )), + ExprKind::Unary { op, expr } => { + Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }, span)) + } ExprKind::Binary { op, left, right } => Ok(Expr::new( ExprKind::Binary { op, @@ -2439,11 +2438,7 @@ fn expand_inline_arg_placeholders<'i>( span, )), ExprKind::Introspection { kind, index, field_span } => Ok(Expr::new( - ExprKind::Introspection { - kind, - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), - field_span, - }, + ExprKind::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), field_span }, span, )), ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index ff84dd62..bf8f23cd 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -379,13 +379,7 @@ impl<'i> DebugSink<'i> { }); } - pub fn record_compiled_function( - &mut self, - name: &str, - script_len: usize, - debug: &FunctionDebugRecorder<'i>, - offset: usize, - ) { + pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionDebugRecorder<'i>, offset: usize) { let Some(rec) = self.recorder_mut() else { return; }; diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug.rs index 020ed760..1514ed93 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug.rs @@ -1,6 +1,6 @@ use crate::ast::Expr; -use serde::{Deserialize, Serialize}; use crate::span; +use serde::{Deserialize, Serialize}; pub mod presentation; pub mod session; diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 780613b1..8c3dd4bf 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -673,11 +673,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { stacks.dstack.iter().map(|item| encode_hex(item)).collect() } - fn evaluate_update_with_shadow_vm( - &self, - function_name: &str, - update: &DebugVariableUpdate<'i>, - ) -> Result { + fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate<'i>) -> Result { self.evaluate_expr_with_shadow_vm(function_name, &update.type_name, &update.expr) } @@ -687,12 +683,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// that pushes current param values then executes the bytecode, run on fresh VM, /// read result from top of stack. This guarantees debugger sees same semantics as /// real execution without duplicating evaluation logic. - fn evaluate_expr_with_shadow_vm( - &self, - function_name: &str, - type_name: &str, - expr: &Expr<'i>, - ) -> Result { + fn evaluate_expr_with_shadow_vm(&self, function_name: &str, type_name: &str, expr: &Expr<'i>) -> Result { let params = self.shadow_param_values(function_name)?; let mut param_indexes = HashMap::new(); let mut param_types = HashMap::new(); @@ -909,11 +900,14 @@ mod tests { .evaluate_expr_with_shadow_vm( "f", "int", - &Expr::new(ExprKind::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::identifier("a")), - right: Box::new(Expr::identifier("b")), - }, span::Span::default()), + &Expr::new( + ExprKind::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::identifier("a")), + right: Box::new(Expr::identifier("b")), + }, + span::Span::default(), + ), ) .unwrap(); assert!(matches!(value, DebugValue::Int(12))); From 49c7e354c456e29146bf937a0ac3081c15492874 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:21:41 +0200 Subject: [PATCH 27/41] debugger: fix tx-context local eval and loop index visibility --- silverscript-lang/src/compiler.rs | 12 +- .../src/compiler/debug_recording.rs | 29 +++++ silverscript-lang/src/debug/session.rs | 34 +++++- .../tests/debug_session_tests.rs | 112 +++++++++++++++++- 4 files changed, 182 insertions(+), 5 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 284df8c8..72e52bca 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1286,11 +1286,12 @@ fn compile_statement<'i>( script_size, debug_recorder, ), - Statement::For { ident, start, end, body, .. } => compile_for_statement( + Statement::For { ident, start, end, body, span, .. } => compile_for_statement( ident, start, end, body, + *span, env, params, types, @@ -2112,6 +2113,7 @@ fn compile_for_statement<'i>( start_expr: &Expr<'i>, end_expr: &Expr<'i>, body: &[Statement<'i>], + for_span: span::Span<'i>, env: &mut HashMap>, params: &HashMap, types: &mut HashMap, @@ -2134,9 +2136,17 @@ fn compile_for_statement<'i>( } let name = ident.to_string(); + let loop_span = SourceSpan::from(for_span); let previous = env.get(&name).cloned(); for value in start..end { env.insert(name.clone(), Expr::int(value)); + debug_recorder.record_virtual_binding( + name.clone(), + "int".to_string(), + Expr::int(value), + builder.script().len(), + Some(loop_span), + ); compile_block( body, env, diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index bf8f23cd..e6ba2c6f 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -196,6 +196,35 @@ impl<'i> FunctionDebugRecorder<'i> { Ok(()) } + /// Emits a virtual debug step that binds a synthetic local variable. + /// Used by lowered constructs (for example unrolled loops) to keep + /// source-level locals visible even when no dedicated statement exists. + pub fn record_virtual_binding( + &mut self, + name: String, + type_name: String, + expr: Expr<'i>, + bytecode_offset: usize, + span: Option, + ) { + if !self.enabled { + return; + } + let Some(sequence) = self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) else { + return; + }; + self.variable_updates.push(DebugVariableUpdate { + name, + type_name, + expr, + bytecode_offset, + span, + function: self.function_name.clone(), + sequence, + frame_id: self.frame_id, + }); + } + /// Starts an inline call recording session and returns a child recorder for /// callee body statements. pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { diff --git a/silverscript-lang/src/debug/session.rs b/silverscript-lang/src/debug/session.rs index 8c3dd4bf..63ebce19 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/silverscript-lang/src/debug/session.rs @@ -1,8 +1,9 @@ use std::collections::{HashMap, HashSet}; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; -use kaspa_consensus_core::tx::PopulatedTransaction; +use kaspa_consensus_core::tx::{PopulatedTransaction, TransactionInput, UtxoEntry}; use kaspa_txscript::caches::Cache; +use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; use serde::{Deserialize, Serialize}; @@ -19,6 +20,15 @@ pub type DebugReused = SigHashReusedValuesUnsync; pub type DebugOpcode<'a> = DynOpcodeImplementation, DebugReused>; pub type DebugEngine<'a> = TxScriptEngine<'a, DebugTx<'a>, DebugReused>; +#[derive(Clone, Copy)] +pub struct ShadowTxContext<'a> { + pub tx: &'a DebugTx<'a>, + pub input: &'a TransactionInput, + pub input_index: usize, + pub utxo_entry: &'a UtxoEntry, + pub covenants_ctx: &'a CovenantsContext, +} + #[derive(Debug, Clone)] pub enum DebugValue { Int(i64), @@ -80,6 +90,7 @@ pub struct OpcodeMeta { pub struct DebugSession<'a, 'i> { engine: DebugEngine<'a>, + shadow_tx_context: Option>, opcodes: Vec>>, op_displays: Vec, opcode_offsets: Vec, @@ -178,6 +189,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { Ok(Self { engine, + shadow_tx_context: None, opcodes, op_displays, opcode_offsets, @@ -205,6 +217,11 @@ impl<'a, 'i> DebugSession<'a, 'i> { Ok(Some(self.state())) } + pub fn with_shadow_tx_context(mut self, shadow_tx_context: ShadowTxContext<'a>) -> Self { + self.shadow_tx_context = Some(shadow_tx_context); + self + } + /// Step into: advance to next source step regardless of call depth. pub fn step_into(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { self.step_with_depth_predicate(|_, _| true) @@ -725,8 +742,19 @@ impl<'a, 'i> DebugSession<'a, 'i> { fn execute_shadow_script(&self, script: &[u8]) -> Result, String> { let sig_cache = Cache::new(0); let reused_values = SigHashReusedValuesUnsync::new(); - let mut engine: DebugEngine<'_> = - TxScriptEngine::new(EngineCtx::new(&sig_cache).with_reused(&reused_values), EngineFlags { covenants_enabled: true }); + let mut engine: DebugEngine<'_> = if let Some(shadow) = self.shadow_tx_context { + let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values).with_covenants_ctx(shadow.covenants_ctx); + TxScriptEngine::from_transaction_input( + shadow.tx, + shadow.input, + shadow.input_index, + shadow.utxo_entry, + ctx, + EngineFlags { covenants_enabled: true }, + ) + } else { + TxScriptEngine::new(EngineCtx::new(&sig_cache).with_reused(&reused_values), EngineFlags { covenants_enabled: true }) + }; for opcode in parse_script::, DebugReused>(script) { let opcode = opcode.map_err(|err| format!("failed to parse shadow script: {err}"))?; engine.execute_opcode(opcode).map_err(|err| format!("failed to execute shadow script: {err}"))?; diff --git a/silverscript-lang/tests/debug_session_tests.rs b/silverscript-lang/tests/debug_session_tests.rs index 81aea52f..ae15adc2 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/silverscript-lang/tests/debug_session_tests.rs @@ -3,14 +3,21 @@ use std::error::Error; use std::fs; use std::path::PathBuf; +use kaspa_consensus_core::Hash; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_consensus_core::tx::{ + PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionOutpoint, TransactionOutput, + UtxoEntry, VerifiableTransaction, +}; use kaspa_txscript::caches::Cache; +use kaspa_txscript::covenants::CovenantsContext; +use kaspa_txscript::opcodes::codes::OpTrue; use kaspa_txscript::{EngineCtx, EngineFlags}; use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; use silverscript_lang::debug::MappingKind; -use silverscript_lang::debug::session::DebugSession; +use silverscript_lang::debug::session::{DebugSession, ShadowTxContext}; fn example_contract_path() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -677,3 +684,106 @@ contract NestedArgs() { Ok(()) }) } + +#[test] +fn debug_session_exposes_loop_index_variable_i() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract LoopIndex() { + entrypoint function main() { + int sum = 0; + for(i,0,2){ + if(i < 2){ + sum = sum + i; + } + } + require(sum >= 0); + } +} +"#; + + with_session_for_source(source, vec![], "main", vec![], |session| { + session.run_to_first_executed_statement()?; + let mut saw_loop_index = false; + + for _ in 0..12 { + if let Ok(i) = session.variable_by_name("i") { + assert_eq!(session.format_value(&i.type_name, &i.value), "0"); + saw_loop_index = true; + break; + } + if session.step_over()?.is_none() { + break; + } + } + + assert!(saw_loop_index, "expected loop index 'i' to be visible while stepping loop body"); + Ok(()) + }) +} + +#[test] +fn debug_session_shadow_eval_uses_tx_context_for_covenant_opcode_locals() -> Result<(), Box> { + let source = r#"pragma silverscript ^0.1.0; + +contract CovLocal() { + entrypoint function main() { + byte[32] covid = OpInputCovenantId(this.activeInputIndex); + require(covid == covid); + } +} +"#; + + let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &[], compile_opts)?; + let debug_info = compiled.debug_info.clone(); + let sigscript = compiled.build_sig_script("main", vec![])?; + + let input = TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: TransactionId::from_bytes([0x44u8; 32]), index: 0 }, + signature_script: sigscript.clone(), + sequence: 0, + sig_op_count: 0, + }; + let output = TransactionOutput { value: 1000, script_public_key: ScriptPublicKey::new(0, vec![OpTrue].into()), covenant: None }; + let tx = Transaction::new(1, vec![input], vec![output], 0, Default::default(), 0, vec![]); + + let covenant_id = Hash::from_bytes([0x11u8; 32]); + let utxo_entry = + UtxoEntry::new(1000, ScriptPublicKey::new(0, compiled.script.clone().into()), 0, tx.is_coinbase(), Some(covenant_id)); + let populated_tx = PopulatedTransaction::new(&tx, vec![utxo_entry]); + let cov_ctx = CovenantsContext::from_tx(&populated_tx)?; + + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values).with_covenants_ctx(&cov_ctx); + let input_ref = &tx.inputs[0]; + let utxo_ref = populated_tx.utxo(0).ok_or("missing utxo for input 0")?; + let engine = silverscript_lang::debug::session::DebugEngine::from_transaction_input( + &populated_tx, + input_ref, + 0, + utxo_ref, + ctx, + EngineFlags { covenants_enabled: true }, + ); + + let shadow_ctx = + ShadowTxContext { tx: &populated_tx, input: input_ref, input_index: 0, utxo_entry: utxo_ref, covenants_ctx: &cov_ctx }; + + let mut session = DebugSession::full(&sigscript, &compiled.script, source, debug_info, engine)?.with_shadow_tx_context(shadow_ctx); + session.run_to_first_executed_statement()?; + + for _ in 0..4 { + if let Ok(covid) = session.variable_by_name("covid") { + let rendered = session.format_value(&covid.type_name, &covid.value); + assert_eq!(rendered, format!("0x{}", "11".repeat(32))); + return Ok(()); + } + if session.step_over()?.is_none() { + break; + } + } + + Err("expected covid local to be evaluated using tx context".into()) +} From 9031d3edb6528ad3e03441e9ec26e7eaad5eb464 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:25:20 +0200 Subject: [PATCH 28/41] refactor: split debugger into separate crates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename debug.rs → debug_info.rs in silverscript-lang (types-only module) - Create debugger-session library crate (session runtime, presentation) - Create cli-debugger binary crate (renamed from sil-debug) - Migrate debug session tests and CLI smoke test to new crates - Update all imports across workspace --- Cargo.lock | 28 +++++++++++++++++++ Cargo.toml | 7 ++++- debugger/cli/Cargo.toml | 22 +++++++++++++++ .../sil-debug.rs => debugger/cli/src/main.rs | 4 +-- .../cli/tests/cli_tests.rs | 12 ++++---- debugger/cli/tests/if_statement.sil | 17 +++++++++++ debugger/session/Cargo.toml | 24 ++++++++++++++++ debugger/session/src/lib.rs | 2 ++ .../session/src}/presentation.rs | 5 ++-- .../debug => debugger/session/src}/session.rs | 18 ++++++------ .../session}/tests/debug_session_tests.rs | 8 +++--- .../session/tests/examples/debug_messages.sil | 8 ++++++ .../session/tests/examples/if_statement.sil | 17 +++++++++++ silverscript-lang/src/compiler.rs | 4 +-- .../src/compiler/debug_recording.rs | 2 +- .../src/{debug.rs => debug_info.rs} | 3 -- silverscript-lang/src/lib.rs | 2 +- 17 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 debugger/cli/Cargo.toml rename silverscript-lang/src/bin/sil-debug.rs => debugger/cli/src/main.rs (99%) rename silverscript-lang/tests/debugger_cli_tests.rs => debugger/cli/tests/cli_tests.rs (80%) create mode 100644 debugger/cli/tests/if_statement.sil create mode 100644 debugger/session/Cargo.toml create mode 100644 debugger/session/src/lib.rs rename {silverscript-lang/src/debug => debugger/session/src}/presentation.rs (98%) rename {silverscript-lang/src/debug => debugger/session/src}/session.rs (98%) rename {silverscript-lang => debugger/session}/tests/debug_session_tests.rs (98%) create mode 100644 debugger/session/tests/examples/debug_messages.sil create mode 100644 debugger/session/tests/examples/if_statement.sil rename silverscript-lang/src/{debug.rs => debug_info.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index 7fe4380b..a2e57781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,6 +493,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cli-debugger" +version = "0.1.0" +dependencies = [ + "clap", + "debugger-session", + "faster-hex 0.10.0", + "kaspa-consensus-core", + "kaspa-txscript", + "kaspa-txscript-errors", + "serde_json", + "silverscript-lang", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -606,6 +620,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "debugger-session" +version = "0.1.0" +dependencies = [ + "faster-hex 0.10.0", + "kaspa-addresses", + "kaspa-consensus-core", + "kaspa-txscript", + "kaspa-txscript-errors", + "serde", + "serde_json", + "silverscript-lang", +] + [[package]] name = "deranged" version = "0.5.5" diff --git a/Cargo.toml b/Cargo.toml index ddfd4973..e300c0bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["silverscript-lang", "covenants/sdk"] +members = [ + "silverscript-lang", + "debugger/session", + "debugger/cli", + "covenants/sdk", +] resolver = "2" [workspace.package] diff --git a/debugger/cli/Cargo.toml b/debugger/cli/Cargo.toml new file mode 100644 index 00000000..3c3d1b78 --- /dev/null +++ b/debugger/cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cli-debugger" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +rust-version.workspace = true + +[[bin]] +name = "cli-debugger" +path = "src/main.rs" + +[dependencies] +debugger-session = { path = "../session" } +silverscript-lang = { path = "../../silverscript-lang" } +kaspa-consensus-core.workspace = true +kaspa-txscript.workspace = true +kaspa-txscript-errors.workspace = true +clap = { version = "4.5.60", features = ["derive"] } +faster-hex = "0.10" +serde_json = "1.0" diff --git a/silverscript-lang/src/bin/sil-debug.rs b/debugger/cli/src/main.rs similarity index 99% rename from silverscript-lang/src/bin/sil-debug.rs rename to debugger/cli/src/main.rs index 8fcd602f..1c50e116 100644 --- a/silverscript-lang/src/bin/sil-debug.rs +++ b/debugger/cli/src/main.rs @@ -6,15 +6,15 @@ use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; use kaspa_txscript::caches::Cache; use kaspa_txscript::{EngineCtx, EngineFlags}; +use debugger_session::session::{DebugEngine, DebugSession}; use silverscript_lang::ast::{Expr, ExprKind, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; -use silverscript_lang::debug::session::{DebugEngine, DebugSession}; use silverscript_lang::span; const PROMPT: &str = "(sdb) "; #[derive(Debug, Parser)] -#[command(name = "sil-debug", about = "SilverScript debugger")] +#[command(name = "cli-debugger", about = "SilverScript debugger")] struct CliArgs { script_path: String, #[arg(long = "no-selector")] diff --git a/silverscript-lang/tests/debugger_cli_tests.rs b/debugger/cli/tests/cli_tests.rs similarity index 80% rename from silverscript-lang/tests/debugger_cli_tests.rs rename to debugger/cli/tests/cli_tests.rs index ba7d99ab..31d76882 100644 --- a/silverscript-lang/tests/debugger_cli_tests.rs +++ b/debugger/cli/tests/cli_tests.rs @@ -4,15 +4,15 @@ use std::process::{Command, Stdio}; fn example_contract_path() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir.join("tests/examples/if_statement.sil") + manifest_dir.join("tests/if_statement.sil") } #[test] -fn sil_debug_repl_all_commands_smoke() { +fn cli_debugger_repl_all_commands_smoke() { let contract_path = example_contract_path(); assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); - let mut child = Command::new(env!("CARGO_BIN_EXE_sil-debug")) + let mut child = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) .arg(contract_path) .arg("--function") .arg("hello") @@ -28,13 +28,13 @@ fn sil_debug_repl_all_commands_smoke() { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() - .expect("failed to spawn sil-debug"); + .expect("failed to spawn cli-debugger"); let input = b"help\nl\nstack\nb 1\nb 7\nb\nn\nsi\nq\n"; child.stdin.as_mut().expect("stdin available").write_all(input).expect("write stdin"); - let output = child.wait_with_output().expect("wait for sil-debug"); - assert!(output.status.success(), "sil-debug exited with status {:?}", output.status.code()); + let output = child.wait_with_output().expect("wait for cli-debugger"); + assert!(output.status.success(), "cli-debugger exited with status {:?}", output.status.code()); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); diff --git a/debugger/cli/tests/if_statement.sil b/debugger/cli/tests/if_statement.sil new file mode 100644 index 00000000..8331be55 --- /dev/null +++ b/debugger/cli/tests/if_statement.sil @@ -0,0 +1,17 @@ +pragma silverscript ^0.1.0; + +contract IfStatement(int x, int y) { + entrypoint function hello(int a, int b) { + int d = a + b; + d = d - a; + if (d == x - 2) { + int c = d + b; + d = a + c; + require(c > d); + } else { + require(d == a); + } + d = d + a; + require(d == y); + } +} diff --git a/debugger/session/Cargo.toml b/debugger/session/Cargo.toml new file mode 100644 index 00000000..76fa8405 --- /dev/null +++ b/debugger/session/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "debugger-session" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +name = "debugger_session" +path = "src/lib.rs" + +[dependencies] +silverscript-lang = { path = "../../silverscript-lang" } +kaspa-consensus-core.workspace = true +kaspa-txscript.workspace = true +kaspa-txscript-errors.workspace = true +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +faster-hex = "0.10" + +[dev-dependencies] +kaspa-addresses.workspace = true diff --git a/debugger/session/src/lib.rs b/debugger/session/src/lib.rs new file mode 100644 index 00000000..0d9cebd9 --- /dev/null +++ b/debugger/session/src/lib.rs @@ -0,0 +1,2 @@ +pub mod presentation; +pub mod session; diff --git a/silverscript-lang/src/debug/presentation.rs b/debugger/session/src/presentation.rs similarity index 98% rename from silverscript-lang/src/debug/presentation.rs rename to debugger/session/src/presentation.rs index 3b052e62..65ce7100 100644 --- a/silverscript-lang/src/debug/presentation.rs +++ b/debugger/session/src/presentation.rs @@ -1,5 +1,6 @@ -use crate::debug::SourceSpan; -use crate::debug::session::DebugValue; +use silverscript_lang::debug_info::SourceSpan; + +use crate::session::DebugValue; #[derive(Debug, Clone)] pub struct SourceContextLine { diff --git a/silverscript-lang/src/debug/session.rs b/debugger/session/src/session.rs similarity index 98% rename from silverscript-lang/src/debug/session.rs rename to debugger/session/src/session.rs index 63ebce19..589b158b 100644 --- a/silverscript-lang/src/debug/session.rs +++ b/debugger/session/src/session.rs @@ -8,12 +8,14 @@ use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; use serde::{Deserialize, Serialize}; -use crate::ast::{Expr, ExprKind}; -use crate::compiler::compile_debug_expr; -use crate::debug::presentation::{build_source_context, format_value as format_debug_value}; -use crate::debug::{DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan}; +use silverscript_lang::ast::{Expr, ExprKind}; +use silverscript_lang::compiler::compile_debug_expr; +use silverscript_lang::debug_info::{ + DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan, +}; -pub use crate::debug::presentation::{SourceContext, SourceContextLine}; +pub use crate::presentation::{SourceContext, SourceContextLine}; +use crate::presentation::{build_source_context, format_value as format_debug_value}; pub type DebugTx<'a> = PopulatedTransaction<'a>; pub type DebugReused = SigHashReusedValuesUnsync; @@ -875,9 +877,9 @@ fn encode_hex(bytes: &[u8]) -> String { mod tests { use super::*; - use crate::ast::{BinaryOp, Expr, ExprKind}; - use crate::debug::{DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugVariableUpdate}; - use crate::span; + use silverscript_lang::ast::{BinaryOp, Expr, ExprKind}; + use silverscript_lang::debug_info::{DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugVariableUpdate}; + use silverscript_lang::span; fn make_session( params: Vec, diff --git a/silverscript-lang/tests/debug_session_tests.rs b/debugger/session/tests/debug_session_tests.rs similarity index 98% rename from silverscript-lang/tests/debug_session_tests.rs rename to debugger/session/tests/debug_session_tests.rs index ae15adc2..eb358638 100644 --- a/silverscript-lang/tests/debug_session_tests.rs +++ b/debugger/session/tests/debug_session_tests.rs @@ -14,10 +14,10 @@ use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::opcodes::codes::OpTrue; use kaspa_txscript::{EngineCtx, EngineFlags}; +use debugger_session::session::{DebugSession, ShadowTxContext}; use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; -use silverscript_lang::debug::MappingKind; -use silverscript_lang::debug::session::{DebugSession, ShadowTxContext}; +use silverscript_lang::debug_info::MappingKind; fn example_contract_path() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -60,7 +60,7 @@ where let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); let flags = EngineFlags { covenants_enabled: true }; - let engine = silverscript_lang::debug::session::DebugEngine::new(ctx, flags); + let engine = debugger_session::session::DebugEngine::new(ctx, flags); let entry = compiled .abi @@ -759,7 +759,7 @@ contract CovLocal() { let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values).with_covenants_ctx(&cov_ctx); let input_ref = &tx.inputs[0]; let utxo_ref = populated_tx.utxo(0).ok_or("missing utxo for input 0")?; - let engine = silverscript_lang::debug::session::DebugEngine::from_transaction_input( + let engine = debugger_session::session::DebugEngine::from_transaction_input( &populated_tx, input_ref, 0, diff --git a/debugger/session/tests/examples/debug_messages.sil b/debugger/session/tests/examples/debug_messages.sil new file mode 100644 index 00000000..35407568 --- /dev/null +++ b/debugger/session/tests/examples/debug_messages.sil @@ -0,0 +1,8 @@ +pragma silverscript ^0.1.0; + +contract DebugMessages() { + entrypoint function spend(int value) { + require(value == 1, "Wrong value passed"); + require(value + 1 == 2, "Sum doesn't work"); + } +} diff --git a/debugger/session/tests/examples/if_statement.sil b/debugger/session/tests/examples/if_statement.sil new file mode 100644 index 00000000..8331be55 --- /dev/null +++ b/debugger/session/tests/examples/if_statement.sil @@ -0,0 +1,17 @@ +pragma silverscript ^0.1.0; + +contract IfStatement(int x, int y) { + entrypoint function hello(int a, int b) { + int d = a + b; + d = d - a; + if (d == x - 2) { + int c = d + b; + d = a + c; + require(c > d); + } else { + require(d == a); + } + d = d + a; + require(d == y); + } +} diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 72e52bca..1e12962d 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -9,8 +9,8 @@ use crate::ast::{ StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, parse_contract_ast, parse_type_ref, }; -use crate::debug::labels::synthetic; -use crate::debug::{DebugInfo, SourceSpan}; +use crate::debug_info::labels::synthetic; +use crate::debug_info::{DebugInfo, SourceSpan}; pub use crate::errors::{CompilerError, ErrorSpan}; use crate::span; diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index e6ba2c6f..eb08ffd0 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::script_builder::ScriptBuilder; use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; -use crate::debug::{ +use crate::debug_info::{ DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugVariableUpdate, SourceSpan, }; diff --git a/silverscript-lang/src/debug.rs b/silverscript-lang/src/debug_info.rs similarity index 99% rename from silverscript-lang/src/debug.rs rename to silverscript-lang/src/debug_info.rs index 1514ed93..16febeac 100644 --- a/silverscript-lang/src/debug.rs +++ b/silverscript-lang/src/debug_info.rs @@ -2,9 +2,6 @@ use crate::ast::Expr; use crate::span; use serde::{Deserialize, Serialize}; -pub mod presentation; -pub mod session; - #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct SourceSpan { pub line: u32, diff --git a/silverscript-lang/src/lib.rs b/silverscript-lang/src/lib.rs index cdd68dfb..d1423e0e 100644 --- a/silverscript-lang/src/lib.rs +++ b/silverscript-lang/src/lib.rs @@ -1,6 +1,6 @@ pub mod ast; pub mod compiler; -pub mod debug; +pub mod debug_info; pub mod diagnostic; pub mod errors; pub mod parser; From 9b738c6d37debe83e0e861f75d47cbd74acfaf1a Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:45:42 +0200 Subject: [PATCH 29/41] enhance expression resolution --- silverscript-lang/src/compiler.rs | 221 +++++++++--------------------- 1 file changed, 64 insertions(+), 157 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 9b9e28ce..debd0270 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2210,104 +2210,117 @@ fn resolve_expr<'i>( expr: Expr<'i>, env: &HashMap>, visiting: &mut HashSet, +) -> Result, CompilerError> { + resolve_expr_with_inline_synthetics(expr, env, visiting, false) +} + +/// Shared expression resolver used by both compile-time resolution and +/// debugger placeholder expansion. +/// +/// - `expand_inline_synthetics = false`: preserve `__arg_*` placeholders. +/// - `expand_inline_synthetics = true`: resolve only `__arg_*` placeholders. +fn resolve_expr_with_inline_synthetics<'i>( + expr: Expr<'i>, + env: &HashMap>, + visiting: &mut HashSet, + expand_inline_synthetics: bool, ) -> Result, CompilerError> { let Expr { kind, span } = expr; match kind { ExprKind::Identifier(name) => { - if name.starts_with("__arg_") { + let is_inline_synthetic = name.starts_with("__arg_"); + if !expand_inline_synthetics && is_inline_synthetic { return Ok(Expr::new(ExprKind::Identifier(name), span)); } - if let Some(value) = env.get(&name) { + if expand_inline_synthetics && !is_inline_synthetic { + return Ok(Expr::new(ExprKind::Identifier(name), span)); + } + if let Some(value) = env.get(&name).cloned() { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr(value.clone(), env, visiting)?; + let resolved = resolve_expr_with_inline_synthetics(value, env, visiting, expand_inline_synthetics)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::new(ExprKind::Identifier(name), span)) } } - ExprKind::Unary { op, expr } => { - Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }, span)) + other => rewrite_expr_children(Expr::new(other, span), |child| { + resolve_expr_with_inline_synthetics(child, env, visiting, expand_inline_synthetics) + }), + } +} + +fn rewrite_expr_children<'i>( + expr: Expr<'i>, + mut recurse: impl FnMut(Expr<'i>) -> Result, CompilerError>, +) -> Result, CompilerError> { + let Expr { kind, span } = expr; + match kind { + ExprKind::Unary { op, expr } => Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(recurse(*expr)?) }, span)), + ExprKind::Binary { op, left, right } => { + Ok(Expr::new(ExprKind::Binary { op, left: Box::new(recurse(*left)?), right: Box::new(recurse(*right)?) }, span)) } - ExprKind::Binary { op, left, right } => Ok(Expr::new( - ExprKind::Binary { - op, - left: Box::new(resolve_expr(*left, env, visiting)?), - right: Box::new(resolve_expr(*right, env, visiting)?), - }, - span, - )), ExprKind::IfElse { condition, then_expr, else_expr } => Ok(Expr::new( ExprKind::IfElse { - condition: Box::new(resolve_expr(*condition, env, visiting)?), - then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), - else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), + condition: Box::new(recurse(*condition)?), + then_expr: Box::new(recurse(*then_expr)?), + else_expr: Box::new(recurse(*else_expr)?), }, span, )), ExprKind::Array(values) => { - let mut resolved = Vec::with_capacity(values.len()); + let mut rewritten = Vec::with_capacity(values.len()); for value in values { - resolved.push(resolve_expr(value, env, visiting)?); + rewritten.push(recurse(value)?); } - Ok(Expr::new(ExprKind::Array(resolved), span)) + Ok(Expr::new(ExprKind::Array(rewritten), span)) } ExprKind::StateObject(fields) => { - let mut resolved_fields = Vec::with_capacity(fields.len()); + let mut rewritten = Vec::with_capacity(fields.len()); for field in fields { - resolved_fields.push(StateFieldExpr { + rewritten.push(StateFieldExpr { name: field.name, - expr: resolve_expr(field.expr, env, visiting)?, + expr: recurse(field.expr)?, span: field.span, name_span: field.name_span, }); } - Ok(Expr::new(ExprKind::StateObject(resolved_fields), span)) + Ok(Expr::new(ExprKind::StateObject(rewritten), span)) } ExprKind::Call { name, args, name_span } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(recurse(arg)?); } - Ok(Expr::new(ExprKind::Call { name, args: resolved, name_span }, span)) + Ok(Expr::new(ExprKind::Call { name, args: rewritten, name_span }, span)) } ExprKind::New { name, args, name_span } => { - let mut resolved = Vec::with_capacity(args.len()); + let mut rewritten = Vec::with_capacity(args.len()); for arg in args { - resolved.push(resolve_expr(arg, env, visiting)?); + rewritten.push(recurse(arg)?); } - Ok(Expr::new(ExprKind::New { name, args: resolved, name_span }, span)) + Ok(Expr::new(ExprKind::New { name, args: rewritten, name_span }, span)) } ExprKind::Split { source, index, part, span: split_span } => Ok(Expr::new( - ExprKind::Split { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), - part, - span: split_span, - }, - span, - )), - ExprKind::ArrayIndex { source, index } => Ok(Expr::new( - ExprKind::ArrayIndex { - source: Box::new(resolve_expr(*source, env, visiting)?), - index: Box::new(resolve_expr(*index, env, visiting)?), - }, + ExprKind::Split { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?), part, span: split_span }, span, )), + ExprKind::ArrayIndex { source, index } => { + Ok(Expr::new(ExprKind::ArrayIndex { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?) }, span)) + } ExprKind::Introspection { kind, index, field_span } => { - Ok(Expr::new(ExprKind::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?), field_span }, span)) + Ok(Expr::new(ExprKind::Introspection { kind, index: Box::new(recurse(*index)?), field_span }, span)) + } + ExprKind::UnarySuffix { source, kind, span: suffix_span } => { + Ok(Expr::new(ExprKind::UnarySuffix { source: Box::new(recurse(*source)?), kind, span: suffix_span }, span)) } - ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( - ExprKind::UnarySuffix { source: Box::new(resolve_expr(*source, env, visiting)?), kind, span: suffix_span }, - span, - )), ExprKind::Slice { source, start, end, span: slice_span } => Ok(Expr::new( ExprKind::Slice { - source: Box::new(resolve_expr(*source, env, visiting)?), - start: Box::new(resolve_expr(*start, env, visiting)?), - end: Box::new(resolve_expr(*end, env, visiting)?), + source: Box::new(recurse(*source)?), + start: Box::new(recurse(*start)?), + end: Box::new(recurse(*end)?), span: slice_span, }, span, @@ -3897,113 +3910,7 @@ fn expand_inline_arg_placeholders<'i>( env: &HashMap>, visiting: &mut HashSet, ) -> Result, CompilerError> { - let Expr { kind, span } = expr; - match kind { - ExprKind::Identifier(name) => { - if !name.starts_with("__arg_") { - return Ok(Expr::new(ExprKind::Identifier(name), span)); - } - let Some(value) = env.get(&name).cloned() else { - return Ok(Expr::new(ExprKind::Identifier(name), span)); - }; - if !visiting.insert(name.clone()) { - return Err(CompilerError::CyclicIdentifier(name)); - } - let expanded = expand_inline_arg_placeholders(value, env, visiting)?; - visiting.remove(&name); - Ok(expanded) - } - ExprKind::Unary { op, expr } => { - Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(expand_inline_arg_placeholders(*expr, env, visiting)?) }, span)) - } - ExprKind::Binary { op, left, right } => Ok(Expr::new( - ExprKind::Binary { - op, - left: Box::new(expand_inline_arg_placeholders(*left, env, visiting)?), - right: Box::new(expand_inline_arg_placeholders(*right, env, visiting)?), - }, - span, - )), - ExprKind::IfElse { condition, then_expr, else_expr } => Ok(Expr::new( - ExprKind::IfElse { - condition: Box::new(expand_inline_arg_placeholders(*condition, env, visiting)?), - then_expr: Box::new(expand_inline_arg_placeholders(*then_expr, env, visiting)?), - else_expr: Box::new(expand_inline_arg_placeholders(*else_expr, env, visiting)?), - }, - span, - )), - ExprKind::Array(values) => { - let mut rewritten = Vec::with_capacity(values.len()); - for value in values { - rewritten.push(expand_inline_arg_placeholders(value, env, visiting)?); - } - Ok(Expr::new(ExprKind::Array(rewritten), span)) - } - ExprKind::StateObject(fields) => { - let mut rewritten = Vec::with_capacity(fields.len()); - for field in fields { - rewritten.push(StateFieldExpr { - name: field.name, - expr: expand_inline_arg_placeholders(field.expr, env, visiting)?, - span: field.span, - name_span: field.name_span, - }); - } - Ok(Expr::new(ExprKind::StateObject(rewritten), span)) - } - ExprKind::Call { name, args, name_span } => { - let mut rewritten = Vec::with_capacity(args.len()); - for arg in args { - rewritten.push(expand_inline_arg_placeholders(arg, env, visiting)?); - } - Ok(Expr::new(ExprKind::Call { name, args: rewritten, name_span }, span)) - } - ExprKind::New { name, args, name_span } => { - let mut rewritten = Vec::with_capacity(args.len()); - for arg in args { - rewritten.push(expand_inline_arg_placeholders(arg, env, visiting)?); - } - Ok(Expr::new(ExprKind::New { name, args: rewritten, name_span }, span)) - } - ExprKind::Split { source, index, part, span: split_span } => Ok(Expr::new( - ExprKind::Split { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), - part, - span: split_span, - }, - span, - )), - ExprKind::ArrayIndex { source, index } => Ok(Expr::new( - ExprKind::ArrayIndex { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), - }, - span, - )), - ExprKind::Introspection { kind, index, field_span } => Ok(Expr::new( - ExprKind::Introspection { kind, index: Box::new(expand_inline_arg_placeholders(*index, env, visiting)?), field_span }, - span, - )), - ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( - ExprKind::UnarySuffix { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - kind, - span: suffix_span, - }, - span, - )), - ExprKind::Slice { source, start, end, span: slice_span } => Ok(Expr::new( - ExprKind::Slice { - source: Box::new(expand_inline_arg_placeholders(*source, env, visiting)?), - start: Box::new(expand_inline_arg_placeholders(*start, env, visiting)?), - end: Box::new(expand_inline_arg_placeholders(*end, env, visiting)?), - span: slice_span, - }, - span, - )), - other => Ok(Expr::new(other, span)), - } + resolve_expr_with_inline_synthetics(expr, env, visiting, true) } #[cfg(test)] From 04130bf9d5c621dbaa06a631aa39f751254ce374 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:04:54 +0200 Subject: [PATCH 30/41] Final touches , readme, tests --- README.md | 6 ++-- debugger/cli/tests/cli_tests.rs | 28 +++++++++++++------ debugger/cli/tests/if_statement.sil | 17 ----------- debugger/session/tests/debug_session_tests.rs | 28 ++++++++++++------- .../session/tests/examples/debug_messages.sil | 8 ------ .../session/tests/examples/if_statement.sil | 17 ----------- 6 files changed, 42 insertions(+), 62 deletions(-) delete mode 100644 debugger/cli/tests/if_statement.sil delete mode 100644 debugger/session/tests/examples/debug_messages.sil delete mode 100644 debugger/session/tests/examples/if_statement.sil diff --git a/README.md b/README.md index ee887248..e69fd745 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ cargo test -p silverscript-lang The workspace includes a source-level debugger for stepping through scripts: ```bash -cargo run -p silverscript-lang --bin sil-debug -- \ +cargo run -p cli-debugger -- \ silverscript-lang/tests/examples/if_statement.sil \ --function hello \ --ctor-arg 3 --ctor-arg 10 \ @@ -31,7 +31,9 @@ cargo run -p silverscript-lang --bin sil-debug -- \ ## Layout -- `silverscript-lang/` – compiler, parser, debugger, and tests +- `silverscript-lang/` – compiler, parser, and tests +- `debugger/session/` – `DebugSession` runtime (stepping, variable inspection) +- `debugger/cli/` – `sil-debug` CLI REPL - `silverscript-lang/tests/examples/` – example contracts (`.sil` files) ## Documentation diff --git a/debugger/cli/tests/cli_tests.rs b/debugger/cli/tests/cli_tests.rs index 31d76882..e7f14717 100644 --- a/debugger/cli/tests/cli_tests.rs +++ b/debugger/cli/tests/cli_tests.rs @@ -1,16 +1,28 @@ use std::io::Write; -use std::path::PathBuf; use std::process::{Command, Stdio}; -fn example_contract_path() -> PathBuf { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir.join("tests/if_statement.sil") -} - #[test] fn cli_debugger_repl_all_commands_smoke() { - let contract_path = example_contract_path(); - assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); + let tmp = std::env::temp_dir().join("cli_test_if_statement.sil"); + std::fs::write(&tmp, r#"pragma silverscript ^0.1.0; + +contract IfStatement(int x, int y) { + entrypoint function hello(int a, int b) { + int d = a + b; + d = d - a; + if (d == x - 2) { + int c = d + b; + d = a + c; + require(c > d); + } else { + require(d == a); + } + d = d + a; + require(d == y); + } +} +"#).expect("write temp contract"); + let contract_path = &tmp; let mut child = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) .arg(contract_path) diff --git a/debugger/cli/tests/if_statement.sil b/debugger/cli/tests/if_statement.sil deleted file mode 100644 index 8331be55..00000000 --- a/debugger/cli/tests/if_statement.sil +++ /dev/null @@ -1,17 +0,0 @@ -pragma silverscript ^0.1.0; - -contract IfStatement(int x, int y) { - entrypoint function hello(int a, int b) { - int d = a + b; - d = d - a; - if (d == x - 2) { - int c = d + b; - d = a + c; - require(c > d); - } else { - require(d == a); - } - d = d + a; - require(d == y); - } -} diff --git a/debugger/session/tests/debug_session_tests.rs b/debugger/session/tests/debug_session_tests.rs index eb358638..c262cdf1 100644 --- a/debugger/session/tests/debug_session_tests.rs +++ b/debugger/session/tests/debug_session_tests.rs @@ -1,7 +1,5 @@ use std::collections::HashSet; use std::error::Error; -use std::fs; -use std::path::PathBuf; use kaspa_consensus_core::Hash; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; @@ -19,21 +17,31 @@ use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; use silverscript_lang::debug_info::MappingKind; -fn example_contract_path() -> PathBuf { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - manifest_dir.join("tests/examples/if_statement.sil") +const IF_STATEMENT_CONTRACT: &str = r#"pragma silverscript ^0.1.0; + +contract IfStatement(int x, int y) { + entrypoint function hello(int a, int b) { + int d = a + b; + d = d - a; + if (d == x - 2) { + int c = d + b; + d = a + c; + require(c > d); + } else { + require(d == a); + } + d = d + a; + require(d == y); + } } +"#; // Convenience harness for the canonical example contract used by baseline session tests. fn with_session(mut f: F) -> Result<(), Box> where F: FnMut(&mut DebugSession<'_, '_>) -> Result<(), Box>, { - let contract_path = example_contract_path(); - assert!(contract_path.exists(), "example contract not found: {}", contract_path.display()); - - let source = fs::read_to_string(&contract_path)?; - with_session_for_source(&source, vec![Expr::int(3), Expr::int(10)], "hello", vec![Expr::int(5), Expr::int(5)], &mut f) + with_session_for_source(IF_STATEMENT_CONTRACT, vec![Expr::int(3), Expr::int(10)], "hello", vec![Expr::int(5), Expr::int(5)], &mut f) } // Generic harness that compiles a contract and boots a debugger session for a selected function call. diff --git a/debugger/session/tests/examples/debug_messages.sil b/debugger/session/tests/examples/debug_messages.sil deleted file mode 100644 index 35407568..00000000 --- a/debugger/session/tests/examples/debug_messages.sil +++ /dev/null @@ -1,8 +0,0 @@ -pragma silverscript ^0.1.0; - -contract DebugMessages() { - entrypoint function spend(int value) { - require(value == 1, "Wrong value passed"); - require(value + 1 == 2, "Sum doesn't work"); - } -} diff --git a/debugger/session/tests/examples/if_statement.sil b/debugger/session/tests/examples/if_statement.sil deleted file mode 100644 index 8331be55..00000000 --- a/debugger/session/tests/examples/if_statement.sil +++ /dev/null @@ -1,17 +0,0 @@ -pragma silverscript ^0.1.0; - -contract IfStatement(int x, int y) { - entrypoint function hello(int a, int b) { - int d = a + b; - d = d - a; - if (d == x - 2) { - int c = d + b; - d = a + c; - require(c > d); - } else { - require(d == a); - } - d = d + a; - require(d == y); - } -} From 4d1d598999a0fcda0a6ac81016a6cbaf1a496fee Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:09:07 +0200 Subject: [PATCH 31/41] fmt --- debugger/cli/tests/cli_tests.rs | 8 ++++++-- debugger/session/tests/debug_session_tests.rs | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/debugger/cli/tests/cli_tests.rs b/debugger/cli/tests/cli_tests.rs index e7f14717..fced76fd 100644 --- a/debugger/cli/tests/cli_tests.rs +++ b/debugger/cli/tests/cli_tests.rs @@ -4,7 +4,9 @@ use std::process::{Command, Stdio}; #[test] fn cli_debugger_repl_all_commands_smoke() { let tmp = std::env::temp_dir().join("cli_test_if_statement.sil"); - std::fs::write(&tmp, r#"pragma silverscript ^0.1.0; + std::fs::write( + &tmp, + r#"pragma silverscript ^0.1.0; contract IfStatement(int x, int y) { entrypoint function hello(int a, int b) { @@ -21,7 +23,9 @@ contract IfStatement(int x, int y) { require(d == y); } } -"#).expect("write temp contract"); +"#, + ) + .expect("write temp contract"); let contract_path = &tmp; let mut child = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) diff --git a/debugger/session/tests/debug_session_tests.rs b/debugger/session/tests/debug_session_tests.rs index c262cdf1..e822ff1b 100644 --- a/debugger/session/tests/debug_session_tests.rs +++ b/debugger/session/tests/debug_session_tests.rs @@ -41,7 +41,13 @@ fn with_session(mut f: F) -> Result<(), Box> where F: FnMut(&mut DebugSession<'_, '_>) -> Result<(), Box>, { - with_session_for_source(IF_STATEMENT_CONTRACT, vec![Expr::int(3), Expr::int(10)], "hello", vec![Expr::int(5), Expr::int(5)], &mut f) + with_session_for_source( + IF_STATEMENT_CONTRACT, + vec![Expr::int(3), Expr::int(10)], + "hello", + vec![Expr::int(5), Expr::int(5)], + &mut f, + ) } // Generic harness that compiles a contract and boots a debugger session for a selected function call. From e2a13fd4a872ecc11928b8b4f4da5ff9b8f60287 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:17:06 +0200 Subject: [PATCH 32/41] Fix compiler compatibility after merging origin/master --- silverscript-lang/src/compiler.rs | 17 ++++++------- silverscript-lang/tests/compiler_tests.rs | 30 ++++++++++++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 035b1634..debd0270 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2,7 +2,6 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::opcodes::codes::*; use kaspa_txscript::script_builder::ScriptBuilder; -use kaspa_txscript::serialize_i64; use serde::{Deserialize, Serialize}; use crate::ast::{ @@ -247,9 +246,8 @@ fn compile_contract_fields<'i>( let ExprKind::Int(value) = &resolved.kind else { return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); }; - let serialized = serialize_i64(*value, Some(8usize)) - .map_err(|err| CompilerError::Unsupported(format!("failed to serialize int literal {}: {err}", value)))?; - builder.add_data(&serialized)?; + builder.add_data(&value.to_le_bytes())?; + builder.add_op(OpBin2Num)?; } else { compile_expr( &resolved, @@ -794,8 +792,7 @@ fn encode_fixed_size_value<'i>(value: &Expr<'i>, type_name: &str) -> Result *number, _ => return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())), }; - serialize_i64(number, Some(8usize)) - .map_err(|err| CompilerError::Unsupported(format!("failed to serialize int literal {}: {err}", number))) + Ok(number.to_le_bytes().to_vec()) } "bool" => { let ExprKind::Bool(flag) = &value.kind else { @@ -1478,9 +1475,7 @@ fn encoded_field_chunk_size<'i>( contract_constants: &HashMap>, ) -> Result { if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { - // Int fields are encoded as PUSHDATA8-prefixed script numbers: - // 1-byte push opcode (0x08) + 8-byte payload from serialize_i64(..., Some(8)). - return Ok(9); + return Ok(10); } if field.type_ref.base != TypeBase::Byte { @@ -1667,6 +1662,10 @@ fn compile_validate_output_state_statement( builder.add_op(OpSwap)?; builder.add_op(OpCat)?; stack_depth -= 1; + builder.add_data(&[OpBin2Num])?; + stack_depth += 1; + builder.add_op(OpCat)?; + stack_depth -= 1; continue; } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 96f2302c..e9f26528 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -1562,6 +1562,8 @@ fn compiles_contract_fields_as_script_prolog() { let expected = ScriptBuilder::new() .add_data(&5i64.to_le_bytes()) .unwrap() + .add_op(OpBin2Num) + .unwrap() .add_data(&[0x12, 0x34]) .unwrap() .add_i64(1) @@ -1620,9 +1622,11 @@ fn compiles_validate_output_state_to_expected_script() { let compiled = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); let expected = ScriptBuilder::new() - // as fixed-size int field encoding: <8-byte little-endian> + // as fixed-size int field encoding: <8-byte little-endian> .add_data(&5i64.to_le_bytes()) .unwrap() + .add_op(OpBin2Num) + .unwrap() // .add_data(&[1u8, 2u8]) .unwrap() @@ -1641,7 +1645,7 @@ fn compiles_validate_output_state_to_expected_script() { .add_op(OpAdd) .unwrap() - // ---- Convert x+1 to fixed-size int field chunk: <0x08><8-byte payload> ---- + // ---- Convert x+1 to fixed-size int field chunk: <0x08><8-byte payload> ---- // convert numeric value to 8-byte payload .add_i64(8) .unwrap() @@ -1654,6 +1658,11 @@ fn compiles_validate_output_state_to_expected_script() { .unwrap() .add_op(OpCat) .unwrap() + // append OpBin2Num opcode byte + .add_data(&[OpBin2Num]) + .unwrap() + .add_op(OpCat) + .unwrap() // ---- Build new_state.y pushdata chunk ---- // raw y bytes @@ -1688,8 +1697,8 @@ fn compiles_validate_output_state_to_expected_script() { // sigscript_len - script_size => bytes before current redeem .add_op(OpSub) .unwrap() - // add fixed current-state field prefix length: len() = 12 - .add_i64(12) + // add fixed current-state field prefix length: len() = 13 + .add_i64(13) .unwrap() // start offset of REST_OF_SCRIPT inside sigscript .add_op(OpAdd) @@ -1822,6 +1831,9 @@ fn compiles_read_input_state_to_expected_script() { // push x payload (8-byte LE) .add_data(&5i64.to_le_bytes()) .unwrap() + // decode x to numeric form + .add_op(OpBin2Num) + .unwrap() // push y payload bytes .add_data(&[1u8, 2u8]) .unwrap() @@ -1905,10 +1917,10 @@ fn compiles_read_input_state_to_expected_script() { // base = sig_len - script_size .add_op(OpSub) .unwrap() - // skip x encoded chunk (9 bytes) + y pushdata prefix (1 byte) - .add_i64(10) + // skip x encoded chunk (10 bytes) + y pushdata prefix (1 byte) + .add_i64(11) .unwrap() - // start_y = base + 10 + // start_y = base + 11 .add_op(OpAdd) .unwrap() @@ -1925,9 +1937,9 @@ fn compiles_read_input_state_to_expected_script() { .add_op(OpSub) .unwrap() // skip x chunk + y prefix - .add_i64(10) + .add_i64(11) .unwrap() - // start_y = base + 10 + // start_y = base + 11 .add_op(OpAdd) .unwrap() // y payload length From bca54f3aab3f570525b407c1d91ffd9b53e828d9 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:40:59 +0200 Subject: [PATCH 33/41] =?UTF-8?q?fix:=20align=20with=20upstream=20PR=20#44?= =?UTF-8?q?=20=E2=80=94=20restore=20serialize=5Fi64=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bad merge resolution had reverted PR #44 changes: - Restore serialize_i64 instead of to_le_bytes+OpBin2Num for int fields - Restore Ok(9) field chunk size (was incorrectly Ok(10)) - Remove spurious OpBin2Num injection in validate_output_state - Update test expectations to match corrected encoding Co-Authored-By: Claude Opus 4.6 --- silverscript-lang/src/compiler.rs | 17 +++++++------ silverscript-lang/tests/compiler_tests.rs | 31 +++++++---------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index debd0270..035b1634 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::opcodes::codes::*; use kaspa_txscript::script_builder::ScriptBuilder; +use kaspa_txscript::serialize_i64; use serde::{Deserialize, Serialize}; use crate::ast::{ @@ -246,8 +247,9 @@ fn compile_contract_fields<'i>( let ExprKind::Int(value) = &resolved.kind else { return Err(CompilerError::Unsupported(format!("contract field '{}' expects compile-time int value", field.name))); }; - builder.add_data(&value.to_le_bytes())?; - builder.add_op(OpBin2Num)?; + let serialized = serialize_i64(*value, Some(8usize)) + .map_err(|err| CompilerError::Unsupported(format!("failed to serialize int literal {}: {err}", value)))?; + builder.add_data(&serialized)?; } else { compile_expr( &resolved, @@ -792,7 +794,8 @@ fn encode_fixed_size_value<'i>(value: &Expr<'i>, type_name: &str) -> Result *number, _ => return Err(CompilerError::Unsupported("array literal element type mismatch".to_string())), }; - Ok(number.to_le_bytes().to_vec()) + serialize_i64(number, Some(8usize)) + .map_err(|err| CompilerError::Unsupported(format!("failed to serialize int literal {}: {err}", number))) } "bool" => { let ExprKind::Bool(flag) = &value.kind else { @@ -1475,7 +1478,9 @@ fn encoded_field_chunk_size<'i>( contract_constants: &HashMap>, ) -> Result { if field.type_ref.array_dims.is_empty() && field.type_ref.base == TypeBase::Int { - return Ok(10); + // Int fields are encoded as PUSHDATA8-prefixed script numbers: + // 1-byte push opcode (0x08) + 8-byte payload from serialize_i64(..., Some(8)). + return Ok(9); } if field.type_ref.base != TypeBase::Byte { @@ -1662,10 +1667,6 @@ fn compile_validate_output_state_statement( builder.add_op(OpSwap)?; builder.add_op(OpCat)?; stack_depth -= 1; - builder.add_data(&[OpBin2Num])?; - stack_depth += 1; - builder.add_op(OpCat)?; - stack_depth -= 1; continue; } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index e9f26528..61d84a9d 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -1562,8 +1562,6 @@ fn compiles_contract_fields_as_script_prolog() { let expected = ScriptBuilder::new() .add_data(&5i64.to_le_bytes()) .unwrap() - .add_op(OpBin2Num) - .unwrap() .add_data(&[0x12, 0x34]) .unwrap() .add_i64(1) @@ -1622,11 +1620,9 @@ fn compiles_validate_output_state_to_expected_script() { let compiled = compile_contract(source, &[5.into(), vec![1u8, 2u8].into()], CompileOptions::default()).expect("compile succeeds"); let expected = ScriptBuilder::new() - // as fixed-size int field encoding: <8-byte little-endian> + // as fixed-size int field encoding: <8-byte little-endian> .add_data(&5i64.to_le_bytes()) .unwrap() - .add_op(OpBin2Num) - .unwrap() // .add_data(&[1u8, 2u8]) .unwrap() @@ -1645,7 +1641,7 @@ fn compiles_validate_output_state_to_expected_script() { .add_op(OpAdd) .unwrap() - // ---- Convert x+1 to fixed-size int field chunk: <0x08><8-byte payload> ---- + // ---- Convert x+1 to fixed-size int field chunk: <0x08><8-byte payload> ---- // convert numeric value to 8-byte payload .add_i64(8) .unwrap() @@ -1658,12 +1654,6 @@ fn compiles_validate_output_state_to_expected_script() { .unwrap() .add_op(OpCat) .unwrap() - // append OpBin2Num opcode byte - .add_data(&[OpBin2Num]) - .unwrap() - .add_op(OpCat) - .unwrap() - // ---- Build new_state.y pushdata chunk ---- // raw y bytes .add_data(&[0x34, 0x12]) @@ -1697,8 +1687,8 @@ fn compiles_validate_output_state_to_expected_script() { // sigscript_len - script_size => bytes before current redeem .add_op(OpSub) .unwrap() - // add fixed current-state field prefix length: len() = 13 - .add_i64(13) + // add fixed current-state field prefix length: len() = 12 + .add_i64(12) .unwrap() // start offset of REST_OF_SCRIPT inside sigscript .add_op(OpAdd) @@ -1831,9 +1821,6 @@ fn compiles_read_input_state_to_expected_script() { // push x payload (8-byte LE) .add_data(&5i64.to_le_bytes()) .unwrap() - // decode x to numeric form - .add_op(OpBin2Num) - .unwrap() // push y payload bytes .add_data(&[1u8, 2u8]) .unwrap() @@ -1917,10 +1904,10 @@ fn compiles_read_input_state_to_expected_script() { // base = sig_len - script_size .add_op(OpSub) .unwrap() - // skip x encoded chunk (10 bytes) + y pushdata prefix (1 byte) - .add_i64(11) + // skip x encoded chunk (9 bytes) + y pushdata prefix (1 byte) + .add_i64(10) .unwrap() - // start_y = base + 11 + // start_y = base + 10 .add_op(OpAdd) .unwrap() @@ -1937,9 +1924,9 @@ fn compiles_read_input_state_to_expected_script() { .add_op(OpSub) .unwrap() // skip x chunk + y prefix - .add_i64(11) + .add_i64(10) .unwrap() - // start_y = base + 11 + // start_y = base + 10 .add_op(OpAdd) .unwrap() // y payload length From 42bad3af03bcb4d3576efe56fec587ef8279b228 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:37:19 +0200 Subject: [PATCH 34/41] PR cleanup - remove redundent logic & structs --- Cargo.lock | 22 +- debugger/session/src/lib.rs | 1 + debugger/session/src/presentation.rs | 26 +- debugger/session/src/session.rs | 90 +-- debugger/session/src/util.rs | 27 + debugger/session/tests/debug_session_tests.rs | 2 +- silverscript-lang/src/compiler.rs | 142 ++-- .../src/compiler/debug_recording.rs | 700 ++++++++++++------ silverscript-lang/src/debug_info.rs | 91 +-- silverscript-lang/src/span.rs | 19 + silverscript-lang/tests/compiler_tests.rs | 32 + 11 files changed, 668 insertions(+), 484 deletions(-) create mode 100644 debugger/session/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 7f0ef3d8..1d6475e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,7 +1614,7 @@ dependencies = [ [[package]] name = "kaspa-addresses" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "borsh", "js-sys", @@ -1629,7 +1629,7 @@ dependencies = [ [[package]] name = "kaspa-consensus-core" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "arc-swap", "async-trait", @@ -1666,7 +1666,7 @@ dependencies = [ [[package]] name = "kaspa-core" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "anyhow", "cfg-if", @@ -1697,7 +1697,7 @@ dependencies = [ [[package]] name = "kaspa-hashes" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "blake2b_simd", "blake3", @@ -1717,7 +1717,7 @@ dependencies = [ [[package]] name = "kaspa-math" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "borsh", "faster-hex 0.9.0", @@ -1737,7 +1737,7 @@ dependencies = [ [[package]] name = "kaspa-merkle" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "kaspa-hashes", ] @@ -1745,7 +1745,7 @@ dependencies = [ [[package]] name = "kaspa-muhash" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "kaspa-hashes", "kaspa-math", @@ -1756,7 +1756,7 @@ dependencies = [ [[package]] name = "kaspa-txscript" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "ark-bn254", "ark-ec", @@ -1802,7 +1802,7 @@ dependencies = [ [[package]] name = "kaspa-txscript-errors" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "borsh", "kaspa-hashes", @@ -1813,7 +1813,7 @@ dependencies = [ [[package]] name = "kaspa-utils" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "arc-swap", "async-channel 2.5.0", @@ -1843,7 +1843,7 @@ dependencies = [ [[package]] name = "kaspa-wasm-core" version = "1.1.0-rc.3" -source = "git+https://github.com/kaspanet/rusty-kaspa?branch=covpp-reset1#a666855272a56402a65731e91488d92d139b62c4" +source = "git+https://github.com/kaspanet/rusty-kaspa?branch=tn12#c6819f3b3fe37f712acd88dafbb583297e2ef938" dependencies = [ "faster-hex 0.9.0", "hexplay", diff --git a/debugger/session/src/lib.rs b/debugger/session/src/lib.rs index 0d9cebd9..cfeaf3a4 100644 --- a/debugger/session/src/lib.rs +++ b/debugger/session/src/lib.rs @@ -1,2 +1,3 @@ pub mod presentation; pub mod session; +pub mod util; diff --git a/debugger/session/src/presentation.rs b/debugger/session/src/presentation.rs index 65ce7100..b56e2529 100644 --- a/debugger/session/src/presentation.rs +++ b/debugger/session/src/presentation.rs @@ -1,6 +1,7 @@ use silverscript_lang::debug_info::SourceSpan; use crate::session::DebugValue; +use crate::util::{decode_i64, encode_hex}; #[derive(Debug, Clone)] pub struct SourceContextLine { @@ -110,28 +111,3 @@ fn array_element_size(element_type: &str) -> Option { other => other.strip_prefix("bytes").and_then(|v| v.parse::().ok()), } } - -fn decode_i64(bytes: &[u8]) -> Result { - if bytes.is_empty() { - return Ok(0); - } - if bytes.len() > 8 { - return Err("numeric value is longer than 8 bytes".to_string()); - } - let msb = bytes[bytes.len() - 1]; - let sign = 1 - 2 * ((msb >> 7) as i64); - let first_byte = (msb & 0x7f) as i64; - let mut value = first_byte; - for byte in bytes[..bytes.len() - 1].iter().rev() { - value = (value << 8) + (*byte as i64); - } - Ok(value * sign) -} - -fn encode_hex(bytes: &[u8]) -> String { - let mut out = vec![0u8; bytes.len() * 2]; - if faster_hex::hex_encode(bytes, &mut out).is_err() { - return String::new(); - } - String::from_utf8(out).unwrap_or_default() -} diff --git a/debugger/session/src/session.rs b/debugger/session/src/session.rs index 589b158b..aa8ad8b8 100644 --- a/debugger/session/src/session.rs +++ b/debugger/session/src/session.rs @@ -6,7 +6,6 @@ use kaspa_txscript::caches::Cache; use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; -use serde::{Deserialize, Serialize}; use silverscript_lang::ast::{Expr, ExprKind}; use silverscript_lang::compiler::compile_debug_expr; @@ -16,6 +15,7 @@ use silverscript_lang::debug_info::{ pub use crate::presentation::{SourceContext, SourceContextLine}; use crate::presentation::{build_source_context, format_value as format_debug_value}; +use crate::util::{decode_i64, encode_hex}; pub type DebugTx<'a> = PopulatedTransaction<'a>; pub type DebugReused = SigHashReusedValuesUnsync; @@ -76,20 +76,6 @@ pub struct SessionState { pub stack: Vec, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StackSnapshot { - pub dstack: Vec, - pub astack: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpcodeMeta { - pub index: usize, - pub byte_offset: usize, - pub display: String, - pub mapping: Option, -} - pub struct DebugSession<'a, 'i> { engine: DebugEngine<'a>, shadow_tx_context: Option>, @@ -124,17 +110,6 @@ struct VariableContext<'a> { impl<'a, 'i> DebugSession<'a, 'i> { // --- Session construction + stepping --- - /// Creates a debug session for lockscript-only execution. - /// Use this when debugging pure contract logic without sigscript setup. - pub fn lockscript_only( - script: &[u8], - source: &str, - debug_info: Option>, - engine: DebugEngine<'a>, - ) -> Result { - Self::from_scripts(script, source, debug_info, engine) - } - /// Creates a debug session simulating a full transaction spend. /// Executes sigscript first to seed the stack, then debugs lockscript execution. pub fn full( @@ -239,11 +214,6 @@ impl<'a, 'i> DebugSession<'a, 'i> { self.step_with_depth_predicate(|candidate, current| candidate < current) } - /// Backward-compatible statement stepping alias. - pub fn step_statement(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { - self.step_over() - } - /// Shared stepping loop for `step_into`, `step_over`, and `step_out`. /// Picks the next steppable mapping whose call depth satisfies `predicate`, /// executes opcodes until that mapping becomes active, and skips candidates @@ -354,35 +324,6 @@ impl<'a, 'i> DebugSession<'a, 'i> { self.engine.is_executing() } - /// Returns the current data and alt stack contents. - pub fn stacks_snapshot(&self) -> StackSnapshot { - let stacks = self.engine.stacks(); - StackSnapshot { - dstack: stacks.dstack.iter().map(|item| encode_hex(item)).collect(), - astack: stacks.astack.iter().map(|item| encode_hex(item)).collect(), - } - } - - /// Returns metadata for all opcodes (executed/pending status, byte offset). - pub fn opcode_metas(&self) -> Vec { - (0..self.op_displays.len()) - .map(|index| { - let byte_offset = self.opcode_offsets.get(index).copied().unwrap_or(self.script_len); - OpcodeMeta { - index, - byte_offset, - display: self.op_displays.get(index).cloned().unwrap_or_default(), - mapping: self.mapping_for_offset(byte_offset).cloned(), - } - }) - .collect() - } - - /// Returns the total number of opcodes in the script. - pub fn opcode_count(&self) -> usize { - self.op_displays.len() - } - pub fn debug_info(&self) -> &DebugInfo<'i> { &self.debug_info } @@ -807,26 +748,6 @@ fn decode_value_by_type(type_name: &str, bytes: Vec) -> Result Result { - if bytes.is_empty() { - return Ok(0); - } - if bytes.len() > 8 { - return Err("numeric value is longer than 8 bytes".to_string()); - } - let msb = bytes[bytes.len() - 1]; - let sign = 1 - 2 * ((msb >> 7) as i64); - let first_byte = (msb & 0x7f) as i64; - let mut value = first_byte; - for byte in bytes[..bytes.len() - 1].iter().rev() { - value = (value << 8) + (*byte as i64); - } - Ok(value * sign) -} - /// Executes sigscript to seed the stack before debugging lockscript. fn seed_engine_with_sigscript(engine: &mut DebugEngine<'_>, sigscript: &[u8]) -> Result<(), kaspa_txscript_errors::TxScriptError> { for opcode in parse_script::, DebugReused>(sigscript) { @@ -853,7 +774,6 @@ fn mapping_kind_order(kind: &MappingKind) -> u8 { MappingKind::Virtual {} => 1, MappingKind::Statement {} => 2, MappingKind::InlineCallExit { .. } => 3, - MappingKind::Synthetic { .. } => 4, } } @@ -865,14 +785,6 @@ fn mapping_matches_offset(mapping: &DebugMapping, offset: usize) -> bool { } } -fn encode_hex(bytes: &[u8]) -> String { - let mut out = vec![0u8; bytes.len() * 2]; - if faster_hex::hex_encode(bytes, &mut out).is_err() { - return String::new(); - } - String::from_utf8(out).unwrap_or_default() -} - #[cfg(test)] mod tests { use super::*; diff --git a/debugger/session/src/util.rs b/debugger/session/src/util.rs new file mode 100644 index 00000000..aac8e8d2 --- /dev/null +++ b/debugger/session/src/util.rs @@ -0,0 +1,27 @@ +/// Decodes a txscript script number (little-endian sign-magnitude, max 8 bytes). +/// Mirrors txscript's internal numeric decode logic; kept local because txscript +/// exposes this helper only as crate-private internals today. +pub fn decode_i64(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(0); + } + if bytes.len() > 8 { + return Err("numeric value is longer than 8 bytes".to_string()); + } + let msb = bytes[bytes.len() - 1]; + let sign = 1 - 2 * ((msb >> 7) as i64); + let first_byte = (msb & 0x7f) as i64; + let mut value = first_byte; + for byte in bytes[..bytes.len() - 1].iter().rev() { + value = (value << 8) + (*byte as i64); + } + Ok(value * sign) +} + +pub fn encode_hex(bytes: &[u8]) -> String { + let mut out = vec![0u8; bytes.len() * 2]; + if faster_hex::hex_encode(bytes, &mut out).is_err() { + return String::new(); + } + String::from_utf8(out).unwrap_or_default() +} diff --git a/debugger/session/tests/debug_session_tests.rs b/debugger/session/tests/debug_session_tests.rs index e822ff1b..7af0e05b 100644 --- a/debugger/session/tests/debug_session_tests.rs +++ b/debugger/session/tests/debug_session_tests.rs @@ -114,7 +114,7 @@ fn debug_session_steps_forward() -> Result<(), Box> { session.run_to_first_executed_statement()?; let before = session.state().pc; let before_span = session.current_span(); - session.step_statement()?; + session.step_over()?; let after = session.state().pc; let after_span = session.current_span(); assert!(after > before || after_span != before_span, "expected statement step to make source progress"); diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 035b1634..a4a62b6f 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -10,14 +10,13 @@ use crate::ast::{ StateBindingAst, StateFieldExpr, Statement, TimeVar, TypeBase, TypeRef, UnaryOp, UnarySuffixKind, parse_contract_ast, parse_type_ref, }; -use crate::debug_info::labels::synthetic; use crate::debug_info::{DebugInfo, SourceSpan}; pub use crate::errors::{CompilerError, ErrorSpan}; use crate::span; mod debug_recording; -use debug_recording::{DebugSink, FunctionDebugRecorder, record_synthetic_range}; +use debug_recording::{ContractRecorder, FunctionRecorder}; #[derive(Debug, Clone, Copy, Default)] pub struct CompileOptions { @@ -110,7 +109,7 @@ fn compile_contract_impl<'i>( let (_contract_fields, field_prolog_script) = compile_contract_fields(&contract.fields, &constants, options, script_size)?; let mut compiled_entrypoints = Vec::new(); - let mut recorder = DebugSink::new(options.record_debug_infos); + let mut recorder = ContractRecorder::new(options.record_debug_infos); recorder.record_constructor_constants(&contract.params, constructor_args); for (index, func) in contract.functions.iter().enumerate() { if func.entrypoint { @@ -138,34 +137,25 @@ fn compile_contract_impl<'i>( let mut builder = ScriptBuilder::new(); let total = compiled_entrypoints.len(); for (index, compiled) in compiled_entrypoints.iter().enumerate() { - record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_GUARD, |builder| { - builder.add_op(OpDup)?; - builder.add_i64(index as i64)?; - builder.add_op(OpNumEqual)?; - builder.add_op(OpIf)?; - builder.add_op(OpDrop)?; - Ok(()) - })?; + builder.add_op(OpDup)?; + builder.add_i64(index as i64)?; + builder.add_op(OpNumEqual)?; + builder.add_op(OpIf)?; + builder.add_op(OpDrop)?; let start = field_prolog_script.len() + builder.script().len(); - builder.add_ops(&compiled.script)?; recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); - record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ELSE, |builder| { - builder.add_op(OpElse)?; - if index == total - 1 { - builder.add_op(OpDrop)?; - builder.add_op(OpFalse)?; - builder.add_op(OpVerify)?; - } - Ok(()) - })?; + builder.add_ops(&compiled.script)?; + builder.add_op(OpElse)?; + if index == total - 1 { + builder.add_op(OpDrop)?; + builder.add_op(OpFalse)?; + builder.add_op(OpVerify)?; + } } - record_synthetic_range(&mut builder, &mut recorder, synthetic::DISPATCHER_ENDIFS, |builder| { - for _ in 0..total { - builder.add_op(OpEndIf)?; - } - Ok(()) - })?; + for _ in 0..total { + builder.add_op(OpEndIf)?; + } builder.drain() }; @@ -920,7 +910,7 @@ pub fn function_branch_index<'i>(contract: &ContractAst<'i>, function_name: &str struct CompiledFunction<'i> { name: String, script: Vec, - debug: FunctionDebugRecorder<'i>, + debug: FunctionRecorder<'i>, } fn compile_function<'i>( @@ -972,7 +962,7 @@ fn compile_function<'i>( env.remove(¶m.name); } let mut builder = ScriptBuilder::new(); - let mut recorder = FunctionDebugRecorder::new(options.record_debug_infos, function, contract_fields); + let mut recorder = FunctionRecorder::new(options.record_debug_infos, function, contract_fields); let mut yields: Vec = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { @@ -1002,7 +992,7 @@ fn compile_function<'i>( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - let env_before = recorder.is_enabled().then(|| env.clone()); + let env_before = recorder.capture_env_snapshot(&env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1012,27 +1002,26 @@ fn compile_function<'i>( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new()).map_err(|err| err.with_span(&expr.span))?; yields.push(resolved); } - recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; - continue; + } else { + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + &mut builder, + options, + contract_fields, + contract_field_prefix_len, + constants, + functions, + function_order, + function_index, + &mut yields, + script_size, + &mut recorder, + ) + .map_err(|err| err.with_span(&stmt.span()))?; } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - &mut builder, - options, - contract_fields, - contract_field_prefix_len, - constants, - functions, - function_order, - function_index, - &mut yields, - script_size, - &mut recorder, - ) - .map_err(|err| err.with_span(&stmt.span()))?; let end = builder.script().len(); recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } @@ -1092,7 +1081,7 @@ fn compile_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder<'i>, + debug_recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { match stmt { Statement::VariableDefinition { type_ref, name, expr, .. } => { @@ -1787,7 +1776,7 @@ fn compile_inline_call<'i>( function_order: &HashMap, caller_index: usize, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder<'i>, + debug_recorder: &mut FunctionRecorder<'i>, ) -> Result>, CompilerError> { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = @@ -1864,7 +1853,7 @@ fn compile_inline_call<'i>( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { let start = builder.script().len(); - let env_before = inline_recorder.is_enabled().then(|| env.clone()); + let env_before = inline_recorder.capture_env_snapshot(&env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1875,27 +1864,26 @@ fn compile_inline_call<'i>( let resolved = resolve_expr(expr.clone(), &env, &mut HashSet::new()).map_err(|err| err.with_span(&expr.span))?; yields.push(resolved); } - inline_recorder.record_statement_with_env_diff(stmt, start, builder.script().len(), env_before.as_ref(), &env, &types)?; - continue; + } else { + compile_statement( + stmt, + &mut env, + ¶ms, + &mut types, + builder, + options, + &[], + 0, + contract_constants, + functions, + function_order, + callee_index, + &mut yields, + script_size, + &mut inline_recorder, + ) + .map_err(|err| err.with_span(&stmt.span()))?; } - compile_statement( - stmt, - &mut env, - ¶ms, - &mut types, - builder, - options, - &[], - 0, - contract_constants, - functions, - function_order, - callee_index, - &mut yields, - script_size, - &mut inline_recorder, - ) - .map_err(|err| err.with_span(&stmt.span()))?; let end = builder.script().len(); inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; } @@ -1932,7 +1920,7 @@ fn compile_if_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder<'i>, + debug_recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( @@ -2071,11 +2059,11 @@ fn compile_block<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder<'i>, + debug_recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { for stmt in statements { let start = builder.script().len(); - let env_before = debug_recorder.is_enabled().then(|| env.clone()); + let env_before = debug_recorder.capture_env_snapshot(env); compile_statement( stmt, env, @@ -2120,7 +2108,7 @@ fn compile_for_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionDebugRecorder<'i>, + debug_recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { let start = eval_const_int(start_expr, contract_constants)?; let end = eval_const_int(end_expr, contract_constants)?; diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index eb08ffd0..47599bec 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -1,37 +1,233 @@ use std::collections::{HashMap, HashSet}; - -use kaspa_txscript::script_builder::ScriptBuilder; +use std::fmt; use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; use crate::debug_info::{ - DebugConstantMapping, DebugEvent, DebugEventKind, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, - DebugVariableUpdate, SourceSpan, + DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugRecorder, DebugVariableUpdate, + MappingKind, SourceSpan, }; use super::{CompilerError, resolve_expr_for_debug}; type ResolvedVariableUpdate<'i> = (String, String, Expr<'i>); -pub(super) fn record_synthetic_range<'i>( - builder: &mut ScriptBuilder, - recorder: &mut DebugSink<'i>, - label: &'static str, - f: impl FnOnce(&mut ScriptBuilder) -> Result<(), CompilerError>, -) -> Result<(), CompilerError> { - let start = builder.script().len(); - f(builder)?; - let end = builder.script().len(); - recorder.record_synthetic_range(start, end, label); - Ok(()) +#[derive(Clone)] +struct FunctionRecorderSnapshot<'i> { + events: Vec, + variable_updates: Vec>, + next_frame_id: u32, +} + +trait FunctionRecorderImpl<'i> { + fn capture_env_snapshot(&self, env: &HashMap>) -> Option>>; + + fn record_statement_with_env_diff( + &mut self, + stmt: &Statement<'i>, + bytecode_start: usize, + bytecode_end: usize, + before_env: Option<&HashMap>>, + after_env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError>; + + fn record_inline_param_updates( + &mut self, + function: &FunctionAst<'i>, + env: &HashMap>, + span: Option, + bytecode_offset: usize, + ) -> Result<(), CompilerError>; + + fn record_virtual_binding( + &mut self, + name: String, + type_name: String, + expr: Expr<'i>, + bytecode_offset: usize, + span: Option, + ); + + fn start_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + ) -> Box + 'i>; + + fn finish_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + inline: &dyn FunctionRecorderImpl<'i>, + ); + + fn sequence_count(&self) -> u32; + + fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>); + + fn snapshot(&self) -> Option>; } /// Per-function debug recorder active during function compilation. /// Records params, statements, and variable updates for a single function. +pub struct FunctionRecorder<'i> { + imp: Box + 'i>, +} + +impl fmt::Debug for FunctionRecorder<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FunctionRecorder").finish_non_exhaustive() + } +} + +impl<'i> FunctionRecorder<'i> { + pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { + if enabled { + Self { imp: Box::new(ActiveFunctionRecorder::new(function, contract_fields)) } + } else { + Self { imp: Box::new(NoopFunctionRecorder) } + } + } + + fn from_impl(imp: Box + 'i>) -> Self { + Self { imp } + } + + pub fn capture_env_snapshot(&self, env: &HashMap>) -> Option>> { + self.imp.capture_env_snapshot(env) + } + + pub fn record_statement_with_env_diff( + &mut self, + stmt: &Statement<'i>, + bytecode_start: usize, + bytecode_end: usize, + before_env: Option<&HashMap>>, + after_env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError> { + self.imp.record_statement_with_env_diff(stmt, bytecode_start, bytecode_end, before_env, after_env, types) + } + + pub fn record_inline_param_updates( + &mut self, + function: &FunctionAst<'i>, + env: &HashMap>, + span: Option, + bytecode_offset: usize, + ) -> Result<(), CompilerError> { + self.imp.record_inline_param_updates(function, env, span, bytecode_offset) + } + + pub fn record_virtual_binding( + &mut self, + name: String, + type_name: String, + expr: Expr<'i>, + bytecode_offset: usize, + span: Option, + ) { + self.imp.record_virtual_binding(name, type_name, expr, bytecode_offset, span) + } + + pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { + Self::from_impl(self.imp.start_inline_call_recording(span, bytecode_offset, callee)) + } + + pub fn finish_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + inline: &FunctionRecorder<'i>, + ) { + self.imp.finish_inline_call_recording(span, bytecode_offset, callee, inline.imp.as_ref()); + } + + pub fn sequence_count(&self) -> u32 { + self.imp.sequence_count() + } + + pub fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { + self.imp.emit_with_offset(offset, seq_base, recorder); + } +} + #[derive(Debug, Default)] -pub struct FunctionDebugRecorder<'i> { +struct NoopFunctionRecorder; + +impl<'i> FunctionRecorderImpl<'i> for NoopFunctionRecorder { + fn capture_env_snapshot(&self, _env: &HashMap>) -> Option>> { + None + } + + fn record_statement_with_env_diff( + &mut self, + _stmt: &Statement<'i>, + _bytecode_start: usize, + _bytecode_end: usize, + _before_env: Option<&HashMap>>, + _after_env: &HashMap>, + _types: &HashMap, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn record_inline_param_updates( + &mut self, + _function: &FunctionAst<'i>, + _env: &HashMap>, + _span: Option, + _bytecode_offset: usize, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn record_virtual_binding( + &mut self, + _name: String, + _type_name: String, + _expr: Expr<'i>, + _bytecode_offset: usize, + _span: Option, + ) { + } + + fn start_inline_call_recording( + &mut self, + _span: Option, + _bytecode_offset: usize, + _callee: &str, + ) -> Box + 'i> { + Box::new(Self) + } + + fn finish_inline_call_recording( + &mut self, + _span: Option, + _bytecode_offset: usize, + _callee: &str, + _inline: &dyn FunctionRecorderImpl<'i>, + ) { + } + + fn sequence_count(&self) -> u32 { + 0 + } + + fn emit_with_offset(&self, _offset: usize, _seq_base: u32, _recorder: &mut DebugRecorder<'i>) {} + + fn snapshot(&self) -> Option> { + None + } +} + +#[derive(Debug, Default)] +struct ActiveFunctionRecorder<'i> { function_name: String, - enabled: bool, - events: Vec, + events: Vec, variable_updates: Vec>, param_mappings: Vec, next_seq: u32, @@ -40,22 +236,14 @@ pub struct FunctionDebugRecorder<'i> { next_frame_id: u32, } -impl<'i> FunctionDebugRecorder<'i> { - pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { +impl<'i> ActiveFunctionRecorder<'i> { + fn new(function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { let mut recorder = - Self { function_name: function.name.clone(), enabled, call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; + Self { function_name: function.name.clone(), call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; recorder.record_stack_bindings(function, contract_fields); recorder } - fn sequence_count(&self) -> u32 { - self.next_seq - } - - pub fn is_enabled(&self) -> bool { - self.enabled - } - fn new_inline_child(&mut self) -> Self { let frame_id = self.next_frame_id; self.next_frame_id = self.next_frame_id.saturating_add(1); @@ -64,7 +252,6 @@ impl<'i> FunctionDebugRecorder<'i> { // child returns, so sibling inline calls never reuse frame ids. Self { function_name: self.function_name.clone(), - enabled: self.enabled, call_depth: self.call_depth.saturating_add(1), frame_id, next_frame_id: self.next_frame_id, @@ -78,18 +265,9 @@ impl<'i> FunctionDebugRecorder<'i> { seq } - fn push_event( - &mut self, - bytecode_start: usize, - bytecode_end: usize, - span: Option, - kind: DebugEventKind, - ) -> Option { - if !self.enabled { - return None; - } + fn push_event(&mut self, bytecode_start: usize, bytecode_end: usize, span: Option, kind: MappingKind) -> u32 { let sequence = self.next_sequence(); - self.events.push(DebugEvent { + self.events.push(DebugMapping { bytecode_start, bytecode_end, span, @@ -98,13 +276,10 @@ impl<'i> FunctionDebugRecorder<'i> { call_depth: self.call_depth, frame_id: self.frame_id, }); - Some(sequence) + sequence } fn record_stack_bindings(&mut self, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { - if !self.enabled { - return; - } let param_count = function.params.len(); let field_count = contract_fields.len(); // Runtime stack layout at function entry is: @@ -129,8 +304,8 @@ impl<'i> FunctionDebugRecorder<'i> { } } - fn record_statement_span(&mut self, span: SourceSpan, bytecode_start: usize, bytecode_len: usize) -> Option { - let kind = if bytecode_len == 0 { DebugEventKind::Virtual {} } else { DebugEventKind::Statement {} }; + fn record_statement_span(&mut self, span: SourceSpan, bytecode_start: usize, bytecode_len: usize) -> u32 { + let kind = if bytecode_len == 0 { MappingKind::Virtual {} } else { MappingKind::Statement {} }; self.push_event(bytecode_start, bytecode_start + bytecode_len, Some(span), kind) } @@ -142,117 +317,20 @@ impl<'i> FunctionDebugRecorder<'i> { variables: Vec>, ) { let span = SourceSpan::from(stmt.span()); - if let Some(sequence) = self.record_statement_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)) { - self.record_variable_updates(variables, bytecode_end, Some(span), sequence); - } - } - - /// Records one source step for `stmt` and emits variable updates for names - /// whose expressions changed between `before_env` and `after_env`. - /// Stored expressions are resolved against `after_env` so debugger shadow - /// evaluation can compute values from the current state. - pub fn record_statement_with_env_diff( - &mut self, - stmt: &Statement<'i>, - bytecode_start: usize, - bytecode_end: usize, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result<(), CompilerError> { - let updates = self.collect_variable_updates(before_env, after_env, types)?; - self.record_statement_updates(stmt, bytecode_start, bytecode_end, updates); - Ok(()) + let sequence = self.record_statement_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)); + self.record_variable_updates(variables, bytecode_end, Some(span), sequence); } - /// Emits explicit inline-callee param updates on the callee's next - /// statement sequence, so args are visible after stepping into the call - /// without adding an extra source step at the call-site. - pub fn record_inline_param_updates( - &mut self, - function: &FunctionAst<'i>, - env: &HashMap>, - span: Option, - bytecode_offset: usize, - ) -> Result<(), CompilerError> { - if !self.enabled { - return Ok(()); - } - // Anchor inline param updates to the next callee statement sequence. - // We intentionally "peek" (do not consume) so these updates align with - // the first real callee statement event sequence. - let sequence = self.next_seq; - let mut variables = Vec::new(); - for param in &function.params { - self.variable_update( - env, - &mut variables, - ¶m.name, - ¶m.type_ref.type_name(), - env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), - )?; - } - self.record_variable_updates(variables, bytecode_offset, span, sequence); - Ok(()) - } - - /// Emits a virtual debug step that binds a synthetic local variable. - /// Used by lowered constructs (for example unrolled loops) to keep - /// source-level locals visible even when no dedicated statement exists. - pub fn record_virtual_binding( - &mut self, - name: String, - type_name: String, - expr: Expr<'i>, - bytecode_offset: usize, - span: Option, - ) { - if !self.enabled { - return; - } - let Some(sequence) = self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::Virtual {}) else { - return; - }; - self.variable_updates.push(DebugVariableUpdate { - name, - type_name, - expr, - bytecode_offset, - span, - function: self.function_name.clone(), - sequence, - frame_id: self.frame_id, - }); - } - - /// Starts an inline call recording session and returns a child recorder for - /// callee body statements. - pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { - self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallEnter { callee: callee.to_string() }); - self.new_inline_child() - } - - /// Merges recorded callee events and emits the inline exit marker. - pub fn finish_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - inline: &FunctionDebugRecorder<'i>, - ) { - self.merge_inline_events(inline); - self.push_event(bytecode_offset, bytecode_offset, span, DebugEventKind::InlineCallExit { callee: callee.to_string() }); - } - - fn merge_inline_events(&mut self, inline: &FunctionDebugRecorder<'i>) { - if !self.enabled || inline.events.is_empty() { + fn merge_inline_events(&mut self, inline: FunctionRecorderSnapshot<'i>) { + if inline.events.is_empty() { // Keep frame-id frontier monotonic even if the inline call recorded // no events; this preserves uniqueness for later sibling calls. self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); return; } + let mut seq_map: HashMap = HashMap::new(); - let mut events = inline.events.clone(); + let mut events = inline.events; events.sort_by_key(|event| event.sequence); for mut event in events { @@ -263,7 +341,7 @@ impl<'i> FunctionDebugRecorder<'i> { seq_map.insert(local_seq, merged_seq); } - let mut updates = inline.variable_updates.clone(); + let mut updates = inline.variable_updates; updates.sort_by_key(|update| update.sequence); for mut update in updates { if let Some(merged_seq) = seq_map.get(&update.sequence) { @@ -271,6 +349,7 @@ impl<'i> FunctionDebugRecorder<'i> { self.variable_updates.push(update); } } + // Child may allocate nested frame ids; advance parent frontier so later // sibling inline calls start after the whole child subtree. self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); @@ -283,9 +362,6 @@ impl<'i> FunctionDebugRecorder<'i> { span: Option, sequence: u32, ) { - if !self.enabled { - return; - } for (name, type_name, expr) in variables { self.variable_updates.push(DebugVariableUpdate { name, @@ -306,9 +382,6 @@ impl<'i> FunctionDebugRecorder<'i> { after_env: &HashMap>, types: &HashMap, ) -> Result>, CompilerError> { - if !self.enabled { - return Ok(Vec::new()); - } let Some(before_env) = before_env else { return Ok(Vec::new()); }; @@ -348,40 +421,179 @@ impl<'i> FunctionDebugRecorder<'i> { type_name: &str, expr: Expr<'i>, ) -> Result<(), CompilerError> { - if !self.enabled { - return Ok(()); - } let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; variables.push((name.to_string(), type_name.to_string(), resolved)); Ok(()) } } +impl<'i> FunctionRecorderImpl<'i> for ActiveFunctionRecorder<'i> { + fn capture_env_snapshot(&self, env: &HashMap>) -> Option>> { + Some(env.clone()) + } + + fn record_statement_with_env_diff( + &mut self, + stmt: &Statement<'i>, + bytecode_start: usize, + bytecode_end: usize, + before_env: Option<&HashMap>>, + after_env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError> { + let updates = self.collect_variable_updates(before_env, after_env, types)?; + self.record_statement_updates(stmt, bytecode_start, bytecode_end, updates); + Ok(()) + } + + fn record_inline_param_updates( + &mut self, + function: &FunctionAst<'i>, + env: &HashMap>, + span: Option, + bytecode_offset: usize, + ) -> Result<(), CompilerError> { + // Anchor inline param updates to the next callee statement sequence. + // We intentionally "peek" (do not consume) so these updates align with + // the first real callee statement event sequence. + let sequence = self.next_seq; + let mut variables = Vec::new(); + for param in &function.params { + self.variable_update( + env, + &mut variables, + ¶m.name, + ¶m.type_ref.type_name(), + env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), + )?; + } + self.record_variable_updates(variables, bytecode_offset, span, sequence); + Ok(()) + } + + fn record_virtual_binding( + &mut self, + name: String, + type_name: String, + expr: Expr<'i>, + bytecode_offset: usize, + span: Option, + ) { + let sequence = self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::Virtual {}); + self.variable_updates.push(DebugVariableUpdate { + name, + type_name, + expr, + bytecode_offset, + span, + function: self.function_name.clone(), + sequence, + frame_id: self.frame_id, + }); + } + + fn start_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + ) -> Box + 'i> { + self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::InlineCallEnter { callee: callee.to_string() }); + Box::new(self.new_inline_child()) + } + + fn finish_inline_call_recording( + &mut self, + span: Option, + bytecode_offset: usize, + callee: &str, + inline: &dyn FunctionRecorderImpl<'i>, + ) { + if let Some(snapshot) = inline.snapshot() { + self.merge_inline_events(snapshot); + } + self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::InlineCallExit { callee: callee.to_string() }); + } + + fn sequence_count(&self) -> u32 { + self.next_seq + } + + fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { + emit_events_with_offset(&self.events, offset, seq_base, recorder); + emit_variable_updates_with_offset(&self.variable_updates, offset, seq_base, recorder); + record_param_mappings(&self.param_mappings, recorder); + } + + fn snapshot(&self) -> Option> { + Some(FunctionRecorderSnapshot { + events: self.events.clone(), + variable_updates: self.variable_updates.clone(), + next_frame_id: self.next_frame_id, + }) + } +} + +trait ContractRecorderImpl<'i> { + fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]); + + fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize); + + fn into_debug_info(self: Box, source: String) -> Option>; +} + /// Global debug recording sink that can be enabled or disabled. /// When Off, all recording calls become no-ops with zero overhead. -pub enum DebugSink<'i> { - Off, - On(DebugRecorder<'i>), +pub struct ContractRecorder<'i> { + imp: Box + 'i>, } -impl<'i> DebugSink<'i> { - pub fn new(enabled: bool) -> Self { - if enabled { Self::On(DebugRecorder::default()) } else { Self::Off } +impl fmt::Debug for ContractRecorder<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ContractRecorder").finish_non_exhaustive() } +} - fn recorder_mut(&mut self) -> Option<&mut DebugRecorder<'i>> { - match self { - Self::Off => None, - Self::On(rec) => Some(rec), - } +impl<'i> ContractRecorder<'i> { + pub fn new(enabled: bool) -> Self { + if enabled { Self { imp: Box::new(ActiveContractRecorder::default()) } } else { Self { imp: Box::new(NoopContractRecorder) } } } pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { - let Some(rec) = self.recorder_mut() else { - return; - }; + self.imp.record_constructor_constants(params, values); + } + + pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize) { + self.imp.record_compiled_function(name, script_len, debug, offset); + } + + pub fn into_debug_info(self, source: String) -> Option> { + self.imp.into_debug_info(source) + } +} + +#[derive(Debug, Default)] +struct NoopContractRecorder; + +impl<'i> ContractRecorderImpl<'i> for NoopContractRecorder { + fn record_constructor_constants(&mut self, _params: &[ParamAst<'i>], _values: &[Expr<'i>]) {} + + fn record_compiled_function(&mut self, _name: &str, _script_len: usize, _debug: &FunctionRecorder<'i>, _offset: usize) {} + + fn into_debug_info(self: Box, _source: String) -> Option> { + None + } +} + +#[derive(Debug, Default)] +struct ActiveContractRecorder<'i> { + recorder: DebugRecorder<'i>, +} + +impl<'i> ContractRecorderImpl<'i> for ActiveContractRecorder<'i> { + fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { for (param, value) in params.iter().zip(values.iter()) { - rec.record_constant(DebugConstantMapping { + self.recorder.record_constant(DebugConstantMapping { name: param.name.clone(), type_name: param.type_ref.type_name(), value: value.clone(), @@ -389,47 +601,24 @@ impl<'i> DebugSink<'i> { } } - pub fn record_synthetic_range(&mut self, start: usize, end: usize, label: &'static str) { - if end <= start { - return; - } - let Some(rec) = self.recorder_mut() else { - return; - }; - let sequence = rec.next_sequence(); - rec.record(DebugEvent { - bytecode_start: start, - bytecode_end: end, - span: None, - kind: DebugEventKind::Synthetic { label: label.to_string() }, - sequence, - call_depth: 0, - frame_id: 0, + fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize) { + let seq_base = self.recorder.reserve_sequence_block(debug.sequence_count()); + debug.emit_with_offset(offset, seq_base, &mut self.recorder); + self.recorder.record_function(DebugFunctionRange { + name: name.to_string(), + bytecode_start: offset, + bytecode_end: offset + script_len, }); } - pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionDebugRecorder<'i>, offset: usize) { - let Some(rec) = self.recorder_mut() else { - return; - }; - let seq_base = rec.reserve_sequence_block(debug.sequence_count()); - emit_events_with_offset(&debug.events, offset, seq_base, rec); - emit_variable_updates_with_offset(&debug.variable_updates, offset, seq_base, rec); - rec.record_function(DebugFunctionRange { name: name.to_string(), bytecode_start: offset, bytecode_end: offset + script_len }); - record_param_mappings(&debug.param_mappings, rec); - } - - pub fn into_debug_info(self, source: String) -> Option> { - match self { - Self::Off => None, - Self::On(rec) => Some(rec.into_debug_info(source)), - } + fn into_debug_info(self: Box, source: String) -> Option> { + Some(self.recorder.into_debug_info(source)) } } -fn emit_events_with_offset(events: &[DebugEvent], offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'_>) { +fn emit_events_with_offset(events: &[DebugMapping], offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'_>) { for event in events { - recorder.record(DebugEvent { + recorder.record(DebugMapping { bytecode_start: event.bytecode_start + offset, bytecode_end: event.bytecode_end + offset, span: event.span, @@ -466,3 +655,96 @@ fn record_param_mappings(params: &[DebugParamMapping], recorder: &mut DebugRecor recorder.record_param(param.clone()); } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::ast::{Expr, parse_contract_ast}; + use crate::debug_info::MappingKind; + + use super::{ContractRecorder, FunctionRecorder, SourceSpan}; + + #[test] + fn noop_recorders_are_pure_noops() { + let source = r#" + contract Demo() { + entrypoint function spend(int x) { + int y = x; + require(true); + } + } + "#; + let contract = parse_contract_ast(source).expect("parse contract"); + let function = contract.functions.first().expect("function"); + let stmt = function.body.first().expect("statement"); + + let mut recorder = FunctionRecorder::new(false, function, &contract.fields); + assert!(recorder.capture_env_snapshot(&HashMap::new()).is_none()); + + recorder.record_statement_with_env_diff(stmt, 0, 1, None, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); + + let inline = recorder.start_inline_call_recording(None, 1, "callee"); + recorder.finish_inline_call_recording(None, 2, "callee", &inline); + recorder.record_virtual_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, None); + assert_eq!(recorder.sequence_count(), 0); + + let mut sink = ContractRecorder::new(false); + sink.record_constructor_constants(&contract.params, &[]); + sink.record_compiled_function("spend", 1, &recorder, 0); + assert!(sink.into_debug_info(String::new()).is_none()); + } + + #[test] + fn active_recorders_preserve_sequences_and_inline_frame_ids() { + let source = r#" + contract Demo() { + entrypoint function spend(int x) { + int y = x; + require(true); + } + } + "#; + let contract = parse_contract_ast(source).expect("parse contract"); + let function = contract.functions.first().expect("function"); + let stmt = function.body.first().expect("statement"); + + let mut recorder = FunctionRecorder::new(true, function, &contract.fields); + + let mut before = HashMap::new(); + before.insert("x".to_string(), Expr::identifier("x")); + + let mut after = before.clone(); + after.insert("y".to_string(), Expr::int(7)); + + let mut types = HashMap::new(); + types.insert("x".to_string(), "int".to_string()); + types.insert("y".to_string(), "int".to_string()); + + recorder.record_statement_with_env_diff(stmt, 0, 1, Some(&before), &after, &types).expect("record first statement"); + + let span = SourceSpan::from(stmt.span()); + let mut inline = recorder.start_inline_call_recording(Some(span), 1, "callee"); + inline.record_virtual_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, Some(span)); + recorder.finish_inline_call_recording(Some(span), 2, "callee", &inline); + + assert_eq!(recorder.sequence_count(), 4); + + let mut sink = ContractRecorder::new(true); + sink.record_compiled_function("spend", 2, &recorder, 0); + let info = sink.into_debug_info(String::new()).expect("debug info available"); + + let sequences = info.mappings.iter().map(|mapping| mapping.sequence).collect::>(); + assert_eq!(sequences, vec![0, 1, 2, 3]); + + let virtual_mapping = + info.mappings.iter().find(|mapping| matches!(&mapping.kind, MappingKind::Virtual {})).expect("virtual mapping exists"); + assert_eq!(virtual_mapping.frame_id, 1); + + let tmp_update = info.variable_updates.iter().find(|update| update.name == "tmp").expect("tmp update exists"); + assert_eq!(tmp_update.frame_id, 1); + assert_eq!(tmp_update.sequence, virtual_mapping.sequence); + + assert!(info.params.iter().any(|param| param.name == "x")); + } +} diff --git a/silverscript-lang/src/debug_info.rs b/silverscript-lang/src/debug_info.rs index 16febeac..569a9fae 100644 --- a/silverscript-lang/src/debug_info.rs +++ b/silverscript-lang/src/debug_info.rs @@ -12,56 +12,17 @@ pub struct SourceSpan { impl<'a> From> for SourceSpan { fn from(span: span::Span<'a>) -> Self { - let (line, col) = span.start_pos().line_col(); - let (end_line, end_col) = span.end_pos().line_col(); + let (line, col, end_line, end_col) = span.line_col_range(); Self { line: line as u32, col: col as u32, end_line: end_line as u32, end_col: end_col as u32 } } } -pub mod labels { - pub mod synthetic { - /// Checks which function was selected (DUP, PUSH index, NUMEQUAL, IF, DROP). - pub const DISPATCHER_GUARD: &str = "dispatcher.guard"; - /// Function didn't match — try next, or fail if last. - pub const DISPATCHER_ELSE: &str = "dispatcher.else"; - /// Closes all dispatcher if/else branches. - pub const DISPATCHER_ENDIFS: &str = "dispatcher.endifs"; - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DebugEventKind { - Statement {}, - Virtual {}, - InlineCallEnter { callee: String }, - InlineCallExit { callee: String }, - Synthetic { label: String }, -} - -/// Single debug mapping recorded during compilation. -/// Maps a bytecode range to source location and event type (statement or synthetic). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DebugEvent { - pub bytecode_start: usize, - pub bytecode_end: usize, - pub span: Option, - pub kind: DebugEventKind, - /// Monotonic event order assigned by the compiler recorder. - /// Used to preserve source step order when bytecode ranges overlap. - #[serde(default)] - pub sequence: u32, - #[serde(default)] - pub call_depth: u32, - #[serde(default)] - pub frame_id: u32, -} - /// Accumulates debug metadata during compilation. /// Collects events, variable updates, param mappings, function ranges, and constants. /// Converted to `DebugInfo` after compilation completes. #[derive(Debug, Default)] pub struct DebugRecorder<'i> { - events: Vec, + events: Vec, variable_updates: Vec>, params: Vec, functions: Vec, @@ -70,8 +31,8 @@ pub struct DebugRecorder<'i> { } impl<'i> DebugRecorder<'i> { - pub fn record(&mut self, event: DebugEvent) { - self.events.push(event); + pub fn record(&mut self, mapping: DebugMapping) { + self.events.push(mapping); } pub fn record_variable_update(&mut self, update: DebugVariableUpdate<'i>) { @@ -106,14 +67,10 @@ impl<'i> DebugRecorder<'i> { base } - pub fn into_events(self) -> Vec { - self.events - } - pub fn into_debug_info(self, source: String) -> DebugInfo<'i> { DebugInfo { source, - mappings: self.events.into_iter().map(DebugMapping::from).collect(), + mappings: self.events, variable_updates: self.variable_updates, params: self.params, functions: self.functions, @@ -214,31 +171,21 @@ pub enum MappingKind { Virtual {}, InlineCallEnter { callee: String }, InlineCallExit { callee: String }, - Synthetic { label: String }, } -impl From for MappingKind { - fn from(kind: DebugEventKind) -> Self { - match kind { - DebugEventKind::Statement {} => MappingKind::Statement {}, - DebugEventKind::Virtual {} => MappingKind::Virtual {}, - DebugEventKind::InlineCallEnter { callee } => MappingKind::InlineCallEnter { callee }, - DebugEventKind::InlineCallExit { callee } => MappingKind::InlineCallExit { callee }, - DebugEventKind::Synthetic { label } => MappingKind::Synthetic { label }, - } - } -} - -impl From for DebugMapping { - fn from(event: DebugEvent) -> Self { - DebugMapping { - bytecode_start: event.bytecode_start, - bytecode_end: event.bytecode_end, - span: event.span, - kind: event.kind.into(), - sequence: event.sequence, - call_depth: event.call_depth, - frame_id: event.frame_id, - } +#[cfg(test)] +mod tests { + use super::SourceSpan; + use crate::span::Span; + + #[test] + fn source_span_from_span_uses_line_col_range() { + let source = "alpha\nbeta\ngamma"; + let span = Span::new(source, 6, 10).expect("span"); + let source_span = SourceSpan::from(span); + assert_eq!(source_span.line, 2); + assert_eq!(source_span.col, 1); + assert_eq!(source_span.end_line, 2); + assert_eq!(source_span.end_col, 5); } } diff --git a/silverscript-lang/src/span.rs b/silverscript-lang/src/span.rs index 00e415d9..cdc1f547 100644 --- a/silverscript-lang/src/span.rs +++ b/silverscript-lang/src/span.rs @@ -19,6 +19,12 @@ impl<'i> Span<'i> { let end = self.end().max(other.end()); Span::new(input, start, end).unwrap_or(*self) } + + pub(crate) fn line_col_range(&self) -> (usize, usize, usize, usize) { + let (line, col) = self.start_pos().line_col(); + let (end_line, end_col) = self.end_pos().line_col(); + (line, col, end_line, end_col) + } } impl<'i> Default for Span<'i> { @@ -79,3 +85,16 @@ impl<'i> SpanUtils for Span<'i> { pub fn join<'i>(left: &Span<'i>, right: &Span<'i>) -> Span<'i> { left.join(right) } + +#[cfg(test)] +mod tests { + use super::Span; + + #[test] + fn line_col_range_reports_expected_bounds() { + let source = "a\nbc\ndef"; + let span = Span::new(source, 2, 3).expect("span"); + let (line, col, end_line, end_col) = span.line_col_range(); + assert_eq!((line, col, end_line, end_col), (2, 1, 2, 2)); + } +} diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 61d84a9d..2c0e06c4 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -139,6 +139,38 @@ fn accepts_constructor_args_with_matching_types() { compile_contract(source, &args, CompileOptions::default()).expect("compile succeeds"); } +#[test] +fn compile_contract_omits_debug_info_when_recording_disabled() { + let source = r#" + contract DebugToggle() { + entrypoint function spend(int x) { + require(x == x); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("compile succeeds"); + assert!(compiled.debug_info.is_none()); +} + +#[test] +fn compile_contract_emits_debug_info_when_recording_enabled() { + let source = r#" + contract DebugToggle() { + entrypoint function spend(int x) { + require(x == x); + } + } + "#; + + let options = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let debug_info = compiled.debug_info.expect("debug info should be present"); + assert!(!debug_info.mappings.is_empty()); + assert!(!debug_info.functions.is_empty()); + assert!(debug_info.params.iter().any(|param| param.name == "x")); +} + #[test] fn rejects_constructor_args_with_wrong_scalar_types() { let source = r#" From b5c461c8ffab7c3661ffb6d32db7a01e51a5a83c Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:54:36 +0200 Subject: [PATCH 35/41] Simplify fn recorder --- debugger/session/src/session.rs | 332 ++++---- debugger/session/tests/debug_session_tests.rs | 23 +- silverscript-lang/src/compiler.rs | 68 +- .../src/compiler/debug_recording.rs | 716 ++++++------------ silverscript-lang/src/debug_info.rs | 188 +++-- silverscript-lang/tests/compiler_tests.rs | 2 +- 6 files changed, 559 insertions(+), 770 deletions(-) diff --git a/debugger/session/src/session.rs b/debugger/session/src/session.rs index aa8ad8b8..30bb485d 100644 --- a/debugger/session/src/session.rs +++ b/debugger/session/src/session.rs @@ -10,7 +10,7 @@ use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEn use silverscript_lang::ast::{Expr, ExprKind}; use silverscript_lang::compiler::compile_debug_expr; use silverscript_lang::debug_info::{ - DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugVariableUpdate, MappingKind, SourceSpan, + DebugFunctionRange, DebugInfo, DebugParamMapping, DebugStep, DebugVariableUpdate, SourceSpan, StepId, StepKind, }; pub use crate::presentation::{SourceContext, SourceContextLine}; @@ -69,10 +69,10 @@ pub struct Variable { } #[derive(Debug, Clone)] -pub struct SessionState { +pub struct SessionState<'i> { pub pc: usize, pub opcode: Option, - pub mapping: Option, + pub step: Option>, pub stack: Vec, } @@ -85,12 +85,12 @@ pub struct DebugSession<'a, 'i> { script_len: usize, pc: usize, debug_info: DebugInfo<'i>, - source_mappings: Vec, + step_order: Vec, current_step_index: Option, source_lines: Vec, breakpoints: HashSet, - // Sequence ids of steppable mappings that were already visited in this session. - executed_sequences: HashSet, + // Source-level step ids that were already visited in this session. + executed_steps: HashSet, } struct ShadowParamValue { @@ -102,9 +102,10 @@ struct ShadowParamValue { struct VariableContext<'a> { function_name: &'a str, + function_start: usize, + function_end: usize, offset: usize, - sequence: u32, - frame_id: u32, + step_id: StepId, } impl<'a, 'i> DebugSession<'a, 'i> { @@ -123,7 +124,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { Self::from_scripts(lockscript, source, debug_info, engine) } - /// Internal constructor: parses script, prepares opcodes, extracts statement mappings. + /// Internal constructor: parses script, prepares opcodes, extracts statement steps. pub fn from_scripts( script: &[u8], source: &str, @@ -137,31 +138,12 @@ impl<'a, 'i> DebugSession<'a, 'i> { let source_lines: Vec = source.lines().map(String::from).collect(); let (opcode_offsets, script_len) = build_opcode_offsets(&opcodes); - let mut source_mappings: Vec = debug_info - .mappings - .iter() - .filter(|mapping| { - matches!( - &mapping.kind, - MappingKind::Statement {} - | MappingKind::Virtual {} - | MappingKind::InlineCallEnter { .. } - | MappingKind::InlineCallExit { .. } - ) - }) - .cloned() - .collect(); + let mut step_order: Vec = (0..debug_info.steps.len()).collect(); // Overlapping inline ranges can share the same bytecode offsets; keep // compiler emission order via sequence before comparing range width. - source_mappings.sort_by_key(|mapping| { - ( - mapping.bytecode_start, - mapping.sequence, - mapping_kind_order(&mapping.kind), - mapping.call_depth, - mapping.bytecode_end, - mapping.frame_id, - ) + step_order.sort_by_key(|&index| { + let step = &debug_info.steps[index]; + (step.bytecode_start, step.sequence, step_kind_order(&step.kind), step.call_depth, step.bytecode_end, step.frame_id) }); Ok(Self { @@ -173,16 +155,16 @@ impl<'a, 'i> DebugSession<'a, 'i> { script_len, pc: 0, debug_info, - source_mappings, + step_order, current_step_index: None, source_lines, breakpoints: HashSet::new(), - executed_sequences: HashSet::new(), + executed_steps: HashSet::new(), }) } /// Executes a single opcode and advances the program counter. - pub fn step_opcode(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + pub fn step_opcode(&mut self) -> Result>, kaspa_txscript_errors::TxScriptError> { if self.pc >= self.opcodes.len() { return Ok(None); } @@ -200,47 +182,46 @@ impl<'a, 'i> DebugSession<'a, 'i> { } /// Step into: advance to next source step regardless of call depth. - pub fn step_into(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + pub fn step_into(&mut self) -> Result>, kaspa_txscript_errors::TxScriptError> { self.step_with_depth_predicate(|_, _| true) } /// Step over: advance to next source step at the same or shallower call depth. - pub fn step_over(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + pub fn step_over(&mut self) -> Result>, kaspa_txscript_errors::TxScriptError> { self.step_with_depth_predicate(|candidate, current| candidate <= current) } /// Step out: advance to next source step at a shallower call depth. - pub fn step_out(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + pub fn step_out(&mut self) -> Result>, kaspa_txscript_errors::TxScriptError> { self.step_with_depth_predicate(|candidate, current| candidate < current) } /// Shared stepping loop for `step_into`, `step_over`, and `step_out`. - /// Picks the next steppable mapping whose call depth satisfies `predicate`, - /// executes opcodes until that mapping becomes active, and skips candidates + /// Picks the next steppable step whose call depth satisfies `predicate`, + /// executes opcodes until that step becomes active, and skips candidates /// that are already behind the current byte offset (for example, non-taken - /// branch mappings). + /// branch steps). fn step_with_depth_predicate( &mut self, predicate: impl Fn(u32, u32) -> bool, - ) -> Result, kaspa_txscript_errors::TxScriptError> { - if self.source_mappings.is_empty() { + ) -> Result>, kaspa_txscript_errors::TxScriptError> { + if self.step_order.is_empty() { return self.step_opcode(); } - let current_depth = self.current_step_mapping().map(|mapping| mapping.call_depth).unwrap_or(0); + let current_depth = self.current_timeline_step().map(|step| step.call_depth).unwrap_or(0); let mut search_from = self.current_step_index; loop { - let Some(target_index) = - self.next_steppable_mapping_index(search_from, |mapping| predicate(mapping.call_depth, current_depth)) + let Some(target_index) = self.next_steppable_step_index(search_from, |step| predicate(step.call_depth, current_depth)) else { while self.step_opcode()?.is_some() {} return Ok(None); }; - if self.advance_to_mapping(target_index)? { + if self.advance_to_step(target_index)? { self.current_step_index = Some(target_index); - self.mark_mapping_executed(target_index); + self.mark_step_executed(target_index); return Ok(Some(self.state())); } @@ -248,18 +229,19 @@ impl<'a, 'i> DebugSession<'a, 'i> { } } - fn advance_to_mapping(&mut self, target_index: usize) -> Result { - let Some(target) = self.source_mappings.get(target_index).cloned() else { + fn advance_to_step(&mut self, target_index: usize) -> Result { + let Some(target) = self.step_at_order(target_index) else { return Ok(false); }; + let (target_start, target_end) = (target.bytecode_start, target.bytecode_end); loop { let offset = self.current_byte_offset(); - if offset > target.bytecode_start { + if offset > target_start { return Ok(false); } - if mapping_matches_offset(&target, offset) && self.engine.is_executing() { + if range_matches_offset(target_start, target_end, offset) && self.engine.is_executing() { return Ok(true); } @@ -271,9 +253,9 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// Advances execution to the first user statement, skipping dispatcher/synthetic bytecode. /// Call this after session creation to skip over contract setup code. - /// Skips opcodes until the first source-mapped statement is encountered. + /// Skips opcodes until the first source step is encountered. pub fn run_to_first_executed_statement(&mut self) -> Result<(), kaspa_txscript_errors::TxScriptError> { - if self.source_mappings.is_empty() { + if self.step_order.is_empty() { return Ok(()); } loop { @@ -282,9 +264,9 @@ impl<'a, 'i> DebugSession<'a, 'i> { } let offset = self.current_byte_offset(); if self.engine.is_executing() { - if let Some(index) = self.steppable_mapping_index_for_offset(offset) { + if let Some(index) = self.steppable_step_index_for_offset(offset) { self.current_step_index = Some(index); - self.mark_mapping_executed(index); + self.mark_step_executed(index); return Ok(()); } } @@ -295,7 +277,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { } /// Continues execution until a breakpoint is hit or script completes. - pub fn continue_to_breakpoint(&mut self) -> Result, kaspa_txscript_errors::TxScriptError> { + pub fn continue_to_breakpoint(&mut self) -> Result>, kaspa_txscript_errors::TxScriptError> { if self.breakpoints.is_empty() { while self.step_opcode()?.is_some() {} return Ok(None); @@ -304,8 +286,8 @@ impl<'a, 'i> DebugSession<'a, 'i> { if self.step_into()?.is_none() { return Ok(None); } - if let Some(mapping) = self.current_step_mapping() { - if self.mapping_hits_breakpoint(mapping) { + if let Some(step) = self.current_timeline_step() { + if self.step_hits_breakpoint(step) { return Ok(Some(self.state())); } } @@ -313,10 +295,10 @@ impl<'a, 'i> DebugSession<'a, 'i> { } /// Returns the current execution state snapshot. - pub fn state(&self) -> SessionState { + pub fn state(&self) -> SessionState<'i> { let executed = self.pc.saturating_sub(1); let opcode = self.op_displays.get(executed).cloned(); - SessionState { pc: self.pc, opcode, mapping: self.current_location(), stack: self.stack() } + SessionState { pc: self.pc, opcode, step: self.current_step(), stack: self.stack() } } /// Returns true if the script engine is still running. @@ -328,10 +310,9 @@ impl<'a, 'i> DebugSession<'a, 'i> { &self.debug_info } - // --- Mapping + source context --- + // --- Step + source context --- /// Returns source lines around the current statement (radius = 6 lines). - /// Active line is marked via `is_active` field. Returns None if no source mapping exists. /// Returns surrounding source lines with the current line highlighted. pub fn source_context(&self) -> Option { let span = self.current_span()?; @@ -341,10 +322,10 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// Adds a breakpoint at the given line number. Returns true if added. pub fn add_breakpoint(&mut self, line: u32) -> bool { let valid = self - .source_mappings + .step_order .iter() - .filter(|mapping| self.is_steppable_mapping(mapping)) - .any(|mapping| mapping.span.is_some_and(|span| line >= span.line && line <= span.end_line)); + .filter_map(|&index| self.debug_info.steps.get(index)) + .any(|step| self.is_steppable_step(step) && line >= step.span.line && line <= step.span.end_line); if valid { self.breakpoints.insert(line); } @@ -369,16 +350,15 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// Includes params, local variables (up to current offset), and constructor constants. /// Values are computed via shadow VM evaluation. pub fn list_variables(&self) -> Result, String> { - let (sequence, frame_id) = self.current_step_sequence_and_frame(); - self.collect_variables(sequence, frame_id) + self.collect_variables(self.current_step_id()) } pub fn list_variables_at_sequence(&self, sequence: u32, frame_id: u32) -> Result, String> { - self.collect_variables(sequence, frame_id) + self.collect_variables(StepId::new(sequence, frame_id)) } - fn collect_variables(&self, sequence: u32, frame_id: u32) -> Result, String> { - let context = self.current_variable_context(sequence, frame_id)?; + fn collect_variables(&self, step_id: StepId) -> Result, String> { + let context = self.current_variable_context(step_id)?; let mut variables = self.collect_variables_map(&context)?.into_values().collect::>(); variables.sort_by(|a, b| a.name.cmp(&b.name)); Ok(variables) @@ -386,8 +366,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// Returns a specific variable by name, or error if not in scope. pub fn variable_by_name(&self, name: &str) -> Result { - let (sequence, frame_id) = self.current_step_sequence_and_frame(); - let context = self.current_variable_context(sequence, frame_id)?; + let context = self.current_variable_context(self.current_step_id())?; let variables = self.collect_variables_map(&context)?; variables.get(name).cloned().ok_or_else(|| format!("unknown variable '{name}'")) } @@ -398,9 +377,9 @@ impl<'a, 'i> DebugSession<'a, 'i> { format_debug_value(type_name, value) } - /// Returns the debug mapping for the current bytecode position. - pub fn current_location(&self) -> Option { - self.current_step_mapping().cloned().or_else(|| self.mapping_for_offset(self.current_byte_offset()).cloned()) + /// Returns the debug step for the current bytecode position. + pub fn current_step(&self) -> Option> { + self.current_timeline_step().cloned().or_else(|| self.step_for_offset(self.current_byte_offset()).cloned()) } /// Returns the current bytecode offset in the script. @@ -410,7 +389,7 @@ impl<'a, 'i> DebugSession<'a, 'i> { /// Returns the source span (line/col range) at the current position. pub fn current_span(&self) -> Option { - self.current_location().and_then(|mapping| mapping.span) + self.current_step().map(|step| step.span) } pub fn call_stack(&self) -> Vec { @@ -418,10 +397,13 @@ impl<'a, 'i> DebugSession<'a, 'i> { let Some(current) = self.current_step_index else { return stack; }; - for mapping in self.source_mappings.iter().take(current + 1) { - match &mapping.kind { - MappingKind::InlineCallEnter { callee } => stack.push(callee.clone()), - MappingKind::InlineCallExit { .. } => { + for order_index in 0..=current { + let Some(step) = self.step_at_order(order_index) else { + continue; + }; + match &step.kind { + StepKind::InlineCallEnter { callee } => stack.push(callee.clone()), + StepKind::InlineCallExit { .. } => { stack.pop(); } _ => {} @@ -440,38 +422,35 @@ impl<'a, 'i> DebugSession<'a, 'i> { self.debug_info.functions.iter().find(|function| offset >= function.bytecode_start && offset < function.bytecode_end) } - fn current_variable_updates( - &self, - function_name: &str, - offset: usize, - sequence: u32, - frame_id: u32, - ) -> HashMap> { - let mut latest: HashMap> = HashMap::new(); - for update in self - .debug_info - .variable_updates - .iter() - .filter(|update| self.update_is_visible(update, function_name, offset, sequence, frame_id)) - { - match latest.get(&update.name) { - Some(existing) if existing.sequence > update.sequence => {} - _ => { - latest.insert(update.name.clone(), update); + fn current_variable_updates(&self, context: &VariableContext<'_>) -> HashMap> { + let mut latest_by_name: HashMap)> = HashMap::new(); + for step in self.debug_info.steps.iter().filter(|step| self.step_updates_are_visible(step, context)) { + for update in &step.variable_updates { + match latest_by_name.get(&update.name) { + Some((existing_sequence, _)) if *existing_sequence > step.sequence => {} + _ => { + latest_by_name.insert(update.name.clone(), (step.sequence, update)); + } } } } - latest + latest_by_name.into_iter().map(|(name, (_, update))| (name, update)).collect() } - fn current_variable_context(&self, sequence: u32, frame_id: u32) -> Result, String> { - let function_name = self.current_function_name().ok_or_else(|| "No function context available".to_string())?; - Ok(VariableContext { function_name, offset: self.current_byte_offset(), sequence, frame_id }) + fn current_variable_context(&self, step_id: StepId) -> Result, String> { + let function = self.current_function_range().ok_or_else(|| "No function context available".to_string())?; + Ok(VariableContext { + function_name: function.name.as_str(), + function_start: function.bytecode_start, + function_end: function.bytecode_end, + offset: self.current_byte_offset(), + step_id, + }) } fn collect_variables_map(&self, context: &VariableContext<'_>) -> Result, String> { let mut variables: HashMap = HashMap::new(); - let var_updates = self.current_variable_updates(context.function_name, context.offset, context.sequence, context.frame_id); + let var_updates = self.current_variable_updates(context); for (name, update) in &var_updates { let value = self.evaluate_update_with_shadow_vm(context.function_name, update).unwrap_or_else(DebugValue::Unknown); @@ -525,36 +504,30 @@ impl<'a, 'i> DebugSession<'a, 'i> { Ok(variables) } - fn update_is_visible( - &self, - update: &DebugVariableUpdate<'i>, - function_name: &str, - offset: usize, - sequence: u32, - frame_id: u32, - ) -> bool { - if update.function != function_name { + fn step_updates_are_visible(&self, step: &DebugStep<'i>, context: &VariableContext<'_>) -> bool { + if step.bytecode_start < context.function_start || step.bytecode_start >= context.function_end { return false; } // Stay in the active inline frame and only consider updates from // steps already executed in this session. - update.frame_id == frame_id - && self.executed_sequences.contains(&update.sequence) - && update.sequence < sequence - && update.bytecode_offset <= offset + let step_id = step.id(); + step_id.frame_id == context.step_id.frame_id + && self.executed_steps.contains(&step_id) + && step_id.sequence < context.step_id.sequence + && step.bytecode_end <= context.offset } - /// Returns the most specific mapping for `offset`. - /// Multiple mappings may overlap; choosing the narrowest bytecode span makes + /// Returns the most specific step for `offset`. + /// Multiple steps may overlap; choosing the narrowest bytecode span makes /// location lookups prefer inner statement/inline ranges over broader ranges. - fn mapping_for_offset(&self, offset: usize) -> Option<&DebugMapping> { - let mut best: Option<&DebugMapping> = None; + fn step_for_offset(&self, offset: usize) -> Option<&DebugStep<'i>> { + let mut best: Option<&DebugStep<'i>> = None; let mut best_len = usize::MAX; - for mapping in &self.debug_info.mappings { - if mapping_matches_offset(mapping, offset) { - let len = mapping.bytecode_end.saturating_sub(mapping.bytecode_start); + for step in &self.debug_info.steps { + if step_matches_offset(step, offset) { + let len = step.bytecode_end.saturating_sub(step.bytecode_start); if len < best_len { - best = Some(mapping); + best = Some(step); best_len = len; } } @@ -562,69 +535,72 @@ impl<'a, 'i> DebugSession<'a, 'i> { best } - fn current_step_mapping(&self) -> Option<&DebugMapping> { - self.current_step_index.and_then(|index| self.source_mappings.get(index)) + fn step_at_order(&self, order_index: usize) -> Option<&DebugStep<'i>> { + let step_index = *self.step_order.get(order_index)?; + self.debug_info.steps.get(step_index) } - fn current_step_sequence_and_frame(&self) -> (u32, u32) { - // Sequence/frame identify the statement context used for variable filtering. - self.current_step_mapping().map(|mapping| (mapping.sequence, mapping.frame_id)).unwrap_or((0, 0)) + fn current_timeline_step(&self) -> Option<&DebugStep<'i>> { + self.current_step_index.and_then(|index| self.step_at_order(index)) } - fn mark_mapping_executed(&mut self, mapping_index: usize) { - if let Some(mapping) = self.source_mappings.get(mapping_index) { - self.executed_sequences.insert(mapping.sequence); + fn current_step_id(&self) -> StepId { + self.current_timeline_step().map(DebugStep::id).unwrap_or(StepId::ROOT) + } + + fn mark_step_executed(&mut self, step_index: usize) { + if let Some(step) = self.step_at_order(step_index) { + self.executed_steps.insert(step.id()); } } fn sync_step_cursor_to_current_offset(&mut self) { let offset = self.current_byte_offset(); - if let Some(index) = self.steppable_mapping_index_for_offset(offset) { + if let Some(index) = self.steppable_step_index_for_offset(offset) { if self.current_step_index.is_some_and(|current| index < current) { - // In sequence mode multiple steps may map to the same byte offset. + // In sequence mode multiple steps may resolve to the same byte offset. // Keep cursor monotonic and avoid snapping backward to an earlier - // mapping for that offset. + // step for that offset. return; } // `si` executes raw opcodes; keep statement cursor in sync so later // source-level steps (`next`/`step`/`finish`) start from the real - // current mapping instead of an old one. + // current step instead of an old one. self.current_step_index = Some(index); - self.mark_mapping_executed(index); + self.mark_step_executed(index); } } - fn is_steppable_mapping(&self, mapping: &DebugMapping) -> bool { + fn is_steppable_step(&self, step: &DebugStep<'i>) -> bool { // InlineCallEnter is steppable so `step_into` can land on a call // boundary and build call-stack transitions. InlineCallExit is not // steppable to avoid synthetic extra stops while unwinding. - matches!(&mapping.kind, MappingKind::Statement {} | MappingKind::Virtual {} | MappingKind::InlineCallEnter { .. }) + matches!(&step.kind, StepKind::Source {} | StepKind::InlineCallEnter { .. }) } - fn steppable_mapping_index_for_offset(&self, offset: usize) -> Option { - self.source_mappings - .iter() - .enumerate() - .find(|(_, mapping)| self.is_steppable_mapping(mapping) && mapping_matches_offset(mapping, offset)) - .map(|(index, _)| index) + fn steppable_step_index_for_offset(&self, offset: usize) -> Option { + self.step_order.iter().enumerate().find_map(|(order_index, &step_index)| { + let step = self.debug_info.steps.get(step_index)?; + (self.is_steppable_step(step) && step_matches_offset(step, offset)).then_some(order_index) + }) } - fn next_steppable_mapping_index(&self, from: Option, predicate: impl Fn(&DebugMapping) -> bool) -> Option { + fn next_steppable_step_index(&self, from: Option, predicate: impl Fn(&DebugStep<'i>) -> bool) -> Option { let start = from.map(|index| index.saturating_add(1)).unwrap_or(0); - for index in start..self.source_mappings.len() { - let mapping = self.source_mappings.get(index)?; - if !self.is_steppable_mapping(mapping) { + for index in start..self.step_order.len() { + let step = self.step_at_order(index)?; + if !self.is_steppable_step(step) { continue; } - if predicate(mapping) { + if predicate(step) { return Some(index); } } None } - fn mapping_hits_breakpoint(&self, mapping: &DebugMapping) -> bool { - mapping.span.map(|span| (span.line..=span.end_line).any(|line| self.breakpoints.contains(&line))).unwrap_or(false) + fn step_hits_breakpoint(&self, step: &DebugStep<'i>) -> bool { + (step.span.line..=step.span.end_line).any(|line| self.breakpoints.contains(&line)) } /// Returns the current main stack as hex-encoded strings. @@ -768,21 +744,20 @@ fn build_opcode_offsets(opcodes: &[Option>]) -> (Vec, usi (offsets, offset) } -fn mapping_kind_order(kind: &MappingKind) -> u8 { +fn step_kind_order(kind: &StepKind) -> u8 { match kind { - MappingKind::InlineCallEnter { .. } => 0, - MappingKind::Virtual {} => 1, - MappingKind::Statement {} => 2, - MappingKind::InlineCallExit { .. } => 3, + StepKind::InlineCallEnter { .. } => 0, + StepKind::Source {} => 1, + StepKind::InlineCallExit { .. } => 2, } } -fn mapping_matches_offset(mapping: &DebugMapping, offset: usize) -> bool { - if mapping.bytecode_start == mapping.bytecode_end { - offset == mapping.bytecode_start - } else { - offset >= mapping.bytecode_start && offset < mapping.bytecode_end - } +fn range_matches_offset(bytecode_start: usize, bytecode_end: usize, offset: usize) -> bool { + if bytecode_start == bytecode_end { offset == bytecode_start } else { offset >= bytecode_start && offset < bytecode_end } +} + +fn step_matches_offset(step: &DebugStep<'_>, offset: usize) -> bool { + range_matches_offset(step.bytecode_start, step.bytecode_end, offset) } #[cfg(test)] @@ -790,12 +765,14 @@ mod tests { use super::*; use silverscript_lang::ast::{BinaryOp, Expr, ExprKind}; - use silverscript_lang::debug_info::{DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugVariableUpdate}; + use silverscript_lang::debug_info::{ + DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugStep, DebugVariableUpdate, SourceSpan, StepKind, + }; use silverscript_lang::span; fn make_session( params: Vec, - updates: Vec>, + steps: Vec>, sigscript: &[u8], ) -> Result, kaspa_txscript_errors::TxScriptError> { let sig_cache = Box::leak(Box::new(Cache::new(10_000))); @@ -804,8 +781,7 @@ mod tests { TxScriptEngine::new(EngineCtx::new(sig_cache).with_reused(reused_values), EngineFlags { covenants_enabled: true }); let debug_info = DebugInfo { source: String::new(), - mappings: vec![], - variable_updates: updates, + steps, params, functions: vec![DebugFunctionRange { name: "f".to_string(), bytecode_start: 0, bytecode_end: 1 }], constants: vec![DebugConstantMapping { name: "K".to_string(), type_name: "int".to_string(), value: Expr::int(7) }], @@ -863,21 +839,25 @@ mod tests { let mut session = make_session( vec![DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }], - vec![DebugVariableUpdate { - name: "x".to_string(), - type_name: "int".to_string(), - expr: Expr::identifier("missing"), - bytecode_offset: 0, - span: None, - function: "f".to_string(), + vec![DebugStep { + bytecode_start: 0, + bytecode_end: 0, + span: SourceSpan { line: 1, col: 1, end_line: 1, end_col: 1 }, + kind: StepKind::Source {}, sequence: 0, + call_depth: 0, frame_id: 0, + variable_updates: vec![DebugVariableUpdate { + name: "x".to_string(), + type_name: "int".to_string(), + expr: Expr::identifier("missing"), + }], }], &sigscript, ) .unwrap(); - session.executed_sequences.insert(0); + session.executed_steps.insert(StepId { sequence: 0, frame_id: 0 }); // In sequence-only mode, query visibility at an explicit sequence that // is after the update's sequence. let vars = session.list_variables_at_sequence(1, 0).unwrap(); diff --git a/debugger/session/tests/debug_session_tests.rs b/debugger/session/tests/debug_session_tests.rs index 7af0e05b..3c34e650 100644 --- a/debugger/session/tests/debug_session_tests.rs +++ b/debugger/session/tests/debug_session_tests.rs @@ -15,7 +15,7 @@ use kaspa_txscript::{EngineCtx, EngineFlags}; use debugger_session::session::{DebugSession, ShadowTxContext}; use silverscript_lang::ast::{Expr, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; -use silverscript_lang::debug_info::MappingKind; +use silverscript_lang::debug_info::StepKind; const IF_STATEMENT_CONTRACT: &str = r#"pragma silverscript ^0.1.0; @@ -64,7 +64,7 @@ where let parsed_contract = parse_contract_ast(source)?; assert_eq!(parsed_contract.params.len(), ctor_args.len()); - // Compile with debug metadata enabled so line mappings and variable updates are available. + // Compile with debug metadata enabled so line steps and variable updates are available. let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; let compiled = compile_contract(source, &ctor_args, compile_opts)?; let debug_info = compiled.debug_info.clone(); @@ -295,16 +295,19 @@ contract Virtuals() { with_session_for_source(source, vec![], "main", vec![Expr::int(3)], |session| { session.run_to_first_executed_statement()?; - let first = session.current_location().ok_or("missing first location")?; - assert!(matches!(first.kind, MappingKind::Virtual {})); + let first = session.current_step().ok_or("missing first location")?; + assert!(matches!(first.kind, StepKind::Source {})); + assert_eq!(first.bytecode_start, first.bytecode_end, "first step should be zero-width"); let first_pc = session.state().pc; - let second = session.step_over()?.ok_or("missing second step")?.mapping.ok_or("missing second mapping")?; - assert!(matches!(second.kind, MappingKind::Virtual {})); + let second = session.step_over()?.ok_or("missing second step")?.step.ok_or("missing second step payload")?; + assert!(matches!(second.kind, StepKind::Source {})); + assert_eq!(second.bytecode_start, second.bytecode_end, "second step should be zero-width"); assert_eq!(session.state().pc, first_pc, "virtual step should not execute opcodes"); - let third = session.step_over()?.ok_or("missing third step")?.mapping.ok_or("missing third mapping")?; - assert!(matches!(third.kind, MappingKind::Statement {})); + let third = session.step_over()?.ok_or("missing third step")?.step.ok_or("missing third step payload")?; + assert!(matches!(third.kind, StepKind::Source {})); + assert!(third.bytecode_end > third.bytecode_start, "third step should execute bytecode"); assert_eq!(session.state().pc, first_pc, "first real statement should still be at same pc boundary"); Ok(()) }) @@ -596,12 +599,12 @@ contract DebugPoC(int const) { with_session_for_source(source, vec![Expr::int(0)], "main", vec![Expr::int(0), Expr::int(0)], |session| { session.run_to_first_executed_statement()?; - let initial = session.current_location().ok_or("missing initial location")?; + let initial = session.current_step().ok_or("missing initial location")?; let mut prev_sequence = initial.sequence; let mut lines = vec![session.current_span().ok_or("missing initial span")?.line]; while session.step_into()?.is_some() { - let loc = session.current_location().ok_or("missing location after step_into")?; + let loc = session.current_step().ok_or("missing location after step_into")?; assert!( loc.sequence >= prev_sequence, "source sequence rewound from {} to {} (lines {:?})", diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index a4a62b6f..9c4909e9 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -162,9 +162,9 @@ fn compile_contract_impl<'i>( let mut script = field_prolog_script.clone(); script.extend(entrypoint_script); + let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); if !uses_script_size { - let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); return Ok(CompiledContract { contract_name: contract.name.clone(), script, @@ -177,7 +177,6 @@ fn compile_contract_impl<'i>( let actual_size = script.len() as i64; if Some(actual_size) == script_size { - let debug_info = recorder.into_debug_info(source.unwrap_or_default().to_string()); return Ok(CompiledContract { contract_name: contract.name.clone(), script, @@ -991,8 +990,7 @@ fn compile_function<'i>( let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { - let start = builder.script().len(); - let env_before = recorder.capture_env_snapshot(&env); + let guard = recorder.begin_statement(&builder, &env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1022,8 +1020,7 @@ fn compile_function<'i>( ) .map_err(|err| err.with_span(&stmt.span()))?; } - let end = builder.script().len(); - recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; + guard.finish(&mut recorder, stmt, &builder, &env, &types)?; } let yield_count = yields.len(); @@ -1081,7 +1078,7 @@ fn compile_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionRecorder<'i>, + recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { match stmt { Statement::VariableDefinition { type_ref, name, expr, .. } => { @@ -1276,7 +1273,7 @@ fn compile_statement<'i>( function_index, yields, script_size, - debug_recorder, + recorder, ), Statement::For { ident, start, end, body, span, .. } => compile_for_statement( ident, @@ -1297,7 +1294,7 @@ fn compile_statement<'i>( function_index, yields, script_size, - debug_recorder, + recorder, ), Statement::Yield { expr, .. } => { let mut visiting = HashSet::new(); @@ -1340,7 +1337,7 @@ fn compile_statement<'i>( let returns = compile_inline_call( name, args, - Some(SourceSpan::from(stmt.span())), + SourceSpan::from(stmt.span()), params, types, env, @@ -1351,7 +1348,7 @@ fn compile_statement<'i>( function_order, function_index, script_size, - debug_recorder, + recorder, )?; if !returns.is_empty() { let mut stack_depth = 0i64; @@ -1409,7 +1406,7 @@ fn compile_statement<'i>( let returns = compile_inline_call( name, args, - Some(SourceSpan::from(stmt.span())), + SourceSpan::from(stmt.span()), params, types, env, @@ -1420,7 +1417,7 @@ fn compile_statement<'i>( function_order, function_index, script_size, - debug_recorder, + recorder, )?; if returns.len() != bindings.len() { return Err(CompilerError::Unsupported("return values count must match function return types".to_string())); @@ -1765,7 +1762,7 @@ fn compile_validate_output_state_statement( fn compile_inline_call<'i>( name: &str, args: &[Expr<'i>], - call_span: Option, + call_span: SourceSpan, caller_params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap>, @@ -1776,7 +1773,7 @@ fn compile_inline_call<'i>( function_order: &HashMap, caller_index: usize, script_size: Option, - debug_recorder: &mut FunctionRecorder<'i>, + recorder: &mut FunctionRecorder<'i>, ) -> Result>, CompilerError> { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = @@ -1845,15 +1842,13 @@ fn compile_inline_call<'i>( } let call_start = builder.script().len(); - let mut inline_recorder = debug_recorder.start_inline_call_recording(call_span, call_start, name); - inline_recorder.record_inline_param_updates(function, &env, call_span, call_start)?; + recorder.begin_call(call_span, call_start, function, &env)?; let mut yields: Vec> = Vec::new(); let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { - let start = builder.script().len(); - let env_before = inline_recorder.capture_env_snapshot(&env); + let guard = recorder.begin_statement(builder, &env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1880,15 +1875,14 @@ fn compile_inline_call<'i>( callee_index, &mut yields, script_size, - &mut inline_recorder, + recorder, ) .map_err(|err| err.with_span(&stmt.span()))?; } - let end = builder.script().len(); - inline_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), &env, &types)?; + guard.finish(recorder, stmt, builder, &env, &types)?; } let call_end = builder.script().len(); - debug_recorder.finish_inline_call_recording(call_span, call_end, name, &inline_recorder); + recorder.finish_call(call_span, call_end, name); for (name, value) in env.iter() { if name.starts_with("__arg_") { @@ -1920,7 +1914,7 @@ fn compile_if_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionRecorder<'i>, + recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( @@ -1955,7 +1949,7 @@ fn compile_if_statement<'i>( function_index, yields, script_size, - debug_recorder, + recorder, )?; let mut else_env = original_env.clone(); @@ -1977,7 +1971,7 @@ fn compile_if_statement<'i>( function_index, yields, script_size, - debug_recorder, + recorder, )?; } @@ -2059,11 +2053,10 @@ fn compile_block<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionRecorder<'i>, + recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { for stmt in statements { - let start = builder.script().len(); - let env_before = debug_recorder.capture_env_snapshot(env); + let guard = recorder.begin_statement(builder, env); compile_statement( stmt, env, @@ -2079,11 +2072,10 @@ fn compile_block<'i>( function_index, yields, script_size, - debug_recorder, + recorder, ) .map_err(|err| err.with_span(&stmt.span()))?; - let end = builder.script().len(); - debug_recorder.record_statement_with_env_diff(stmt, start, end, env_before.as_ref(), env, types)?; + guard.finish(recorder, stmt, builder, env, types)?; } Ok(()) } @@ -2108,7 +2100,7 @@ fn compile_for_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - debug_recorder: &mut FunctionRecorder<'i>, + recorder: &mut FunctionRecorder<'i>, ) -> Result<(), CompilerError> { let start = eval_const_int(start_expr, contract_constants)?; let end = eval_const_int(end_expr, contract_constants)?; @@ -2121,13 +2113,7 @@ fn compile_for_statement<'i>( let previous = env.get(&name).cloned(); for value in start..end { env.insert(name.clone(), Expr::int(value)); - debug_recorder.record_virtual_binding( - name.clone(), - "int".to_string(), - Expr::int(value), - builder.script().len(), - Some(loop_span), - ); + recorder.record_binding(name.clone(), "int".to_string(), Expr::int(value), builder.script().len(), loop_span); compile_block( body, env, @@ -2143,7 +2129,7 @@ fn compile_for_statement<'i>( function_index, yields, script_size, - debug_recorder, + recorder, )?; } diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 47599bec..2f4d7329 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -3,77 +3,17 @@ use std::fmt; use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; use crate::debug_info::{ - DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugMapping, DebugParamMapping, DebugRecorder, DebugVariableUpdate, - MappingKind, SourceSpan, + DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugStep, DebugVariableUpdate, SourceSpan, + StepKind, }; use super::{CompilerError, resolve_expr_for_debug}; -type ResolvedVariableUpdate<'i> = (String, String, Expr<'i>); - -#[derive(Clone)] -struct FunctionRecorderSnapshot<'i> { - events: Vec, - variable_updates: Vec>, - next_frame_id: u32, -} - -trait FunctionRecorderImpl<'i> { - fn capture_env_snapshot(&self, env: &HashMap>) -> Option>>; - - fn record_statement_with_env_diff( - &mut self, - stmt: &Statement<'i>, - bytecode_start: usize, - bytecode_end: usize, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result<(), CompilerError>; - - fn record_inline_param_updates( - &mut self, - function: &FunctionAst<'i>, - env: &HashMap>, - span: Option, - bytecode_offset: usize, - ) -> Result<(), CompilerError>; - - fn record_virtual_binding( - &mut self, - name: String, - type_name: String, - expr: Expr<'i>, - bytecode_offset: usize, - span: Option, - ); - - fn start_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - ) -> Box + 'i>; - - fn finish_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - inline: &dyn FunctionRecorderImpl<'i>, - ); - - fn sequence_count(&self) -> u32; - - fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>); - - fn snapshot(&self) -> Option>; -} - /// Per-function debug recorder active during function compilation. /// Records params, statements, and variable updates for a single function. +/// When disabled (`inner` is `None`), all methods are zero-cost no-ops. pub struct FunctionRecorder<'i> { - imp: Box + 'i>, + inner: Option>, } impl fmt::Debug for FunctionRecorder<'_> { @@ -84,19 +24,7 @@ impl fmt::Debug for FunctionRecorder<'_> { impl<'i> FunctionRecorder<'i> { pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { - if enabled { - Self { imp: Box::new(ActiveFunctionRecorder::new(function, contract_fields)) } - } else { - Self { imp: Box::new(NoopFunctionRecorder) } - } - } - - fn from_impl(imp: Box + 'i>) -> Self { - Self { imp } - } - - pub fn capture_env_snapshot(&self, env: &HashMap>) -> Option>> { - self.imp.capture_env_snapshot(env) + if enabled { Self { inner: Some(ActiveFunctionRecorder::new(function, contract_fields)) } } else { Self { inner: None } } } pub fn record_statement_with_env_diff( @@ -108,186 +36,200 @@ impl<'i> FunctionRecorder<'i> { after_env: &HashMap>, types: &HashMap, ) -> Result<(), CompilerError> { - self.imp.record_statement_with_env_diff(stmt, bytecode_start, bytecode_end, before_env, after_env, types) + if let Some(rec) = &mut self.inner { + let updates = rec.collect_variable_updates(before_env, after_env, types)?; + rec.record_statement_step(stmt, bytecode_start, bytecode_end, updates); + } + Ok(()) + } + + pub fn record_binding(&mut self, name: String, type_name: String, expr: Expr<'i>, bytecode_offset: usize, span: SourceSpan) { + if let Some(rec) = &mut self.inner { + let step_index = rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::Source {}); + rec.steps[step_index].variable_updates.push(DebugVariableUpdate { name, type_name, expr }); + } } - pub fn record_inline_param_updates( + pub fn begin_call( &mut self, + span: SourceSpan, + bytecode_offset: usize, function: &FunctionAst<'i>, env: &HashMap>, - span: Option, - bytecode_offset: usize, ) -> Result<(), CompilerError> { - self.imp.record_inline_param_updates(function, env, span, bytecode_offset) + match &mut self.inner { + Some(rec) => { + let parent_depth = rec.current_call_depth(); + let callee_frame_id = rec.allocate_frame_id(); + let enter_step_index = rec.push_step_with_context( + bytecode_offset, + bytecode_offset, + span, + StepKind::InlineCallEnter { callee: function.name.clone() }, + parent_depth, + callee_frame_id, + ); + + let mut updates = Vec::new(); + for param in &function.params { + rec.resolve_variable_update( + env, + &mut updates, + ¶m.name, + ¶m.type_ref.type_name(), + env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), + )?; + } + rec.add_updates_to_step(enter_step_index, updates); + rec.push_call_frame(callee_frame_id, parent_depth.saturating_add(1)); + Ok(()) + } + None => Ok(()), + } } - pub fn record_virtual_binding( - &mut self, - name: String, - type_name: String, - expr: Expr<'i>, - bytecode_offset: usize, - span: Option, - ) { - self.imp.record_virtual_binding(name, type_name, expr, bytecode_offset, span) + pub fn finish_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str) { + if let Some(rec) = &mut self.inner { + rec.pop_call_frame(); + rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::InlineCallExit { callee: callee.to_string() }); + } } - pub fn start_inline_call_recording(&mut self, span: Option, bytecode_offset: usize, callee: &str) -> Self { - Self::from_impl(self.imp.start_inline_call_recording(span, bytecode_offset, callee)) + pub fn step_count(&self) -> u32 { + self.inner.as_ref().map_or(0, |rec| rec.next_step_sequence) } - pub fn finish_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - inline: &FunctionRecorder<'i>, - ) { - self.imp.finish_inline_call_recording(span, bytecode_offset, callee, inline.imp.as_ref()); - } - - pub fn sequence_count(&self) -> u32 { - self.imp.sequence_count() + pub fn emit_steps_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { + if let Some(rec) = &self.inner { + for step in &rec.steps { + recorder.record_step(DebugStep { + bytecode_start: step.bytecode_start + offset, + bytecode_end: step.bytecode_end + offset, + span: step.span, + kind: step.kind.clone(), + sequence: seq_base.saturating_add(step.sequence), + call_depth: step.call_depth, + frame_id: step.frame_id, + variable_updates: step.variable_updates.clone(), + }); + } + for param in &rec.params { + recorder.record_param(param.clone()); + } + } } - pub fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { - self.imp.emit_with_offset(offset, seq_base, recorder); + pub fn begin_statement(&mut self, builder: &super::ScriptBuilder, env: &HashMap>) -> StatementGuard<'i> { + StatementGuard { start: builder.script().len(), env_before: self.inner.as_ref().map(|_| env.clone()) } } } -#[derive(Debug, Default)] -struct NoopFunctionRecorder; - -impl<'i> FunctionRecorderImpl<'i> for NoopFunctionRecorder { - fn capture_env_snapshot(&self, _env: &HashMap>) -> Option>> { - None - } - - fn record_statement_with_env_diff( - &mut self, - _stmt: &Statement<'i>, - _bytecode_start: usize, - _bytecode_end: usize, - _before_env: Option<&HashMap>>, - _after_env: &HashMap>, - _types: &HashMap, - ) -> Result<(), CompilerError> { - Ok(()) - } +pub struct StatementGuard<'i> { + start: usize, + env_before: Option>>, +} - fn record_inline_param_updates( - &mut self, - _function: &FunctionAst<'i>, - _env: &HashMap>, - _span: Option, - _bytecode_offset: usize, +impl<'i> StatementGuard<'i> { + /// Finishes recording: snapshots the current bytecode offset, diffs the env, + /// and records the debug step on the given recorder. + pub fn finish( + self, + recorder: &mut FunctionRecorder<'i>, + stmt: &Statement<'i>, + builder: &super::ScriptBuilder, + env: &HashMap>, + types: &HashMap, ) -> Result<(), CompilerError> { - Ok(()) - } - - fn record_virtual_binding( - &mut self, - _name: String, - _type_name: String, - _expr: Expr<'i>, - _bytecode_offset: usize, - _span: Option, - ) { - } - - fn start_inline_call_recording( - &mut self, - _span: Option, - _bytecode_offset: usize, - _callee: &str, - ) -> Box + 'i> { - Box::new(Self) - } - - fn finish_inline_call_recording( - &mut self, - _span: Option, - _bytecode_offset: usize, - _callee: &str, - _inline: &dyn FunctionRecorderImpl<'i>, - ) { - } - - fn sequence_count(&self) -> u32 { - 0 - } - - fn emit_with_offset(&self, _offset: usize, _seq_base: u32, _recorder: &mut DebugRecorder<'i>) {} - - fn snapshot(&self) -> Option> { - None + let end = builder.script().len(); + recorder.record_statement_with_env_diff(stmt, self.start, end, self.env_before.as_ref(), env, types) } } #[derive(Debug, Default)] struct ActiveFunctionRecorder<'i> { - function_name: String, - events: Vec, - variable_updates: Vec>, - param_mappings: Vec, - next_seq: u32, - call_depth: u32, - frame_id: u32, + steps: Vec>, + params: Vec, + next_step_sequence: u32, + call_stack: Vec, next_frame_id: u32, } +#[derive(Debug, Clone, Copy)] +struct CallFrame { + frame_id: u32, + call_depth: u32, +} + impl<'i> ActiveFunctionRecorder<'i> { fn new(function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { - let mut recorder = - Self { function_name: function.name.clone(), call_depth: 0, frame_id: 0, next_frame_id: 1, ..Default::default() }; - recorder.record_stack_bindings(function, contract_fields); + let mut recorder = Self { call_stack: vec![CallFrame { frame_id: 0, call_depth: 0 }], next_frame_id: 1, ..Default::default() }; + recorder.record_param_bindings(function, contract_fields); recorder } - fn new_inline_child(&mut self) -> Self { + fn allocate_frame_id(&mut self) -> u32 { let frame_id = self.next_frame_id; self.next_frame_id = self.next_frame_id.saturating_add(1); - // Child starts allocating from the parent's current frontier. - // Parent frontier is reconciled back in `merge_inline_events` after the - // child returns, so sibling inline calls never reuse frame ids. - Self { - function_name: self.function_name.clone(), - call_depth: self.call_depth.saturating_add(1), - frame_id, - next_frame_id: self.next_frame_id, - ..Default::default() + frame_id + } + + fn push_call_frame(&mut self, frame_id: u32, call_depth: u32) { + self.call_stack.push(CallFrame { frame_id, call_depth }); + } + + fn pop_call_frame(&mut self) { + if self.call_stack.len() > 1 { + self.call_stack.pop(); } } + fn current_frame(&self) -> CallFrame { + self.call_stack.last().copied().unwrap_or(CallFrame { frame_id: 0, call_depth: 0 }) + } + + fn current_call_depth(&self) -> u32 { + self.current_frame().call_depth + } + fn next_sequence(&mut self) -> u32 { - let seq = self.next_seq; - self.next_seq = self.next_seq.saturating_add(1); + let seq = self.next_step_sequence; + self.next_step_sequence = self.next_step_sequence.saturating_add(1); seq } - fn push_event(&mut self, bytecode_start: usize, bytecode_end: usize, span: Option, kind: MappingKind) -> u32 { + fn push_step(&mut self, bytecode_start: usize, bytecode_end: usize, span: SourceSpan, kind: StepKind) -> usize { + let frame = self.current_frame(); + self.push_step_with_context(bytecode_start, bytecode_end, span, kind, frame.call_depth, frame.frame_id) + } + + fn push_step_with_context( + &mut self, + bytecode_start: usize, + bytecode_end: usize, + span: SourceSpan, + kind: StepKind, + call_depth: u32, + frame_id: u32, + ) -> usize { let sequence = self.next_sequence(); - self.events.push(DebugMapping { + self.steps.push(DebugStep { bytecode_start, bytecode_end, span, kind, sequence, - call_depth: self.call_depth, - frame_id: self.frame_id, + call_depth, + frame_id, + variable_updates: Vec::new(), }); - sequence + self.steps.len().saturating_sub(1) } - fn record_stack_bindings(&mut self, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { + fn record_param_bindings(&mut self, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { let param_count = function.params.len(); let field_count = contract_fields.len(); - // Runtime stack layout at function entry is: - // top -> contract fields (reverse declaration order), then function args. - // Keep debug stack indexes aligned with that layout so shadow evaluation - // reads the same values as normal execution. for (index, param) in function.params.iter().enumerate() { - self.param_mappings.push(DebugParamMapping { + self.params.push(DebugParamMapping { name: param.name.clone(), type_name: param.type_ref.type_name(), stack_index: (field_count + (param_count - 1 - index)) as i64, @@ -295,7 +237,7 @@ impl<'i> ActiveFunctionRecorder<'i> { }); } for (index, field) in contract_fields.iter().enumerate() { - self.param_mappings.push(DebugParamMapping { + self.params.push(DebugParamMapping { name: field.name.clone(), type_name: field.type_ref.type_name(), stack_index: (field_count - 1 - index) as i64, @@ -304,76 +246,24 @@ impl<'i> ActiveFunctionRecorder<'i> { } } - fn record_statement_span(&mut self, span: SourceSpan, bytecode_start: usize, bytecode_len: usize) -> u32 { - let kind = if bytecode_len == 0 { MappingKind::Virtual {} } else { MappingKind::Statement {} }; - self.push_event(bytecode_start, bytecode_start + bytecode_len, Some(span), kind) - } - - fn record_statement_updates( + fn record_statement_step( &mut self, stmt: &Statement<'i>, bytecode_start: usize, bytecode_end: usize, - variables: Vec>, + updates: Vec>, ) { let span = SourceSpan::from(stmt.span()); - let sequence = self.record_statement_span(span, bytecode_start, bytecode_end.saturating_sub(bytecode_start)); - self.record_variable_updates(variables, bytecode_end, Some(span), sequence); + let bytecode_len = bytecode_end.saturating_sub(bytecode_start); + let step_index = self.push_step(bytecode_start, bytecode_start + bytecode_len, span, StepKind::Source {}); + self.add_updates_to_step(step_index, updates); } - fn merge_inline_events(&mut self, inline: FunctionRecorderSnapshot<'i>) { - if inline.events.is_empty() { - // Keep frame-id frontier monotonic even if the inline call recorded - // no events; this preserves uniqueness for later sibling calls. - self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); + fn add_updates_to_step(&mut self, step_index: usize, updates: Vec>) { + let Some(step) = self.steps.get_mut(step_index) else { return; - } - - let mut seq_map: HashMap = HashMap::new(); - let mut events = inline.events; - events.sort_by_key(|event| event.sequence); - - for mut event in events { - let local_seq = event.sequence; - let merged_seq = self.next_sequence(); - event.sequence = merged_seq; - self.events.push(event); - seq_map.insert(local_seq, merged_seq); - } - - let mut updates = inline.variable_updates; - updates.sort_by_key(|update| update.sequence); - for mut update in updates { - if let Some(merged_seq) = seq_map.get(&update.sequence) { - update.sequence = *merged_seq; - self.variable_updates.push(update); - } - } - - // Child may allocate nested frame ids; advance parent frontier so later - // sibling inline calls start after the whole child subtree. - self.next_frame_id = self.next_frame_id.max(inline.next_frame_id); - } - - fn record_variable_updates( - &mut self, - variables: Vec>, - bytecode_offset: usize, - span: Option, - sequence: u32, - ) { - for (name, type_name, expr) in variables { - self.variable_updates.push(DebugVariableUpdate { - name, - type_name, - expr, - bytecode_offset, - span, - function: self.function_name.clone(), - sequence, - frame_id: self.frame_id, - }); - } + }; + step.variable_updates.extend(updates); } fn collect_variable_updates( @@ -381,18 +271,16 @@ impl<'i> ActiveFunctionRecorder<'i> { before_env: Option<&HashMap>>, after_env: &HashMap>, types: &HashMap, - ) -> Result>, CompilerError> { + ) -> Result>, CompilerError> { let Some(before_env) = before_env else { return Ok(Vec::new()); }; - // Stable ordering keeps debug metadata deterministic across runs. let mut names: Vec = after_env.keys().cloned().collect(); names.sort_unstable(); let mut updates = Vec::new(); for name in names { - // Inline synthetic args are plumbing, not user-facing variables. if name.starts_with("__arg_") { continue; } @@ -405,147 +293,29 @@ impl<'i> ActiveFunctionRecorder<'i> { let Some(type_name) = types.get(&name) else { continue; }; - self.variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; + self.resolve_variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; } Ok(updates) } - /// Records a variable update by resolving its expression against the current environment. - /// This expands locals and synthetic inline placeholders (`__arg_*`) into - /// caller-visible expressions, leaving only real param identifiers. - fn variable_update( + fn resolve_variable_update( &self, env: &HashMap>, - variables: &mut Vec>, + updates: &mut Vec>, name: &str, type_name: &str, expr: Expr<'i>, ) -> Result<(), CompilerError> { let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; - variables.push((name.to_string(), type_name.to_string(), resolved)); + updates.push(DebugVariableUpdate { name: name.to_string(), type_name: type_name.to_string(), expr: resolved }); Ok(()) } } -impl<'i> FunctionRecorderImpl<'i> for ActiveFunctionRecorder<'i> { - fn capture_env_snapshot(&self, env: &HashMap>) -> Option>> { - Some(env.clone()) - } - - fn record_statement_with_env_diff( - &mut self, - stmt: &Statement<'i>, - bytecode_start: usize, - bytecode_end: usize, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result<(), CompilerError> { - let updates = self.collect_variable_updates(before_env, after_env, types)?; - self.record_statement_updates(stmt, bytecode_start, bytecode_end, updates); - Ok(()) - } - - fn record_inline_param_updates( - &mut self, - function: &FunctionAst<'i>, - env: &HashMap>, - span: Option, - bytecode_offset: usize, - ) -> Result<(), CompilerError> { - // Anchor inline param updates to the next callee statement sequence. - // We intentionally "peek" (do not consume) so these updates align with - // the first real callee statement event sequence. - let sequence = self.next_seq; - let mut variables = Vec::new(); - for param in &function.params { - self.variable_update( - env, - &mut variables, - ¶m.name, - ¶m.type_ref.type_name(), - env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), - )?; - } - self.record_variable_updates(variables, bytecode_offset, span, sequence); - Ok(()) - } - - fn record_virtual_binding( - &mut self, - name: String, - type_name: String, - expr: Expr<'i>, - bytecode_offset: usize, - span: Option, - ) { - let sequence = self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::Virtual {}); - self.variable_updates.push(DebugVariableUpdate { - name, - type_name, - expr, - bytecode_offset, - span, - function: self.function_name.clone(), - sequence, - frame_id: self.frame_id, - }); - } - - fn start_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - ) -> Box + 'i> { - self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::InlineCallEnter { callee: callee.to_string() }); - Box::new(self.new_inline_child()) - } - - fn finish_inline_call_recording( - &mut self, - span: Option, - bytecode_offset: usize, - callee: &str, - inline: &dyn FunctionRecorderImpl<'i>, - ) { - if let Some(snapshot) = inline.snapshot() { - self.merge_inline_events(snapshot); - } - self.push_event(bytecode_offset, bytecode_offset, span, MappingKind::InlineCallExit { callee: callee.to_string() }); - } - - fn sequence_count(&self) -> u32 { - self.next_seq - } - - fn emit_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { - emit_events_with_offset(&self.events, offset, seq_base, recorder); - emit_variable_updates_with_offset(&self.variable_updates, offset, seq_base, recorder); - record_param_mappings(&self.param_mappings, recorder); - } - - fn snapshot(&self) -> Option> { - Some(FunctionRecorderSnapshot { - events: self.events.clone(), - variable_updates: self.variable_updates.clone(), - next_frame_id: self.next_frame_id, - }) - } -} - -trait ContractRecorderImpl<'i> { - fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]); - - fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize); - - fn into_debug_info(self: Box, source: String) -> Option>; -} - -/// Global debug recording sink that can be enabled or disabled. -/// When Off, all recording calls become no-ops with zero overhead. +/// Contract-level debug recorder that merges per-function recordings. +/// When disabled (`inner` is `None`), all methods are zero-cost no-ops. pub struct ContractRecorder<'i> { - imp: Box + 'i>, + inner: Option>, } impl fmt::Debug for ContractRecorder<'_> { @@ -556,32 +326,35 @@ impl fmt::Debug for ContractRecorder<'_> { impl<'i> ContractRecorder<'i> { pub fn new(enabled: bool) -> Self { - if enabled { Self { imp: Box::new(ActiveContractRecorder::default()) } } else { Self { imp: Box::new(NoopContractRecorder) } } + if enabled { Self { inner: Some(ActiveContractRecorder::default()) } } else { Self { inner: None } } } pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { - self.imp.record_constructor_constants(params, values); + if let Some(rec) = &mut self.inner { + for (param, value) in params.iter().zip(values.iter()) { + rec.recorder.record_constant(DebugConstantMapping { + name: param.name.clone(), + type_name: param.type_ref.type_name(), + value: value.clone(), + }); + } + } } pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize) { - self.imp.record_compiled_function(name, script_len, debug, offset); + if let Some(rec) = &mut self.inner { + let seq_base = rec.recorder.reserve_sequence_block(debug.step_count()); + debug.emit_steps_with_offset(offset, seq_base, &mut rec.recorder); + rec.recorder.record_function(DebugFunctionRange { + name: name.to_string(), + bytecode_start: offset, + bytecode_end: offset + script_len, + }); + } } pub fn into_debug_info(self, source: String) -> Option> { - self.imp.into_debug_info(source) - } -} - -#[derive(Debug, Default)] -struct NoopContractRecorder; - -impl<'i> ContractRecorderImpl<'i> for NoopContractRecorder { - fn record_constructor_constants(&mut self, _params: &[ParamAst<'i>], _values: &[Expr<'i>]) {} - - fn record_compiled_function(&mut self, _name: &str, _script_len: usize, _debug: &FunctionRecorder<'i>, _offset: usize) {} - - fn into_debug_info(self: Box, _source: String) -> Option> { - None + self.inner.map(|rec| rec.recorder.into_debug_info(source)) } } @@ -590,78 +363,12 @@ struct ActiveContractRecorder<'i> { recorder: DebugRecorder<'i>, } -impl<'i> ContractRecorderImpl<'i> for ActiveContractRecorder<'i> { - fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { - for (param, value) in params.iter().zip(values.iter()) { - self.recorder.record_constant(DebugConstantMapping { - name: param.name.clone(), - type_name: param.type_ref.type_name(), - value: value.clone(), - }); - } - } - - fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize) { - let seq_base = self.recorder.reserve_sequence_block(debug.sequence_count()); - debug.emit_with_offset(offset, seq_base, &mut self.recorder); - self.recorder.record_function(DebugFunctionRange { - name: name.to_string(), - bytecode_start: offset, - bytecode_end: offset + script_len, - }); - } - - fn into_debug_info(self: Box, source: String) -> Option> { - Some(self.recorder.into_debug_info(source)) - } -} - -fn emit_events_with_offset(events: &[DebugMapping], offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'_>) { - for event in events { - recorder.record(DebugMapping { - bytecode_start: event.bytecode_start + offset, - bytecode_end: event.bytecode_end + offset, - span: event.span, - kind: event.kind.clone(), - sequence: seq_base.saturating_add(event.sequence), - call_depth: event.call_depth, - frame_id: event.frame_id, - }); - } -} - -fn emit_variable_updates_with_offset<'i>( - updates: &[DebugVariableUpdate<'i>], - offset: usize, - seq_base: u32, - recorder: &mut DebugRecorder<'i>, -) { - for update in updates { - recorder.record_variable_update(DebugVariableUpdate { - name: update.name.clone(), - type_name: update.type_name.clone(), - expr: update.expr.clone(), - bytecode_offset: update.bytecode_offset + offset, - span: update.span, - function: update.function.clone(), - sequence: seq_base.saturating_add(update.sequence), - frame_id: update.frame_id, - }); - } -} - -fn record_param_mappings(params: &[DebugParamMapping], recorder: &mut DebugRecorder<'_>) { - for param in params { - recorder.record_param(param.clone()); - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; use crate::ast::{Expr, parse_contract_ast}; - use crate::debug_info::MappingKind; + use crate::debug_info::StepKind; use super::{ContractRecorder, FunctionRecorder, SourceSpan}; @@ -680,14 +387,15 @@ mod tests { let stmt = function.body.first().expect("statement"); let mut recorder = FunctionRecorder::new(false, function, &contract.fields); - assert!(recorder.capture_env_snapshot(&HashMap::new()).is_none()); + + let span = SourceSpan::from(stmt.span()); recorder.record_statement_with_env_diff(stmt, 0, 1, None, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); - let inline = recorder.start_inline_call_recording(None, 1, "callee"); - recorder.finish_inline_call_recording(None, 2, "callee", &inline); - recorder.record_virtual_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, None); - assert_eq!(recorder.sequence_count(), 0); + recorder.begin_call(span, 1, function, &HashMap::new()).expect("noop begin call recording"); + recorder.finish_call(span, 2, "callee"); + recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, span); + assert_eq!(recorder.step_count(), 0); let mut sink = ContractRecorder::new(false); sink.record_constructor_constants(&contract.params, &[]); @@ -721,29 +429,45 @@ mod tests { types.insert("x".to_string(), "int".to_string()); types.insert("y".to_string(), "int".to_string()); - recorder.record_statement_with_env_diff(stmt, 0, 1, Some(&before), &after, &types).expect("record first statement"); + recorder.record_statement_with_env_diff(stmt, 0, 1, Some(&before), &after, &types).expect("record_step first statement"); let span = SourceSpan::from(stmt.span()); - let mut inline = recorder.start_inline_call_recording(Some(span), 1, "callee"); - inline.record_virtual_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, Some(span)); - recorder.finish_inline_call_recording(Some(span), 2, "callee", &inline); + let mut inline_env = HashMap::new(); + inline_env.insert("x".to_string(), Expr::int(3)); + recorder.begin_call(span, 1, function, &inline_env).expect("begin call recording"); + recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, span); + recorder.finish_call(span, 2, "callee"); - assert_eq!(recorder.sequence_count(), 4); + assert_eq!(recorder.step_count(), 4); let mut sink = ContractRecorder::new(true); sink.record_compiled_function("spend", 2, &recorder, 0); let info = sink.into_debug_info(String::new()).expect("debug info available"); - let sequences = info.mappings.iter().map(|mapping| mapping.sequence).collect::>(); + let sequences = info.steps.iter().map(|step| step.sequence).collect::>(); assert_eq!(sequences, vec![0, 1, 2, 3]); - let virtual_mapping = - info.mappings.iter().find(|mapping| matches!(&mapping.kind, MappingKind::Virtual {})).expect("virtual mapping exists"); - assert_eq!(virtual_mapping.frame_id, 1); - - let tmp_update = info.variable_updates.iter().find(|update| update.name == "tmp").expect("tmp update exists"); - assert_eq!(tmp_update.frame_id, 1); - assert_eq!(tmp_update.sequence, virtual_mapping.sequence); + let inline_enter_step = info + .steps + .iter() + .find(|step| matches!(&step.kind, StepKind::InlineCallEnter { .. }) && step.frame_id == 1) + .expect("inline enter step exists"); + assert!(inline_enter_step.variable_updates.iter().any(|update| update.name == "x")); + + let inline_zero_width_source_step = info + .steps + .iter() + .find(|step| { + step.is_zero_width() + && step.frame_id == 1 + && matches!(&step.kind, StepKind::Source {}) + && step.variable_updates.iter().any(|update| update.name == "tmp") + }) + .expect("inline zero-width source step exists"); + assert_eq!(inline_zero_width_source_step.variable_updates.len(), 1); + + let tmp_update = inline_zero_width_source_step.variable_updates.first().expect("tmp update exists"); + assert_eq!(tmp_update.name, "tmp"); assert!(info.params.iter().any(|param| param.name == "x")); } diff --git a/silverscript-lang/src/debug_info.rs b/silverscript-lang/src/debug_info.rs index 569a9fae..493cf009 100644 --- a/silverscript-lang/src/debug_info.rs +++ b/silverscript-lang/src/debug_info.rs @@ -18,12 +18,11 @@ impl<'a> From> for SourceSpan { } /// Accumulates debug metadata during compilation. -/// Collects events, variable updates, param mappings, function ranges, and constants. +/// Collects steps, variable updates, param mappings, function ranges, and constants. /// Converted to `DebugInfo` after compilation completes. #[derive(Debug, Default)] pub struct DebugRecorder<'i> { - events: Vec, - variable_updates: Vec>, + steps: Vec>, params: Vec, functions: Vec, constants: Vec>, @@ -31,12 +30,8 @@ pub struct DebugRecorder<'i> { } impl<'i> DebugRecorder<'i> { - pub fn record(&mut self, mapping: DebugMapping) { - self.events.push(mapping); - } - - pub fn record_variable_update(&mut self, update: DebugVariableUpdate<'i>) { - self.variable_updates.push(update); + pub fn record_step(&mut self, step: DebugStep<'i>) { + self.steps.push(step); } pub fn record_param(&mut self, param: DebugParamMapping) { @@ -68,14 +63,7 @@ impl<'i> DebugRecorder<'i> { } pub fn into_debug_info(self, source: String) -> DebugInfo<'i> { - DebugInfo { - source, - mappings: self.events, - variable_updates: self.variable_updates, - params: self.params, - functions: self.functions, - constants: self.constants, - } + DebugInfo { source, steps: self.steps, params: self.params, functions: self.functions, constants: self.constants } } } @@ -84,8 +72,7 @@ impl<'i> DebugRecorder<'i> { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DebugInfo<'i> { pub source: String, - pub mappings: Vec, - pub variable_updates: Vec>, + pub steps: Vec>, pub params: Vec, pub functions: Vec, pub constants: Vec>, @@ -93,14 +80,7 @@ pub struct DebugInfo<'i> { impl<'i> DebugInfo<'i> { pub fn empty() -> Self { - Self { - source: String::new(), - mappings: Vec::new(), - variable_updates: Vec::new(), - params: Vec::new(), - functions: Vec::new(), - constants: Vec::new(), - } + Self { source: String::new(), steps: Vec::new(), params: Vec::new(), functions: Vec::new(), constants: Vec::new() } } } @@ -111,15 +91,6 @@ pub struct DebugVariableUpdate<'i> { /// Pre-resolved expression with all local variable references expanded inline. /// Only function parameter Identifiers remain. Enables shadow VM evaluation. pub expr: Expr<'i>, - pub bytecode_offset: usize, - pub span: Option, - pub function: String, - /// Sequence of the statement/virtual mapping that produced this update. - /// The debugger uses this to show locals only after that step executes. - #[serde(default)] - pub sequence: u32, - #[serde(default)] - pub frame_id: u32, } /// Maps function parameter to its stack position. @@ -151,31 +122,63 @@ pub struct DebugConstantMapping<'i> { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DebugMapping { +pub struct DebugStep<'i> { pub bytecode_start: usize, pub bytecode_end: usize, - pub span: Option, - pub kind: MappingKind, - /// Global event order used as a stable tiebreak for overlapping mappings. + pub span: SourceSpan, + pub kind: StepKind, + /// Global step order used as a stable tiebreak for overlapping steps. #[serde(default)] pub sequence: u32, #[serde(default)] pub call_depth: u32, #[serde(default)] pub frame_id: u32, + #[serde(default)] + pub variable_updates: Vec>, +} + +impl<'i> DebugStep<'i> { + pub fn id(&self) -> StepId { + StepId { sequence: self.sequence, frame_id: self.frame_id } + } + + pub fn is_zero_width(&self) -> bool { + self.bytecode_start == self.bytecode_end + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct StepId { + pub sequence: u32, + pub frame_id: u32, +} + +impl StepId { + pub const ROOT: Self = Self { sequence: 0, frame_id: 0 }; + + pub fn new(sequence: u32, frame_id: u32) -> Self { + Self { sequence, frame_id } + } } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum MappingKind { - Statement {}, - Virtual {}, - InlineCallEnter { callee: String }, - InlineCallExit { callee: String }, +pub enum StepKind { + #[serde(alias = "Statement", alias = "Virtual")] + Source {}, + InlineCallEnter { + callee: String, + }, + InlineCallExit { + callee: String, + }, } #[cfg(test)] mod tests { - use super::SourceSpan; + use serde_json::json; + + use super::{DebugInfo, SourceSpan, StepKind}; use crate::span::Span; #[test] @@ -188,4 +191,97 @@ mod tests { assert_eq!(source_span.end_line, 2); assert_eq!(source_span.end_col, 5); } + + #[test] + fn debug_info_schema_requires_step_span() { + let value = json!({ + "source": "", + "steps": [{ + "bytecode_start": 0, + "bytecode_end": 1, + "kind": { "Source": {} }, + "sequence": 0, + "call_depth": 0, + "frame_id": 0, + "variable_updates": [] + }], + "variable_updates": [], + "params": [], + "functions": [], + "constants": [] + }); + + let parsed: Result, _> = serde_json::from_value(value); + assert!(parsed.is_err(), "step span should be required"); + } + + #[test] + fn debug_info_schema_nests_variable_updates_in_steps() { + let value = json!({ + "source": "", + "steps": [{ + "bytecode_start": 0, + "bytecode_end": 1, + "span": { "line": 1, "col": 1, "end_line": 1, "end_col": 1 }, + "kind": { "Source": {} }, + "sequence": 0, + "call_depth": 0, + "frame_id": 0, + "variable_updates": [] + }], + "variable_updates": [], + "params": [], + "functions": [], + "constants": [] + }); + + let parsed: DebugInfo<'static> = serde_json::from_value(value).expect("parse debug info"); + let serialized = serde_json::to_value(parsed).expect("serialize debug info"); + + assert!(serialized.get("variable_updates").is_none(), "top-level variable_updates should not exist"); + assert!(serialized["steps"][0].get("variable_updates").is_some(), "step should carry variable_updates"); + } + + #[test] + fn debug_info_schema_accepts_legacy_statement_and_virtual_kind_names() { + let statement_value = json!({ + "source": "", + "steps": [{ + "bytecode_start": 0, + "bytecode_end": 1, + "span": { "line": 1, "col": 1, "end_line": 1, "end_col": 1 }, + "kind": { "Statement": {} }, + "sequence": 0, + "call_depth": 0, + "frame_id": 0, + "variable_updates": [] + }], + "params": [], + "functions": [], + "constants": [] + }); + + let virtual_value = json!({ + "source": "", + "steps": [{ + "bytecode_start": 0, + "bytecode_end": 0, + "span": { "line": 1, "col": 1, "end_line": 1, "end_col": 1 }, + "kind": { "Virtual": {} }, + "sequence": 0, + "call_depth": 0, + "frame_id": 0, + "variable_updates": [] + }], + "params": [], + "functions": [], + "constants": [] + }); + + let statement: DebugInfo<'static> = serde_json::from_value(statement_value).expect("legacy statement parses"); + let virtual_step: DebugInfo<'static> = serde_json::from_value(virtual_value).expect("legacy virtual parses"); + + assert!(matches!(statement.steps[0].kind, StepKind::Source {})); + assert!(matches!(virtual_step.steps[0].kind, StepKind::Source {})); + } } diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 2c0e06c4..77b5fd02 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -166,7 +166,7 @@ fn compile_contract_emits_debug_info_when_recording_enabled() { let options = CompileOptions { record_debug_infos: true, ..Default::default() }; let compiled = compile_contract(source, &[], options).expect("compile succeeds"); let debug_info = compiled.debug_info.expect("debug info should be present"); - assert!(!debug_info.mappings.is_empty()); + assert!(!debug_info.steps.is_empty()); assert!(!debug_info.functions.is_empty()); assert!(debug_info.params.iter().any(|param| param.name == "x")); } From 71a44aa70df19fdc7c96666e53cf1c4f3ee2a344 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:29:37 +0200 Subject: [PATCH 36/41] More docs, and better namings --- debugger/session/src/session.rs | 151 +++++++++++++--- silverscript-lang/src/compiler.rs | 164 ++++++++---------- .../src/compiler/debug_recording.rs | 149 +++++++++------- silverscript-lang/src/debug_info.rs | 15 +- 4 files changed, 294 insertions(+), 185 deletions(-) diff --git a/debugger/session/src/session.rs b/debugger/session/src/session.rs index 30bb485d..4b5825fd 100644 --- a/debugger/session/src/session.rs +++ b/debugger/session/src/session.rs @@ -453,7 +453,11 @@ impl<'a, 'i> DebugSession<'a, 'i> { let var_updates = self.current_variable_updates(context); for (name, update) in &var_updates { - let value = self.evaluate_update_with_shadow_vm(context.function_name, update).unwrap_or_else(DebugValue::Unknown); + if is_inline_synthetic_name(name) { + continue; + } + let value = + self.evaluate_update_with_shadow_vm(context.function_name, update, &var_updates).unwrap_or_else(DebugValue::Unknown); variables.insert( name.clone(), Variable { @@ -609,25 +613,34 @@ impl<'a, 'i> DebugSession<'a, 'i> { stacks.dstack.iter().map(|item| encode_hex(item)).collect() } - fn evaluate_update_with_shadow_vm(&self, function_name: &str, update: &DebugVariableUpdate<'i>) -> Result { - self.evaluate_expr_with_shadow_vm(function_name, &update.type_name, &update.expr) - } - /// Evaluates an expression using shadow VM execution. /// /// Strategy: compile the pre-resolved expression to bytecode, build a mini-script /// that pushes current param values then executes the bytecode, run on fresh VM, /// read result from top of stack. This guarantees debugger sees same semantics as /// real execution without duplicating evaluation logic. - fn evaluate_expr_with_shadow_vm(&self, function_name: &str, type_name: &str, expr: &Expr<'i>) -> Result { + fn evaluate_update_with_shadow_vm( + &self, + function_name: &str, + update: &DebugVariableUpdate<'i>, + updates: &HashMap>, + ) -> Result { let params = self.shadow_param_values(function_name)?; + let type_name = &update.type_name; + let expr = &update.expr; let mut param_indexes = HashMap::new(); let mut param_types = HashMap::new(); for param in ¶ms { param_indexes.insert(param.name.clone(), param.stack_index); param_types.insert(param.name.clone(), param.type_name.clone()); } - let bytecode = compile_debug_expr(expr, ¶m_indexes, ¶m_types) + let mut env: HashMap> = HashMap::new(); + let mut eval_types = param_types; + for (name, update) in updates { + env.insert((*name).clone(), update.expr.clone()); + eval_types.insert((*name).clone(), update.type_name.clone()); + } + let bytecode = compile_debug_expr(expr, &env, ¶m_indexes, &eval_types) .map_err(|err| format!("failed to compile debug expression: {err}"))?; let script = self.build_shadow_script(¶ms, &bytecode)?; let bytes = self.execute_shadow_script(&script)?; @@ -760,6 +773,10 @@ fn step_matches_offset(step: &DebugStep<'_>, offset: usize) -> bool { range_matches_offset(step.bytecode_start, step.bytecode_end, offset) } +fn is_inline_synthetic_name(name: &str) -> bool { + name.starts_with("__arg_") +} + #[cfg(test)] mod tests { use super::*; @@ -814,20 +831,15 @@ mod tests { ) .unwrap(); - let value = session - .evaluate_expr_with_shadow_vm( - "f", - "int", - &Expr::new( - ExprKind::Binary { - op: BinaryOp::Add, - left: Box::new(Expr::identifier("a")), - right: Box::new(Expr::identifier("b")), - }, - span::Span::default(), - ), - ) - .unwrap(); + let update = DebugVariableUpdate { + name: "x".to_string(), + type_name: "int".to_string(), + expr: Expr::new( + ExprKind::Binary { op: BinaryOp::Add, left: Box::new(Expr::identifier("a")), right: Box::new(Expr::identifier("b")) }, + span::Span::default(), + ), + }; + let value = session.evaluate_update_with_shadow_vm("f", &update, &HashMap::new()).unwrap(); assert!(matches!(value, DebugValue::Int(12))); } @@ -864,4 +876,101 @@ mod tests { let x = vars.into_iter().find(|var| var.name == "x").expect("x variable"); assert!(matches!(x.value, DebugValue::Unknown(_))); } + + #[test] + fn list_variables_hides_inline_synthetics_but_uses_them_for_shadow_eval() { + let mut sig_builder = ScriptBuilder::new(); + sig_builder.add_i64(5).unwrap(); + let sigscript = sig_builder.drain(); + + let mut session = make_session( + vec![DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }], + vec![DebugStep { + bytecode_start: 0, + bytecode_end: 0, + span: SourceSpan { line: 1, col: 1, end_line: 1, end_col: 1 }, + kind: StepKind::Source {}, + sequence: 0, + call_depth: 0, + frame_id: 0, + variable_updates: vec![ + DebugVariableUpdate { name: "__arg_f_0".to_string(), type_name: "int".to_string(), expr: Expr::identifier("a") }, + DebugVariableUpdate { + name: "x".to_string(), + type_name: "int".to_string(), + expr: Expr::new( + ExprKind::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::identifier("__arg_f_0")), + right: Box::new(Expr::int(1)), + }, + span::Span::default(), + ), + }, + ], + }], + &sigscript, + ) + .unwrap(); + + session.executed_steps.insert(StepId { sequence: 0, frame_id: 0 }); + let vars = session.list_variables_at_sequence(1, 0).unwrap(); + + assert!(!vars.iter().any(|var| var.name.starts_with("__arg_"))); + let x = vars.into_iter().find(|var| var.name == "x").expect("x variable"); + assert!(matches!(x.value, DebugValue::Int(6))); + } + + #[test] + fn shadow_eval_resolves_nested_inline_synthetic_chain() { + let mut sig_builder = ScriptBuilder::new(); + sig_builder.add_i64(5).unwrap(); + let sigscript = sig_builder.drain(); + + let mut session = make_session( + vec![DebugParamMapping { name: "a".to_string(), type_name: "int".to_string(), stack_index: 0, function: "f".to_string() }], + vec![DebugStep { + bytecode_start: 0, + bytecode_end: 0, + span: SourceSpan { line: 1, col: 1, end_line: 1, end_col: 1 }, + kind: StepKind::Source {}, + sequence: 0, + call_depth: 0, + frame_id: 0, + variable_updates: vec![ + DebugVariableUpdate { + name: "__arg_outer_0".to_string(), + type_name: "int".to_string(), + expr: Expr::identifier("a"), + }, + DebugVariableUpdate { + name: "__arg_inner_0".to_string(), + type_name: "int".to_string(), + expr: Expr::identifier("__arg_outer_0"), + }, + DebugVariableUpdate { + name: "x".to_string(), + type_name: "int".to_string(), + expr: Expr::new( + ExprKind::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::identifier("__arg_inner_0")), + right: Box::new(Expr::int(1)), + }, + span::Span::default(), + ), + }, + ], + }], + &sigscript, + ) + .unwrap(); + + session.executed_steps.insert(StepId { sequence: 0, frame_id: 0 }); + let vars = session.list_variables_at_sequence(1, 0).unwrap(); + + assert!(!vars.iter().any(|var| var.name.starts_with("__arg_"))); + let x = vars.into_iter().find(|var| var.name == "x").expect("x variable"); + assert!(matches!(x.value, DebugValue::Int(6))); + } } diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 9c4909e9..eacc8995 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -16,7 +16,7 @@ use crate::span; mod debug_recording; -use debug_recording::{ContractRecorder, FunctionRecorder}; +use debug_recording::{ContractRecorder, EntrypointRecorder}; #[derive(Debug, Clone, Copy, Default)] pub struct CompileOptions { @@ -113,7 +113,7 @@ fn compile_contract_impl<'i>( recorder.record_constructor_constants(&contract.params, constructor_args); for (index, func) in contract.functions.iter().enumerate() { if func.entrypoint { - compiled_entrypoints.push(compile_function( + compiled_entrypoints.push(compile_entrypoint_function( func, index, &contract.fields, @@ -131,7 +131,7 @@ fn compile_contract_impl<'i>( let compiled = compiled_entrypoints .first() .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, field_prolog_script.len()); + recorder.record_compiled_entrypoint(&compiled.name, compiled.script.len(), &compiled.debug, field_prolog_script.len()); compiled.script.clone() } else { let mut builder = ScriptBuilder::new(); @@ -143,7 +143,7 @@ fn compile_contract_impl<'i>( builder.add_op(OpIf)?; builder.add_op(OpDrop)?; let start = field_prolog_script.len() + builder.script().len(); - recorder.record_compiled_function(&compiled.name, compiled.script.len(), &compiled.debug, start); + recorder.record_compiled_entrypoint(&compiled.name, compiled.script.len(), &compiled.debug, start); builder.add_ops(&compiled.script)?; builder.add_op(OpElse)?; if index == total - 1 { @@ -906,13 +906,13 @@ pub fn function_branch_index<'i>(contract: &ContractAst<'i>, function_name: &str } #[derive(Debug)] -struct CompiledFunction<'i> { +struct CompiledEntryPoint<'i> { name: String, script: Vec, - debug: FunctionRecorder<'i>, + debug: EntrypointRecorder<'i>, } -fn compile_function<'i>( +fn compile_entrypoint_function<'i>( function: &FunctionAst<'i>, function_index: usize, contract_fields: &[ContractFieldAst<'i>], @@ -922,7 +922,7 @@ fn compile_function<'i>( functions: &HashMap>, function_order: &HashMap, script_size: Option, -) -> Result, CompilerError> { +) -> Result, CompilerError> { let contract_field_count = contract_fields.len(); let param_count = function.params.len(); let mut params = function @@ -961,7 +961,7 @@ fn compile_function<'i>( env.remove(¶m.name); } let mut builder = ScriptBuilder::new(); - let mut recorder = FunctionRecorder::new(options.record_debug_infos, function, contract_fields); + let mut recorder = EntrypointRecorder::new(options.record_debug_infos, function, contract_fields); let mut yields: Vec = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { @@ -1059,7 +1059,7 @@ fn compile_function<'i>( builder.add_op(OpDrop)?; } } - Ok(CompiledFunction { name: function.name.clone(), script: builder.drain(), debug: recorder }) + Ok(CompiledEntryPoint { name: function.name.clone(), script: builder.drain(), debug: recorder }) } #[allow(clippy::too_many_arguments)] @@ -1078,7 +1078,7 @@ fn compile_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, ) -> Result<(), CompilerError> { match stmt { Statement::VariableDefinition { type_ref, name, expr, .. } => { @@ -1773,7 +1773,7 @@ fn compile_inline_call<'i>( function_order: &HashMap, caller_index: usize, script_size: Option, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, ) -> Result>, CompilerError> { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = @@ -1803,7 +1803,7 @@ fn compile_inline_call<'i>( let mut env: HashMap> = contract_constants.clone(); // Preserve caller synthetic inline bindings so nested inline calls can - // continue resolving chains like __arg_inner_0 -> __arg_outer_0. + // resolve chains like __arg_inner_0 -> __arg_outer_0 during compilation. for (name, value) in caller_env.iter() { if name.starts_with("__arg_") { env.insert(name.clone(), value.clone()); @@ -1842,7 +1842,7 @@ fn compile_inline_call<'i>( } let call_start = builder.script().len(); - recorder.begin_call(call_span, call_start, function, &env)?; + recorder.begin_inline_call(call_span, call_start, function, &env)?; let mut yields: Vec> = Vec::new(); let params = caller_params.clone(); @@ -1882,7 +1882,7 @@ fn compile_inline_call<'i>( guard.finish(recorder, stmt, builder, &env, &types)?; } let call_end = builder.script().len(); - recorder.finish_call(call_span, call_end, name); + recorder.finish_inline_call(call_span, call_end, name); for (name, value) in env.iter() { if name.starts_with("__arg_") { @@ -1914,7 +1914,7 @@ fn compile_if_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( @@ -2053,7 +2053,7 @@ fn compile_block<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, ) -> Result<(), CompilerError> { for stmt in statements { let guard = recorder.begin_statement(builder, env); @@ -2100,7 +2100,7 @@ fn compile_for_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, ) -> Result<(), CompilerError> { let start = eval_const_int(start_expr, contract_constants)?; let end = eval_const_int(end_expr, contract_constants)?; @@ -2185,117 +2185,104 @@ fn resolve_expr<'i>( expr: Expr<'i>, env: &HashMap>, visiting: &mut HashSet, -) -> Result, CompilerError> { - resolve_expr_with_inline_synthetics(expr, env, visiting, false) -} - -/// Shared expression resolver used by both compile-time resolution and -/// debugger placeholder expansion. -/// -/// - `expand_inline_synthetics = false`: preserve `__arg_*` placeholders. -/// - `expand_inline_synthetics = true`: resolve only `__arg_*` placeholders. -fn resolve_expr_with_inline_synthetics<'i>( - expr: Expr<'i>, - env: &HashMap>, - visiting: &mut HashSet, - expand_inline_synthetics: bool, ) -> Result, CompilerError> { let Expr { kind, span } = expr; match kind { ExprKind::Identifier(name) => { - let is_inline_synthetic = name.starts_with("__arg_"); - if !expand_inline_synthetics && is_inline_synthetic { - return Ok(Expr::new(ExprKind::Identifier(name), span)); - } - if expand_inline_synthetics && !is_inline_synthetic { + if name.starts_with("__arg_") { return Ok(Expr::new(ExprKind::Identifier(name), span)); } - if let Some(value) = env.get(&name).cloned() { + if let Some(value) = env.get(&name) { if !visiting.insert(name.clone()) { return Err(CompilerError::CyclicIdentifier(name)); } - let resolved = resolve_expr_with_inline_synthetics(value, env, visiting, expand_inline_synthetics)?; + let resolved = resolve_expr(value.clone(), env, visiting)?; visiting.remove(&name); Ok(resolved) } else { Ok(Expr::new(ExprKind::Identifier(name), span)) } } - other => rewrite_expr_children(Expr::new(other, span), |child| { - resolve_expr_with_inline_synthetics(child, env, visiting, expand_inline_synthetics) - }), - } -} - -fn rewrite_expr_children<'i>( - expr: Expr<'i>, - mut recurse: impl FnMut(Expr<'i>) -> Result, CompilerError>, -) -> Result, CompilerError> { - let Expr { kind, span } = expr; - match kind { - ExprKind::Unary { op, expr } => Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(recurse(*expr)?) }, span)), - ExprKind::Binary { op, left, right } => { - Ok(Expr::new(ExprKind::Binary { op, left: Box::new(recurse(*left)?), right: Box::new(recurse(*right)?) }, span)) + ExprKind::Unary { op, expr } => { + Ok(Expr::new(ExprKind::Unary { op, expr: Box::new(resolve_expr(*expr, env, visiting)?) }, span)) } + ExprKind::Binary { op, left, right } => Ok(Expr::new( + ExprKind::Binary { + op, + left: Box::new(resolve_expr(*left, env, visiting)?), + right: Box::new(resolve_expr(*right, env, visiting)?), + }, + span, + )), ExprKind::IfElse { condition, then_expr, else_expr } => Ok(Expr::new( ExprKind::IfElse { - condition: Box::new(recurse(*condition)?), - then_expr: Box::new(recurse(*then_expr)?), - else_expr: Box::new(recurse(*else_expr)?), + condition: Box::new(resolve_expr(*condition, env, visiting)?), + then_expr: Box::new(resolve_expr(*then_expr, env, visiting)?), + else_expr: Box::new(resolve_expr(*else_expr, env, visiting)?), }, span, )), ExprKind::Array(values) => { - let mut rewritten = Vec::with_capacity(values.len()); + let mut resolved = Vec::with_capacity(values.len()); for value in values { - rewritten.push(recurse(value)?); + resolved.push(resolve_expr(value, env, visiting)?); } - Ok(Expr::new(ExprKind::Array(rewritten), span)) + Ok(Expr::new(ExprKind::Array(resolved), span)) } ExprKind::StateObject(fields) => { - let mut rewritten = Vec::with_capacity(fields.len()); + let mut resolved_fields = Vec::with_capacity(fields.len()); for field in fields { - rewritten.push(StateFieldExpr { + resolved_fields.push(StateFieldExpr { name: field.name, - expr: recurse(field.expr)?, + expr: resolve_expr(field.expr, env, visiting)?, span: field.span, name_span: field.name_span, }); } - Ok(Expr::new(ExprKind::StateObject(rewritten), span)) + Ok(Expr::new(ExprKind::StateObject(resolved_fields), span)) } ExprKind::Call { name, args, name_span } => { - let mut rewritten = Vec::with_capacity(args.len()); + let mut resolved = Vec::with_capacity(args.len()); for arg in args { - rewritten.push(recurse(arg)?); + resolved.push(resolve_expr(arg, env, visiting)?); } - Ok(Expr::new(ExprKind::Call { name, args: rewritten, name_span }, span)) + Ok(Expr::new(ExprKind::Call { name, args: resolved, name_span }, span)) } ExprKind::New { name, args, name_span } => { - let mut rewritten = Vec::with_capacity(args.len()); + let mut resolved = Vec::with_capacity(args.len()); for arg in args { - rewritten.push(recurse(arg)?); + resolved.push(resolve_expr(arg, env, visiting)?); } - Ok(Expr::new(ExprKind::New { name, args: rewritten, name_span }, span)) + Ok(Expr::new(ExprKind::New { name, args: resolved, name_span }, span)) } ExprKind::Split { source, index, part, span: split_span } => Ok(Expr::new( - ExprKind::Split { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?), part, span: split_span }, + ExprKind::Split { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + part, + span: split_span, + }, + span, + )), + ExprKind::ArrayIndex { source, index } => Ok(Expr::new( + ExprKind::ArrayIndex { + source: Box::new(resolve_expr(*source, env, visiting)?), + index: Box::new(resolve_expr(*index, env, visiting)?), + }, span, )), - ExprKind::ArrayIndex { source, index } => { - Ok(Expr::new(ExprKind::ArrayIndex { source: Box::new(recurse(*source)?), index: Box::new(recurse(*index)?) }, span)) - } ExprKind::Introspection { kind, index, field_span } => { - Ok(Expr::new(ExprKind::Introspection { kind, index: Box::new(recurse(*index)?), field_span }, span)) - } - ExprKind::UnarySuffix { source, kind, span: suffix_span } => { - Ok(Expr::new(ExprKind::UnarySuffix { source: Box::new(recurse(*source)?), kind, span: suffix_span }, span)) + Ok(Expr::new(ExprKind::Introspection { kind, index: Box::new(resolve_expr(*index, env, visiting)?), field_span }, span)) } + ExprKind::UnarySuffix { source, kind, span: suffix_span } => Ok(Expr::new( + ExprKind::UnarySuffix { source: Box::new(resolve_expr(*source, env, visiting)?), kind, span: suffix_span }, + span, + )), ExprKind::Slice { source, start, end, span: slice_span } => Ok(Expr::new( ExprKind::Slice { - source: Box::new(recurse(*source)?), - start: Box::new(recurse(*start)?), - end: Box::new(recurse(*end)?), + source: Box::new(resolve_expr(*source, env, visiting)?), + start: Box::new(resolve_expr(*start, env, visiting)?), + end: Box::new(resolve_expr(*end, env, visiting)?), span: slice_span, }, span, @@ -3849,16 +3836,16 @@ fn data_prefix(data_len: usize) -> Vec { /// Compiles a pre-resolved expression for debugger shadow evaluation. pub fn compile_debug_expr<'i>( expr: &Expr<'i>, + env: &HashMap>, params: &HashMap, types: &HashMap, ) -> Result, CompilerError> { - let env = HashMap::new(); let constants = HashMap::new(); let mut builder = ScriptBuilder::new(); let mut stack_depth = 0i64; compile_expr( expr, - &env, + env, params, types, &mut builder, @@ -3876,16 +3863,7 @@ pub(super) fn resolve_expr_for_debug<'i>( env: &HashMap>, visiting: &mut HashSet, ) -> Result, CompilerError> { - let resolved = resolve_expr(expr, env, visiting)?; - expand_inline_arg_placeholders(resolved, env, &mut HashSet::new()) -} - -fn expand_inline_arg_placeholders<'i>( - expr: Expr<'i>, - env: &HashMap>, - visiting: &mut HashSet, -) -> Result, CompilerError> { - resolve_expr_with_inline_synthetics(expr, env, visiting, true) + resolve_expr(expr, env, visiting) } #[cfg(test)] diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 2f4d7329..711e2ec6 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -12,37 +12,24 @@ use super::{CompilerError, resolve_expr_for_debug}; /// Per-function debug recorder active during function compilation. /// Records params, statements, and variable updates for a single function. /// When disabled (`inner` is `None`), all methods are zero-cost no-ops. -pub struct FunctionRecorder<'i> { - inner: Option>, +pub struct EntrypointRecorder<'i> { + inner: Option>, } -impl fmt::Debug for FunctionRecorder<'_> { +impl fmt::Debug for EntrypointRecorder<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FunctionRecorder").finish_non_exhaustive() + f.debug_struct("EntrypointRecorder").finish_non_exhaustive() } } -impl<'i> FunctionRecorder<'i> { +impl<'i> EntrypointRecorder<'i> { + /// Creates a recorder for one function compile pass. + /// If disabled, all methods are no-ops. pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { - if enabled { Self { inner: Some(ActiveFunctionRecorder::new(function, contract_fields)) } } else { Self { inner: None } } - } - - pub fn record_statement_with_env_diff( - &mut self, - stmt: &Statement<'i>, - bytecode_start: usize, - bytecode_end: usize, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result<(), CompilerError> { - if let Some(rec) = &mut self.inner { - let updates = rec.collect_variable_updates(before_env, after_env, types)?; - rec.record_statement_step(stmt, bytecode_start, bytecode_end, updates); - } - Ok(()) + if enabled { Self { inner: Some(ActiveEntrypointRecorder::new(function, contract_fields)) } } else { Self { inner: None } } } + /// Records one explicit variable binding as its own zero-width Source step. pub fn record_binding(&mut self, name: String, type_name: String, expr: Expr<'i>, bytecode_offset: usize, span: SourceSpan) { if let Some(rec) = &mut self.inner { let step_index = rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::Source {}); @@ -50,7 +37,9 @@ impl<'i> FunctionRecorder<'i> { } } - pub fn begin_call( + /// Records inline-call entry and opens a new debug frame. + /// Adds synthetic args first, then callee params. + pub fn begin_inline_call( &mut self, span: SourceSpan, bytecode_offset: usize, @@ -71,6 +60,14 @@ impl<'i> FunctionRecorder<'i> { ); let mut updates = Vec::new(); + let mut synthetic_names: Vec = env.keys().filter(|name| name.starts_with("__arg_")).cloned().collect(); + // Sort so updates are emitted in a fixed order. + synthetic_names.sort_unstable(); + for name in synthetic_names { + if let Some(expr) = env.get(&name).cloned() { + rec.resolve_variable_update(env, &mut updates, &name, "internal", expr)?; + } + } for param in &function.params { rec.resolve_variable_update( env, @@ -88,37 +85,30 @@ impl<'i> FunctionRecorder<'i> { } } - pub fn finish_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str) { + /// Records inline-call exit and restores caller frame context. + pub fn finish_inline_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str) { if let Some(rec) = &mut self.inner { rec.pop_call_frame(); rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::InlineCallExit { callee: callee.to_string() }); } } + /// Returns how many steps were recorded for this function. pub fn step_count(&self) -> u32 { self.inner.as_ref().map_or(0, |rec| rec.next_step_sequence) } - pub fn emit_steps_with_offset(&self, offset: usize, seq_base: u32, recorder: &mut DebugRecorder<'i>) { - if let Some(rec) = &self.inner { - for step in &rec.steps { - recorder.record_step(DebugStep { - bytecode_start: step.bytecode_start + offset, - bytecode_end: step.bytecode_end + offset, - span: step.span, - kind: step.kind.clone(), - sequence: seq_base.saturating_add(step.sequence), - call_depth: step.call_depth, - frame_id: step.frame_id, - variable_updates: step.variable_updates.clone(), - }); - } - for param in &rec.params { - recorder.record_param(param.clone()); - } - } + /// Returns recorded per-function steps. + pub fn steps(&self) -> &[DebugStep<'i>] { + self.inner.as_ref().map_or(&[], |rec| rec.steps.as_slice()) + } + + /// Returns recorded per-function parameter mappings. + pub fn params(&self) -> &[DebugParamMapping] { + self.inner.as_ref().map_or(&[], |rec| rec.params.as_slice()) } + /// Starts statement recording by capturing current byte offset and env. pub fn begin_statement(&mut self, builder: &super::ScriptBuilder, env: &HashMap>) -> StatementGuard<'i> { StatementGuard { start: builder.script().len(), env_before: self.inner.as_ref().map(|_| env.clone()) } } @@ -134,19 +124,22 @@ impl<'i> StatementGuard<'i> { /// and records the debug step on the given recorder. pub fn finish( self, - recorder: &mut FunctionRecorder<'i>, + recorder: &mut EntrypointRecorder<'i>, stmt: &Statement<'i>, builder: &super::ScriptBuilder, env: &HashMap>, types: &HashMap, ) -> Result<(), CompilerError> { let end = builder.script().len(); - recorder.record_statement_with_env_diff(stmt, self.start, end, self.env_before.as_ref(), env, types) + if let Some(active) = &mut recorder.inner { + active.record_statement(stmt, self.start, end, self.env_before.as_ref(), env, types)?; + } + Ok(()) } } #[derive(Debug, Default)] -struct ActiveFunctionRecorder<'i> { +struct ActiveEntrypointRecorder<'i> { steps: Vec>, params: Vec, next_step_sequence: u32, @@ -160,7 +153,7 @@ struct CallFrame { call_depth: u32, } -impl<'i> ActiveFunctionRecorder<'i> { +impl<'i> ActiveEntrypointRecorder<'i> { fn new(function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { let mut recorder = Self { call_stack: vec![CallFrame { frame_id: 0, call_depth: 0 }], next_frame_id: 1, ..Default::default() }; recorder.record_param_bindings(function, contract_fields); @@ -246,17 +239,21 @@ impl<'i> ActiveFunctionRecorder<'i> { } } - fn record_statement_step( + fn record_statement( &mut self, stmt: &Statement<'i>, bytecode_start: usize, bytecode_end: usize, - updates: Vec>, - ) { + before_env: Option<&HashMap>>, + after_env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError> { + let updates = self.collect_variable_updates(before_env, after_env, types)?; let span = SourceSpan::from(stmt.span()); let bytecode_len = bytecode_end.saturating_sub(bytecode_start); let step_index = self.push_step(bytecode_start, bytecode_start + bytecode_len, span, StepKind::Source {}); self.add_updates_to_step(step_index, updates); + Ok(()) } fn add_updates_to_step(&mut self, step_index: usize, updates: Vec>) { @@ -277,13 +274,11 @@ impl<'i> ActiveFunctionRecorder<'i> { }; let mut names: Vec = after_env.keys().cloned().collect(); + // Sort so updates are emitted in a fixed order. names.sort_unstable(); let mut updates = Vec::new(); for name in names { - if name.starts_with("__arg_") { - continue; - } let Some(after_expr) = after_env.get(&name) else { continue; }; @@ -313,7 +308,7 @@ impl<'i> ActiveFunctionRecorder<'i> { } /// Contract-level debug recorder that merges per-function recordings. -/// When disabled (`inner` is `None`), all methods are zero-cost no-ops. +/// When disabled (`inner` is `None`), all methods are no-ops. pub struct ContractRecorder<'i> { inner: Option>, } @@ -325,10 +320,12 @@ impl fmt::Debug for ContractRecorder<'_> { } impl<'i> ContractRecorder<'i> { + /// Creates a contract-level recorder. pub fn new(enabled: bool) -> Self { if enabled { Self { inner: Some(ActiveContractRecorder::default()) } } else { Self { inner: None } } } + /// Records constructor constants for debugger visibility. pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { if let Some(rec) = &mut self.inner { for (param, value) in params.iter().zip(values.iter()) { @@ -341,10 +338,25 @@ impl<'i> ContractRecorder<'i> { } } - pub fn record_compiled_function(&mut self, name: &str, script_len: usize, debug: &FunctionRecorder<'i>, offset: usize) { + /// Merges one compiled entrypoint recording into contract-level debug info. + pub fn record_compiled_entrypoint(&mut self, name: &str, script_len: usize, debug: &EntrypointRecorder<'i>, offset: usize) { if let Some(rec) = &mut self.inner { let seq_base = rec.recorder.reserve_sequence_block(debug.step_count()); - debug.emit_steps_with_offset(offset, seq_base, &mut rec.recorder); + for step in debug.steps() { + rec.recorder.record_step(DebugStep { + bytecode_start: step.bytecode_start + offset, + bytecode_end: step.bytecode_end + offset, + span: step.span, + kind: step.kind.clone(), + sequence: seq_base.saturating_add(step.sequence), + call_depth: step.call_depth, + frame_id: step.frame_id, + variable_updates: step.variable_updates.clone(), + }); + } + for param in debug.params() { + rec.recorder.record_param(param.clone()); + } rec.recorder.record_function(DebugFunctionRange { name: name.to_string(), bytecode_start: offset, @@ -353,6 +365,7 @@ impl<'i> ContractRecorder<'i> { } } + /// Finalizes and returns `DebugInfo` when recording is enabled. pub fn into_debug_info(self, source: String) -> Option> { self.inner.map(|rec| rec.recorder.into_debug_info(source)) } @@ -370,7 +383,7 @@ mod tests { use crate::ast::{Expr, parse_contract_ast}; use crate::debug_info::StepKind; - use super::{ContractRecorder, FunctionRecorder, SourceSpan}; + use super::{ContractRecorder, EntrypointRecorder, SourceSpan}; #[test] fn noop_recorders_are_pure_noops() { @@ -386,20 +399,22 @@ mod tests { let function = contract.functions.first().expect("function"); let stmt = function.body.first().expect("statement"); - let mut recorder = FunctionRecorder::new(false, function, &contract.fields); + let mut recorder = EntrypointRecorder::new(false, function, &contract.fields); let span = SourceSpan::from(stmt.span()); - recorder.record_statement_with_env_diff(stmt, 0, 1, None, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); + let builder = super::super::ScriptBuilder::new(); + let guard = recorder.begin_statement(&builder, &HashMap::new()); + guard.finish(&mut recorder, stmt, &builder, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); - recorder.begin_call(span, 1, function, &HashMap::new()).expect("noop begin call recording"); - recorder.finish_call(span, 2, "callee"); + recorder.begin_inline_call(span, 1, function, &HashMap::new()).expect("noop begin call recording"); + recorder.finish_inline_call(span, 2, "callee"); recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, span); assert_eq!(recorder.step_count(), 0); let mut sink = ContractRecorder::new(false); sink.record_constructor_constants(&contract.params, &[]); - sink.record_compiled_function("spend", 1, &recorder, 0); + sink.record_compiled_entrypoint("spend", 1, &recorder, 0); assert!(sink.into_debug_info(String::new()).is_none()); } @@ -417,7 +432,7 @@ mod tests { let function = contract.functions.first().expect("function"); let stmt = function.body.first().expect("statement"); - let mut recorder = FunctionRecorder::new(true, function, &contract.fields); + let mut recorder = EntrypointRecorder::new(true, function, &contract.fields); let mut before = HashMap::new(); before.insert("x".to_string(), Expr::identifier("x")); @@ -429,19 +444,21 @@ mod tests { types.insert("x".to_string(), "int".to_string()); types.insert("y".to_string(), "int".to_string()); - recorder.record_statement_with_env_diff(stmt, 0, 1, Some(&before), &after, &types).expect("record_step first statement"); + let builder = super::super::ScriptBuilder::new(); + let guard = recorder.begin_statement(&builder, &before); + guard.finish(&mut recorder, stmt, &builder, &after, &types).expect("record_step first statement"); let span = SourceSpan::from(stmt.span()); let mut inline_env = HashMap::new(); inline_env.insert("x".to_string(), Expr::int(3)); - recorder.begin_call(span, 1, function, &inline_env).expect("begin call recording"); + recorder.begin_inline_call(span, 1, function, &inline_env).expect("begin call recording"); recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, span); - recorder.finish_call(span, 2, "callee"); + recorder.finish_inline_call(span, 2, "callee"); assert_eq!(recorder.step_count(), 4); let mut sink = ContractRecorder::new(true); - sink.record_compiled_function("spend", 2, &recorder, 0); + sink.record_compiled_entrypoint("spend", 2, &recorder, 0); let info = sink.into_debug_info(String::new()).expect("debug info available"); let sequences = info.steps.iter().map(|step| step.sequence).collect::>(); diff --git a/silverscript-lang/src/debug_info.rs b/silverscript-lang/src/debug_info.rs index 493cf009..40b45715 100644 --- a/silverscript-lang/src/debug_info.rs +++ b/silverscript-lang/src/debug_info.rs @@ -24,24 +24,28 @@ impl<'a> From> for SourceSpan { pub struct DebugRecorder<'i> { steps: Vec>, params: Vec, - functions: Vec, + entry_points: Vec, constants: Vec>, next_sequence: u32, } impl<'i> DebugRecorder<'i> { + /// Appends one recorded step. pub fn record_step(&mut self, step: DebugStep<'i>) { self.steps.push(step); } + /// Appends one parameter stack mapping. pub fn record_param(&mut self, param: DebugParamMapping) { self.params.push(param); } + /// Appends one compiled function bytecode range. pub fn record_function(&mut self, function: DebugFunctionRange) { - self.functions.push(function); + self.entry_points.push(function); } + /// Appends one constructor constant mapping. pub fn record_constant(&mut self, constant: DebugConstantMapping<'i>) { self.constants.push(constant); } @@ -62,8 +66,9 @@ impl<'i> DebugRecorder<'i> { base } + /// Builds the final serializable debug payload. pub fn into_debug_info(self, source: String) -> DebugInfo<'i> { - DebugInfo { source, steps: self.steps, params: self.params, functions: self.functions, constants: self.constants } + DebugInfo { source, steps: self.steps, params: self.params, functions: self.entry_points, constants: self.constants } } } @@ -88,8 +93,8 @@ impl<'i> DebugInfo<'i> { pub struct DebugVariableUpdate<'i> { pub name: String, pub type_name: String, - /// Pre-resolved expression with all local variable references expanded inline. - /// Only function parameter Identifiers remain. Enables shadow VM evaluation. + /// Pre-resolved expression for debugger shadow evaluation. + /// Identifiers may include inline synthetic placeholders (`__arg_*`). pub expr: Expr<'i>, } From 8c8afdc5df707864ef7991cbb34fcc383dbfdc19 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:02:02 +0200 Subject: [PATCH 37/41] fix: function param shadowing and nested inline call arg resolution - Remove constructor/constant names from env when they collide with function param names (prioritizing function parameters). - Propagate caller's __arg_ bindings and params map into inline calls, allowing nested synthetic argument chains to resolve correctly. - Extract magic string into pub const SYNTHETIC_ARG_PREFIX. - Add regression tests for both fixes. --- silverscript-lang/src/compiler.rs | 24 ++++++++-- silverscript-lang/tests/compiler_tests.rs | 54 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 05c05361..1c5b34ae 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -13,6 +13,9 @@ use crate::ast::{ pub use crate::errors::{CompilerError, ErrorSpan}; use crate::span; +/// Prefix used for synthetic argument bindings during inline function expansion. +pub const SYNTHETIC_ARG_PREFIX: &str = "__arg_"; + #[derive(Debug, Clone, Copy, Default)] pub struct CompileOptions { pub allow_yield: bool, @@ -927,6 +930,10 @@ fn compile_function<'i>( } } let mut env: HashMap> = constants.clone(); + // Remove any constructor/constant names that collide with function param names (prioritizing function parameters on name collision). + for param in &function.params { + env.remove(¶m.name); + } let mut builder = ScriptBuilder::new(); let mut yields: Vec = Vec::new(); @@ -1296,6 +1303,7 @@ fn compile_statement<'i>( let returns = compile_inline_call( name, args, + params, types, env, builder, @@ -1362,6 +1370,7 @@ fn compile_statement<'i>( let returns = compile_inline_call( name, args, + params, types, env, builder, @@ -1715,6 +1724,7 @@ fn compile_validate_output_state_statement( fn compile_inline_call<'i>( name: &str, args: &[Expr<'i>], + caller_params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap>, builder: &mut ScriptBuilder, @@ -1752,9 +1762,15 @@ fn compile_inline_call<'i>( } let mut env: HashMap> = contract_constants.clone(); + // Copy the caller's __arg_ (function param) bindings into the new inline call's env, allowing nested synthetic argument chain. + for (name, value) in caller_env.iter() { + if name.starts_with(SYNTHETIC_ARG_PREFIX) { + env.insert(name.clone(), value.clone()); + } + } for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let temp_name = format!("__arg_{name}_{index}"); + let temp_name = format!("{SYNTHETIC_ARG_PREFIX}{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); env.insert(temp_name.clone(), resolved.clone()); types.insert(temp_name.clone(), param_type_name.clone()); @@ -1785,7 +1801,7 @@ fn compile_inline_call<'i>( } let mut yields: Vec> = Vec::new(); - let params = HashMap::new(); + let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { if let Statement::Return { exprs, .. } = stmt { @@ -1820,7 +1836,7 @@ fn compile_inline_call<'i>( } for (name, value) in env.iter() { - if name.starts_with("__arg_") { + if name.starts_with(SYNTHETIC_ARG_PREFIX) { if let Some(type_name) = types.get(name) { caller_types.entry(name.clone()).or_insert_with(|| type_name.clone()); } @@ -2112,7 +2128,7 @@ fn resolve_expr<'i>( let Expr { kind, span } = expr; match kind { ExprKind::Identifier(name) => { - if name.starts_with("__arg_") { + if name.starts_with(SYNTHETIC_ARG_PREFIX) { return Ok(Expr::new(ExprKind::Identifier(name), span)); } if let Some(value) = env.get(&name) { diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 96f2302c..667b0bbe 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -3559,3 +3559,57 @@ fn empty_array_statement_expr_evaluation_compiles_to_empty_array_data() { assert_eq!(compiled.script[0], OpFalse); assert_eq!(compiled.script[1], OpFalse); } + +#[test] +fn function_param_shadows_constructor_constant_with_same_name() { + // When a constructor constant and a function parameter share the same name, + // the function parameter value must be used (not the constant). + let source = r#" + contract Shadow(int fee) { + entrypoint function main(int fee) { + int local = fee + 1; + require(local == 4); + } + } + "#; + + // Constructor fee=2, param fee=3 => local = 3+1 = 4 => pass + let compiled = compile_contract(source, &[Expr::int(2)], CompileOptions::default()).expect("compile succeeds"); + let sigscript = compiled.build_sig_script("main", vec![Expr::int(3)]).expect("sigscript builds"); + let result = run_script_with_sigscript(compiled.script.clone(), sigscript); + assert!(result.is_ok(), "function param should shadow constructor constant: {}", result.unwrap_err()); + + // Constructor fee=2, param fee=2 => local = 2+1 = 3 != 4 => fail (proves it's not always the constant) + let sigscript_wrong = compiled.build_sig_script("main", vec![Expr::int(2)]).expect("sigscript builds"); + let result_wrong = run_script_with_sigscript(compiled.script, sigscript_wrong); + assert!(result_wrong.is_err(), "require(3==4) should fail, proving the param value matters"); +} + +#[test] +fn nested_inline_calls_with_args_compile_and_execute() { + // Nested inline calls must propagate synthetic __arg_ bindings so that + // inner calls can resolve arguments that flow through outer calls. + let source = r#" + contract NestedArgs() { + function inner(int x) { + int y = x + 1; + require(y > 0); + } + + function outer(int v) { + inner(v); + require(v >= 0); + } + + entrypoint function main(int a) { + outer(a); + require(a >= 0); + } + } + "#; + + let compiled = compile_contract(source, &[], CompileOptions::default()).expect("nested inline calls should compile"); + let sigscript = compiled.build_sig_script("main", vec![Expr::int(5)]).expect("sigscript builds"); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "nested inline calls should execute correctly: {}", result.unwrap_err()); +} From 95c9654923f2aadecd9fdc3a9f74d80fb72c6850 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:15:01 +0200 Subject: [PATCH 38/41] Refactor debug recorder to unify entrypoint and contract recorders --- silverscript-lang/src/compiler.rs | 60 +- .../src/compiler/debug_recording.rs | 624 +++++++++++------- silverscript-lang/src/debug_info.rs | 15 +- silverscript-lang/tests/compiler_tests.rs | 83 +++ 4 files changed, 492 insertions(+), 290 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 365e0c9c..15dd8fd4 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -16,7 +16,7 @@ use crate::span; mod debug_recording; -use debug_recording::{ContractRecorder, EntrypointRecorder}; +use debug_recording::DebugRecorder; /// Prefix used for synthetic argument bindings during inline function expansion. pub const SYNTHETIC_ARG_PREFIX: &str = "__arg_"; @@ -111,7 +111,7 @@ fn compile_contract_impl<'i>( let (_contract_fields, field_prolog_script) = compile_contract_fields(&contract.fields, &constants, options, script_size)?; let mut compiled_entrypoints = Vec::new(); - let mut recorder = ContractRecorder::new(options.record_debug_infos); + let mut recorder = DebugRecorder::new(options.record_debug_infos); recorder.record_constructor_constants(&contract.params, constructor_args); for (index, func) in contract.functions.iter().enumerate() { if func.entrypoint { @@ -125,28 +125,29 @@ fn compile_contract_impl<'i>( &functions_map, &function_order, script_size, + &mut recorder, )?); } } let entrypoint_script = if without_selector { - let compiled = compiled_entrypoints + let (name, script) = compiled_entrypoints .first() .ok_or_else(|| CompilerError::Unsupported("contract has no entrypoint functions".to_string()))?; - recorder.record_compiled_entrypoint(&compiled.name, compiled.script.len(), &compiled.debug, field_prolog_script.len()); - compiled.script.clone() + recorder.set_entrypoint_start(name, field_prolog_script.len()); + script.clone() } else { let mut builder = ScriptBuilder::new(); let total = compiled_entrypoints.len(); - for (index, compiled) in compiled_entrypoints.iter().enumerate() { + for (index, (name, script)) in compiled_entrypoints.iter().enumerate() { builder.add_op(OpDup)?; builder.add_i64(index as i64)?; builder.add_op(OpNumEqual)?; builder.add_op(OpIf)?; builder.add_op(OpDrop)?; let start = field_prolog_script.len() + builder.script().len(); - recorder.record_compiled_entrypoint(&compiled.name, compiled.script.len(), &compiled.debug, start); - builder.add_ops(&compiled.script)?; + recorder.set_entrypoint_start(name, start); + builder.add_ops(script)?; builder.add_op(OpElse)?; if index == total - 1 { builder.add_op(OpDrop)?; @@ -907,13 +908,6 @@ pub fn function_branch_index<'i>(contract: &ContractAst<'i>, function_name: &str .ok_or_else(|| CompilerError::Unsupported(format!("function '{function_name}' not found"))) } -#[derive(Debug)] -struct CompiledEntryPoint<'i> { - name: String, - script: Vec, - debug: EntrypointRecorder<'i>, -} - fn compile_entrypoint_function<'i>( function: &FunctionAst<'i>, function_index: usize, @@ -924,7 +918,8 @@ fn compile_entrypoint_function<'i>( functions: &HashMap>, function_order: &HashMap, script_size: Option, -) -> Result, CompilerError> { + recorder: &mut DebugRecorder<'i>, +) -> Result<(String, Vec), CompilerError> { let contract_field_count = contract_fields.len(); let param_count = function.params.len(); let mut params = function @@ -962,7 +957,6 @@ fn compile_entrypoint_function<'i>( env.remove(¶m.name); } let mut builder = ScriptBuilder::new(); - let mut recorder = EntrypointRecorder::new(options.record_debug_infos, function, contract_fields); let mut yields: Vec = Vec::new(); if !options.allow_yield && function.body.iter().any(contains_yield) { @@ -989,9 +983,11 @@ fn compile_entrypoint_function<'i>( } } + recorder.begin_entrypoint(&function.name, function, contract_fields); + let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { - let guard = recorder.begin_statement(&builder, &env); + recorder.begin_statement_at(builder.script().len(), &env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1017,11 +1013,11 @@ fn compile_entrypoint_function<'i>( function_index, &mut yields, script_size, - &mut recorder, + recorder, ) .map_err(|err| err.with_span(&stmt.span()))?; } - guard.finish(&mut recorder, stmt, &builder, &env, &types)?; + recorder.finish_statement_at(stmt, builder.script().len(), &env, &types)?; } let yield_count = yields.len(); @@ -1060,7 +1056,9 @@ fn compile_entrypoint_function<'i>( builder.add_op(OpDrop)?; } } - Ok(CompiledEntryPoint { name: function.name.clone(), script: builder.drain(), debug: recorder }) + let script = builder.drain(); + recorder.finish_entrypoint(script.len()); + Ok((function.name.clone(), script)) } #[allow(clippy::too_many_arguments)] @@ -1079,7 +1077,7 @@ fn compile_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut EntrypointRecorder<'i>, + recorder: &mut DebugRecorder<'i>, ) -> Result<(), CompilerError> { match stmt { Statement::VariableDefinition { type_ref, name, expr, .. } => { @@ -1774,7 +1772,7 @@ fn compile_inline_call<'i>( function_order: &HashMap, caller_index: usize, script_size: Option, - recorder: &mut EntrypointRecorder<'i>, + recorder: &mut DebugRecorder<'i>, ) -> Result>, CompilerError> { let function = functions.get(name).ok_or_else(|| CompilerError::Unsupported(format!("function '{}' not found", name)))?; let callee_index = @@ -1848,7 +1846,7 @@ fn compile_inline_call<'i>( let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { - let guard = recorder.begin_statement(builder, &env); + recorder.begin_statement_at(builder.script().len(), &env); if let Statement::Return { exprs, .. } = stmt { if index != body_len - 1 { return Err(CompilerError::Unsupported("return statement must be the last statement".to_string())); @@ -1879,7 +1877,7 @@ fn compile_inline_call<'i>( ) .map_err(|err| err.with_span(&stmt.span()))?; } - guard.finish(recorder, stmt, builder, &env, &types)?; + recorder.finish_statement_at(stmt, builder.script().len(), &env, &types)?; } let call_end = builder.script().len(); recorder.finish_inline_call(call_span, call_end, name); @@ -1914,7 +1912,7 @@ fn compile_if_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut EntrypointRecorder<'i>, + recorder: &mut DebugRecorder<'i>, ) -> Result<(), CompilerError> { let mut stack_depth = 0i64; compile_expr( @@ -2053,10 +2051,10 @@ fn compile_block<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut EntrypointRecorder<'i>, + recorder: &mut DebugRecorder<'i>, ) -> Result<(), CompilerError> { for stmt in statements { - let guard = recorder.begin_statement(builder, env); + recorder.begin_statement_at(builder.script().len(), env); compile_statement( stmt, env, @@ -2075,7 +2073,7 @@ fn compile_block<'i>( recorder, ) .map_err(|err| err.with_span(&stmt.span()))?; - guard.finish(recorder, stmt, builder, env, types)?; + recorder.finish_statement_at(stmt, builder.script().len(), env, types)?; } Ok(()) } @@ -2100,7 +2098,7 @@ fn compile_for_statement<'i>( function_index: usize, yields: &mut Vec>, script_size: Option, - recorder: &mut EntrypointRecorder<'i>, + recorder: &mut DebugRecorder<'i>, ) -> Result<(), CompilerError> { let start = eval_const_int(start_expr, contract_constants)?; let end = eval_const_int(end_expr, contract_constants)?; @@ -2113,7 +2111,7 @@ fn compile_for_statement<'i>( let previous = env.get(&name).cloned(); for value in start..end { env.insert(name.clone(), Expr::int(value)); - recorder.record_binding(name.clone(), "int".to_string(), Expr::int(value), builder.script().len(), loop_span); + recorder.record_variable_binding(name.clone(), "int".to_string(), Expr::int(value), builder.script().len(), loop_span); compile_block( body, env, diff --git a/silverscript-lang/src/compiler/debug_recording.rs b/silverscript-lang/src/compiler/debug_recording.rs index 711e2ec6..6d4a9ee5 100644 --- a/silverscript-lang/src/compiler/debug_recording.rs +++ b/silverscript-lang/src/compiler/debug_recording.rs @@ -3,42 +3,69 @@ use std::fmt; use crate::ast::{ContractFieldAst, Expr, FunctionAst, ParamAst, Statement}; use crate::debug_info::{ - DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugParamMapping, DebugRecorder, DebugStep, DebugVariableUpdate, SourceSpan, - StepKind, + DebugConstantMapping, DebugFunctionRange, DebugInfo, DebugInfoRecorder, DebugParamMapping, DebugStep, DebugVariableUpdate, + SourceSpan, StepKind, }; use super::{CompilerError, resolve_expr_for_debug}; -/// Per-function debug recorder active during function compilation. -/// Records params, statements, and variable updates for a single function. -/// When disabled (`inner` is `None`), all methods are zero-cost no-ops. -pub struct EntrypointRecorder<'i> { - inner: Option>, +/// Contract-level debug recorder used by the compiler. +/// +/// This facade routes calls to either an active backend (records debug metadata) +/// or a no-op backend (recording disabled), keeping compiler call sites uniform. +pub struct DebugRecorder<'i> { + inner: Box + 'i>, } -impl fmt::Debug for EntrypointRecorder<'_> { +impl fmt::Debug for DebugRecorder<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("EntrypointRecorder").finish_non_exhaustive() + f.debug_struct("DebugRecorder").finish_non_exhaustive() } } -impl<'i> EntrypointRecorder<'i> { - /// Creates a recorder for one function compile pass. - /// If disabled, all methods are no-ops. - pub fn new(enabled: bool, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { - if enabled { Self { inner: Some(ActiveEntrypointRecorder::new(function, contract_fields)) } } else { Self { inner: None } } +impl<'i> DebugRecorder<'i> { + /// Creates a debug recorder. When `enabled` is false, all methods become no-ops. + pub fn new(enabled: bool) -> Self { + if enabled { Self { inner: Box::new(ActiveDebugRecorder::default()) } } else { Self { inner: Box::new(NoopDebugRecorder) } } } - /// Records one explicit variable binding as its own zero-width Source step. - pub fn record_binding(&mut self, name: String, type_name: String, expr: Expr<'i>, bytecode_offset: usize, span: SourceSpan) { - if let Some(rec) = &mut self.inner { - let step_index = rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::Source {}); - rec.steps[step_index].variable_updates.push(DebugVariableUpdate { name, type_name, expr }); - } + /// Records constructor constants for debugger display. + pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { + self.inner.record_constructor_constants(params, values); + } + + /// Starts staging debug metadata for one entrypoint compilation. + pub fn begin_entrypoint(&mut self, name: &str, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { + self.inner.begin_entrypoint(name, function, contract_fields); + } + + /// Finishes the active entrypoint stage and stores its local script length. + pub fn finish_entrypoint(&mut self, script_len: usize) { + self.inner.finish_entrypoint(script_len); } - /// Records inline-call entry and opens a new debug frame. - /// Adds synthetic args first, then callee params. + /// Sets the absolute script start of a staged entrypoint in final contract bytecode. + pub fn set_entrypoint_start(&mut self, name: &str, bytecode_start: usize) { + self.inner.set_entrypoint_start(name, bytecode_start); + } + + /// Starts one statement frame at the provided bytecode offset. + pub fn begin_statement_at(&mut self, bytecode_offset: usize, env: &HashMap>) { + self.inner.begin_statement_at(bytecode_offset, env); + } + + /// Finishes one statement frame and records variable diffs and bytecode range. + pub fn finish_statement_at( + &mut self, + stmt: &Statement<'i>, + bytecode_end: usize, + env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError> { + self.inner.finish_statement_at(stmt, bytecode_end, env, types) + } + + /// Records an inline call entry step and opens a nested call frame. pub fn begin_inline_call( &mut self, span: SourceSpan, @@ -46,118 +73,306 @@ impl<'i> EntrypointRecorder<'i> { function: &FunctionAst<'i>, env: &HashMap>, ) -> Result<(), CompilerError> { - match &mut self.inner { - Some(rec) => { - let parent_depth = rec.current_call_depth(); - let callee_frame_id = rec.allocate_frame_id(); - let enter_step_index = rec.push_step_with_context( - bytecode_offset, - bytecode_offset, - span, - StepKind::InlineCallEnter { callee: function.name.clone() }, - parent_depth, - callee_frame_id, - ); - - let mut updates = Vec::new(); - let mut synthetic_names: Vec = env.keys().filter(|name| name.starts_with("__arg_")).cloned().collect(); - // Sort so updates are emitted in a fixed order. - synthetic_names.sort_unstable(); - for name in synthetic_names { - if let Some(expr) = env.get(&name).cloned() { - rec.resolve_variable_update(env, &mut updates, &name, "internal", expr)?; - } - } - for param in &function.params { - rec.resolve_variable_update( - env, - &mut updates, - ¶m.name, - ¶m.type_ref.type_name(), - env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), - )?; - } - rec.add_updates_to_step(enter_step_index, updates); - rec.push_call_frame(callee_frame_id, parent_depth.saturating_add(1)); - Ok(()) - } - None => Ok(()), - } + self.inner.begin_inline_call(span, bytecode_offset, function, env) } - /// Records inline-call exit and restores caller frame context. + /// Records an inline call exit step and closes the active nested call frame. pub fn finish_inline_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str) { - if let Some(rec) = &mut self.inner { - rec.pop_call_frame(); - rec.push_step(bytecode_offset, bytecode_offset, span, StepKind::InlineCallExit { callee: callee.to_string() }); - } + self.inner.finish_inline_call(span, bytecode_offset, callee); } - /// Returns how many steps were recorded for this function. - pub fn step_count(&self) -> u32 { - self.inner.as_ref().map_or(0, |rec| rec.next_step_sequence) + /// Records an explicit variable binding as a zero-width source step. + pub fn record_variable_binding( + &mut self, + name: String, + type_name: String, + expr: Expr<'i>, + bytecode_offset: usize, + span: SourceSpan, + ) { + self.inner.record_variable_binding(name, type_name, expr, bytecode_offset, span); } - /// Returns recorded per-function steps. - pub fn steps(&self) -> &[DebugStep<'i>] { - self.inner.as_ref().map_or(&[], |rec| rec.steps.as_slice()) + /// Finalizes and returns debug info if recording is enabled. + pub fn into_debug_info(self, source: String) -> Option> { + self.inner.into_debug_info(source) } +} - /// Returns recorded per-function parameter mappings. - pub fn params(&self) -> &[DebugParamMapping] { - self.inner.as_ref().map_or(&[], |rec| rec.params.as_slice()) +trait DebugRecorderImpl<'i>: fmt::Debug { + fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]); + fn begin_entrypoint(&mut self, name: &str, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]); + fn finish_entrypoint(&mut self, script_len: usize); + fn set_entrypoint_start(&mut self, name: &str, bytecode_start: usize); + fn begin_statement_at(&mut self, bytecode_offset: usize, env: &HashMap>); + fn finish_statement_at( + &mut self, + stmt: &Statement<'i>, + bytecode_end: usize, + env: &HashMap>, + types: &HashMap, + ) -> Result<(), CompilerError>; + fn begin_inline_call( + &mut self, + span: SourceSpan, + bytecode_offset: usize, + function: &FunctionAst<'i>, + env: &HashMap>, + ) -> Result<(), CompilerError>; + fn finish_inline_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str); + fn record_variable_binding(&mut self, name: String, type_name: String, expr: Expr<'i>, bytecode_offset: usize, span: SourceSpan); + fn into_debug_info(self: Box, source: String) -> Option>; +} + +#[derive(Debug, Default)] +struct NoopDebugRecorder; + +impl<'i> DebugRecorderImpl<'i> for NoopDebugRecorder { + fn record_constructor_constants(&mut self, _params: &[ParamAst<'i>], _values: &[Expr<'i>]) {} + fn begin_entrypoint(&mut self, _name: &str, _function: &FunctionAst<'i>, _contract_fields: &[ContractFieldAst<'i>]) {} + fn finish_entrypoint(&mut self, _script_len: usize) {} + fn set_entrypoint_start(&mut self, _name: &str, _bytecode_start: usize) {} + fn begin_statement_at(&mut self, _bytecode_offset: usize, _env: &HashMap>) {} + + fn finish_statement_at( + &mut self, + _stmt: &Statement<'i>, + _bytecode_end: usize, + _env: &HashMap>, + _types: &HashMap, + ) -> Result<(), CompilerError> { + Ok(()) } - /// Starts statement recording by capturing current byte offset and env. - pub fn begin_statement(&mut self, builder: &super::ScriptBuilder, env: &HashMap>) -> StatementGuard<'i> { - StatementGuard { start: builder.script().len(), env_before: self.inner.as_ref().map(|_| env.clone()) } + fn begin_inline_call( + &mut self, + _span: SourceSpan, + _bytecode_offset: usize, + _function: &FunctionAst<'i>, + _env: &HashMap>, + ) -> Result<(), CompilerError> { + Ok(()) + } + + fn finish_inline_call(&mut self, _span: SourceSpan, _bytecode_offset: usize, _callee: &str) {} + fn record_variable_binding( + &mut self, + _name: String, + _type_name: String, + _expr: Expr<'i>, + _bytecode_offset: usize, + _span: SourceSpan, + ) { + } + + fn into_debug_info(self: Box, _source: String) -> Option> { + None } } -pub struct StatementGuard<'i> { - start: usize, - env_before: Option>>, +#[derive(Debug, Default)] +struct ActiveDebugRecorder<'i> { + recorder: DebugInfoRecorder<'i>, + entrypoints: Vec>, + active_entrypoint: Option, +} + +impl<'i> ActiveDebugRecorder<'i> { + fn active_entrypoint_mut(&mut self) -> Option<&mut StagedEntrypointDebug<'i>> { + let index = self.active_entrypoint?; + self.entrypoints.get_mut(index) + } } -impl<'i> StatementGuard<'i> { - /// Finishes recording: snapshots the current bytecode offset, diffs the env, - /// and records the debug step on the given recorder. - pub fn finish( - self, - recorder: &mut EntrypointRecorder<'i>, +impl<'i> DebugRecorderImpl<'i> for ActiveDebugRecorder<'i> { + fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { + for (param, value) in params.iter().zip(values.iter()) { + self.recorder.record_constant(DebugConstantMapping { + name: param.name.clone(), + type_name: param.type_ref.type_name(), + value: value.clone(), + }); + } + } + + fn begin_entrypoint(&mut self, name: &str, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) { + debug_assert!(self.active_entrypoint.is_none(), "begin_entrypoint called while another entrypoint is active"); + self.entrypoints.push(StagedEntrypointDebug::new(name.to_string(), function, contract_fields)); + self.active_entrypoint = Some(self.entrypoints.len().saturating_sub(1)); + } + + fn finish_entrypoint(&mut self, script_len: usize) { + let Some(index) = self.active_entrypoint.take() else { + return; + }; + let Some(entrypoint) = self.entrypoints.get_mut(index) else { + return; + }; + entrypoint.script_len = script_len; + debug_assert!(entrypoint.statement_stack.is_empty(), "entrypoint ended with unclosed statement frames"); + debug_assert!(entrypoint.call_stack.len() == 1, "entrypoint ended with unclosed inline call frames"); + } + + fn set_entrypoint_start(&mut self, name: &str, bytecode_start: usize) { + let Some(entrypoint) = self.entrypoints.iter_mut().find(|entrypoint| entrypoint.name == name) else { + return; + }; + entrypoint.bytecode_start = Some(bytecode_start); + } + + fn begin_statement_at(&mut self, bytecode_offset: usize, env: &HashMap>) { + let Some(entrypoint) = self.active_entrypoint_mut() else { + return; + }; + entrypoint.statement_stack.push(StatementFrame { start: bytecode_offset, env_before: env.clone() }); + } + + fn finish_statement_at( + &mut self, stmt: &Statement<'i>, - builder: &super::ScriptBuilder, + bytecode_end: usize, env: &HashMap>, types: &HashMap, ) -> Result<(), CompilerError> { - let end = builder.script().len(); - if let Some(active) = &mut recorder.inner { - active.record_statement(stmt, self.start, end, self.env_before.as_ref(), env, types)?; + let Some(entrypoint) = self.active_entrypoint_mut() else { + return Ok(()); + }; + let Some(frame) = entrypoint.statement_stack.pop() else { + return Ok(()); + }; + + let updates = collect_variable_updates(&frame.env_before, env, types)?; + let span = SourceSpan::from(stmt.span()); + let bytecode_len = bytecode_end.saturating_sub(frame.start); + let step_index = entrypoint.push_step(frame.start, frame.start + bytecode_len, span, StepKind::Source {}); + entrypoint.steps[step_index].variable_updates.extend(updates); + Ok(()) + } + + fn begin_inline_call( + &mut self, + span: SourceSpan, + bytecode_offset: usize, + function: &FunctionAst<'i>, + env: &HashMap>, + ) -> Result<(), CompilerError> { + let Some(entrypoint) = self.active_entrypoint_mut() else { + return Ok(()); + }; + + let parent_depth = entrypoint.current_call_depth(); + let callee_frame_id = entrypoint.allocate_frame_id(); + let enter_step_index = entrypoint.push_step_with_context( + bytecode_offset, + bytecode_offset, + span, + StepKind::InlineCallEnter { callee: function.name.clone() }, + parent_depth, + callee_frame_id, + ); + + let mut updates = Vec::new(); + let mut synthetic_names: Vec = env.keys().filter(|name| name.starts_with("__arg_")).cloned().collect(); + synthetic_names.sort_unstable(); + for name in synthetic_names { + if let Some(expr) = env.get(&name).cloned() { + resolve_variable_update(env, &mut updates, &name, "internal", expr)?; + } + } + + for param in &function.params { + resolve_variable_update( + env, + &mut updates, + ¶m.name, + ¶m.type_ref.type_name(), + env.get(¶m.name).cloned().unwrap_or_else(|| Expr::identifier(param.name.clone())), + )?; } + + entrypoint.steps[enter_step_index].variable_updates.extend(updates); + entrypoint.push_call_frame(callee_frame_id, parent_depth.saturating_add(1)); Ok(()) } + + fn finish_inline_call(&mut self, span: SourceSpan, bytecode_offset: usize, callee: &str) { + let Some(entrypoint) = self.active_entrypoint_mut() else { + return; + }; + entrypoint.pop_call_frame(); + entrypoint.push_step(bytecode_offset, bytecode_offset, span, StepKind::InlineCallExit { callee: callee.to_string() }); + } + + fn record_variable_binding(&mut self, name: String, type_name: String, expr: Expr<'i>, bytecode_offset: usize, span: SourceSpan) { + let Some(entrypoint) = self.active_entrypoint_mut() else { + return; + }; + let step_index = entrypoint.push_step(bytecode_offset, bytecode_offset, span, StepKind::Source {}); + entrypoint.steps[step_index].variable_updates.push(DebugVariableUpdate { name, type_name, expr }); + } + + fn into_debug_info(mut self: Box, source: String) -> Option> { + for entrypoint in self.entrypoints.drain(..) { + debug_assert!(entrypoint.bytecode_start.is_some(), "missing bytecode start for staged entrypoint '{}'", entrypoint.name); + let bytecode_start = entrypoint.bytecode_start.unwrap_or(0); + let seq_base = self.recorder.reserve_sequence_block(entrypoint.next_step_sequence); + + for step in entrypoint.steps { + self.recorder.record_step(DebugStep { + bytecode_start: step.bytecode_start + bytecode_start, + bytecode_end: step.bytecode_end + bytecode_start, + span: step.span, + kind: step.kind, + sequence: seq_base.saturating_add(step.sequence), + call_depth: step.call_depth, + frame_id: step.frame_id, + variable_updates: step.variable_updates, + }); + } + + for param in entrypoint.params { + self.recorder.record_param(param); + } + + self.recorder.record_function(DebugFunctionRange { + name: entrypoint.name, + bytecode_start, + bytecode_end: bytecode_start + entrypoint.script_len, + }); + } + + Some(self.recorder.into_debug_info(source)) + } } -#[derive(Debug, Default)] -struct ActiveEntrypointRecorder<'i> { +#[derive(Debug)] +struct StagedEntrypointDebug<'i> { + name: String, + script_len: usize, + bytecode_start: Option, steps: Vec>, params: Vec, next_step_sequence: u32, call_stack: Vec, next_frame_id: u32, + statement_stack: Vec>, } -#[derive(Debug, Clone, Copy)] -struct CallFrame { - frame_id: u32, - call_depth: u32, -} - -impl<'i> ActiveEntrypointRecorder<'i> { - fn new(function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { - let mut recorder = Self { call_stack: vec![CallFrame { frame_id: 0, call_depth: 0 }], next_frame_id: 1, ..Default::default() }; - recorder.record_param_bindings(function, contract_fields); - recorder +impl<'i> StagedEntrypointDebug<'i> { + fn new(name: String, function: &FunctionAst<'i>, contract_fields: &[ContractFieldAst<'i>]) -> Self { + let mut entrypoint = Self { + name, + script_len: 0, + bytecode_start: None, + steps: Vec::new(), + params: Vec::new(), + next_step_sequence: 0, + call_stack: vec![CallFrame { frame_id: 0, call_depth: 0 }], + next_frame_id: 1, + statement_stack: Vec::new(), + }; + entrypoint.record_param_bindings(function, contract_fields); + entrypoint } fn allocate_frame_id(&mut self) -> u32 { @@ -185,9 +400,9 @@ impl<'i> ActiveEntrypointRecorder<'i> { } fn next_sequence(&mut self) -> u32 { - let seq = self.next_step_sequence; + let sequence = self.next_step_sequence; self.next_step_sequence = self.next_step_sequence.saturating_add(1); - seq + sequence } fn push_step(&mut self, bytecode_start: usize, bytecode_end: usize, span: SourceSpan, kind: StepKind) -> usize { @@ -238,142 +453,54 @@ impl<'i> ActiveEntrypointRecorder<'i> { }); } } - - fn record_statement( - &mut self, - stmt: &Statement<'i>, - bytecode_start: usize, - bytecode_end: usize, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result<(), CompilerError> { - let updates = self.collect_variable_updates(before_env, after_env, types)?; - let span = SourceSpan::from(stmt.span()); - let bytecode_len = bytecode_end.saturating_sub(bytecode_start); - let step_index = self.push_step(bytecode_start, bytecode_start + bytecode_len, span, StepKind::Source {}); - self.add_updates_to_step(step_index, updates); - Ok(()) - } - - fn add_updates_to_step(&mut self, step_index: usize, updates: Vec>) { - let Some(step) = self.steps.get_mut(step_index) else { - return; - }; - step.variable_updates.extend(updates); - } - - fn collect_variable_updates( - &self, - before_env: Option<&HashMap>>, - after_env: &HashMap>, - types: &HashMap, - ) -> Result>, CompilerError> { - let Some(before_env) = before_env else { - return Ok(Vec::new()); - }; - - let mut names: Vec = after_env.keys().cloned().collect(); - // Sort so updates are emitted in a fixed order. - names.sort_unstable(); - - let mut updates = Vec::new(); - for name in names { - let Some(after_expr) = after_env.get(&name) else { - continue; - }; - if before_env.get(&name).is_some_and(|before_expr| before_expr == after_expr) { - continue; - } - let Some(type_name) = types.get(&name) else { - continue; - }; - self.resolve_variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; - } - Ok(updates) - } - - fn resolve_variable_update( - &self, - env: &HashMap>, - updates: &mut Vec>, - name: &str, - type_name: &str, - expr: Expr<'i>, - ) -> Result<(), CompilerError> { - let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; - updates.push(DebugVariableUpdate { name: name.to_string(), type_name: type_name.to_string(), expr: resolved }); - Ok(()) - } } -/// Contract-level debug recorder that merges per-function recordings. -/// When disabled (`inner` is `None`), all methods are no-ops. -pub struct ContractRecorder<'i> { - inner: Option>, +#[derive(Debug)] +struct StatementFrame<'i> { + start: usize, + env_before: HashMap>, } -impl fmt::Debug for ContractRecorder<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ContractRecorder").finish_non_exhaustive() - } +#[derive(Debug, Clone, Copy)] +struct CallFrame { + frame_id: u32, + call_depth: u32, } -impl<'i> ContractRecorder<'i> { - /// Creates a contract-level recorder. - pub fn new(enabled: bool) -> Self { - if enabled { Self { inner: Some(ActiveContractRecorder::default()) } } else { Self { inner: None } } - } - - /// Records constructor constants for debugger visibility. - pub fn record_constructor_constants(&mut self, params: &[ParamAst<'i>], values: &[Expr<'i>]) { - if let Some(rec) = &mut self.inner { - for (param, value) in params.iter().zip(values.iter()) { - rec.recorder.record_constant(DebugConstantMapping { - name: param.name.clone(), - type_name: param.type_ref.type_name(), - value: value.clone(), - }); - } - } - } - - /// Merges one compiled entrypoint recording into contract-level debug info. - pub fn record_compiled_entrypoint(&mut self, name: &str, script_len: usize, debug: &EntrypointRecorder<'i>, offset: usize) { - if let Some(rec) = &mut self.inner { - let seq_base = rec.recorder.reserve_sequence_block(debug.step_count()); - for step in debug.steps() { - rec.recorder.record_step(DebugStep { - bytecode_start: step.bytecode_start + offset, - bytecode_end: step.bytecode_end + offset, - span: step.span, - kind: step.kind.clone(), - sequence: seq_base.saturating_add(step.sequence), - call_depth: step.call_depth, - frame_id: step.frame_id, - variable_updates: step.variable_updates.clone(), - }); - } - for param in debug.params() { - rec.recorder.record_param(param.clone()); - } - rec.recorder.record_function(DebugFunctionRange { - name: name.to_string(), - bytecode_start: offset, - bytecode_end: offset + script_len, - }); +fn collect_variable_updates<'i>( + before_env: &HashMap>, + after_env: &HashMap>, + types: &HashMap, +) -> Result>, CompilerError> { + let mut names: Vec = after_env.keys().cloned().collect(); + names.sort_unstable(); + + let mut updates = Vec::new(); + for name in names { + let Some(after_expr) = after_env.get(&name) else { + continue; + }; + if before_env.get(&name).is_some_and(|before_expr| before_expr == after_expr) { + continue; } + let Some(type_name) = types.get(&name) else { + continue; + }; + resolve_variable_update(after_env, &mut updates, &name, type_name, after_expr.clone())?; } - - /// Finalizes and returns `DebugInfo` when recording is enabled. - pub fn into_debug_info(self, source: String) -> Option> { - self.inner.map(|rec| rec.recorder.into_debug_info(source)) - } + Ok(updates) } -#[derive(Debug, Default)] -struct ActiveContractRecorder<'i> { - recorder: DebugRecorder<'i>, +fn resolve_variable_update<'i>( + env: &HashMap>, + updates: &mut Vec>, + name: &str, + type_name: &str, + expr: Expr<'i>, +) -> Result<(), CompilerError> { + let resolved = resolve_expr_for_debug(expr, env, &mut HashSet::new())?; + updates.push(DebugVariableUpdate { name: name.to_string(), type_name: type_name.to_string(), expr: resolved }); + Ok(()) } #[cfg(test)] @@ -383,7 +510,7 @@ mod tests { use crate::ast::{Expr, parse_contract_ast}; use crate::debug_info::StepKind; - use super::{ContractRecorder, EntrypointRecorder, SourceSpan}; + use super::{DebugRecorder, SourceSpan}; #[test] fn noop_recorders_are_pure_noops() { @@ -399,23 +526,21 @@ mod tests { let function = contract.functions.first().expect("function"); let stmt = function.body.first().expect("statement"); - let mut recorder = EntrypointRecorder::new(false, function, &contract.fields); + let mut recorder = DebugRecorder::new(false); + recorder.record_constructor_constants(&contract.params, &[]); + recorder.begin_entrypoint("spend", function, &contract.fields); let span = SourceSpan::from(stmt.span()); - let builder = super::super::ScriptBuilder::new(); - let guard = recorder.begin_statement(&builder, &HashMap::new()); - guard.finish(&mut recorder, stmt, &builder, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); + recorder.begin_statement_at(0, &HashMap::new()); + recorder.finish_statement_at(stmt, 0, &HashMap::new(), &HashMap::new()).expect("noop statement recording"); recorder.begin_inline_call(span, 1, function, &HashMap::new()).expect("noop begin call recording"); recorder.finish_inline_call(span, 2, "callee"); - recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, span); - assert_eq!(recorder.step_count(), 0); + recorder.record_variable_binding("tmp".to_string(), "int".to_string(), Expr::int(1), 2, span); + recorder.finish_entrypoint(1); - let mut sink = ContractRecorder::new(false); - sink.record_constructor_constants(&contract.params, &[]); - sink.record_compiled_entrypoint("spend", 1, &recorder, 0); - assert!(sink.into_debug_info(String::new()).is_none()); + assert!(recorder.into_debug_info(String::new()).is_none()); } #[test] @@ -432,7 +557,8 @@ mod tests { let function = contract.functions.first().expect("function"); let stmt = function.body.first().expect("statement"); - let mut recorder = EntrypointRecorder::new(true, function, &contract.fields); + let mut recorder = DebugRecorder::new(true); + recorder.begin_entrypoint("spend", function, &contract.fields); let mut before = HashMap::new(); before.insert("x".to_string(), Expr::identifier("x")); @@ -444,22 +570,20 @@ mod tests { types.insert("x".to_string(), "int".to_string()); types.insert("y".to_string(), "int".to_string()); - let builder = super::super::ScriptBuilder::new(); - let guard = recorder.begin_statement(&builder, &before); - guard.finish(&mut recorder, stmt, &builder, &after, &types).expect("record_step first statement"); + recorder.begin_statement_at(0, &before); + recorder.finish_statement_at(stmt, 0, &after, &types).expect("record_step first statement"); let span = SourceSpan::from(stmt.span()); let mut inline_env = HashMap::new(); inline_env.insert("x".to_string(), Expr::int(3)); recorder.begin_inline_call(span, 1, function, &inline_env).expect("begin call recording"); - recorder.record_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, span); + recorder.record_variable_binding("tmp".to_string(), "int".to_string(), Expr::int(9), 1, span); recorder.finish_inline_call(span, 2, "callee"); - assert_eq!(recorder.step_count(), 4); + recorder.finish_entrypoint(2); + recorder.set_entrypoint_start("spend", 0); - let mut sink = ContractRecorder::new(true); - sink.record_compiled_entrypoint("spend", 2, &recorder, 0); - let info = sink.into_debug_info(String::new()).expect("debug info available"); + let info = recorder.into_debug_info(String::new()).expect("debug info available"); let sequences = info.steps.iter().map(|step| step.sequence).collect::>(); assert_eq!(sequences, vec![0, 1, 2, 3]); diff --git a/silverscript-lang/src/debug_info.rs b/silverscript-lang/src/debug_info.rs index 40b45715..0bf09fb6 100644 --- a/silverscript-lang/src/debug_info.rs +++ b/silverscript-lang/src/debug_info.rs @@ -17,11 +17,13 @@ impl<'a> From> for SourceSpan { } } +/// `DebugInfo` builder used by compiler-side recorders. +/// /// Accumulates debug metadata during compilation. /// Collects steps, variable updates, param mappings, function ranges, and constants. /// Converted to `DebugInfo` after compilation completes. #[derive(Debug, Default)] -pub struct DebugRecorder<'i> { +pub struct DebugInfoRecorder<'i> { steps: Vec>, params: Vec, entry_points: Vec, @@ -29,7 +31,7 @@ pub struct DebugRecorder<'i> { next_sequence: u32, } -impl<'i> DebugRecorder<'i> { +impl<'i> DebugInfoRecorder<'i> { /// Appends one recorded step. pub fn record_step(&mut self, step: DebugStep<'i>) { self.steps.push(step); @@ -169,14 +171,9 @@ impl StepId { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum StepKind { - #[serde(alias = "Statement", alias = "Virtual")] Source {}, - InlineCallEnter { - callee: String, - }, - InlineCallExit { - callee: String, - }, + InlineCallEnter { callee: String }, + InlineCallExit { callee: String }, } #[cfg(test)] diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index a63bb1ca..2c77fda4 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -171,6 +171,89 @@ fn compile_contract_emits_debug_info_when_recording_enabled() { assert!(debug_info.params.iter().any(|param| param.name == "x")); } +#[test] +fn debug_info_single_entrypoint_sequences_and_offsets_are_stable() { + let source = r#" + contract DebugSingle() { + entrypoint function spend(int x) { + int y = x; + require(y == x); + } + } + "#; + + let options = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + assert!(compiled.without_selector); + + let debug_info = compiled.debug_info.expect("debug info should be present"); + let function = debug_info.functions.iter().find(|function| function.name == "spend").expect("function range for spend"); + assert_eq!(function.bytecode_start, 0, "single-entrypoint contract should not use selector prefix"); + assert!(function.bytecode_end > function.bytecode_start); + + let mut sequences = debug_info.steps.iter().map(|step| step.sequence).collect::>(); + sequences.sort_unstable(); + assert_eq!(sequences, (0..debug_info.steps.len() as u32).collect::>(), "step sequences should be contiguous"); + + let function_steps = debug_info + .steps + .iter() + .filter(|step| step.bytecode_start >= function.bytecode_start && step.bytecode_end <= function.bytecode_end) + .collect::>(); + assert!(!function_steps.is_empty(), "function should contain at least one debug step"); + assert!(function_steps.iter().all(|step| step.bytecode_start <= step.bytecode_end), "step ranges should be valid"); +} + +#[test] +fn debug_info_selector_entrypoints_have_global_sequences_and_offset_ranges() { + let source = r#" + contract DebugSelector() { + entrypoint function a(int x) { + int y = x; + require(y == x); + } + + entrypoint function b(int x) { + int z = x; + require(z == x); + } + } + "#; + + let options = CompileOptions { record_debug_infos: true, ..Default::default() }; + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + assert!(!compiled.without_selector); + + let debug_info = compiled.debug_info.expect("debug info should be present"); + let function_a = debug_info.functions.iter().find(|function| function.name == "a").expect("function range for a"); + let function_b = debug_info.functions.iter().find(|function| function.name == "b").expect("function range for b"); + + assert!(function_a.bytecode_start > 0, "selector mode should prepend dispatcher ops"); + assert!(function_a.bytecode_start < function_b.bytecode_start, "entrypoint ranges should follow compile order"); + assert!(function_a.bytecode_end <= function_b.bytecode_start, "entrypoint ranges should not overlap"); + + let steps_for_a = debug_info + .steps + .iter() + .filter(|step| step.bytecode_start >= function_a.bytecode_start && step.bytecode_end <= function_a.bytecode_end) + .collect::>(); + let steps_for_b = debug_info + .steps + .iter() + .filter(|step| step.bytecode_start >= function_b.bytecode_start && step.bytecode_end <= function_b.bytecode_end) + .collect::>(); + assert!(!steps_for_a.is_empty(), "entrypoint a should contain debug steps"); + assert!(!steps_for_b.is_empty(), "entrypoint b should contain debug steps"); + + let mut sequences = debug_info.steps.iter().map(|step| step.sequence).collect::>(); + sequences.sort_unstable(); + assert_eq!(sequences, (0..debug_info.steps.len() as u32).collect::>(), "global step sequences should be contiguous"); + + let max_a_sequence = steps_for_a.iter().map(|step| step.sequence).max().expect("a sequence max"); + let min_b_sequence = steps_for_b.iter().map(|step| step.sequence).min().expect("b sequence min"); + assert!(max_a_sequence < min_b_sequence, "later entrypoint should reserve a later sequence block"); +} + #[test] fn rejects_constructor_args_with_wrong_scalar_types() { let source = r#" From aeb3a70eda1fbcf5cc42b125fbbf01ff4404086d Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:01:39 +0200 Subject: [PATCH 39/41] Enhance session module with failure reporting and call stack management --- Cargo.lock | 2 - debugger/cli/Cargo.toml | 2 - debugger/cli/src/main.rs | 489 ++++++++++++++++++--------- debugger/cli/tests/cli_tests.rs | 152 ++++++++- debugger/session/src/args.rs | 130 +++++++ debugger/session/src/lib.rs | 5 + debugger/session/src/presentation.rs | 66 +++- debugger/session/src/session.rs | 146 ++++++++ debugger/session/src/test_runner.rs | 249 ++++++++++++++ silverscript-lang/src/debug_info.rs | 47 +-- 10 files changed, 1084 insertions(+), 204 deletions(-) create mode 100644 debugger/session/src/args.rs create mode 100644 debugger/session/src/test_runner.rs diff --git a/Cargo.lock b/Cargo.lock index 1d6475e5..22356892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -767,11 +767,9 @@ version = "0.1.0" dependencies = [ "clap", "debugger-session", - "faster-hex 0.10.0", "kaspa-consensus-core", "kaspa-txscript", "kaspa-txscript-errors", - "serde_json", "silverscript-lang", ] diff --git a/debugger/cli/Cargo.toml b/debugger/cli/Cargo.toml index 3c3d1b78..7c288061 100644 --- a/debugger/cli/Cargo.toml +++ b/debugger/cli/Cargo.toml @@ -18,5 +18,3 @@ kaspa-consensus-core.workspace = true kaspa-txscript.workspace = true kaspa-txscript-errors.workspace = true clap = { version = "4.5.60", features = ["derive"] } -faster-hex = "0.10" -serde_json = "1.0" diff --git a/debugger/cli/src/main.rs b/debugger/cli/src/main.rs index 1c50e116..42c30bcb 100644 --- a/debugger/cli/src/main.rs +++ b/debugger/cli/src/main.rs @@ -1,22 +1,44 @@ +use std::collections::HashMap; use std::fs; use std::io::{self, BufRead, Write}; +use std::path::{Path, PathBuf}; use clap::Parser; +use debugger_session::args::{parse_call_args, parse_ctor_args, parse_hex_bytes}; +use debugger_session::format_failure_report; +use debugger_session::session::{DebugEngine, DebugSession, ShadowTxContext}; +use debugger_session::test_runner::{ + TestExpectation, TestTxInputScenarioResolved, TestTxOutputScenarioResolved, TestTxScenarioResolved, resolve_contract_test, +}; +use kaspa_consensus_core::Hash; use kaspa_consensus_core::hashing::sighash::SigHashReusedValuesUnsync; +use kaspa_consensus_core::tx::{ + CovenantBinding, PopulatedTransaction, ScriptPublicKey, Transaction, TransactionId, TransactionInput, TransactionOutpoint, + TransactionOutput, UtxoEntry, VerifiableTransaction, +}; use kaspa_txscript::caches::Cache; -use kaspa_txscript::{EngineCtx, EngineFlags}; - -use debugger_session::session::{DebugEngine, DebugSession}; -use silverscript_lang::ast::{Expr, ExprKind, parse_contract_ast}; +use kaspa_txscript::covenants::CovenantsContext; +use kaspa_txscript::script_builder::ScriptBuilder; +use kaspa_txscript::{EngineCtx, EngineFlags, pay_to_script_hash_script}; +use silverscript_lang::ast::{ContractAst, parse_contract_ast}; use silverscript_lang::compiler::{CompileOptions, compile_contract}; -use silverscript_lang::span; const PROMPT: &str = "(sdb) "; #[derive(Debug, Parser)] #[command(name = "cli-debugger", about = "SilverScript debugger")] struct CliArgs { - script_path: String, + script_path: Option, + #[arg(long = "test-file")] + test_file: Option, + #[arg(long = "test-name")] + test_name: Option, + /// Run non-interactively: execute and report pass/fail + #[arg(long = "run", short = 'r')] + run: bool, + /// Run all tests in a test file + #[arg(long = "run-all")] + run_all: bool, #[arg(long = "no-selector")] without_selector: bool, #[arg(long = "function", short = 'f')] @@ -27,104 +49,59 @@ struct CliArgs { raw_args: Vec, } -fn parse_int_arg(raw: &str) -> Result> { - let cleaned = raw.replace('_', ""); - if let Some(hex) = cleaned.strip_prefix("0x").or_else(|| cleaned.strip_prefix("0X")) { - return Ok(i64::from_str_radix(hex, 16)?); +fn compile_script_for_ctor_args( + source: &str, + parsed_contract: &ContractAst<'_>, + raw_ctor_args: &[String], + cache: &mut HashMap, Vec>, +) -> Result, Box> { + if let Some(script) = cache.get(raw_ctor_args) { + return Ok(script.clone()); } - Ok(cleaned.parse::()?) + let ctor_args = parse_ctor_args(parsed_contract, raw_ctor_args)?; + let compiled = compile_contract(source, &ctor_args, CompileOptions::default())?; + cache.insert(raw_ctor_args.to_vec(), compiled.script.clone()); + Ok(compiled.script) } -fn parse_hex_bytes(raw: &str) -> Result, Box> { - let trimmed = raw.trim(); - let hex_str = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")).unwrap_or(trimmed); - if hex_str.is_empty() { - return Ok(vec![]); +fn parse_hash32(raw: &str) -> Result> { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 32 { + return Err(format!("hash expects 32 bytes, got {}", bytes.len()).into()); } - // Allow odd length by implicitly left-padding with 0. - let normalized = if hex_str.len() % 2 != 0 { format!("0{hex_str}") } else { hex_str.to_string() }; - let mut out = vec![0u8; normalized.len() / 2]; - faster_hex::hex_decode(normalized.as_bytes(), &mut out)?; - Ok(out) + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(Hash::from_bytes(array)) } -fn bytes_expr(bytes: Vec) -> Expr<'static> { - Expr::new(ExprKind::Array(bytes.into_iter().map(Expr::byte).collect()), span::Span::default()) +fn parse_txid32(raw: &str) -> Result> { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 32 { + return Err(format!("txid expects 32 bytes, got {}", bytes.len()).into()); + } + let mut array = [0u8; 32]; + array.copy_from_slice(&bytes); + Ok(TransactionId::from_bytes(array)) } -fn parse_typed_arg(type_name: &str, raw: &str) -> Result, Box> { - if let Some(element_type) = type_name.strip_suffix("[]") { - let trimmed = raw.trim(); - if trimmed.starts_with('[') { - let values = serde_json::from_str::>(trimmed)?; - let mut out = Vec::with_capacity(values.len()); - for value in values { - let expr = match value { - serde_json::Value::Number(n) => Expr::int(n.as_i64().ok_or("invalid int in array")?), - serde_json::Value::Bool(b) => Expr::bool(b), - serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, - _ => return Err("unsupported array element (expected number/bool/string)".into()), - }; - out.push(expr); - } - return Ok(Expr::new(ExprKind::Array(out), span::Span::default())); - } - if element_type == "byte" { - return Ok(bytes_expr(parse_hex_bytes(trimmed)?)); - } - return Err(format!("unsupported array literal format for '{type_name}'").into()); - } +fn build_p2pk_script(pubkey: &[u8]) -> Vec { + ScriptBuilder::new() + .add_data(pubkey) + .expect("push pubkey") + .add_op(kaspa_txscript::opcodes::codes::OpCheckSig) + .expect("add OpCheckSig") + .drain() +} - match type_name { - "int" => Ok(Expr::int(parse_int_arg(raw)?)), - "bool" => match raw { - "true" => Ok(Expr::bool(true)), - "false" => Ok(Expr::bool(false)), - _ => Err(format!("invalid bool '{raw}' (expected true/false)").into()), - }, - "string" => Ok(Expr::string(raw.to_string())), - "byte" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() == 1 { Ok(Expr::byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len()).into()) } - } - "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), - "pubkey" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 32 { - return Err(format!("pubkey expects 32 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - "sig" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 65 { - return Err(format!("sig expects 65 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - "datasig" => { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != 64 { - return Err(format!("datasig expects 64 bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } - other => { - let size = other - .strip_prefix("bytes") - .and_then(|v| v.parse::().ok()) - .or_else(|| other.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok())); - if let Some(size) = size { - let bytes = parse_hex_bytes(raw)?; - if bytes.len() != size { - return Err(format!("{other} expects {size} bytes, got {}", bytes.len()).into()); - } - Ok(bytes_expr(bytes)) - } else { - Err(format!("unsupported arg type '{other}'").into()) - } - } - } +fn sigscript_push_script(script: &[u8]) -> Vec { + ScriptBuilder::new().add_data(script).expect("push script data").drain() +} + +fn combine_action_and_redeem(action: &[u8], redeem_script: &[u8]) -> Result, Box> { + let mut builder = ScriptBuilder::new(); + builder.add_ops(action)?; + builder.add_data(redeem_script)?; + Ok(builder.drain()) } fn show_stack(session: &DebugSession<'_, '_>) { @@ -174,7 +151,13 @@ fn show_step_view(session: &DebugSession<'_, '_>) { show_vars(session); } -fn run_repl(session: &mut DebugSession<'_, '_>) -> Result<(), kaspa_txscript_errors::TxScriptError> { +fn print_failure(session: &DebugSession<'_, '_>, err: kaspa_txscript_errors::TxScriptError) { + let report = session.build_failure_report(&err); + let formatted = format_failure_report(&report, &|type_name, value| session.format_value(type_name, value)); + eprintln!("{formatted}"); +} + +fn run_repl(session: &mut DebugSession<'_, '_>) -> Result<(), Box> { let stdin = io::stdin(); loop { print!("{PROMPT}"); @@ -188,45 +171,65 @@ fn run_repl(session: &mut DebugSession<'_, '_>) -> Result<(), kaspa_txscript_err let cmd = cmd.trim(); if cmd.is_empty() || cmd == "n" || cmd == "next" { - match session.step_over()? { - Some(_) => show_step_view(session), - None => { + match session.step_over() { + Ok(Some(_)) => show_step_view(session), + Ok(None) => { println!("Done."); break; } + Err(err) => { + print_failure(session, err); + break; + } } continue; } let mut parts = cmd.split_whitespace(); match parts.next().unwrap_or("") { - "step" | "s" => match session.step_into()? { - Some(_) => show_step_view(session), - None => { + "step" | "s" => match session.step_into() { + Ok(Some(_)) => show_step_view(session), + Ok(None) => { println!("Done."); break; } + Err(err) => { + print_failure(session, err); + break; + } }, - "si" => match session.step_opcode()? { - Some(_) => show_step_view(session), - None => { + "si" => match session.step_opcode() { + Ok(Some(_)) => show_step_view(session), + Ok(None) => { println!("Done."); break; } + Err(err) => { + print_failure(session, err); + break; + } }, - "finish" | "out" => match session.step_out()? { - Some(_) => show_step_view(session), - None => { + "finish" | "out" => match session.step_out() { + Ok(Some(_)) => show_step_view(session), + Ok(None) => { println!("Done."); break; } + Err(err) => { + print_failure(session, err); + break; + } }, - "c" | "continue" => match session.continue_to_breakpoint()? { - Some(_) => show_step_view(session), - None => { + "c" | "continue" => match session.continue_to_breakpoint() { + Ok(Some(_)) => show_step_view(session), + Ok(None) => { println!("Done."); break; } + Err(err) => { + print_failure(session, err); + break; + } }, "b" | "break" => { if let Some(arg) = parts.next() { @@ -285,68 +288,252 @@ fn run_repl(session: &mut DebugSession<'_, '_>) -> Result<(), kaspa_txscript_err Ok(()) } +fn run_all_tests(test_file: &str) -> Result<(), Box> { + use debugger_session::test_runner::read_contract_test_file; + let test_file_path = Path::new(test_file); + let parsed = read_contract_test_file(test_file_path)?; + let test_names: Vec = parsed.tests.iter().map(|t| t.name.clone()).collect(); + let total = test_names.len(); + let mut passed = 0; + let mut failed = 0; + for name in &test_names { + let result = std::process::Command::new(std::env::current_exe()?) + .args(["--run", "--test-file", test_file, "--test-name", name]) + .output()?; + let stderr = String::from_utf8_lossy(&result.stderr); + if result.status.success() { + passed += 1; + println!(" PASS {name}"); + } else { + failed += 1; + println!(" FAIL {name}"); + if !stderr.is_empty() { + for line in stderr.lines() { + println!(" {line}"); + } + } + } + } + println!("\n{total} tests: {passed} passed, {failed} failed"); + if failed > 0 { Err("some tests failed".into()) } else { Ok(()) } +} + fn main() -> Result<(), Box> { let cli = CliArgs::parse(); - let script_path = cli.script_path; - let without_selector = cli.without_selector; - let function_name = cli.function_name; - let raw_ctor_args = cli.raw_ctor_args; - let raw_args = cli.raw_args; - let source = fs::read_to_string(&script_path)?; - let parsed_contract = parse_contract_ast(&source)?; - - let entrypoint_count = parsed_contract.functions.iter().filter(|func| func.entrypoint).count(); - if without_selector && entrypoint_count != 1 { - return Err("--no-selector requires exactly one entrypoint function".into()); + if cli.run_all { + let test_file = cli.test_file.as_deref().ok_or("--run-all requires --test-file")?; + return run_all_tests(test_file); } - if parsed_contract.params.len() != raw_ctor_args.len() { - return Err(format!("constructor expects {} arguments, got {}", parsed_contract.params.len(), raw_ctor_args.len()).into()); - } + // Resolve source, ctor args, function, call args, and tx from test file or CLI flags + let (script_path, raw_ctor_args, selected_name, raw_args, tx_scenario, expect) = if let Some(test_file) = cli.test_file.as_deref() + { + let test_name = cli.test_name.as_deref().ok_or("--test-file requires --test-name")?; + let script_override = cli.script_path.as_deref().map(Path::new); + let resolved = resolve_contract_test(Path::new(test_file), test_name, script_override) + .map_err(|e| -> Box { e.into() })?; + let ctor = if !cli.raw_ctor_args.is_empty() { cli.raw_ctor_args.clone() } else { resolved.test.constructor_args }; + let fname = cli.function_name.clone().unwrap_or(resolved.test.function); + let args = if !cli.raw_args.is_empty() { cli.raw_args.clone() } else { resolved.test.args }; + let expect = Some(resolved.test.expect); + (resolved.script_path, ctor, fname, args, resolved.test.tx, expect) + } else { + let path = cli.script_path.as_deref().ok_or("missing script path: pass SCRIPT_PATH or --test-file")?; + let ctor = cli.raw_ctor_args.clone(); + let args = cli.raw_args.clone(); + (PathBuf::from(path), ctor, cli.function_name.clone().unwrap_or_default(), args, None, None) + }; + + let source = fs::read_to_string(&script_path)?; + let parsed_contract = parse_contract_ast(&source)?; - let mut ctor_args = Vec::with_capacity(raw_ctor_args.len()); - for (param, raw) in parsed_contract.params.iter().zip(raw_ctor_args.iter()) { - ctor_args.push(parse_typed_arg(¶m.type_ref.type_name(), raw)?); + if cli.without_selector { + let entrypoint_count = parsed_contract.functions.iter().filter(|func| func.entrypoint).count(); + if entrypoint_count != 1 { + return Err("--no-selector requires exactly one entrypoint function".into()); + } } + let ctor_args = parse_ctor_args(&parsed_contract, &raw_ctor_args)?; let compile_opts = CompileOptions { record_debug_infos: true, ..Default::default() }; let compiled = compile_contract(&source, &ctor_args, compile_opts)?; let debug_info = compiled.debug_info.clone(); + let mut ctor_script_cache = HashMap::, Vec>::new(); + ctor_script_cache.insert(raw_ctor_args.clone(), compiled.script.clone()); - let sig_cache = Cache::new(10_000); - let reused_values = SigHashReusedValuesUnsync::new(); - let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values); - - let flags = EngineFlags { covenants_enabled: true }; - let engine = DebugEngine::new(ctx, flags); - - // Seed the stack like a real spend: run sigscript pushes before locking script. - let default_name = compiled.abi.first().map(|entry| entry.name.clone()).ok_or("contract has no functions")?; - let selected_name = function_name.unwrap_or(default_name); + let selected_name = if selected_name.is_empty() { + compiled.abi.first().map(|entry| entry.name.clone()).ok_or("contract has no functions")? + } else { + selected_name + }; let entry = compiled .abi .iter() .find(|entry| entry.name == selected_name) .ok_or_else(|| format!("function '{selected_name}' not found"))?; - if entry.inputs.len() != raw_args.len() { - return Err(format!("function '{selected_name}' expects {} arguments, got {}", entry.inputs.len(), raw_args.len()).into()); + let input_types = entry.inputs.iter().map(|input| input.type_name.clone()).collect::>(); + let typed_args = parse_call_args(&input_types, &raw_args)?; + let sigscript = compiled.build_sig_script(&selected_name, typed_args)?; + + let tx = tx_scenario.unwrap_or_else(|| TestTxScenarioResolved { + version: 1, + lock_time: 0, + active_input_index: 0, + inputs: vec![TestTxInputScenarioResolved { + prev_txid: None, + prev_index: 0, + sequence: 0, + sig_op_count: 100, + utxo_value: 5000, + covenant_id: None, + constructor_args: None, + signature_script_hex: None, + utxo_script_hex: None, + }], + outputs: vec![TestTxOutputScenarioResolved { + value: 5000, + covenant_id: None, + authorizing_input: None, + constructor_args: None, + script_hex: None, + p2pk_pubkey: None, + }], + }); + + if tx.inputs.is_empty() { + return Err("tx.inputs must contain at least one input".into()); + } + if tx.active_input_index >= tx.inputs.len() { + return Err(format!("tx.active_input_index {} out of range for {} inputs", tx.active_input_index, tx.inputs.len()).into()); } - let mut typed_args = Vec::with_capacity(raw_args.len()); - for (input, raw) in entry.inputs.iter().zip(raw_args.iter()) { - typed_args.push(parse_typed_arg(&input.type_name, raw)?); + let mut tx_inputs = Vec::with_capacity(tx.inputs.len()); + let mut utxo_specs = Vec::with_capacity(tx.inputs.len()); + for (input_idx, input) in tx.inputs.iter().enumerate() { + let mut default_prev_txid = [0u8; 32]; + default_prev_txid.fill(input_idx as u8); + let prev_txid = if let Some(raw_txid) = input.prev_txid.as_deref() { + parse_txid32(raw_txid)? + } else { + TransactionId::from_bytes(default_prev_txid) + }; + + let input_ctor_raw = input.constructor_args.clone().unwrap_or_else(|| raw_ctor_args.clone()); + let redeem_script = if input.utxo_script_hex.is_none() { + Some(compile_script_for_ctor_args(&source, &parsed_contract, &input_ctor_raw, &mut ctor_script_cache)?) + } else { + None + }; + + let signature_script = if let Some(raw_sig) = input.signature_script_hex.as_deref() { + parse_hex_bytes(raw_sig)? + } else if input_idx == tx.active_input_index { + if let Some(redeem) = redeem_script.as_ref() { combine_action_and_redeem(&sigscript, redeem)? } else { sigscript.clone() } + } else if let Some(redeem) = redeem_script.as_ref() { + sigscript_push_script(redeem) + } else { + vec![] + }; + + let utxo_spk = if let Some(raw_script) = input.utxo_script_hex.as_deref() { + ScriptPublicKey::new(0, parse_hex_bytes(raw_script)?.into()) + } else { + let redeem = redeem_script.as_ref().ok_or("internal error: missing redeem script for tx input without utxo_script_hex")?; + pay_to_script_hash_script(redeem) + }; + + let covenant_id = if let Some(raw) = input.covenant_id.as_deref() { Some(parse_hash32(raw)?) } else { None }; + + tx_inputs.push(TransactionInput { + previous_outpoint: TransactionOutpoint { transaction_id: prev_txid, index: input.prev_index }, + signature_script, + sequence: input.sequence, + sig_op_count: input.sig_op_count, + }); + utxo_specs.push((input.utxo_value, utxo_spk, covenant_id)); } - // Always seed: even in --no-selector mode the function params must be pushed. - let sigscript = compiled.build_sig_script(&selected_name, typed_args)?; - let mut session = DebugSession::full(&sigscript, &compiled.script, &source, debug_info, engine)?; + let mut tx_outputs = Vec::with_capacity(tx.outputs.len()); + for output in tx.outputs.iter() { + let script_public_key = if let Some(raw_script) = output.script_hex.as_deref() { + ScriptPublicKey::new(0, parse_hex_bytes(raw_script)?.into()) + } else if let Some(raw_pubkey) = output.p2pk_pubkey.as_deref() { + let pubkey_bytes = parse_hex_bytes(raw_pubkey)?; + let p2pk_script = build_p2pk_script(&pubkey_bytes); + ScriptPublicKey::new(0, p2pk_script.into()) + } else { + let output_ctor_raw = output.constructor_args.clone().unwrap_or_else(|| raw_ctor_args.clone()); + let output_script = compile_script_for_ctor_args(&source, &parsed_contract, &output_ctor_raw, &mut ctor_script_cache)?; + pay_to_script_hash_script(&output_script) + }; + + let covenant = if let Some(raw) = output.covenant_id.as_deref() { + Some(CovenantBinding { + authorizing_input: output.authorizing_input.unwrap_or(tx.active_input_index as u16), + covenant_id: parse_hash32(raw)?, + }) + } else { + None + }; + + tx_outputs.push(TransactionOutput { value: output.value, script_public_key, covenant }); + } - println!("Stepping through {} bytes of script", compiled.script.len()); - session.run_to_first_executed_statement()?; - show_source_context(&session); - run_repl(&mut session)?; + let kas_tx = Transaction::new(tx.version, tx_inputs, tx_outputs, tx.lock_time, Default::default(), 0, vec![]); - Ok(()) + let sig_cache = Cache::new(10_000); + let reused_values = SigHashReusedValuesUnsync::new(); + let flags = EngineFlags { covenants_enabled: true }; + + let utxos = utxo_specs + .into_iter() + .map(|(value, spk, covenant_id)| UtxoEntry::new(value, spk, 0, kas_tx.is_coinbase(), covenant_id)) + .collect::>(); + let populated_tx = PopulatedTransaction::new(&kas_tx, utxos); + let cov_ctx = CovenantsContext::from_tx(&populated_tx)?; + let ctx = EngineCtx::new(&sig_cache).with_reused(&reused_values).with_covenants_ctx(&cov_ctx); + let active_input = + kas_tx.inputs.get(tx.active_input_index).ok_or_else(|| format!("missing tx input at index {}", tx.active_input_index))?; + let active_utxo = + populated_tx.utxo(tx.active_input_index).ok_or_else(|| format!("missing utxo entry for input {}", tx.active_input_index))?; + let engine = DebugEngine::from_transaction_input(&populated_tx, active_input, tx.active_input_index, active_utxo, ctx, flags); + let shadow_tx_context = ShadowTxContext { + tx: &populated_tx, + input: active_input, + input_index: tx.active_input_index, + utxo_entry: active_utxo, + covenants_ctx: &cov_ctx, + }; + let mut session = + DebugSession::full(&sigscript, &compiled.script, &source, debug_info, engine)?.with_shadow_tx_context(shadow_tx_context); + + if cli.run { + let expect_fail = expect == Some(TestExpectation::Fail); + match session.continue_to_breakpoint() { + Ok(_) if expect_fail => { + eprintln!("FAIL: expected failure but script passed"); + Err("FAIL".into()) + } + Ok(_) => { + println!("PASS"); + Ok(()) + } + Err(_) if expect_fail => { + println!("PASS (expected failure)"); + Ok(()) + } + Err(err) => { + print_failure(&session, err); + Err("FAIL".into()) + } + } + } else { + println!("Stepping through {} bytes of script", compiled.script.len()); + session.run_to_first_executed_statement()?; + show_source_context(&session); + run_repl(&mut session)?; + Ok(()) + } } diff --git a/debugger/cli/tests/cli_tests.rs b/debugger/cli/tests/cli_tests.rs index fced76fd..d1566479 100644 --- a/debugger/cli/tests/cli_tests.rs +++ b/debugger/cli/tests/cli_tests.rs @@ -1,5 +1,54 @@ use std::io::Write; use std::process::{Command, Stdio}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn write_test_fixture() -> (std::path::PathBuf, std::path::PathBuf) { + let nonce = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_nanos(); + let dir = std::env::temp_dir().join(format!("cli_debugger_test_fixture_{}_{}", std::process::id(), nonce)); + std::fs::create_dir_all(&dir).expect("create temp fixture dir"); + + let script_path = dir.join("simple.sil"); + let test_file_path = dir.join("simple.test.json"); + + std::fs::write( + &script_path, + r#"pragma silverscript ^0.1.0; + +contract Simple(int x) { + entrypoint function check(int a) { + require(a == x); + } +} +"#, + ) + .expect("write fixture contract"); + + std::fs::write( + &test_file_path, + r#"{ + "tests": [ + { + "name": "pass_case", + "function": "check", + "constructor_args": [5], + "args": [5], + "expect": "pass" + }, + { + "name": "fail_case", + "function": "check", + "constructor_args": [5], + "args": [4], + "expect": "fail" + } + ] +} +"#, + ) + .expect("write fixture test file"); + + (script_path, test_file_path) +} #[test] fn cli_debugger_repl_all_commands_smoke() { @@ -60,7 +109,106 @@ contract IfStatement(int x, int y) { assert!(stdout.contains("(sdb)"), "missing prompt output"); assert!(stdout.contains("Commands:"), "missing help output"); assert!(stdout.contains("Stack:"), "missing stack output"); - assert!(stdout.contains("no statement at line 1"), "missing invalid breakpoint warning"); + let saw_line1_feedback = stdout.contains("no statement at line 1") || stdout.contains("Breakpoint set at line 1"); + assert!(saw_line1_feedback, "missing breakpoint feedback for line 1"); assert!(stdout.contains("Breakpoint set at line 7"), "missing line-7 breakpoint success"); - assert!(stdout.contains("Breakpoints: 7"), "missing breakpoint listing"); + let listing_contains_7 = stdout.lines().any(|line| line.contains("Breakpoints:") && line.contains('7')); + assert!(listing_contains_7, "missing breakpoint listing containing line 7"); +} + +#[test] +fn cli_debugger_run_test_file_pass_case() { + let (_script_path, test_file_path) = write_test_fixture(); + + let output = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) + .arg("--run") + .arg("--test-file") + .arg(&test_file_path) + .arg("--test-name") + .arg("pass_case") + .output() + .expect("run cli-debugger pass test"); + + assert!( + output.status.success(), + "expected success, status={:?}, stderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("PASS"), "expected PASS in stdout, got: {stdout}"); +} + +#[test] +fn cli_debugger_run_test_file_expected_fail_case() { + let (_script_path, test_file_path) = write_test_fixture(); + + let output = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) + .arg("--run") + .arg("--test-file") + .arg(&test_file_path) + .arg("--test-name") + .arg("fail_case") + .output() + .expect("run cli-debugger expected-fail test"); + + assert!( + output.status.success(), + "expected success for expected-fail test, status={:?}, stderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("PASS (expected failure)"), "expected expected-failure PASS marker in stdout, got: {stdout}"); +} + +#[test] +fn cli_debugger_run_all_uses_test_file_suite() { + let (_script_path, test_file_path) = write_test_fixture(); + + let output = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) + .arg("--run-all") + .arg("--test-file") + .arg(&test_file_path) + .output() + .expect("run cli-debugger --run-all"); + + assert!( + output.status.success(), + "expected success for run-all, status={:?}, stderr={}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("PASS pass_case"), "missing pass_case line: {stdout}"); + assert!(stdout.contains("PASS fail_case"), "missing fail_case line (expected-fail test should still pass): {stdout}"); + assert!(stdout.contains("2 tests: 2 passed, 0 failed"), "missing summary line: {stdout}"); +} + +#[test] +fn cli_debugger_run_all_requires_test_file() { + let output = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) + .arg("--run-all") + .output() + .expect("run cli-debugger --run-all without test file"); + + assert!(!output.status.success(), "expected failure when --test-file is missing"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--run-all requires --test-file"), "unexpected stderr: {stderr}"); +} + +#[test] +fn cli_debugger_test_file_requires_test_name_in_run_mode() { + let (_script_path, test_file_path) = write_test_fixture(); + + let output = Command::new(env!("CARGO_BIN_EXE_cli-debugger")) + .arg("--run") + .arg("--test-file") + .arg(&test_file_path) + .output() + .expect("run cli-debugger --run --test-file without test-name"); + + assert!(!output.status.success(), "expected failure when --test-name is missing"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--test-file requires --test-name"), "unexpected stderr: {stderr}"); } diff --git a/debugger/session/src/args.rs b/debugger/session/src/args.rs new file mode 100644 index 00000000..e796a541 --- /dev/null +++ b/debugger/session/src/args.rs @@ -0,0 +1,130 @@ +use silverscript_lang::ast::{ContractAst, Expr, ExprKind}; +use silverscript_lang::span; + +pub fn parse_int_arg(raw: &str) -> Result { + let cleaned = raw.replace('_', ""); + if let Some(hex) = cleaned.strip_prefix("0x").or_else(|| cleaned.strip_prefix("0X")) { + return i64::from_str_radix(hex, 16).map_err(|err| format!("invalid hex int '{raw}': {err}")); + } + cleaned.parse::().map_err(|err| format!("invalid int '{raw}': {err}")) +} + +pub fn parse_hex_bytes(raw: &str) -> Result, String> { + let trimmed = raw.trim(); + let hex_str = trimmed.strip_prefix("0x").or_else(|| trimmed.strip_prefix("0X")).unwrap_or(trimmed); + if hex_str.is_empty() { + return Ok(vec![]); + } + let normalized = if hex_str.len() % 2 != 0 { format!("0{hex_str}") } else { hex_str.to_string() }; + if !normalized.chars().all(|ch| ch.is_ascii_hexdigit()) { + return Err(format!("invalid hex bytes '{raw}'")); + } + let mut out = vec![0u8; normalized.len() / 2]; + faster_hex::hex_decode(normalized.as_bytes(), &mut out).map_err(|err| format!("invalid hex '{raw}': {err}"))?; + Ok(out) +} + +pub fn bytes_expr(bytes: Vec) -> Expr<'static> { + Expr::new(ExprKind::Array(bytes.into_iter().map(Expr::byte).collect()), span::Span::default()) +} + +pub fn parse_typed_arg(type_name: &str, raw: &str) -> Result, String> { + if let Some(element_type) = type_name.strip_suffix("[]") { + let trimmed = raw.trim(); + if trimmed.starts_with('[') { + let values = + serde_json::from_str::>(trimmed).map_err(|err| format!("invalid array arg '{raw}': {err}"))?; + let mut out = Vec::with_capacity(values.len()); + for value in values { + let expr = match value { + serde_json::Value::Number(n) => Expr::int(n.as_i64().ok_or_else(|| "invalid int in array".to_string())?), + serde_json::Value::Bool(b) => Expr::bool(b), + serde_json::Value::String(s) => parse_typed_arg(element_type, &s)?, + _ => return Err("unsupported array element (expected number/bool/string)".to_string()), + }; + out.push(expr); + } + return Ok(Expr::new(ExprKind::Array(out), span::Span::default())); + } + if element_type == "byte" { + return Ok(bytes_expr(parse_hex_bytes(trimmed)?)); + } + return Err(format!("unsupported array literal format for '{type_name}'")); + } + + match type_name { + "int" => Ok(Expr::int(parse_int_arg(raw)?)), + "bool" => match raw { + "true" => Ok(Expr::bool(true)), + "false" => Ok(Expr::bool(false)), + _ => Err(format!("invalid bool '{raw}' (expected true/false)")), + }, + "string" => Ok(Expr::string(raw.to_string())), + "byte" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() == 1 { Ok(Expr::byte(bytes[0])) } else { Err(format!("byte expects 1 byte, got {}", bytes.len())) } + } + "bytes" => Ok(bytes_expr(parse_hex_bytes(raw)?)), + "pubkey" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 32 { + return Err(format!("pubkey expects 32 bytes, got {}", bytes.len())); + } + Ok(bytes_expr(bytes)) + } + "sig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 65 && bytes.len() != 32 { + return Err(format!("sig expects 65 bytes (or 32-byte secret key for auto-sign), got {}", bytes.len())); + } + Ok(bytes_expr(bytes)) + } + "datasig" => { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != 64 && bytes.len() != 32 { + return Err(format!("datasig expects 64 bytes (or 32-byte secret key for auto-sign), got {}", bytes.len())); + } + Ok(bytes_expr(bytes)) + } + other => { + let size = other + .strip_prefix("bytes") + .and_then(|v| v.parse::().ok()) + .or_else(|| other.strip_prefix("byte[").and_then(|v| v.strip_suffix(']')).and_then(|v| v.parse::().ok())); + + if let Some(size) = size { + let bytes = parse_hex_bytes(raw)?; + if bytes.len() != size { + return Err(format!("{other} expects {size} bytes, got {}", bytes.len())); + } + Ok(bytes_expr(bytes)) + } else { + Err(format!("unsupported arg type '{other}'")) + } + } + } +} + +pub fn parse_ctor_args(parsed_contract: &ContractAst<'_>, raw_ctor_args: &[String]) -> Result>, String> { + if parsed_contract.params.len() != raw_ctor_args.len() { + return Err(format!("constructor expects {} arguments, got {}", parsed_contract.params.len(), raw_ctor_args.len())); + } + + let mut out = Vec::with_capacity(raw_ctor_args.len()); + for (param, raw) in parsed_contract.params.iter().zip(raw_ctor_args.iter()) { + out.push(parse_typed_arg(¶m.type_ref.type_name(), raw)?); + } + Ok(out) +} + +pub fn parse_call_args(input_types: &[String], raw_args: &[String]) -> Result>, String> { + if input_types.len() != raw_args.len() { + return Err(format!("function expects {} arguments, got {}", input_types.len(), raw_args.len())); + } + + let mut typed_args = Vec::with_capacity(raw_args.len()); + for (input_type, raw) in input_types.iter().zip(raw_args.iter()) { + typed_args.push(parse_typed_arg(input_type, raw)?); + } + Ok(typed_args) +} diff --git a/debugger/session/src/lib.rs b/debugger/session/src/lib.rs index cfeaf3a4..a97f3ea6 100644 --- a/debugger/session/src/lib.rs +++ b/debugger/session/src/lib.rs @@ -1,3 +1,8 @@ +pub mod args; pub mod presentation; pub mod session; +pub mod test_runner; pub mod util; + +pub use presentation::format_failure_report; +pub use session::{CallStackEntry, FailureFrame, FailureReport}; diff --git a/debugger/session/src/presentation.rs b/debugger/session/src/presentation.rs index b56e2529..272a554a 100644 --- a/debugger/session/src/presentation.rs +++ b/debugger/session/src/presentation.rs @@ -1,6 +1,6 @@ use silverscript_lang::debug_info::SourceSpan; -use crate::session::DebugValue; +use crate::session::{DebugValue, FailureReport}; use crate::util::{decode_i64, encode_hex}; #[derive(Debug, Clone)] @@ -111,3 +111,67 @@ fn array_element_size(element_type: &str) -> Option { other => other.strip_prefix("bytes").and_then(|v| v.parse::().ok()), } } + +/// Renders a `FailureReport` in a Rust-style diagnostic format. +pub fn format_failure_report(report: &FailureReport, format_var: &dyn Fn(&str, &DebugValue) -> String) -> String { + let source_lines: Vec<&str> = report.source_text.lines().collect(); + let mut out = String::new(); + + let max_line = report.frames.iter().filter_map(|f| f.span.map(|s| s.line)).max().unwrap_or(1); + let w = format!("{max_line}").len().max(2); + let pad = " ".repeat(w); + + out.push_str(&format!("error: {}\n", report.message)); + + for (frame_idx, frame) in report.frames.iter().enumerate() { + let Some(span) = frame.span else { + continue; + }; + + let line_idx = span.line.saturating_sub(1) as usize; + + if frame_idx == 0 { + out.push_str(&format!("{pad} --> {}:{}\n", span.line, span.col)); + } else { + out.push_str(&format!("{pad} ::: called from {}\n", frame.function_name)); + } + + out.push_str(&format!("{pad} |\n")); + + if line_idx > 0 { + if let Some(prev) = source_lines.get(line_idx - 1) { + out.push_str(&format!("{:>w$} | {prev}\n", span.line - 1)); + } + } + + if let Some(line_text) = source_lines.get(line_idx) { + out.push_str(&format!("{:>w$} | {line_text}\n", span.line)); + + let start_col = span.col.saturating_sub(1) as usize; + let end_col = if span.end_line == span.line && span.end_col > span.col { + span.end_col.saturating_sub(1) as usize + } else { + line_text.len() + }; + let underline_len = end_col.saturating_sub(start_col).max(1); + let marker_pad = " ".repeat(start_col); + let underline = "^".repeat(underline_len); + let label = if frame_idx == 0 { " verification failed here" } else { " in this call" }; + out.push_str(&format!("{pad} | {marker_pad}{underline}{label}\n")); + + if !frame.variables.is_empty() { + let vars_str = frame + .variables + .iter() + .map(|var| format!("{} = {}", var.name, format_var(&var.type_name, &var.value))) + .collect::>() + .join(", "); + out.push_str(&format!("{pad} | {vars_str}\n")); + } + } + + out.push_str(&format!("{pad} |\n")); + } + + out +} diff --git a/debugger/session/src/session.rs b/debugger/session/src/session.rs index 4b5825fd..511ef46d 100644 --- a/debugger/session/src/session.rs +++ b/debugger/session/src/session.rs @@ -6,6 +6,7 @@ use kaspa_txscript::caches::Cache; use kaspa_txscript::covenants::CovenantsContext; use kaspa_txscript::script_builder::ScriptBuilder; use kaspa_txscript::{DynOpcodeImplementation, EngineCtx, EngineFlags, TxScriptEngine, parse_script}; +use serde::{Deserialize, Serialize}; use silverscript_lang::ast::{Expr, ExprKind}; use silverscript_lang::compiler::compile_debug_expr; @@ -76,6 +77,48 @@ pub struct SessionState<'i> { pub stack: Vec, } +#[derive(Debug, Clone)] +pub struct CallStackEntry { + pub callee_name: String, + pub call_site_span: Option, + /// Sequence of the InlineCallEnter step (caller's context). + pub sequence: u32, + /// Frame ID of the InlineCallEnter step (caller's frame). + pub frame_id: u32, +} + +#[derive(Debug, Clone)] +pub struct FailureFrame { + pub function_name: String, + /// Source location: failure site for innermost frame, call-site for callers. + pub span: Option, + pub variables: Vec, +} + +#[derive(Debug, Clone)] +pub struct FailureReport { + /// Human-readable description, e.g. "require() failed". + pub message: String, + /// Innermost frame first. + pub frames: Vec, + /// Full source text for rendering context lines. + pub source_text: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StackSnapshot { + pub dstack: Vec, + pub astack: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpcodeMeta<'i> { + pub index: usize, + pub byte_offset: usize, + pub display: String, + pub step: Option>, +} + pub struct DebugSession<'a, 'i> { engine: DebugEngine<'a>, shadow_tx_context: Option>, @@ -332,6 +375,34 @@ impl<'a, 'i> DebugSession<'a, 'i> { valid } + /// Resolves a requested source line to a steppable line, preferring exact + /// hits then the next steppable line. + pub fn resolve_breakpoint_line(&self, line: u32) -> Option { + let mut next: Option = None; + for step in self.step_order.iter().filter_map(|&index| self.debug_info.steps.get(index)) { + if !self.is_steppable_step(step) { + continue; + } + if line >= step.span.line && line <= step.span.end_line { + return Some(line); + } + if step.span.line > line { + match next { + Some(current) if current <= step.span.line => {} + _ => next = Some(step.span.line), + } + } + } + next + } + + /// Resolves and adds a breakpoint. Returns the actual line if set. + pub fn add_breakpoint_resolved(&mut self, line: u32) -> Option { + let resolved = self.resolve_breakpoint_line(line)?; + self.breakpoints.insert(resolved); + Some(resolved) + } + /// Returns all currently set breakpoint line numbers. pub fn breakpoints(&self) -> Vec { let mut lines = self.breakpoints.iter().copied().collect::>(); @@ -412,6 +483,32 @@ impl<'a, 'i> DebugSession<'a, 'i> { stack } + /// Returns the active inline call stack with source spans and frame identity. + pub fn call_stack_with_spans(&self) -> Vec { + let mut stack = Vec::new(); + let Some(current) = self.current_step_index else { + return stack; + }; + for order_index in 0..=current { + let Some(step) = self.step_at_order(order_index) else { + continue; + }; + match &step.kind { + StepKind::InlineCallEnter { callee } => stack.push(CallStackEntry { + callee_name: callee.clone(), + call_site_span: Some(step.span), + sequence: step.sequence, + frame_id: step.frame_id, + }), + StepKind::InlineCallExit { .. } => { + stack.pop(); + } + _ => {} + } + } + stack + } + /// Returns the name of the function currently being executed. pub fn current_function_name(&self) -> Option<&str> { self.current_function_range().map(|range| range.name.as_str()) @@ -613,6 +710,55 @@ impl<'a, 'i> DebugSession<'a, 'i> { stacks.dstack.iter().map(|item| encode_hex(item)).collect() } + /// Returns both main and alt stacks as hex strings. + pub fn stack_snapshot(&self) -> StackSnapshot { + let stacks = self.engine.stacks(); + StackSnapshot { + dstack: stacks.dstack.iter().map(|item| encode_hex(item)).collect(), + astack: stacks.astack.iter().map(|item| encode_hex(item)).collect(), + } + } + + /// Returns bytecode/opcode metadata aligned with source steps. + pub fn opcode_metas(&self) -> Vec> { + self.op_displays + .iter() + .enumerate() + .map(|(index, display)| OpcodeMeta { + index, + byte_offset: self.opcode_offsets.get(index).copied().unwrap_or(self.script_len), + display: display.clone(), + step: self.step_for_offset(self.opcode_offsets.get(index).copied().unwrap_or(self.script_len)).cloned(), + }) + .collect() + } + + /// Builds a structured failure report suitable for CLI/DAP rendering. + pub fn build_failure_report(&self, error: &kaspa_txscript_errors::TxScriptError) -> FailureReport { + let failure_span = self.current_span(); + let call_stack = self.call_stack_with_spans(); + let innermost_function = self.current_function_name().unwrap_or("").to_string(); + let innermost_vars: Vec = self.list_variables().unwrap_or_default().into_iter().filter(|v| !v.is_constant).collect(); + + let mut frames = + vec![FailureFrame { function_name: innermost_function.clone(), span: failure_span, variables: innermost_vars }]; + + let entry_name = self.current_function_name().unwrap_or("").to_string(); + for idx in (0..call_stack.len()).rev() { + let entry = &call_stack[idx]; + let caller_vars: Vec = self + .list_variables_at_sequence(entry.sequence, entry.frame_id) + .unwrap_or_default() + .into_iter() + .filter(|v| !v.is_constant) + .collect(); + let caller_name = if idx == 0 { entry_name.clone() } else { call_stack[idx - 1].callee_name.clone() }; + frames.push(FailureFrame { function_name: caller_name, span: entry.call_site_span, variables: caller_vars }); + } + + FailureReport { message: format!("{error}"), frames, source_text: self.source_lines.join("\n") } + } + /// Evaluates an expression using shadow VM execution. /// /// Strategy: compile the pre-resolved expression to bytecode, build a mini-script diff --git a/debugger/session/src/test_runner.rs b/debugger/session/src/test_runner.rs new file mode 100644 index 00000000..8d65267b --- /dev/null +++ b/debugger/session/src/test_runner.rs @@ -0,0 +1,249 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; +use serde_json::Value; + +#[derive(Debug, Clone, Deserialize)] +pub struct ContractTestFile { + pub tests: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ContractTestCase { + pub name: String, + pub function: String, + #[serde(default)] + pub constructor_args: Vec, + #[serde(default)] + pub args: Vec, + pub expect: TestExpectation, + #[serde(default)] + pub tx: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TestExpectation { + Pass, + Fail, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TestTxScenario { + #[serde(default = "default_tx_version")] + pub version: u16, + #[serde(default)] + pub lock_time: u64, + #[serde(default)] + pub active_input_index: usize, + pub inputs: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TestTxInputScenario { + #[serde(default)] + pub prev_txid: Option, + #[serde(default)] + pub prev_index: u32, + #[serde(default)] + pub sequence: u64, + #[serde(default = "default_sig_op_count")] + pub sig_op_count: u8, + pub utxo_value: u64, + #[serde(default)] + pub covenant_id: Option, + #[serde(default)] + pub constructor_args: Option>, + #[serde(default)] + pub signature_script_hex: Option, + #[serde(default)] + pub utxo_script_hex: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TestTxOutputScenario { + pub value: u64, + #[serde(default)] + pub covenant_id: Option, + #[serde(default)] + pub authorizing_input: Option, + #[serde(default)] + pub constructor_args: Option>, + #[serde(default)] + pub script_hex: Option, + #[serde(default)] + pub p2pk_pubkey: Option, +} + +#[derive(Debug, Clone)] +pub struct ResolvedContractTest { + pub script_path: PathBuf, + pub test_file_path: PathBuf, + pub test: ContractTestCaseResolved, +} + +#[derive(Debug, Clone)] +pub struct ContractTestCaseResolved { + pub name: String, + pub function: String, + pub constructor_args: Vec, + pub args: Vec, + pub expect: TestExpectation, + pub tx: Option, +} + +#[derive(Debug, Clone)] +pub struct TestTxScenarioResolved { + pub version: u16, + pub lock_time: u64, + pub active_input_index: usize, + pub inputs: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Clone)] +pub struct TestTxInputScenarioResolved { + pub prev_txid: Option, + pub prev_index: u32, + pub sequence: u64, + pub sig_op_count: u8, + pub utxo_value: u64, + pub covenant_id: Option, + pub constructor_args: Option>, + pub signature_script_hex: Option, + pub utxo_script_hex: Option, +} + +#[derive(Debug, Clone)] +pub struct TestTxOutputScenarioResolved { + pub value: u64, + pub covenant_id: Option, + pub authorizing_input: Option, + pub constructor_args: Option>, + pub script_hex: Option, + pub p2pk_pubkey: Option, +} + +fn default_tx_version() -> u16 { + 1 +} + +fn default_sig_op_count() -> u8 { + 100 +} + +pub fn discover_sidecar_path(script_path: &Path) -> Result { + let stem = script_path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| format!("failed to derive stem from '{}'", script_path.display()))?; + let sidecar_name = format!("{stem}.test.json"); + Ok(script_path.with_file_name(sidecar_name)) +} + +pub fn read_contract_test_file(test_file_path: &Path) -> Result { + let raw = std::fs::read_to_string(test_file_path) + .map_err(|err| format!("failed to read test file '{}': {err}", test_file_path.display()))?; + serde_json::from_str::(&raw).map_err(|err| format!("invalid test file '{}': {err}", test_file_path.display())) +} + +pub fn resolve_contract_test( + test_file_path: &Path, + test_name: &str, + script_path_override: Option<&Path>, +) -> Result { + let script_path = if let Some(script_path) = script_path_override { + std::fs::canonicalize(script_path) + .map_err(|err| format!("failed to canonicalize script path '{}': {err}", script_path.display()))? + } else { + let inferred = infer_script_path_from_sidecar(test_file_path)?; + std::fs::canonicalize(&inferred) + .map_err(|err| format!("failed to canonicalize inferred script path '{}': {err}", inferred.display()))? + }; + + let canonical_test_file = std::fs::canonicalize(test_file_path) + .map_err(|err| format!("failed to canonicalize test file '{}': {err}", test_file_path.display()))?; + + let parsed = read_contract_test_file(&canonical_test_file)?; + let test = parsed + .tests + .into_iter() + .find(|entry| entry.name == test_name) + .ok_or_else(|| format!("test '{test_name}' not found in '{}'", canonical_test_file.display()))?; + + let resolved = ContractTestCaseResolved { + name: test.name, + function: test.function, + constructor_args: values_to_args(&test.constructor_args)?, + args: values_to_args(&test.args)?, + expect: test.expect, + tx: test.tx.map(resolve_tx_scenario).transpose()?, + }; + + Ok(ResolvedContractTest { script_path, test_file_path: canonical_test_file, test: resolved }) +} + +fn infer_script_path_from_sidecar(test_file_path: &Path) -> Result { + let file_name = test_file_path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| format!("invalid test file name '{}'", test_file_path.display()))?; + + let script_name = file_name + .strip_suffix(".test.json") + .ok_or_else(|| format!("test file '{}' must end with '.test.json'", test_file_path.display()))?; + + Ok(test_file_path.with_file_name(format!("{script_name}.sil"))) +} + +pub fn resolve_tx_scenario(tx: TestTxScenario) -> Result { + let mut inputs = Vec::with_capacity(tx.inputs.len()); + for input in tx.inputs { + inputs.push(TestTxInputScenarioResolved { + prev_txid: input.prev_txid, + prev_index: input.prev_index, + sequence: input.sequence, + sig_op_count: input.sig_op_count, + utxo_value: input.utxo_value, + covenant_id: input.covenant_id, + constructor_args: input.constructor_args.as_ref().map(|values| values_to_args(values)).transpose()?, + signature_script_hex: input.signature_script_hex, + utxo_script_hex: input.utxo_script_hex, + }); + } + + let mut outputs = Vec::with_capacity(tx.outputs.len()); + for output in tx.outputs { + outputs.push(TestTxOutputScenarioResolved { + value: output.value, + covenant_id: output.covenant_id, + authorizing_input: output.authorizing_input, + constructor_args: output.constructor_args.as_ref().map(|values| values_to_args(values)).transpose()?, + script_hex: output.script_hex, + p2pk_pubkey: output.p2pk_pubkey, + }); + } + + Ok(TestTxScenarioResolved { + version: tx.version, + lock_time: tx.lock_time, + active_input_index: tx.active_input_index, + inputs, + outputs, + }) +} + +pub fn values_to_args(values: &[Value]) -> Result, String> { + values.iter().map(value_to_arg).collect() +} + +fn value_to_arg(value: &Value) -> Result { + match value { + Value::String(raw) => Ok(raw.clone()), + Value::Number(raw) => Ok(raw.to_string()), + Value::Bool(raw) => Ok(raw.to_string()), + Value::Null => Ok("null".to_string()), + Value::Array(_) | Value::Object(_) => serde_json::to_string(value).map_err(|err| format!("invalid arg value: {err}")), + } +} diff --git a/silverscript-lang/src/debug_info.rs b/silverscript-lang/src/debug_info.rs index 0bf09fb6..f901ca6d 100644 --- a/silverscript-lang/src/debug_info.rs +++ b/silverscript-lang/src/debug_info.rs @@ -180,7 +180,7 @@ pub enum StepKind { mod tests { use serde_json::json; - use super::{DebugInfo, SourceSpan, StepKind}; + use super::{DebugInfo, SourceSpan}; use crate::span::Span; #[test] @@ -231,7 +231,6 @@ mod tests { "frame_id": 0, "variable_updates": [] }], - "variable_updates": [], "params": [], "functions": [], "constants": [] @@ -240,50 +239,6 @@ mod tests { let parsed: DebugInfo<'static> = serde_json::from_value(value).expect("parse debug info"); let serialized = serde_json::to_value(parsed).expect("serialize debug info"); - assert!(serialized.get("variable_updates").is_none(), "top-level variable_updates should not exist"); assert!(serialized["steps"][0].get("variable_updates").is_some(), "step should carry variable_updates"); } - - #[test] - fn debug_info_schema_accepts_legacy_statement_and_virtual_kind_names() { - let statement_value = json!({ - "source": "", - "steps": [{ - "bytecode_start": 0, - "bytecode_end": 1, - "span": { "line": 1, "col": 1, "end_line": 1, "end_col": 1 }, - "kind": { "Statement": {} }, - "sequence": 0, - "call_depth": 0, - "frame_id": 0, - "variable_updates": [] - }], - "params": [], - "functions": [], - "constants": [] - }); - - let virtual_value = json!({ - "source": "", - "steps": [{ - "bytecode_start": 0, - "bytecode_end": 0, - "span": { "line": 1, "col": 1, "end_line": 1, "end_col": 1 }, - "kind": { "Virtual": {} }, - "sequence": 0, - "call_depth": 0, - "frame_id": 0, - "variable_updates": [] - }], - "params": [], - "functions": [], - "constants": [] - }); - - let statement: DebugInfo<'static> = serde_json::from_value(statement_value).expect("legacy statement parses"); - let virtual_step: DebugInfo<'static> = serde_json::from_value(virtual_value).expect("legacy virtual parses"); - - assert!(matches!(statement.steps[0].kind, StepKind::Source {})); - assert!(matches!(virtual_step.steps[0].kind, StepKind::Source {})); - } } From ae125d61a2985b0de0f3c21fcc7f0693ee14bbd3 Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:50:24 +0200 Subject: [PATCH 40/41] sync: align debugger with merged #46 and resolve merge artifacts --- silverscript-lang/src/compiler.rs | 4 ++-- silverscript-lang/tests/compiler_tests.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 15dd8fd4..469a21e4 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -18,7 +18,7 @@ mod debug_recording; use debug_recording::DebugRecorder; /// Prefix used for synthetic argument bindings during inline function expansion. -pub const SYNTHETIC_ARG_PREFIX: &str = "__arg_"; +pub const SYNTHETIC_ARG_PREFIX: &str = "__arg"; #[derive(Debug, Clone, Copy, Default)] pub struct CompileOptions { @@ -1809,7 +1809,7 @@ fn compile_inline_call<'i>( } for (index, (param, arg)) in function.params.iter().zip(args.iter()).enumerate() { let resolved = resolve_expr(arg.clone(), caller_env, &mut HashSet::new())?; - let temp_name = format!("{SYNTHETIC_ARG_PREFIX}{name}_{index}"); + let temp_name = format!("{SYNTHETIC_ARG_PREFIX}_{name}_{index}"); let param_type_name = type_name_from_ref(¶m.type_ref); env.insert(temp_name.clone(), resolved.clone()); types.insert(temp_name.clone(), param_type_name.clone()); diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 2c77fda4..d0316070 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -3702,7 +3702,7 @@ fn function_param_shadows_constructor_constant_with_same_name() { #[test] fn nested_inline_calls_with_args_compile_and_execute() { // Nested inline calls must propagate synthetic __arg_ bindings so that - // inner calls can resolve arguments that flow through outer calls. + // deeply nested calls can resolve arguments that flow through outer calls. let source = r#" contract NestedArgs() { function inner(int x) { @@ -3715,8 +3715,13 @@ fn nested_inline_calls_with_args_compile_and_execute() { require(v >= 0); } + function top(int z) { + outer(z); + require(z >= 0); + } + entrypoint function main(int a) { - outer(a); + top(a); require(a >= 0); } } From d626a116b0c9694255f2083102d3844c70ecb15c Mon Sep 17 00:00:00 2001 From: Manyfestation <240733973+Manyfestation@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:14:47 +0200 Subject: [PATCH 41/41] rename caller_params to params --- silverscript-lang/src/compiler.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index 469a21e4..9428d87a 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1762,7 +1762,7 @@ fn compile_inline_call<'i>( name: &str, args: &[Expr<'i>], call_span: SourceSpan, - caller_params: &HashMap, + params: &HashMap, caller_types: &mut HashMap, caller_env: &mut HashMap>, builder: &mut ScriptBuilder, @@ -1843,7 +1843,6 @@ fn compile_inline_call<'i>( recorder.begin_inline_call(call_span, call_start, function, &env)?; let mut yields: Vec> = Vec::new(); - let params = caller_params.clone(); let body_len = function.body.len(); for (index, stmt) in function.body.iter().enumerate() { recorder.begin_statement_at(builder.script().len(), &env); @@ -1861,7 +1860,7 @@ fn compile_inline_call<'i>( compile_statement( stmt, &mut env, - ¶ms, + params, &mut types, builder, options,