diff options
30 files changed, 1981 insertions, 807 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 0190999..122d4ae 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,6 +5,7 @@ test-wasm-2="test --package tinywasm --test test-wasm-2 --release" test-wasm-3="test --package tinywasm --test test-wasm-3 --release" test-wast="test --package tinywasm --test test-wast" test-wasm-custom="test --package tinywasm --test test-wasm-custom --release" +cli="run -p tinywasm-cli --bin tinywasm --release --" [target.x86_64-unknown-linux-gnu] rustflags=["-C", "target-cpu=x86-64-v3"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddae8f..76bf8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This release is a major runtime and API rework. It adds support for several newe - `engine::Config` support for fuel policy, stack sizing, memory backend selection, and trap-on-OOM behavior - New feature flags: `canonicalize-nans`, `simd-x86`, `guest-debug`, `debug`, and `parallel-parser` - Top-level parser re-exports behind the `parser` feature: `parse_bytes`, `parse_file`, and `parse_stream` +- Completely new `tinywasm` cli ### Changed @@ -27,40 +27,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] -name = "argh" -version = "0.1.19" +name = "anstyle-parse" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211818e820cda9ca6f167a64a5c808837366a6dfd807157c64c1304c486cd033" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ - "argh_derive", - "argh_shared", + "utf8parse", ] [[package]] -name = "argh_derive" -version = "0.1.19" +name = "anstyle-query" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c442a9d18cef5dde467405d27d461d080d68972d6d0dfd0408265b6749ec427d" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "argh_shared", - "proc-macro2", - "quote", - "syn", + "windows-sys", ] [[package]] -name = "argh_shared" -version = "0.1.19" +name = "anstyle-wincon" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ade012bac4db278517a0132c8c10c6427025868dca16c801087c28d5a411f1" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ - "serde", + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "assert_cmd" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", ] [[package]] @@ -76,6 +110,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -137,6 +182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -145,8 +191,31 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -165,6 +234,12 @@ dependencies = [ ] [[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] name = "criterion" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -230,6 +305,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -267,6 +348,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] name = "eyre" version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -277,12 +368,46 @@ dependencies = [ ] [[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -295,11 +420,26 @@ dependencies = [ [[package]] name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -312,6 +452,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] name = "include_dir" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -343,7 +489,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] @@ -358,6 +506,12 @@ dependencies = [ ] [[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -391,6 +545,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -403,6 +563,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -418,6 +584,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -452,6 +624,36 @@ dependencies = [ ] [[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] name = "pretty_env_logger" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -462,6 +664,16 @@ dependencies = [ ] [[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -480,6 +692,12 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] name = "rayon" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -529,6 +747,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -593,6 +824,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -604,6 +841,19 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -613,6 +863,12 @@ dependencies = [ ] [[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -655,6 +911,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "tinywasm-cli", "tinywasm-parser", "tinywasm-types", "wasm-testsuite", @@ -666,12 +923,19 @@ dependencies = [ name = "tinywasm-cli" version = "0.9.0-alpha.0" dependencies = [ - "argh", + "anstream", + "assert_cmd", + "clap", + "clap_complete", "eyre", "log", + "owo-colors", + "predicates", "pretty_env_logger", + "tempfile", "tinywasm", "wast", + "wat", ] [[package]] @@ -680,7 +944,7 @@ version = "0.9.0-alpha.0" dependencies = [ "log", "tinywasm-types", - "wasmparser", + "wasmparser 0.247.0", "wat", ] @@ -716,6 +980,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -726,13 +1011,53 @@ dependencies = [ ] [[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] name = "wasm-encoder" version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] @@ -747,6 +1072,18 @@ dependencies = [ [[package]] name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" @@ -766,7 +1103,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.247.0", ] [[package]] @@ -825,6 +1162,100 @@ dependencies = [ ] [[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", +] + +[[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 8472afb..add21d3 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,30 +1,45 @@ [package] name="tinywasm-cli" version.workspace=true -description="Command-line interface for TinyWasm" +description="Minimal command-line interface for TinyWasm" edition.workspace=true license.workspace=true authors.workspace=true repository.workspace=true +documentation="https://docs.rs/tinywasm-cli" rust-version.workspace=true -keywords.workspace=true -categories=["wasm"] +keywords=["tinywasm", "wasm", "webassembly", "cli", "runtime"] +categories=["command-line-utilities", "wasm"] readme="README.md" +[lib] +name="tinywasm_cli" +path="src/lib.rs" + [[bin]] -name="tinywasm-cli" +name="tinywasm" path="src/bin.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap={version="4.5", features=["derive"]} +clap_complete="4.5" eyre.workspace=true log.workspace=true pretty_env_logger.workspace=true tinywasm={version="0.9.0-alpha.0", path="../tinywasm", features=["std", "parser"]} -argh="0.1" +wat={workspace=true, optional=true} wast={workspace=true, optional=true} +owo-colors={workspace=true} +anstream={version="1.0"} [features] -default=["wat"] -wat=["dep:wast"] +default=["wat", "wast"] +wat=["dep:wat"] +wast=["dep:wast"] + +[dev-dependencies] +assert_cmd="2.0" +predicates="3.1" +tempfile="3.20" diff --git a/crates/cli/README.md b/crates/cli/README.md index aa4d0c8..88172c9 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -1,13 +1,30 @@ # `tinywasm-cli` -The `tinywasm-cli` crate contains the command line interface for the `tinywasm` project. See [`tinywasm`](https://crates.io/crates/tinywasm) for more information. +The `tinywasm-cli` package installs the `tinywasm` binary for `tinywasm`. See [`tinywasm`](https://crates.io/crates/tinywasm) for the embedding API. It is recommended to use the library directly instead of the CLI. +The crate also exposes reusable helpers such as `tinywasm_cli::wast_runner::WastRunner` so workspace tests can drive the same WAST execution logic directly. + ## Usage ```bash -$ cargo install tinywasm-cli -$ tinywasm-cli --help -$ tinywasm-cli run ./module.wasm -$ tinywasm-cli run ./module.wasm -f add -a i32:1 -a i32:2 +$ cargo install tinywasm-cli --version 0.9.0-alpha.0 --bin tinywasm +$ tinywasm --help +$ tinywasm ./module.wasm +$ tinywasm run --invoke add ./module.wasm 1 2 +$ tinywasm compile ./module.wat -o ./module.twasm +$ tinywasm dump ./module.twasm +$ tinywasm inspect ./module.wasm +$ tinywasm wast ./spec-tests/address.wast ``` + +Notes: + +- `run`, `dump`, and `inspect` accept `.wasm`, `.wat`, and `.twasm` inputs. +- Use `-` as the input path to read a module from stdin. +- Without `--invoke`, `tinywasm` expects the module to have a start function or `_start` export. +- `compile` writes TinyWasm's `twasm` archive format. +- Function invocation arguments are parsed from the export signature, so `tinywasm run --invoke add ./module.wasm 1 2` works without repeating Wasm types on the command line. +- `inspect` uses ANSI colors automatically when writing to a terminal; set `NO_COLOR=1` to disable them. +- Stack flags support both fixed sizes like `--value-stack-size 4096` and dynamic sizes like `--value-stack-dynamic 1024:8192`. +- `wast` is a separate command for WebAssembly spec scripts and accepts files or folders containing `.wast` files. diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs deleted file mode 100644 index 0333c92..0000000 --- a/crates/cli/src/args.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::str::FromStr; -use tinywasm::types::WasmValue; - -#[derive(Debug)] -pub struct WasmArg(WasmValue); - -pub fn to_wasm_args(args: Vec<WasmArg>) -> Vec<WasmValue> { - args.into_iter().map(Into::into).collect() -} - -impl From<WasmArg> for WasmValue { - fn from(value: WasmArg) -> Self { - value.0 - } -} - -impl FromStr for WasmArg { - type Err = String; - fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> { - let [ty, val]: [&str; 2] = s - .split(':') - .collect::<Vec<_>>() - .try_into() - .map_err(|_e| "invalid argument format; expected type:value".to_string())?; - - let arg: WasmValue = match ty { - "i32" => val.parse::<i32>().map_err(|e| format!("invalid argument value for i32: {e:?}"))?.into(), - "i64" => val.parse::<i64>().map_err(|e| format!("invalid argument value for i64: {e:?}"))?.into(), - "f32" => val.parse::<f32>().map_err(|e| format!("invalid argument value for f32: {e:?}"))?.into(), - "f64" => val.parse::<f64>().map_err(|e| format!("invalid argument value for f64: {e:?}"))?.into(), - "v128" => val.parse::<i128>().map_err(|e| format!("invalid argument value for v128: {e:?}"))?.into(), - t => return Err(format!("invalid arg type `{t}`; expected one of i32, i64, f32, f64, v128")), - }; - - Ok(WasmArg(arg)) - } -} diff --git a/crates/cli/src/bin.rs b/crates/cli/src/bin.rs index 7e32fed..592fda7 100644 --- a/crates/cli/src/bin.rs +++ b/crates/cli/src/bin.rs @@ -1,118 +1,9 @@ -use std::str::FromStr; - -use argh::FromArgs; -use args::WasmArg; +use clap::Parser; use eyre::Result; -use log::{debug, info}; -use tinywasm::{Module, ModuleInstance, types::WasmValue}; - -use crate::args::to_wasm_args; -mod args; -mod util; - -#[cfg(feature = "wat")] -mod wat; - -#[derive(FromArgs)] -/// `TinyWasm` CLI -struct TinyWasmCli { - #[argh(subcommand)] - nested: TinyWasmSubcommand, - - /// log level: trace, debug, info, warn, or error - #[argh(option, short = 'l', default = "\"info\".to_string()")] - log_level: String, -} - -#[derive(FromArgs)] -#[argh(subcommand)] -enum TinyWasmSubcommand { - Run(Run), -} - -enum Engine { - Main, -} - -impl FromStr for Engine { - type Err = String; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "main" => Ok(Self::Main), - _ => Err(format!("unknown engine: {s}")), - } - } -} - -#[derive(FromArgs)] -/// run a wasm file -#[argh(subcommand, name = "run")] -struct Run { - /// wasm file to run - #[argh(positional)] - wasm_file: String, - - /// exported function to run; omit to only instantiate and run start - #[argh(option, short = 'f')] - func: Option<String>, - - /// arguments passed to the function in type:value form - #[argh(option, short = 'a')] - args: Vec<WasmArg>, - - /// engine to use (currently only `main`) - #[argh(option, short = 'e', default = "Engine::Main")] - engine: Engine, -} +use tinywasm_cli::{Cli, run_cli}; fn main() -> Result<()> { - let args: TinyWasmCli = argh::from_env(); - let level = match args.log_level.as_str() { - "trace" => log::LevelFilter::Trace, - "debug" => log::LevelFilter::Debug, - "warn" => log::LevelFilter::Warn, - "error" => log::LevelFilter::Error, - "info" => log::LevelFilter::Info, - other => return Err(eyre::eyre!("invalid log level `{other}`; expected trace, debug, info, warn, or error")), - }; - - pretty_env_logger::formatted_builder().filter_level(level).init(); - let cwd = std::env::current_dir()?; - - match args.nested { - TinyWasmSubcommand::Run(Run { wasm_file, engine, args, func }) => { - debug!("args: {args:?}"); - - let path = cwd.join(&wasm_file); - let module = match wasm_file.ends_with(".wat") { - #[cfg(feature = "wat")] - true => { - let wat = std::fs::read_to_string(path)?; - let wasm = wat::wat2wasm(&wat); - tinywasm::parse_bytes(&wasm)? - } - #[cfg(not(feature = "wat"))] - true => return Err(eyre::eyre!("wat support is not enabled in this build")), - false => tinywasm::parse_file(path)?, - }; - - match engine { - Engine::Main => run(module, func, &to_wasm_args(args)), - } - } - } -} - -fn run(module: Module, func: Option<String>, args: &[WasmValue]) -> Result<()> { - let mut store = tinywasm::Store::default(); - let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - - if let Some(func) = func { - let func = instance.func_untyped(&store, &func)?; - let res = func.call(&mut store, args)?; - info!("{res:?}"); - } - - Ok(()) + let cli = Cli::parse(); + pretty_env_logger::formatted_builder().filter_level(cli.log_level.into()).init(); + run_cli(cli) } diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs new file mode 100644 index 0000000..8b9df2d --- /dev/null +++ b/crates/cli/src/cli.rs @@ -0,0 +1,128 @@ +use clap::{ + Args, Parser, Subcommand, ValueEnum, + builder::{ + Styles, + styling::{AnsiColor, Effects}, + }, +}; +use clap_complete::Shell; + +use crate::engine_flags::EngineFlags; + +// based on https://github.com/crate-ci/clap-cargo/blob/master/src/style.rs +const STYLES: Styles = Styles::styled() + .header(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD)) + .usage(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD)) + .literal(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD)) + .placeholder(AnsiColor::Cyan.on_default()) + .error(AnsiColor::BrightRed.on_default().effects(Effects::BOLD)) + .valid(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD)) + .invalid(AnsiColor::Yellow.on_default()); + +#[derive(Parser)] +#[command( + name = "tinywasm", + about = "TinyWasm CLI", + styles = STYLES, + version, + args_conflicts_with_subcommands = true, + subcommand_negates_reqs = true +)] +pub struct Cli { + #[arg(long, global = true, value_enum, default_value_t = LogLevel::Info)] + pub log_level: LogLevel, + + #[command(subcommand)] + pub command: Option<Commands>, + + #[command(flatten)] + pub run: RunArgs, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Run a module + Run(RunArgs), + /// Compile a Wasm/WAT module to a .twasm archive + Compile(CompileArgs), + /// Dump lowered TinyWasm bytecode + Dump(ModuleInputArgs), + /// Inspect imports and exports + Inspect(ModuleInputArgs), + #[cfg(feature = "wast")] + /// Execute WebAssembly spec scripts (.wast) + Wast(WastArgs), + /// Generate shell completions + Completion(CompletionArgs), +} + +#[derive(Args, Clone)] +pub struct RunArgs { + /// Module path, or `-` to read from stdin + pub module: Option<String>, + + /// Invoke a named export instead of the default entrypoint + #[arg(long)] + pub invoke: Option<String>, + + #[command(flatten)] + pub engine: EngineFlags, + + /// Arguments passed to the invoked Wasm function + #[arg(trailing_var_arg = true)] + pub args: Vec<String>, +} + +#[derive(Args, Clone)] +pub struct CompileArgs { + /// Input module path, or `-` to read from stdin + pub input: String, + + /// Output path, or `-` to write to stdout + #[arg(short, long)] + pub output: Option<String>, + + /// Overwrite the output file if it already exists + #[arg(short, long)] + pub force: bool, +} + +#[derive(Args, Clone)] +pub struct ModuleInputArgs { + /// Module path, or `-` to read from stdin + pub module: String, +} + +#[derive(Args, Clone)] +pub struct CompletionArgs { + pub shell: Shell, +} + +#[cfg(feature = "wast")] +#[derive(Args, Clone)] +pub struct WastArgs { + /// WAST files or directories containing .wast files + #[arg(required = true)] + pub paths: Vec<String>, +} + +#[derive(Clone, Copy, ValueEnum)] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl From<LogLevel> for log::LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Trace => log::LevelFilter::Trace, + LogLevel::Debug => log::LevelFilter::Debug, + LogLevel::Info => log::LevelFilter::Info, + LogLevel::Warn => log::LevelFilter::Warn, + LogLevel::Error => log::LevelFilter::Error, + } + } +} diff --git a/crates/cli/src/cmd/compile.rs b/crates/cli/src/cmd/compile.rs new file mode 100644 index 0000000..93e38d5 --- /dev/null +++ b/crates/cli/src/cmd/compile.rs @@ -0,0 +1,15 @@ +use eyre::Result; + +use crate::cli::CompileArgs; +use crate::load::{default_twasm_output_path, load_compilable_module, write_output_bytes}; + +pub fn run(args: CompileArgs) -> Result<()> { + let module = load_compilable_module(&args.input)?; + let twasm = module.serialize_twasm()?; + let output = match args.output { + Some(output) => output, + None => default_twasm_output_path(&args.input)?, + }; + + write_output_bytes(&output, &twasm, args.force) +} diff --git a/crates/cli/src/cmd/completion.rs b/crates/cli/src/cmd/completion.rs new file mode 100644 index 0000000..5815f57 --- /dev/null +++ b/crates/cli/src/cmd/completion.rs @@ -0,0 +1,12 @@ +use std::io; + +use clap::CommandFactory; +use eyre::Result; + +use crate::cli::{Cli, CompletionArgs}; + +pub fn run(args: CompletionArgs) -> Result<()> { + let mut cmd = Cli::command(); + clap_complete::generate(args.shell, &mut cmd, "tinywasm", &mut io::stdout()); + Ok(()) +} diff --git a/crates/cli/src/cmd/dump.rs b/crates/cli/src/cmd/dump.rs new file mode 100644 index 0000000..78fb28b --- /dev/null +++ b/crates/cli/src/cmd/dump.rs @@ -0,0 +1,71 @@ +use anstream::println; +use eyre::Result; +use owo_colors::OwoColorize; +use tinywasm::types::{ExternalKind, ImportKind}; + +use crate::cli::ModuleInputArgs; +use crate::load::load_module; + +pub fn run(args: ModuleInputArgs) -> Result<()> { + let loaded = load_module(&args.module)?; + let module = loaded.module; + + let imported_func_count = + module.imports.iter().filter(|import| matches!(import.kind, ImportKind::Function(_))).count() as u32; + + for (func_idx, func) in module.funcs.iter().enumerate() { + let global_idx = imported_func_count + func_idx as u32; + + let exports = module + .exports + .iter() + .filter(|export| export.kind == ExternalKind::Func && export.index == global_idx) + .map(|export| export.name.as_ref()) + .collect::<Vec<_>>(); + + let header = format!("func[{func_idx}]").blue().bold().to_string(); + if exports.is_empty() { + println!("{header}"); + } else { + println!("{header} {}", format!("exports={}", format!("{exports:?}").cyan()).bright_black()); + } + + for (ip, instr) in func.instructions.iter().enumerate() { + let instr = print_instr(instr); + println!(" {}: {}", print_ip(ip), instr); + } + println!(); + } + + Ok(()) +} + +fn print_ip(ip: usize) -> String { + let s = format!("{ip:04}"); + let first_non_zero = s.find(|c| c != '0').unwrap_or(s.len() - 1); + + format!( + "{}{}", + &s[..first_non_zero].to_string().bright_black().dimmed(), + &s[first_non_zero..].to_string().bright_black() + ) +} + +fn print_instr(instr: &tinywasm::types::Instruction) -> String { + let instr = format!("{instr:?}"); + let Some(split) = instr.find(['(', ' ', '{']) else { + return instr.bold().to_string(); + }; + + let (name, rest) = instr.split_at(split); + + let rest = rest + .replace('(', &"(".bright_black().to_string()) + .replace(')', &")".bright_black().to_string()) + .replace('{', &"{".bright_black().to_string()) + .replace('}', &"}".bright_black().to_string()) + .replace(',', &",".bright_black().to_string()) + .replace(':', &":".bright_black().to_string()); + + format!("{}{}", name.bold(), rest) +} diff --git a/crates/cli/src/cmd/inspect.rs b/crates/cli/src/cmd/inspect.rs new file mode 100644 index 0000000..ea2cec7 --- /dev/null +++ b/crates/cli/src/cmd/inspect.rs @@ -0,0 +1,35 @@ +use eyre::Result; + +use crate::cli::ModuleInputArgs; +use crate::load::load_module; +use crate::output::{format_export_type, format_import_type}; +use anstream::println; +use owo_colors::OwoColorize; + +pub fn run(args: ModuleInputArgs) -> Result<()> { + let loaded = load_module(&args.module)?; + let module = loaded.module; + + println!("{}", "Imports".bold()); + let mut import_count = 0usize; + for import in module.imports() { + import_count += 1; + println!(" {}.{}: {}", import.module.blue(), import.name.cyan(), format_import_type(import.ty).yellow()); + } + if import_count == 0 { + println!(" {}", "(none)".yellow()); + } + + println!(); + println!("{}", "Exports".bold()); + let mut export_count = 0usize; + for export in module.exports() { + export_count += 1; + println!(" {}: {}", export.name.green(), format_export_type(export.ty).yellow()); + } + if export_count == 0 { + println!(" {}", "(none)".yellow()); + } + + Ok(()) +} diff --git a/crates/cli/src/cmd/mod.rs b/crates/cli/src/cmd/mod.rs new file mode 100644 index 0000000..0861af2 --- /dev/null +++ b/crates/cli/src/cmd/mod.rs @@ -0,0 +1,7 @@ +pub mod compile; +pub mod completion; +pub mod dump; +pub mod inspect; +pub mod run; +#[cfg(feature = "wast")] +pub mod wast; diff --git a/crates/cli/src/cmd/run.rs b/crates/cli/src/cmd/run.rs new file mode 100644 index 0000000..b2f5e03 --- /dev/null +++ b/crates/cli/src/cmd/run.rs @@ -0,0 +1,47 @@ +use eyre::{Result, bail}; +use tinywasm::types::ExportType; +use tinywasm::{ModuleInstance, Store}; + +use crate::cli::RunArgs; +use crate::load::load_module; +use crate::output::print_results; +use crate::value_parse::parse_invocation_args; + +pub fn run(args: RunArgs) -> Result<()> { + let module_path = args.module.as_deref().ok_or_else(|| eyre::eyre!("missing module path"))?; + let loaded = load_module(module_path)?; + let mut store = Store::new(args.engine.build_engine()?); + let instance = ModuleInstance::instantiate_no_start(&mut store, &loaded.module, None)?; + + match args.invoke.as_deref() { + Some(export) => { + if loaded.module.start_func.is_some() { + let _ = instance.start(&mut store)?; + } + + let func_ty = loaded + .module + .exports() + .find_map(|item| match (item.name == export, item.ty) { + (true, ExportType::Func(ty)) => Some(ty), + _ => None, + }) + .ok_or_else(|| eyre::eyre!("export is not a function: {export}"))?; + let func = instance.func_untyped(&store, export)?; + let params = parse_invocation_args(func_ty, &args.args)?; + let results = func.call(&mut store, ¶ms)?; + print_results(&results); + Ok(()) + } + None => { + if instance.start_func(&store)?.is_none() { + bail!( + "module has no start function or `_start` export; use `tinywasm inspect {module_path}` or `tinywasm run --invoke <export> {module_path}`" + ) + } + + let _ = instance.start(&mut store)?; + Ok(()) + } + } +} diff --git a/crates/cli/src/cmd/wast.rs b/crates/cli/src/cmd/wast.rs new file mode 100644 index 0000000..ef9c263 --- /dev/null +++ b/crates/cli/src/cmd/wast.rs @@ -0,0 +1,12 @@ +use std::path::PathBuf; + +use eyre::Result; + +use crate::cli::WastArgs; +use crate::wast_runner::WastRunner; + +pub fn run(args: WastArgs) -> Result<()> { + let paths = args.paths.into_iter().map(PathBuf::from).collect::<Vec<_>>(); + let mut runner = WastRunner::new(); + runner.run_paths(&paths) +} diff --git a/crates/cli/src/engine_flags.rs b/crates/cli/src/engine_flags.rs new file mode 100644 index 0000000..2965a3e --- /dev/null +++ b/crates/cli/src/engine_flags.rs @@ -0,0 +1,154 @@ +use clap::{Args, ValueEnum}; +use eyre::{Result, bail}; +use tinywasm::{Engine, StackConfig, engine::FuelPolicy}; + +#[derive(Args, Clone, Default)] +pub struct EngineFlags { + /// Fuel accounting policy for budgeted execution APIs + #[arg(long, value_enum)] + pub fuel_policy: Option<FuelPolicyArg>, + + /// Trap immediately on memory or stack allocation failure + #[arg(long)] + pub trap_on_oom: bool, + + /// Memory backend to use for instantiated memories + #[arg(long, value_enum)] + pub memory_backend: Option<MemoryBackendArg>, + + /// Chunk size in bytes for the paged memory backend + #[arg(long, default_value_t = 64 * 1024)] + pub memory_page_chunk_size: usize, + + /// Fixed value stack size for all value lanes + #[arg(long, conflicts_with = "value_stack_dynamic")] + pub value_stack_size: Option<usize>, + + /// Dynamic value stack config in initial:max form for all value lanes + #[arg(long, value_name = "INITIAL:MAX", conflicts_with = "value_stack_size")] + pub value_stack_dynamic: Option<StackSpec>, + + /// Fixed call stack size in frames + #[arg(long, conflicts_with = "call_stack_dynamic")] + pub call_stack_size: Option<usize>, + + /// Dynamic call stack config in initial:max form + #[arg(long, value_name = "INITIAL:MAX", conflicts_with = "call_stack_size")] + pub call_stack_dynamic: Option<StackSpec>, +} + +#[derive(Clone, Copy, ValueEnum)] +pub enum FuelPolicyArg { + PerInstruction, + Weighted, +} + +#[derive(Clone, Copy, ValueEnum)] +pub enum MemoryBackendArg { + Vec, + Paged, +} + +#[derive(Clone)] +pub struct StackSpec { + initial: usize, + max: usize, +} + +impl core::str::FromStr for StackSpec { + type Err = String; + + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { + let (initial, max) = s.split_once(':').ok_or_else(|| "expected INITIAL:MAX".to_string())?; + let initial = initial.parse::<usize>().map_err(|e| format!("invalid initial stack size: {e}"))?; + let max = max.parse::<usize>().map_err(|e| format!("invalid max stack size: {e}"))?; + if initial > max { + return Err("initial stack size must be less than or equal to max stack size".to_string()); + } + Ok(Self { initial, max }) + } +} + +impl StackSpec { + fn into_stack_config(self) -> StackConfig { + StackConfig::dynamic(self.initial, self.max) + } +} + +impl EngineFlags { + pub fn build_engine(&self) -> Result<Engine> { + let mut config = tinywasm::engine::Config::new(); + + if let Some(fuel_policy) = self.fuel_policy { + config = config.with_fuel_policy(match fuel_policy { + FuelPolicyArg::PerInstruction => FuelPolicy::PerInstruction, + FuelPolicyArg::Weighted => FuelPolicy::Weighted, + }); + } + + if let Some(memory_backend) = self.memory_backend { + config = config.with_memory_backend(match memory_backend { + MemoryBackendArg::Vec => tinywasm::MemoryBackend::vec(), + MemoryBackendArg::Paged => { + if self.memory_page_chunk_size == 0 { + bail!("--memory-page-chunk-size must be greater than zero") + } + tinywasm::MemoryBackend::paged(self.memory_page_chunk_size) + } + }); + } + + if let Some(value_stack_size) = self.value_stack_size { + config = config.with_value_stack(StackConfig::fixed(value_stack_size)); + } + + if let Some(value_stack_dynamic) = self.value_stack_dynamic.clone() { + config = config.with_value_stack(value_stack_dynamic.into_stack_config()); + } + + if let Some(call_stack_size) = self.call_stack_size { + config = config.with_call_stack(StackConfig::fixed(call_stack_size)); + } + + if let Some(call_stack_dynamic) = self.call_stack_dynamic.clone() { + config = config.with_call_stack(call_stack_dynamic.into_stack_config()); + } + + if self.trap_on_oom { + config = config.with_trap_on_oom(true); + } + + Ok(Engine::new(config)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_stack_spec() { + let spec: StackSpec = "16:64".parse().unwrap(); + let cfg = spec.into_stack_config(); + assert_eq!(cfg.initial_size, 16); + assert_eq!(cfg.max_size, 64); + assert!(cfg.dynamic); + } + + #[test] + fn builds_dynamic_stack_engine() { + let flags = EngineFlags { + value_stack_dynamic: Some("8:32".parse().unwrap()), + call_stack_dynamic: Some("4:12".parse().unwrap()), + ..Default::default() + }; + + let engine = flags.build_engine().unwrap(); + assert!(engine.config().value_stack_32.dynamic); + assert_eq!(engine.config().value_stack_32.initial_size, 8); + assert_eq!(engine.config().value_stack_32.max_size, 32); + assert!(engine.config().call_stack.dynamic); + assert_eq!(engine.config().call_stack.initial_size, 4); + assert_eq!(engine.config().call_stack.max_size, 12); + } +} diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs new file mode 100644 index 0000000..bef6b72 --- /dev/null +++ b/crates/cli/src/lib.rs @@ -0,0 +1,34 @@ +pub mod cli; +pub mod cmd; +pub mod engine_flags; +pub mod load; +pub mod output; +pub mod value_parse; +#[cfg(feature = "wast")] +pub mod wast_runner; + +use clap::CommandFactory; +use eyre::Result; + +pub use cli::{Cli, Commands}; + +pub fn run_cli(cli: Cli) -> Result<()> { + match cli.command { + Some(Commands::Run(args)) => cmd::run::run(args), + Some(Commands::Compile(args)) => cmd::compile::run(args), + Some(Commands::Dump(args)) => cmd::dump::run(args), + Some(Commands::Inspect(args)) => cmd::inspect::run(args), + #[cfg(feature = "wast")] + Some(Commands::Wast(args)) => cmd::wast::run(args), + Some(Commands::Completion(args)) => cmd::completion::run(args), + None => match cli.run.module.as_deref() { + Some(_) => cmd::run::run(cli.run), + None => { + let mut cmd = Cli::command(); + cmd.print_help()?; + println!(); + Ok(()) + } + }, + } +} diff --git a/crates/cli/src/load.rs b/crates/cli/src/load.rs new file mode 100644 index 0000000..d16469b --- /dev/null +++ b/crates/cli/src/load.rs @@ -0,0 +1,103 @@ +use std::ffi::OsStr; +use std::io::{Read, Write}; +use std::path::Path; + +use eyre::{Context, Result, bail}; +use tinywasm::Module; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InputFormat { + Wasm, + Wat, + Twasm, +} + +pub struct LoadedModule { + pub module: Module, + pub format: InputFormat, +} + +pub fn load_module(input: &str) -> Result<LoadedModule> { + let bytes = read_input_bytes(input)?; + load_module_from_bytes(input, &bytes) +} + +pub fn load_compilable_module(input: &str) -> Result<Module> { + let loaded = load_module(input)?; + if loaded.format == InputFormat::Twasm { + bail!("input is already a twasm archive; use `run`, `dump`, or `inspect` instead") + } + Ok(loaded.module) +} + +pub fn default_twasm_output_path(input: &str) -> Result<String> { + if input == "-" { + bail!("--output is required when compiling from stdin") + } + + let path = Path::new(input); + let stem = path.file_stem().and_then(OsStr::to_str).unwrap_or("module"); + let output = path.with_file_name(format!("{stem}.twasm")); + Ok(output.to_string_lossy().into_owned()) +} + +pub fn write_output_bytes(output: &str, bytes: &[u8], force: bool) -> Result<()> { + if output == "-" { + std::io::stdout().write_all(bytes)?; + std::io::stdout().flush()?; + return Ok(()); + } + + let path = Path::new(output); + if path.exists() && !force { + bail!("output file already exists: {output}; pass --force to overwrite") + } + + std::fs::write(path, bytes).with_context(|| format!("failed to write output file `{output}`"))?; + Ok(()) +} + +fn load_module_from_bytes(input: &str, bytes: &[u8]) -> Result<LoadedModule> { + if bytes.starts_with(b"TWAS") { + let module = Module::try_from_twasm(bytes).with_context(|| format!("failed to read twasm input `{input}`"))?; + return Ok(LoadedModule { module, format: InputFormat::Twasm }); + } + + #[cfg(feature = "wat")] + if input != "-" && has_extension(input, "wat") { + let wasm = wat::parse_bytes(bytes).with_context(|| format!("failed to parse WAT input `{input}`"))?; + let module = + tinywasm::parse_bytes(&wasm).with_context(|| format!("failed to parse Wasm generated from `{input}`"))?; + return Ok(LoadedModule { module, format: InputFormat::Wat }); + } + + #[cfg(not(feature = "wat"))] + if input != "-" && has_extension(input, "wat") { + bail!("wat support is not enabled in this build") + } + + #[cfg(feature = "wat")] + if input == "-" + && let Ok(wasm) = wat::parse_bytes(bytes) + { + let module = tinywasm::parse_bytes(&wasm).context("failed to parse Wasm generated from stdin WAT input")?; + return Ok(LoadedModule { module, format: InputFormat::Wat }); + } + + let module = tinywasm::parse_bytes(bytes).with_context(|| format!("failed to parse Wasm input `{input}`"))?; + Ok(LoadedModule { module, format: InputFormat::Wasm }) +} + +fn read_input_bytes(input: &str) -> Result<Vec<u8>> { + if input == "-" { + let mut bytes = Vec::new(); + std::io::stdin().read_to_end(&mut bytes).context("failed to read stdin")?; + return Ok(bytes); + } + + std::fs::read(input).with_context(|| format!("failed to read input `{input}`")) +} + +fn has_extension(path: &str, extension: &str) -> bool { + Path::new(path).extension().and_then(OsStr::to_str) == Some(extension) +} diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs new file mode 100644 index 0000000..bc0e450 --- /dev/null +++ b/crates/cli/src/output.rs @@ -0,0 +1,85 @@ +use tinywasm::types::{ + ExportType, FuncType, GlobalType, ImportType, MemoryArch, MemoryType, TableType, WasmType, WasmValue, +}; + +pub fn print_results(results: &[WasmValue]) { + match results { + [] => {} + [value] => println!("{}", format_value(value)), + values => { + let formatted = values.iter().map(format_value).collect::<Vec<_>>().join(", "); + println!("[{formatted}]"); + } + } +} + +pub fn format_value(value: &WasmValue) -> String { + format!("{value:?}") +} + +pub fn color_enabled() -> bool { + use std::io::IsTerminal; + + std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none() +} + +pub fn format_wasm_type(ty: WasmType) -> &'static str { + match ty { + WasmType::I32 => "i32", + WasmType::I64 => "i64", + WasmType::F32 => "f32", + WasmType::F64 => "f64", + WasmType::V128 => "v128", + WasmType::RefFunc => "funcref", + WasmType::RefExtern => "externref", + } +} + +pub fn format_func_type(ty: &FuncType) -> String { + let params = ty.params().iter().map(|ty| format_wasm_type(*ty)).collect::<Vec<_>>().join(", "); + let results = ty.results().iter().map(|ty| format_wasm_type(*ty)).collect::<Vec<_>>().join(", "); + + if ty.results().is_empty() { format!("({params})") } else { format!("({params}) -> ({results})") } +} + +pub fn format_memory_type(ty: &MemoryType) -> String { + let arch = match ty.arch() { + MemoryArch::I32 => "i32", + MemoryArch::I64 => "i64", + }; + let max = if ty.page_count_max() == ty.page_count_initial() && ty.max_size() == ty.initial_size() { + ty.page_count_initial().to_string() + } else { + ty.page_count_max().to_string() + }; + + format!("memory[{arch}] initial={} max={} page_size={}", ty.page_count_initial(), max, ty.page_size()) +} + +pub fn format_table_type(ty: &TableType) -> String { + let max = ty.size_max.map(|v| v.to_string()).unwrap_or_else(|| "unbounded".to_string()); + format!("table[{}] initial={} max={max}", format_wasm_type(ty.element_type), ty.size_initial) +} + +pub fn format_global_type(ty: &GlobalType) -> String { + let mutability = if ty.mutable { "mut" } else { "const" }; + format!("global[{mutability} {}]", format_wasm_type(ty.ty)) +} + +pub fn format_export_type(ty: ExportType<'_>) -> String { + match ty { + ExportType::Func(ty) => format!("func {}", format_func_type(ty)), + ExportType::Memory(ty) => format_memory_type(ty), + ExportType::Table(ty) => format_table_type(ty), + ExportType::Global(ty) => format_global_type(ty), + } +} + +pub fn format_import_type(ty: ImportType<'_>) -> String { + match ty { + ImportType::Func(ty) => format!("func {}", format_func_type(ty)), + ImportType::Memory(ty) => format_memory_type(ty), + ImportType::Table(ty) => format_table_type(ty), + ImportType::Global(ty) => format_global_type(ty), + } +} diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/cli/src/util.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/cli/src/value_parse.rs b/crates/cli/src/value_parse.rs new file mode 100644 index 0000000..f64d335 --- /dev/null +++ b/crates/cli/src/value_parse.rs @@ -0,0 +1,55 @@ +use eyre::{Result, bail}; +use tinywasm::types::{FuncType, WasmType, WasmValue}; + +use crate::output::format_wasm_type; + +pub fn parse_invocation_args(ty: &FuncType, args: &[String]) -> Result<Vec<WasmValue>> { + if args.len() != ty.params().len() { + bail!("wrong number of arguments: expected {}, got {}", ty.params().len(), args.len()) + } + + ty.params().iter().enumerate().map(|(idx, param_ty)| parse_arg(idx, *param_ty, &args[idx])).collect() +} + +fn parse_arg(index: usize, ty: WasmType, value: &str) -> Result<WasmValue> { + let parsed = match ty { + WasmType::I32 => value.parse::<i32>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?, + WasmType::I64 => value.parse::<i64>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?, + WasmType::F32 => value.parse::<f32>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?, + WasmType::F64 => value.parse::<f64>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?, + WasmType::V128 => value.parse::<i128>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?, + WasmType::RefFunc | WasmType::RefExtern => { + bail!( + "unsupported CLI argument type at position {}: {}; use the embedding API for reference values", + index + 1, + format_wasm_type(ty) + ) + } + }; + + Ok(parsed) +} + +fn format_error(index: usize, ty: WasmType, value: &str, error: impl core::fmt::Display) -> eyre::Report { + eyre::eyre!("failed to parse argument {} as {} from `{value}`: {error}", index + 1, format_wasm_type(ty)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_numeric_args() { + let ty = FuncType::new(&[WasmType::I32, WasmType::F64], &[WasmType::I32]); + let args = vec!["1".to_string(), "2.5".to_string()]; + let parsed = parse_invocation_args(&ty, &args).unwrap(); + assert_eq!(parsed, vec![WasmValue::I32(1), WasmValue::F64(2.5)]); + } + + #[test] + fn rejects_wrong_arity() { + let ty = FuncType::new(&[WasmType::I32], &[]); + let err = parse_invocation_args(&ty, &[]).unwrap_err(); + assert!(err.to_string().contains("wrong number of arguments")); + } +} diff --git a/crates/tinywasm/tests/testsuite/run.rs b/crates/cli/src/wast_runner.rs index 37a408f..8a759b7 100644 --- a/crates/tinywasm/tests/testsuite/run.rs +++ b/crates/cli/src/wast_runner.rs @@ -1,20 +1,24 @@ -use crate::testsuite::util::*; -use std::{borrow::Cow, collections::HashMap, fs::canonicalize, path::PathBuf}; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::{Display, Formatter}; +use std::fs::canonicalize; +use std::path::PathBuf; +use std::{ + panic::{self, AssertUnwindSafe}, + time::Duration, +}; -use super::TestSuite; -use eyre::{Result, eyre}; -use indexmap::IndexMap; -use log::{debug, error, info}; -use tinywasm::{Global, HostFunction, Imports, Memory, ModuleInstance, Table}; -use tinywasm_types::{ExternVal, MemoryType, ModuleInstanceAddr, TableType, WasmType, WasmValue}; -use wasm_testsuite::data::TestFile; -use wasm_testsuite::wast; -use wasm_testsuite::wast::{Wast, lexer::Lexer, parser::ParseBuffer}; +use eyre::{Context, Result, bail, eyre}; +use log::{debug, error}; +use tinywasm::types::{ExternRef, FuncRef, MemoryType, TableType, WasmType, WasmValue}; +use tinywasm::{ExecProgress, Global, HostFunction, Imports, Memory, Module, ModuleInstance, Store, Table}; +use wast::{QuoteWat, core::AbstractHeapType}; + +const TEST_TIME_SLICE: Duration = Duration::from_millis(20); +const TEST_MAX_SUSPENSIONS: u32 = 1000; #[derive(Default)] struct ModuleRegistry { modules: HashMap<String, ModuleInstance>, - named_modules: HashMap<String, ModuleInstance>, last_module: Option<ModuleInstance>, } @@ -30,29 +34,21 @@ impl ModuleRegistry { self.named_modules.insert(name, module); } } + fn register(&mut self, name: String, module: ModuleInstance) { - log::debug!("registering module: {name}"); + debug!("registering module: {name}"); self.modules.insert(name.clone(), module.clone()); - self.last_module = Some(module.clone()); self.named_modules.insert(name, module); } - fn get_idx(&self, module_id: Option<wast::token::Id<'_>>) -> Option<ModuleInstanceAddr> { + fn get_idx(&self, module_id: Option<wast::token::Id<'_>>) -> Option<u32> { match module_id { - Some(module) => { - log::debug!("getting module: {}", module.name()); - - if let Some(module) = self.modules.get(module.name()) { - return Some(module.id()); - } - - if let Some(module) = self.named_modules.get(module.name()) { - return Some(module.id()); - } - - None - } + Some(module) => self + .modules + .get(module.name()) + .or_else(|| self.named_modules.get(module.name())) + .map(ModuleInstance::id), None => self.last_module.as_ref().map(ModuleInstance::id), } } @@ -71,23 +67,93 @@ impl ModuleRegistry { } } -impl TestSuite { +#[derive(Default)] +pub struct WastRunner(BTreeMap<String, TestGroup>); + +#[derive(Clone, Debug)] +pub struct GroupResult { + pub name: String, + pub file: String, + pub passed: usize, + pub failed: usize, +} + +impl WastRunner { + pub fn new() -> Self { + Self::default() + } + pub fn run_paths(&mut self, tests: &[PathBuf]) -> Result<()> { - for file_name in tests { - let group_wast = std::fs::read(file_name).expect("failed to read test wast"); + for path in expand_paths(tests)? { + let contents = + std::fs::read_to_string(&path).context(format!("failed to read file: {}", path.to_string_lossy()))?; + let file = TestFile { - contents: std::str::from_utf8(&group_wast).expect("failed to convert to utf8"), - name: canonicalize(file_name).expect("failed to canonicalize file name").to_string_lossy().to_string(), - parent: "(custom group)".into(), + contents: &contents, + name: path.to_string_lossy().to_string(), + parent: canonicalize(&path)?.to_string_lossy().to_string(), }; - self.run_file(file).expect("failed to run group"); + self.run_file(file)?; + } + + self.print_errors(); + if self.failed() { + anstream::println!("{self}"); + Err(eyre!("failed one or more tests")) + } else { + anstream::println!("{self}"); + Ok(()) + } + } + + pub fn set_log_level(level: log::LevelFilter) { + let _ = pretty_env_logger::formatted_builder().filter_level(level).try_init(); + } + + pub fn failed(&self) -> bool { + self.0.values().any(|group| group.stats().1 > 0) + } + + pub fn print_errors(&self) { + for group in self.0.values() { + for test in &group.tests { + if let Err(err) = &test.result { + eprintln!( + "{}:{}:{} {} failed: {}", + group.file, + test.linecol.0 + 1, + test.linecol.1 + 1, + test.name, + err + ); + } + } } + } + pub fn group_results(&self) -> Vec<GroupResult> { + self.0 + .iter() + .map(|(name, group)| { + let (passed, failed) = group.stats(); + GroupResult { name: name.clone(), file: group.file.clone(), passed, failed } + }) + .collect() + } + + fn test_group(&mut self, name: &str, file: &str) -> &mut TestGroup { + self.0.entry(name.to_string()).or_insert_with(|| TestGroup::new(file)) + } + + pub fn run_files<'a>(&mut self, tests: impl IntoIterator<Item = TestFile<'a>>) -> Result<()> { + for file in tests { + self.run_file(file)?; + } Ok(()) } - fn imports(store: &mut tinywasm::Store, modules: &HashMap<std::string::String, ModuleInstance>) -> Result<Imports> { + fn imports(store: &mut Store, modules: &HashMap<String, ModuleInstance>) -> Result<Imports> { let mut imports = Imports::new(); let table = Table::new( @@ -95,51 +161,15 @@ impl TestSuite { TableType::new(WasmType::RefFunc, 10, Some(20)), WasmValue::default_for(WasmType::RefFunc), )?; - - let print = HostFunction::from(store, |_ctx: tinywasm::FuncContext, (): ()| { - log::debug!("print"); - Ok(()) - }); - - let print_i32 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, arg: i32| { - log::debug!("print_i32: {arg}"); - Ok(()) - }); - - let print_i64 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, arg: i64| { - log::debug!("print_i64: {arg}"); - Ok(()) - }); - - let print_f32 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, arg: f32| { - log::debug!("print_f32: {arg}"); - Ok(()) - }); - - let print_f64 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, arg: f64| { - log::debug!("print_f64: {arg}"); - Ok(()) - }); - - let print_i32_f32 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, args: (i32, f32)| { - log::debug!("print_i32_f32: {}, {}", args.0, args.1); - Ok(()) - }); - - let print_f64_f64 = HostFunction::from(store, |_ctx: tinywasm::FuncContext, args: (f64, f64)| { - log::debug!("print_f64_f64: {}, {}", args.0, args.1); - Ok(()) - }); - let memory = Memory::new(store, MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)))?; let global_i32 = - Global::new(store, tinywasm_types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?; + Global::new(store, tinywasm::types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?; let global_i64 = - Global::new(store, tinywasm_types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?; + Global::new(store, tinywasm::types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?; let global_f32 = - Global::new(store, tinywasm_types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?; + Global::new(store, tinywasm::types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?; let global_f64 = - Global::new(store, tinywasm_types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?; + Global::new(store, tinywasm::types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?; imports .define("spectest", "memory", memory) @@ -148,45 +178,36 @@ impl TestSuite { .define("spectest", "global_i64", global_i64) .define("spectest", "global_f32", global_f32) .define("spectest", "global_f64", global_f64) - .define("spectest", "print", print) - .define("spectest", "print_i32", print_i32) - .define("spectest", "print_i64", print_i64) - .define("spectest", "print_f32", print_f32) - .define("spectest", "print_f64", print_f64) - .define("spectest", "print_i32_f32", print_i32_f32) - .define("spectest", "print_f64_f64", print_f64_f64); + .define("spectest", "print", HostFunction::from(store, |_ctx: tinywasm::FuncContext, (): ()| Ok(()))) + .define("spectest", "print_i32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i32| Ok(()))) + .define("spectest", "print_i64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i64| Ok(()))) + .define("spectest", "print_f32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f32| Ok(()))) + .define("spectest", "print_f64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f64| Ok(()))) + .define( + "spectest", + "print_i32_f32", + HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (i32, f32)| Ok(())), + ) + .define( + "spectest", + "print_f64_f64", + HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (f64, f64)| Ok(())), + ); for (name, module) in modules { - log::debug!("registering module: {name}"); imports.link_module(name, module.clone())?; } Ok(imports) } - pub fn run_files<'a>(&mut self, tests: impl IntoIterator<Item = TestFile<'a>>) -> Result<()> { - tests.into_iter().for_each(|group| { - let name = group.name(); - println!("running group: {name}"); - if self.1.contains(&name.to_string()) { - info!("skipping group: {name}"); - self.test_group(&format!("{name} (skipped)"), name); - return; - } - - self.run_file(group).expect("failed to run group"); - }); - - Ok(()) - } - pub fn run_file(&mut self, file: TestFile<'_>) -> Result<()> { let test_group = self.test_group(file.name(), file.parent()); let wast_raw = file.raw(); let wast = file.wast()?; let directives = wast.directives()?; - let mut store = tinywasm::Store::default(); + let mut store = Store::default(); let mut module_registry = ModuleRegistry::default(); println!("running {} tests for group: {}", directives.len(), file.name()); @@ -210,19 +231,16 @@ impl TestSuite { module_registry.register(name.to_string(), last); test_group.add_result(&format!("Register({i})"), span.linecol_in(wast_raw), Ok(())); } - Wat(module) => { - debug!("got wat module"); let result = catch_unwind_silent(|| { let (name, bytes) = encode_quote_wat(module); let module = parse_module_bytes(&bytes).expect("failed to parse module bytes"); let imports = Self::imports(&mut store, module_registry.modules()).unwrap(); let module_instance = ModuleInstance::instantiate(&mut store, &module, Some(imports)) .expect("failed to instantiate module"); - (name, module_instance) }) - .map_err(|e| eyre!("failed to parse wat module: {:?}", try_downcast_panic(e))); + .map_err(|e| eyre!("failed to parse wat module: {}", try_downcast_panic(e))); match &result { Err(err) => debug!("failed to parse module: {err:?}"), @@ -231,49 +249,39 @@ impl TestSuite { test_group.add_result(&format!("Wat({i})"), span.linecol_in(wast_raw), result.map(|_| ())); } - AssertMalformed { span, mut module, message } => { - let Ok(module) = module.encode() else { + let Ok(encoded) = module.encode() else { test_group.add_result(&format!("AssertMalformed({i})"), span.linecol_in(wast_raw), Ok(())); continue; }; - - let res = catch_unwind_silent(|| parse_module_bytes(&module)) - .map_err(|e| eyre!("failed to parse module (expected): {:?}", try_downcast_panic(e))) + let res = catch_unwind_silent(|| parse_module_bytes(&encoded)) + .map_err(|e| eyre!("failed to parse module (expected): {}", try_downcast_panic(e))) .and_then(|res| res); - test_group.add_result( &format!("AssertMalformed({i})"), span.linecol_in(wast_raw), match res { Ok(_) => { - // - skip "zero byte expected" as the magic number is not checked by wasmparser - // (Don't need to error on this, doesn't matter if it's malformed) - // - skip "integer representation too long" as this has some false positives on older tests if message == "zero byte expected" || message == "integer representation too long" || message == "zero flag expected" { continue; } - Err(eyre!("expected module to be malformed: {message}")) } Err(_) => Ok(()), }, ); } - AssertInvalid { span, mut module, message } => { if ["multiple memories", "type mismatch"].contains(&message) { test_group.add_result(&format!("AssertInvalid({i})"), span.linecol_in(wast_raw), Ok(())); continue; } - let res = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap())) - .map_err(|e| eyre!("failed to parse module (invalid): {:?}", try_downcast_panic(e))) + .map_err(|e| eyre!("failed to parse module (invalid): {}", try_downcast_panic(e))) .and_then(|res| res); - test_group.add_result( &format!("AssertInvalid({i})"), span.linecol_in(wast_raw), @@ -283,13 +291,11 @@ impl TestSuite { }, ); } - AssertExhaustion { call, message, span } => { let module = module_registry.get_idx(call.module); - let args = convert_wastargs(call.args).expect("failed to convert args"); + let args = convert_wastargs(call.args)?; let res = catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ())); - let Ok(Err(tinywasm::Error::Trap(trap))) = res else { test_group.add_result( &format!("AssertExhaustion({i})"), @@ -298,7 +304,6 @@ impl TestSuite { ); continue; }; - if !message.starts_with(trap.message()) && !trap.message().starts_with(message) { test_group.add_result( &format!("AssertExhaustion({i})"), @@ -307,10 +312,8 @@ impl TestSuite { ); continue; } - test_group.add_result(&format!("AssertExhaustion({i})"), span.linecol_in(wast_raw), Ok(())); } - AssertTrap { exec, message, span } => { let res: Result<tinywasm::Result<()>, _> = catch_unwind_silent(|| { let invoke = match exec { @@ -321,22 +324,19 @@ impl TestSuite { ModuleInstance::instantiate(&mut store, &module, Some(imports))?; return Ok(()); } - wast::WastExecute::Get { .. } => { - panic!("get not supported"); - } + wast::WastExecute::Get { .. } => panic!("get not supported"), wast::WastExecute::Invoke(invoke) => invoke, }; - let module = module_registry.get_idx(invoke.module); - let args = convert_wastargs(invoke.args).expect("failed to convert args"); + let args = + convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?; exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) }); - match res { Err(err) => test_group.add_result( &format!("AssertTrap({i})"), span.linecol_in(wast_raw), - Err(eyre!("test panicked: {:?}", try_downcast_panic(err))), + Err(eyre!("test panicked: {}", try_downcast_panic(err))), ), Ok(Err(tinywasm::Error::Trap(trap))) => { if !message.starts_with(trap.message()) && !trap.message().starts_with(message) { @@ -347,7 +347,6 @@ impl TestSuite { ); continue; } - test_group.add_result(&format!("AssertTrap({i})"), span.linecol_in(wast_raw), Ok(())); } Ok(Err(err)) => test_group.add_result( @@ -362,7 +361,6 @@ impl TestSuite { ), } } - AssertUnlinkable { mut module, span, message } => { let res = catch_unwind_silent(|| { let module = parse_module_bytes(&module.encode().expect("failed to encode module")) @@ -370,12 +368,11 @@ impl TestSuite { let imports = Self::imports(&mut store, module_registry.modules()).unwrap(); ModuleInstance::instantiate(&mut store, &module, Some(imports)) }); - match res { Err(err) => test_group.add_result( &format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), - Err(eyre!("test panicked: {:?}", try_downcast_panic(err))), + Err(eyre!("test panicked: {}", try_downcast_panic(err))), ), Ok(Err(tinywasm::Error::Linker(err))) => { if err.message() != message @@ -389,7 +386,6 @@ impl TestSuite { ); continue; } - test_group.add_result(&format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), Ok(())); } Ok(Err(err)) => test_group.add_result( @@ -404,10 +400,8 @@ impl TestSuite { ), } } - Invoke(invoke) => { let name = invoke.name; - let res: Result<Result<()>, _> = catch_unwind_silent(|| { let args = convert_wastargs(invoke.args)?; let module = module_registry.get_idx(invoke.module); @@ -417,19 +411,16 @@ impl TestSuite { })?; Ok(()) }); - - let res = res.map_err(|e| eyre!("test panicked: {:?}", try_downcast_panic(e))).and_then(|r| r); + let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); test_group.add_result(&format!("Invoke({name}-{i})"), span.linecol_in(wast_raw), res); } - AssertReturn { span, exec, results } => { - info!("AssertReturn: {exec:?}"); let expected_alternatives = match convert_wastret(results.into_iter()) { Err(err) => { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("failed to convert expected results: {:?}", err)), + Err(eyre!("failed to convert expected results: {err:?}")), ); continue; } @@ -439,8 +430,7 @@ impl TestSuite { let invoke = match match exec { wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")), wast::WastExecute::Get { module: module_id, global, .. } => { - let module = module_registry.get(module_id); - let Some(module) = module else { + let Some(module) = module_registry.get(module_id) else { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), @@ -448,17 +438,13 @@ impl TestSuite { ); continue; }; - - let module_global = match match module.export_addr(global) { - Some(ExternVal::Global(addr)) => Ok(store.get_global_val(addr)), - _ => Err(eyre!("no module to get global from")), - } { - Ok(module_global) => module_global, + let module_global = match module.global_get(&store, global) { + Ok(value) => value, Err(err) => { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("failed to get global: {:?}", err)), + Err(eyre!("failed to get global: {err:?}")), ); continue; } @@ -466,8 +452,7 @@ impl TestSuite { let expected = expected_alternatives .iter() .filter_map(|alts| alts.first()) - .find(|exp| module_global.attach_type(WasmType::from(*exp)).unwrap().eq_loose(exp)); - + .find(|exp| module_global.eq_loose(exp)); if expected.is_none() { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), @@ -479,15 +464,12 @@ impl TestSuite { ); continue; } - test_group.add_result( &format!("AssertReturn({global}-{i})"), span.linecol_in(wast_raw), Ok(()), ); - continue; - // check if module_global matches the expected results } wast::WastExecute::Invoke(invoke) => Ok(invoke), } { @@ -496,7 +478,7 @@ impl TestSuite { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("unsupported directive: {:?}", err)), + Err(eyre!("unsupported directive: {err:?}")), ); continue; } @@ -504,23 +486,19 @@ impl TestSuite { let invoke_name = invoke.name; let res: Result<Result<()>, _> = catch_unwind_silent(|| { - debug!("invoke: {invoke:?}"); let args = convert_wastargs(invoke.args)?; let module = module_registry.get_idx(invoke.module); let outcomes = exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| { error!("failed to execute function: {e:?}"); e })?; - if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) { return Err(eyre!( - "span: {:?} expected {} results, got {}", - span, + "expected {} results, got {}", expected_alternatives.first().map_or(0, |v| v.len()), outcomes.len() )); } - if expected_alternatives.iter().any(|expected| { expected.len() == outcomes.len() && outcomes.iter().zip(expected.iter()).all(|(outcome, exp)| outcome.eq_loose(exp)) @@ -531,7 +509,7 @@ impl TestSuite { } }); - let res = res.map_err(|e| eyre!("test panicked: {:?}", try_downcast_panic(e))).and_then(|r| r); + let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); test_group.add_result(&format!("AssertReturn({invoke_name}-{i})"), span.linecol_in(wast_raw), res); } _ => test_group.add_result( @@ -545,3 +523,363 @@ impl TestSuite { Ok(()) } } + +impl Display for WastRunner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + use owo_colors::OwoColorize; + + let mut total_passed = 0; + let mut total_failed = 0; + + for group in self.group_results() { + total_passed += group.passed; + total_failed += group.failed; + + writeln!(f, "{}", group.name.bold().underline())?; + writeln!(f, " Tests Passed: {}", group.passed.to_string().green())?; + if group.failed != 0 { + writeln!(f, " Tests Failed: {}", group.failed.to_string().red())?; + } + } + + writeln!(f, "\n{}", "Total Test Summary:".bold().underline())?; + writeln!(f, " Total Tests: {}", total_passed + total_failed)?; + writeln!(f, " Total Passed: {}", total_passed.to_string().green())?; + writeln!(f, " Total Failed: {}", total_failed.to_string().red())?; + Ok(()) + } +} + +#[derive(Debug)] +struct TestGroup { + tests: Vec<TestCase>, + file: String, +} + +impl TestGroup { + fn new(file: &str) -> Self { + Self { tests: Vec::new(), file: file.to_string() } + } + + fn stats(&self) -> (usize, usize) { + let mut passed = 0; + let mut failed = 0; + for test in &self.tests { + match test.result { + Ok(()) => passed += 1, + Err(_) => failed += 1, + } + } + (passed, failed) + } + + fn add_result(&mut self, name: &str, linecol: (usize, usize), result: Result<()>) { + self.tests.push(TestCase { name: name.to_string(), linecol, result }); + } +} + +#[derive(Debug)] +struct TestCase { + name: String, + linecol: (usize, usize), + result: Result<()>, +} + +fn expand_paths(paths: &[PathBuf]) -> Result<Vec<PathBuf>> { + let mut files = Vec::new(); + for path in paths { + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "wast") { + files.push(path); + } + } + } else { + files.push(path.clone()); + } + } + files.sort(); + Ok(files) +} + +#[derive(Debug)] +pub struct TestFile<'a> { + pub name: String, + pub contents: &'a str, + pub parent: String, +} + +impl<'a> TestFile<'a> { + pub fn name(&self) -> &str { + &self.name + } + + pub fn raw(&self) -> &'a str { + self.contents + } + + pub fn parent(&self) -> &str { + &self.parent + } + + pub fn wast(&self) -> wast::parser::Result<WastBuffer<'a>> { + let mut lexer = wast::lexer::Lexer::new(self.contents); + lexer.allow_confusing_unicode(true); + let parse_buffer = wast::parser::ParseBuffer::new_with_lexer(lexer)?; + Ok(WastBuffer { buffer: parse_buffer }) + } +} + +pub struct WastBuffer<'a> { + buffer: wast::parser::ParseBuffer<'a>, +} + +impl<'a> WastBuffer<'a> { + pub fn directives(&'a self) -> wast::parser::Result<Vec<wast::WastDirective<'a>>> { + Ok(wast::parser::parse::<wast::Wast<'a>>(&self.buffer)?.directives) + } +} + +fn exec_with_budget( + func: &tinywasm::Function, + store: &mut Store, + args: &[WasmValue], +) -> Result<Vec<WasmValue>, tinywasm::Error> { + let mut exec = func.call_resumable(store, args)?; + for _ in 0..TEST_MAX_SUSPENSIONS { + match exec.resume_with_time_budget(TEST_TIME_SLICE)? { + ExecProgress::Completed(values) => return Ok(values), + ExecProgress::Suspended => {} + } + } + Err(tinywasm::Error::Other(format!( + "testsuite execution timed out after {} time slices of {:?}", + TEST_MAX_SUSPENSIONS, TEST_TIME_SLICE + ))) +} + +fn try_downcast_panic(panic: Box<dyn std::any::Any + Send>) -> String { + let info = panic.downcast_ref::<panic::PanicHookInfo>().map(ToString::to_string); + let info_string = panic.downcast_ref::<String>().cloned(); + let info_str = panic.downcast::<&str>().ok().map(|s| *s); + info.unwrap_or_else(|| info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string()) +} + +fn exec_fn_instance( + instance: Option<u32>, + store: &mut Store, + name: &str, + args: &[WasmValue], +) -> Result<Vec<WasmValue>, tinywasm::Error> { + let Some(instance) = instance else { + return Err(tinywasm::Error::Other("no instance found".to_string())); + }; + let Some(instance) = store.get_module_instance(instance) else { + return Err(tinywasm::Error::Other("no instance found".to_string())); + }; + let func = instance.func_untyped(store, name)?; + exec_with_budget(&func, store, args) +} + +fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> { + let prev_hook = panic::take_hook(); + panic::set_hook(Box::new(|_| {})); + let result = panic::catch_unwind(AssertUnwindSafe(f)); + panic::set_hook(prev_hook); + result +} + +fn encode_quote_wat(module: QuoteWat) -> (Option<String>, Vec<u8>) { + match module { + QuoteWat::QuoteModule(_, quoted_wat) => { + let wat = quoted_wat + .iter() + .map(|(_, s)| std::str::from_utf8(s).expect("failed to convert wast to utf8")) + .collect::<Vec<_>>() + .join("\n"); + let lexer = wast::lexer::Lexer::new(&wat); + let buf = wast::parser::ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer"); + let mut wat_data = wast::parser::parse::<wast::Wat>(&buf).expect("failed to parse wat"); + (None, wat_data.encode().expect("failed to encode module")) + } + QuoteWat::Wat(mut wat) => { + let wast::Wat::Module(ref module) = wat else { unimplemented!("Not supported") }; + (module.id.map(|id| id.name().to_string()), wat.encode().expect("failed to encode module")) + } + QuoteWat::QuoteComponent(..) => unimplemented!("components are not supported"), + } +} + +fn parse_module_bytes(bytes: &[u8]) -> Result<Module> { + Ok(tinywasm::parse_bytes(bytes)?) +} + +fn convert_wastargs(args: Vec<wast::WastArg>) -> Result<Vec<WasmValue>> { + args.into_iter().map(wastarg2tinywasmvalue).collect() +} + +fn convert_wastret<'a>(args: impl Iterator<Item = wast::WastRet<'a>>) -> Result<Vec<Vec<WasmValue>>> { + let mut alternatives = vec![Vec::new()]; + for arg in args { + let choices = wastret2tinywasmvalues(arg)?; + let mut next = Vec::with_capacity(alternatives.len() * choices.len()); + for prefix in alternatives { + for choice in &choices { + let mut candidate = prefix.clone(); + candidate.push(*choice); + next.push(candidate); + } + } + alternatives = next; + } + Ok(alternatives) +} + +fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result<WasmValue> { + let wast::WastArg::Core(arg) = arg else { bail!("unsupported arg type: Component") }; + use wast::core::WastArgCore::*; + Ok(match arg { + F32(f) => WasmValue::F32(f32::from_bits(f.bits)), + F64(f) => WasmValue::F64(f64::from_bits(f.bits)), + I32(i) => WasmValue::I32(i), + I64(i) => WasmValue::I64(i), + V128(i) => WasmValue::V128(i128::from_le_bytes(i.to_le_bytes())), + RefExtern(v) => WasmValue::RefExtern(ExternRef::new(Some(v))), + RefNull(t) => match t { + wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => { + WasmValue::RefFunc(FuncRef::null()) + } + wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern } => { + WasmValue::RefExtern(ExternRef::null()) + } + _ => bail!("unsupported arg type: refnull: {:?}", t), + }, + RefHost(_) => bail!("unsupported arg type: RefHost"), + }) +} + +fn wast_i128_to_i128(i: wast::core::V128Pattern) -> i128 { + let res: Vec<u8> = match i { + wast::core::V128Pattern::F32x4(f) => { + f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f32().unwrap().to_le_bytes()).collect() + } + wast::core::V128Pattern::F64x2(f) => { + f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f64().unwrap().to_le_bytes()).collect() + } + wast::core::V128Pattern::I16x8(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), + wast::core::V128Pattern::I32x4(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), + wast::core::V128Pattern::I64x2(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), + wast::core::V128Pattern::I8x16(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), + }; + i128::from_le_bytes(res.try_into().unwrap()) +} + +fn wastret2tinywasmvalues(ret: wast::WastRet) -> Result<Vec<WasmValue>> { + let wast::WastRet::Core(ret) = ret else { bail!("unsupported arg type") }; + match ret { + wast::core::WastRetCore::Either(options) => { + options.into_iter().map(wastretcore2tinywasmvalue).collect::<Result<Vec<_>>>() + } + ret => Ok(vec![wastretcore2tinywasmvalue(ret)?]), + } +} + +fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result<WasmValue> { + use wast::core::WastRetCore::{F32, F64, I32, I64, RefExtern, RefFunc, RefNull, V128}; + Ok(match ret { + F32(f) => nanpattern2tinywasmvalue(f)?, + F64(f) => nanpattern2tinywasmvalue(f)?, + I32(i) => WasmValue::I32(i), + I64(i) => WasmValue::I64(i), + V128(i) => WasmValue::V128(wast_i128_to_i128(i)), + RefNull(t) => match t { + Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func }) => { + WasmValue::RefFunc(FuncRef::null()) + } + Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern }) => { + WasmValue::RefExtern(ExternRef::null()) + } + _ => bail!("unsupported arg type: refnull: {:?}", t), + }, + RefExtern(v) => WasmValue::RefExtern(ExternRef::new(v)), + RefFunc(v) => WasmValue::RefFunc(FuncRef::new(match v { + Some(wast::token::Index::Num(n, _)) => Some(n), + _ => bail!("unsupported arg type: reffunc: {:?}", v), + })), + a => bail!("unsupported arg type {:?}", a), + }) +} + +enum Bits { + U32(u32), + U64(u64), +} + +trait FloatToken { + fn bits(&self) -> Bits; + fn canonical_nan() -> WasmValue; + fn arithmetic_nan() -> WasmValue; + fn value(&self) -> WasmValue { + match self.bits() { + Bits::U32(v) => WasmValue::F32(f32::from_bits(v)), + Bits::U64(v) => WasmValue::F64(f64::from_bits(v)), + } + } +} + +impl FloatToken for wast::token::F32 { + fn bits(&self) -> Bits { + Bits::U32(self.bits) + } + fn canonical_nan() -> WasmValue { + WasmValue::F32(f32::NAN) + } + fn arithmetic_nan() -> WasmValue { + WasmValue::F32(f32::NAN) + } +} + +impl FloatToken for wast::token::F64 { + fn bits(&self) -> Bits { + Bits::U64(self.bits) + } + fn canonical_nan() -> WasmValue { + WasmValue::F64(f64::NAN) + } + fn arithmetic_nan() -> WasmValue { + WasmValue::F64(f64::NAN) + } +} + +fn nanpattern2tinywasmvalue<T>(arg: wast::core::NanPattern<T>) -> Result<WasmValue> +where + T: FloatToken, +{ + use wast::core::NanPattern::{ArithmeticNan, CanonicalNan, Value}; + Ok(match arg { + CanonicalNan => T::canonical_nan(), + ArithmeticNan => T::arithmetic_nan(), + Value(v) => v.value(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runs_simple_wast_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("simple.wast"); + std::fs::write( + &path, + "(module (func (export \"add\") (result i32) i32.const 1))\n(assert_return (invoke \"add\") (i32.const 1))", + ) + .unwrap(); + + let mut runner = WastRunner::new(); + runner.run_paths(&[path]).unwrap(); + } +} diff --git a/crates/cli/src/wat.rs b/crates/cli/src/wat.rs deleted file mode 100644 index 8d2998d..0000000 --- a/crates/cli/src/wat.rs +++ /dev/null @@ -1,10 +0,0 @@ -use wast::{ - Wat, - parser::{self, ParseBuffer}, -}; - -pub fn wat2wasm(wat: &str) -> Vec<u8> { - let buf = ParseBuffer::new(wat).expect("failed to create parse buffer"); - let mut module = parser::parse::<Wat>(&buf).expect("failed to parse wat"); - module.encode().expect("failed to encode wat") -} diff --git a/crates/cli/tests/cli.rs b/crates/cli/tests/cli.rs new file mode 100644 index 0000000..b7d303e --- /dev/null +++ b/crates/cli/tests/cli.rs @@ -0,0 +1,137 @@ +use std::fs; + +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::tempdir; + +fn write_module(dir: &tempfile::TempDir, name: &str, source: &str) -> String { + let path = dir.path().join(name); + fs::write(&path, source).unwrap(); + path.to_string_lossy().into_owned() +} + +#[test] +fn run_invoke_accepts_positional_args() { + let dir = tempdir().unwrap(); + let module = write_module( + &dir, + "add.wat", + r#"(module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add))"#, + ); + + Command::cargo_bin("tinywasm") + .unwrap() + .args(["run", "--invoke", "add", &module, "1", "2"]) + .assert() + .success() + .stdout(predicate::str::contains("i32(3)")); +} + +#[test] +fn compile_and_run_twasm() { + let dir = tempdir().unwrap(); + let input = write_module( + &dir, + "add.wat", + r#"(module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add))"#, + ); + let output = dir.path().join("add.twasm"); + + Command::cargo_bin("tinywasm") + .unwrap() + .args(["compile", &input, "-o", output.to_str().unwrap()]) + .assert() + .success(); + + Command::cargo_bin("tinywasm") + .unwrap() + .args(["run", "--invoke", "add", output.to_str().unwrap(), "3", "4"]) + .assert() + .success() + .stdout(predicate::str::contains("i32(7)")); +} + +#[test] +fn bare_run_requires_default_entrypoint() { + let dir = tempdir().unwrap(); + let module = write_module(&dir, "add.wat", r#"(module (func (export "add") (result i32) i32.const 1))"#); + + Command::cargo_bin("tinywasm") + .unwrap() + .arg(&module) + .assert() + .failure() + .stderr(predicate::str::contains("no start function or `_start` export")); +} + +#[test] +fn inspect_lists_exports() { + let dir = tempdir().unwrap(); + let module = write_module( + &dir, + "inspect.wat", + r#"(module + (memory (export "memory") 1) + (func (export "answer") (result i32) i32.const 42))"#, + ); + + Command::cargo_bin("tinywasm").unwrap().args(["inspect", &module]).assert().success().stdout( + predicate::str::contains("answer: func () -> (i32)").and(predicate::str::contains("memory: memory[i32]")), + ); +} + +#[test] +fn dump_prints_lowered_instructions() { + let dir = tempdir().unwrap(); + let module = write_module(&dir, "dump.wat", r#"(module (func (export "noop")))"#); + + Command::cargo_bin("tinywasm") + .unwrap() + .args(["dump", &module]) + .assert() + .success() + .stdout(predicate::str::contains("func[0]").and(predicate::str::contains("0000:"))); +} + +#[test] +fn run_accepts_wat_from_stdin() { + Command::cargo_bin("tinywasm") + .unwrap() + .args(["run", "--invoke", "add", "-", "8", "9"]) + .write_stdin( + r#"(module + (func (export "add") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add))"#, + ) + .assert() + .success() + .stdout(predicate::str::contains("i32(17)")); +} + +#[test] +fn wast_command_runs_simple_spec_script() { + let dir = tempdir().unwrap(); + let path = dir.path().join("simple.wast"); + fs::write( + &path, + "(module (func (export \"add\") (result i32) i32.const 1))\n(assert_return (invoke \"add\") (i32.const 1))", + ) + .unwrap(); + + Command::cargo_bin("tinywasm") + .unwrap() + .args(["wast", path.to_str().unwrap()]) + .assert() + .success() + .stdout(predicate::str::contains("Tests Passed:")); +} diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 2f80695..15ae832 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -27,6 +27,7 @@ libm={version="0.2", default-features=false} [dev-dependencies] wasm-testsuite.workspace=true +tinywasm-cli={version="0.9.0-alpha.0", path="../cli", features=["wast"]} indexmap.workspace=true wast.workspace=true wat.workspace=true diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index 8198b55..e9f1029 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use core::fmt::Debug; use core::hint::cold_path; -use crate::{Function, Global, LinkingError, Memory, Result, Table, log}; +use crate::{Function, Global, LinkingError, Memory, Result, Table}; use tinywasm_types::*; #[derive(Clone)] @@ -147,7 +147,6 @@ impl Imports { fn compare_types<T: PartialEq>(import: &Import, actual: &T, expected: &T) -> Result<()> { if expected != actual { cold_path(); - log::error!("failed to link import {}", import.name); return Err(LinkingError::incompatible_import_type(import).into()); } Ok(()) @@ -157,7 +156,6 @@ impl Imports { fn compare_types<T: PartialEq + Debug>(import: &Import, actual: &T, expected: &T) -> Result<()> { if expected != actual { cold_path(); - log::error!("failed to link import {}: expected {:?}, got {:?}", import.name, expected, actual); return Err(LinkingError::incompatible_import_type(import).into()); } Ok(()) diff --git a/crates/tinywasm/tests/test-wast.rs b/crates/tinywasm/tests/test-wast.rs index ff2b2ed..663a5fb 100644 --- a/crates/tinywasm/tests/test-wast.rs +++ b/crates/tinywasm/tests/test-wast.rs @@ -1,9 +1,7 @@ use std::path::PathBuf; use eyre::{Result, bail}; -use testsuite::TestSuite; - -mod testsuite; +use tinywasm_cli::wast_runner::WastRunner; fn main() -> Result<()> { let args = std::env::args().collect::<Vec<_>>(); @@ -27,9 +25,6 @@ fn main() -> Result<()> { vec![arg] }; - TestSuite::set_log_level(log::LevelFilter::Debug); - let mut test_suite = TestSuite::new(); - test_suite.run_paths(&files)?; - test_suite.print_errors(); - test_suite.report_status() + let mut test_suite = WastRunner::new(); + test_suite.run_paths(&files) } diff --git a/crates/tinywasm/tests/testsuite/mod.rs b/crates/tinywasm/tests/testsuite/mod.rs index 0221c6a..ea44c29 100644 --- a/crates/tinywasm/tests/testsuite/mod.rs +++ b/crates/tinywasm/tests/testsuite/mod.rs @@ -1,19 +1,11 @@ -#![allow(unused)] +#![allow(dead_code)] + use eyre::{Result, eyre}; -use indexmap::IndexMap; use owo_colors::OwoColorize; -use std::fmt::Display; -use std::io::{BufRead, Seek, SeekFrom}; -use std::{ - collections::BTreeMap, - fmt::{Debug, Formatter}, - io::BufReader, -}; - -mod run; -mod util; - use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::io::{BufRead, BufReader, Seek, SeekFrom}; +use tinywasm_cli::wast_runner::{GroupResult, TestFile as RunnerTestFile, WastRunner}; #[derive(Serialize, Deserialize)] pub struct TestGroupResult { @@ -22,56 +14,74 @@ pub struct TestGroupResult { pub failed: usize, } -pub struct TestSuite(BTreeMap<String, TestGroup>, Vec<String>); +pub struct TestSuite { + runner: WastRunner, +} impl TestSuite { pub fn set_log_level(level: log::LevelFilter) { - pretty_env_logger::formatted_builder().filter_level(level).init(); + WastRunner::set_log_level(level); } - pub fn report_status(&self) -> Result<()> { - if self.failed() { - println!(); - Err(eyre!(format!("{}:\n{self}", "failed one or more tests".red().bold()))) - } else { - println!("\n\npassed all tests:\n{self}"); - Ok(()) - } + pub fn new() -> Self { + Self { runner: WastRunner::new() } } - pub fn print_errors(&self) { - for (group_name, group) in &self.0 { - let tests = &group.tests; - for (test_name, test) in tests { - if let Err(e) = &test.result { - eprintln!( - "{} {} failed: {:?}", - link(group_name, &group.file, Some(test.linecol.0 + 1)).bold().underline(), - test_name.bold(), - e.to_string().bright_red() - ); + pub fn run_paths(&mut self, tests: &[std::path::PathBuf]) -> Result<()> { + let mut files = Vec::new(); + for path in tests { + if path.is_dir() { + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "wast") { + files.push(path); + } } + } else { + files.push(path.clone()); } } - } + files.sort(); - pub fn new() -> Self { - Self(BTreeMap::new(), Vec::new()) + let runner_files = files + .iter() + .map(|path| { + let contents = std::fs::read_to_string(path)?; + let name = path.to_string_lossy().into_owned(); + Ok((name, contents)) + }) + .collect::<Result<Vec<_>>>()?; + + self.runner.run_files(runner_files.iter().map(|(name, contents)| RunnerTestFile { + name: name.clone(), + parent: name.clone(), + contents, + })) } - pub fn skip(&mut self, name: &str) { - self.1.push(name.to_string()); + pub fn run_files<'a>(&mut self, tests: impl IntoIterator<Item = wasm_testsuite::data::TestFile<'a>>) -> Result<()> { + self.runner.run_files(tests.into_iter().map(|file| RunnerTestFile { + name: file.name().to_string(), + parent: file.parent().to_string(), + contents: file.raw(), + })) } - pub fn failed(&self) -> bool { - self.0.values().any(|group| group.stats().1 > 0) + pub fn print_errors(&self) { + self.runner.print_errors(); } - fn test_group(&mut self, name: &str, file: &str) -> &mut TestGroup { - self.0.entry(name.to_string()).or_insert_with(|| TestGroup::new(file)) + pub fn report_status(&self) -> Result<()> { + if self.runner.failed() { + println!(); + Err(eyre!(format!("{}:\n{self}", "failed one or more tests".red().bold()))) + } else { + println!("{self}"); + Ok(()) + } } - // create or add to a test result file pub fn save_csv(&self, path: &str, version: &str) -> Result<()> { use std::fs::OpenOptions; use std::io::Write; @@ -79,45 +89,40 @@ impl TestSuite { let mut file = OpenOptions::new().create(true).append(true).read(true).open(path)?; let last_line = BufReader::new(&file).lines().last().transpose()?; - // Check if the last line starts with the current commit if let Some(last) = last_line && last.starts_with(version) { - // Truncate the file size to remove the last line let len_to_truncate = last.len() as i64; file.set_len(file.metadata()?.len() - len_to_truncate as u64 - 1)?; } - // Seek to the end of the file for appending file.seek(SeekFrom::End(0))?; let mut passed = 0; let mut failed = 0; - let mut groups = Vec::new(); - for (name, group) in &self.0 { - let (group_passed, group_failed) = group.stats(); - passed += group_passed; - failed += group_failed; - groups.push(TestGroupResult { name: name.to_string(), passed: group_passed, failed: group_failed }); + for group in self.runner.group_results() { + passed += group.passed; + failed += group.failed; + groups.push(TestGroupResult { name: group.name, passed: group.passed, failed: group.failed }); } let groups = serde_json::to_string(&groups)?; let line = format!("{version},{passed},{failed},{groups}\n"); - file.write_all(line.as_bytes()).expect("failed to write to csv file"); - + file.write_all(line.as_bytes())?; Ok(()) } -} -fn link(name: &str, file: &str, line: Option<usize>) -> String { - let (path, name) = match line { - None => (file.to_string(), name.to_owned()), - Some(line) => (format!("{file}:{line}:0"), (format!("{name}:{line}"))), - }; + fn group_results(&self) -> Vec<GroupResult> { + self.runner.group_results() + } +} - format!("\x1b]8;;file://{path}\x1b\\{name}\x1b]8;;\x1b\\") +impl Default for TestSuite { + fn default() -> Self { + Self::new() + } } impl Display for TestSuite { @@ -125,59 +130,21 @@ impl Display for TestSuite { let mut total_passed = 0; let mut total_failed = 0; - for (group_name, group) in &self.0 { - let (group_passed, group_failed) = group.stats(); - total_passed += group_passed; - total_failed += group_failed; + for group in self.group_results() { + total_passed += group.passed; + total_failed += group.failed; - writeln!(f, "{}", link(group_name, &group.file, None).bold().underline())?; - writeln!(f, " Tests Passed: {}", group_passed.to_string().green())?; - - if group_failed != 0 { - writeln!(f, " Tests Failed: {}", group_failed.to_string().red())?; + writeln!(f, "{}", group.name.bold().underline())?; + writeln!(f, " Tests Passed: {}", group.passed.to_string().green())?; + if group.failed != 0 { + writeln!(f, " Tests Failed: {}", group.failed.to_string().red())?; } } writeln!(f, "\n{}", "Total Test Summary:".bold().underline())?; - writeln!(f, " Total Tests: {}", (total_passed + total_failed))?; + writeln!(f, " Total Tests: {}", total_passed + total_failed)?; writeln!(f, " Total Passed: {}", total_passed.to_string().green())?; writeln!(f, " Total Failed: {}", total_failed.to_string().red())?; Ok(()) } } - -#[derive(Debug)] -struct TestGroup { - tests: IndexMap<String, TestCase>, - file: String, -} - -impl TestGroup { - fn new(file: &str) -> Self { - Self { tests: IndexMap::new(), file: file.to_string() } - } - - fn stats(&self) -> (usize, usize) { - let mut passed_count = 0; - let mut failed_count = 0; - - for test in self.tests.values() { - match test.result { - Ok(()) => passed_count += 1, - Err(_) => failed_count += 1, - } - } - - (passed_count, failed_count) - } - - fn add_result(&mut self, name: &str, linecol: (usize, usize), result: Result<()>) { - self.tests.insert(name.to_string(), TestCase { result, linecol }); - } -} - -#[derive(Debug)] -struct TestCase { - result: Result<()>, - linecol: (usize, usize), -} diff --git a/crates/tinywasm/tests/testsuite/util.rs b/crates/tinywasm/tests/testsuite/util.rs deleted file mode 100644 index 1714543..0000000 --- a/crates/tinywasm/tests/testsuite/util.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::panic::{self, AssertUnwindSafe}; -use std::time::Duration; - -use eyre::{Result, bail, eyre}; -use tinywasm::{ExecProgress, ModuleInstance}; -use tinywasm_types::{ExternRef, FuncRef, Module, ModuleInstanceAddr, WasmType, WasmValue}; -use wasm_testsuite::wast; -use wasm_testsuite::wast::{QuoteWat, core::AbstractHeapType}; - -const TEST_TIME_SLICE: Duration = Duration::from_millis(20); -const TEST_MAX_SUSPENSIONS: u32 = 1000; - -fn exec_with_budget( - func: &tinywasm::Function, - store: &mut tinywasm::Store, - args: &[tinywasm_types::WasmValue], -) -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> { - let mut exec = func.call_resumable(store, args)?; - - for _ in 0..TEST_MAX_SUSPENSIONS { - match exec.resume_with_time_budget(TEST_TIME_SLICE)? { - ExecProgress::Completed(values) => return Ok(values), - ExecProgress::Suspended => {} - } - } - - Err(tinywasm::Error::Other(format!( - "testsuite execution timed out after {} time slices of {:?}", - TEST_MAX_SUSPENSIONS, TEST_TIME_SLICE - ))) -} - -pub fn try_downcast_panic(panic: Box<dyn std::any::Any + Send>) -> String { - let info = panic.downcast_ref::<panic::PanicHookInfo>().or(None).map(ToString::to_string).clone(); - let info_string = panic.downcast_ref::<String>().cloned(); - let info_str = panic.downcast::<&str>().ok().map(|s| *s); - info.unwrap_or(info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string()) -} - -pub fn exec_fn_instance( - instance: Option<ModuleInstanceAddr>, - store: &mut tinywasm::Store, - name: &str, - args: &[tinywasm_types::WasmValue], -) -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> { - let Some(instance) = instance else { - return Err(tinywasm::Error::Other("no instance found".to_string())); - }; - - let Some(instance) = store.get_module_instance(instance) else { - return Err(tinywasm::Error::Other("no instance found".to_string())); - }; - - let func = instance.func_untyped(store, name)?; - exec_with_budget(&func, store, args) -} - -pub fn exec_fn( - module: Option<&Module>, - name: &str, - args: &[tinywasm_types::WasmValue], - imports: Option<tinywasm::Imports>, -) -> Result<Vec<tinywasm_types::WasmValue>, tinywasm::Error> { - let Some(module) = module else { - return Err(tinywasm::Error::Other("no module found".to_string())); - }; - - let mut store = tinywasm::Store::default(); - let instance = ModuleInstance::instantiate(&mut store, module, imports)?; - let func = instance.func_untyped(&store, name)?; - exec_with_budget(&func, &mut store, args) -} - -pub fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> { - let prev_hook = panic::take_hook(); - panic::set_hook(Box::new(|_| {})); - let result = panic::catch_unwind(AssertUnwindSafe(f)); - panic::set_hook(prev_hook); - result -} - -pub fn encode_quote_wat(module: QuoteWat) -> (Option<String>, Vec<u8>) { - match module { - QuoteWat::QuoteModule(_, quoted_wat) => { - let wat = quoted_wat - .iter() - .map(|(_, s)| std::str::from_utf8(s).expect("failed to convert wast to utf8")) - .collect::<Vec<_>>() - .join("\n"); - - let lexer = wast::lexer::Lexer::new(&wat); - let buf = wast::parser::ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer"); - let mut wat_data = wast::parser::parse::<wast::Wat>(&buf).expect("failed to parse wat"); - (None, wat_data.encode().expect("failed to encode module")) - } - QuoteWat::Wat(mut wat) => { - let wast::Wat::Module(ref module) = wat else { - unimplemented!("Not supported"); - }; - (module.id.map(|id| id.name().to_string()), wat.encode().expect("failed to encode module")) - } - QuoteWat::QuoteComponent(..) => unimplemented!("components are not supported"), - } -} - -pub fn parse_module_bytes(bytes: &[u8]) -> Result<Module> { - let parser = tinywasm_parser::Parser::new(); - Ok(parser.parse_module_bytes(bytes)?) -} - -pub fn convert_wastargs(args: Vec<wast::WastArg>) -> Result<Vec<tinywasm_types::WasmValue>> { - args.into_iter().map(|a| wastarg2tinywasmvalue(a)).collect() -} - -pub fn convert_wastret<'a>( - args: impl Iterator<Item = wast::WastRet<'a>>, -) -> Result<Vec<Vec<tinywasm_types::WasmValue>>> { - let mut alternatives = vec![Vec::new()]; - - for arg in args { - let choices = wastret2tinywasmvalues(arg)?; - let mut next = Vec::with_capacity(alternatives.len() * choices.len()); - - for prefix in alternatives { - for choice in &choices { - let mut candidate = prefix.clone(); - candidate.push(*choice); - next.push(candidate); - } - } - - alternatives = next; - } - - Ok(alternatives) -} - -fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result<tinywasm_types::WasmValue> { - let wast::WastArg::Core(arg) = arg else { - bail!("unsupported arg type: Component"); - }; - - use wast::core::WastArgCore::*; - Ok(match arg { - F32(f) => WasmValue::F32(f32::from_bits(f.bits)), - F64(f) => WasmValue::F64(f64::from_bits(f.bits)), - I32(i) => WasmValue::I32(i), - I64(i) => WasmValue::I64(i), - V128(i) => WasmValue::V128(i128::from_le_bytes(i.to_le_bytes())), - RefExtern(v) => WasmValue::RefExtern(ExternRef::new(Some(v))), - RefNull(t) => match t { - wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => { - WasmValue::RefFunc(FuncRef::null()) - } - wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern } => { - WasmValue::RefExtern(ExternRef::null()) - } - _ => bail!("unsupported arg type: refnull: {:?}", t), - }, - RefHost(v) => bail!("unsupported arg type: RefHost"), - }) -} - -fn wast_i128_to_i128(i: wast::core::V128Pattern) -> i128 { - let res: Vec<u8> = match i { - wast::core::V128Pattern::F32x4(f) => { - f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f32().unwrap().to_le_bytes()).collect() - } - wast::core::V128Pattern::F64x2(f) => { - f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f64().unwrap().to_le_bytes()).collect() - } - wast::core::V128Pattern::I16x8(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), - wast::core::V128Pattern::I32x4(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), - wast::core::V128Pattern::I64x2(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), - wast::core::V128Pattern::I8x16(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(), - }; - - i128::from_le_bytes(res.try_into().unwrap()) -} - -fn wastret2tinywasmvalues(ret: wast::WastRet) -> Result<Vec<tinywasm_types::WasmValue>> { - let wast::WastRet::Core(ret) = ret else { - bail!("unsupported arg type"); - }; - - match ret { - wast::core::WastRetCore::Either(options) => { - options.into_iter().map(wastretcore2tinywasmvalue).collect::<Result<Vec<_>>>() - } - ret => Ok(vec![wastretcore2tinywasmvalue(ret)?]), - } -} - -fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result<tinywasm_types::WasmValue> { - use wast::core::WastRetCore::{F32, F64, I32, I64, RefExtern, RefFunc, RefNull, V128}; - Ok(match ret { - F32(f) => nanpattern2tinywasmvalue(f)?, - F64(f) => nanpattern2tinywasmvalue(f)?, - I32(i) => WasmValue::I32(i), - I64(i) => WasmValue::I64(i), - V128(i) => WasmValue::V128(wast_i128_to_i128(i)), - RefNull(t) => match t { - Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func }) => { - WasmValue::RefFunc(FuncRef::null()) - } - Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern }) => { - WasmValue::RefExtern(ExternRef::null()) - } - _ => bail!("unsupported arg type: refnull: {:?}", t), - }, - RefExtern(v) => WasmValue::RefExtern(ExternRef::new(v)), - RefFunc(v) => WasmValue::RefFunc(FuncRef::new(match v { - Some(wast::token::Index::Num(n, _)) => Some(n), - _ => bail!("unsupported arg type: reffunc: {:?}", v), - })), - a => bail!("unsupported arg type {:?}", a), - }) -} - -enum Bits { - U32(u32), - U64(u64), -} - -trait FloatToken { - fn bits(&self) -> Bits; - fn canonical_nan() -> WasmValue; - fn arithmetic_nan() -> WasmValue; - fn value(&self) -> WasmValue { - match self.bits() { - Bits::U32(v) => WasmValue::F32(f32::from_bits(v)), - Bits::U64(v) => WasmValue::F64(f64::from_bits(v)), - } - } -} - -impl FloatToken for wast::token::F32 { - fn bits(&self) -> Bits { - Bits::U32(self.bits) - } - - fn canonical_nan() -> WasmValue { - WasmValue::F32(f32::NAN) - } - - fn arithmetic_nan() -> WasmValue { - WasmValue::F32(f32::NAN) - } -} - -impl FloatToken for wast::token::F64 { - fn bits(&self) -> Bits { - Bits::U64(self.bits) - } - - fn canonical_nan() -> WasmValue { - WasmValue::F64(f64::NAN) - } - - fn arithmetic_nan() -> WasmValue { - WasmValue::F64(f64::NAN) - } -} - -fn nanpattern2tinywasmvalue<T>(arg: wast::core::NanPattern<T>) -> Result<tinywasm_types::WasmValue> -where - T: FloatToken, -{ - use wast::core::NanPattern::{ArithmeticNan, CanonicalNan, Value}; - Ok(match arg { - CanonicalNan => T::canonical_nan(), - ArithmeticNan => T::arithmetic_nan(), - Value(v) => v.value(), - }) -} diff --git a/examples/dump-bytecode.rs b/examples/dump-bytecode.rs deleted file mode 100644 index 912da41..0000000 --- a/examples/dump-bytecode.rs +++ /dev/null @@ -1,53 +0,0 @@ -use eyre::{Result, bail}; -use std::io::Read; -use std::path::Path; -use tinywasm::parser::Parser; -use tinywasm::types::{ExternalKind, ImportKind}; - -fn read_input(path: &str) -> Result<Vec<u8>> { - if path == "-" { - let mut source = String::new(); - std::io::stdin().read_to_string(&mut source)?; - return Ok(wat::parse_str(source)?); - } - - let bytes = std::fs::read(path)?; - let is_wasm = Path::new(path).extension().and_then(|s| s.to_str()) == Some("wasm"); - if is_wasm { Ok(bytes) } else { Ok(wat::parse_bytes(&bytes)?.into_owned()) } -} - -fn main() -> Result<()> { - let args = std::env::args().collect::<Vec<_>>(); - if args.len() != 2 { - bail!("usage: cargo run --example dump-bytecode -- <module.wat|module.wasm|->") - } - - let wasm = read_input(&args[1])?; - let module = Parser::new().parse_module_bytes(&wasm)?; - - let imported_func_count = - module.imports.iter().filter(|import| matches!(import.kind, ImportKind::Function(_))).count() as u32; - - for (func_idx, func) in module.funcs.iter().enumerate() { - let global_idx = imported_func_count + func_idx as u32; - let exports = module - .exports - .iter() - .filter(|export| export.kind == ExternalKind::Func && export.index == global_idx) - .map(|export| export.name.as_ref()) - .collect::<Vec<_>>(); - - if exports.is_empty() { - println!("func[{func_idx}] global={global_idx}"); - } else { - println!("func[{func_idx}] global={global_idx} exports={exports:?}"); - } - - for (ip, instr) in func.instructions.iter().enumerate() { - println!(" {ip:04}: {instr:?}"); - } - println!(); - } - - Ok(()) -} |
