summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHenry <mail@henrygressmann.de>2026-04-26 19:44:34 +0200
committerHenry <mail@henrygressmann.de>2026-04-26 19:44:34 +0200
commit463a3a6f56a5ca06cda890fd6fe7f16485ef70a0 (patch)
treebfa657a2de16c04cd36f4b6c61e77b370fcd46fc
parent27004a8738c7a3c32ac4fb6966f32647b4c96db7 (diff)
feat: new tinywasm cli
Signed-off-by: Henry <mail@henrygressmann.de>
-rw-r--r--.cargo/config.toml1
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock473
-rw-r--r--crates/cli/Cargo.toml29
-rw-r--r--crates/cli/README.md27
-rw-r--r--crates/cli/src/args.rs37
-rw-r--r--crates/cli/src/bin.rs119
-rw-r--r--crates/cli/src/cli.rs128
-rw-r--r--crates/cli/src/cmd/compile.rs15
-rw-r--r--crates/cli/src/cmd/completion.rs12
-rw-r--r--crates/cli/src/cmd/dump.rs71
-rw-r--r--crates/cli/src/cmd/inspect.rs35
-rw-r--r--crates/cli/src/cmd/mod.rs7
-rw-r--r--crates/cli/src/cmd/run.rs47
-rw-r--r--crates/cli/src/cmd/wast.rs12
-rw-r--r--crates/cli/src/engine_flags.rs154
-rw-r--r--crates/cli/src/lib.rs34
-rw-r--r--crates/cli/src/load.rs103
-rw-r--r--crates/cli/src/output.rs85
-rw-r--r--crates/cli/src/util.rs1
-rw-r--r--crates/cli/src/value_parse.rs55
-rw-r--r--crates/cli/src/wast_runner.rs (renamed from crates/tinywasm/tests/testsuite/run.rs)668
-rw-r--r--crates/cli/src/wat.rs10
-rw-r--r--crates/cli/tests/cli.rs137
-rw-r--r--crates/tinywasm/Cargo.toml1
-rw-r--r--crates/tinywasm/src/imports.rs4
-rw-r--r--crates/tinywasm/tests/test-wast.rs11
-rw-r--r--crates/tinywasm/tests/testsuite/mod.rs183
-rw-r--r--crates/tinywasm/tests/testsuite/util.rs275
-rw-r--r--examples/dump-bytecode.rs53
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
diff --git a/Cargo.lock b/Cargo.lock
index 0ee7915..43cc6a7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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, &params)?;
+ 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(())
-}