diff --git a/.gitignore b/.gitignore index 424d050..64ffcdc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ site/ venv/ env/ *.out +*.masp node_modules/ *DS_Store ._* diff --git a/Cargo.lock b/Cargo.lock index 3678a12..49fcbf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,9 +35,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -50,15 +50,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -128,6 +128,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + [[package]] name = "bincode" version = "1.3.3" @@ -169,7 +175,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -210,9 +216,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -234,7 +240,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] @@ -244,7 +261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -263,9 +280,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -273,9 +290,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -284,9 +301,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -296,15 +313,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compact_str" @@ -341,6 +358,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -437,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -457,14 +483,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -480,13 +529,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -522,6 +582,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -536,9 +602,21 @@ dependencies = [ [[package]] name = "dissimilar" -version = "1.0.10" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "dummy" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" +checksum = "1cac124e13ae9aa56acc4241f8c8207501d93afdd8d8e62f0c1f2e12f6508c65" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "ecdsa" @@ -615,9 +693,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -625,9 +703,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -652,6 +730,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fake" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d391ba4af7f1d93f01fcf7b2f29e2bc9348e109dfdbf4dcbdc51dfa38dab0b6" +dependencies = [ + "deunicode", + "dummy", + "rand 0.8.5", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -720,6 +809,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.32" @@ -854,9 +952,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -868,6 +968,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -980,11 +1081,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", @@ -1017,9 +1118,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -1085,7 +1186,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1215,13 +1316,12 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miden-air" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5322d00bef8b19f4cd3415da2533a87c8860c7d9b80043d6cce0f184b40c5fff" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "miden-core", "miden-crypto", "miden-utils-indexing", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -1236,7 +1336,7 @@ dependencies = [ "miden-core", "miden-mast-package", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1258,14 +1358,13 @@ dependencies = [ "rustc_version 0.4.1", "semver 1.0.27", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] name = "miden-core" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf4f5601b0d669aa125cce3bba4b98f2c8df729e2d53e66777429ac5f53e228" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "derive_more", "itertools 0.14.0", @@ -1274,10 +1373,26 @@ dependencies = [ "miden-formatting", "miden-utils-core-derive", "miden-utils-indexing", - "miden-utils-sync", + "miden-utils-sync 0.22.0 (git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46)", "num-derive", "num-traits", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "miden-core-lib" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82595fabb062315c32f6fc11c31755d3e5c6f8bc8c67d35154a067397d65b1de" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-core", + "miden-crypto", + "miden-processor", + "miden-utils-sync 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.18", ] [[package]] @@ -1311,14 +1426,14 @@ dependencies = [ "p3-symmetric", "p3-util", "rand 0.9.2", - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", "rand_hc", "serde", "sha2", "sha3", "subtle", - "thiserror", + "thiserror 2.0.18", "x25519-dalek", ] @@ -1345,17 +1460,24 @@ dependencies = [ "miden-assembly", "miden-assembly-syntax", "miden-core", + "miden-crypto", + "miden-debug-dap", + "miden-debug-engine", "miden-debug-types", "miden-mast-package", "miden-processor", + "miden-protocol", "miden-thiserror", + "miden-tx", "num-traits", "proptest", "ratatui", "rustc-demangle", "serde", + "serde_json", "signal-hook", "smallvec", + "socket2", "syntect", "tokio", "tokio-util", @@ -1363,22 +1485,58 @@ dependencies = [ "tui-input", ] +[[package]] +name = "miden-debug-dap" +version = "0.1.0" +dependencies = [ + "fake", + "rand 0.10.0", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "miden-debug-engine" +version = "0.1.0" +dependencies = [ + "clap", + "glob", + "log", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-debug-dap", + "miden-debug-types", + "miden-mast-package", + "miden-processor", + "miden-thiserror", + "miden-tx", + "num-traits", + "proptest", + "rustc-demangle", + "serde", + "serde_json", + "smallvec", + "socket2", + "toml 0.8.23", +] + [[package]] name = "miden-debug-types" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ef08bafef275f0d6a15108108b3f6df6642772e0a1c05e102cb7e96841e888" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "memchr", "miden-crypto", "miden-formatting", "miden-miette", "miden-utils-indexing", - "miden-utils-sync", + "miden-utils-sync 0.22.0 (git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46)", "paste", "serde", - "serde_spanned 1.0.4", - "thiserror", + "serde_spanned 1.1.0", + "thiserror 2.0.18", ] [[package]] @@ -1396,7 +1554,7 @@ dependencies = [ "rand 0.10.0", "serde", "subtle", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1418,7 +1576,7 @@ dependencies = [ "miden-assembly-syntax", "miden-core", "miden-debug-types", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1441,7 +1599,7 @@ dependencies = [ "strip-ansi-escapes", "syn 2.0.117", "textwrap", - "thiserror", + "thiserror 2.0.18", "trybuild", "unicode-width 0.1.14", ] @@ -1460,8 +1618,7 @@ dependencies = [ [[package]] name = "miden-processor" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba53ff06ef0affa0c3fb13e7e2ef5bde99f96eebcec8c360c6658050480ef676" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "itertools 0.14.0", "miden-air", @@ -1471,7 +1628,60 @@ dependencies = [ "miden-utils-indexing", "paste", "rayon", - "thiserror", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "miden-protocol" +version = "0.14.0" +source = "git+https://github.com/0xMiden/protocol?rev=ae4b45778c7e2c73be87b67906b7fa1ae8e2a285#ae4b45778c7e2c73be87b67906b7fa1ae8e2a285" +dependencies = [ + "bech32", + "fs-err", + "getrandom 0.3.4", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-mast-package", + "miden-processor", + "miden-protocol-macros", + "miden-utils-sync 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", + "miden-verifier", + "rand 0.9.2", + "regex", + "semver 1.0.27", + "thiserror 2.0.18", + "walkdir", +] + +[[package]] +name = "miden-protocol-macros" +version = "0.14.0" +source = "git+https://github.com/0xMiden/protocol?rev=ae4b45778c7e2c73be87b67906b7fa1ae8e2a285#ae4b45778c7e2c73be87b67906b7fa1ae8e2a285" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "miden-prover" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15462425359e87540d92e277cf1174a85a174ca433bd63d27286f65ab318f2d4" +dependencies = [ + "bincode", + "miden-air", + "miden-core", + "miden-crypto", + "miden-debug-types", + "miden-processor", + "serde", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -1486,6 +1696,22 @@ dependencies = [ "p3-goldilocks", ] +[[package]] +name = "miden-standards" +version = "0.14.0" +source = "git+https://github.com/0xMiden/protocol?rev=ae4b45778c7e2c73be87b67906b7fa1ae8e2a285#ae4b45778c7e2c73be87b67906b7fa1ae8e2a285" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-core-lib", + "miden-processor", + "miden-protocol", + "regex", + "thiserror 2.0.18", + "walkdir", +] + [[package]] name = "miden-thiserror" version = "1.0.59" @@ -1506,11 +1732,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "miden-tx" +version = "0.14.0" +source = "git+https://github.com/0xMiden/protocol?rev=ae4b45778c7e2c73be87b67906b7fa1ae8e2a285#ae4b45778c7e2c73be87b67906b7fa1ae8e2a285" +dependencies = [ + "miden-processor", + "miden-protocol", + "miden-prover", + "miden-standards", + "miden-verifier", + "thiserror 2.0.18", +] + [[package]] name = "miden-utils-core-derive" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "477db426fc31f666d7e65b0cc907fe431d36d88d611a0594cf266104eb168b4c" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "proc-macro2", "quote", @@ -1520,8 +1758,7 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "785c1ec4ad9994100b117b8eab8c453dcc35d3d168e4f72ac818efb700abe7b1" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "miden-crypto", "miden-debug-types", @@ -1533,11 +1770,10 @@ dependencies = [ [[package]] name = "miden-utils-indexing" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46cec00c8cf32ec46df7542fb9ea15fbe7a5149920ef97776a4f4bc3a563e8de" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "miden-crypto", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1545,6 +1781,16 @@ name = "miden-utils-sync" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529c1c173506f30d3949f7a54b65f1eb318098e37ed5730a1bb9027eee2fa4b" +dependencies = [ + "lock_api", + "loom", + "once_cell", +] + +[[package]] +name = "miden-utils-sync" +version = "0.22.0" +source = "git+https://github.com/0xMiden/miden-vm.git?rev=1b60fa4b54e60075cad7a761b593d5c00c6aaf46#1b60fa4b54e60075cad7a761b593d5c00c6aaf46" dependencies = [ "lock_api", "loom", @@ -1552,6 +1798,21 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "miden-verifier" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997c842047ffa2d011eb65bf638a3135b2d52bce5b20770fcc6040f1b48c624a" +dependencies = [ + "bincode", + "miden-air", + "miden-core", + "miden-crypto", + "serde", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "midenc-hir-type" version = "0.5.3" @@ -1563,7 +1824,7 @@ dependencies = [ "serde", "serde_repr", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1699,9 +1960,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1898,7 +2159,7 @@ dependencies = [ "p3-field", "p3-matrix", "p3-util", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1917,7 +2178,7 @@ dependencies = [ "p3-miden-transcript", "p3-util", "rand 0.10.0", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -1938,7 +2199,7 @@ dependencies = [ "p3-miden-stateful-hasher", "p3-miden-transcript", "p3-util", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -1958,7 +2219,7 @@ dependencies = [ "p3-util", "rand 0.10.0", "serde", - "thiserror", + "thiserror 2.0.18", "tracing", ] @@ -1981,7 +2242,7 @@ dependencies = [ "p3-challenger", "p3-field", "serde", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2130,7 +2391,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -2143,9 +2404,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -2186,16 +2447,16 @@ dependencies = [ [[package]] name = "proptest" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", "bitflags", "num-traits", "rand 0.9.2", - "rand_chacha", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -2230,13 +2491,24 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] @@ -2246,9 +2518,21 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", "rand_core 0.10.0", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_chacha" version = "0.9.0" @@ -2513,6 +2797,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "semver-parser" @@ -2585,9 +2873,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -2599,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2702,6 +2990,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -2833,7 +3131,7 @@ dependencies = [ "regex-syntax", "serde", "serde_derive", - "thiserror", + "thiserror 2.0.18", "walkdir", "yaml-rust", ] @@ -2846,9 +3144,9 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -2886,13 +3184,33 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2973,17 +3291,17 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 1.0.0+spec-1.1.0", + "serde_spanned 1.1.0", + "toml_datetime 1.1.0+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.0", ] [[package]] @@ -2997,9 +3315,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] @@ -3015,16 +3333,16 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -3035,9 +3353,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "tracing" @@ -3084,9 +3402,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3123,7 +3441,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 1.0.6+spec-1.1.0", + "toml 1.1.0+spec-1.1.0", ] [[package]] @@ -3162,9 +3480,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" @@ -3400,6 +3718,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -3491,6 +3818,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3600,18 +3933,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0e4cf23..270cb9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,7 @@ +[workspace] +members = [".", "crates/dap", "crates/engine"] +resolver = "2" + [package] name = "miden-debug" description = "An interactive debugger for Miden VM programs" @@ -23,15 +27,23 @@ test = false bench = false required-features = ["tui"] +[[example]] +name = "compile-masm" +required-features = ["std"] + [features] default = ["tui"] -tui = ["std", "dep:crossterm", "dep:env_logger", "dep:ratatui", "dep:tui-input", "dep:signal-hook", "dep:syntect"] -std = ["dep:glob", "clap/std", "clap/env", "miden-assembly-syntax/std"] -proptest = ["dep:proptest"] +tui = ["std", "dep:crossterm", "dep:env_logger", "dep:ratatui", "dep:tui-input", "dep:signal-hook", "dep:syntect", "miden-debug-engine/tui"] +dap = ["dep:dap", "dep:socket2", "miden-debug-engine/dap"] +std = ["dep:glob", "clap/std", "clap/env", "miden-assembly-syntax/std", "miden-debug-engine/std"] +proptest = ["dep:proptest", "miden-debug-engine/proptest"] [dependencies] clap = { version = "4.5", default-features = false, features = ["derive", "std", "env", "help", "suggestions", "error-context"]} crossterm = { version = "0.28.1", optional = true, features = ["event-stream"] } +dap = { package = "miden-debug-dap", path = "crates/dap", optional = true, features = ["client"] } +miden-debug-engine = { path = "crates/engine", default-features = false } +serde_json = "1" env_logger = { version = "0.11", optional = true } log = "0.4" glob = { version = "0.3.1", optional = true } @@ -41,6 +53,8 @@ miden-core = { version = "0.22", default-features = false } miden-debug-types = { version = "0.22", default-features = false } miden-mast-package = { version = "0.22", default-features = false } miden-processor = { version = "0.22", default-features = false } +miden-protocol = { git = "https://github.com/0xMiden/protocol", rev = "ae4b45778c7e2c73be87b67906b7fa1ae8e2a285", default-features = false } +miden-tx = { git = "https://github.com/0xMiden/protocol", rev = "ae4b45778c7e2c73be87b67906b7fa1ae8e2a285", default-features = false } num-traits = "0.2" ratatui = { version = "0.29.0", optional = true } rustc-demangle = { version = "0.1", features = ["std"] } @@ -67,7 +81,19 @@ thiserror = { package = "miden-thiserror", version = "1.0" } toml = { version = "0.8", features = ["preserve_order"] } tui-input = { version = "0.11", optional = true } +socket2 = { version = "0.5", optional = true } tokio = { version = "1.39.2", features = ["rt", "time", "macros", "rt-multi-thread"] } tokio-util = "0.7.11" futures = "0.3.30" proptest = { version = "1.4", optional = true } + +miden-crypto = { version = "=0.23.0", default-features = false } + +[patch.crates-io] +miden-air = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-core = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-debug-types = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-processor = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-utils-diagnostics = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-utils-indexing = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } +miden-utils-core-derive = { git = "https://github.com/0xMiden/miden-vm.git", rev = "1b60fa4b54e60075cad7a761b593d5c00c6aaf46" } diff --git a/Makefile.toml b/Makefile.toml index 62c0e90..d21fd6b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -294,6 +294,7 @@ command = "cargo" args = [ "nextest", "run", + "--no-tests=pass", "@@remove-empty(CARGO_MAKE_CARGO_VERBOSE_FLAGS)", "@@split(CARGO_MAKE_CARGO_BUILD_TEST_FLAGS, )", "${@}", diff --git a/README.md b/README.md index 70f41d4..ce3a976 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,15 @@ in tests, etc. See the [documentation](https://github.com/0xMiden/compiler/tree/next/docs/external/src/guides/debugger.md) for more details on the `miden debug` command, and how to use the debugger. +## Transaction Debugging Stack + +The transaction debugging work on `pr/enable-tx-debugging` is a stacked integration: + +- This branch depends on the debugger VM port work from issue `#30` / `pr/migrate-to-vm-v0.21`. +- The executor-factory abstraction is intended to live in the protocol/transaction layer + (`miden-protocol` / `miden-tx`), not as a long-term VM or `miden-debug` API. +- The remaining PR order is protocol + `miden-tx` first, then `miden-client` once that hook lands. + ## License MIT diff --git a/crates/dap/Cargo.toml b/crates/dap/Cargo.toml new file mode 100644 index 0000000..eb430e8 --- /dev/null +++ b/crates/dap/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "miden-debug-dap" +version = "0.1.0" +edition = "2024" +rust-version = "1.90" +authors = ["Miden contributors"] +description = "In-repo Debug Adapter Protocol support for miden-debug" +license = "MIT OR Apache-2.0" +repository = "https://github.com/0xMiden/miden-debug" +homepage = "https://github.com/0xMiden/miden-debug" +keywords = ["debugging", "protocol", "debug", "framework"] +categories = ["development-tools::debugging"] + +[lib] +name = "dap" + +[features] +integration_testing = ["fake", "rand"] +client = [] + +[dependencies] +serde = { version = "1.*", features = ["derive"] } +serde_json = "1.*" +thiserror = "1.*" +fake = { version = "2.*", features = ["derive"], optional = true } +rand = { version = "0.*", optional = true } diff --git a/crates/dap/src/base_message.rs b/crates/dap/src/base_message.rs new file mode 100644 index 0000000..e3621c5 --- /dev/null +++ b/crates/dap/src/base_message.rs @@ -0,0 +1,46 @@ +#[cfg(feature = "client")] +use serde::Deserialize; +use serde::Serialize; + +use crate::{events::Event, responses::Response, reverse_requests::ReverseRequest}; + +/// Represents the base protocol message, in which all other messages are wrapped. +/// +/// Specification: [Response](https://microsoft.github.io/debug-adapter-protocol/specification) +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct BaseMessage { + /// Sequence number of the message. The `seq` for + /// the first message is 1, and for each message is incremented by 1. + pub seq: i64, + #[serde(flatten)] + pub message: Sendable, +} + +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(tag = "type")] +pub enum Sendable { + Response(Response), + Event(Event), + ReverseRequest(ReverseRequest), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_message_serialize() { + let message = BaseMessage { + seq: 10, + message: Sendable::Event(Event::Initialized), + }; + let json = serde_json::to_string(&message).unwrap(); + + let expected = "{\"seq\":10,\"type\":\"event\",\"event\":\"initialized\"}"; + assert_eq!(json, expected); + } +} diff --git a/crates/dap/src/errors.rs b/crates/dap/src/errors.rs new file mode 100644 index 0000000..3d06556 --- /dev/null +++ b/crates/dap/src/errors.rs @@ -0,0 +1,43 @@ +use std::fmt::Debug; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DeserializationError { + #[error("could not parse value '{value}' to enum variant of '{enum_name}'")] + StringToEnumParseError { enum_name: String, value: String }, + #[error("Error while deserializing")] + SerdeError(#[from] serde_json::Error), + #[error("Error decoding character stream")] + DecodingError(std::str::Utf8Error), +} + +#[derive(Debug, Error)] +pub enum ServerError { + #[error("I/O error")] + IoError(std::io::Error), + + #[error("Unknown header: {header}")] + UnknownHeader { header: String }, + + #[error("Parse error")] + ParseError(#[from] DeserializationError), + + #[error("Could not parse header line '{line}'")] + HeaderParseError { line: String }, + + #[error("Protocol error while reading line '{line}', reason: '{reason}'")] + ProtocolError { reason: String, line: String }, + + #[error("Serialization error")] + SerializationError(#[from] serde_json::Error), + + #[error( + "Trying to construct a non-sense response (such as an ACK for a request that requires a \ + response body" + )] + ResponseConstructError, + + #[error("Output lock is poisoned")] + OutputLockError, +} diff --git a/crates/dap/src/events.rs b/crates/dap/src/events.rs new file mode 100644 index 0000000..b155574 --- /dev/null +++ b/crates/dap/src/events.rs @@ -0,0 +1,434 @@ +#[cfg(feature = "client")] +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; + +use crate::types::{ + Breakpoint, BreakpointEventReason, Capabilities, InvalidatedAreas, LoadedSourceEventReason, + Module, ModuleEventReason, OutputEventCategory, OutputEventGroup, ProcessEventStartMethod, + Source, StoppedEventReason, ThreadEventReason, +}; + +/// Arguments for a Breakpoint event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BreakpointEventBody { + /// The reason for the event. + /// Values: 'changed', 'new', 'removed', etc. + pub reason: BreakpointEventReason, + /// The `id` attribute is used to find the target breakpoint, the other + /// attributes are used as the new values. + pub breakpoint: Breakpoint, +} + +/// Arguments for a Capabilities event +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CapabilitiesEventBody { + pub capabilities: Capabilities, +} + +/// Arguments for a Continued event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ContinuedEventBody { + /// The thread which was continued. + pub thread_id: i64, + /// If `allThreadsContinued` is true, a debug adapter can announce that all threads have + /// continued. + pub all_threads_continued: Option, +} + +/// Arguments for a Exited event +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExitedEventBody { + /// The exit code returned from the debuggee. + pub exit_code: i64, +} + +/// Arguments for a Invalidated event +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InvalidatedEventBody { + /// Set of logical areas that got invalidated. This property has a hint + /// characteristic: a client can only be expected to make a 'best effort' in + /// honouring the areas but there are no guarantees. If this property is + /// missing, empty, or if values are not understood, the client should assume + /// a single value `all`. + pub areas: Option>, + /// If specified, the client only needs to refetch data related to this + /// thread. + pub thread_id: Option, + /// If specified, the client only needs to refetch data related to this stack + /// frame (and the `threadId` is ignored). + pub stack_frame_id: Option, +} + +/// Arguments for a LoadedSource event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LoadedSourceEventBody { + /// The reason for the event. + /// Values: 'new', 'changed', 'removed' + pub reason: LoadedSourceEventReason, + /// The new, changed, or removed source. + pub source: Source, +} + +/// Arguments for a Memory event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MemoryEventBody { + /// Memory reference of a memory range that has been updated. + pub memory_reference: String, + /// Starting offset in bytes where memory has been updated. Can be negative. + pub offset: i64, + /// Number of bytes updated. + pub count: i64, +} + +/// Arguments for a Module event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ModuleEventBody { + /// The reason for the event. + /// Values: 'new', 'changed', 'removed' + pub reason: ModuleEventReason, + /// The new, changed, or removed module. In case of `removed` only the module + /// id is used. + pub module: Module, +} + +/// Arguments for an Output event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OutputEventBody { + /// The output category. If not specified or if the category is not + /// understood by the client, `console` is assumed. + /// Values: + /// 'console': Show the output in the client's default message UI, e.g. a + /// 'debug console'. This category should only be used for informational + /// output from the debugger (as opposed to the debuggee). + /// 'important': A hint for the client to show the output in the client's UI + /// for important and highly visible information, e.g. as a popup + /// notification. This category should only be used for important messages + /// from the debugger (as opposed to the debuggee). Since this category value + /// is a hint, clients might ignore the hint and assume the `console` + /// category. + /// 'stdout': Show the output as normal program output from the debuggee. + /// 'stderr': Show the output as error program output from the debuggee. + /// 'telemetry': Send the output to telemetry instead of showing it to the + /// user. + /// etc. + pub category: Option, + /// The output to report. + pub output: String, + /// Support for keeping an output log organized by grouping related messages. + /// Values: + /// 'start': Start a new group in expanded mode. Subsequent output events are + /// members of the group and should be shown indented. + /// The `output` attribute becomes the name of the group and is not indented. + /// 'startCollapsed': Start a new group in collapsed mode. Subsequent output + /// events are members of the group and should be shown indented (as soon as + /// the group is expanded). + /// The `output` attribute becomes the name of the group and is not indented. + /// 'end': End the current group and decrease the indentation of subsequent + /// output events. + /// A non-empty `output` attribute is shown as the unindented end of the + /// group. + pub group: Option, + /// If an attribute `variablesReference` exists and its value is > 0, the + /// output contains objects which can be retrieved by passing + /// `variablesReference` to the `variables` request. The value should be less + /// than or equal to 2147483647 (2^31-1). + pub variables_reference: Option, + /// The source location where the output was produced. + pub source: Option, + /// The source location's line where the output was produced. + pub line: Option, + /// The position in `line` where the output was produced. It is measured in + /// UTF-16 code units and the client capability `columnsStartAt1` determines + /// whether it is 0- or 1-based. + pub column: Option, + /// Additional data to report. For the `telemetry` category the data is sent + /// to telemetry, for the other categories the data is shown in JSON format. + pub data: Option, +} + +/// Arguments for an Process event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProcessEventBody { + /// The logical name of the process. This is usually the full path to + /// process's executable file. Example: /home/example/myproj/program.js. + pub name: String, + /// The system process id of the debugged process. This property is missing + /// for non-system processes. + pub system_process_id: Option, + /// If true, the process is running on the same computer as the debug + /// adapter. + pub is_local_process: Option, + /// Describes how the debug engine started debugging this process. + /// Values: + /// 'launch': Process was launched under the debugger. + /// 'attach': Debugger attached to an existing process. + /// 'attachForSuspendedLaunch': A project launcher component has launched a + /// new process in a suspended state and then asked the debugger to attach. + pub start_method: Option, + /// The size of a pointer or address for this process, in bits. This value + /// may be used by clients when formatting addresses for display. + pub pointer_size: Option, +} + +/// Arguments for a ProgressEnd event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProgressEndEventBody { + /// The ID that was introduced in the initial `ProgressStartEvent`. + pub progress_id: String, + /// More detailed progress message. If omitted, the previous message (if any) + /// is used. + pub message: Option, +} + +/// Arguments for a ProgressStart event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProgressStartEventBody { + /// An ID that can be used in subsequent `progressUpdate` and `progressEnd` + /// events to make them refer to the same progress reporting. + /// IDs must be unique within a debug session. + pub progress_id: String, + /// Short title of the progress reporting. Shown in the UI to describe the + /// long running operation. + pub title: String, + /// The request ID that this progress report is related to. If specified a + /// debug adapter is expected to emit progress events for the long running + /// request until the request has been either completed or cancelled. + /// If the request ID is omitted, the progress report is assumed to be + /// related to some general activity of the debug adapter. + pub request_id: Option, + /// If true, the request that reports progress may be cancelled with a + /// `cancel` request. + /// So this property basically controls whether the client should use UX that + /// supports cancellation. + /// Clients that don't support cancellation are allowed to ignore the + /// setting. + pub cancellable: Option, + /// More detailed progress message. + pub message: Option, + /// Progress percentage to display (value range: 0 to 100). If omitted no + /// percentage is shown. + pub percentage: Option, +} + +/// Arguments for a ProgressUpdate event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ProgressUpdateEventBody { + /// The ID that was introduced in the initial `progressStart` event. + pub progress_id: String, + /// More detailed progress message. If omitted, the previous message (if any) + /// is used. + pub message: Option, + /// Progress percentage to display (value range: 0 to 100). If omitted no + /// percentage is shown. + pub percentage: Option, +} + +/// Arguments for a Stopped event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StoppedEventBody { + /// The reason for the event. + /// For backward compatibility this String is shown in the UI if the + /// `description` attribute is missing (but it must not be translated). + /// Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', + /// 'function breakpoint', 'data breakpoint', 'instruction breakpoint', etc. + pub reason: StoppedEventReason, + /// The full reason for the event, e.g. 'Paused on exception'. This String is + /// shown in the UI as is and can be translated. + pub description: Option, + /// The thread which was stopped. + pub thread_id: Option, + /// A value of true hints to the client that this event should not change the + /// focus. + pub preserve_focus_hint: Option, + /// Additional information. E.g. if reason is `exception`, text contains the + /// exception name. This String is shown in the UI. + pub text: Option, + /// If `allThreadsStopped` is true, a debug adapter can announce that all + /// threads have stopped. + /// + /// - The client should use this information to enable that all threads can + /// be expanded to access their stacktraces. + /// - If the attribute is missing or false, only the thread with the given + /// `threadId` can be expanded. + pub all_threads_stopped: Option, + /// Ids of the breakpoints that triggered the event. In most cases there is + /// only a single breakpoint but here are some examples for multiple + /// breakpoints: + /// + /// - Different types of breakpoints map to the same location. + /// - Multiple source breakpoints get collapsed to the same instruction by + /// the compiler/runtime. + /// - Multiple function breakpoints with different function names map to the + /// same location. + pub hit_breakpoint_ids: Option>, +} + +/// Arguments for a Terminated event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TerminatedEventBody { + /// A debug adapter may set `restart` to true (or to an arbitrary object) to + /// request that the client restarts the session. + /// The value is not interpreted by the client and passed unmodified as an + /// attribute `__restart` to the `launch` and `attach` requests. + pub restart: Option, +} + +/// Arguments for a Thread event. +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ThreadEventBody { + /// The reason for the event. + /// Values: 'started', 'exited', etc. + pub reason: ThreadEventReason, + /// The identifier of the thread. + pub thread_id: i64, +} + +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "event", content = "body", rename_all = "camelCase")] +pub enum Event { + /// This event indicates that the debug adapter is ready to accept configuration requests (e.g. + /// `setBreakpoints`, `setExceptionBreakpoints`). + /// A debug adapter is expected to send this event when it is ready to accept configuration + /// requests (but not before the initialize request has finished). + /// + /// Specification: [Initialized event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized) + Initialized, + /// The event indicates that one or more capabilities have changed. + /// Since the capabilities are dependent on the client and its UI, it might not be possible to + /// change that at random times (or too late). + /// Consequently this event has a hint characteristic: a client can only be expected to make a + /// ‘best effort’ in honouring individual capabilities but there are no guarantees. + /// Only changed capabilities need to be included, all other capabilities keep their values. + /// + /// Specification: [Capabilities event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Capabilities) + Capabilities(CapabilitiesEventBody), + /// The event indicates that some information about a breakpoint has changed. + /// + /// Specification: [Breakpoint event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Breakpoint) + Breakpoint(BreakpointEventBody), + /// The event indicates that the execution of the debuggee has continued. + /// Please note: a debug adapter is not expected to send this event in response to a request that + /// implies that execution continues, e.g. launch or continue. + /// It is only necessary to send a continued event if there was no previous request that implied this. + /// + /// Specification: [Continued event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Continued) + Continued(ContinuedEventBody), + /// The event indicates that the debuggee has exited and returns its exit code. + /// + /// Specification: [Exited event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Exited) + Exited(ExitedEventBody), + /// This event signals that some state in the debug adapter has changed and requires that the + /// client needs to re-render the data snapshot previously requested. + /// Debug adapters do not have to emit this event for runtime changes like stopped or thread + /// events because in that case the client refetches the new state anyway. But the event can be + /// used for example to refresh the UI after rendering formatting has changed in the debug adapter. + /// + /// Specification: [Invalidated event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Invalidated) + Invalidated(InvalidatedEventBody), + /// The event indicates that some source has been added, changed, or removed from the set of all + /// loaded sources. + /// + /// Specification: [LoadedSource event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_LoadedSource) + LoadedSource(LoadedSourceEventBody), + /// This event indicates that some memory range has been updated. It should only be sent if the + /// corresponding capability supportsMemoryEvent is true. + /// Clients typically react to the event by re-issuing a readMemory request if they show the + /// memory identified by the memoryReference and if the updated memory range overlaps the + /// displayed range. Clients should not make assumptions how individual memory references relate + /// to each other, so they should not assume that they are part of a single continuous address + /// range and might overlap. + /// + /// Specification: [Memory event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Memory) + Memory(MemoryEventBody), + /// The event indicates that some information about a module has changed. + /// + /// Specification: [Module event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Module) + Module(ModuleEventBody), + /// The event indicates that the target has produced some output. + /// + /// Specification: [Output event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Output) + Output(OutputEventBody), + /// The event indicates that the debugger has begun debugging a new process. Either one that it + /// has launched, or one that it has attached to. + /// + /// Specification: [Process event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Process) + Process(ProcessEventBody), + /// The event signals the end of the progress reporting with a final message. + /// This event should only be sent if the corresponding capability supportsProgressReporting is + /// true. + /// + /// Specification: [ProgressEnd event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_ProgressEnd) + ProgressEnd(ProgressEndEventBody), + /// The event signals that a long running operation is about to start and provides additional + /// information for the client to set up a corresponding progress and cancellation UI. + /// The client is free to delay the showing of the UI in order to reduce flicker. + /// This event should only be sent if the corresponding capability `supportsProgressReporting` is + /// true. + /// + /// Specification: [ProgressStart event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_ProgressStart) + ProgressStart(ProgressStartEventBody), + /// The event signals that the progress reporting needs to be updated with a new message and/or + /// percentage. + /// The client does not have to update the UI immediately, but the clients needs to keep track of + /// the message and/or percentage values. + /// This event should only be sent if the corresponding capability supportsProgressReporting is + /// true. + /// + /// Specification: [ProgressUpdate event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_ProgressUpdate) + ProgressUpdate(ProgressUpdateEventBody), + /// The event indicates that the execution of the debuggee has stopped due to some condition. + /// This can be caused by a breakpoint previously set, a stepping request has completed, by + /// executing a debugger statement etc. + /// + /// Specification: [Stopped event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Stopped) + Stopped(StoppedEventBody), + /// The event indicates that debugging of the debuggee has terminated. This does not mean that + /// the debuggee itself has exited. + /// + /// Specification: [Terminated event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Terminated) + Terminated(Option), + /// The event indicates that a thread has started or exited. + /// + /// Specification: [Thread event](https://microsoft.github.io/debug-adapter-protocol/specification#Events_Thread) + Thread(ThreadEventBody), + /// Custom Miden event: server-pushed bundled UI state snapshot. + /// + /// Emitted immediately before a `Stopped` event so the DAP client can + /// update its UI from the snapshot without an extra evaluate round-trip. + #[serde(rename = "miden/uiState")] + MidenUiState(Value), +} diff --git a/crates/dap/src/lib.rs b/crates/dap/src/lib.rs new file mode 100644 index 0000000..7a6d54b --- /dev/null +++ b/crates/dap/src/lib.rs @@ -0,0 +1,110 @@ +//! # miden-debug-dap, a Rust implementation of the Debug Adapter Protocol +//! +//! ## Introduction +//! +//! This crate is a Rust implementation of the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) +//! (or DAP for short). +//! +//! The best way to think of DAP is to compare it to [LSP](https://microsoft.github.io/language-server-protocol/) +//! (Language Server Protocol) but for debuggers. The core idea is the same: a protocol that serves +//! as *lingua franca* for editors and debuggers to talk to each other. This means that an editor +//! that implements DAP can use a debugger that also implements DAP. +//! +//! In practice, the adapter might be separate from the actual debugger. For example, one could +//! implement an adapter that writes commands to the stdin of a gdb subprocess, then parses +//! the output it receives (this is why it's called an "adapter" - it adapts the debugger to +//! editors that know DAP). +//! +//! ## Minimal example +//! +//! To get started, create a binary project and add `miden-debug-dap` to your Cargo.toml: +//! +//! ```toml +//! [package] +//! name = "dummy-server" +//! version = "*" +//! edition = "2024" +//! +//! [dependencies] +//! miden-debug-dap = "*" +//! ``` +//! +//! Our dummy server is going to read its input from a text file and write the output to stdout. +//! +//! ```rust +//! use std::fs::File; +//! use std::io::{BufReader, BufWriter}; +//! +//! use thiserror::Error; +//! +//! use dap::prelude::*; +//! +//! #[derive(Error, Debug)] +//! enum MyAdapterError { +//! #[error("Unhandled command")] +//! UnhandledCommandError, +//! +//! #[error("Missing command")] +//! MissingCommandError, +//! } +//! +//! type DynResult = std::result::Result>; +//! +//! fn main() -> DynResult<()> { +//! let output = BufWriter::new(std::io::stdout()); +//! let f = File::open("testinput.txt")?; +//! let input = BufReader::new(f); +//! let mut server = Server::new(input, output); +//! +//! let req = match server.poll_request()? { +//! Some(req) => req, +//! None => return Err(Box::new(MyAdapterError::MissingCommandError)), +//! }; +//! if let Command::Initialize(_) = req.command { +//! let rsp = req.success( +//! ResponseBody::Initialize(Some(types::Capabilities { +//! ..Default::default() +//! })), +//! ); +//! +//! // When you call respond, send_event etc. the message will be wrapped +//! // in a base message with a appropriate seq number, so you don't have to keep track of that yourself +//! server.respond(rsp)?; +//! +//! server.send_event(Event::Initialized)?; +//! } else { +//! return Err(Box::new(MyAdapterError::UnhandledCommandError)); +//! } +//! +//! // You can send events from other threads while the server is blocked +//! // polling for requests by grabbing a `ServerOutput` mutex: +//! let server_output = server.output.clone(); +//! std::thread::spawn(move || { +//! std::thread::sleep(std::time::Duration::from_millis(500)); +//! +//! let mut server_output = server_output.lock().unwrap(); +//! server_output +//! .send_event(Event::Capabilities(events::CapabilitiesEventBody { +//! ..Default::default() +//! })) +//! .unwrap(); +//! }); +//! +//! // The thread will concurrently send an event while we are polling +//! // for the next request +//! let _ = server.poll_request()?; +//! +//! Ok(()) +//! } +//! ``` +pub mod base_message; +pub mod errors; +pub mod events; +pub mod prelude; +pub mod requests; +pub mod responses; +pub mod reverse_requests; +pub mod server; +pub mod types; +pub mod utils; +pub use utils::get_spec_version; diff --git a/crates/dap/src/prelude.rs b/crates/dap/src/prelude.rs new file mode 100644 index 0000000..ccf2f55 --- /dev/null +++ b/crates/dap/src/prelude.rs @@ -0,0 +1,9 @@ +#[doc(hidden)] +pub use crate::{ + events::{self, Event}, + requests::{self, Command, Request}, + responses::{self, Response, ResponseBody}, + reverse_requests::{ReverseCommand, ReverseRequest}, + server::Server, + types, +}; diff --git a/crates/dap/src/requests.rs b/crates/dap/src/requests.rs new file mode 100644 index 0000000..3b574e8 --- /dev/null +++ b/crates/dap/src/requests.rs @@ -0,0 +1,1182 @@ +use serde::Deserialize; +#[cfg(feature = "client")] +use serde::Serialize; +use serde_json::Value; + +use crate::{ + errors::ServerError, + prelude::{Response, ResponseBody}, + responses::ResponseMessage, + types::{ + DataBreakpoint, EvaluateArgumentsContext, ExceptionFilterOptions, ExceptionOptions, + FunctionBreakpoint, InstructionBreakpoint, Source, SourceBreakpoint, StackFrameFormat, + SteppingGranularity, ValueFormat, VariablesArgumentsFilter, + }, +}; + +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum PathFormat { + Path, + Uri, + #[serde(untagged)] + Other(String), +} + +/// Arguments for an Initialize request. +/// In specification: [Initialize](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize) +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InitializeArguments { + /// The ID of the client using this adapter. + #[serde(rename = "clientID")] + pub client_id: Option, + /// The human-readable name of the client using this adapter. + pub client_name: Option, + /// The ID of the debug adapter. + #[serde(rename = "adapterID")] + pub adapter_id: String, + /// The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. + pub locale: Option, + /// If true all line i64s are 1-based (default). + pub lines_start_at1: Option, + /// If true all column i64s are 1-based (default). + pub columns_start_at1: Option, + /// Determines in what format paths are specified. The default is `path`, which + /// is the native format. + pub path_format: Option, + /// Client supports the `type` attribute for variables. + pub supports_variable_type: Option, + /// Client supports the paging of variables. + pub supports_variable_paging: Option, + /// Client supports the `runInTerminal` request. + pub supports_run_in_terminal_request: Option, + /// Client supports memory references. + pub supports_memory_references: Option, + /// Client supports progress reporting. + pub supports_progress_reporting: Option, + /// Client supports the `invalidated` event. + pub supports_invalidated_event: Option, + /// Client supports the `memory` event. + pub supports_memory_event: Option, + /// Client supports the `argsCanBeInterpretedByShell` attribute on the `runInTerminal` request. + pub supports_args_can_be_interpreted_by_shell: Option, + /// Client supports the `startDebugging` request. + pub supports_start_debugging_request: Option, +} + +/// Arguments for an SetBreakpoints request. +/// In specification: [SetBreakpoints](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize) +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetBreakpointsArguments { + /// The source location of the breakpoints, either `source.path` or + /// `source.sourceReference` must be specified. + pub source: Source, + /// The code locations of the breakpoints. + pub breakpoints: Option>, + /// Deprecated: The code locations of the breakpoints. + #[deprecated] + pub lines: Option>, + /// A value of true indicates that the underlying source has been modified + /// which results in new breakpoint locations. + pub source_modified: Option, +} + +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CancelArguments { + /// The ID (attribute `seq`) of the request to cancel. If missing no request is + /// cancelled. + /// Both a `requestId` and a `progressId` can be specified in one request. + pub request_id: Option, + /// The ID (attribute `progressId`) of the progress to cancel. If missing no + /// progress is cancelled. + /// Both a `requestId` and a `progressId` can be specified in one request. + pub progress_id: Option, +} + +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetExceptionBreakpointsArguments { + /// Set of exception filters specified by their ID. The set of all possible + /// exception filters is defined by the `exceptionBreakpointFilters` + /// capability. The `filter` and `filterOptions` sets are additive. + pub filters: Vec, + /// Set of exception filters and their options. The set of all possible + /// exception filters is defined by the `exceptionBreakpointFilters` + /// capability. This attribute is only honored by a debug adapter if the + /// corresponding capability `supportsExceptionFilterOptions` is true. The + /// `filter` and `filterOptions` sets are additive. + pub filter_options: Option>, + /// Configuration options for selected exceptions. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsExceptionOptions` is true. + pub exception_options: Option>, +} + +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetFunctionBreakpointsArguments { + /// The function names of the breakpoints. + pub breakpoints: Vec, +} + +/// Arguments for a Launch request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct LaunchRequestArguments { + /// If true, the launch request should launch the program without enabling + /// debugging. + pub no_debug: Option, + /// Arbitrary data from the previous, restarted session. + /// The data is sent as the `restart` attribute of the `terminated` event. + /// The client should leave the data intact. + /// + /// Rust-specific: this data must be a string. Server requiring storing binary data should use + /// an encoding that is suitable for string (e.g. base85 or similar). + #[serde(rename = "__restart")] + pub restart_data: Option, + /// The request may include additional implementation specific attributes. + #[serde(flatten)] + pub additional_data: Option, +} + +/// Arguments for an Attach request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AttachRequestArguments { + /// Arbitrary data from the previous, restarted session. + /// The data is sent as the `restart` attribute of the `terminated` event. + /// The client should leave the data intact. + #[serde(rename = "__restart")] + pub restart_data: Option, + + /// The request may include additional implementation specific attributes. + #[serde(flatten)] + pub additional_data: Option, +} + +/// Union of Attach and Launch arguments for the Restart request. +/// Currently the same as LaunchRequestArguments but might not be in the future. +#[derive(Deserialize, Debug, Default, Clone)] +#[cfg_attr(feature = "client", derive(Serialize))] +#[serde(rename_all = "camelCase")] +pub struct AttachOrLaunchArguments { + /// If true, the launch request should launch the program without enabling + /// debugging. + pub no_debug: Option, + + /// Arbitrary data from the previous, restarted session. + /// The data is sent as the `restart` attribute of the `terminated` event. + /// The client should leave the data intact. + #[serde(rename = "__restart")] + pub restart_data: Option, + + /// The request may include additional implementation specific attributes. + #[serde(flatten)] + pub additional_data: Option, +} + +/// Arguments for a BreakpointLocations request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BreakpointLocationsArguments { + /// The source location of the breakpoints, either `source.path` or + /// `source.reference` must be specified. + pub source: Source, + /// Start line of range to search possible breakpoint locations in. If only the + /// line is specified, the request returns all possible locations in that line. + pub line: i64, + /// Start position within `line` to search possible breakpoint locations in. It + /// is measured in UTF-16 code units and the client capability + /// `columnsStartAt1` determines whether it is 0- or 1-based. If no column is + /// given, the first position in the start line is assumed. + pub column: Option, + /// End line of range to search possible breakpoint locations in. If no end + /// line is given, then the end line is assumed to be the start line. + pub end_line: Option, + /// End position within `endLine` to search possible breakpoint locations in. + /// It is measured in UTF-16 code units and the client capability + /// `columnsStartAt1` determines whether it is 0- or 1-based. If no end column + /// is given, the last position in the end line is assumed. + pub end_column: Option, +} + +/// Arguments for a Completions request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CompletionsArguments { + /// Returns completions in the scope of this stack frame. If not specified, the + /// completions are returned for the global scope. + pub frame_id: Option, + /// One or more source lines. Typically this is the text users have typed into + /// the debug console before they asked for completion. + pub text: String, + /// The position within `text` for which to determine the completion proposals. + /// It is measured in UTF-16 code units and the client capability + /// `columnsStartAt1` determines whether it is 0- or 1-based. + pub column: i64, + /// A line for which to determine the completion proposals. If missing the + /// first line of the text is assumed. + pub line: Option, +} + +/// Arguments for a Continue request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ContinueArguments { + /// Specifies the active thread. If the debug adapter supports single thread + /// execution (see `supportsSingleThreadExecutionRequests`) and the argument + /// `singleThread` is true, only the thread with this ID is resumed. + pub thread_id: i64, + /// If this flag is true, execution is resumed only for the thread with given + /// `threadId`. + pub single_thread: Option, +} + +/// Arguments for a DataBreakpointInfo request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DataBreakpointInfoArguments { + /// Reference to the variable container if the data breakpoint is requested for + /// a child of the container. The `variablesReference` must have been obtained + /// in the current suspended state. + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: Option, + /// The name of the variable's child to obtain data breakpoint information for. + /// If `variablesReference` isn't specified, this can be an expression. + pub name: String, + /// When `name` is an expression, evaluate it in the scope of this stack frame. + /// If not specified, the expression is evaluated in the global scope. When + /// `variablesReference` is specified, this property has no effect. + pub frame_id: Option, +} + +/// Arguments for a Disassemble request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DisassembleArguments { + /// Memory reference to the base location containing the instructions to + /// disassemble. + pub memory_reference: String, + /// Offset (in bytes) to be applied to the reference location before + /// disassembling. Can be negative. + pub offset: Option, + /// Offset (in instructions) to be applied after the byte offset (if any) + /// before disassembling. Can be negative. + pub instruction_offset: Option, + /// Number of instructions to disassemble starting at the specified location + /// and offset. + /// An adapter must return exactly this i64 of instructions - any + /// unavailable instructions should be replaced with an implementation-defined + /// 'invalid instruction' value. + pub instruction_count: i64, + /// If true, the adapter should attempt to resolve memory addresses and other + /// values to symbolic names. + pub resolve_symbols: Option, +} + +/// Arguments for a Disconnect request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DisconnectArguments { + /// A value of true indicates that this `disconnect` request is part of a + /// restart sequence. + pub restart: Option, + /// Indicates whether the debuggee should be terminated when the debugger is + /// disconnected. + /// If unspecified, the debug adapter is free to do whatever it thinks is best. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportTerminateDebuggee` is true. + pub terminate_debuggee: Option, + /// Indicates whether the debuggee should stay suspended when the debugger is + /// disconnected. + /// If unspecified, the debuggee should resume execution. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportSuspendDebuggee` is true. + pub suspend_debuggee: Option, +} + +/// Arguments for a Evaluate request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct EvaluateArguments { + /// The expression to evaluate. + pub expression: String, + /// Evaluate the expression in the scope of this stack frame. If not specified, + /// the expression is evaluated in the global scope. + pub frame_id: Option, + /// The context in which the evaluate request is used. + /// Values: + /// 'variables': evaluate is called from a variables view context. + /// 'watch': evaluate is called from a watch view context. + /// 'repl': evaluate is called from a REPL context. + /// 'hover': evaluate is called to generate the debug hover contents. + /// This value should only be used if the corresponding capability + /// `supportsEvaluateForHovers` is true. + /// 'clipboard': evaluate is called to generate clipboard contents. + /// This value should only be used if the corresponding capability + /// `supportsClipboardContext` is true. + /// etc. + pub context: Option, + /// Specifies details on how to format the result. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsValueFormattingOptions` is true. + pub format: Option, +} + +/// Arguments for a ExceptionInfo request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ExceptionInfoArguments { + /// Thread for which exception information should be retrieved. + pub thread_id: i64, +} + +/// Arguments for a Goto request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GotoArguments { + /// Set the goto target for this thread. + pub thread_id: i64, + /// The location where the debuggee will continue to run. + pub target_id: i64, +} + +/// Arguments for a GotoTargets request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GotoTargetsArguments { + /// The source location for which the goto targets are determined. + pub source: Source, + /// The line location for which the goto targets are determined. + pub line: i64, + /// The position within `line` for which the goto targets are determined. It is + /// measured in UTF-16 code units and the client capability `columnsStartAt1` + /// determines whether it is 0- or 1-based. + pub column: Option, +} + +/// Arguments for a Modules request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ModulesArguments { + /// The index of the first module to return, if omitted modules start at 0. + pub start_module: Option, + /// The i64 of modules to return. If `moduleCount` is not specified or 0, + /// all modules are returned. + pub module_count: Option, +} + +/// Arguments for a Next request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct NextArguments { + /// Specifies the thread for which to resume execution for one step (of the + /// given granularity). + pub thread_id: i64, + /// If this flag is true, all other suspended threads are not resumed. + pub single_thread: Option, + /// Stepping granularity. If no granularity is specified, a granularity of + /// `statement` is assumed. + pub granularity: Option, +} + +/// Arguments for a Pause request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PauseArguments { + /// Pause execution for this thread. + pub thread_id: i64, +} + +/// Arguments for a ReadMemory request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ReadMemoryArguments { + /// Memory reference to the base location from which data should be read. + pub memory_reference: String, + /// Offset (in bytes) to be applied to the reference location before reading + /// data. Can be negative. + pub offset: Option, + /// Number of bytes to read at the specified location and offset. + pub count: i64, +} + +/// Arguments for a ReadMemory request. +#[derive(Deserialize, Debug, Clone)] +#[cfg_attr(feature = "client", derive(Serialize))] +#[serde(rename_all = "camelCase")] +pub struct RestartArguments { + pub arguments: Option, +} + +/// Arguments for a RestartFrame request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RestartFrameArguments { + /// Restart this stackframe. + pub frame_id: i64, +} + +/// Arguments for a ReverseContinue request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ReverseContinueArguments { + /// Specifies the active thread. If the debug adapter supports single thread + /// execution (see `supportsSingleThreadExecutionRequests`) and the + /// `singleThread` argument is true, only the thread with this ID is resumed. + pub thread_id: i64, + /// If this flag is true, backward execution is resumed only for the thread + /// with given `threadId`. + pub single_thread: Option, +} + +/// Arguments for a Scopes request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScopesArguments { + /// Retrieve the scopes for this stackframe. + pub frame_id: i64, +} + +/// Arguments for a SetDataBreakpoints request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetDataBreakpointsArguments { + /// The contents of this array replaces all existing data breakpoints. An empty + /// array clears all data breakpoints. + pub breakpoints: Vec, +} + +/// Arguments for a SetExpression request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetExpressionArguments { + /// The l-value expression to assign to. + pub expression: String, + /// The value expression to assign to the l-value expression. + pub value: String, + /// Evaluate the expressions in the scope of this stack frame. If not + /// specified, the expressions are evaluated in the global scope. + pub frame_id: Option, + /// Specifies how the resulting value should be formatted. + pub format: Option, +} + +/// Arguments for a SetExpression request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetInstructionBreakpointsArguments { + /// The instruction references of the breakpoints + pub breakpoints: Vec, +} + +/// Arguments for a SetVariable request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetVariableArguments { + /// The reference of the variable container. + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: i64, + /// The name of the variable in the container. + pub name: String, + /// The value of the variable. + pub value: String, + /// Specifies details on how to format the response value. + pub format: Option, +} + +/// Arguments for a Source request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SourceArguments { + /// Specifies the source content to load. Either `source.path` or + /// `source.sourceReference` must be specified. + pub source: Option, + /// The reference to the source. This is the same as `source.sourceReference`. + /// This is provided for backward compatibility since old clients do not + /// understand the `source` attribute. + pub source_reference: i64, +} + +/// Arguments for a StackTrace request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StackTraceArguments { + /// Retrieve the stacktrace for this thread. + pub thread_id: i64, + /// The index of the first frame to return, if omitted frames start at 0. + pub start_frame: Option, + /// The maximum i64 of frames to return. If levels is not specified or 0, + /// all frames are returned. + pub levels: Option, + /// Specifies details on how to format the stack frames. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsValueFormattingOptions` is true. + pub format: Option, +} + +/// Arguments for a StepBack request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StepBackArguments { + /// Specifies the thread for which to resume execution for one step backwards + /// (of the given granularity). + pub thread_id: i64, + /// If this flag is true, all other suspended threads are not resumed. + pub single_thread: Option, + /// Stepping granularity to step. If no granularity is specified, a granularity + /// of `statement` is assumed. + pub granularity: Option, +} + +/// Arguments for a StepIn request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StepInArguments { + /// Specifies the thread for which to resume execution for one step-into (of + /// the given granularity). + pub thread_id: i64, + /// If this flag is true, all other suspended threads are not resumed. + pub single_thread: Option, + /// Id of the target to step into. + pub target_id: Option, + /// Stepping granularity. If no granularity is specified, a granularity of + /// `statement` is assumed. + pub granularity: Option, +} + +/// Arguments for a StepInTargets request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StepInTargetsArguments { + /// The stack frame for which to retrieve the possible step-in targets. + pub frame_id: i64, +} + +/// Arguments for a StepOut request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StepOutArguments { + /// Specifies the thread for which to resume execution for one step-out (of the + /// given granularity). + pub thread_id: i64, + /// If this flag is true, all other suspended threads are not resumed. + pub single_thread: Option, + /// Stepping granularity. If no granularity is specified, a granularity of + /// `statement` is assumed. + pub granularity: Option, +} + +/// Arguments for a Terminate request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TerminateArguments { + /// A value of true indicates that this `terminate` request is part of a + /// restart sequence. + pub restart: Option, +} + +/// Arguments for a TerminateThreads request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TerminateThreadsArguments { + /// Ids of threads to be terminated. + pub thread_ids: Option>, +} + +/// Arguments for a Variables request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VariablesArguments { + /// The variable for which to retrieve its children. The `variablesReference` + /// must have been obtained in the current suspended state. + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: i64, + /// Filter to limit the child variables to either named or indexed. If omitted, + /// both types are fetched. + /// Values: 'indexed', 'named' + pub filter: Option, + /// The index of the first variable to return, if omitted children start at 0. + pub start: Option, + /// The i64 of variables to return. If count is missing or 0, all variables + /// are returned. + pub count: Option, + /// Specifies details on how to format the Variable values. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsValueFormattingOptions` is true. + pub format: Option, +} + +/// Arguments for a WriteMemory request. +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WriteMemoryArguments { + /// Memory reference to the base location to which data should be written. + pub memory_reference: String, + /// Offset (in bytes) to be applied to the reference location before writing + /// data. Can be negative. + pub offset: Option, + /// Property to control partial writes. If true, the debug adapter should + /// attempt to write memory even if the entire memory region is not writable. + /// In such a case the debug adapter should stop after hitting the first byte + /// of memory that cannot be written and return the i64 of bytes written in + /// the response via the `offset` and `bytesWritten` properties. + /// If false or missing, a debug adapter should attempt to verify the region is + /// writable before writing, and fail the response if it is not. + pub allow_partial: Option, + /// Bytes to write, encoded using base64. + pub data: String, +} + +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Clone)] +#[serde(tag = "command", content = "arguments", rename_all = "camelCase")] +pub enum Command { + /// The attach request is sent from the client to the debug adapter to attach to a debuggee that + /// is already running. + /// kSince attaching is debugger/runtime specific, the arguments for this request are not part of + /// this specification. + /// + /// Specification: [Attach request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Attach) + Attach(AttachRequestArguments), + /// The `breakpointLocations` request returns all possible locations for source breakpoints in a + /// given range. + /// Clients should only call this request if the corresponding capability + /// `supportsBreakpointLocationsRequest` is true. + /// + /// Specification: [BreakpointLocations request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_BreakpointLocations) + BreakpointLocations(BreakpointLocationsArguments), + /// Returns a list of possible completions for a given caret position and text. + /// Clients should only call this request if the corresponding capability + /// `supportsCompletionsRequest` is true. + /// + /// Specification: [Completions request]: https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Completions + Completions(CompletionsArguments), + /// This request indicates that the client has finished initialization of the debug adapter. + /// So it is the last request in the sequence of configuration requests (which was started by the initialized event). + /// Clients should only call this request if the corresponding capability supportsConfigurationDoneRequest is true. + /// + /// Specification: [ConfigurationDone](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone) + ConfigurationDone, + /// The request resumes execution of all threads. If the debug adapter supports single thread + /// execution (see capability `supportsSingleThreadExecutionRequests`), setting the singleThread + /// argument to true resumes only the specified thread. If not all threads were resumed, the + /// `allThreadsContinued` attribute of the response should be set to false. + /// + /// Specification: [Continue request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue) + Continue(ContinueArguments), + /// Obtains information on a possible data breakpoint that could be set on an expression or + /// variable. + /// Clients should only call this request if the corresponding capability supportsDataBreakpoints + /// is true. + /// + /// Specification: [DataBreakpointInfo request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_DataBreakpointInfo) + DataBreakpointInfo(DataBreakpointInfoArguments), + /// Disassembles code stored at the provided location. + /// Clients should only call this request if the corresponding capability + /// `supportsDisassembleRequest` is true. + /// + /// Specification: [Disassemble request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disassemble) + Disassemble(DisassembleArguments), + /// The `disconnect` request asks the debug adapter to disconnect from the debuggee (thus ending + /// the debug session) and then to shut down itself (the debug adapter). + /// In addition, the debug adapter must terminate the debuggee if it was started with the `launch` + /// request. If an `attach` request was used to connect to the debuggee, then the debug adapter + /// must not terminate the debuggee. + /// This implicit behavior of when to terminate the debuggee can be overridden with the + /// `terminateDebuggee` argument (which is only supported by a debug adapter if the corresponding + /// capability `supportTerminateDebuggee` is true). + /// + /// Specification: [Disconnect request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disconnect) + Disconnect(DisconnectArguments), + /// Evaluates the given expression in the context of the topmost stack frame. + /// The expression has access to any variables and arguments that are in scope. + /// + /// Specification: [Evaluate arguments](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Evaluate) + Evaluate(EvaluateArguments), + /// Retrieves the details of the exception that caused this event to be raised. + /// Clients should only call this request if the corresponding capability + /// `supportsExceptionInfoRequest` is true. + /// + /// Specification: [ExceptionInfo request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ExceptionInfo) + ExceptionInfo(ExceptionInfoArguments), + /// The request sets the location where the debuggee will continue to run. + /// This makes it possible to skip the execution of code or to execute code again. + /// The code between the current location and the goto target is not executed but skipped. + /// The debug adapter first sends the response and then a stopped event with reason goto. + /// Clients should only call this request if the corresponding capability + /// `supportsGotoTargetsRequest` is true (because only then goto targets exist that can be passed + /// as arguments). + /// + /// Specification: [Goto request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Goto) + Goto(GotoArguments), + /// This request retrieves the possible goto targets for the specified source location. + /// These targets can be used in the goto request. + /// Clients should only call this request if the corresponding capability + /// `supportsGotoTargetsRequest` is true. + /// + /// Specification: [GotoTargets request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_GotoTargets) + GotoTargets(GotoTargetsArguments), + /// The initialize request is sent as the first request from the client to the debug adapter in + /// order to configure it with client capabilities and to retrieve capabilities from the debug + /// adapter. + /// + /// Until the debug adapter has responded with an initialize response, the client must not send any + /// additional requests or events to the debug adapter. + /// + /// In addition the debug adapter is not allowed to send any requests or events to the client until + /// it has responded with an initialize response. + /// + /// The initialize request may only be sent once. + /// + /// Specification: [InitializeRequest](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize) + Initialize(InitializeArguments), + /// This launch request is sent from the client to the debug adapter to start the debuggee with + /// or without debugging (if noDebug is true). + /// Since launching is debugger/runtime specific, the arguments for this request are not part of + /// this specification. + /// + /// Specification: [Launch request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch) + Launch(LaunchRequestArguments), + /// Retrieves the set of all sources currently loaded by the debugged process. + /// Clients should only call this request if the corresponding capability supportsLoadedSourcesRequest is true. + /// + /// Specification: [LoadedSources request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_LoadedSources) + LoadedSources, + /// Modules can be retrieved from the debug adapter with this request which can either return + /// all modules or a range of modules to support paging. + /// Clients should only call this request if the corresponding capability + /// `supportsModulesRequest` is true. + /// + /// Specification: [Modules request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Modules) + Modules(ModulesArguments), + /// The request executes one step (in the given granularity) for the specified thread and allows + /// all other threads to run freely by resuming them. + /// If the debug adapter supports single thread execution (see capability + /// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true + /// prevents other suspended threads from resuming. + /// The debug adapter first sends the response and then a stopped event (with reason step) after + /// the step has completed. + /// + /// Specification: [Next request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next) + Next(NextArguments), + /// The request suspends the debuggee. + /// The debug adapter first sends the response and then a `stopped` event (with reason pause) + /// after the thread has been paused successfully. + /// + /// Specification: [Pause request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause) + Pause(PauseArguments), + /// Reads bytes from memory at the provided location. + /// Clients should only call this request if the corresponding capability + /// `supportsReadMemoryRequest` is true. + /// + /// Specification: [ReadMemory request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ReadMemory) + ReadMemory(ReadMemoryArguments), + /// Restarts a debug session. Clients should only call this request if the corresponding + /// capability supportsRestartRequest is true. + /// If the capability is missing or has the value false, a typical client emulates restart by + /// terminating the debug adapter first and then launching it anew. + /// + /// Specification: [Restart request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Restart) + Restart(RestartArguments), + /// The request restarts execution of the specified stackframe. + /// The debug adapter first sends the response and then a `stopped` event (with reason `restart`) + /// after the restart has completed. + /// Clients should only call this request if the corresponding capability `supportsRestartFrame` + /// is true. + /// + /// Specification: [RestartFrame request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_RestartFrame) + RestartFrame(RestartFrameArguments), + /// The request resumes backward execution of all threads. If the debug adapter supports single + /// thread execution (see capability `supportsSingleThreadExecutionRequests`), setting the + /// singleThread argument to true resumes only the specified thread. If not all threads were + /// resumed, the `allThreadsContinued` attribute of the response should be set to false. + /// Clients should only call this request if the corresponding capability supportsStepBack is true. + /// + /// Specification: [ReverseContinue request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ReverseContinue) + ReverseContinue(ReverseContinueArguments), + /// The request returns the variable scopes for a given stackframe ID. + /// + /// Specification: [Scopes request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes) + Scopes(ScopesArguments), + /// Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. + /// + /// To clear all breakpoint for a source, specify an empty array. + /// + /// When a breakpoint is hit, a stopped event (with reason breakpoint) is generated. + /// + /// Specification: [SetBreakpointsRequest](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints) + SetBreakpoints(SetBreakpointsArguments), + /// Replaces all existing data breakpoints with new data breakpoints. + /// To clear all data breakpoints, specify an empty array. + /// When a data breakpoint is hit, a `stopped` event (with reason `data breakpoint`) is generated. + /// Clients should only call this request if the corresponding capability + /// `supportsDataBreakpoints` is true. + /// + /// Specification: [SetDataBreakpoints request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetDataBreakpoints) + SetDataBreakpoints(SetDataBreakpointsArguments), + /// The request configures the debugger’s response to thrown exceptions. + /// If an exception is configured to break, a stopped event is fired (with reason exception). + /// Clients should only call this request if the corresponding capability exceptionBreakpointFilters returns one or more filters. + /// + /// Specification: [SetExceptionBreakpoints](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints) + SetExceptionBreakpoints(SetExceptionBreakpointsArguments), + /// Evaluates the given `value` expression and assigns it to the `expression` which must be a + /// modifiable l-value. + /// The expressions have access to any variables and arguments that are in scope of the specified + /// frame. + /// Clients should only call this request if the corresponding capability `supportsSetExpression` + /// is true. + /// If a debug adapter implements both `setExpression` and `setVariable`, a client uses + /// `setExpression` if the variable has an `evaluateName` property. + /// + /// Specification: [SetExpression request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExpression) + SetExpression(SetExpressionArguments), + /// Replaces all existing function breakpoints with new function breakpoints. + /// To clear all function breakpoints, specify an empty array. + /// When a function breakpoint is hit, a stopped event (with reason function breakpoint) is + /// generated. + /// Clients should only call this request if the corresponding capability + /// supportsFunctionBreakpoints is true. + /// + /// Specification: [SetFunctionBreakpointsArguments](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetFunctionBreakpoints) + SetFunctionBreakpoints(SetFunctionBreakpointsArguments), + /// Replaces all existing instruction breakpoints. Typically, instruction breakpoints would be set from a disassembly window. + /// To clear all instruction breakpoints, specify an empty array. + /// When an instruction breakpoint is hit, a `stopped` event (with reason + /// `instruction breakpoint`) is generated. + /// Clients should only call this request if the corresponding capability + /// `supportsInstructionBreakpoints` is true. + /// + /// Specification: [SetInstructionBreakpoints request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetInstructionBreakpoints) + SetInstructionBreakpoints(SetInstructionBreakpointsArguments), + /// Set the variable with the given name in the variable container to a new value. Clients should + /// only call this request if the corresponding capability `supportsSetVariable` is true. + /// If a debug adapter implements both `setVariable` and `setExpression`, a client will only use + /// `setExpression` if the variable has an `evaluateName` property. + /// + /// Specification: [SetVariable request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetVariable) + SetVariable(SetVariableArguments), + /// The request retrieves the source code for a given source reference. + /// + /// Specification: [Sources request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source) + Source(SourceArguments), + /// The request returns a stacktrace from the current execution state of a given thread. + /// A client can request all stack frames by omitting the startFrame and levels arguments. For + /// performance-conscious clients and if the corresponding capability + /// `supportsDelayedStackTraceLoading` is true, stack frames can be retrieved in a piecemeal way + /// with the startFrame and levels arguments. The response of the stackTrace request may + /// contain a totalFrames property that hints at the total number of frames in the stack. If a + /// client needs this total number upfront, it can issue a request for a single (first) frame + /// and depending on the value of totalFrames decide how to proceed. In any case a client should + /// be prepared to receive fewer frames than requested, which is an indication that the end of + /// the stack has been reached. + /// + /// Specification: [StackTrace request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace) + StackTrace(StackTraceArguments), + /// The request executes one backward step (in the given granularity) for the specified thread + /// and allows all other threads to run backward freely by resuming them. + /// If the debug adapter supports single thread execution (see capability + /// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true prevents + /// other suspended threads from resuming. + /// The debug adapter first sends the response and then a stopped event (with reason step) after + /// the step has completed. + /// Clients should only call this request if the corresponding capability `supportsStepBack` is + /// true. + /// + /// Specification: [StepBack request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepBack) + StepBack(StepBackArguments), + /// The request resumes the given thread to step into a function/method and allows all other + /// threads to run freely by resuming them. + /// If the debug adapter supports single thread execution (see capability + /// `supportsSingleThreadExecutionRequests`), setting the `singleThread` argument to true + /// prevents other suspended threads from resuming. + /// + /// Specification: [StepIn request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn) + StepIn(StepInArguments), + /// This request retrieves the possible step-in targets for the specified stack frame. + /// These targets can be used in the `stepIn` request. + /// Clients should only call this request if the corresponding capability + /// `supportsStepInTargetsRequest` is true. + /// + /// Specification: [StepInTargets request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepInTargets) + StepInTargets(StepInTargetsArguments), + /// The request resumes the given thread to step out (return) from a function/method and allows + /// all other threads to run freely by resuming them. + /// If the debug adapter supports single thread execution (see capability + /// `supportsSingleThreadExecutionRequests`), setting the singleThread argument to true prevents + /// other suspended threads from resuming. + /// The debug adapter first sends the response and then a stopped event (with reason step) after + /// the step has completed. + /// + /// Specification: [StepOut request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut) + StepOut(StepOutArguments), + /// The terminate request is sent from the client to the debug adapter in order to shut down the + /// debuggee gracefully. Clients should only call this request if the capability + /// `supportsTerminateRequest` is true. + /// + /// Specification: [Terminate request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Terminate) + Terminate(TerminateArguments), + /// The request terminates the threads with the given ids. + /// Clients should only call this request if the corresponding capability + /// `supportsTerminateThreadsRequest` is true. + /// + /// Specification: [TerminateThreads request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_TerminateThreads) + TerminateThreads(TerminateThreadsArguments), + /// The request retrieves a list of all threads. + /// + /// Specification: [Threads request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads) + Threads, + /// Retrieves all child variables for the given variable reference. + /// A filter can be used to limit the fetched children to either named or indexed children. + /// + /// Specification: [Variables request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables) + Variables(VariablesArguments), + /// Writes bytes to memory at the provided location. + /// Clients should only call this request if the corresponding capability + /// `supportsWriteMemoryRequest` is true. + /// + /// Specification: [WriteMemory request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_WriteMemory) + WriteMemory(WriteMemoryArguments), + /// The cancel request is used by the client in two situations: + /// + /// to indicate that it is no longer interested in the result produced by a specific request + /// issued earlier to cancel a progress sequence. Clients should only call this request if the + /// corresponding capability supportsCancelRequest is true. + /// + /// Specification: [CancelRequest](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_Cancel) + Cancel(CancelArguments), +} + +/// Represents a request from a client. +/// +/// Note that unlike the specification, this implementation does not define a ProtocolMessage base +/// interface. Instead, the only common part (the sequence number) is repeated in the struct. +/// +/// Specification: [Request](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_Request) +#[cfg_attr(feature = "client", derive(Serialize))] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Request { + /// Sequence number for the Request. + /// + /// From the [specification](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage): + /// + /// Sequence number of the message (also known as message ID). The `seq` for + /// the first message sent by a client or debug adapter is 1, and for each + /// subsequent message is 1 greater than the previous message sent by that + /// actor. `seq` can be used to order requests, responses, and events, and to + /// associate requests with their corresponding responses. For protocol + /// messages of type `request` the sequence number can be used to cancel the + /// request. + pub seq: i64, + /// The command to execute. + /// + /// This is stringly typed in the specification, but represented as an enum for better + /// ergonomics in Rust code, along with the arguments when present. + #[serde(flatten)] + pub command: Command, +} + +impl Request { + /// Create a successful response for a given request. The sequence number will be copied + /// from `request`, `message` will be `None` (as its neither cancelled nor an error). + /// The `body` argument contains the response itself. + pub fn success(self, body: ResponseBody) -> Response { + Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(body), // to love + error: None, + } + } + + /// Create an error response for a given request. The sequence number will be copied + /// from the request, `message` will be `None` (as its neither cancelled nor an error). + /// + /// ## Arguments + /// + /// * `req`: The request this response corresponds to. + /// * `body`: The body of the response to attach. + pub fn error(self, error: &str) -> Response { + Response { + request_seq: self.seq, + success: false, + message: Some(ResponseMessage::Error(error.to_string())), + body: None, + error: None, + } + } + + /// Create a cancellation response for the given request. The sequence number will be copied + /// from the request, message will be [`ResponseMessage::Cancelled`], `success` will be false, + /// and `body` will be `None`. + pub fn cancellation(self) -> Response { + Response { + request_seq: self.seq, + success: false, + message: Some(ResponseMessage::Cancelled), + body: None, + error: None, + } + } + + /// Create an acknowledgement response. This is a shorthand for responding to requests + /// where the response does not require a body. + pub fn ack(self) -> Result { + match self.command { + Command::Attach(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Attach), + error: None, + }), + Command::ConfigurationDone => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::ConfigurationDone), + error: None, + }), + Command::Disconnect(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Disconnect), + error: None, + }), + Command::Goto(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Goto), + error: None, + }), + Command::Launch(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Launch), + error: None, + }), + Command::Next(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Next), + error: None, + }), + Command::Pause(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Pause), + error: None, + }), + Command::Restart(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Next), + error: None, + }), + Command::RestartFrame(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::RestartFrame), + error: None, + }), + Command::ReverseContinue(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::ReverseContinue), + error: None, + }), + Command::StepBack(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::StepBack), + error: None, + }), + Command::StepIn(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::StepIn), + error: None, + }), + Command::StepOut(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::StepOut), + error: None, + }), + Command::Terminate(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::Terminate), + error: None, + }), + Command::TerminateThreads(_) => Ok(Response { + request_seq: self.seq, + success: true, + message: None, + body: Some(ResponseBody::TerminateThreads), + error: None, + }), + _ => Err(ServerError::ResponseConstructError), + } + } +} diff --git a/crates/dap/src/responses.rs b/crates/dap/src/responses.rs new file mode 100644 index 0000000..29895a9 --- /dev/null +++ b/crates/dap/src/responses.rs @@ -0,0 +1,710 @@ +#[cfg(feature = "integration_testing")] +use fake::Dummy; +#[cfg(feature = "client")] +use serde::Deserialize; +use serde::Serialize; + +use crate::types::{ + Breakpoint, BreakpointLocation, Capabilities, CompletionItem, DataBreakpointAccessType, + DisassembledInstruction, ExceptionBreakMode, ExceptionDetails, GotoTarget, Message, Module, + Scope, Source, StackFrame, Thread, Variable, VariablePresentationHint, +}; + +/// Represents a response message that is either a cancellation or a short error string. +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ResponseMessage { + /// Should be sent when the request was canceled + Cancelled, + /// The request may be retried once the adapter is in a 'stopped' state. + NotStopped, + /// Contains the raw error in short form if [`Response::success`](Response::success) is false. + /// This raw error might be interpreted by the client and is not shown in the UI. + #[serde(untagged)] + Error(String), +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct BreakpointLocationsResponse { + /// Sorted set of possible breakpoint locations. + pub breakpoints: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct CompletionsResponse { + /// The possible completions + pub targets: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ContinueResponse { + /// The value true (or a missing property) signals to the client that all + /// threads have been resumed. The value false indicates that not all threads + /// were resumed. + #[serde(skip_serializing_if = "Option::is_none")] + pub all_threads_continued: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct DataBreakpointInfoResponse { + /// An identifier for the data on which a data breakpoint can be registered + /// with the `setDataBreakpoints` request or null if no data breakpoint is + /// available. If a `variablesReference` or `frameId` is passed, the `dataId` + /// is valid in the current suspended state, otherwise it's valid + /// indefinitely. See 'Lifetime of Object References' in the Overview section + /// for details. Breakpoints set using the `dataId` in the + /// `setDataBreakpoints` request may outlive the lifetime of the associated + /// `dataId`. + pub data_id: Option, + /// UI String that describes on what data the breakpoint is set on or why a + /// data breakpoint is not available. + pub description: String, + /// Attribute lists the available access types for a potential data + /// breakpoint. A UI client could surface this information. + #[serde(skip_serializing_if = "Option::is_none")] + pub access_types: Option>, + /// Attribute indicates that a potential data breakpoint could be persisted + /// across sessions. + #[serde(skip_serializing_if = "Option::is_none")] + pub can_persist: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct DisassembleResponse { + /// The list of disassembled instructions. + pub instructions: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct EvaluateResponse { + /// The result of the evaluate request. + pub result: String, + /// The type of the evaluate result. + /// This attribute should only be returned by a debug adapter if the + /// corresponding capability `supportsVariableType` is true. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_field: Option, + /// Properties of an evaluate result that can be used to determine how to + /// render the result in the UI. + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + /// If `variablesReference` is > 0, the evaluate result is structured and its + /// children can be retrieved by passing `variablesReference` to the + /// `variables` request. + /// The value should be less than or equal to 2147483647 (2^31-1). + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: i64, + /// The i64 of named child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + /// The i64 of indexed child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + /// A memory reference to a location appropriate for this result. + /// For pointer type eval results, this is generally a reference to the + /// memory address contained in the pointer. + /// This attribute should be returned by a debug adapter if corresponding + /// capability `supportsMemoryReferences` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_reference: Option, +} + +#[derive(Serialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionInfoResponse { + /// ID of the exception that was thrown. + pub exception_id: String, + /// Descriptive text for the exception. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Mode that caused the exception notification to be raised. + pub break_mode: ExceptionBreakMode, + /// Detailed information about the exception. + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct GotoTargetsResponse { + /// The possible goto targets of the specified location. + pub targets: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct LoadedSourcesResponse { + /// Set of loaded sources. + pub sources: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ModulesResponse { + /// All modules or range of modules. + pub modules: Vec, + /// The total i64 of modules available. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_modules: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ReadMemoryResponse { + /// The address of the first byte of data returned. + /// Treated as a hex value if prefixed with `0x`, or as a decimal value + /// otherwise. + pub address: String, + /// The i64 of unreadable bytes encountered after the last successfully + /// read byte. + /// This can be used to determine the i64 of bytes that should be skipped + /// before a subsequent `readMemory` request succeeds. + #[serde(skip_serializing_if = "Option::is_none")] + pub unreadable_bytes: Option, + /// The bytes read from memory, encoded using base64. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ScopesResponse { + /// The scopes of the stackframe. If the array has length zero, there are no + /// scopes available. + pub scopes: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetBreakpointsResponse { + /// Information about the breakpoints. + /// The array elements are in the same order as the elements of the + /// `breakpoints` (or the deprecated `lines`) array in the arguments. + pub breakpoints: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetDataBreakpointsResponse { + /// Information about the breakpoints. + /// The array elements are in the same order as the elements of the `breakpoints` array + /// in the arguments. + pub breakpoints: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetExceptionBreakpointsResponse { + /// Information about the exception breakpoints or filters. + /// The breakpoints returned are in the same order as the elements of the + /// `filters`, `filterOptions`, `exceptionOptions` arrays in the arguments. + /// If both `filters` and `filterOptions` are given, the returned array must + /// start with `filters` information first, followed by `filterOptions` + /// information. + #[serde(skip_serializing_if = "Option::is_none")] + pub breakpoints: Option>, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetFunctionBreakpointsResponse { + /// Information about the breakpoints. The array elements correspond to the + /// elements of the `breakpoints` array. + pub breakpoints: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetInstructionBreakpointsResponse { + /// Information about the breakpoints. The array elements correspond to the + /// elements of the `breakpoints` array. + pub breakpoints: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetVariableResponse { + /// The new value of the variable. + pub value: String, + /// The type of the new value. Typically shown in the UI when hovering over + /// the value. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_field: Option, + /// If `variablesReference` is > 0, the new value is structured and its + /// children can be retrieved by passing `variablesReference` to the + /// `variables` request. + /// The value should be less than or equal to 2147483647 (2^31-1). + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + #[serde(skip_serializing_if = "Option::is_none")] + pub variables_reference: Option, + /// The number of named child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + /// The number of indexed child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SourceResponse { + /// Content of the source reference. + pub content: String, + /// Content type (MIME type) of the source. + #[serde(skip_serializing_if = "Option::is_none")] + pub mime_type: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SetExpressionResponse { + /// The new value of the expression. + pub value: String, + /// The type of the value. + /// This attribute should only be returned by a debug adapter if the + /// corresponding capability `supportsVariableType` is true. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_field: Option, + /// Properties of a value that can be used to determine how to render the + /// result in the UI. + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + /// If `variablesReference` is > 0, the value is structured and its children + /// can be retrieved by passing `variablesReference` to the `variables` + /// request. + /// The value should be less than or equal to 2147483647 (2^31-1). + /// See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + #[serde(skip_serializing_if = "Option::is_none")] + pub variables_reference: Option, + /// The number of named child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + /// The number of indexed child variables. + /// The client can use this information to present the variables in a paged + /// UI and fetch them in chunks. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct StackTraceResponse { + /// The frames of the stackframe. If the array has length zero, there are no + /// stackframes available. + /// This means that there is no location information available. + pub stack_frames: Vec, + /// The total i64 of frames available in the stack. If omitted or if + /// `totalFrames` is larger than the available frames, a client is expected + /// to request frames until a request returns less frames than requested + /// (which indicates the end of the stack). Returning monotonically + /// increasing `totalFrames` values for subsequent requests can be used to + /// enforce paging in the client. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_frames: Option, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ThreadsResponse { + /// All threads. + pub threads: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct VariablesResponse { + /// All (or a range) of variables for the given variable reference. + pub variables: Vec, +} + +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct WriteMemoryResponse { + /// Property that should be returned when `allowPartial` is true to indicate + /// the offset of the first byte of data successfully written. Can be + /// negative. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// Property that should be returned when `allowPartial` is true to indicate + /// the i64 of bytes starting from address that were successfully written. + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes_written: Option, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "command", content = "body", rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ResponseBody { + /// Response to attach request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Attach request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Attach) + Attach, + /// Response to breakpointLocations request. Contains possible locations for source breakpoints. + /// + /// Specification: [BreakpointLocations request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_BreakpointLocations) + BreakpointLocations(BreakpointLocationsResponse), + /// Response to a `completions` request + /// + /// Specification: [Completions request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Completions) + Completions(CompletionsResponse), + /// Response to `configurationDone` request. This is just an acknowledgement, so no body field is + /// required. + /// + /// Specification: [ConfigurationDone request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone) + ConfigurationDone, + /// Response to `continue` request. + /// + /// Specification: [Continue request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue) + Continue(ContinueResponse), + /// Response to `dataBreakpointInfo` request. + /// + /// Specification: [DataBreakpointInfo request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_DataBreakpointInfo) + DataBreakpointInfo(DataBreakpointInfoResponse), + /// Response to `disassemble` request. + /// + /// NOTE: we are straying away from the spec here, as the spec says that the response body is + /// optional, but we are always returning a body because I could not find a way to express + /// skipping the optional body with serde (and serializing null will make the schema validation + /// complain). + /// + /// Specification: [Disassembly request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disassemble) + Disassemble(DisassembleResponse), + /// Response to disconnect request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Disconnect request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disconnect) + Disconnect, + /// Response to `evaluate` request. + /// + /// Specification: [Evaluate request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Evaluate) + Evaluate(EvaluateResponse), + /// Response to `exceptionInfo` request. + /// + /// Specification: [ExceptionInfo](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ExceptionInfo) + ExceptionInfo(ExceptionInfoResponse), + /// Response to `goto` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Goto request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Goto) + Goto, + /// Response to `gotoTargets` request. + /// + /// Specification: [GotoTargets request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_GotoTargets) + GotoTargets(GotoTargetsResponse), + /// Response to `initialize` request. + /// + /// NOTE: we are straying away from the spec here, as the spec says that the response body is + /// optional, but we are always returning a body because I could not find a way to express + /// skipping the optional body with serde (and serializing null will make the schema validation + /// complain). + /// + /// Specification: [Initialize request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize) + Initialize(Capabilities), + /// Response to launch request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Launch request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch) + Launch, + /// Response to `loadedSources` request. + /// + /// Specification: [LoadedSources request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_LoadedSources) + LoadedSources(LoadedSourcesResponse), + /// Response to `modules` request. + /// + /// Specification: [Modules request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Modules) + Modules(ModulesResponse), + /// Response to next request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Next request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next) + Next, + /// Response to `pause` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Pause request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause) + Pause, + /// Response to readMemory request. + /// + /// NOTE: we are straying away from the spec here, as the spec says that the response body is + /// optional, but we are always returning a body because I could not find a way to express + /// skipping the optional body with serde (and serializing null will make the schema validation + /// complain). + /// + /// Specification: [ReadMemory request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ReadMemory) + ReadMemory(ReadMemoryResponse), + /// Response to `restart` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Restart request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Restart) + Restart, + /// Response to `restartFrame` request. This is just an acknowledgement, so no body field is + /// required. + /// + /// Specification: [RestartFrame request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_RestartFrame) + RestartFrame, + /// Response to `reverseContinue` request. This is just an acknowledgement, so no body field is + /// required. + /// + /// Specification: [ReverseContinue request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ReverseContinue) + ReverseContinue, + /// Response to scopes request. + /// + /// Specification: [Scopes request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes) + Scopes(ScopesResponse), + /// Response to setBreakpoints request. + /// Returned is information about each breakpoint created by this request. + /// This includes the actual code location and whether the breakpoint could be verified. + /// The breakpoints returned are in the same order as the elements of the breakpoints + /// (or the deprecated lines) array in the arguments. + /// + /// Specification: [SetBreakpointsRequest](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints) + SetBreakpoints(SetBreakpointsResponse), + /// Replaces all existing data breakpoints with new data breakpoints. + /// To clear all data breakpoints, specify an empty array. + /// When a data breakpoint is hit, a `stopped` event (with reason `date breakpoint`) is generated. + /// Clients should only call this request if the corresponding capability + /// `supportsDataBreakpoints` is true. + /// + /// Specification: [SetDataBreakpoints request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetDataBreakpoints) + SetDataBreakpoints(SetDataBreakpointsResponse), + /// Response to `setExceptionBreakpoint` request. + /// + /// The response contains an array of `Breakpoint` objects with information about each exception + /// breakpoint or filter. The Breakpoint objects are in the same order as the elements of the + /// `filters`, `filterOptions`, `exceptionOptions` arrays given as arguments. If both `filters` + /// and `filterOptions` are given, the returned array must start with filters information first, + /// followed by `filterOptions` information. + /// + /// The `verified` property of a `Breakpoint` object signals whether the exception breakpoint or + /// filter could be successfully created and whether the condition or hit count expressions are + /// valid. In case of an error the message property explains the problem. The id property can be + /// used to introduce a unique ID for the exception breakpoint or filter so that it can be + /// updated subsequently by sending breakpoint events. + /// + /// For backward compatibility both the `breakpoints` array and the enclosing body are optional. + /// If these elements are missing a client is not able to show problems for individual exception + /// breakpoints or filters. + /// + /// NOTE: we are straying away from the spec here, as the spec says that the response body is + /// optional, but we are always returning a body because I could not find a way to express + /// skipping the optional body with serde (and serializing null will make the schema validation + /// complain). + SetExceptionBreakpoints(SetExceptionBreakpointsResponse), + /// Response to setExpression request. + /// + /// Specification: [SetExpression request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExpression) + SetExpression(SetExpressionResponse), + /// Response to setFunctionBreakpoints request. + /// Returned is information about each breakpoint created by this request. + /// + /// Specification: [SetFunctionBreakpointsArguments](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetFunctionBreakpoints) + SetFunctionBreakpoints(SetFunctionBreakpointsResponse), + /// Response to `setInstructionBreakpoints` request + /// + /// Specification: [SetInstructionBreakpoints request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetInstructionBreakpoints) + SetInstructionBreakpoints(SetInstructionBreakpointsResponse), + /// Response to `setVariable` request. + /// + /// Specification: [SetVariable request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetVariable) + SetVariable(SetVariableResponse), + /// Response to `source` request. + /// + /// Specification: [Sources request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source) + Source(SourceResponse), + /// Response to stackTrace request. + /// + /// Specification: [StackTrace request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace) + StackTrace(StackTraceResponse), + /// Response to `stepBack` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [StepBack request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepBack) + StepBack, + /// Response to `stepIn` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [StepIn request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn) + StepIn, + /// Response to `stepOut` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [StepOut request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut) + StepOut, + /// Response to `terminate` request. This is just an acknowledgement, so no body field is required. + /// + /// Specification: [Terminate request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Terminate) + Terminate, + /// Response to `terminateThreads` request. This is just an acknowledgement, so no body field is + /// required. + /// + /// Specification: [TerminateThreads request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_TerminateThreads) + TerminateThreads, + /// Response to threads request. + /// + /// Specification: [Threads request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads) + Threads(ThreadsResponse), + /// Response to `variables` request. + /// + /// Specification: [Variables request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables) + Variables(VariablesResponse), + /// Response to `writeMemory` request. + /// + /// NOTE: we are straying away from the spec here, as the spec says that the response body is + /// optional, but we are always returning a body because I could not find a way to express + /// skipping the optional body with serde (and serializing null will make the schema validation + /// complain). + /// + /// Specification: [WriteMemory request](https://microsoft.github.io/debug-adapter-protocol/specification#Requests_WriteMemory) + WriteMemory(WriteMemoryResponse), +} + +/// Represents response to the client. +/// +/// The command field (which is a string) is used as a tag in the ResponseBody enum, so users +/// of this crate will control it by selecting the appropriate enum variant for the body. +/// +/// There is also no separate `ErrorResponse` struct. Instead, `Error` is just a variant of the +/// ResponseBody enum. +/// +/// Specification: [Response](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_Response) +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "client", derive(Deserialize))] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Response { + /// Sequence number of the corresponding request. + #[serde(rename = "request_seq")] + pub request_seq: i64, + /// Outcome of the request. + /// If true, the request was successful and the `body` attribute may contain + /// the result of the request. + /// If the value is false, the attribute `message` contains the error in short + /// form and the `body` may contain additional information (see + /// `ErrorResponse.body.error`). + pub success: bool, + /// Contains the raw error in short form if `success` is false. + /// This raw error might be interpreted by the client and is not shown in the + /// UI. + /// Some predefined values exist. + /// Values: + /// 'cancelled': request was cancelled. + /// etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Contains request result if success is true and error details if success is + /// false. + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub body: Option, + /// A structured error message. + pub error: Option, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_responsemessage_is_flattened() { + let a = Response { + request_seq: 1, + success: false, + message: Some(ResponseMessage::Error("test".to_string())), + body: None, + error: None, + }; + let val = serde_json::to_value(a).unwrap(); + + assert!(val.get("message").unwrap().is_string()); + assert!(val.get("message").unwrap().as_str().unwrap() == "test"); + assert!(!val.get("message").unwrap().is_object()); + + let a = Response { + request_seq: 1, + success: false, + message: Some(ResponseMessage::Cancelled), + body: None, + error: None, + }; + let val = serde_json::to_value(a).unwrap(); + assert!(val.get("message").unwrap().is_string()); + assert!(val.get("message").unwrap().as_str().unwrap() == "cancelled"); + + let a = Response { + request_seq: 1, + success: false, + message: Some(ResponseMessage::NotStopped), + body: None, + error: None, + }; + let val = serde_json::to_value(a).unwrap(); + assert!(val.get("message").unwrap().is_string()); + assert!(val.get("message").unwrap().as_str().unwrap() == "notStopped"); + } +} diff --git a/crates/dap/src/reverse_requests.rs b/crates/dap/src/reverse_requests.rs new file mode 100644 index 0000000..4515bd2 --- /dev/null +++ b/crates/dap/src/reverse_requests.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; + +#[cfg(feature = "client")] +use serde::Deserialize; +use serde::Serialize; + +use crate::types::{RunInTerminalRequestArgumentsKind, StartDebuggingRequestKind}; + +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RunInTerminalRequestArguments { + /// What kind of terminal to launch. + /// Values: 'integrated', 'external' + pub kind: Option, + /// Title of the terminal. + pub title: Option, + /// Working directory for the command. For non-empty, valid paths this + /// typically results in execution of a change directory command. + pub cwd: String, + /// List of arguments. The first argument is the command to run. + pub args: Vec, + /// Environment key-value pairs that are added to or removed from the default + /// environment. + pub env: Option>>, + /// This property should only be set if the corresponding capability + /// `supportsArgsCanBeInterpretedByShell` is true. If the client uses an + /// intermediary shell to launch the application, then the client must not + /// attempt to escape characters with special meanings for the shell. The user + /// is fully responsible for escaping as needed and that arguments using + /// special characters may not be portable across shells. + pub args_can_be_interpreted_by_shell: Option, +} + +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StartDebuggingRequestArguments { + /// Arguments passed to the new debug session. The arguments must only contain + /// properties understood by the `launch` or `attach` requests of the debug + /// adapter and they must not contain any client-specific properties (e.g. + /// `type`) or client-specific features (e.g. substitutable 'variables'). + pub configuration: HashMap, + /// Indicates whether the new debug session should be started with a `launch` + /// or `attach` request. + /// Values: 'launch', 'attach' + pub request: StartDebuggingRequestKind, +} + +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "command", content = "arguments", rename_all = "camelCase")] +pub enum ReverseCommand { + /// This request is sent from the debug adapter to the client to run a command in a terminal. + /// + /// This is typically used to launch the debuggee in a terminal provided by the client. + /// + /// This request should only be called if the corresponding client capability + /// `supportsRunInTerminalRequest` is true. + /// + /// Client implementations of `runInTerminal` are free to run the command however they choose + /// including issuing the command to a command line interpreter (aka 'shell'). Argument strings + /// passed to the `runInTerminal` request must arrive verbatim in the command to be run. + /// As a consequence, clients which use a shell are responsible for escaping any special shell + /// characters in the argument strings to prevent them from being interpreted (and modified) by + /// the shell. + /// + /// Some users may wish to take advantage of shell processing in the argument strings. For + /// clients which implement `runInTerminal` using an intermediary shell, the + /// `argsCanBeInterpretedByShell` property can be set to true. In this case the client is + /// requested not to escape any special shell characters in the argument strings. + /// + /// Specification: [RunInTerminal](https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_RunInTerminal) + RunInTerminal(RunInTerminalRequestArguments), + /// This request is sent from the debug adapter to the client to start a new debug session of the + /// same type as the caller. + /// + /// This request should only be sent if the corresponding client capability + /// `supportsStartDebuggingRequest` is true. + /// + /// Specification: [StartDebugging](https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_StartDebugging) + StartDebugging(StartDebuggingRequestArguments), +} + +/// A debug adapter initiated request. +/// +/// The specification treats reverse requests identically to all other requests +/// (even though there is a separate section for them). However, in Rust, it is +/// beneficial to separate them because then we don't need to generate a huge +/// amount of serialization code for all requests and supporting types (that the +/// vast majority of would never be serialized by the adapter, only deserialized). +#[cfg_attr(feature = "client", derive(Deserialize))] +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ReverseRequest { + /// Sequence number for the Request. + /// + /// From the [specification](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage): + /// + /// Sequence number of the message (also known as message ID). The `seq` for + /// the first message sent by a client or debug adapter is 1, and for each + /// subsequent message is 1 greater than the previous message sent by that + /// actor. `seq` can be used to order requests, responses, and events, and to + /// associate requests with their corresponding responses. For protocol + /// messages of type `request` the sequence number can be used to cancel the + /// request. + pub seq: i64, + /// The command to execute. + /// + /// This is stringly typed in the specification, but represented as an enum for better + /// ergonomics in Rust code, along with the arguments when present. + #[serde(flatten)] + pub command: ReverseCommand, +} diff --git a/crates/dap/src/server.rs b/crates/dap/src/server.rs new file mode 100644 index 0000000..dc2d27b --- /dev/null +++ b/crates/dap/src/server.rs @@ -0,0 +1,300 @@ +use std::{ + fmt::Debug, + io::{BufRead, BufReader, BufWriter, Read, Write}, + sync::{Arc, Mutex}, +}; + +use serde_json; + +use crate::{ + base_message::{BaseMessage, Sendable}, + errors::{DeserializationError, ServerError}, + events::Event, + requests::Request, + responses::Response, + reverse_requests::ReverseRequest, +}; + +#[derive(Debug)] +enum ServerState { + /// Expecting a header + Header, + /// Expecting content + Content, +} + +/// Handles message encoding and decoding of messages. +/// +/// The `Server` is responsible for reading the incoming bytestream and constructing deserialized +/// requests from it, as well as constructing and serializing outgoing messages. +pub struct Server { + input_buffer: BufReader, + + /// A sharable `ServerOutput` object for sending messages and events from + /// other threads. + pub output: Arc>>, +} + +/// Handles emission of messages through the connection. +/// +/// `ServerOutput` is responsible for sending messages to the connection. +/// It's only accessible through a mutex that can be shared with other +/// threads. This makes it possible to send e.g. events while the server is +/// blocked polling requests. +pub struct ServerOutput { + output_buffer: BufWriter, + sequence_number: i64, +} + +impl Server { + /// Construct a new Server using the given input and output streams. + pub fn new(input: BufReader, output: BufWriter) -> Self { + let server_output = Arc::new(Mutex::new(ServerOutput { + output_buffer: output, + sequence_number: 0, + })); + + Self { + input_buffer: input, + output: server_output, + } + } + + /// Wait for a request from the development tool + /// + /// This will start reading the `input` buffer that is passed to it and will try to interpret + /// the incoming bytes according to the DAP protocol. + pub fn poll_request(&mut self) -> Result, ServerError> { + let mut state = ServerState::Header; + let mut buffer = String::new(); + let mut content_length: usize = 0; + + loop { + match self.input_buffer.read_line(&mut buffer) { + Ok(read_size) => { + if read_size == 0 { + break Ok(None); + } + match state { + ServerState::Header => { + let parts: Vec<&str> = buffer.trim_end().split(':').collect(); + if parts.len() == 2 { + match parts[0] { + "Content-Length" => { + content_length = match parts[1].trim().parse() { + Ok(val) => val, + Err(_) => { + return Err(ServerError::HeaderParseError { + line: buffer, + }); + } + }; + buffer.clear(); + buffer.reserve(content_length); + state = ServerState::Content; + } + other => { + return Err(ServerError::UnknownHeader { + header: other.to_string(), + }); + } + } + } else { + return Err(ServerError::HeaderParseError { line: buffer }); + } + } + ServerState::Content => { + buffer.clear(); + let mut content = vec![0; content_length]; + self.input_buffer + .read_exact(content.as_mut_slice()) + .map_err(ServerError::IoError)?; + + let content = std::str::from_utf8(content.as_slice()).map_err(|e| { + ServerError::ParseError(DeserializationError::DecodingError(e)) + })?; + eprintln!( + "[DAP-RS] Received content ({content_length} bytes): {content}" + ); + // Deserialize seq and command separately to avoid serde + // #[serde(flatten)] issues with newer serde versions (>=1.0.171) + // where flatten + adjacently-tagged enums can fail. + let raw: serde_json::Value = + serde_json::from_str(content).map_err(|e| { + eprintln!("[DAP-RS] JSON parse error: {e}"); + ServerError::ParseError(DeserializationError::SerdeError(e)) + })?; + let seq = raw.get("seq").and_then(|v| v.as_i64()).ok_or_else(|| { + eprintln!("[DAP-RS] Missing seq field"); + ServerError::ParseError(DeserializationError::SerdeError( + serde_json::from_str::<()>("\"missing seq field\"") + .unwrap_err(), + )) + })?; + // Handle the conflict between unit variants (e.g. ConfigurationDone) + // and struct variants with all-optional fields (e.g. Launch): + // - Unit variants fail with "arguments": {} ("invalid type: map, expected unit variant") + // - Struct variants fail without "arguments" ("missing field `arguments`") + // When arguments is an empty object, try without it first (unit variant), + // then fall back to keeping it (struct variant with optional fields). + let command: crate::requests::Command = if raw + .get("arguments") + .and_then(|v| v.as_object()) + .is_some_and(|m| m.is_empty()) + { + let mut without_args = raw.clone(); + without_args.as_object_mut().unwrap().remove("arguments"); + match serde_json::from_value::( + without_args, + ) { + Ok(cmd) => cmd, + Err(_) => serde_json::from_value(raw.clone()).map_err(|e| { + eprintln!("[DAP-RS] Command deserialize error: {e}"); + ServerError::ParseError(DeserializationError::SerdeError(e)) + })?, + } + } else { + serde_json::from_value(raw.clone()).map_err(|e| { + eprintln!("[DAP-RS] Command deserialize error: {e}"); + ServerError::ParseError(DeserializationError::SerdeError(e)) + })? + }; + eprintln!("[DAP-RS] Successfully parsed request seq={seq}"); + let request = Request { seq, command }; + return Ok(Some(request)); + } + } + } + Err(e) => return Err(ServerError::IoError(e)), + } + } + } + + pub fn send(&mut self, body: Sendable) -> Result<(), ServerError> { + let mut output = self.output.lock().map_err(|_| ServerError::OutputLockError)?; + output.send(body) + } + + pub fn respond(&mut self, response: Response) -> Result<(), ServerError> { + self.send(Sendable::Response(response)) + } + + pub fn send_event(&mut self, event: Event) -> Result<(), ServerError> { + self.send(Sendable::Event(event)) + } + + pub fn send_reverse_request(&mut self, request: ReverseRequest) -> Result<(), ServerError> { + self.send(Sendable::ReverseRequest(request)) + } +} + +impl ServerOutput { + pub fn send(&mut self, body: Sendable) -> Result<(), ServerError> { + self.sequence_number += 1; + + let message = BaseMessage { + seq: self.sequence_number, + message: body, + }; + + let resp_json = serde_json::to_string(&message).map_err(ServerError::SerializationError)?; + write!(self.output_buffer, "Content-Length: {}\r\n\r\n", resp_json.len()) + .map_err(ServerError::IoError)?; + + write!(self.output_buffer, "{}\r\n", resp_json).map_err(ServerError::IoError)?; + self.output_buffer.flush().map_err(ServerError::IoError)?; + Ok(()) + } + + pub fn respond(&mut self, response: Response) -> Result<(), ServerError> { + self.send(Sendable::Response(response)) + } + + pub fn send_event(&mut self, event: Event) -> Result<(), ServerError> { + self.send(Sendable::Event(event)) + } + + pub fn send_reverse_request(&mut self, request: ReverseRequest) -> Result<(), ServerError> { + self.send(Sendable::ReverseRequest(request)) + } +} + +#[cfg(test)] +mod tests { + + use std::io::Cursor; + + use serde_json::Value; + + use super::*; + use crate::requests::{AttachOrLaunchArguments, Command, RestartArguments}; + + fn simulate_poll_request(input: &str) -> Request { + let mut server_in = Cursor::new(input.as_bytes().to_vec()); + let server_out = Vec::new(); + let mut server = Server::new(BufReader::new(&mut server_in), BufWriter::new(server_out)); + + server.poll_request().unwrap().unwrap() + } + + #[test] + fn test_server_init_request() { + let req = simulate_poll_request( + "Content-Length: 155\r\n\r\n{\"seq\": 152,\"type\": \"request\",\"command\": \ + \"initialize\",\"arguments\": {\"adapterID\": \ + \"0001e357-72c7-4f03-ae8f-c5b54bd8dabf\", \"clientName\": \"Some Cool Editor\"}}", + ); + + assert_eq!(req.seq, 152); + assert!(matches!(req.command, Command::Initialize { .. })); + } + + #[test] + fn test_server_restart_request() { + let req = simulate_poll_request( + "Content-Length: 67\r\n\r\n{\"seq\": 152,\"type\": \"request\",\"command\": \ + \"restart\",\"arguments\": {}}", + ); + + assert!(matches!( + req.command, + Command::Restart { + 0: RestartArguments { arguments: None } + } + )); + + // Restarting a launch request + let req = simulate_poll_request( + "Content-Length: 96\r\n\r\n{\"seq\": 152,\"type\": \"request\",\"command\": \ + \"restart\",\"arguments\": {\"arguments\": {\"noDebug\":true}}}", + ); + assert!(matches!( + req.command, + Command::Restart { + 0: RestartArguments { + arguments: Some(AttachOrLaunchArguments { + no_debug: Some(_), + .. + }) + } + } + )); + + // Restarting a launch or attach request + let req = simulate_poll_request( + "Content-Length: 98\r\n\r\n{\"seq\": 152,\"type\": \"request\",\"command\": \ + \"restart\",\"arguments\": {\"arguments\": {\"__restart\":true}}}", + ); + assert!(matches!( + req.command, + Command::Restart { + 0: RestartArguments { + arguments: Some(AttachOrLaunchArguments { + restart_data: Some(Value::Bool(true)), + .. + }) + } + } + )); + } +} diff --git a/crates/dap/src/types.rs b/crates/dap/src/types.rs new file mode 100644 index 0000000..9a25dd7 --- /dev/null +++ b/crates/dap/src/types.rs @@ -0,0 +1,1394 @@ +use std::collections::HashMap; + +#[cfg(feature = "integration_testing")] +use fake::{Dummy, Fake, Faker}; +#[cfg(feature = "integration_testing")] +use rand::Rng; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionBreakpointsFilter { + /// The internal ID of the filter option. This value is passed to the + /// `setExceptionBreakpoints` request. + pub filter: String, + /// The name of the filter option. This is shown in the UI. + pub label: String, + /// A help text providing additional information about the exception filter. + /// This string is typically shown as a hover and can be translated. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Initial value of the filter option. If not specified a value false is + /// assumed. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, + /// Controls whether a condition can be specified for this filter option. If + /// false or missing, a condition can not be set. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_condition: Option, + /// A help text providing information about the condition. This string is shown + /// as the placeholder text for a text box and can be translated. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition_description: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ColumnDescriptorType { + String, + Number, + Boolean, + UnixTimestampUTC, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ColumnDescriptor { + /// Name of the attribute rendered in this column. + pub attribute_name: String, + /// Header UI label of column. + pub label: String, + /// Format to use for the rendered values in this column. TBD how the format + /// strings looks like. + pub format: String, + /// Datatype of values in this column. Defaults to `string` if not specified. + /// Values: 'string', 'number', 'bool', 'unixTimestampUTC' + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub column_descriptor_type: Option, + /// Width of this column in characters (hint only). + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ChecksumAlgorithm { + MD5, + SHA1, + SHA256, + #[serde(rename = "timestamp")] + Timestamp, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Capabilities { + /// The debug adapter supports the `configurationDone` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_configuration_done_request: Option, + /// The debug adapter supports function breakpoints. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_function_breakpoints: Option, + /// The debug adapter supports conditional breakpoints. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_conditional_breakpoints: Option, + /// The debug adapter supports breakpoints that break execution after a + /// specified i64 of hits. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_hit_conditional_breakpoints: Option, + /// The debug adapter supports a (side effect free) `evaluate` request for data + /// hovers. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_evaluate_for_hovers: Option, + /// Available exception filter options for the `setExceptionBreakpoints` + /// request. + #[serde(skip_serializing_if = "Option::is_none")] + pub exception_breakpoint_filters: Option>, + /// The debug adapter supports stepping back via the `stepBack` and + /// `reverseContinue` requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_step_back: Option, + /// The debug adapter supports setting a variable to a value. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_set_variable: Option, + /// The debug adapter supports restarting a frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_restart_frame: Option, + /// The debug adapter supports the `gotoTargets` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_goto_targets_request: Option, + /// The debug adapter supports the `stepInTargets` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_step_in_targets_request: Option, + /// The debug adapter supports the `completions` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_completions_request: Option, + /// The set of characters that should trigger completion in a REPL. If not + /// specified, the UI should assume the `.` character. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_trigger_characters: Option>, + /// The debug adapter supports the `modules` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_modules_request: Option, + /// The set of additional module information exposed by the debug adapter. + #[serde(skip_serializing_if = "Option::is_none")] + pub additional_module_columns: Option>, + /// Checksum algorithms supported by the debug adapter. + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_checksum_algorithms: Option>, + /// The debug adapter supports the `restart` request. In this case a client + /// should not implement `restart` by terminating and relaunching the adapter + /// but by calling the `restart` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_restart_request: Option, + /// The debug adapter supports `exceptionOptions` on the + /// `setExceptionBreakpoints` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_options: Option, + /// The debug adapter supports a `format` attribute on the `stackTrace`, + /// `variables`, and `evaluate` requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_value_formatting_options: Option, + /// The debug adapter supports the `exceptionInfo` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_info_request: Option, + /// The debug adapter supports the `terminateDebuggee` attribute on the + /// `disconnect` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub support_terminate_debuggee: Option, + /// The debug adapter supports the `suspendDebuggee` attribute on the + /// `disconnect` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub support_suspend_debuggee: Option, + /// The debug adapter supports the delayed loading of parts of the stack, which + /// requires that both the `startFrame` and `levels` arguments and the + /// `totalFrames` result of the `stackTrace` request are supported. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_delayed_stack_trace_loading: Option, + /// The debug adapter supports the `loadedSources` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_loaded_sources_request: Option, + /// The debug adapter supports log points by interpreting the `logMessage` + /// attribute of the `SourceBreakpoint`. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_log_points: Option, + /// The debug adapter supports the `terminateThreads` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_terminate_threads_request: Option, + /// The debug adapter supports the `setExpression` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_set_expression: Option, + /// The debug adapter supports the `terminate` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_terminate_request: Option, + /// The debug adapter supports data breakpoints. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_data_breakpoints: Option, + /// The debug adapter supports the `readMemory` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_read_memory_request: Option, + /// The debug adapter supports the `writeMemory` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_write_memory_request: Option, + /// The debug adapter supports the `disassemble` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_disassemble_request: Option, + /// The debug adapter supports the `cancel` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_cancel_request: Option, + /// The debug adapter supports the `breakpointLocations` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_breakpoint_locations_request: Option, + /// The debug adapter supports the `clipboard` context value in the `evaluate` + /// request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_clipboard_context: Option, + /// The debug adapter supports stepping granularities (argument `granularity`) + /// for the stepping requests. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_stepping_granularity: Option, + /// The debug adapter supports adding breakpoints based on instruction + /// references. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_instruction_breakpoints: Option, + /// The debug adapter supports `filterOptions` as an argument on the + /// `setExceptionBreakpoints` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_exception_filter_options: Option, + /// The debug adapter supports the `singleThread` property on the execution + /// requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, + /// `stepBack`). + #[serde(skip_serializing_if = "Option::is_none")] + pub supports_single_thread_execution_requests: Option, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +pub struct CustomValue(pub Value); + +#[cfg(feature = "integration_testing")] +struct ValueFaker; + +#[cfg(feature = "integration_testing")] +impl Dummy for CustomValue { + fn dummy_with_rng(_: &ValueFaker, rng: &mut R) -> Self { + CustomValue(match rng.gen_range(0..=5) { + 1 => Value::Bool(rng.r#gen()), + 2 => Value::Number(serde_json::Number::from_f64(rng.r#gen()).unwrap()), + 3 => Value::String(Faker.fake::()), + _ => Value::Null, + }) + } +} + +/// A Source is a descriptor for source code. +/// +/// It is returned from the debug adapter as part of a StackFrame and it is used by clients when +/// specifying breakpoints. +/// +/// Specification: [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Source { + /// The short name of the source. Every source returned from the debug adapter + /// has a name. + /// When sending a source to the debug adapter this name is optional. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// The path of the source to be shown in the UI. + /// It is only used to locate and load the content of the source if no + /// `sourceReference` is specified (or its value is 0). + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + /// If the value > 0 the contents of the source must be retrieved through the + /// `source` request (even if a path is specified). + /// Since a `sourceReference` is only valid for a session, it can not be used + /// to persist a source. + /// The value should be less than or equal to 2147483647 (2^31-1). + #[serde(skip_serializing_if = "Option::is_none")] + pub source_reference: Option, + /// A hint for how to present the source in the UI. + /// A value of `deemphasize` can be used to indicate that the source is not + /// available or that it is skipped on stepping. + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + /// The origin of this source. For example, 'internal module', 'inlined content + /// from source map', etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub origin: Option, + /// A list of sources that are related to this source. These may be the source + /// that generated this source. + #[serde(skip_serializing_if = "Option::is_none")] + pub sources: Option>, + /// Additional data that a debug adapter might want to loop through the client. + /// The client should leave the data intact and persist it across sessions. The + /// client should not interpret the data. + #[cfg_attr(feature = "integration_testing", dummy(faker = "ValueFaker"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub adapter_data: Option, + /// The checksums associated with this file. + #[serde(skip_serializing_if = "Option::is_none")] + pub checksums: Option>, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct SourceBreakpoint { + /// The source line of the breakpoint or logpoint. + pub line: i64, + /// Start position within source line of the breakpoint or logpoint. It is + /// measured in UTF-16 code units and the client capability `columnsStartAt1` + /// determines whether it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The expression for conditional breakpoints. + /// It is only honored by a debug adapter if the corresponding capability + /// `supportsConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + /// The expression that controls how many hits of the breakpoint are ignored. + /// The debug adapter is expected to interpret the expression as needed. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsHitConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_condition: Option, + /// If this attribute exists and is non-empty, the debug adapter must not + /// 'break' (stop) + /// but log the message instead. Expressions within `{}` are interpolated. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsLogPoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub log_message: Option, +} + +/// Information about a breakpoint created in setBreakpoints, setFunctionBreakpoints, +/// setInstructionBreakpoints, or setDataBreakpoints requests. +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Breakpoint { + /// The identifier for the breakpoint. It is needed if breakpoint events are + /// used to update or remove breakpoints. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// If true, the breakpoint could be set (but not necessarily at the desired + /// location). + pub verified: bool, + /// A message about the state of the breakpoint. + /// This is shown to the user and can be used to explain why a breakpoint could + /// not be verified. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// The source where the breakpoint is located. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// The start line of the actual range covered by the breakpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + /// Start position of the source range covered by the breakpoint. It is + /// measured in UTF-16 code units and the client capability `columnsStartAt1` + /// determines whether it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The end line of the actual range covered by the breakpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// End position of the source range covered by the breakpoint. It is measured + /// in UTF-16 code units and the client capability `columnsStartAt1` determines + /// whether it is 0- or 1-based. + /// If no end line is given, then the end column is assumed to be in the start + /// line. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, + /// A memory reference to where the breakpoint is set. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_reference: Option, + /// The offset from the instruction reference. + /// This can be negative. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum PresentationHint { + Normal, + Emphasize, + DeEmphasize, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Checksum { + /// The algorithm used to calculate this checksum. + pub algorithm: ChecksumAlgorithm, + /// Value of the checksum, encoded as a hexadecimal value. + pub checksum: String, +} + +/// An ExceptionFilterOptions is used to specify an exception filter together with a condition for +/// the setExceptionBreakpoints request. +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionFilterOptions { + /// ID of an exception filter returned by the `exceptionBreakpointFilters` + /// capability. + pub filter_id: String, + /// An expression for conditional exceptions. + /// The exception breaks into the debugger if the result of the condition is + /// true. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, +} + +/// This enumeration defines all possible conditions when a thrown exception should result in a +/// break. +/// +/// Specification: [`ExceptionBreakMode`](https://microsoft.github.io/debug-adapter-protocol/specification#Types_ExceptionBreakMode) +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ExceptionBreakMode { + /// never breaks + #[default] + Never, + /// always breaks + Always, + /// breaks when exception unhandled + Unhandled, + /// breaks if the exception is not handled by user code + UserUnhandled, +} + +/// An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in +/// a tree of exceptions. +/// If a segment consists of more than one name, it matches the names provided if negate is false +/// or missing, or it matches anything except the names provided if negate is true. +/// +/// Specification: [`ExceptionPathSegment`](https://microsoft.github.io/debug-adapter-protocol/specification#Types_ExceptionPathSegment) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionPathSegment { + /// If false or missing this segment matches the names provided, otherwise it + /// matches anything except the names provided. + #[serde(skip_serializing_if = "Option::is_none")] + pub negate: Option, + /// Depending on the value of `negate` the names that should match or not + /// match. + pub names: Vec, +} + +/// An ExceptionOptions assigns configuration options to a set of exceptions. +/// +/// Specification: [`ExceptionOptions`](https://microsoft.github.io/debug-adapter-protocol/specification#Types_ExceptionOptions) +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionOptions { + /// A path that selects a single or multiple exceptions in a tree. If `path` is + /// missing, the whole tree is selected. + /// By convention the first segment of the path is a category that is used to + /// group exceptions in the UI. + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option>, + /// Condition when a thrown exception should result in a break. + pub break_mode: ExceptionBreakMode, +} + +/// Properties of a breakpoint passed to the setFunctionBreakpoints request. +/// +/// Specification: [FunctionBreakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_FunctionBreakpoint) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct FunctionBreakpoint { + /// The name of the function. + pub name: String, + /// An expression for conditional breakpoints. + /// It is only honored by a debug adapter if the corresponding capability + /// `supportsConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + /// An expression that controls how many hits of the breakpoint are ignored. + /// The debug adapter is expected to interpret the expression as needed. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsHitConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_condition: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum BreakpointEventReason { + Changed, + New, + Removed, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum InvalidatedAreas { + All, + Stacks, + Threads, + Variables, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum LoadedSourceEventReason { + New, + Changed, + Removed, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ModuleEventReason { + New, + Changed, + Removed, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Module { + /// Unique identifier for the module. + pub id: ModuleId, + /// A name of the module. + pub name: String, + /// Logical full path to the module. The exact definition is implementation + /// defined, but usually this would be a full path to the on-disk file for the + /// module. + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + /// True if the module is optimized. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_optimized: Option, + /// True if the module is considered 'user code' by a debugger that supports + /// 'Just My Code'. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_user_code: Option, + /// Version of Module. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + /// User-understandable description of if symbols were found for the module + /// (ex: 'Symbols Loaded', 'Symbols not found', etc.) + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_status: Option, + /// Logical full path to the symbol file. The exact definition is + /// implementation defined. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_file_path: Option, + /// Module created or modified, encoded as a RFC 3339 timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + pub date_time_stamp: Option, + /// Address range covered by this module. + #[serde(skip_serializing_if = "Option::is_none")] + pub address_range: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ModuleId { + Number, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum OutputEventCategory { + Console, + Important, + Stdout, + Stderr, + Telemetry, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum OutputEventGroup { + Start, + StartCollapsed, + End, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ProcessEventStartMethod { + Launch, + Attach, + AttachForSuspendedLaunch, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum StoppedEventReason { + Step, + Breakpoint, + Exception, + Pause, + Entry, + Goto, + Function, + Data, + Instruction, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ThreadEventReason { + Started, + Exited, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ValueFormat { + /// Display the value in hex. + #[serde(skip_serializing_if = "Option::is_none")] + pub hex: Option, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct StackFrameFormat { + /// Displays parameters for the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option, + /// Displays the types of parameters for the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_types: Option, + /// Displays the names of parameters for the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_names: Option, + /// Displays the values of parameters for the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_values: Option, + /// Displays the line i64 of the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + /// Displays the module of the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub module: Option, + /// Includes all stack frames, including those the debug adapter might + /// otherwise hide. + #[serde(skip_serializing_if = "Option::is_none")] + pub include_all: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum EvaluateArgumentsContext { + Variables, + Watch, + Repl, + Hover, + Clipboard, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum SteppingGranularity { + Statement, + Line, + Instruction, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum DataBreakpointAccessType { + Read, + Write, + ReadWrite, +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct DataBreakpoint { + /// An id representing the data. This id is returned from the + /// `dataBreakpointInfo` request. + pub data_id: String, + /// The access type of the data. + #[serde(skip_serializing_if = "Option::is_none")] + pub access_type: Option, + /// An expression for conditional breakpoints. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + /// An expression that controls how many hits of the breakpoint are ignored. + /// The debug adapter is expected to interpret the expression as needed. + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_condition: Option, +} + +/// Properties of a breakpoint passed to the setInstructionBreakpoints request +/// +/// Specfication: [InstructionBreakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_InstructionBreakpoint) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct InstructionBreakpoint { + /// The instruction reference of the breakpoint. + /// This should be a memory or instruction pointer reference from an + /// `EvaluateResponse`, `Variable`, `StackFrame`, `GotoTarget`, or + /// `Breakpoint`. + pub instruction_reference: String, + /// The offset from the instruction reference. + /// This can be negative. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// An expression for conditional breakpoints. + /// It is only honored by a debug adapter if the corresponding capability + /// `supportsConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + /// An expression that controls how many hits of the breakpoint are ignored. + /// The debug adapter is expected to interpret the expression as needed. + /// The attribute is only honored by a debug adapter if the corresponding + /// capability `supportsHitConditionalBreakpoints` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub hit_condition: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum VariablesArgumentsFilter { + Indexed, + Named, +} + +/// Properties of a breakpoint location returned from the breakpointLocations request. +/// Specfication: [BreakpointLocation](https://microsoft.github.io/debug-adapter-protocol/specification#Types_BreakpointLocation) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct BreakpointLocation { + /// Start line of breakpoint location. + pub line: i64, + /// The start position of a breakpoint location. Position is measured in UTF-16 + /// code units and the client capability `columnsStartAt1` determines whether + /// it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The end line of breakpoint location if the location covers a range. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// The end position of a breakpoint location (if the location covers a range). + /// Position is measured in UTF-16 code units and the client capability + /// `columnsStartAt1` determines whether it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, +} + +/// Some predefined types for the CompletionItem. Please note that not all clients have specific +/// icons for all of them +/// +/// Specification: [CompletionItemType](https://microsoft.github.io/debug-adapter-protocol/specification#Types_CompletionItemType) +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum CompletionItemType { + Method, + Function, + Constructor, + Field, + Variable, + Class, + Interface, + Module, + Property, + Unit, + Value, + Enum, + Keyword, + Snippet, + Text, + Color, + File, + Reference, + CustomColor, +} + +/// `CompletionItems` are the suggestions returned from the `completions` request. +/// +/// Specification: [CompletionItem](https://microsoft.github.io/debug-adapter-protocol/specification#Types_CompletionItem) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct CompletionItem { + /// The label of this completion item. By default this is also the text that is + /// inserted when selecting this completion. + pub label: String, + /// If text is returned and not an empty String, then it is inserted instead of + /// the label. + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + /// A String that should be used when comparing this item with other items. If + /// not returned or an empty String, the `label` is used instead. + #[serde(skip_serializing_if = "Option::is_none")] + pub sort_text: Option, + /// A human-readable String with additional information about this item, like + /// type or symbol information. + #[serde(skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// The item's type. Typically the client uses this information to render the + /// item in the UI with an icon. + #[serde(rename = "type")] + #[serde(skip_serializing_if = "Option::is_none")] + pub type_field: Option, + /// Start position (within the `text` attribute of the `completions` request) + /// where the completion text is added. The position is measured in UTF-16 code + /// units and the client capability `columnsStartAt1` determines whether it is + /// 0- or 1-based. If the start position is omitted the text is added at the + /// location specified by the `column` attribute of the `completions` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, + /// Length determines how many characters are overwritten by the completion + /// text and it is measured in UTF-16 code units. If missing the value 0 is + /// assumed which results in the completion text being inserted. + #[serde(skip_serializing_if = "Option::is_none")] + pub length: Option, + /// Determines the start of the new selection after the text has been inserted + /// (or replaced). `selectionStart` is measured in UTF-16 code units and must + /// be in the range 0 and length of the completion text. If omitted the + /// selection starts at the end of the completion text. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_start: Option, + /// Determines the length of the new selection after the text has been inserted + /// (or replaced) and it is measured in UTF-16 code units. The selection can + /// not extend beyond the bounds of the completion text. If omitted the length + /// is assumed to be 0. + #[serde(skip_serializing_if = "Option::is_none")] + pub selection_length: Option, +} + +/// Represents a single disassembled instruction. +/// +/// Specification: [DisassembledInstruction](https://microsoft.github.io/debug-adapter-protocol/specification#Types_DisassembledInstruction) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct DisassembledInstruction { + /// The address of the instruction. Treated as a hex value if prefixed with + /// `0x`, or as a decimal value otherwise. + pub address: String, + /// Raw bytes representing the instruction and its operands, in an + /// implementation-defined format. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_bytes: Option, + /// Text representing the instruction and its operands, in an + /// implementation-defined format. + pub instruction: String, + /// Name of the symbol that corresponds with the location of this instruction, + /// if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol: Option, + /// Source location that corresponds to this instruction, if any. + /// Should always be set (if available) on the first instruction returned, + /// but can be omitted afterwards if this instruction maps to the same source + /// file as the previous instruction. + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + /// The line within the source location that corresponds to this instruction, + /// if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + /// The column within the line that corresponds to this instruction, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The end line of the range that corresponds to this instruction, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// The end column of the range that corresponds to this instruction, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum VariablePresentationHintKind { + /// Indicates that the object is a property. + Property, + /// Indicates that the object is a method. + Method, + /// Indicates that the object is a class. + Class, + /// Indicates that the object is data. + Data, + /// Indicates that the object is an event. + Event, + /// Indicates that the object is a base class. + BaseClass, + /// Indicates that the object is an inner class. + InnerClass, + /// Indicates that the object is an interface. + Interface, + /// Indicates that the object is the most derived class. + MostDerivedClass, + /// Indicates that the object is virtual, that means it is a + /// synthetic object introduced by the adapter for rendering purposes, e.g. an + /// index range for large arrays. + Virtual, + /// Deprecated: Indicates that a data breakpoint is + /// registered for the object. The `hasDataBreakpoint` attribute should + /// generally be used instead. + DataBreakpoint, + #[serde(untagged)] + String(String), +} + +/// Set of attributes represented as an array of Strings. Before introducing +/// additional values, try to use the listed values. +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum VariablePresentationHintAttributes { + /// Indicates that the object is static. + Static, + /// Indicates that the object is a constant. + Constant, + /// Indicates that the object is read only. + ReadOnly, + /// Indicates that the object is a raw String. + RawString, + /// Indicates that the object can have an Object ID created for it. + HasObjectId, + /// Indicates that the object has an Object ID associated with it. + CanHaveObjectId, + /// Indicates that the evaluation had side effects. + HasSideEffects, + /// Indicates that the object has its value tracked by a data breakpoint. + HasDataBreakpoint, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum VariablePresentationHintVisibility { + Public, + Private, + Protected, + Internal, + Final, + #[serde(untagged)] + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +#[serde(rename_all = "camelCase")] +pub struct VariablePresentationHint { + /// The kind of variable. Before introducing additional values, try to use the + /// listed values. + /// Values: + /// 'property': Indicates that the object is a property. + /// 'method': Indicates that the object is a method. + /// 'class': Indicates that the object is a class. + /// 'data': Indicates that the object is data. + /// 'event': Indicates that the object is an event. + /// 'baseClass': Indicates that the object is a base class. + /// 'innerClass': Indicates that the object is an inner class. + /// 'interface': Indicates that the object is an interface. + /// 'mostDerivedClass': Indicates that the object is the most derived class. + /// 'virtual': Indicates that the object is virtual, that means it is a + /// synthetic object introduced by the adapter for rendering purposes, e.g. an + /// index range for large arrays. + /// 'dataBreakpoint': Deprecated: Indicates that a data breakpoint is + /// registered for the object. The `hasDataBreakpoint` attribute should + /// generally be used instead. + /// etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Set of attributes represented as an array of Strings. Before introducing + /// additional values, try to use the listed values. + /// Values: + /// 'static': Indicates that the object is static. + /// 'constant': Indicates that the object is a constant. + /// 'readOnly': Indicates that the object is read only. + /// 'rawString': Indicates that the object is a raw String. + /// 'hasObjectId': Indicates that the object can have an Object ID created for + /// it. + /// 'canHaveObjectId': Indicates that the object has an Object ID associated + /// with it. + /// 'hasSideEffects': Indicates that the evaluation had side effects. + /// 'hasDataBreakpoint': Indicates that the object has its value tracked by a + /// data breakpoint. + /// etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes: Option>, + /// Visibility of variable. Before introducing additional values, try to use + /// the listed values. + /// Values: 'public', 'private', 'protected', 'internal', 'final', etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, + /// If true, clients can present the variable with a UI that supports a + /// specific gesture to trigger its evaluation. + /// This mechanism can be used for properties that require executing code when + /// retrieving their value and where the code execution can be expensive and/or + /// produce side-effects. A typical example are properties based on a getter + /// function. + /// Please note that in addition to the `lazy` flag, the variable's + /// `variablesReference` is expected to refer to a variable that will provide + /// the value through another `variable` request. + #[serde(skip_serializing_if = "Option::is_none")] + pub lazy: Option, +} + +/// Detailed information about an exception that has occurred. +/// +/// Specification: [ExceptionDetails](https://microsoft.github.io/debug-adapter-protocol/specification#Types_ExceptionDetails) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct ExceptionDetails { + /// Message contained in the exception. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Short type name of the exception object. + #[serde(skip_serializing_if = "Option::is_none")] + pub type_name: Option, + /// Fully-qualified type name of the exception object. + #[serde(skip_serializing_if = "Option::is_none")] + pub full_type_name: Option, + /// An expression that can be evaluated in the current scope to obtain the + /// exception object. + #[serde(skip_serializing_if = "Option::is_none")] + pub evaluate_name: Option, + /// Stack trace at the time the exception was thrown. + #[serde(skip_serializing_if = "Option::is_none")] + pub stack_trace: Option, + /// Details of the exception contained by this exception, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub inner_exception: Option>, +} + +/// A `GotoTarget` describes a code location that can be used as a target in the +/// goto request. +/// The possible goto targets can be determined via the gotoTargets request. +/// +/// Specification: [GotoTarget](https://microsoft.github.io/debug-adapter-protocol/specification#Types_GotoTarget) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct GotoTarget { + /// Unique identifier for a goto target. This is used in the `goto` request. + pub id: i64, + /// The name of the goto target (shown in the UI). + pub label: String, + /// The line of the goto target. + pub line: i64, + /// The column of the goto target. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The end line of the range covered by the goto target. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// The end column of the range covered by the goto target. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, + /// A memory reference for the instruction pointer value represented by this + /// target. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_pointer_reference: Option, +} + +/// A hint for how to present this scope in the UI. If this attribute is +/// missing, the scope is shown with a generic UI. +/// +/// Specification: [Scope](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Scope) +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum ScopePresentationhint { + /// Scope contains method arguments. + Arguments, + /// Scope contains local variables. + Locals, + /// Scope contains registers. Only a single `registers` scope + /// should be returned from a `scopes` request. + Registers, + #[serde(untagged)] + String(String), +} + +/// A Scope is a named container for variables. Optionally a scope can map to a source or a range +/// within a source. +/// +/// Specification: [Scope](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Scope) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Scope { + /// Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This + /// String is shown in the UI as is and can be translated. + pub name: String, + /// A hint for how to present this scope in the UI. If this attribute is + /// missing, the scope is shown with a generic UI. + /// Values: + /// 'arguments': Scope contains method arguments. + /// 'locals': Scope contains local variables. + /// 'registers': Scope contains registers. Only a single `registers` scope + /// should be returned from a `scopes` request. + /// etc. + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + /// The variables of this scope can be retrieved by passing the value of + /// `variablesReference` to the `variables` request as long as execution + /// remains suspended. See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: i64, + /// The i64 of named variables in this scope. + /// The client can use this information to present the variables in a paged UI + /// and fetch them in chunks. + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + /// The i64 of indexed variables in this scope. + /// The client can use this information to present the variables in a paged UI + /// and fetch them in chunks. + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + /// If true, the i64 of variables in this scope is large or expensive to + /// retrieve. + pub expensive: bool, + /// The source for this scope. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// The start line of the range covered by this scope. + #[serde(skip_serializing_if = "Option::is_none")] + pub line: Option, + /// Start position of the range covered by the scope. It is measured in UTF-16 + /// code units and the client capability `columnsStartAt1` determines whether + /// it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub column: Option, + /// The end line of the range covered by this scope. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// End position of the range covered by the scope. It is measured in UTF-16 + /// code units and the client capability `columnsStartAt1` determines whether + /// it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(untagged)] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum StackFrameModuleid { + Number(i64), + String(String), +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum StackFramePresentationhint { + Normal, + Label, + Subtle, +} + +/// A Stackframe contains the source location. +/// +/// Specification: [StackFrame](https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct StackFrame { + /// An identifier for the stack frame. It must be unique across all threads. + /// This id can be used to retrieve the scopes of the frame with the `scopes` + /// request or to restart the execution of a stackframe. + pub id: i64, + /// The name of the stack frame, typically a method name. + pub name: String, + /// The source of the frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + /// The line within the source of the frame. If the source attribute is missing + /// or doesn't exist, `line` is 0 and should be ignored by the client. + pub line: i64, + /// Start position of the range covered by the stack frame. It is measured in + /// UTF-16 code units and the client capability `columnsStartAt1` determines + /// whether it is 0- or 1-based. If attribute `source` is missing or doesn't + /// exist, `column` is 0 and should be ignored by the client. + pub column: i64, + /// The end line of the range covered by the stack frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_line: Option, + /// End position of the range covered by the stack frame. It is measured in + /// UTF-16 code units and the client capability `columnsStartAt1` determines + /// whether it is 0- or 1-based. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_column: Option, + /// Indicates whether this frame can be restarted with the `restart` request. + /// Clients should only use this if the debug adapter supports the `restart` + /// request and the corresponding capability `supportsRestartRequest` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub can_restart: Option, + /// A memory reference for the current instruction pointer in this frame. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_pointer_reference: Option, + /// The module associated with this frame, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub module_id: Option, + /// A hint for how to present this frame in the UI. + /// A value of `label` can be used to indicate that the frame is an artificial + /// frame that is used as a visual label or separator. A value of `subtle` can + /// be used to change the appearance of a frame in a 'subtle' way. + /// Values: 'normal', 'label', 'subtle' + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, +} + +/// A thread. +/// +/// Specification: [Thread](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Thread) +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Thread { + /// Unique identifier for the thread. + pub id: i64, + /// The name of the thread. + pub name: String, +} + +/// A Variable is a name/value pair. +/// +/// The `type` attribute is shown if space permits or when hovering over the variable’s name. +/// +/// The `kind` attribute is used to render additional properties of the variable, e.g. different +/// icons can be used to indicate that a variable is public or private. +/// +/// If the value is structured (has children), a handle is provided to retrieve the children with +/// the `variables` request. +/// +/// If the number of named or indexed children is large, the numbers should be returned via the +/// `namedVariables` and `indexedVariables` attributes. +/// +/// The client can use this information to present the children in a paged UI and fetch them in +/// chunks. +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub struct Variable { + /// The variable's name. + pub name: String, + /// The variable's value. + /// This can be a multi-line text, e.g. for a function the body of a function. + /// For structured variables (which do not have a simple value), it is + /// recommended to provide a one-line representation of the structured object. + /// This helps to identify the structured object in the collapsed state when + /// its children are not yet visible. + /// An empty String can be used if no value should be shown in the UI. + pub value: String, + /// The type of the variable's value. Typically shown in the UI when hovering + /// over the value. + /// This attribute should only be returned by a debug adapter if the + /// corresponding capability `supportsVariableType` is true. + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_field: Option, + /// Properties of a variable that can be used to determine how to render the + /// variable in the UI. + #[serde(skip_serializing_if = "Option::is_none")] + pub presentation_hint: Option, + /// The evaluatable name of this variable which can be passed to the `evaluate` + /// request to fetch the variable's value. + #[serde(skip_serializing_if = "Option::is_none")] + pub evaluate_name: Option, + /// If `variablesReference` is > 0, the variable is structured and its children + /// can be retrieved by passing `variablesReference` to the `variables` request + /// as long as execution remains suspended. See [Lifetime of Object References](https://microsoft.github.io/debug-adapter-protocol/overview#lifetime-of-objects-references) + /// in the Overview section of the specification for details. + pub variables_reference: i64, + /// The i64 of named child variables. + /// The client can use this information to present the children in a paged UI + /// and fetch them in chunks. + #[serde(skip_serializing_if = "Option::is_none")] + pub named_variables: Option, + /// The i64 of indexed child variables. + /// The client can use this information to present the children in a paged UI + /// and fetch them in chunks. + #[serde(skip_serializing_if = "Option::is_none")] + pub indexed_variables: Option, + /// The memory reference for the variable if the variable represents executable + /// code, such as a function pointer. + /// This attribute is only required if the corresponding capability + /// `supportsMemoryReferences` is true. + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_reference: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum RunInTerminalRequestArgumentsKind { + Integrated, + External, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +pub enum StartDebuggingRequestKind { + Launch, + Attach, +} + +/// A structured message object. Used to return errors from requests. +/// +/// Specification: [Message](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Message) +#[derive(Serialize, Debug, Default, Clone)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "integration_testing", derive(Dummy))] +#[cfg_attr(feature = "client", derive(Deserialize))] +pub struct Message { + /// Unique (within a debug adapter implementation) identifier for the message. + /// The purpose of these error IDs is to help extension authors that have the + /// requirement that every user visible error message needs a corresponding + /// error i64, so that users or customer support can find information about + /// the specific error more easily. + pub id: i64, + /// A format String for the message. Embedded variables have the form `{name}`. + /// If variable name starts with an underscore character, the variable does not + /// contain user data (PII) and can be safely used for telemetry purposes. + pub format: String, + /// An object used as a dictionary for looking up the variables in the format string. + pub variables: HashMap, + /// An object used as a dictionary for looking up the variables in the format + /// String. + /// If true send to telemetry. + pub send_telemetry: Option, + /// If true show user. + pub show_user: Option, + /// A url where additional information about this message can be found. + pub url: Option, + /// A label that is presented to the user as the UI for opening the url. + pub url_label: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(unused)] + #[test] + fn test_checksum_algorithm_serde() { + let sha = ChecksumAlgorithm::SHA256; + let sha_ser = serde_json::to_value(sha).unwrap(); + assert_eq!("SHA256", sha_ser); + let sha_deser: ChecksumAlgorithm = serde_json::from_value(sha_ser).unwrap(); + assert!(matches!(ChecksumAlgorithm::SHA256, sha_deser)); + + let ts = ChecksumAlgorithm::Timestamp; + let ts_ser = serde_json::to_value(&ts).unwrap(); + assert_eq!("timestamp", ts_ser); + #[allow(unused)] + let ts_deser: ChecksumAlgorithm = serde_json::from_value(ts_ser).unwrap(); + assert!(matches!(ChecksumAlgorithm::Timestamp, ts_deser)); + } + + #[allow(unused)] + #[test] + fn test_invalidated_areas_serde() { + let str = "string".to_string(); + let untagged = InvalidatedAreas::String(str.clone()); + let untagged_ser = serde_json::to_value(untagged).unwrap(); + assert_eq!(str, untagged_ser); + let untagged_deser: InvalidatedAreas = serde_json::from_value(untagged_ser).unwrap(); + assert!(matches!(InvalidatedAreas::String(str), untagged_deser)); + } +} diff --git a/crates/dap/src/utils.rs b/crates/dap/src/utils.rs new file mode 100644 index 0000000..b6a19a6 --- /dev/null +++ b/crates/dap/src/utils.rs @@ -0,0 +1,66 @@ +use std::fmt::{Display, Formatter}; + +/// A struct representing a version of the DAP specification. +/// This version corresponds to the [changelog](https://microsoft.github.io/debug-adapter-protocol/changelog) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Version { + /// Major version. + pub major: i64, + /// Minor version. + pub minor: i64, + /// Patch version. Historically, this is "x" in the changelog for most versions. That value + /// is represented as `None` here. + pub patch: Option, + /// The git commit in the DAP repo that corresponds to this version. + /// Please note that historically, versions (as of 1.62.x) are not tagged in the DAP repo. + /// Until that changes, we are using the commit that updates the JSON-schema in the gh-pages + /// branch. + pub git_commit: String, +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.patch { + Some(patch) => write!(f, "{}.{}.{}", self.major, self.minor, patch), + None => write!(f, "{}.{}.x", self.major, self.minor), + } + } +} + +/// Returns the version of the DAP specification that this crate implements. +/// +/// Please note that historically, the DAP changelog hasn't been super accurate and the +/// versions (as of 1.62.x) are not tagged in the DAP repo. Until that changes, we are +/// using the commit that *adds the corresponding JSON-schema in the **gh-pages** branch*. +pub fn get_spec_version() -> Version { + Version { + major: 1, + minor: 62, + patch: None, + git_commit: "7f284b169ecd19602487eb4d290ae651d4398ce7".to_string(), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_version_display() { + let version = Version { + major: 1, + minor: 62, + patch: None, + git_commit: "7f284b169ecd19602487eb4d290ae651d4398ce7".to_string(), + }; + assert_eq!(version.to_string(), "1.62.x"); + + let version = Version { + major: 1, + minor: 62, + patch: Some(1), + git_commit: "7f284b169ecd19602487eb4d290ae651d4398ce7".to_string(), + }; + assert_eq!(version.to_string(), "1.62.1"); + } +} diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml new file mode 100644 index 0000000..87ae5f6 --- /dev/null +++ b/crates/engine/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "miden-debug-engine" +description = "Core debugger engine for miden-debug" +version = "0.1.0" +rust-version = "1.90" +authors = ["Miden contributors"] +repository = "https://github.com/0xMiden/miden-debug" +homepage = "https://github.com/0xMiden/miden-debug" +documentation = "https://github.com/0xMiden/miden-debug" +categories = ["development-tools"] +keywords = ["miden", "debugger"] +license = "MIT" +edition = "2024" + +[features] +default = ["std"] +tui = [] +dap = ["dep:dap", "dep:socket2"] +std = ["dep:glob", "clap/std", "clap/env", "miden-assembly-syntax/std"] +proptest = ["dep:proptest"] + +[dependencies] +clap = { version = "4.5", default-features = false, features = ["derive", "std", "env", "help", "suggestions", "error-context"] } +dap = { package = "miden-debug-dap", path = "../dap", optional = true, features = ["client"] } +glob = { version = "0.3.1", optional = true } +log = "0.4" +miden-assembly = { version = "0.22", default-features = false } +miden-assembly-syntax = { version = "0.22", default-features = false } +miden-core = { version = "0.22", default-features = false } +miden-debug-types = { version = "0.22", default-features = false } +miden-mast-package = { version = "0.22", default-features = false } +miden-processor = { version = "0.22", default-features = false } +miden-tx = { git = "https://github.com/0xMiden/protocol", rev = "ae4b45778c7e2c73be87b67906b7fa1ae8e2a285", default-features = false } +num-traits = "0.2" +proptest = { version = "1.4", optional = true } +rustc-demangle = { version = "0.1", features = ["std"] } +serde = { version = "1.0", default-features = false, features = [ + "serde_derive", + "alloc", + "rc", +] } +serde_json = "1" +socket2 = { version = "0.5", optional = true } +smallvec = { version = "1.14", default-features = false, features = [ + "union", + "const_generics", + "const_new", + "drain_filter", +] } +thiserror = { package = "miden-thiserror", version = "1.0" } +toml = { version = "0.8", features = ["preserve_order"] } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs new file mode 100644 index 0000000..3fe360f --- /dev/null +++ b/crates/engine/src/lib.rs @@ -0,0 +1,18 @@ +// Keep using the existing source files for now so the crate boundary can land +// without a large mechanical move. A follow-up can relocate these files under +// `crates/engine/src` once the split settles. +#[path = "../../../src/debug/mod.rs"] +pub mod debug; +#[path = "../../../src/exec/mod.rs"] +pub mod exec; +#[path = "../../../src/felt.rs"] +pub mod felt; +#[cfg(test)] +#[path = "../../../src/test_utils.rs"] +mod test_utils; + +pub use self::{ + debug::*, + exec::*, + felt::{Felt, FromMidenRepr, ToMidenRepr, bytes_to_words, push_wasm_ty_to_operand_stack}, +}; diff --git a/examples/compile-masm.rs b/examples/compile-masm.rs new file mode 100644 index 0000000..ffb335c --- /dev/null +++ b/examples/compile-masm.rs @@ -0,0 +1,51 @@ +//! Compile a .masm file into a .masp package that miden-debug can load. +//! +//! Usage: +//! cargo run --example compile-masm -- examples/simple.masm +//! +//! This produces `examples/simple.masp` which you can then debug: +//! cargo run -- examples/simple.masp + +use std::{env, path::PathBuf, sync::Arc}; + +use miden_assembly::{Assembler, DefaultSourceManager, SourceManager}; +use miden_core::serde::Serializable; +use miden_mast_package::{MastArtifact, Package, PackageKind, PackageManifest}; + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: compile-masm "); + std::process::exit(1); + } + + let input_path = PathBuf::from(&args[1]); + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + + // Read and assemble the MASM source + let source = std::fs::read_to_string(&input_path)?; + let assembler = Assembler::new(source_manager.clone()); + let program = assembler.assemble_program(&source)?; + + // Build a package from the program + let package = Package { + name: input_path.file_stem().and_then(|s| s.to_str()).unwrap_or("program").to_string(), + version: None, + description: None, + kind: PackageKind::Executable, + mast: MastArtifact::Executable(Arc::new(program)), + manifest: PackageManifest::new(vec![]), + sections: vec![], + }; + + // Write the .masp file + let output_path = input_path.with_extension("masp"); + let bytes = package.to_bytes(); + std::fs::write(&output_path, bytes)?; + + println!("Compiled {} -> {}", input_path.display(), output_path.display()); + println!("\nTo debug:"); + println!(" cargo run -- {}", output_path.display()); + + Ok(()) +} diff --git a/examples/simple.masm b/examples/simple.masm new file mode 100644 index 0000000..c40802d --- /dev/null +++ b/examples/simple.masm @@ -0,0 +1,8 @@ +begin + push.1 + push.2 + add + push.10 + mul + swap drop +end diff --git a/src/config.rs b/src/config.rs index 40afb42..99da4ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,8 +16,8 @@ pub struct DebuggerConfig { /// Miden Assembly programs are emitted by the compiler with a `.masp` extension. /// /// You may use `-` as a file name to read a file from stdin. - #[cfg_attr(feature = "tui", arg(required(true), value_name = "FILE"))] - pub input: InputFile, + #[cfg_attr(feature = "tui", arg(value_name = "FILE"))] + pub input: Option, /// Specify the path to a file containing program inputs. /// /// Program inputs are stack and advice provider values which the program can @@ -71,6 +71,16 @@ pub struct DebuggerConfig { /// in the format `::` #[cfg_attr(feature = "tui", arg(long, help_heading = "Execution"))] pub entrypoint: Option, + /// Connect to a remote DAP debug server instead of running a local program. + /// + /// Specify the address of the DAP server (e.g. "127.0.0.1:4711"). + /// When this flag is set, the debugger connects to an existing remote session. + #[cfg(feature = "dap")] + #[cfg_attr( + feature = "tui", + arg(long, value_name = "ADDR", help_heading = "Execution") + )] + pub dap_connect: Option, /// Specify one or more search paths for link libraries requested via `-l` #[cfg_attr( feature = "tui", @@ -164,7 +174,7 @@ impl ColorChoice { } } - #[cfg(all(feature = "tui", not(windows)))] + #[cfg(not(windows))] pub fn env_allows_color(&self) -> bool { match std::env::var_os("TERM") { // If TERM isn't set, then we are in a weird environment that @@ -184,7 +194,7 @@ impl ColorChoice { true } - #[cfg(all(feature = "tui", windows))] + #[cfg(windows)] pub fn env_allows_color(&self) -> bool { // On Windows, if TERM isn't set, then we shouldn't automatically // assume that colors aren't allowed. This is unlike Unix environments diff --git a/src/debug/memory.rs b/src/debug/memory.rs index 2bb4915..ebaec63 100644 --- a/src/debug/memory.rs +++ b/src/debug/memory.rs @@ -39,6 +39,56 @@ impl FromStr for ReadMemoryExpr { } } +impl fmt::Display for ReadMemoryExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.raw_addr())?; + write!(f, " -t {}", self.ty_name())?; + if self.count != 1 { + write!(f, " -c {}", self.count)?; + } + if self.mode != MemoryMode::Word { + write!(f, " -m {}", self.mode)?; + } + if self.format != FormatType::Decimal { + write!(f, " -f {}", self.format)?; + } + Ok(()) + } +} + +impl ReadMemoryExpr { + fn raw_addr(&self) -> u32 { + match self.mode { + MemoryMode::Word => self.addr.addr, + MemoryMode::Byte => self.addr.addr.saturating_mul(4) + u32::from(self.addr.offset), + } + } + + fn ty_name(&self) -> &'static str { + match &self.ty { + Type::I1 => "i1", + Type::I8 => "i8", + Type::I16 => "i16", + Type::I32 => "i32", + Type::I64 => "i64", + Type::I128 => "i128", + Type::U8 => "u8", + Type::U16 => "u16", + Type::U32 => "u32", + Type::U64 => "u64", + Type::U128 => "u128", + Type::Felt => "felt", + Type::Array(array_ty) + if array_ty.element_type() == &Type::Felt && array_ty.len() == 4 => + { + "word" + } + Type::Ptr(_) => "ptr", + ty => panic!("unsupported memory read type serialization: {ty}"), + } + } +} + #[derive(Default, Debug, Parser)] #[command(name = "read")] pub struct Read { diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 809ab27..aea38a8 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -8,6 +8,7 @@ pub use self::{ memory::{FormatType, MemoryMode, ReadMemoryExpr}, native_ptr::NativePtr, stacktrace::{ - CallFrame, CallStack, CurrentFrame, OpDetail, ResolvedLocation, StackTrace, StepInfo, + CallFrame, CallStack, ControlFlowOp, CurrentFrame, OpDetail, ResolvedLocation, StackTrace, + StepInfo, }, }; diff --git a/src/debug/stacktrace.rs b/src/debug/stacktrace.rs index aba7656..1a0c539 100644 --- a/src/debug/stacktrace.rs +++ b/src/debug/stacktrace.rs @@ -14,8 +14,18 @@ use miden_processor::{ContextId, operation::Operation, trace::RowIndex}; use crate::exec::TraceEvent; +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ControlFlowOp { + Span, + Respan, + Join, + Split, + End, +} + pub struct StepInfo<'a> { pub op: Option, + pub control: Option, pub asmop: Option<&'a AssemblyOp>, pub clk: RowIndex, pub ctx: ContextId, @@ -43,6 +53,17 @@ impl CallStack { } } + /// Build a [CallStack] from pre-built frames — used in DAP client mode. + #[cfg(feature = "dap")] + pub fn from_remote_frames(frames: Vec) -> Self { + Self { + trace_events: Rc::new(RefCell::new(BTreeMap::new())), + contexts: BTreeSet::default(), + frames, + block_stack: vec![], + } + } + pub fn stacktrace<'a>( &'a self, recent: &'a VecDeque, @@ -67,85 +88,108 @@ impl CallStack { /// /// Returns the call frame exited this cycle, if any pub fn next(&mut self, info: &StepInfo<'_>) -> Option { - if let Some(op) = info.op { - // Get the current procedure name context, if available - let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name())); - - // Handle trace events for this cycle - let event = self.trace_events.borrow().get(&info.clk).copied(); - log::trace!("handling {op} at cycle {}: {:?}", info.clk, &event); - let popped_frame = self.handle_trace_event(event, procedure.as_ref()); - let is_frame_end = popped_frame.is_some(); + let procedure = info.asmop.map(|op| self.cache_procedure_name(op.context_name())); + + let event = self.trace_events.borrow().get(&info.clk).copied(); + log::trace!( + "handling {:?}/{:?} at cycle {}: {:?}", + info.control, + info.op, + info.clk, + &event + ); + let popped_frame = self.handle_trace_event(event, procedure.as_ref()); + let is_frame_end = popped_frame.is_some(); + + match info.control { + Some(ControlFlowOp::Span) => { + if let Some(asmop) = info.asmop { + log::debug!("{asmop:#?}"); + self.block_stack.push(Some(SpanContext { + frame_index: self.frames.len().saturating_sub(1), + location: asmop.location().cloned(), + })); + } else { + self.block_stack.push(None); + } + } + Some(ControlFlowOp::Join | ControlFlowOp::Split) => { + self.block_stack.push(None); + } + Some(ControlFlowOp::End) => { + self.block_stack.pop(); + } + Some(ControlFlowOp::Respan) | None => {} + } - let ignore = false; + let Some(op) = info.op else { + return popped_frame; + }; - if ignore || is_frame_end { - return popped_frame; - } + if is_frame_end { + return popped_frame; + } - // Attempt to supply procedure context from the current span context, if needed + - // available - let (procedure, asmop) = match procedure { - proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)), - None => match self.block_stack.last() { - Some(Some(span_ctx)) => { - let proc = - self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone()); - let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| { - let context_name = proc.as_deref().unwrap_or("").to_string(); - let raw_asmop = AssemblyOp::new( - span_ctx.location.clone(), - context_name, - 1, - op.to_string(), - ); - Some(Cow::Owned(raw_asmop)) - }); - (proc, asmop_cow) - } - _ => (None, info.asmop.map(Cow::Borrowed)), - }, - }; + // Attempt to supply procedure context from the current span context, if needed + + // available + let (procedure, asmop) = match procedure { + proc @ Some(_) => (proc, info.asmop.map(Cow::Borrowed)), + None => match self.block_stack.last() { + Some(Some(span_ctx)) => { + let proc = + self.frames.get(span_ctx.frame_index).and_then(|f| f.procedure.clone()); + let asmop_cow = info.asmop.map(Cow::Borrowed).or_else(|| { + let context_name = proc.as_deref().unwrap_or("").to_string(); + let raw_asmop = AssemblyOp::new( + span_ctx.location.clone(), + context_name, + 1, + op.to_string(), + ); + Some(Cow::Owned(raw_asmop)) + }); + (proc, asmop_cow) + } + _ => (None, info.asmop.map(Cow::Borrowed)), + }, + }; - // Use the current frame's procedure context, if no other more precise context is - // available - let procedure = - procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone())); + // Use the current frame's procedure context, if no other more precise context is + // available + let procedure = procedure.or_else(|| self.frames.last().and_then(|f| f.procedure.clone())); - // Do we have a frame? If not, create one - if self.frames.is_empty() { - self.frames.push(CallFrame::new(procedure.clone())); - } + // Do we have a frame? If not, create one + if self.frames.is_empty() { + self.frames.push(CallFrame::new(procedure.clone())); + } - let current_frame = self.frames.last_mut().unwrap(); + let current_frame = self.frames.last_mut().unwrap(); - // Does the current frame have a procedure context/location? Use the one from this op if - // so - let procedure_context_updated = - current_frame.procedure.is_none() && procedure.is_some(); - if procedure_context_updated { - current_frame.procedure.clone_from(&procedure); - } + // Does the current frame have a procedure context/location? Use the one from this op if + // so + let procedure_context_updated = current_frame.procedure.is_none() && procedure.is_some(); + if procedure_context_updated { + current_frame.procedure.clone_from(&procedure); + } - // Push op into call frame if this is any op other than `nop` or frame setup - if !matches!(op, Operation::Noop) { - let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1); - current_frame.push(op, cycle_idx, asmop.as_deref()); - } + // Push op into call frame if this is any op other than `nop` or frame setup + if !matches!(op, Operation::Noop) { + let cycle_idx = info.asmop.map(|a| a.num_cycles()).unwrap_or(1); + current_frame.push(op, cycle_idx, asmop.as_deref()); + } - // Check if we should also update the caller frame's exec detail - let num_frames = self.frames.len(); - if procedure_context_updated && num_frames > 1 { - let caller_frame = &mut self.frames[num_frames - 2]; - if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut() - && callee.is_none() - { - *callee = procedure; - } + // Check if we should also update the caller frame's exec detail + let num_frames = self.frames.len(); + if procedure_context_updated && num_frames > 1 { + let caller_frame = &mut self.frames[num_frames - 2]; + if let Some(OpDetail::Exec { callee }) = caller_frame.context.back_mut() + && callee.is_none() + { + *callee = procedure; } } - None + popped_frame } // Get or cache procedure name/context as `Rc` @@ -203,6 +247,32 @@ impl CallFrame { } } + /// Build a frame from remote (DAP) data — used in DAP client mode. + /// + /// The frame stores the procedure name and an optional [ResolvedLocation] + /// as a pre-resolved `OpDetail::Full` entry so that `last_resolved()` and + /// `recent()` work correctly for pane rendering. + #[cfg(feature = "dap")] + pub fn from_remote(name: Option, resolved: Option) -> Self { + let procedure = name.map(|n| Rc::from(n.into_boxed_str())); + let mut context = VecDeque::new(); + if let Some(loc) = resolved { + let cell = OnceCell::new(); + cell.set(Some(loc)).ok(); + context.push_back(OpDetail::Full { + op: miden_processor::operation::Operation::Noop, + location: None, + resolved: cell, + }); + } + Self { + procedure, + context, + display_name: Default::default(), + finishing: false, + } + } + pub fn procedure(&self, strip_prefix: &str) -> Option> { self.procedure.as_ref()?; let name = self.display_name.get_or_init(|| { diff --git a/src/exec/config.rs b/src/exec/config.rs index 9ca4298..9293f13 100644 --- a/src/exec/config.rs +++ b/src/exec/config.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "tui")] use std::{ffi::OsStr, path::Path}; use miden_processor::{ExecutionOptions, StackInputs, advice::AdviceInputs}; diff --git a/src/exec/dap.rs b/src/exec/dap.rs new file mode 100644 index 0000000..466b217 --- /dev/null +++ b/src/exec/dap.rs @@ -0,0 +1,1372 @@ +use std::{ + io::{BufReader, BufWriter}, + net::TcpListener, + sync::{ + Arc, OnceLock, + atomic::{AtomicBool, Ordering}, + }, +}; + +use dap::prelude::*; +use miden_core::{ + Word, + operations::{AssemblyOp, DebugOptions}, + precompile::PrecompileTranscript, + program::Program, +}; +use miden_processor::{ + ExecutionError, ExecutionOptions, ExecutionOutput, FastProcessor, FutureMaybeSend, Host, + ProcessorState, ResumeContext, StackInputs, StackOutputs, TraceError, + advice::{AdviceInputs, AdviceMutation}, + event::EventError, + mast::MastForest, + trace::RowIndex, +}; + +use super::{ProgramExecutor, TraceEvent, state::extract_current_op}; +use crate::debug::{FormatType, ReadMemoryExpr}; + +// DAP CONFIG +// ================================================================================================ + +static DAP_CONFIG: OnceLock = OnceLock::new(); + +/// Configuration for the DAP debug server. +#[derive(Clone, Debug)] +pub struct DapConfig { + /// The address to listen on, e.g. "127.0.0.1:4711". + pub listen_addr: String, + /// Shared flag: set by the server when a Phase 2 restart (terminate-and-reconnect) is + /// requested, read by the caller to decide whether to recompile and re-enter. + restart_requested: Arc, +} + +impl DapConfig { + pub fn new(listen_addr: impl Into) -> Self { + Self { + listen_addr: listen_addr.into(), + restart_requested: Arc::new(AtomicBool::new(false)), + } + } + + /// Returns `true` if the server has signalled a Phase 2 restart. + pub fn restart_requested(&self) -> bool { + self.restart_requested.load(Ordering::Acquire) + } + + /// Clear the restart flag (called by the caller after acting on it). + pub fn reset_restart(&self) { + self.restart_requested.store(false, Ordering::Release); + } + + /// Set the global DAP configuration. Must be called before `execute_transaction()`. + /// + /// Only the first call takes effect; subsequent calls are ignored. + pub fn set_global(config: DapConfig) { + DAP_CONFIG.set(config).ok(); + } +} + +impl Default for DapConfig { + fn default() -> Self { + Self::new("127.0.0.1:4711") + } +} + +// DAP HOST WRAPPER +// ================================================================================================ + +/// A single frame in the DAP call stack. +#[derive(Debug, Clone)] +struct DapCallFrame { + name: String, + source_path: Option, + line: i64, + column: i64, +} + +/// A host wrapper that intercepts trace events to track the call stack for DAP stack traces, +/// while delegating all other operations to the inner host. +struct DapHostWrapper<'a, H: Host> { + inner: &'a mut H, + call_depth: usize, + frames: Vec, +} + +impl<'a, H: Host> DapHostWrapper<'a, H> { + fn new(inner: &'a mut H) -> Self { + Self { + inner, + call_depth: 0, + frames: Vec::new(), + } + } +} + +impl Host for DapHostWrapper<'_, H> { + fn get_label_and_source_file( + &self, + location: &miden_debug_types::Location, + ) -> (miden_debug_types::SourceSpan, Option>) { + self.inner.get_label_and_source_file(location) + } + + fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend>> { + self.inner.get_mast_forest(node_digest) + } + + fn on_event( + &mut self, + process: &ProcessorState<'_>, + ) -> impl FutureMaybeSend, EventError>> { + self.inner.on_event(process) + } + + fn on_debug( + &mut self, + process: &ProcessorState<'_>, + options: &DebugOptions, + ) -> Result<(), miden_processor::DebugError> { + self.inner.on_debug(process, options) + } + + fn on_trace(&mut self, process: &ProcessorState<'_>, trace_id: u32) -> Result<(), TraceError> { + let event = TraceEvent::from(trace_id); + match event { + TraceEvent::FrameStart => { + self.call_depth += 1; + self.frames.push(DapCallFrame { + name: String::new(), + source_path: None, + line: 0, + column: 0, + }); + } + TraceEvent::FrameEnd => { + self.call_depth = self.call_depth.saturating_sub(1); + self.frames.pop(); + } + _ => {} + } + self.inner.on_trace(process, trace_id) + } + + fn resolve_event( + &self, + event_id: miden_core::events::EventId, + ) -> Option<&miden_core::events::EventName> { + self.inner.resolve_event(event_id) + } +} + +// POLL IMMEDIATELY +// ================================================================================================ + +/// Resolve a future that is expected to complete immediately (synchronous host methods). +fn poll_immediately(fut: impl std::future::Future) -> T { + let waker = std::task::Waker::noop(); + let mut cx = std::task::Context::from_waker(waker); + let mut fut = std::pin::pin!(fut); + match fut.as_mut().poll(&mut cx) { + std::task::Poll::Ready(val) => val, + std::task::Poll::Pending => panic!("future was expected to complete immediately"), + } +} + +macro_rules! write_with_format_type { + ($out:ident, $read_expr:ident, $value:expr) => { + match $read_expr.format { + FormatType::Decimal => write!(&mut $out, "{}", $value).unwrap(), + FormatType::Hex => write!(&mut $out, "{:0x}", $value).unwrap(), + FormatType::Binary => write!(&mut $out, "{:0b}", $value).unwrap(), + } + }; +} + +fn read_memory_at_current_state( + processor: &mut FastProcessor, + cycle: usize, + expr: &ReadMemoryExpr, +) -> Result { + use core::fmt::Write; + + use miden_assembly_syntax::ast::types::Type; + + const U32_MASK: u64 = u32::MAX as u64; + + if expr.count > 1 { + return Err("-count with value > 1 is not yet implemented".into()); + } + + let cycle = RowIndex::from(u32::try_from(cycle).map_err(|_| "cycle value overflowed u32")?); + let ctx = processor.state().ctx(); + let mut output = String::new(); + + if matches!(expr.ty, Type::Felt) { + if !expr.addr.is_element_aligned() { + return Err("read failed: type 'felt' must be aligned to an element boundary".into()); + } + + let felt = processor + .memory() + .read_element(ctx, miden_processor::Felt::new(u64::from(expr.addr.addr))) + .ok() + .unwrap_or(miden_processor::Felt::ZERO); + write_with_format_type!(output, expr, felt.as_canonical_u64()); + return Ok(output); + } + + if matches!( + expr.ty, + Type::Array(ref array_ty) if array_ty.element_type() == &Type::Felt && array_ty.len() == 4 + ) { + if !expr.addr.is_word_aligned() { + return Err("read failed: type 'word' must be aligned to a word boundary".into()); + } + + let word = processor + .memory() + .read_word(ctx, miden_processor::Felt::new(u64::from(expr.addr.addr)), cycle) + .ok() + .unwrap_or_default(); + + output.push('['); + for (i, elem) in word.iter().enumerate() { + if i > 0 { + output.push_str(", "); + } + write_with_format_type!(output, expr, elem.as_canonical_u64()); + } + output.push(']'); + return Ok(output); + } + + if !expr.addr.is_element_aligned() { + return Err("invalid read: unaligned reads are not supported yet".into()); + } + + let mut elems = Vec::with_capacity(expr.ty.size_in_felts()); + for i in 0..expr.ty.size_in_felts() { + let addr = expr + .addr + .addr + .checked_add(u32::try_from(i).map_err(|_| "address overflow")?) + .ok_or_else(|| { + "invalid read: attempted to read beyond end of linear memory".to_string() + })?; + let felt = processor + .memory() + .read_element(ctx, miden_processor::Felt::new(u64::from(addr))) + .ok() + .unwrap_or_default(); + elems.push(felt); + } + + let mut bytes = Vec::with_capacity(expr.ty.size_in_bytes()); + let mut needed = expr.ty.size_in_bytes(); + for elem in elems { + let elem_bytes = ((elem.as_canonical_u64() & U32_MASK) as u32).to_le_bytes(); + let take = core::cmp::min(needed, 4); + bytes.extend(&elem_bytes[..take]); + needed -= take; + } + + match &expr.ty { + Type::I1 => match expr.format { + FormatType::Decimal => write!(&mut output, "{}", bytes[0] != 0).unwrap(), + FormatType::Hex => write!(&mut output, "{:#0x}", (bytes[0] != 0) as u8).unwrap(), + FormatType::Binary => write!(&mut output, "{:#0b}", (bytes[0] != 0) as u8).unwrap(), + }, + Type::I8 => write_with_format_type!(output, expr, bytes[0] as i8), + Type::U8 => write_with_format_type!(output, expr, bytes[0]), + Type::I16 => { + write_with_format_type!(output, expr, i16::from_le_bytes([bytes[0], bytes[1]])) + } + Type::U16 => { + write_with_format_type!(output, expr, u16::from_le_bytes([bytes[0], bytes[1]])) + } + Type::I32 => write_with_format_type!( + output, + expr, + i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) + ), + Type::U32 => write_with_format_type!( + output, + expr, + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) + ), + ty @ (Type::I64 | Type::U64) => { + let val = u64::from_le_bytes(bytes[..8].try_into().unwrap()); + if matches!(ty, Type::I64) { + write_with_format_type!(output, expr, val as i64); + } else { + write_with_format_type!(output, expr, val); + } + } + ty => return Err(format!("support for reads of type '{ty}' are not implemented yet")), + } + + Ok(output) +} + +fn build_ui_state( + processor: &mut FastProcessor, + host: &DapHostWrapper<'_, H>, + current_asmop: Option<&AssemblyOp>, + cycle: usize, +) -> crate::exec::DapUiState { + // Build callstack from the host's frame stack (bottom-to-top order, reversed so the + // top-of-stack / most-recent frame comes first — matching DAP convention). + let callstack: Vec = if host.frames.is_empty() { + // Fallback when no frames have been recorded yet. + let (name, source_path, line) = match current_asmop { + Some(asmop) => { + let loc = resolve_asmop_location(asmop, host); + let (source_path, line) = loc.map_or((None, 0), |(path, line)| (Some(path), line)); + (asmop.context_name().to_string(), source_path, line) + } + None => (format!("cycle {cycle}"), None, 0), + }; + vec![crate::exec::DapUiFrame { + name, + source_path, + line, + column: 0, + }] + } else { + host.frames + .iter() + .rev() + .map(|frame| crate::exec::DapUiFrame { + name: frame.name.clone(), + source_path: frame.source_path.clone(), + line: frame.line, + column: frame.column, + }) + .collect() + }; + + let current_stack = processor + .state() + .get_stack_state() + .iter() + .map(|felt| felt.as_canonical_u64()) + .collect(); + + crate::exec::DapUiState { + cycle, + current_stack, + callstack, + } +} + +// BREAKPOINT STORAGE +// ================================================================================================ + +/// A source breakpoint stored from a SetBreakpoints request. +#[derive(Debug, Clone)] +struct StoredBreakpoint { + /// The source file path as sent by the client. + path: String, + /// 1-indexed line number. + line: i64, +} + +/// A function/pattern breakpoint stored from a SetFunctionBreakpoints request. +/// +/// Matching is done in two ways: +/// 1. Glob pattern match against the full context name and source path +/// 2. Suffix match — the raw name is checked as a suffix of the context name +/// (e.g. `prologue::foo` matches `::$kernel::prologue::foo`) +#[derive(Debug, Clone)] +struct StoredFunctionBreakpoint { + /// The raw name string for suffix matching. + name: String, + /// Compiled glob pattern for matching. + pattern: glob::Pattern, +} + +// RESOLVE ASMOP LOCATION +// ================================================================================================ + +/// Resolve an `AssemblyOp`'s location to a (file_path, line_number) pair using the host. +fn resolve_asmop_location(asmop: &AssemblyOp, host: &H) -> Option<(String, i64)> { + let location = asmop.location()?; + let (span, source_file) = host.get_label_and_source_file(location); + let source_file = source_file?; + let file_line_col = source_file.location(span); + let path = file_line_col.uri.as_ref().to_string(); + let line = file_line_col.line.to_u32() as i64; + Some((path, line)) +} + +/// Update the top frame on the host's frame stack with the current asmop's name and location. +/// +/// If the frame stack is empty (e.g. before the first FrameStart trace event), a root frame +/// is pushed so there is always at least one frame visible in the stack trace. +fn update_top_frame(host: &mut DapHostWrapper<'_, H>, current_asmop: Option<&AssemblyOp>) { + let (name, source_path, line) = match current_asmop { + Some(asmop) => { + let loc = resolve_asmop_location(asmop, &*host); + let (source_path, line) = loc.map_or((None, 0), |(p, l)| (Some(p), l)); + (asmop.context_name().to_string(), source_path, line) + } + None => (String::new(), None, 0), + }; + + if host.frames.is_empty() { + host.frames.push(DapCallFrame { + name, + source_path, + line, + column: 0, + }); + } else if let Some(top) = host.frames.last_mut() { + top.name = name; + top.source_path = source_path; + top.line = line; + } +} + +// DAP EXECUTOR +// ================================================================================================ + +/// A [`ProgramExecutor`] that starts a DAP TCP server and steps through the program +/// interactively. +/// +/// When used with `TransactionExecutor`, instead of running to completion, `execute()` binds a TCP +/// listener and waits for a DAP client (e.g. VS Code) to connect. The client then controls +/// execution via standard DAP commands (continue, step, breakpoints, inspect stack/memory). +pub struct DapExecutor { + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + options: ExecutionOptions, + config: DapConfig, +} + +/// Variables reference IDs for scopes. +const SCOPE_STACK: i64 = 1; +const SCOPE_MEMORY: i64 = 2; + +impl ProgramExecutor for DapExecutor { + fn new( + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + options: ExecutionOptions, + ) -> Self { + let config = DAP_CONFIG.get().cloned().unwrap_or_default(); + DapExecutor { + stack_inputs, + advice_inputs, + options, + config, + } + } + + fn execute( + self, + program: &Program, + host: &mut H, + ) -> impl FutureMaybeSend> { + async move { self.run_dap_server(program, host) } + } +} + +impl DapExecutor { + fn run_dap_server( + self, + program: &Program, + host: &mut H, + ) -> Result { + // Clone inputs so they can be reused across restarts. + let stack_inputs = self.stack_inputs; + let advice_inputs = self.advice_inputs; + let options = self.options.with_debugging(true).with_tracing(true); + + // Bind TCP listener with SO_REUSEADDR to allow rebinding during Phase 2 restarts. + let listener = { + use std::net::ToSocketAddrs; + + use socket2::{Domain, Socket, Type}; + let addr: std::net::SocketAddr = self + .config + .listen_addr + .to_socket_addrs() + .unwrap_or_else(|e| { + panic!("invalid listen address '{}': {e}", self.config.listen_addr) + }) + .next() + .unwrap_or_else(|| { + panic!( + "listen address '{}' did not resolve to any address", + self.config.listen_addr + ) + }); + let socket = Socket::new(Domain::for_address(addr), Type::STREAM, None) + .unwrap_or_else(|e| panic!("failed to create socket: {e}")); + socket.set_reuse_address(true).ok(); + socket + .bind(&addr.into()) + .unwrap_or_else(|e| panic!("DAP server failed to bind to {addr}: {e}")); + socket + .listen(1) + .unwrap_or_else(|e| panic!("DAP server failed to listen on {addr}: {e}")); + let listener: TcpListener = socket.into(); + listener + }; + eprintln!( + "DAP server listening on {}. Waiting for client connection...", + self.config.listen_addr + ); + + // Accept one client connection (persists across restarts). + let (stream, addr) = + listener.accept().unwrap_or_else(|e| panic!("DAP server accept failed: {e}")); + eprintln!("DAP client connected from {addr}"); + + let reader = BufReader::new( + stream.try_clone().unwrap_or_else(|e| panic!("Failed to clone TCP stream: {e}")), + ); + let writer = BufWriter::new(stream); + + let mut server = Server::new(reader, writer); + let mut breakpoints: Vec = Vec::new(); + let mut function_breakpoints: Vec = Vec::new(); + let mut is_restart = false; + let restart_flag = self.config.restart_requested.clone(); + + // Outer restart loop — on restart, the DapHostWrapper borrow is dropped, + // a fresh FastProcessor is created, and the inner event loop re-enters. + // + // NOTE: The inner host `H` (e.g. from miden-client's transaction executor) + // retains mutations from prior execution. A restarted session may therefore + // diverge from a pristine fresh session. Phase 2 (terminate-and-reconnect) + // solves this by creating a completely fresh host. + loop { + let mut processor = FastProcessor::new(stack_inputs) + .with_advice(advice_inputs.clone()) + .with_options(options); + + let resume_ctx = processor.get_initial_resume_context(program)?; + let mut wrapper = DapHostWrapper::new(host); + + let mut resume_ctx = Some(resume_ctx); + let mut cycle: usize = 0; + let mut current_asmop: Option = None; + + // Extract initial asmop and populate the root frame. + if let Some(ctx) = resume_ctx.as_ref() { + let (_op, node_id, op_idx, _control) = extract_current_op(ctx); + current_asmop = node_id + .and_then(|nid| ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); + } + update_top_frame(&mut wrapper, current_asmop.as_ref()); + + // On restart, emit initial state immediately (no handshake needed). + if is_restart { + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + server + .send_event(Event::Stopped(events::StoppedEventBody { + reason: types::StoppedEventReason::Entry, + description: Some("Restarted at program entry".into()), + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .ok(); + } + + let mut restart_requested = false; + let mut phase2_requested = false; + + loop { + let req = match server.poll_request() { + Ok(Some(req)) => req, + Ok(None) => break, + Err(e) => { + eprintln!("DAP protocol error: {e:#?}"); + break; + } + }; + + match req.command { + // --- Handshake --- + Command::Initialize(_) => { + let caps = types::Capabilities { + supports_configuration_done_request: Some(true), + supports_stepping_granularity: Some(true), + supports_restart_request: Some(true), + supports_function_breakpoints: Some(true), + ..Default::default() + }; + let resp = req.success(ResponseBody::Initialize(caps)); + server.respond(resp).ok(); + server.send_event(Event::Initialized).ok(); + } + + Command::Launch(_) => { + server.respond(req.success(ResponseBody::Launch)).ok(); + } + + Command::ConfigurationDone => { + if let Ok(resp) = req.ack() { + server.respond(resp).ok(); + } + // Push initial UI state snapshot, then pause at entry point. + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + server + .send_event(Event::Stopped(events::StoppedEventBody { + reason: types::StoppedEventReason::Entry, + description: Some("Paused at program entry".into()), + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .ok(); + } + + Command::Restart(ref args) => { + let has_arguments = args.arguments.is_some(); + server.respond(req.success(ResponseBody::Restart)).ok(); + if has_arguments { + // Phase 2: terminate-and-reconnect for recompilation. + restart_flag.store(true, Ordering::Release); + server + .send_event(Event::Terminated(Some(events::TerminatedEventBody { + restart: Some(serde_json::Value::Bool(true)), + }))) + .ok(); + phase2_requested = true; + break; + } else { + // Phase 1: in-process reset (same program, same host). + restart_requested = true; + break; + } + } + + Command::Disconnect(_) => { + if let Ok(resp) = req.ack() { + server.respond(resp).ok(); + } + break; + } + + // --- Execution Control --- + Command::Continue(_) => { + let resp = + req.success(ResponseBody::Continue(responses::ContinueResponse { + all_threads_continued: Some(true), + })); + server.respond(resp).ok(); + + match step_until_breakpoint( + &mut processor, + &mut wrapper, + &mut resume_ctx, + &mut cycle, + &mut current_asmop, + &breakpoints, + &function_breakpoints, + ) { + StepResult::Stepped | StepResult::Breakpoint(_) => { + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + server + .send_event(Event::Stopped(events::StoppedEventBody { + reason: types::StoppedEventReason::Breakpoint, + description: Some("Hit breakpoint".into()), + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .ok(); + } + StepResult::Terminated => { + server.send_event(Event::Terminated(None)).ok(); + } + StepResult::Error(e) => { + server.send_event(Event::Terminated(None)).ok(); + return Err(e); + } + } + } + + Command::Next(_) => { + let resp = req.success(ResponseBody::Next); + server.respond(resp).ok(); + + match step_over( + &mut processor, + &mut wrapper, + &mut resume_ctx, + &mut cycle, + &mut current_asmop, + ) { + StepResult::Stepped | StepResult::Breakpoint(_) => { + if resume_ctx.is_none() { + server.send_event(Event::Terminated(None)).ok(); + } else { + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + send_stopped_step(&mut server); + } + } + StepResult::Terminated => { + server.send_event(Event::Terminated(None)).ok(); + } + StepResult::Error(e) => { + server.send_event(Event::Terminated(None)).ok(); + return Err(e); + } + } + } + + Command::StepIn(_) => { + let resp = req.success(ResponseBody::StepIn); + server.respond(resp).ok(); + + match step_one( + &mut processor, + &mut wrapper, + &mut resume_ctx, + &mut cycle, + &mut current_asmop, + ) { + StepResult::Stepped | StepResult::Breakpoint(_) => { + if resume_ctx.is_none() { + server.send_event(Event::Terminated(None)).ok(); + } else { + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + send_stopped_step(&mut server); + } + } + StepResult::Terminated => { + server.send_event(Event::Terminated(None)).ok(); + } + StepResult::Error(e) => { + server.send_event(Event::Terminated(None)).ok(); + return Err(e); + } + } + } + + Command::StepOut(_) => { + let resp = req.success(ResponseBody::StepOut); + server.respond(resp).ok(); + + match step_out( + &mut processor, + &mut wrapper, + &mut resume_ctx, + &mut cycle, + &mut current_asmop, + ) { + StepResult::Stepped | StepResult::Breakpoint(_) => { + if resume_ctx.is_none() { + server.send_event(Event::Terminated(None)).ok(); + } else { + send_ui_state_snapshot( + &mut server, + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + ); + send_stopped_step(&mut server); + } + } + StepResult::Terminated => { + server.send_event(Event::Terminated(None)).ok(); + } + StepResult::Error(e) => { + server.send_event(Event::Terminated(None)).ok(); + return Err(e); + } + } + } + + // --- State Inspection --- + Command::Threads => { + let resp = req.success(ResponseBody::Threads(responses::ThreadsResponse { + threads: vec![types::Thread { + id: 1, + name: "main".into(), + }], + })); + server.respond(resp).ok(); + } + + Command::StackTrace(ref _args) => { + // Build stack frames from the host's frame stack (reversed: top-of-stack + // first, matching DAP convention). + let frames: Vec = if wrapper.frames.is_empty() { + // Fallback when no frames have been recorded. + let (name, source, line) = if let Some(asmop) = current_asmop.as_ref() { + let loc = resolve_asmop_location(asmop, &wrapper); + let (path, line_num) = + loc.unwrap_or_else(|| ("".into(), 0)); + let source = types::Source { + name: Some( + path.rsplit('/').next().unwrap_or(&path).to_string(), + ), + path: Some(path), + ..Default::default() + }; + (asmop.context_name().to_string(), Some(source), line_num) + } else { + (format!("cycle {cycle}"), None, 0) + }; + vec![types::StackFrame { + id: 0, + name, + source, + line, + column: 0, + ..Default::default() + }] + } else { + wrapper + .frames + .iter() + .rev() + .enumerate() + .map(|(id, frame)| { + let source = + frame.source_path.as_ref().map(|path| types::Source { + name: Some( + path.rsplit('/').next().unwrap_or(path).to_string(), + ), + path: Some(path.clone()), + ..Default::default() + }); + types::StackFrame { + id: id as i64, + name: frame.name.clone(), + source, + line: frame.line, + column: frame.column, + ..Default::default() + } + }) + .collect() + }; + + let total = frames.len() as i64; + let resp = + req.success(ResponseBody::StackTrace(responses::StackTraceResponse { + stack_frames: frames, + total_frames: Some(total), + })); + server.respond(resp).ok(); + } + + Command::Scopes(ref _args) => { + let resp = req.success(ResponseBody::Scopes(responses::ScopesResponse { + scopes: vec![ + types::Scope { + name: "Operand Stack".into(), + variables_reference: SCOPE_STACK, + expensive: false, + ..Default::default() + }, + types::Scope { + name: "Memory".into(), + variables_reference: SCOPE_MEMORY, + expensive: false, + ..Default::default() + }, + ], + })); + server.respond(resp).ok(); + } + + Command::Variables(ref args) => { + let variables = match args.variables_reference { + SCOPE_STACK => { + let state = processor.state(); + let stack = state.get_stack_state(); + stack + .iter() + .enumerate() + .map(|(i, felt)| types::Variable { + name: format!("[{i}]"), + value: format!("{}", felt.as_canonical_u64()), + type_field: Some("Felt".into()), + variables_reference: 0, + ..Default::default() + }) + .collect() + } + SCOPE_MEMORY => { + let state = processor.state(); + let ctx = state.ctx(); + let mem = state.get_mem_state(ctx); + mem.iter() + .map(|(addr, felt)| { + let addr_u32: u32 = (*addr).into(); + types::Variable { + name: format!("0x{addr_u32:08x}"), + value: format!("{}", felt.as_canonical_u64()), + type_field: Some("Felt".into()), + variables_reference: 0, + ..Default::default() + } + }) + .collect() + } + _ => Vec::new(), + }; + + let resp = + req.success(ResponseBody::Variables(responses::VariablesResponse { + variables, + })); + server.respond(resp).ok(); + } + + // --- Breakpoints --- + Command::SetBreakpoints(ref args) => { + let source_path = args.source.path.clone().unwrap_or_default(); + breakpoints.retain(|bp| bp.path != source_path); + + let mut confirmed = Vec::new(); + if let Some(bps) = &args.breakpoints { + for sbp in bps { + breakpoints.push(StoredBreakpoint { + path: source_path.clone(), + line: sbp.line, + }); + confirmed.push(types::Breakpoint { + verified: true, + line: Some(sbp.line), + source: Some(types::Source { + path: Some(source_path.clone()), + ..Default::default() + }), + ..Default::default() + }); + } + } + + let resp = req.success(ResponseBody::SetBreakpoints( + responses::SetBreakpointsResponse { + breakpoints: confirmed, + }, + )); + server.respond(resp).ok(); + } + + Command::SetFunctionBreakpoints(ref args) => { + function_breakpoints.clear(); + let mut confirmed = Vec::new(); + for fbp in &args.breakpoints { + let verified = match glob::Pattern::new(&fbp.name) { + Ok(pattern) => { + function_breakpoints.push(StoredFunctionBreakpoint { + name: fbp.name.clone(), + pattern, + }); + true + } + Err(_) => false, + }; + confirmed.push(types::Breakpoint { + verified, + ..Default::default() + }); + } + + let resp = req.success(ResponseBody::SetFunctionBreakpoints( + responses::SetFunctionBreakpointsResponse { + breakpoints: confirmed, + }, + )); + server.respond(resp).ok(); + } + + // --- Evaluate (custom state query) --- + Command::Evaluate(ref args) if args.expression == "__miden_ui_state" => { + let state_json = serde_json::to_string(&build_ui_state( + &mut processor, + &wrapper, + current_asmop.as_ref(), + cycle, + )) + .expect("bundled DAP UI state should serialize"); + let resp = + req.success(ResponseBody::Evaluate(responses::EvaluateResponse { + result: state_json, + type_field: Some("json".into()), + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + })); + server.respond(resp).ok(); + } + + Command::Evaluate(ref args) + if args.expression.starts_with("__miden_read_memory ") => + { + let expr = args + .expression + .strip_prefix("__miden_read_memory ") + .expect("prefix checked above"); + + match expr.parse::().and_then(|expr| { + read_memory_at_current_state(&mut processor, cycle, &expr) + }) { + Ok(result) => { + let resp = req.success(ResponseBody::Evaluate( + responses::EvaluateResponse { + result, + type_field: Some("string".into()), + presentation_hint: None, + variables_reference: 0, + named_variables: None, + indexed_variables: None, + memory_reference: None, + }, + )); + server.respond(resp).ok(); + } + Err(err) => { + server.respond(req.error(&err)).ok(); + } + } + } + + Command::Evaluate(ref _args) => { + server.respond(req.error("Unsupported expression")).ok(); + } + + // --- Unhandled --- + _ => { + server.respond(req.error("Unsupported command")).ok(); + } + } + } + + if phase2_requested { + eprintln!("DAP Phase 2 restart: returning from execute() for recompilation..."); + return Ok(ExecutionOutput { + stack: StackOutputs::new(&[]).expect("empty stack outputs"), + advice: Default::default(), + memory: Default::default(), + final_pc_transcript: PrecompileTranscript::default(), + }); + } + + if restart_requested { + is_restart = true; + eprintln!("DAP restart requested. Resetting processor..."); + // wrapper is dropped here, releasing the &mut H borrow. + // The outer loop creates a fresh processor and wrapper. + continue; + } + + // Normal exit (disconnect or connection closed). + eprintln!("DAP session ended. Building execution output..."); + + // Run the program to completion if it hasn't finished. + if let Some(ctx) = resume_ctx { + let mut ctx = Some(ctx); + while let Some(resume) = ctx.take() { + match poll_immediately(processor.step(&mut wrapper, resume)) { + Ok(Some(new_ctx)) => { + ctx = Some(new_ctx); + } + Ok(None) => break, + Err(e) => return Err(e), + } + } + } + + // Build ExecutionOutput from the processor's final state. + let stack_top: Vec<_> = processor.stack_top().iter().rev().copied().collect(); + let stack = StackOutputs::new(&stack_top) + .unwrap_or_else(|_| StackOutputs::new(&[]).expect("empty stack outputs")); + + let (advice, memory, final_pc_transcript) = processor.into_parts(); + return Ok(ExecutionOutput { + stack, + advice, + memory, + final_pc_transcript, + }); + } // end outer restart loop + } +} + +// STEPPING HELPERS +// ================================================================================================ + +/// Execute a single VM step. +fn step_one( + processor: &mut FastProcessor, + host: &mut DapHostWrapper<'_, H>, + resume_ctx: &mut Option, + cycle: &mut usize, + current_asmop: &mut Option, +) -> StepResult { + let ctx = match resume_ctx.take() { + Some(ctx) => ctx, + None => return StepResult::Terminated, + }; + + match poll_immediately(processor.step(host, ctx)) { + Ok(Some(new_ctx)) => { + *cycle += 1; + let (_op, node_id, op_idx, _control) = extract_current_op(&new_ctx); + *current_asmop = node_id + .and_then(|nid| new_ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); + *resume_ctx = Some(new_ctx); + update_top_frame(host, current_asmop.as_ref()); + StepResult::Stepped + } + Ok(None) => { + *cycle += 1; + *current_asmop = None; + StepResult::Terminated + } + Err(e) => StepResult::Error(e), + } +} + +/// Step over: advance until the current assembly operation changes. +fn step_over( + processor: &mut FastProcessor, + host: &mut DapHostWrapper<'_, H>, + resume_ctx: &mut Option, + cycle: &mut usize, + current_asmop: &mut Option, +) -> StepResult { + let original_asmop = current_asmop.clone(); + + loop { + let ctx = match resume_ctx.take() { + Some(ctx) => ctx, + None => return StepResult::Terminated, + }; + + match poll_immediately(processor.step(host, ctx)) { + Ok(Some(new_ctx)) => { + *cycle += 1; + let (_op, node_id, op_idx, _control) = extract_current_op(&new_ctx); + *current_asmop = node_id + .and_then(|nid| new_ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); + *resume_ctx = Some(new_ctx); + + if *current_asmop != original_asmop { + update_top_frame(host, current_asmop.as_ref()); + return StepResult::Stepped; + } + } + Ok(None) => { + *cycle += 1; + *current_asmop = None; + return StepResult::Terminated; + } + Err(e) => return StepResult::Error(e), + } + } +} + +/// Step out: advance until call depth decreases. +fn step_out( + processor: &mut FastProcessor, + host: &mut DapHostWrapper<'_, H>, + resume_ctx: &mut Option, + cycle: &mut usize, + current_asmop: &mut Option, +) -> StepResult { + let target_depth = host.call_depth.saturating_sub(1); + + loop { + let ctx = match resume_ctx.take() { + Some(ctx) => ctx, + None => return StepResult::Terminated, + }; + + match poll_immediately(processor.step(host, ctx)) { + Ok(Some(new_ctx)) => { + *cycle += 1; + let (_op, node_id, op_idx, _control) = extract_current_op(&new_ctx); + *current_asmop = node_id + .and_then(|nid| new_ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); + *resume_ctx = Some(new_ctx); + + if host.call_depth <= target_depth { + update_top_frame(host, current_asmop.as_ref()); + return StepResult::Stepped; + } + } + Ok(None) => { + *cycle += 1; + *current_asmop = None; + return StepResult::Terminated; + } + Err(e) => return StepResult::Error(e), + } + } +} + +/// Continue: step until a breakpoint is hit or the program terminates. +fn step_until_breakpoint( + processor: &mut FastProcessor, + host: &mut DapHostWrapper<'_, H>, + resume_ctx: &mut Option, + cycle: &mut usize, + current_asmop: &mut Option, + breakpoints: &[StoredBreakpoint], + function_breakpoints: &[StoredFunctionBreakpoint], +) -> StepResult { + loop { + let ctx = match resume_ctx.take() { + Some(ctx) => ctx, + None => return StepResult::Terminated, + }; + + match poll_immediately(processor.step(host, ctx)) { + Ok(Some(new_ctx)) => { + *cycle += 1; + let (_op, node_id, op_idx, _control) = extract_current_op(&new_ctx); + *current_asmop = node_id + .and_then(|nid| new_ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); + *resume_ctx = Some(new_ctx); + + if let Some(asmop) = current_asmop.as_ref() { + let resolved = resolve_asmop_location(asmop, host); + + // Check line breakpoints + if let Some((ref path, line)) = resolved { + for bp in breakpoints { + if bp.line == line + && (path.ends_with(&bp.path) || bp.path.ends_with(path)) + { + update_top_frame(host, current_asmop.as_ref()); + return StepResult::Breakpoint(line); + } + } + } + + // Check function/pattern breakpoints — match against context name and + // source file path. Context names may have a leading `::` (absolute + // paths like `::prologue::foo`), so we also try matching without it. + if !function_breakpoints.is_empty() { + let context_name = asmop.context_name(); + let stripped_name = context_name.strip_prefix("::").unwrap_or(context_name); + for fbp in function_breakpoints { + // Match via glob pattern or suffix (e.g. "prologue::foo" + // matches "::$kernel::prologue::foo"). + if fbp.pattern.matches(context_name) + || fbp.pattern.matches(stripped_name) + || context_name.ends_with(&fbp.name) + || stripped_name.ends_with(&fbp.name) + { + update_top_frame(host, current_asmop.as_ref()); + let line = resolved.as_ref().map_or(0, |(_, l)| *l); + return StepResult::Breakpoint(line); + } + if let Some((ref path, line)) = resolved + && fbp.pattern.matches(path) + { + update_top_frame(host, current_asmop.as_ref()); + return StepResult::Breakpoint(line); + } + } + } + } + } + Ok(None) => { + *cycle += 1; + *current_asmop = None; + return StepResult::Terminated; + } + Err(e) => return StepResult::Error(e), + } + } +} + +/// Push a `miden/uiState` snapshot event to the DAP client. +/// +/// This custom event carries the bundled UI state (cycle, stack, callstack) so +/// the client can refresh without an extra evaluate round-trip. It is emitted +/// immediately before the standard `stopped` event because the DAP `stopped` +/// event itself does not carry VM state — it only signals that execution paused. +fn send_ui_state_snapshot( + server: &mut Server, + processor: &mut FastProcessor, + host: &DapHostWrapper<'_, H>, + current_asmop: Option<&AssemblyOp>, + cycle: usize, +) { + let ui_state = build_ui_state(processor, host, current_asmop, cycle); + if let Ok(json) = serde_json::to_value(&ui_state) { + server.send_event(Event::MidenUiState(json)).ok(); + } +} + +/// Send a "stopped(step)" event to the DAP client. +fn send_stopped_step(server: &mut Server) { + server + .send_event(Event::Stopped(events::StoppedEventBody { + reason: types::StoppedEventReason::Step, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: Some(true), + hit_breakpoint_ids: None, + })) + .ok(); +} + +/// Result of a stepping operation. +#[allow(dead_code)] +enum StepResult { + /// Successfully advanced one or more steps. + Stepped, + /// Hit a breakpoint at the given line. + Breakpoint(i64), + /// Program terminated normally. + Terminated, + /// An execution error occurred. + Error(ExecutionError), +} diff --git a/src/exec/dap_client.rs b/src/exec/dap_client.rs new file mode 100644 index 0000000..bff5a5c --- /dev/null +++ b/src/exec/dap_client.rs @@ -0,0 +1,400 @@ +//! A DAP protocol client for connecting to a remote DAP server. +//! +//! The `dap` crate provides a [`Server`] type for reading requests and sending responses, +//! but no client-side equivalent. This module implements a simple DAP client using the +//! same types for requests, responses, and events. + +use std::{ + io::{BufRead, BufReader, BufWriter, Read, Write}, + net::TcpStream, +}; + +use dap::{ + events::Event, + responses::{Response, ResponseBody}, + types, +}; + +use super::DapUiState; +use crate::debug::ReadMemoryExpr; + +/// Variables reference IDs for scopes (must match the DAP server). +pub const SCOPE_STACK: i64 = 1; +pub const SCOPE_MEMORY: i64 = 2; + +/// The reason the debuggee stopped after a step/continue command. +#[derive(Debug)] +pub enum DapStopReason { + /// The debuggee stopped (step, breakpoint, entry, etc.) and the server + /// pushed a bundled UI state snapshot via the custom `miden/uiState` event. + Stopped(DapUiState), + /// The debuggee terminated. + Terminated, + /// Phase 2: server signaled terminate-and-reconnect. + Restarting, +} + +/// A message received from the DAP server. +#[derive(Debug)] +enum DapMessage { + Response(Response), + Event(Event), +} + +/// A simple DAP protocol client that communicates over TCP. +pub struct DapClient { + reader: BufReader, + writer: BufWriter, + seq: i64, +} + +impl DapClient { + /// Connect to a DAP server at the given address (e.g. "127.0.0.1:4711"). + pub fn connect(addr: &str) -> Result { + let stream = TcpStream::connect(addr) + .map_err(|e| format!("failed to connect to DAP server at {addr}: {e}"))?; + let reader = BufReader::new( + stream.try_clone().map_err(|e| format!("failed to clone TCP stream: {e}"))?, + ); + let writer = BufWriter::new(stream); + Ok(Self { + reader, + writer, + seq: 0, + }) + } + + /// Perform the DAP handshake: Initialize + Launch + ConfigurationDone. + /// Waits for the initial Stopped(entry) event and returns the server-pushed + /// UI state snapshot. + pub fn handshake(&mut self) -> Result { + // Initialize + self.send_request( + "initialize", + serde_json::json!({ + "adapterID": "miden-debug-tui", + "clientName": "miden-debug TUI", + "linesStartAt1": true, + "columnsStartAt1": true, + }), + )?; + self.wait_for_response("initialize")?; + + // Launch + self.send_request("launch", serde_json::json!({}))?; + self.wait_for_response("launch")?; + + // ConfigurationDone + self.send_request("configurationDone", serde_json::json!({}))?; + self.wait_for_response("configurationDone")?; + + // Wait for Stopped(entry) event plus the pushed UI state snapshot. + match self.wait_for_stopped()? { + DapStopReason::Stopped(snapshot) => Ok(snapshot), + DapStopReason::Terminated => Err("server terminated before entry stop".into()), + DapStopReason::Restarting => Err("server requested restart during handshake".into()), + } + } + + /// Send a StepIn command and wait for a Stopped/Terminated event. + pub fn step_in(&mut self) -> Result { + self.send_request("stepIn", serde_json::json!({"threadId": 1}))?; + self.wait_for_response("stepIn")?; + self.wait_for_stopped() + } + + /// Send a Next (step over) command and wait for a Stopped/Terminated event. + pub fn step_over(&mut self) -> Result { + self.send_request("next", serde_json::json!({"threadId": 1}))?; + self.wait_for_response("next")?; + self.wait_for_stopped() + } + + /// Send a StepOut command and wait for a Stopped/Terminated event. + pub fn step_out(&mut self) -> Result { + self.send_request("stepOut", serde_json::json!({"threadId": 1}))?; + self.wait_for_response("stepOut")?; + self.wait_for_stopped() + } + + /// Send a Continue command and wait for a Stopped/Terminated event. + pub fn continue_(&mut self) -> Result { + self.send_request("continue", serde_json::json!({"threadId": 1}))?; + self.wait_for_response("continue")?; + self.wait_for_stopped() + } + + /// Query the current stack trace. + pub fn stack_trace(&mut self) -> Result, String> { + self.send_request("stackTrace", serde_json::json!({"threadId": 1}))?; + let resp = self.wait_for_response("stackTrace")?; + match resp.body { + Some(ResponseBody::StackTrace(st)) => Ok(st.stack_frames), + _ => Err("unexpected response to stackTrace".into()), + } + } + + /// Query variables for a given scope reference. + pub fn variables(&mut self, variables_reference: i64) -> Result, String> { + self.send_request( + "variables", + serde_json::json!({ + "variablesReference": variables_reference + }), + )?; + let resp = self.wait_for_response("variables")?; + match resp.body { + Some(ResponseBody::Variables(v)) => Ok(v.variables), + _ => Err("unexpected response to variables".into()), + } + } + + /// Evaluate a custom expression (e.g. "__miden_state"). + pub fn evaluate(&mut self, expression: &str) -> Result { + self.send_request( + "evaluate", + serde_json::json!({ + "expression": expression + }), + )?; + let resp = self.wait_for_response("evaluate")?; + match resp.body { + Some(ResponseBody::Evaluate(e)) => Ok(e.result), + _ => Err("unexpected response to evaluate".into()), + } + } + + /// Read memory from the remote debuggee via the DAP server. + pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result { + self.evaluate(&format!("__miden_read_memory {expr}")) + } + + /// Set breakpoints for a source file. + pub fn set_breakpoints(&mut self, path: &str, lines: &[i64]) -> Result<(), String> { + let breakpoints: Vec = + lines.iter().map(|&line| serde_json::json!({"line": line})).collect(); + self.send_request( + "setBreakpoints", + serde_json::json!({ + "source": {"path": path}, + "breakpoints": breakpoints, + }), + )?; + self.wait_for_response("setBreakpoints")?; + Ok(()) + } + + /// Set function breakpoints (matched as glob patterns against context names and file paths). + pub fn set_function_breakpoints(&mut self, names: &[String]) -> Result<(), String> { + let breakpoints: Vec = + names.iter().map(|name| serde_json::json!({"name": name})).collect(); + self.send_request( + "setFunctionBreakpoints", + serde_json::json!({ + "breakpoints": breakpoints, + }), + )?; + self.wait_for_response("setFunctionBreakpoints")?; + Ok(()) + } + + /// Send a Restart command and wait for a Stopped event (program restarted at entry). + /// + /// The server resets the processor to the beginning of the program with the same + /// inputs and re-emits `miden/uiState` + `Stopped(entry)`. + pub fn restart(&mut self) -> Result { + self.send_request("restart", serde_json::json!({}))?; + self.wait_for_response("restart")?; + self.wait_for_stopped() + } + + /// Send a Phase 2 restart command (with arguments) and wait for the server's response. + /// + /// The server will respond with `Terminated(restart=true)` and shut down so the caller + /// can recompile and reconnect. + pub fn restart_phase2(&mut self) -> Result { + self.send_request("restart", serde_json::json!({"arguments": {}}))?; + self.wait_for_response("restart")?; + self.wait_for_stopped() + } + + /// Connect to a DAP server with exponential backoff, for Phase 2 reconnection. + /// + /// Polls with delays from 50ms up to 1s, timing out after `timeout`. + pub fn connect_with_retry(addr: &str, timeout: std::time::Duration) -> Result { + let start = std::time::Instant::now(); + let mut delay = std::time::Duration::from_millis(50); + loop { + match TcpStream::connect(addr) { + Ok(stream) => { + let reader = BufReader::new( + stream + .try_clone() + .map_err(|e| format!("failed to clone TCP stream: {e}"))?, + ); + let writer = BufWriter::new(stream); + return Ok(Self { + reader, + writer, + seq: 0, + }); + } + Err(_) if start.elapsed() < timeout => { + std::thread::sleep(delay); + delay = (delay * 2).min(std::time::Duration::from_secs(1)); + } + Err(e) => { + return Err(format!( + "failed to reconnect to {addr} after {:.1}s: {e}", + timeout.as_secs_f64() + )); + } + } + } + } + + /// Disconnect from the DAP server. + pub fn disconnect(&mut self) -> Result<(), String> { + self.send_request("disconnect", serde_json::json!({}))?; + // Best-effort: try to read response but don't fail if connection closes + let _ = self.wait_for_response("disconnect"); + Ok(()) + } + + // --- Internal helpers --- + + /// Send a DAP request with Content-Length framing. + fn send_request(&mut self, command: &str, arguments: serde_json::Value) -> Result<(), String> { + self.seq += 1; + let msg = serde_json::json!({ + "seq": self.seq, + "command": command, + "arguments": arguments, + }); + let body = serde_json::to_string(&msg).map_err(|e| format!("serialize error: {e}"))?; + write!(self.writer, "Content-Length: {}\r\n\r\n{}", body.len(), body) + .map_err(|e| format!("write error: {e}"))?; + self.writer.flush().map_err(|e| format!("flush error: {e}"))?; + Ok(()) + } + + /// Read a single DAP message from the stream (Content-Length framing). + fn read_message(&mut self) -> Result { + // Read headers. Skip any blank lines before the header (the server + // appends `\r\n` after each JSON body, which may appear before the + // next message's Content-Length header). + let mut content_length: usize = 0; + loop { + let mut line = String::new(); + self.reader.read_line(&mut line).map_err(|e| format!("read error: {e}"))?; + let trimmed = line.trim(); + if trimmed.is_empty() { + if content_length > 0 { + // Empty line after a header — end of headers + break; + } + // Empty line before any header — skip (trailing \r\n from previous message) + continue; + } + if let Some(val) = trimmed.strip_prefix("Content-Length:") { + content_length = val + .trim() + .parse::() + .map_err(|e| format!("invalid Content-Length: {e}"))?; + } + } + + if content_length == 0 { + return Err("missing or zero Content-Length header".into()); + } + + // Read content body + let mut buf = vec![0u8; content_length]; + self.reader.read_exact(&mut buf).map_err(|e| format!("read body error: {e}"))?; + let content = std::str::from_utf8(&buf).map_err(|e| format!("invalid utf-8: {e}"))?; + + // The server wraps everything in a BaseMessage: { seq, type, ... } + // We parse the "type" field to determine if it's a response or event. + let raw: serde_json::Value = + serde_json::from_str(content).map_err(|e| format!("JSON parse error: {e}"))?; + + match raw.get("type").and_then(|t| t.as_str()) { + Some("response") => { + let resp: Response = serde_json::from_value(raw) + .map_err(|e| format!("response parse error: {e}"))?; + Ok(DapMessage::Response(resp)) + } + Some("event") => { + let event: Event = + serde_json::from_value(raw).map_err(|e| format!("event parse error: {e}"))?; + Ok(DapMessage::Event(event)) + } + other => Err(format!("unexpected message type: {other:?}")), + } + } + + /// Wait for a response to a specific command, discarding events along the way. + fn wait_for_response(&mut self, _command: &str) -> Result { + loop { + match self.read_message()? { + DapMessage::Response(resp) => { + if !resp.success { + let msg = resp + .message + .as_ref() + .map(|m| format!("{m:?}")) + .unwrap_or_else(|| "unknown error".into()); + return Err(format!("DAP error: {msg}")); + } + return Ok(resp); + } + DapMessage::Event(_) => { + // Skip events while waiting for response + continue; + } + } + } + } + + /// Wait for a Stopped or Terminated event, capturing the server-pushed + /// `miden/uiState` snapshot that arrives before the stop signal. + /// + /// The server emits the custom `MidenUiState` event immediately before + /// the standard `Stopped` event, so this loop naturally captures it. + fn wait_for_stopped(&mut self) -> Result { + let mut snapshot: Option = None; + loop { + match self.read_message()? { + DapMessage::Event(Event::Stopped(_)) => { + let snapshot = snapshot + .expect("server must emit miden/uiState before stopped; this is a bug"); + return Ok(DapStopReason::Stopped(snapshot)); + } + DapMessage::Event(Event::Terminated(body)) => { + let is_restart = body + .as_ref() + .and_then(|b| b.restart.as_ref()) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if is_restart { + return Ok(DapStopReason::Restarting); + } + return Ok(DapStopReason::Terminated); + } + DapMessage::Event(Event::MidenUiState(value)) => { + snapshot = Some( + serde_json::from_value::(value) + .map_err(|e| format!("invalid miden/uiState payload: {e}"))?, + ); + } + _ => continue, + } + } + } +} + +impl Drop for DapClient { + fn drop(&mut self) { + let _ = self.disconnect(); + } +} diff --git a/src/exec/dap_types.rs b/src/exec/dap_types.rs new file mode 100644 index 0000000..f9c7a2d --- /dev/null +++ b/src/exec/dap_types.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +/// A bundled snapshot of the remote state needed by the TUI. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DapUiState { + pub cycle: usize, + pub current_stack: Vec, + pub callstack: Vec, +} + +/// A single remote frame within a bundled UI-state snapshot. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DapUiFrame { + pub name: String, + pub source_path: Option, + pub line: i64, + pub column: i64, +} diff --git a/src/exec/diagnostic.rs b/src/exec/diagnostic.rs new file mode 100644 index 0000000..603e814 --- /dev/null +++ b/src/exec/diagnostic.rs @@ -0,0 +1,180 @@ +use std::{sync::Arc, vec::Vec}; + +use miden_core::{Word, operations::DebugOptions, program::Program}; +use miden_processor::{ + ExecutionError, ExecutionOptions, ExecutionOutput, FastProcessor, Felt, FutureMaybeSend, Host, + ProcessorState, StackInputs, TraceError, + advice::{AdviceInputs, AdviceMutation}, + event::EventError, + mast::MastForest, + trace::RowIndex, +}; + +use super::{ProgramExecutor, TraceEvent}; + +// DIAGNOSTIC HOST WRAPPER +// ================================================================================================ + +/// A host wrapper that intercepts trace events to track call frames and processor state, +/// while delegating all other operations to the inner host. +/// +/// This enables capturing diagnostic information during transaction execution (or any program +/// execution) without modifying the inner host. +struct DiagnosticHostWrapper<'a, H: Host> { + inner: &'a mut H, + /// Call depth tracked from FrameStart/FrameEnd trace events. + call_depth: usize, + /// Stack state captured at the last trace or event callback. + last_stack_state: Vec, + /// Clock cycle at the last trace or event callback. + last_cycle: RowIndex, +} + +impl<'a, H: Host> DiagnosticHostWrapper<'a, H> { + fn new(inner: &'a mut H) -> Self { + Self { + inner, + call_depth: 0, + last_stack_state: Vec::new(), + last_cycle: RowIndex::from(0u32), + } + } + + /// Report diagnostic information when an execution error occurs. + fn report_diagnostics(&self, err: &ExecutionError) { + eprintln!("\n=== Transaction Execution Failed ==="); + eprintln!("Error: {err}"); + eprintln!("Last known cycle: {}", self.last_cycle); + eprintln!("Call depth at failure: {}", self.call_depth); + + if !self.last_stack_state.is_empty() { + let stack_display: Vec<_> = + self.last_stack_state.iter().take(16).map(|f| f.as_canonical_u64()).collect(); + eprintln!("Last known stack state (top 16): {stack_display:?}"); + } + + eprintln!("====================================\n"); + } + + fn capture_state(&mut self, process: &ProcessorState<'_>) { + self.last_stack_state = process.get_stack_state(); + self.last_cycle = process.clock(); + } +} + +impl Host for DiagnosticHostWrapper<'_, H> { + fn get_label_and_source_file( + &self, + location: &miden_debug_types::Location, + ) -> (miden_debug_types::SourceSpan, Option>) { + self.inner.get_label_and_source_file(location) + } + + fn get_mast_forest(&self, node_digest: &Word) -> impl FutureMaybeSend>> { + self.inner.get_mast_forest(node_digest) + } + + fn on_event( + &mut self, + process: &ProcessorState<'_>, + ) -> impl FutureMaybeSend, EventError>> { + self.capture_state(process); + self.inner.on_event(process) + } + + fn on_debug( + &mut self, + process: &ProcessorState<'_>, + options: &DebugOptions, + ) -> Result<(), miden_processor::DebugError> { + self.inner.on_debug(process, options) + } + + fn on_trace(&mut self, process: &ProcessorState<'_>, trace_id: u32) -> Result<(), TraceError> { + self.capture_state(process); + + let event = TraceEvent::from(trace_id); + match event { + TraceEvent::FrameStart => self.call_depth += 1, + TraceEvent::FrameEnd => self.call_depth = self.call_depth.saturating_sub(1), + _ => {} + } + + self.inner.on_trace(process, trace_id) + } + + fn resolve_event( + &self, + event_id: miden_core::events::EventId, + ) -> Option<&miden_core::events::EventName> { + self.inner.resolve_event(event_id) + } +} + +// DIAGNOSTIC EXECUTOR +// ================================================================================================ + +/// A [`ProgramExecutor`] that wraps [`FastProcessor`] with diagnostic capabilities. +/// +/// When execution fails, it captures and reports rich diagnostic information including: +/// - The clock cycle at failure +/// - The call depth (from trace events) +/// - The last known operand stack state +/// +/// This executor is intended for use with [`TransactionExecutor`] to provide better error +/// diagnostics when transactions fail during testing or development. +/// +/// # Usage +/// +/// ```ignore +/// use miden_tx::TransactionExecutor; +/// use miden_debug::DiagnosticExecutor; +/// +/// let executor = TransactionExecutor::new(&store) +/// .with_program_executor::() +/// .execute_transaction(account_id, block_num, notes, tx_args) +/// .await; +/// ``` +pub struct DiagnosticExecutor { + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + options: ExecutionOptions, +} + +impl ProgramExecutor for DiagnosticExecutor { + fn new( + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + options: ExecutionOptions, + ) -> Self { + DiagnosticExecutor { + stack_inputs, + advice_inputs, + options, + } + } + + fn execute( + self, + program: &Program, + host: &mut H, + ) -> impl FutureMaybeSend> { + async move { + // Enable debugging and tracing for richer diagnostics. + let options = self.options.with_debugging(true).with_tracing(true); + let processor = FastProcessor::new(self.stack_inputs) + .with_advice(self.advice_inputs) + .with_options(options); + + let mut wrapper = DiagnosticHostWrapper::new(host); + + match processor.execute(program, &mut wrapper).await { + Ok(output) => Ok(output), + Err(err) => { + wrapper.report_diagnostics(&err); + Err(err) + } + } + } + } +} diff --git a/src/exec/executor.rs b/src/exec/executor.rs index 15bc612..d702d96 100644 --- a/src/exec/executor.rs +++ b/src/exec/executor.rs @@ -16,8 +16,9 @@ use miden_mast_package::{ }; use miden_processor::{ ContextId, ExecutionError, ExecutionOptions, FastProcessor, Felt, - advice::AdviceInputs, + advice::{AdviceInputs, AdviceMutation}, event::{EventHandler, EventName}, + mast::MastForest, trace::RowIndex, }; @@ -208,6 +209,76 @@ impl Executor { } } + /// Convert this [Executor] into a [DebugExecutor] with event replay support. + /// + /// Like [`into_debug`](Self::into_debug), but additionally: + /// - Loads `extra_forests` into the host's MAST forest store + /// - Sets the event replay queue so that `on_event()` returns pre-recorded mutations + /// + /// This is used for transaction debugging where events were recorded during a prior + /// execution with the real transaction host. + pub fn into_debug_with_replay( + mut self, + program: &Program, + source_manager: Arc, + extra_forests: Vec>, + event_replay: VecDeque>, + ) -> DebugExecutor { + log::debug!("creating debug executor with event replay"); + + let mut host = DebuggerHost::new(source_manager.clone()); + for lib in core::mem::take(&mut self.libraries) { + host.load_mast_forest(lib.mast_forest().clone()); + } + for forest in extra_forests { + host.load_mast_forest(forest); + } + host.set_event_replay(event_replay); + + let trace_events: Rc>> = Rc::new(Default::default()); + let frame_start_events = Rc::clone(&trace_events); + host.register_trace_handler(TraceEvent::FrameStart, move |clk, event| { + frame_start_events.borrow_mut().insert(clk, event); + }); + let frame_end_events = Rc::clone(&trace_events); + host.register_trace_handler(TraceEvent::FrameEnd, move |clk, event| { + frame_end_events.borrow_mut().insert(clk, event); + }); + let assertion_events = Rc::clone(&trace_events); + host.register_assert_failed_tracer(move |clk, event| { + assertion_events.borrow_mut().insert(clk, event); + }); + + let mut processor = FastProcessor::new(self.stack) + .with_advice(self.advice) + .with_options(self.options) + .with_debugging(true) + .with_tracing(true); + + let root_context = ContextId::root(); + let resume_ctx = processor + .get_initial_resume_context(program) + .expect("failed to get initial resume context"); + + let callstack = CallStack::new(trace_events); + DebugExecutor { + processor, + host, + resume_ctx: Some(resume_ctx), + current_stack: vec![], + current_op: None, + current_asmop: None, + stack_outputs: Default::default(), + contexts: Default::default(), + root_context, + current_context: root_context, + callstack, + recent: VecDeque::with_capacity(5), + cycle: 0, + stopped: false, + } + } + /// Execute the given program until termination, producing a trace pub fn capture_trace( self, diff --git a/src/exec/host.rs b/src/exec/host.rs index c32e84f..288817e 100644 --- a/src/exec/host.rs +++ b/src/exec/host.rs @@ -1,4 +1,8 @@ -use std::{collections::BTreeMap, num::NonZeroU32, sync::Arc}; +use std::{ + collections::{BTreeMap, VecDeque}, + num::NonZeroU32, + sync::Arc, +}; use miden_assembly::SourceManager; use miden_core::{ @@ -26,6 +30,7 @@ pub struct DebuggerHost { tracing_callbacks: BTreeMap>>, on_assert_failed: Option>, source_manager: Arc, + event_replay: VecDeque>, } impl DebuggerHost where @@ -39,9 +44,19 @@ where tracing_callbacks: Default::default(), on_assert_failed: None, source_manager, + event_replay: VecDeque::new(), } } + /// Set the event replay queue. + /// + /// When non-empty, `on_event()` will pop mutations from this queue instead of + /// returning empty results. This is used for transaction debugging where events + /// were recorded during a prior execution. + pub fn set_event_replay(&mut self, events: VecDeque>) { + self.event_replay = events; + } + /// Register a trace handler for `event` pub fn register_trace_handler(&mut self, event: TraceEvent, callback: F) where @@ -108,6 +123,11 @@ where &mut self, process: &ProcessorState<'_>, ) -> impl FutureMaybeSend, EventError>> { + if !self.event_replay.is_empty() { + let mutations = self.event_replay.pop_front().unwrap_or_default(); + return std::future::ready(Ok(mutations)); + } + let event_id = EventId::from_felt(process.get_stack_item(0)); let result = match self.event_handlers.handle_event(event_id, process) { Ok(Some(mutations)) => Ok(mutations), diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 9a865db..cc04b89 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -1,12 +1,29 @@ mod config; +#[cfg(feature = "dap")] +mod dap; +#[cfg(feature = "dap")] +mod dap_client; +#[cfg(feature = "dap")] +mod dap_types; +mod diagnostic; mod executor; mod host; mod state; mod trace; mod trace_event; +mod tx_executor; +#[cfg(feature = "dap")] +pub use self::dap::{DapConfig, DapExecutor}; +#[cfg(feature = "dap")] +pub use self::dap_client::{DapClient, DapStopReason, SCOPE_MEMORY, SCOPE_STACK}; +#[cfg(feature = "dap")] +pub use self::dap_types::{DapUiFrame, DapUiState}; +#[doc(hidden)] +pub use self::tx_executor::ProgramExecutor; pub use self::{ config::ExecutionConfig, + diagnostic::DiagnosticExecutor, executor::Executor, host::DebuggerHost, state::DebugExecutor, diff --git a/src/exec/state.rs b/src/exec/state.rs index f311164..c1ece65 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -10,7 +10,7 @@ use miden_processor::{ }; use super::{DebuggerHost, ExecutionTrace}; -use crate::debug::{CallFrame, CallStack, StepInfo}; +use crate::debug::{CallFrame, CallStack, ControlFlowOp, StepInfo}; /// Resolve a future that is expected to complete immediately (synchronous host methods). /// @@ -70,9 +70,9 @@ pub struct DebugExecutor { /// Extract the current operation and assembly info from the continuation stack /// before a step is executed. This lets us know what operation will run next. -fn extract_current_op( +pub(crate) fn extract_current_op( ctx: &ResumeContext, -) -> (Option, Option, Option) { +) -> (Option, Option, Option, Option) { let forest = ctx.current_forest(); for cont in ctx.continuation_stack().iter_continuations_for_next_clock() { match cont { @@ -90,20 +90,47 @@ fn extract_current_op( } global_idx += op_idx_in_batch; let op = block.op_batches()[*batch_index].ops().get(*op_idx_in_batch).copied(); - return (op, Some(*node_id), Some(global_idx)); + return (op, Some(*node_id), Some(global_idx), None); + } + } + Continuation::Respan { + node_id, + batch_index, + } => { + let node = &forest[*node_id]; + if let MastNode::Block(block) = node { + let mut global_idx = 0; + for batch in &block.op_batches()[..*batch_index] { + global_idx += batch.ops().len(); + } + return (None, Some(*node_id), Some(global_idx), Some(ControlFlowOp::Respan)); } } Continuation::StartNode(node_id) => { - return (None, Some(*node_id), None); + let control = match &forest[*node_id] { + MastNode::Block(_) => Some(ControlFlowOp::Span), + MastNode::Join(_) => Some(ControlFlowOp::Join), + MastNode::Split(_) => Some(ControlFlowOp::Split), + _ => None, + }; + return (None, Some(*node_id), None, control); + } + Continuation::FinishBasicBlock(_) + | Continuation::FinishJoin(_) + | Continuation::FinishSplit(_) + | Continuation::FinishLoop { .. } + | Continuation::FinishCall(_) + | Continuation::FinishDyn(_) + | Continuation::FinishExternal(_) => { + return (None, None, None, Some(ControlFlowOp::End)); } - Continuation::FinishBasicBlock(_) => return (None, None, None), other if other.increments_clk() => { - return (None, None, None); + return (None, None, None, None); } _ => continue, } } - (None, None, None) + (None, None, None, None) } impl DebugExecutor { @@ -127,7 +154,7 @@ impl DebugExecutor { }; // Before step: peek continuation to determine what will execute - let (op, node_id, op_idx) = extract_current_op(&resume_ctx); + let (op, node_id, op_idx, control) = extract_current_op(&resume_ctx); let asmop = node_id .and_then(|nid| resume_ctx.current_forest().get_assembly_op(nid, op_idx).cloned()); @@ -161,6 +188,7 @@ impl DebugExecutor { // Update call stack let step_info = StepInfo { op, + control, asmop: self.current_asmop.as_ref(), clk: RowIndex::from(self.cycle as u32), ctx: self.current_context, diff --git a/src/exec/trace.rs b/src/exec/trace.rs index 25d4f2b..2f398f7 100644 --- a/src/exec/trace.rs +++ b/src/exec/trace.rs @@ -1,5 +1,5 @@ use miden_core::Word; -use miden_processor::{ContextId, FastProcessor, Felt, StackOutputs, trace::RowIndex}; +use miden_processor::{ContextId, FastProcessor, Felt, StackInputs, StackOutputs, trace::RowIndex}; use smallvec::SmallVec; use super::TraceEvent; @@ -30,6 +30,18 @@ pub struct ExecutionTrace { } impl ExecutionTrace { + /// Create an empty [ExecutionTrace] with no memory and no outputs. + /// + /// Used in DAP client mode where no local execution trace is available. + pub fn empty() -> Self { + Self { + root_context: ContextId::root(), + last_cycle: RowIndex::from(0u32), + processor: FastProcessor::new(StackInputs::default()), + outputs: StackOutputs::default(), + } + } + /// Parse the program outputs on the operand stack as a value of type `T` pub fn parse_result(&self) -> Option where diff --git a/src/exec/tx_executor.rs b/src/exec/tx_executor.rs new file mode 100644 index 0000000..1f82246 --- /dev/null +++ b/src/exec/tx_executor.rs @@ -0,0 +1 @@ +pub use miden_tx::ProgramExecutor; diff --git a/src/lib.rs b/src/lib.rs index 486f83e..84f88ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,21 @@ +pub use miden_debug_engine::{debug, exec, felt}; + mod config; -mod debug; -mod exec; -mod felt; mod input; mod linker; -#[cfg(test)] -mod test_utils; +#[cfg(feature = "tui")] +mod logger; +#[cfg(feature = "tui")] +mod ui; + +#[cfg(feature = "tui")] +pub use self::ui::{DebugMode, State, run, run_with_state}; pub use self::{ + config::{ColorChoice, DebuggerConfig}, debug::*, exec::*, felt::{Felt, FromMidenRepr, ToMidenRepr, bytes_to_words, push_wasm_ty_to_operand_stack}, + input::InputFile, linker::{LibraryKind, LinkLibrary}, }; diff --git a/src/main.rs b/src/main.rs index 0be56bd..dc2f633 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,10 @@ -#![allow(unused)] -mod config; -mod debug; -mod exec; -mod felt; -mod input; -mod linker; -mod logger; -mod ui; - use std::env; use clap::Parser; use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report, WrapErr}; +use miden_debug::{DebuggerConfig, run}; +#[cfg(feature = "dap")] +use miden_debug::{State, run_with_state}; pub fn main() -> Result<(), Report> { setup_diagnostics(); @@ -37,7 +30,7 @@ pub fn main() -> Result<(), Report> { } let logger = Box::new(builder.build()); - let mut config = Box::new(config::DebuggerConfig::parse()); + let mut config = Box::new(DebuggerConfig::parse()); if config.working_dir.is_none() { let cwd = env::current_dir() @@ -47,7 +40,13 @@ pub fn main() -> Result<(), Report> { config.working_dir = Some(cwd); } - ui::run(config, logger) + #[cfg(feature = "dap")] + if let Some(addr) = config.dap_connect.as_ref() { + let state = State::new_for_dap(addr)?; + return run_with_state(state, logger); + } + + run(config, logger) } fn setup_diagnostics() { diff --git a/src/ui/app.rs b/src/ui/app.rs index be4d3de..11a0892 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -40,8 +40,17 @@ pub struct App { pub type KeyBindings = HashMap, Action>>; impl App { + #[allow(dead_code)] pub async fn new(config: Box) -> Result { let state = State::new(config)?; + Self::from_state(state).await + } + + /// Create an [App] from a pre-built [State]. + /// + /// This is used for transaction debugging where the state is constructed + /// externally with pre-recorded event replay data. + pub async fn from_state(state: State) -> Result { let home = Home::new()?; Ok(Self { pages: vec![Box::new(home)], diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 70b3b08..568eb63 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,15 +9,28 @@ mod tui; use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report}; +pub use self::state::{DebugMode, State}; use self::{action::Action, app::App}; use crate::config::DebuggerConfig; +#[allow(dead_code)] pub fn run(config: Box, logger: Box) -> Result<(), Report> { let mut builder = tokio::runtime::Builder::new_current_thread(); let rt = builder.enable_all().build().into_diagnostic()?; rt.block_on(async move { start_ui(config, logger).await }) } +/// Launch the TUI debugger with a pre-built [State]. +/// +/// This is the programmatic entry point used by transaction debugging, where +/// the caller constructs a [State] with pre-recorded event replay data. +pub fn run_with_state(state: State, logger: Box) -> Result<(), Report> { + let mut builder = tokio::runtime::Builder::new_current_thread(); + let rt = builder.enable_all().build().into_diagnostic()?; + rt.block_on(async move { start_ui_with_state(state, logger).await }) +} + +#[allow(dead_code)] pub async fn start_ui( config: Box, logger: Box, @@ -38,3 +51,21 @@ pub async fn start_ui( Ok(()) } + +async fn start_ui_with_state(state: State, logger: Box) -> Result<(), Report> { + use ratatui::crossterm as term; + + crate::logger::DebugLogger::install(logger); + + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let _ = term::terminal::disable_raw_mode(); + let _ = term::execute!(std::io::stdout(), term::terminal::LeaveAlternateScreen); + original_hook(panic_info); + })); + + let mut app = App::from_state(state).await?; + app.run().await?; + + Ok(()) +} diff --git a/src/ui/pages/home.rs b/src/ui/pages/home.rs index 3530c83..ee728aa 100644 --- a/src/ui/pages/home.rs +++ b/src/ui/pages/home.rs @@ -161,24 +161,38 @@ impl Page for Home { } } Action::Continue => { - let start_cycle = state.executor.cycle; + // DAP client mode: send step commands over the network + #[cfg(feature = "dap")] + if state.debug_mode == crate::ui::state::DebugMode::Remote { + self.step_remote(state, &mut actions)?; + // Send any pending actions + if let Some(tx) = &mut self.command_tx { + actions.into_iter().flatten().for_each(|action| { + tx.send(action).ok(); + }); + } + return Ok(None); + } + + let start_cycle = state.executor().cycle; + let start_asmop = state.executor().current_asmop.clone(); let mut breakpoints = core::mem::take(&mut state.breakpoints); state.stopped = false; let stopped = loop { // If stepping the program results in the program terminating succesfully, stop - if state.executor.stopped { + if state.executor().stopped { break true; } let mut consume_most_recent_finish = false; - match state.executor.step() { + match state.executor_mut().step() { Ok(Some(exited)) if exited.should_break_on_exit() => { consume_most_recent_finish = true; } Ok(_) => (), Err(err) => { // Execution terminated with an error - state.execution_failed = Some(err); + state.set_execution_failed(err); break true; } } @@ -189,14 +203,14 @@ impl Page for Home { } let (_op, is_op_boundary, proc, loc) = { - let op = state.executor.current_op; + let op = state.executor().current_op; let is_boundary = state - .executor + .executor() .current_asmop .as_ref() .map(|_info| true) .unwrap_or(false); - let (proc, loc) = match state.executor.callstack.current_frame() { + let (proc, loc) = match state.executor().callstack.current_frame() { Some(frame) => { let loc = frame .recent() @@ -211,7 +225,7 @@ impl Page for Home { }; // Remove all breakpoints triggered at this cycle - let current_cycle = state.executor.cycle; + let current_cycle = state.executor().cycle; let cycles_stepped = current_cycle - start_cycle; breakpoints.retain_mut(|bp| { if let Some(n) = bp.cycles_to_skip(current_cycle) { @@ -231,6 +245,7 @@ impl Page for Home { if cycles_stepped > 0 && is_op_boundary && matches!(&bp.ty, BreakpointType::Next) + && state.executor().current_asmop != start_asmop { state.breakpoints_hit.push(core::mem::take(bp)); return false; @@ -289,8 +304,8 @@ impl Page for Home { state.stopped = stopped; // Report program termination to the user - if stopped && state.executor.stopped { - if let Some(err) = state.execution_failed.as_ref() { + if stopped && state.executor().stopped { + if let Some(err) = state.execution_failed() { actions.push(Some(Action::StatusLine(err.to_string()))); } else { actions.push(Some(Action::StatusLine( @@ -371,24 +386,24 @@ impl Page for Home { EventResponse::Stop(Action::Continue) } // Only step if we're stopped, and execution has not terminated - KeyCode::Char('s') if state.stopped && !state.executor.stopped => { + KeyCode::Char('s') if state.stopped && !state.executor().stopped => { state.create_breakpoint(BreakpointType::Step); state.stopped = false; EventResponse::Stop(Action::Continue) } // Only step-next if we're stopped, and execution has not terminated - KeyCode::Char('n') if state.stopped && !state.executor.stopped => { + KeyCode::Char('n') if state.stopped && !state.executor().stopped => { state.create_breakpoint(BreakpointType::Next); state.stopped = false; EventResponse::Stop(Action::Continue) } // Only resume execution if we're stopped, and execution has not terminated - KeyCode::Char('c') if state.stopped && !state.executor.stopped => { + KeyCode::Char('c') if state.stopped && !state.executor().stopped => { state.stopped = false; EventResponse::Stop(Action::Continue) } // Do not try to continue if execution has terminated, but warn user - KeyCode::Char('c' | 's' | 'n') if state.stopped && state.executor.stopped => { + KeyCode::Char('c' | 's' | 'n') if state.stopped && state.executor().stopped => { EventResponse::Stop(Action::TimedStatusLine( "program has terminated, cannot continue".to_string(), 3, @@ -440,3 +455,46 @@ impl Page for Home { Ok(()) } } + +#[cfg(feature = "dap")] +impl Home { + /// Handle stepping in DAP remote mode. + /// + /// Determines which DAP command to send based on the current breakpoints, + /// sends it, waits for the response, and updates the TUI state. + fn step_remote( + &mut self, + state: &mut State, + actions: &mut Vec>, + ) -> Result<(), Report> { + use crate::exec::DapStopReason; + let result = state.step_remote(); + + match result { + Ok(DapStopReason::Stopped(_)) => state.stopped = true, + Ok(DapStopReason::Terminated) => { + state.executor_mut().stopped = true; + state.stopped = true; + actions.push(Some(Action::StatusLine("program terminated successfully".into()))); + } + Ok(DapStopReason::Restarting) => { + state.stopped = true; + actions.push(Some(Action::StatusLine( + "server signaled Phase 2 restart during step".into(), + ))); + } + Err(e) => { + state.executor_mut().stopped = true; + state.stopped = true; + actions.push(Some(Action::StatusLine(format!("DAP error: {e}")))); + } + } + + // Update panes with latest state + for pane in self.panes.iter_mut() { + actions.push(pane.update(Action::Update, state)?); + } + + Ok(()) + } +} diff --git a/src/ui/panes/breakpoints.rs b/src/ui/panes/breakpoints.rs index 0be2c78..cd855b8 100644 --- a/src/ui/panes/breakpoints.rs +++ b/src/ui/panes/breakpoints.rs @@ -52,7 +52,7 @@ impl Pane for BreakpointsPane { } fn init(&mut self, state: &State) -> Result<(), Report> { - self.breakpoint_cycle = state.executor.cycle; + self.breakpoint_cycle = state.executor().cycle; self.breakpoints_hit.clear(); self.breakpoint_selected = None; Ok(()) @@ -106,7 +106,7 @@ impl Pane for BreakpointsPane { self.init(state)?; } Action::Update => { - if self.breakpoint_cycle < state.executor.cycle { + if self.breakpoint_cycle < state.executor().cycle { self.breakpoints_hit.clear(); self.breakpoints_hit.append(&mut state.breakpoints_hit); if let Some(prev) = self.breakpoint_selected @@ -115,7 +115,7 @@ impl Pane for BreakpointsPane { self.breakpoint_selected = None; } } - self.breakpoint_cycle = state.executor.cycle; + self.breakpoint_cycle = state.executor().cycle; } _ => {} } @@ -142,7 +142,7 @@ impl Pane for BreakpointsPane { let user_breakpoints_hit = self.breakpoints_hit.iter().filter(|bp| !bp.is_internal()).count(); - let fg = Color::White; + let fg = Color::Gray; let bg = Color::Black; let yellow = Color::Yellow; let gray = Color::Gray; diff --git a/src/ui/panes/disasm.rs b/src/ui/panes/disasm.rs index 9c3e7a8..b51bf27 100644 --- a/src/ui/panes/disasm.rs +++ b/src/ui/panes/disasm.rs @@ -57,16 +57,16 @@ impl Pane for DisassemblyPane { } fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> { - let (current_proc, lines) = match state.executor.callstack.current_frame() { + let (current_proc, lines) = match state.executor().callstack.current_frame() { None => { let proc = Line::from("in ").right_aligned(); ( proc, state - .executor + .executor() .recent .iter() - .map(|op| Line::from(vec![Span::styled(format!(" | {op}"), Color::White)])) + .map(|op| Line::from(vec![Span::styled(format!(" | {op}"), Color::Gray)])) .collect::>(), ) } @@ -84,7 +84,7 @@ impl Pane for DisassemblyPane { .map(|op| { Line::from(vec![Span::styled( format!(" | {}", &op.display()), - Color::White, + Color::Gray, )]) }) .collect::>(), @@ -97,7 +97,7 @@ impl Pane for DisassemblyPane { .block(Block::default().borders(Borders::ALL)) .highlight_symbol(symbols::scrollbar::HORIZONTAL.end) .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); let mut list_state = ListState::default().with_selected(Some(selected_line)); frame.render_stateful_widget(list, area, &mut list_state); @@ -110,7 +110,7 @@ impl Pane for DisassemblyPane { .title_bottom(current_proc) .title( Line::styled( - format!(" at cycle {}", state.executor.cycle), + format!(" at cycle {}", state.executor().cycle), Style::default().add_modifier(Modifier::ITALIC), ) .right_aligned(), diff --git a/src/ui/panes/source_code.rs b/src/ui/panes/source_code.rs index 6d1a12f..baec576 100644 --- a/src/ui/panes/source_code.rs +++ b/src/ui/panes/source_code.rs @@ -104,7 +104,7 @@ impl SourceCodePane { /// Get the [ResolvedLocation] for the current state fn current_location(&self, state: &State) -> Option { - match state.executor.callstack.current_frame() { + match state.executor().callstack.current_frame() { Some(frame) => { let resolved = frame.last_resolved(&state.source_manager); resolved.cloned() @@ -203,7 +203,7 @@ impl SourceCodePane { self.selected_line = 0; self.current_file = None; - if let Some(frame) = state.executor.callstack.current_frame() + if let Some(frame) = state.executor().callstack.current_frame() && let Some(loc) = frame.last_resolved(&state.source_manager) { self.current_file = Some(self.highlight_file(loc)); @@ -248,7 +248,7 @@ impl Pane for SourceCodePane { fn init(&mut self, state: &State) -> Result<(), Report> { self.enable_syntax_highlighting(state); - if let Some(frame) = state.executor.callstack.current_frame() + if let Some(frame) = state.executor().callstack.current_frame() && let Some(loc) = frame.last_resolved(&state.source_manager) { self.current_file = Some(self.highlight_file(loc)); diff --git a/src/ui/panes/stack.rs b/src/ui/panes/stack.rs index 38d5771..a2895ba 100644 --- a/src/ui/panes/stack.rs +++ b/src/ui/panes/stack.rs @@ -1,5 +1,4 @@ use miden_assembly_syntax::diagnostics::Report; -use miden_core::field::PrimeField64; use ratatui::{ prelude::*, widgets::{block::*, *}, @@ -58,16 +57,16 @@ impl Pane for OperandStackPane { } fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> { - let lines: Vec> = if state.executor.current_stack.is_empty() { + let lines: Vec> = if state.executor().current_stack.is_empty() { vec![] } else { state - .executor + .executor() .current_stack .iter() .rev() .map(|item| { - Line::from(Span::styled(format!(" {}", item.as_canonical_u64()), Color::White)) + Line::from(Span::styled(format!(" {}", item.as_canonical_u64()), Color::Gray)) }) .collect() }; @@ -78,7 +77,7 @@ impl Pane for OperandStackPane { .block(Block::default().borders(Borders::ALL)) .highlight_symbol(symbols::scrollbar::HORIZONTAL.end) .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); let mut list_state = ListState::default().with_selected(Some(selected_line)); frame.render_stateful_widget(list, area, &mut list_state); diff --git a/src/ui/panes/stacktrace.rs b/src/ui/panes/stacktrace.rs index 5e89094..f9e790e 100644 --- a/src/ui/panes/stacktrace.rs +++ b/src/ui/panes/stacktrace.rs @@ -58,8 +58,8 @@ impl Pane for StackTracePane { fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> { let mut lines = Vec::default(); - let num_frames = state.executor.callstack.frames().len(); - for (i, frame) in state.executor.callstack.frames().iter().enumerate() { + let num_frames = state.executor().callstack.frames().len(); + for (i, frame) in state.executor().callstack.frames().iter().enumerate() { let is_top = i + 1 == num_frames; let mut parts = vec![]; /* @@ -69,7 +69,7 @@ impl Pane for StackTracePane { Span::styled(" |-> ", Color::Gray) }; */ - let gutter = Span::styled(" ", Color::White); + let gutter = Span::styled(" ", Color::Gray); parts.push(gutter); let name = frame.procedure(""); let name = name.as_deref().unwrap_or("").to_string(); @@ -78,7 +78,7 @@ impl Pane for StackTracePane { } else { Span::styled( name, - Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::BOLD), + Style::default().fg(Color::Cyan).bg(Color::Black).add_modifier(Modifier::BOLD), ) }; parts.push(name); @@ -131,7 +131,7 @@ impl Pane for StackTracePane { .block(Block::default().borders(Borders::ALL)) .highlight_symbol(symbols::scrollbar::HORIZONTAL.end) .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); let mut list_state = ListState::default().with_selected(Some(selected_line)); frame.render_stateful_widget(list, area, &mut list_state); diff --git a/src/ui/state.rs b/src/ui/state.rs index 0340344..36690ca 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -1,12 +1,13 @@ -use std::sync::Arc; +use std::{collections::VecDeque, sync::Arc}; use miden_assembly::{DefaultSourceManager, SourceManager}; use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report}; -use miden_core::{ - field::{PrimeCharacteristicRing, PrimeField64}, - serde::Deserializable, +use miden_core::{program::Program, serde::Deserializable}; +use miden_processor::{ + Felt, StackInputs, + advice::{AdviceInputs, AdviceMutation}, + mast::MastForest, }; -use miden_processor::{Felt, StackInputs}; use crate::{ config::DebuggerConfig, @@ -15,18 +16,51 @@ use crate::{ input::InputFile, }; +/// Whether the debugger is debugging a plain program or a transaction. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DebugMode { + /// Debugging a plain MASM program loaded from a package. + Program, + /// Debugging a Miden transaction with pre-recorded event replay. + Transaction, + /// Debugging remotely via a DAP server connection. + Remote, +} + +fn clone_advice_mutation(mutation: &AdviceMutation) -> AdviceMutation { + match mutation { + AdviceMutation::ExtendStack { values } => AdviceMutation::ExtendStack { + values: values.clone(), + }, + AdviceMutation::ExtendMap { other } => AdviceMutation::ExtendMap { + other: other.clone(), + }, + AdviceMutation::ExtendMerkleStore { infos } => AdviceMutation::ExtendMerkleStore { + infos: infos.clone(), + }, + AdviceMutation::ExtendPrecompileRequests { data } => { + AdviceMutation::ExtendPrecompileRequests { data: data.clone() } + } + } +} + +fn clone_event_replay_queue(event_replay: &[Vec]) -> VecDeque> { + event_replay + .iter() + .map(|batch| batch.iter().map(clone_advice_mutation).collect()) + .collect() +} + pub struct State { - pub package: Arc, pub source_manager: Arc, pub config: Box, - pub executor: DebugExecutor, - pub execution_trace: ExecutionTrace, - pub execution_failed: Option, pub input_mode: InputMode, pub breakpoints: Vec, pub breakpoints_hit: Vec, pub next_breakpoint_id: u8, pub stopped: bool, + pub debug_mode: DebugMode, + session: SessionState, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] @@ -38,7 +72,193 @@ pub enum InputMode { Command, } +struct LocalState { + executor: DebugExecutor, + execution_trace: ExecutionTrace, + execution_failed: Option, +} + +#[cfg(feature = "dap")] +struct RemoteState { + client: crate::exec::DapClient, + executor: DebugExecutor, + addr: String, + /// Tracks which source files have had breakpoints synced to the DAP server, + /// so we can send empty breakpoint lists when all breakpoints for a file are removed. + synced_bp_files: std::collections::BTreeSet, +} + +enum SessionState { + Local(Box), + #[cfg(feature = "dap")] + Remote(Box), +} + +#[cfg(feature = "dap")] +struct RemoteSnapshot { + callstack: crate::debug::CallStack, + current_stack: Vec, + cycle: usize, +} + +#[cfg(feature = "dap")] +impl RemoteState { + fn connect(addr: &str, source_manager: &Arc) -> Result { + use std::collections::BTreeSet; + + use miden_processor::{ContextId, FastProcessor}; + + use crate::exec::DebuggerHost; + + let mut client = crate::exec::DapClient::connect(addr).map_err(Report::msg)?; + let ui_state = client.handshake().map_err(Report::msg)?; + let snapshot = convert_ui_state(&ui_state, source_manager); + + let processor = FastProcessor::new(StackInputs::default()); + let host = DebuggerHost::new(source_manager.clone()); + let executor = DebugExecutor { + processor, + host, + resume_ctx: None, + current_stack: snapshot.current_stack, + current_op: None, + current_asmop: None, + stack_outputs: Default::default(), + contexts: BTreeSet::new(), + root_context: ContextId::root(), + current_context: ContextId::root(), + callstack: snapshot.callstack, + recent: VecDeque::new(), + cycle: snapshot.cycle, + stopped: false, + }; + + Ok(Self { + client, + executor, + addr: addr.to_string(), + synced_bp_files: std::collections::BTreeSet::new(), + }) + } + + fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result { + self.client.read_memory(expr) + } + + fn sync_breakpoints(&mut self, breakpoints: &[Breakpoint]) { + use std::collections::BTreeMap; + + // Group Line breakpoints by their file pattern string. + let mut by_file: BTreeMap> = BTreeMap::new(); + // Collect Called and File patterns as function breakpoints. + let mut func_names: Vec = Vec::new(); + + for bp in breakpoints { + match &bp.ty { + BreakpointType::Line { pattern, line } => { + by_file.entry(pattern.as_str().to_string()).or_default().push(*line as i64); + } + BreakpointType::Called(pattern) | BreakpointType::File(pattern) => { + func_names.push(pattern.as_str().to_string()); + } + _ => {} + } + } + + // Send empty breakpoint lists for files that were previously synced but no longer have + // breakpoints. + let stale_files: Vec = self + .synced_bp_files + .iter() + .filter(|f| !by_file.contains_key(f.as_str())) + .cloned() + .collect(); + for file in &stale_files { + let _ = self.client.set_breakpoints(file, &[]); + } + + // Send breakpoints for each file. + for (file, lines) in &by_file { + let _ = self.client.set_breakpoints(file, lines); + } + + // Send function/pattern breakpoints (replaces the full set each time). + let _ = self.client.set_function_breakpoints(&func_names); + + // Update tracked set. + self.synced_bp_files = by_file.into_keys().collect(); + } + + fn resume(&mut self, breakpoints: &[Breakpoint]) -> Result { + // Sync user-defined breakpoints to the DAP server before choosing a step command. + self.sync_breakpoints(breakpoints); + + let has_step = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Step)); + let has_next = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Next)); + let has_finish = breakpoints.iter().any(|bp| matches!(bp.ty, BreakpointType::Finish)); + + if has_step { + self.client.step_in() + } else if has_next { + self.client.step_over() + } else if has_finish { + self.client.step_out() + } else { + self.client.continue_() + } + } + + fn refresh_executor( + &mut self, + source_manager: &Arc, + pushed: &crate::exec::DapUiState, + ) { + // Standard DAP `stopped` events tell us execution paused, but do not + // carry the refreshed VM state (stack, callstack, cycle). The server + // pushes a custom `miden/uiState` event with the bundled snapshot + // immediately before each `stopped` event, so we consume that here + // instead of issuing an extra evaluate round-trip. + let snapshot = convert_ui_state(pushed, source_manager); + self.executor.current_stack = snapshot.current_stack; + self.executor.callstack = snapshot.callstack; + self.executor.cycle = snapshot.cycle; + } + + fn reconnect(&mut self, source_manager: &Arc) -> Result<(), Report> { + let timeout = std::time::Duration::from_secs(30); + let mut new_client = + crate::exec::DapClient::connect_with_retry(&self.addr, timeout).map_err(Report::msg)?; + let ui_state = new_client.handshake().map_err(Report::msg)?; + let snapshot = convert_ui_state(&ui_state, source_manager); + + self.client = new_client; + self.executor.current_stack = snapshot.current_stack; + self.executor.callstack = snapshot.callstack; + self.executor.cycle = snapshot.cycle; + Ok(()) + } +} + impl State { + fn new_local( + source_manager: Arc, + config: Box, + debug_mode: DebugMode, + local: LocalState, + ) -> Self { + Self { + source_manager, + config, + input_mode: InputMode::Normal, + breakpoints: vec![], + breakpoints_hit: vec![], + next_breakpoint_id: 0, + stopped: true, + debug_mode, + session: SessionState::Local(Box::new(local)), + } + } + pub fn new(config: Box) -> Result { let source_manager = Arc::new(DefaultSourceManager::default()); let mut inputs = config.inputs.clone().unwrap_or_default(); @@ -90,22 +310,100 @@ impl State { let execution_trace = trace_executor.capture_trace(&program, source_manager.clone()); - Ok(Self { - package, + Ok(Self::new_local( source_manager, config, - executor, - execution_trace, - execution_failed: None, - input_mode: InputMode::Normal, - breakpoints: vec![], - breakpoints_hit: vec![], - next_breakpoint_id: 0, - stopped: true, - }) + DebugMode::Program, + LocalState { + executor, + execution_trace, + execution_failed: None, + }, + )) + } + + /// Create a new debugger state for transaction debugging. + /// + /// This uses pre-recorded event mutations to replay host events during + /// step-by-step debugging, since the debugger's host doesn't have access + /// to the real transaction host. + pub fn new_for_transaction( + program: Arc, + stack_inputs: StackInputs, + advice_inputs: AdviceInputs, + source_manager: Arc, + mast_forests: Vec>, + event_replay: Vec>, + ) -> Result { + let args = stack_inputs.iter().copied().rev().collect::>(); + + // Create debug executor with event replay + let mut executor = Executor::new(args.clone()); + executor.with_advice_inputs(advice_inputs.clone()); + let debug_executor = executor.into_debug_with_replay( + &program, + source_manager.clone(), + mast_forests.clone(), + clone_event_replay_queue(&event_replay), + ); + + // Create trace executor with a cloned replay queue + let mut trace_executor = Executor::new(args); + trace_executor.with_advice_inputs(advice_inputs); + let trace_debug = trace_executor.into_debug_with_replay( + &program, + source_manager.clone(), + mast_forests, + clone_event_replay_queue(&event_replay), + ); + + // Run trace executor to completion to capture execution trace + let execution_trace = run_to_trace(trace_debug); + + Ok(Self::new_local( + source_manager, + Box::default(), + DebugMode::Transaction, + LocalState { + executor: debug_executor, + execution_trace, + execution_failed: None, + }, + )) } pub fn reload(&mut self) -> Result<(), Report> { + if self.debug_mode == DebugMode::Transaction { + return Err(Report::msg("reload is not supported in transaction debug mode")); + } + if self.debug_mode == DebugMode::Remote { + #[cfg(feature = "dap")] + { + let source_manager = self.source_manager.clone(); + let SessionState::Remote(remote) = &mut self.session else { + return Err(Report::msg("no remote debug session")); + }; + let result = remote.client.restart_phase2().map_err(Report::msg)?; + match result { + crate::exec::DapStopReason::Restarting => { + remote.reconnect(&source_manager)?; + } + crate::exec::DapStopReason::Stopped(snapshot) => { + // Fallback: server treated it as Phase 1. + remote.refresh_executor(&source_manager, &snapshot); + } + crate::exec::DapStopReason::Terminated => { + return Err(Report::msg("server terminated without restart signal")); + } + } + self.breakpoints_hit.clear(); + self.stopped = true; + return Ok(()); + } + #[cfg(not(feature = "dap"))] + return Err(Report::msg("remote debug mode requires the `dap` feature")); + } + log::debug!("reloading program"); let package = load_package(&self.config)?; @@ -155,10 +453,11 @@ impl State { trace_executor.with_advice_inputs(core::mem::take(&mut inputs.advice_inputs)); let execution_trace = trace_executor.capture_trace(&program, self.source_manager.clone()); - self.package = package; - self.executor = executor; - self.execution_trace = execution_trace; - self.execution_failed = None; + self.session = SessionState::Local(Box::new(LocalState { + executor, + execution_trace, + execution_failed: None, + })); self.breakpoints_hit.clear(); let breakpoints = core::mem::take(&mut self.breakpoints); self.breakpoints.reserve(breakpoints.len()); @@ -172,10 +471,10 @@ impl State { pub fn create_breakpoint(&mut self, ty: BreakpointType) { let id = self.next_breakpoint_id(); - let creation_cycle = self.executor.cycle; + let creation_cycle = self.executor().cycle; log::trace!("created breakpoint with id {id} at cycle {creation_cycle}"); if matches!(ty, BreakpointType::Finish) - && let Some(frame) = self.executor.callstack.current_frame_mut() + && let Some(frame) = self.executor_mut().callstack.current_frame_mut() { frame.break_on_exit(); } @@ -206,6 +505,48 @@ impl State { break candidate; } } + + pub fn executor(&self) -> &DebugExecutor { + match &self.session { + SessionState::Local(local) => &local.executor, + #[cfg(feature = "dap")] + SessionState::Remote(remote) => &remote.executor, + } + } + + pub fn executor_mut(&mut self) -> &mut DebugExecutor { + match &mut self.session { + SessionState::Local(local) => &mut local.executor, + #[cfg(feature = "dap")] + SessionState::Remote(remote) => &mut remote.executor, + } + } + + pub fn execution_failed(&self) -> Option<&miden_processor::ExecutionError> { + match &self.session { + SessionState::Local(local) => local.execution_failed.as_ref(), + #[cfg(feature = "dap")] + SessionState::Remote(_) => None, + } + } + + pub fn set_execution_failed(&mut self, error: miden_processor::ExecutionError) { + match &mut self.session { + SessionState::Local(local) => local.execution_failed = Some(error), + #[cfg(feature = "dap")] + SessionState::Remote(_) => { + panic!("cannot record local execution failure while in remote mode") + } + } + } + + fn local_session(&self) -> &LocalState { + match &self.session { + SessionState::Local(local) => local, + #[cfg(feature = "dap")] + SessionState::Remote(_) => panic!("local session requested while in remote mode"), + } + } } macro_rules! write_with_format_type { @@ -219,15 +560,29 @@ macro_rules! write_with_format_type { } impl State { - pub fn read_memory(&self, expr: &ReadMemoryExpr) -> Result { + pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result { use core::fmt::Write; use miden_assembly_syntax::ast::types::Type; use crate::debug::FormatType; - let cycle = miden_processor::trace::RowIndex::from(self.executor.cycle); - let context = self.executor.current_context; + #[cfg(feature = "dap")] + if self.debug_mode == DebugMode::Remote { + let SessionState::Remote(remote) = &mut self.session else { + return Err("no remote debug session".into()); + }; + return remote.read_memory(expr); + } + + #[cfg(not(feature = "dap"))] + if self.debug_mode == DebugMode::Remote { + return Err("remote debug mode requires the `dap` feature".into()); + } + + let cycle = miden_processor::trace::RowIndex::from(self.executor().cycle); + let context = self.executor().current_context; + let local = self.local_session(); let mut output = String::new(); if expr.count > 1 { return Err("-count with value > 1 is not yet implemented".into()); @@ -237,7 +592,7 @@ impl State { "read failed: type 'felt' must be aligned to an element boundary".into() ); } - let felt = self + let felt = local .execution_trace .read_memory_element_in_context(expr.addr.addr, context, cycle) .unwrap_or(Felt::ZERO); @@ -249,7 +604,7 @@ impl State { if !expr.addr.is_word_aligned() { return Err("read failed: type 'word' must be aligned to a word boundary".into()); } - let word = self.execution_trace.read_memory_word(expr.addr.addr).unwrap_or_default(); + let word = local.execution_trace.read_memory_word(expr.addr.addr).unwrap_or_default(); output.push('['); for (i, elem) in word.iter().enumerate() { if i > 0 { @@ -259,7 +614,7 @@ impl State { } output.push(']'); } else { - let bytes = self + let bytes = local .execution_trace .read_bytes_for_type(expr.addr, &expr.ty, context, cycle) .map_err(|err| format!("invalid read: {err}"))?; @@ -311,6 +666,116 @@ impl State { } } +// DAP CLIENT MODE +// ================================================================================================ + +#[cfg(feature = "dap")] +impl State { + /// Create a new debugger state for remote DAP debugging. + /// + /// Connects to a DAP server, performs the handshake, and queries the + /// initial state to populate the executor fields that the TUI panes read. + pub fn new_for_dap(addr: &str) -> Result { + let source_manager: Arc = Arc::new(DefaultSourceManager::default()); + let remote = RemoteState::connect(addr, &source_manager)?; + + Ok(Self { + source_manager, + config: Box::default(), + input_mode: InputMode::Normal, + breakpoints: vec![], + breakpoints_hit: vec![], + next_breakpoint_id: 0, + stopped: true, + debug_mode: DebugMode::Remote, + session: SessionState::Remote(Box::new(remote)), + }) + } + + pub fn step_remote(&mut self) -> Result { + let source_manager = self.source_manager.clone(); + let SessionState::Remote(remote) = &mut self.session else { + return Err(Report::msg("no remote debug session")); + }; + let result = remote.resume(&self.breakpoints).map_err(Report::msg)?; + + self.breakpoints.retain(|bp| !bp.is_one_shot()); + + match &result { + crate::exec::DapStopReason::Stopped(snapshot) => { + remote.refresh_executor(&source_manager, snapshot); + self.stopped = true; + } + crate::exec::DapStopReason::Terminated => { + remote.executor.stopped = true; + self.stopped = true; + } + crate::exec::DapStopReason::Restarting => { + return Err(Report::msg("unexpected Phase 2 restart signal during step")); + } + } + + Ok(result) + } +} + +/// Convert a server-pushed [`DapUiState`](crate::exec::DapUiState) snapshot into a +/// [`RemoteSnapshot`] that the TUI executor can consume. +#[cfg(feature = "dap")] +fn convert_ui_state( + snapshot: &crate::exec::DapUiState, + source_manager: &Arc, +) -> RemoteSnapshot { + use crate::debug::{CallFrame, CallStack}; + + let call_frames: Vec = snapshot + .callstack + .iter() + .map(|frame| { + let resolved = resolve_remote_frame(frame, source_manager); + CallFrame::from_remote(Some(frame.name.clone()), resolved) + }) + .collect(); + + let current_stack = snapshot.current_stack.iter().copied().map(Felt::new).collect(); + + RemoteSnapshot { + callstack: CallStack::from_remote_frames(call_frames), + current_stack, + cycle: snapshot.cycle, + } +} + +/// Resolve a remote frame to a [ResolvedLocation] by loading the source file from disk. +#[cfg(feature = "dap")] +fn resolve_remote_frame( + frame: &crate::exec::DapUiFrame, + source_manager: &Arc, +) -> Option { + use std::path::Path; + + use miden_debug_types::{SourceManagerExt, SourceSpan}; + + let path_str = frame.source_path.as_ref()?; + let path = Path::new(path_str); + let source_file = source_manager.load_file(path).ok()?; + let line = frame.line.max(1) as u32; + let col = frame.column.max(1) as u32; + + // Compute a span from the line number — use the byte range of the line + let content = source_file.content(); + let line_index = miden_debug_types::LineIndex::from(line.saturating_sub(1)); + let range = content.line_range(line_index)?; + let span = SourceSpan::new(source_file.id(), range); + + Some(crate::debug::ResolvedLocation { + source_file, + line, + col, + span, + }) +} + /// Attempts to load the standard library from the sysroot/toolchain directory. /// /// Supports both formats: @@ -370,9 +835,24 @@ fn load_sysroot_libs( Ok(libs) } +/// Run a [DebugExecutor] to completion and return the [ExecutionTrace]. +fn run_to_trace(mut executor: DebugExecutor) -> ExecutionTrace { + loop { + if executor.stopped { + break; + } + match executor.step() { + Ok(_) => continue, + Err(_) => break, + } + } + executor.into_execution_trace() +} + fn load_package(config: &DebuggerConfig) -> Result, Report> { - let package = match config.input { - InputFile::Real(ref path) => { + let input = config.input.as_ref().ok_or_else(|| Report::msg("no input file specified"))?; + let package = match input { + InputFile::Real(path) => { let bytes = std::fs::read(path).into_diagnostic()?; miden_mast_package::Package::read_from_bytes(&bytes) .map(Arc::new) @@ -383,7 +863,7 @@ fn load_package(config: &DebuggerConfig) -> Result miden_mast_package::Package::read_from_bytes(bytes) + InputFile::Stdin(bytes) => miden_mast_package::Package::read_from_bytes(bytes) .map(Arc::new) .map_err(|e| Report::msg(format!("failed to load Miden package from stdin: {e}")))?, };