diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e15584..19d47a2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,5 +3,7 @@ version-dev="workspaces version --no-git-commit --force tinywasm*" dev="run -- -l debug run" test-mvp="test --package tinywasm --test test-mvp --release -- --enable " +test-2="test --package tinywasm --test test-two --release -- --enable " test-wast="test --package tinywasm --test test-wast -- --enable " +test-wast-release="test --package tinywasm --test test-wast --release -- --enable " generate-charts="test --package tinywasm --test generate-charts -- --enable " diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0baaba9..92d56f4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,9 +17,7 @@ jobs: submodules: true - name: Install stable Rust toolchain - run: | - rustup toolchain install stable - rustup default stable + run: rustup update stable - name: Build (stable) run: cargo +stable build --workspace --exclude wasm-testsuite @@ -27,19 +25,26 @@ jobs: - name: Run tests (stable) run: cargo +stable test --workspace --exclude wasm-testsuite + - name: Run MVP testsuite + run: cargo +stable test-mvp + test-no-std: name: Test without default features on nightly Rust runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true + - name: Install nightly Rust toolchain - run: | - rustup toolchain install nightly - rustup default nightly + run: rustup update nightly - name: Build (nightly, no default features) - run: cargo +nightly build --workspace --exclude wasm-testsuite --no-default-features + run: cargo +nightly build --workspace --exclude wasm-testsuite --exclude rust-wasm-examples --no-default-features - name: Run tests (nightly, no default features) - run: cargo +nightly test --workspace --exclude wasm-testsuite --no-default-features + run: cargo +nightly test --workspace --exclude wasm-testsuite --exclude rust-wasm-examples --no-default-features + + - name: Run MVP testsuite (nightly) + run: cargo +nightly test-mvp diff --git a/.gitignore b/.gitignore index 7bb669d..f5dcc83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target notes.md -examples/tinywasm.wat +examples/rust/out/* examples/wast/* diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..8705e5a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1 @@ +# TinyWasm's Architecture diff --git a/Cargo.lock b/Cargo.lock index 0bbe617..b145eff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,9 +112,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "bitvec" @@ -210,25 +210,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.48.5", -] - -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", + "windows-targets 0.52.0", ] [[package]] @@ -238,10 +229,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" dependencies = [ "backtrace", + "color-spantrace", "eyre", "indenter", "once_cell", "owo-colors 3.5.0", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors 3.5.0", + "tracing-core", + "tracing-error", ] [[package]] @@ -390,9 +395,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -423,9 +428,9 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] @@ -488,9 +493,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "freetype" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee38378a9e3db1cc693b4f88d166ae375338a0ff75cb8263e1c601d51f35dc6" +checksum = "efc8599a3078adf8edeb86c71e9f8fa7d88af5ca31e806a867756081f90f5d83" dependencies = [ "freetype-sys", "libc", @@ -498,11 +503,11 @@ dependencies = [ [[package]] name = "freetype-sys" -version = "0.13.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" +checksum = "66ee28c39a43d89fbed8b4798fb4ba56722cfd2b5af81f9326c27614ba88ecd5" dependencies = [ - "cmake", + "cc", "libc", "pkg-config", ] @@ -574,9 +579,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "humantime" @@ -609,15 +614,14 @@ dependencies = [ [[package]] name = "image" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" dependencies = [ "bytemuck", "byteorder", "color_quant", "jpeg-decoder", - "num-rational", "num-traits", "png", ] @@ -653,15 +657,15 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jpeg-decoder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -694,22 +698,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" @@ -733,27 +743,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.17" @@ -809,11 +798,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + [[package]] name = "pkg-config" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" [[package]] name = "plotters" @@ -863,9 +858,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.10" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -886,9 +881,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -950,9 +945,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -962,9 +957,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", @@ -1050,6 +1045,13 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-wasm-examples" +version = "0.0.0" +dependencies = [ + "tinywasm", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1067,11 +1069,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -1147,6 +1149,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1189,9 +1200,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "termcolor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -1216,6 +1227,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1233,9 +1254,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tinywasm" -version = "0.2.0" +version = "0.3.0" dependencies = [ "eyre", + "libm", "log", "owo-colors 4.0.0", "plotters", @@ -1250,7 +1272,7 @@ dependencies = [ [[package]] name = "tinywasm-cli" -version = "0.2.0" +version = "0.3.0" dependencies = [ "argh", "color-eyre", @@ -1262,21 +1284,70 @@ dependencies = [ [[package]] name = "tinywasm-parser" -version = "0.2.0" +version = "0.3.0" dependencies = [ "log", "tinywasm-types", "wasmparser-nostd", ] +[[package]] +name = "tinywasm-root" +version = "0.0.0" +dependencies = [ + "color-eyre", + "tinywasm", +] + [[package]] name = "tinywasm-types" -version = "0.2.0" +version = "0.3.0" dependencies = [ "log", "rkyv", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "ttf-parser" version = "0.17.1" @@ -1303,9 +1374,15 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "uuid" -version = "1.6.1" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" + +[[package]] +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "version_check" @@ -1331,9 +1408,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1341,9 +1418,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -1356,9 +1433,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1366,9 +1443,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -1379,9 +1456,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-encoder" @@ -1394,7 +1471,7 @@ dependencies = [ [[package]] name = "wasm-testsuite" -version = "0.2.0" +version = "0.2.1" dependencies = [ "rust-embed", ] @@ -1422,9 +1499,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -1432,9 +1509,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 90f4e32..ee9a9df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,26 @@ [workspace] -members=["crates/*"] -default-members=["crates/cli"] +members=["crates/*", "examples/rust"] resolver="2" +[profile.wasm] +opt-level="s" +lto="thin" +codegen-units=1 +panic="abort" +inherits="release" + [workspace.package] -version="0.2.0" +version="0.3.0" edition="2021" license="MIT OR Apache-2.0" authors=["Henry Gressmann "] repository="https://github.com/explodingcamera/tinywasm" + +[package] +name="tinywasm-root" +publish=false +edition="2021" + +[dev-dependencies] +color-eyre="0.6" +tinywasm={path="crates/tinywasm"} diff --git a/README.md b/README.md index 00b828e..5055889 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,43 @@ [![docs.rs](https://img.shields.io/docsrs/tinywasm?logo=rust)](https://docs.rs/tinywasm) [![Crates.io](https://img.shields.io/crates/v/tinywasm.svg?logo=rust)](https://crates.io/crates/tinywasm) [![Crates.io](https://img.shields.io/crates/l/tinywasm.svg)](./LICENSE-APACHE) -# 🚧 Status +# Status -> [!WARNING] -> This project is still in development and is not ready for use. +TinyWasm, starting from version `0.3.0`, passes all the WebAssembly 1.0 tests in the [WebAssembly Test Suite](https://github.com/WebAssembly/testsuite). The 2.0 tests are in progress (notably `simd` and `bulk-memory-operations` are not implemented yet). This is enough to run most WebAssembly programs, including TinyWasm itself compiled to WebAssembly (see [examples/wasm-rust.rs](./examples/wasm-rust.rs)). -I'm currently working on supporting the WebAssembly MVP (1.0) specification. You can see the current status in the graph below. The goal is to support all the features of the MVP specification and then move on to the next version. +Some APIs to interact with the runtime are not yet exposed, and the existing ones are subject to change, but the core functionality is mostly complete. +Results of the tests can be found [here](https://github.com/explodingcamera/tinywasm/tree/main/crates/tinywasm/tests/generated). -

- - - -

+TinyWasm is not designed for performance, but rather for size and portability. However, it is still reasonably fast. +There are a couple of low-hanging fruits on the performance side, but they are not a priority at the moment. -## Features +## Supported Proposals + +- [**Mutable Globals**](https://github.com/WebAssembly/mutable-global/blob/master/proposals/mutable-global/Overview.md) - **Fully implemented** +- [**Multi-value**](https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md) - **Fully implemented** +- [**Sign-extension operators**](https://github.com/WebAssembly/spec/blob/master/proposals/sign-extension-ops/Overview.md) - **Fully implemented** +- [**Reference Types**](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md) - **_Partially implemented_** +- [**Multiple Memories**](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md) - **_Partially implemented_** (not tested yet) +- [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) - **_Partially implemented_** (only 32-bit addressing is supported at the moment, but larger memories can be created) + +## Usage + +TinyWasm can be used through the `tinywasm-cli` CLI tool or as a library in your Rust project. Documentation can be found [here](https://docs.rs/tinywasm). + +### CLI + +```sh +$ cargo install tinywasm-cli +$ tinywasm-cli --help +``` + +### Library + +```sh +$ cargo add tinywasm +``` + +## Feature Flags - **`std`**\ Enables the use of `std` and `std::io` for parsing from files and streams. This is enabled by default. @@ -32,16 +55,12 @@ I'm currently working on supporting the WebAssembly MVP (1.0) specification. You - **`parser`**\ Enables the `tinywasm-parser` crate. This is enabled by default. -# 🎯 Goals +With all these features disabled, TinyWasm only depends on `core`, `alloc` and `libm` and can be used in `no_std` environments. +Since `libm` is not as performant as the compiler's built-in math intrinsics, it is recommended to use the `std` feature if possible (at least [for now](https://github.com/rust-lang/rfcs/issues/2505)). + +## Performance -- Interpreted Runtime (no JIT) -- Self-hosted (can run itself compiled to WebAssembly) -- No unsafe code -- Works on `no_std` (with `alloc` the feature and nightly compiler) -- Fully support WebAssembly MVP (1.0) -- Low Memory Usage (less than 10kb) -- Fast Startup Time -- Preemptive multitasking support +> Benchmarks are coming soon. # 📄 License @@ -49,4 +68,4 @@ Licensed under either of [Apache License, Version 2.0](./LICENSE-APACHE) or [MIT Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in TinyWasm by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -**Note:** The GitHub repository contains a Submodule (`crates/tinywasm-parser/data`) which is licensed only under the [Apache License, Version 2.0](https://github.com/WebAssembly/spec/blob/main/test/LICENSE). This is because the data is generated from the [WebAssembly Specification](https://github.com/WebAssembly/spec/tree/main/test) and is only used for testing purposes and is not included in the final binary. +**Note:** The GitHub repository contains a Submodule (`crates/tinywasm-parser/data`) which is licensed only under the [Apache License, Version 2.0](https://github.com/WebAssembly/spec/blob/main/test/LICENSE). This data is generated from the [WebAssembly Specification](https://github.com/WebAssembly/spec/tree/main/test) and is only used for testing purposes and not included in the final binary. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 65a9e72..9cde64e 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,7 +14,7 @@ path="src/bin.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tinywasm={version="0.2.0-alpha.0", path="../tinywasm", features=["std", "parser"]} +tinywasm={version="0.3.0-alpha.0", path="../tinywasm", features=["std", "parser"]} argh="0.1" color-eyre={version="0.6", default-features=false} log="0.4" diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 379ecf8..3959572 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -17,29 +17,14 @@ impl From for WasmValue { impl FromStr for WasmArg { type Err = String; fn from_str(s: &str) -> std::prelude::v1::Result { - let [ty, val]: [&str; 2] = s - .split(':') - .collect::>() - .try_into() - .map_err(|e| format!("invalid arguments: {:?}", e))?; + let [ty, val]: [&str; 2] = + s.split(':').collect::>().try_into().map_err(|e| format!("invalid arguments: {:?}", e))?; let arg: WasmValue = match ty { - "i32" => val - .parse::() - .map_err(|e| format!("invalid argument value for i32: {e:?}"))? - .into(), - "i64" => val - .parse::() - .map_err(|e| format!("invalid argument value for i64: {e:?}"))? - .into(), - "f32" => val - .parse::() - .map_err(|e| format!("invalid argument value for f32: {e:?}"))? - .into(), - "f64" => val - .parse::() - .map_err(|e| format!("invalid argument value for f64: {e:?}"))? - .into(), + "i32" => val.parse::().map_err(|e| format!("invalid argument value for i32: {e:?}"))?.into(), + "i64" => val.parse::().map_err(|e| format!("invalid argument value for i64: {e:?}"))?.into(), + "f32" => val.parse::().map_err(|e| format!("invalid argument value for f32: {e:?}"))?.into(), + "f64" => val.parse::().map_err(|e| format!("invalid argument value for f64: {e:?}"))?.into(), t => return Err(format!("Invalid arg type: {}", t)), }; diff --git a/crates/cli/src/bin.rs b/crates/cli/src/bin.rs index c8ab379..1c166cf 100644 --- a/crates/cli/src/bin.rs +++ b/crates/cli/src/bin.rs @@ -84,12 +84,7 @@ fn main() -> Result<()> { let cwd = std::env::current_dir()?; match args.nested { - TinyWasmSubcommand::Run(Run { - wasm_file, - engine, - args, - func, - }) => { + TinyWasmSubcommand::Run(Run { wasm_file, engine, args, func }) => { debug!("args: {:?}", args); let path = cwd.join(wasm_file.clone()); diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index 9e6dc71..1592f85 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -11,7 +11,7 @@ repository.workspace=true # fork of wasmparser with no_std support, see https://github.com/bytecodealliance/wasmtime/issues/3495 wasmparser={version="0.100", package="wasmparser-nostd", default-features=false} log={version="0.4", optional=true} -tinywasm-types={version="0.2.0-alpha.0", path="../types"} +tinywasm-types={version="0.3.0-alpha.0", path="../types"} [features] default=["std", "logging"] diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index b9e674e..835842f 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -1,9 +1,6 @@ +use crate::log; use alloc::{boxed::Box, format, string::ToString, vec::Vec}; -use log::info; -use tinywasm_types::{ - BlockArgs, ConstInstruction, ElementItem, Export, ExternalKind, FuncType, Global, GlobalType, Import, ImportKind, - Instruction, MemArg, MemoryArch, MemoryType, TableType, ValType, -}; +use tinywasm_types::*; use wasmparser::{FuncValidator, OperatorsReader, ValidatorResources}; use crate::{module::CodeSection, Result}; @@ -11,19 +8,13 @@ use crate::{module::CodeSection, Result}; pub(crate) fn convert_module_elements<'a, T: IntoIterator>>>( elements: T, ) -> Result> { - let elements = elements - .into_iter() - .map(|element| convert_module_element(element?)) - .collect::>>()?; + let elements = elements.into_iter().map(|element| convert_module_element(element?)).collect::>>()?; Ok(elements) } pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result { let kind = match element.kind { - wasmparser::ElementKind::Active { - table_index, - offset_expr, - } => tinywasm_types::ElementKind::Active { + wasmparser::ElementKind::Active { table_index, offset_expr } => tinywasm_types::ElementKind::Active { table: table_index, offset: process_const_operators(offset_expr.get_operators_reader())?, }, @@ -32,38 +23,24 @@ pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result }; let items = match element.items { - wasmparser::ElementItems::Functions(funcs) => funcs - .into_iter() - .map(|func| Ok(ElementItem::Func(func?))) - .collect::>>()? - .into_boxed_slice(), + wasmparser::ElementItems::Functions(funcs) => { + funcs.into_iter().map(|func| Ok(ElementItem::Func(func?))).collect::>>()?.into_boxed_slice() + } wasmparser::ElementItems::Expressions(exprs) => exprs .into_iter() - .map(|expr| { - Ok(ElementItem::Expr(process_const_operators( - expr?.get_operators_reader(), - )?)) - }) + .map(|expr| Ok(ElementItem::Expr(process_const_operators(expr?.get_operators_reader())?))) .collect::>>()? .into_boxed_slice(), }; - Ok(tinywasm_types::Element { - kind, - items, - ty: convert_valtype(&element.ty), - range: element.range, - }) + Ok(tinywasm_types::Element { kind, items, ty: convert_valtype(&element.ty), range: element.range }) } pub(crate) fn convert_module_data_sections<'a, T: IntoIterator>>>( data_sections: T, ) -> Result> { - let data_sections = data_sections - .into_iter() - .map(|data| convert_module_data(data?)) - .collect::>>()?; + let data_sections = data_sections.into_iter().map(|data| convert_module_data(data?)).collect::>>()?; Ok(data_sections) } @@ -72,15 +49,9 @@ pub(crate) fn convert_module_data(data: wasmparser::Data<'_>) -> Result { + wasmparser::DataKind::Active { memory_index, offset_expr } => { let offset = process_const_operators(offset_expr.get_operators_reader())?; - tinywasm_types::DataKind::Active { - mem: memory_index, - offset, - } + tinywasm_types::DataKind::Active { mem: memory_index, offset } } wasmparser::DataKind::Passive => tinywasm_types::DataKind::Passive, }, @@ -90,10 +61,7 @@ pub(crate) fn convert_module_data(data: wasmparser::Data<'_>) -> Result>>>( imports: T, ) -> Result> { - let imports = imports - .into_iter() - .map(|import| convert_module_import(import?)) - .collect::>>()?; + let imports = imports.into_iter().map(|import| convert_module_import(import?)).collect::>>()?; Ok(imports) } @@ -102,18 +70,14 @@ pub(crate) fn convert_module_import(import: wasmparser::Import<'_>) -> Result ImportKind::Func(ty), + wasmparser::TypeRef::Func(ty) => ImportKind::Function(ty), wasmparser::TypeRef::Table(ty) => ImportKind::Table(convert_module_table(ty)?), - wasmparser::TypeRef::Memory(ty) => ImportKind::Mem(convert_module_memory(ty)?), - wasmparser::TypeRef::Global(ty) => ImportKind::Global(GlobalType { - mutable: ty.mutable, - ty: convert_valtype(&ty.content_type), - }), + wasmparser::TypeRef::Memory(ty) => ImportKind::Memory(convert_module_memory(ty)?), + wasmparser::TypeRef::Global(ty) => { + ImportKind::Global(GlobalType { mutable: ty.mutable, ty: convert_valtype(&ty.content_type) }) + } wasmparser::TypeRef::Tag(ty) => { - return Err(crate::ParseError::UnsupportedOperator(format!( - "Unsupported import kind: {:?}", - ty - ))) + return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported import kind: {:?}", ty))) } }, }) @@ -122,10 +86,8 @@ pub(crate) fn convert_module_import(import: wasmparser::Import<'_>) -> Result>>( memory_types: T, ) -> Result> { - let memory_type = memory_types - .into_iter() - .map(|memory| convert_module_memory(memory?)) - .collect::>>()?; + let memory_type = + memory_types.into_iter().map(|memory| convert_module_memory(memory?)).collect::>>()?; Ok(memory_type) } @@ -144,21 +106,14 @@ pub(crate) fn convert_module_memory(memory: wasmparser::MemoryType) -> Result>>( table_types: T, ) -> Result> { - let table_type = table_types - .into_iter() - .map(|table| convert_module_table(table?)) - .collect::>>()?; + let table_type = table_types.into_iter().map(|table| convert_module_table(table?)).collect::>>()?; Ok(table_type) } pub(crate) fn convert_module_table(table: wasmparser::TableType) -> Result { let ty = convert_valtype(&table.element_type); - Ok(TableType { - element_type: ty, - size_initial: table.initial, - size_max: table.maximum, - }) + Ok(TableType { element_type: ty, size_initial: table.initial, size_max: table.maximum }) } pub(crate) fn convert_module_globals<'a, T: IntoIterator>>>( @@ -171,13 +126,7 @@ pub(crate) fn convert_module_globals<'a, T: IntoIterator>>()?; Ok(globals) @@ -190,18 +139,11 @@ pub(crate) fn convert_module_export(export: wasmparser::Export) -> Result ExternalKind::Memory, wasmparser::ExternalKind::Global => ExternalKind::Global, wasmparser::ExternalKind::Tag => { - return Err(crate::ParseError::UnsupportedOperator(format!( - "Unsupported export kind: {:?}", - export.kind - ))) + return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported export kind: {:?}", export.kind))) } }; - Ok(Export { - index: export.index, - name: Box::from(export.name), - kind, - }) + Ok(Export { index: export.index, name: Box::from(export.name), kind }) } pub(crate) fn convert_module_code( @@ -224,27 +166,16 @@ pub(crate) fn convert_module_code( let body_reader = func.get_operators_reader()?; let body = process_operators(body_reader.original_position(), body_reader.into_iter(), validator)?; - Ok(CodeSection { - locals: locals.into_boxed_slice(), - body, - }) + Ok(CodeSection { locals: locals.into_boxed_slice(), body }) } pub(crate) fn convert_module_type(ty: wasmparser::Type) -> Result { let wasmparser::Type::Func(ty) = ty; - let params = ty - .params() - .iter() - .map(|p| Ok(convert_valtype(p))) - .collect::>>()? - .into_boxed_slice(); - - let results = ty - .results() - .iter() - .map(|p| Ok(convert_valtype(p))) - .collect::>>()? - .into_boxed_slice(); + let params = + ty.params().iter().map(|p| Ok(convert_valtype(p))).collect::>>()?.into_boxed_slice(); + + let results = + ty.results().iter().map(|p| Ok(convert_valtype(p))).collect::>>()?.into_boxed_slice(); Ok(FuncType { params, results }) } @@ -253,11 +184,6 @@ pub(crate) fn convert_blocktype(blocktype: wasmparser::BlockType) -> BlockArgs { use wasmparser::BlockType::*; match blocktype { Empty => BlockArgs::Empty, - - // We should maybe have all this in a single variant for our custom bytecode - - // TODO: maybe solve this differently so we can support 128-bit values - // without having to increase the size of the WasmValue enum Type(ty) => BlockArgs::Type(convert_valtype(&ty)), FuncType(ty) => BlockArgs::FuncType(ty), } @@ -270,19 +196,14 @@ pub(crate) fn convert_valtype(valtype: &wasmparser::ValType) -> ValType { I64 => ValType::I64, F32 => ValType::F32, F64 => ValType::F64, - V128 => ValType::V128, - FuncRef => ValType::FuncRef, - ExternRef => ValType::ExternRef, + V128 => unimplemented!("128-bit values are not supported yet"), + FuncRef => ValType::RefFunc, + ExternRef => ValType::RefExtern, } } -pub(crate) fn convert_memarg(memarg: wasmparser::MemArg) -> MemArg { - MemArg { - offset: memarg.offset, - align: memarg.align, - align_max: memarg.max_align, - mem_addr: memarg.memory, - } +pub(crate) fn convert_memarg(memarg: wasmparser::MemArg) -> MemoryArg { + MemoryArg { offset: memarg.offset, align: memarg.align, align_max: memarg.max_align, mem_addr: memarg.memory } } pub(crate) fn process_const_operators(ops: OperatorsReader) -> Result { @@ -300,16 +221,12 @@ pub fn process_const_operator(op: wasmparser::Operator) -> Result Ok(ConstInstruction::RefNull(convert_valtype(&ty))), wasmparser::Operator::RefFunc { function_index } => Ok(ConstInstruction::RefFunc(function_index)), - wasmparser::Operator::I32Const { value } => Ok(ConstInstruction::I32Const(value)), wasmparser::Operator::I64Const { value } => Ok(ConstInstruction::I64Const(value)), - wasmparser::Operator::F32Const { value } => Ok(ConstInstruction::F32Const(f32::from_bits(value.bits()))), // TODO: check if this is correct - wasmparser::Operator::F64Const { value } => Ok(ConstInstruction::F64Const(f64::from_bits(value.bits()))), // TODO: check if this is correct + wasmparser::Operator::F32Const { value } => Ok(ConstInstruction::F32Const(f32::from_bits(value.bits()))), + wasmparser::Operator::F64Const { value } => Ok(ConstInstruction::F64Const(f64::from_bits(value.bits()))), wasmparser::Operator::GlobalGet { global_index } => Ok(ConstInstruction::GlobalGet(global_index)), - op => Err(crate::ParseError::UnsupportedOperator(format!( - "Unsupported instruction: {:?}", - op - ))), + op => Err(crate::ParseError::UnsupportedOperator(format!("Unsupported const instruction: {:?}", op))), } } @@ -322,7 +239,7 @@ pub fn process_operators<'a>( let mut labels_ptrs = Vec::new(); // indexes into the instructions array for op in ops { - info!("op: {:?}", op); + log::debug!("op: {:?}", op); let op = op?; validator.op(offset, &op)?; @@ -332,9 +249,7 @@ pub fn process_operators<'a>( let res = match op { BrTable { targets } => { let def = targets.default(); - let targets = targets - .targets() - .collect::, wasmparser::BinaryReaderError>>()?; + let targets = targets.targets().collect::, wasmparser::BinaryReaderError>>()?; instructions.push(Instruction::BrTable(def, targets.len())); instructions.extend(targets.into_iter().map(Instruction::BrLabel)); continue; @@ -359,7 +274,7 @@ pub fn process_operators<'a>( } End => { if let Some(label_pointer) = labels_ptrs.pop() { - info!("ending block: {:?}", instructions[label_pointer]); + log::debug!("ending block: {:?}", instructions[label_pointer]); let current_instr_ptr = instructions.len(); @@ -406,11 +321,7 @@ pub fn process_operators<'a>( BrIf { relative_depth } => Instruction::BrIf(relative_depth), Return => Instruction::Return, Call { function_index } => Instruction::Call(function_index), - CallIndirect { - type_index, - table_index, - .. - } => Instruction::CallIndirect(type_index, table_index), + CallIndirect { type_index, table_index, .. } => Instruction::CallIndirect(type_index, table_index), Drop => Instruction::Drop, Select => Instruction::Select(None), TypedSelect { ty } => Instruction::Select(Some(convert_valtype(&ty))), @@ -587,12 +498,16 @@ pub fn process_operators<'a>( I64TruncSatF32U => Instruction::I64TruncSatF32U, I64TruncSatF64S => Instruction::I64TruncSatF64S, I64TruncSatF64U => Instruction::I64TruncSatF64U, + TableGet { table } => Instruction::TableGet(table), + TableSet { table } => Instruction::TableSet(table), + TableInit { table, elem_index } => Instruction::TableInit(table, elem_index), + TableCopy { src_table, dst_table } => Instruction::TableCopy { from: src_table, to: dst_table }, + TableGrow { table } => Instruction::TableGrow(table), + TableSize { table } => Instruction::TableSize(table), + TableFill { table } => Instruction::TableFill(table), op => { log::error!("Unsupported instruction: {:?}", op); - return Err(crate::ParseError::UnsupportedOperator(format!( - "Unsupported instruction: {:?}", - op - ))); + return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported instruction: {:?}", op))); } }; @@ -600,10 +515,7 @@ pub fn process_operators<'a>( } if !labels_ptrs.is_empty() { - panic!( - "last_label_pointer should be None after processing all instructions: {:?}", - labels_ptrs - ); + panic!("last_label_pointer should be None after processing all instructions: {:?}", labels_ptrs); } validator.finish(offset)?; diff --git a/crates/parser/src/error.rs b/crates/parser/src/error.rs index acacfa0..35bad28 100644 --- a/crates/parser/src/error.rs +++ b/crates/parser/src/error.rs @@ -4,6 +4,7 @@ use alloc::string::{String, ToString}; use wasmparser::Encoding; #[derive(Debug)] +/// Errors that can occur when parsing a WebAssembly module pub enum ParseError { InvalidType, UnsupportedSection(String), @@ -43,10 +44,7 @@ impl crate::std::error::Error for ParseError {} impl From for ParseError { fn from(value: wasmparser::BinaryReaderError) -> Self { - Self::ParseError { - message: value.message().to_string(), - offset: value.offset(), - } + Self::ParseError { message: value.message().to_string(), offset: value.offset() } } } diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 561d5c6..edcd280 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -14,16 +14,18 @@ use log; #[cfg(not(feature = "logging"))] mod log { macro_rules! debug ( ($($tt:tt)*) => {{}} ); + macro_rules! error ( ($($tt:tt)*) => {{}} ); pub(crate) use debug; + pub(crate) use error; } mod conversion; mod error; mod module; -use alloc::vec::Vec; +use alloc::{string::ToString, vec::Vec}; pub use error::*; use module::ModuleReader; -use tinywasm_types::Function; +use tinywasm_types::WasmFunction; use wasmparser::Validator; pub use tinywasm_types::TinyWasmModule; @@ -103,15 +105,30 @@ impl TryFrom for TinyWasmModule { return Err(ParseError::EndNotReached); } - let func_types = reader.func_addrs; + let code_type_addrs = reader.code_type_addrs; + let local_function_count = reader.code.len(); + + if code_type_addrs.len() != local_function_count { + return Err(ParseError::Other("Code and code type address count mismatch".to_string())); + } + let funcs = reader .code .into_iter() - .zip(func_types) - .map(|(f, ty)| Function { - instructions: f.body, - locals: f.locals, - ty, + .zip(code_type_addrs) + .map(|(f, ty_idx)| { + ( + ty_idx, + WasmFunction { + instructions: f.body, + locals: f.locals, + ty: reader + .func_types + .get(ty_idx as usize) + .expect("No func type for func, this is a bug") + .clone(), + }, + ) }) .collect::>(); @@ -119,17 +136,17 @@ impl TryFrom for TinyWasmModule { let table_types = reader.table_types; Ok(TinyWasmModule { - version: reader.version, - start_func: reader.start_func, - func_types: reader.func_types.into_boxed_slice(), funcs: funcs.into_boxed_slice(), - exports: reader.exports.into_boxed_slice(), + func_types: reader.func_types.into_boxed_slice(), globals: globals.into_boxed_slice(), table_types: table_types.into_boxed_slice(), - memory_types: reader.memory_types.into_boxed_slice(), imports: reader.imports.into_boxed_slice(), + version: reader.version, + start_func: reader.start_func, data: reader.data.into_boxed_slice(), + exports: reader.exports.into_boxed_slice(), elements: reader.elements.into_boxed_slice(), + memory_types: reader.memory_types.into_boxed_slice(), }) } } diff --git a/crates/parser/src/module.rs b/crates/parser/src/module.rs index 660a702..e0fb0cc 100644 --- a/crates/parser/src/module.rs +++ b/crates/parser/src/module.rs @@ -17,7 +17,10 @@ pub struct ModuleReader { pub start_func: Option, pub func_types: Vec, - pub func_addrs: Vec, + + // map from local function index to type index + pub code_type_addrs: Vec, + pub exports: Vec, pub code: Vec, pub globals: Vec, @@ -36,7 +39,7 @@ impl Debug for ModuleReader { f.debug_struct("ModuleReader") .field("version", &self.version) .field("func_types", &self.func_types) - .field("func_addrs", &self.func_addrs) + .field("func_addrs", &self.code_type_addrs) .field("code", &self.code) .field("exports", &self.exports) .field("globals", &self.globals) @@ -88,13 +91,13 @@ impl ModuleReader { .collect::>>()?; } FunctionSection(reader) => { - if !self.func_addrs.is_empty() { + if !self.code_type_addrs.is_empty() { return Err(ParseError::DuplicateSection("Function section".into())); } debug!("Found function section"); validator.function_section(&reader)?; - self.func_addrs = reader.into_iter().map(|f| Ok(f?)).collect::>>()?; + self.code_type_addrs = reader.into_iter().map(|f| Ok(f?)).collect::>>()?; } GlobalSection(reader) => { if !self.globals.is_empty() { @@ -148,9 +151,7 @@ impl ModuleReader { debug!("Found code section entry"); let v = validator.code_section_entry(&function)?; let func_validator = v.into_validator(Default::default()); - - self.code - .push(conversion::convert_module_code(function, func_validator)?); + self.code.push(conversion::convert_module_code(function, func_validator)?); } ImportSection(reader) => { if !self.imports.is_empty() { @@ -168,10 +169,8 @@ impl ModuleReader { debug!("Found export section"); validator.export_section(&reader)?; - self.exports = reader - .into_iter() - .map(|e| conversion::convert_module_export(e?)) - .collect::>>()?; + self.exports = + reader.into_iter().map(|e| conversion::convert_module_export(e?)).collect::>>()?; } End(offset) => { debug!("Reached end of module"); @@ -182,21 +181,16 @@ impl ModuleReader { validator.end(offset)?; self.end_reached = true; } - CustomSection(reader) => { + CustomSection(_reader) => { debug!("Found custom section"); - debug!("Skipping custom section: {:?}", reader.name()); + debug!("Skipping custom section: {:?}", _reader.name()); } // TagSection(tag) => { // debug!("Found tag section"); // validator.tag_section(&tag)?; // } UnknownSection { .. } => return Err(ParseError::UnsupportedSection("Unknown section".into())), - section => { - return Err(ParseError::UnsupportedSection(format!( - "Unsupported section: {:?}", - section - ))) - } + section => return Err(ParseError::UnsupportedSection(format!("Unsupported section: {:?}", section))), }; Ok(()) diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 931bb45..f1a3242 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -14,8 +14,9 @@ path="src/lib.rs" [dependencies] log={version="0.4", optional=true} -tinywasm-parser={version="0.2.0-alpha.0", path="../parser", default-features=false, optional=true} -tinywasm-types={version="0.2.0-alpha.0", path="../types", default-features=false} +tinywasm-parser={version="0.3.0-alpha.0", path="../parser", default-features=false, optional=true} +tinywasm-types={version="0.3.0-alpha.0", path="../types", default-features=false} +libm={version="0.2", default-features=false} [dev-dependencies] wasm-testsuite={path="../wasm-testsuite"} @@ -41,6 +42,10 @@ harness=false name="test-mvp" harness=false +[[test]] +name="test-two" +harness=false + [[test]] name="test-wast" harness=false diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index a7dac81..4f68eaa 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -1,8 +1,80 @@ -use alloc::string::String; +use alloc::string::{String, ToString}; use core::fmt::Display; +use tinywasm_types::FuncType; #[cfg(feature = "parser")] -use tinywasm_parser::ParseError; +pub use tinywasm_parser::ParseError; + +/// Errors that can occur for TinyWasm operations +#[derive(Debug)] +pub enum Error { + #[cfg(feature = "std")] + /// An I/O error occurred + Io(crate::std::io::Error), + + #[cfg(feature = "parser")] + /// A parsing error occurred + ParseError(ParseError), + + /// A WebAssembly trap occurred + Trap(Trap), + + /// A linking error occurred + Linker(LinkingError), + + /// A WebAssembly feature is not supported + UnsupportedFeature(String), + + /// An unknown error occurred + Other(String), + + /// A function did not return a value + FuncDidNotReturn, + + /// The stack is empty + StackUnderflow, + + /// The label stack is empty + LabelStackUnderflow, + + /// An invalid label type was encountered + InvalidLabelType, + + /// The call stack is empty + CallStackEmpty, + + /// The store is not the one that the module instance was instantiated in + InvalidStore, +} + +#[derive(Debug)] +/// Errors that can occur when linking a WebAssembly module +pub enum LinkingError { + /// An unknown import was encountered + UnknownImport { + /// The module name + module: String, + /// The import name + name: String, + }, + /// A mismatched import type was encountered + IncompatibleImportType { + /// The module name + module: String, + /// The import name + name: String, + }, +} + +impl LinkingError { + pub(crate) fn incompatible_import_type(import: &tinywasm_types::Import) -> Self { + Self::IncompatibleImportType { module: import.module.to_string(), name: import.name.to_string() } + } + + pub(crate) fn unknown_import(import: &tinywasm_types::Import) -> Self { + Self::UnknownImport { module: import.module.to_string(), name: import.name.to_string() } + } +} #[derive(Debug)] /// A WebAssembly trap @@ -22,6 +94,16 @@ pub enum Trap { max: usize, }, + /// An out-of-bounds table access occurred + TableOutOfBounds { + /// The offset of the access + offset: usize, + /// The size of the access + len: usize, + /// The maximum size of the memory + max: usize, + }, + /// A division by zero occurred DivisionByZero, @@ -30,45 +112,69 @@ pub enum Trap { /// Integer Overflow IntegerOverflow, -} -#[derive(Debug)] -/// A tinywasm error -pub enum Error { - #[cfg(feature = "parser")] - /// A parsing error occurred - ParseError(ParseError), - - #[cfg(feature = "std")] - /// An I/O error occurred - Io(crate::std::io::Error), + /// Call stack overflow + CallStackOverflow, - /// A WebAssembly feature is not supported - UnsupportedFeature(String), - - /// An unknown error occurred - Other(String), - - /// A WebAssembly trap occurred - Trap(Trap), + /// An undefined element was encountered + UndefinedElement { + /// The element index + index: usize, + }, - /// A function did not return a value - FuncDidNotReturn, + /// An uninitialized element was encountered + UninitializedElement { + /// The element index + index: usize, + }, - /// The stack is empty - StackUnderflow, + /// Indirect call type mismatch + IndirectCallTypeMismatch { + /// The expected type + expected: FuncType, + /// The actual type + actual: FuncType, + }, +} - /// The label stack is empty - LabelStackUnderflow, +impl Trap { + /// Get the message of the trap + pub fn message(&self) -> &'static str { + match self { + Self::Unreachable => "unreachable", + Self::MemoryOutOfBounds { .. } => "out of bounds memory access", + Self::TableOutOfBounds { .. } => "out of bounds table access", + Self::DivisionByZero => "integer divide by zero", + Self::InvalidConversionToInt => "invalid conversion to integer", + Self::IntegerOverflow => "integer overflow", + Self::CallStackOverflow => "call stack exhausted", + Self::UndefinedElement { .. } => "undefined element", + Self::UninitializedElement { .. } => "uninitialized element", + Self::IndirectCallTypeMismatch { .. } => "indirect call type mismatch", + } + } +} - /// An invalid label type was encountered - InvalidLabelType, +impl LinkingError { + /// Get the message of the linking error + pub fn message(&self) -> &'static str { + match self { + Self::UnknownImport { .. } => "unknown import", + Self::IncompatibleImportType { .. } => "incompatible import type", + } + } +} - /// The call stack is empty - CallStackEmpty, +impl From for Error { + fn from(value: LinkingError) -> Self { + Self::Linker(value) + } +} - /// The store is not the one that the module instance was instantiated in - InvalidStore, +impl From for Error { + fn from(value: Trap) -> Self { + Self::Trap(value) + } } impl Display for Error { @@ -80,20 +186,56 @@ impl Display for Error { #[cfg(feature = "std")] Self::Io(err) => write!(f, "I/O error: {}", err), - Self::Trap(trap) => write!(f, "trap: {:?}", trap), - + Self::Trap(trap) => write!(f, "trap: {}", trap), + Self::Linker(err) => write!(f, "linking error: {}", err), + Self::CallStackEmpty => write!(f, "call stack empty"), Self::InvalidLabelType => write!(f, "invalid label type"), Self::Other(message) => write!(f, "unknown error: {}", message), Self::UnsupportedFeature(feature) => write!(f, "unsupported feature: {}", feature), Self::FuncDidNotReturn => write!(f, "function did not return"), Self::LabelStackUnderflow => write!(f, "label stack underflow"), Self::StackUnderflow => write!(f, "stack underflow"), - Self::CallStackEmpty => write!(f, "call stack empty"), Self::InvalidStore => write!(f, "invalid store"), } } } +impl Display for LinkingError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::UnknownImport { module, name } => write!(f, "unknown import: {}.{}", module, name), + Self::IncompatibleImportType { module, name } => { + write!(f, "incompatible import type: {}.{}", module, name) + } + } + } +} + +impl Display for Trap { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Unreachable => write!(f, "unreachable"), + Self::MemoryOutOfBounds { offset, len, max } => { + write!(f, "out of bounds memory access: offset={}, len={}, max={}", offset, len, max) + } + Self::TableOutOfBounds { offset, len, max } => { + write!(f, "out of bounds table access: offset={}, len={}, max={}", offset, len, max) + } + Self::DivisionByZero => write!(f, "integer divide by zero"), + Self::InvalidConversionToInt => write!(f, "invalid conversion to integer"), + Self::IntegerOverflow => write!(f, "integer overflow"), + Self::CallStackOverflow => write!(f, "call stack exhausted"), + Self::UndefinedElement { index } => write!(f, "undefined element: index={}", index), + Self::UninitializedElement { index } => { + write!(f, "uninitialized element: index={}", index) + } + Self::IndirectCallTypeMismatch { expected, actual } => { + write!(f, "indirect call type mismatch: expected={:?}, actual={:?}", expected, actual) + } + } + } +} + #[cfg(any(feature = "std", all(not(feature = "std"), nightly)))] impl crate::std::error::Error for Error {} @@ -104,5 +246,5 @@ impl From for Error { } } -/// A specialized [`Result`] type for tinywasm operations +/// A wrapper around [`core::result::Result`] for tinywasm operations pub type Result = crate::std::result::Result; diff --git a/crates/tinywasm/src/export.rs b/crates/tinywasm/src/export.rs index c6ae6d3..8b13789 100644 --- a/crates/tinywasm/src/export.rs +++ b/crates/tinywasm/src/export.rs @@ -1,18 +1 @@ -use alloc::{boxed::Box, format}; -use tinywasm_types::{Export, ExternalKind}; -use crate::{Error, Result}; - -#[derive(Debug)] -/// Exports of a module instance -pub struct ExportInstance(pub(crate) Box<[Export]>); - -impl ExportInstance { - /// Get an export by name - pub fn get(&self, name: &str, ty: ExternalKind) -> Result<&Export> { - self.0 - .iter() - .find(|e| e.name == name.into() && e.kind == ty) - .ok_or(Error::Other(format!("export {} not found", name))) - } -} diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs index b0b359d..8433236 100644 --- a/crates/tinywasm/src/func.rs +++ b/crates/tinywasm/src/func.rs @@ -1,10 +1,10 @@ -use alloc::{format, string::String, string::ToString, vec, vec::Vec}; -use log::{debug, info}; -use tinywasm_types::{FuncAddr, FuncType, WasmValue}; +use crate::log; +use alloc::{boxed::Box, format, string::String, string::ToString, vec, vec::Vec}; +use tinywasm_types::{FuncAddr, FuncType, ValType, WasmValue}; use crate::{ runtime::{CallFrame, Stack}, - Error, ModuleInstance, Result, Store, + Error, FuncContext, ModuleInstance, Result, Store, }; #[derive(Debug)] @@ -27,14 +27,14 @@ impl FuncHandle { // 1. Assert: funcs[func_addr] exists // 2. let func_inst be the functiuon instance funcs[func_addr] - let func_inst = store.get_func(self.addr as usize)?; + let func_inst = store.get_func(self.addr as usize)?.clone(); // 3. Let func_ty be the function type let func_ty = &self.ty; // 4. If the length of the provided argument values is different from the number of expected arguments, then fail if func_ty.params.len() != params.len() { - info!("func_ty.params: {:?}", func_ty.params); + log::info!("func_ty.params: {:?}", func_ty.params); return Err(Error::Other(format!( "param count mismatch: expected {}, got {}", func_ty.params.len(), @@ -52,39 +52,44 @@ impl FuncHandle { } } + let locals = match &func_inst.func { + crate::Function::Host(h) => { + let func = h.func.clone(); + let ctx = FuncContext { store, module: &self.module }; + return (func)(ctx, params); + } + crate::Function::Wasm(ref f) => f.locals.to_vec(), + }; + // 6. Let f be the dummy frame - debug!("locals: {:?}", func_inst.locals()); - let call_frame = CallFrame::new(self.addr as usize, params, func_inst.locals().to_vec()); + log::debug!("locals: {:?}", locals); + let call_frame = CallFrame::new(func_inst, params, locals); // 7. Push the frame f to the call stack // & 8. Push the values to the stack (Not needed since the call frame owns the values) - stack.call_stack.push(call_frame); + stack.call_stack.push(call_frame)?; // 9. Invoke the function instance let runtime = store.runtime(); - runtime.exec(store, &mut stack, self.module.clone())?; + runtime.exec(store, &mut stack)?; // Once the function returns: let result_m = func_ty.results.len(); // 1. Assert: m values are on the top of the stack (Ensured by validation) - debug_assert!(stack.values.len() >= result_m); + assert!(stack.values.len() >= result_m); // 2. Pop m values from the stack let res = stack.values.last_n(result_m)?; // The values are returned as the results of the invocation. - Ok(res - .iter() - .zip(func_ty.results.iter()) - .map(|(v, ty)| v.attach_type(*ty)) - .collect()) + Ok(res.iter().zip(func_ty.results.iter()).map(|(v, ty)| v.attach_type(*ty)).collect()) } } #[derive(Debug)] /// A typed function handle -pub struct TypedFuncHandle { +pub struct FuncHandleTyped { /// The underlying function handle pub func: FuncHandle, pub(crate) marker: core::marker::PhantomData<(P, R)>, @@ -100,7 +105,7 @@ pub trait FromWasmValueTuple { Self: Sized; } -impl TypedFuncHandle { +impl FuncHandleTyped { /// Call a typed function pub fn call(&self, store: &mut Store, params: P) -> Result { // Convert params into Vec @@ -128,6 +133,21 @@ macro_rules! impl_into_wasm_value_tuple { } } +macro_rules! impl_into_wasm_value_tuple_single { + ($T:ident) => { + impl IntoWasmValueTuple for $T { + fn into_wasm_value_tuple(self) -> Vec { + vec![self.into()] + } + } + }; +} + +impl_into_wasm_value_tuple_single!(i32); +impl_into_wasm_value_tuple_single!(i64); +impl_into_wasm_value_tuple_single!(f32); +impl_into_wasm_value_tuple_single!(f64); + impl_into_wasm_value_tuple!(); impl_into_wasm_value_tuple!(T1); impl_into_wasm_value_tuple!(T1, T2); @@ -147,13 +167,15 @@ macro_rules! impl_from_wasm_value_tuple { fn from_wasm_value_tuple(values: Vec) -> Result { #[allow(unused_variables, unused_mut)] let mut iter = values.into_iter(); + Ok(( $( $T::try_from( iter.next() .ok_or(Error::Other("Not enough values in WasmValue vector".to_string()))? ) - .map_err(|_| Error::Other("Could not convert WasmValue to expected type".to_string()))?, + .map_err(|e| Error::Other(format!("FromWasmValueTuple: Could not convert WasmValue to expected type: {:?}", e, + )))?, )* )) } @@ -161,6 +183,29 @@ macro_rules! impl_from_wasm_value_tuple { } } +macro_rules! impl_from_wasm_value_tuple_single { + ($T:ident) => { + impl FromWasmValueTuple for $T { + fn from_wasm_value_tuple(values: Vec) -> Result { + #[allow(unused_variables, unused_mut)] + let mut iter = values.into_iter(); + $T::try_from(iter.next().ok_or(Error::Other("Not enough values in WasmValue vector".to_string()))?) + .map_err(|e| { + Error::Other(format!( + "FromWasmValueTupleSingle: Could not convert WasmValue to expected type: {:?}", + e + )) + }) + } + } + }; +} + +impl_from_wasm_value_tuple_single!(i32); +impl_from_wasm_value_tuple_single!(i64); +impl_from_wasm_value_tuple_single!(f32); +impl_from_wasm_value_tuple_single!(f64); + impl_from_wasm_value_tuple!(); impl_from_wasm_value_tuple!(T1); impl_from_wasm_value_tuple!(T1, T2); @@ -170,3 +215,70 @@ impl_from_wasm_value_tuple!(T1, T2, T3, T4, T5); impl_from_wasm_value_tuple!(T1, T2, T3, T4, T5, T6); impl_from_wasm_value_tuple!(T1, T2, T3, T4, T5, T6, T7); impl_from_wasm_value_tuple!(T1, T2, T3, T4, T5, T6, T7, T8); + +pub trait ValTypesFromTuple { + fn val_types() -> Box<[ValType]>; +} + +pub trait ToValType { + fn to_val_type() -> ValType; +} + +impl ToValType for i32 { + fn to_val_type() -> ValType { + ValType::I32 + } +} + +impl ToValType for i64 { + fn to_val_type() -> ValType { + ValType::I64 + } +} + +impl ToValType for f32 { + fn to_val_type() -> ValType { + ValType::F32 + } +} + +impl ToValType for f64 { + fn to_val_type() -> ValType { + ValType::F64 + } +} + +macro_rules! impl_val_types_from_tuple { + ($($t:ident),+) => { + impl<$($t),+> ValTypesFromTuple for ($($t,)+) + where + $($t: ToValType,)+ + { + fn val_types() -> Box<[ValType]> { + Box::new([$($t::to_val_type(),)+]) + } + } + }; +} + +impl ValTypesFromTuple for () { + fn val_types() -> Box<[ValType]> { + Box::new([]) + } +} + +impl ValTypesFromTuple for T1 +where + T1: ToValType, +{ + fn val_types() -> Box<[ValType]> { + Box::new([T1::to_val_type()]) + } +} + +impl_val_types_from_tuple!(T1); +impl_val_types_from_tuple!(T1, T2); +impl_val_types_from_tuple!(T1, T2, T3); +impl_val_types_from_tuple!(T1, T2, T3, T4); +impl_val_types_from_tuple!(T1, T2, T3, T4, T5); +impl_val_types_from_tuple!(T1, T2, T3, T4, T5, T6); diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index 1b3a9cf..a640081 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -1,30 +1,173 @@ -use crate::Result; +#![allow(dead_code)] + +use core::fmt::Debug; + +use crate::{ + func::{FromWasmValueTuple, IntoWasmValueTuple, ValTypesFromTuple}, + log, LinkingError, Result, +}; use alloc::{ collections::BTreeMap, string::{String, ToString}, + sync::Arc, + vec::Vec, }; -use tinywasm_types::{Global, GlobalType, ModuleInstanceAddr, WasmValue}; +use tinywasm_types::*; + +/// The internal representation of a function +#[derive(Debug, Clone)] +pub enum Function { + /// A host function + Host(HostFunction), + + /// A function defined in WebAssembly + Wasm(WasmFunction), +} + +impl Function { + pub(crate) fn ty(&self) -> &FuncType { + match self { + Self::Host(f) => &f.ty, + Self::Wasm(f) => &f.ty, + } + } +} + +/// A host function +#[derive(Clone)] +pub struct HostFunction { + pub(crate) ty: tinywasm_types::FuncType, + pub(crate) func: HostFuncInner, +} + +impl HostFunction { + /// Get the function's type + pub fn ty(&self) -> &tinywasm_types::FuncType { + &self.ty + } + + /// Call the function + pub fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result> { + (self.func)(ctx, args) + } +} +pub(crate) type HostFuncInner = + Arc, &[WasmValue]) -> Result> + 'static + Send + Sync>; + +/// The context of a host-function call #[derive(Debug)] +pub struct FuncContext<'a> { + pub(crate) store: &'a mut crate::Store, + pub(crate) module: &'a crate::ModuleInstance, +} + +impl FuncContext<'_> { + /// Get a mutable reference to the store + pub fn store_mut(&mut self) -> &mut crate::Store { + self.store + } + + /// Get a reference to the store + pub fn store(&self) -> &crate::Store { + self.store + } + + /// Get a reference to the module instance + pub fn module(&self) -> &crate::ModuleInstance { + self.module + } +} + +impl Debug for HostFunction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("HostFunction").field("ty", &self.ty).field("func", &"...").finish() + } +} + +#[derive(Debug, Clone)] #[non_exhaustive] /// An external value pub enum Extern { /// A global value - Global(Global), - // Func(HostFunc), - // Table(Table), + Global { + /// The type of the global value. + ty: GlobalType, + /// The actual value of the global, encapsulated in `WasmValue`. + val: WasmValue, + }, + + /// A table + Table { + /// Defines the type of the table, including its element type and limits. + ty: TableType, + /// The initial value of the table. + init: WasmValue, + }, + + /// A memory + Memory { + /// Defines the type of the memory, including its limits and the type of its pages. + ty: MemoryType, + }, + + /// A function + Function(Function), } impl Extern { /// Create a new global import pub fn global(val: WasmValue, mutable: bool) -> Self { - Self::Global(Global { - ty: GlobalType { - ty: val.val_type(), - mutable, - }, - init: val.const_instr(), - }) + Self::Global { ty: GlobalType { ty: val.val_type(), mutable }, val } + } + + /// Create a new table import + pub fn table(ty: TableType, init: WasmValue) -> Self { + Self::Table { ty, init } + } + + /// Create a new memory import + pub fn memory(ty: MemoryType) -> Self { + Self::Memory { ty } + } + + /// Create a new function import + pub fn func( + ty: &tinywasm_types::FuncType, + func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result> + 'static + Send + Sync, + ) -> Self { + let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| { + let args = args.to_vec(); + func(ctx, &args) + }; + + Self::Function(Function::Host(HostFunction { func: Arc::new(inner_func), ty: ty.clone() })) + } + + /// Create a new typed function import + pub fn typed_func(func: impl Fn(FuncContext<'_>, P) -> Result + 'static + Send + Sync) -> Self + where + P: FromWasmValueTuple + ValTypesFromTuple, + R: IntoWasmValueTuple + ValTypesFromTuple + Debug, + { + let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result> { + let args = P::from_wasm_value_tuple(args.to_vec())?; + let result = func(ctx, args)?; + Ok(result.into_wasm_value_tuple()) + }; + + let ty = tinywasm_types::FuncType { params: P::val_types(), results: R::val_types() }; + + Self::Function(Function::Host(HostFunction { func: Arc::new(inner_func), ty })) + } + + pub(crate) fn kind(&self) -> ExternalKind { + match self { + Self::Global { .. } => ExternalKind::Global, + Self::Table { .. } => ExternalKind::Table, + Self::Memory { .. } => ExternalKind::Memory, + Self::Function { .. } => ExternalKind::Func, + } } } @@ -35,38 +178,81 @@ pub struct ExternName { name: String, } +impl From<&Import> for ExternName { + fn from(import: &Import) -> Self { + Self { module: import.module.to_string(), name: import.name.to_string() } + } +} + #[derive(Debug, Default)] /// Imports for a module instance +/// +/// This is used to link a module instance to its imports +/// +/// ## Example +/// ```rust +/// # fn main() -> tinywasm::Result<()> { +/// use tinywasm::{Imports, Extern}; +/// use tinywasm::types::{ValType, TableType, MemoryType, WasmValue}; +/// let mut imports = Imports::new(); +/// +/// // function args can be either a single +/// // value that implements `TryFrom` or a tuple of them +/// let print_i32 = Extern::typed_func(|_ctx: tinywasm::FuncContext<'_>, arg: i32| { +/// log::debug!("print_i32: {}", arg); +/// Ok(()) +/// }); +/// +/// let table_type = TableType::new(ValType::RefFunc, 10, Some(20)); +/// let table_init = WasmValue::default_for(ValType::RefFunc); +/// +/// imports +/// .define("my_module", "print_i32", print_i32)? +/// .define("my_module", "table", Extern::table(table_type, table_init))? +/// .define("my_module", "memory", Extern::memory(MemoryType::new_32(1, Some(2))))? +/// .define("my_module", "global_i32", Extern::global(WasmValue::I32(666), false))? +/// .link_module("my_other_module", 0)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Note that module instance addresses for [`Imports::link_module`] can be obtained from [`crate::ModuleInstance::id`]. +/// Now, the imports object can be passed to [`crate::ModuleInstance::instantiate`]. pub struct Imports { values: BTreeMap, modules: BTreeMap, } -pub(crate) struct LinkedImports { - pub(crate) values: BTreeMap, +pub(crate) enum ResolvedExtern { + // already in the store + Store(S), + + // needs to be added to the store, provided value + Extern(V), } -impl LinkedImports { - pub(crate) fn get(&self, module: &str, name: &str) -> Option<&Extern> { - self.values.get(&ExternName { - module: module.to_string(), - name: name.to_string(), - }) +pub(crate) struct ResolvedImports { + pub(crate) globals: Vec, + pub(crate) tables: Vec, + pub(crate) memories: Vec, + pub(crate) funcs: Vec, +} + +impl ResolvedImports { + pub(crate) fn new() -> Self { + Self { globals: Vec::new(), tables: Vec::new(), memories: Vec::new(), funcs: Vec::new() } } } impl Imports { /// Create a new empty import set pub fn new() -> Self { - Imports { - values: BTreeMap::new(), - modules: BTreeMap::new(), - } + Imports { values: BTreeMap::new(), modules: BTreeMap::new() } } /// Link a module /// - /// This will automatically link all imported values + /// This will automatically link all imported values on instantiation pub fn link_module(&mut self, name: &str, addr: ModuleInstanceAddr) -> Result<&mut Self> { self.modules.insert(name.to_string(), addr); Ok(self) @@ -74,19 +260,170 @@ impl Imports { /// Define an import pub fn define(&mut self, module: &str, name: &str, value: Extern) -> Result<&mut Self> { - self.values.insert( - ExternName { - module: module.to_string(), - name: name.to_string(), - }, - value, - ); + self.values.insert(ExternName { module: module.to_string(), name: name.to_string() }, value); Ok(self) } - pub(crate) fn link(self, _store: &mut crate::Store, _module: &crate::Module) -> Result { - // TODO: link to other modules (currently only direct imports are supported) - let values = self.values; - Ok(LinkedImports { values }) + pub(crate) fn take( + &mut self, + store: &mut crate::Store, + import: &Import, + ) -> Option> { + let name = ExternName::from(import); + if let Some(v) = self.values.get(&name) { + return Some(ResolvedExtern::Extern(v.clone())); + } + + if let Some(addr) = self.modules.get(&name.module) { + let instance = store.get_module_instance(*addr)?; + return Some(ResolvedExtern::Store(instance.export(&import.name)?)); + } + + None + } + + fn compare_types(import: &Import, actual: &T, expected: &T) -> Result<()> + where + T: Debug + PartialEq, + { + if expected != actual { + log::error!("failed to link import {}, expected {:?}, got {:?}", import.name, expected, actual); + return Err(LinkingError::incompatible_import_type(import).into()); + } + + Ok(()) + } + + fn compare_table_types(import: &Import, expected: &TableType, actual: &TableType) -> Result<()> { + Self::compare_types(import, &actual.element_type, &expected.element_type)?; + + if actual.size_initial > expected.size_initial { + return Err(LinkingError::incompatible_import_type(import).into()); + } + + match (expected.size_max, actual.size_max) { + (None, Some(_)) => return Err(LinkingError::incompatible_import_type(import).into()), + (Some(expected_max), Some(actual_max)) if actual_max < expected_max => { + return Err(LinkingError::incompatible_import_type(import).into()) + } + _ => {} + } + + Ok(()) + } + + fn compare_memory_types( + import: &Import, + expected: &MemoryType, + actual: &MemoryType, + real_size: Option, + ) -> Result<()> { + Self::compare_types(import, &expected.arch, &actual.arch)?; + + if actual.page_count_initial > expected.page_count_initial { + if let Some(real_size) = real_size { + if actual.page_count_initial > real_size as u64 { + return Err(LinkingError::incompatible_import_type(import).into()); + } + } else { + return Err(LinkingError::incompatible_import_type(import).into()); + } + } + + match (expected.page_count_max, actual.page_count_max) { + (None, Some(_)) => return Err(LinkingError::incompatible_import_type(import).into()), + (Some(expected_max), Some(actual_max)) if actual_max < expected_max => { + return Err(LinkingError::incompatible_import_type(import).into()) + } + _ => {} + } + + Ok(()) + } + + pub(crate) fn link( + mut self, + store: &mut crate::Store, + module: &crate::Module, + idx: ModuleInstanceAddr, + ) -> Result { + let mut imports = ResolvedImports::new(); + + for import in module.data.imports.iter() { + let val = self.take(store, import).ok_or_else(|| LinkingError::unknown_import(import))?; + + match val { + // A link to something that needs to be added to the store + ResolvedExtern::Extern(ex) => match (ex, &import.kind) { + (Extern::Global { ty, val }, ImportKind::Global(import_ty)) => { + Self::compare_types(import, &ty, import_ty)?; + imports.globals.push(store.add_global(ty, val.into(), idx)?); + } + (Extern::Table { ty, .. }, ImportKind::Table(import_ty)) => { + Self::compare_table_types(import, &ty, import_ty)?; + imports.tables.push(store.add_table(ty, idx)?); + } + (Extern::Memory { ty }, ImportKind::Memory(import_ty)) => { + Self::compare_memory_types(import, &ty, import_ty, None)?; + imports.memories.push(store.add_mem(ty, idx)?); + } + (Extern::Function(extern_func), ImportKind::Function(ty)) => { + let import_func_type = module + .data + .func_types + .get(*ty as usize) + .ok_or_else(|| LinkingError::incompatible_import_type(import))?; + + Self::compare_types(import, extern_func.ty(), import_func_type)?; + imports.funcs.push(store.add_func(extern_func, *ty, idx)?); + } + _ => return Err(LinkingError::incompatible_import_type(import).into()), + }, + + // A link to something already in the store + ResolvedExtern::Store(val) => { + // check if the kind matches + if val.kind() != (&import.kind).into() { + return Err(LinkingError::incompatible_import_type(import).into()); + } + + match (val, &import.kind) { + (ExternVal::Global(global_addr), ImportKind::Global(ty)) => { + let global = store.get_global(global_addr as usize)?; + Self::compare_types(import, &global.borrow().ty, ty)?; + imports.globals.push(global_addr); + } + (ExternVal::Table(table_addr), ImportKind::Table(ty)) => { + let table = store.get_table(table_addr as usize)?; + Self::compare_table_types(import, &table.borrow().kind, ty)?; + imports.tables.push(table_addr); + } + (ExternVal::Mem(memory_addr), ImportKind::Memory(ty)) => { + let mem = store.get_mem(memory_addr as usize)?; + let (size, kind) = { + let mem = mem.borrow(); + (mem.page_count(), mem.kind) + }; + Self::compare_memory_types(import, &kind, ty, Some(size))?; + imports.memories.push(memory_addr); + } + (ExternVal::Func(func_addr), ImportKind::Function(ty)) => { + let func = store.get_func(func_addr as usize)?; + let import_func_type = module + .data + .func_types + .get(*ty as usize) + .ok_or_else(|| LinkingError::incompatible_import_type(import))?; + + Self::compare_types(import, func.func.ty(), import_func_type)?; + imports.funcs.push(func_addr); + } + _ => return Err(LinkingError::incompatible_import_type(import).into()), + } + } + } + } + + Ok(imports) } } diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index ad5e937..d750cd5 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -1,16 +1,17 @@ -use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, format, string::ToString, sync::Arc}; use tinywasm_types::{ - DataAddr, ElmAddr, ExternalKind, FuncAddr, FuncType, GlobalAddr, Import, MemAddr, ModuleInstanceAddr, TableAddr, + DataAddr, ElemAddr, Export, ExternVal, ExternalKind, FuncAddr, FuncType, GlobalAddr, Import, MemAddr, + ModuleInstanceAddr, TableAddr, }; use crate::{ func::{FromWasmValueTuple, IntoWasmValueTuple}, - Error, ExportInstance, FuncHandle, Imports, Module, Result, Store, TypedFuncHandle, + log, Error, FuncHandle, FuncHandleTyped, Imports, Module, Result, Store, }; -/// A WebAssembly Module Instance +/// An instanciated WebAssembly module /// -/// Addrs are indices into the store's data structures. +/// Backed by an Arc, so cloning is cheap /// /// See #[derive(Debug, Clone)] @@ -19,23 +20,31 @@ pub struct ModuleInstance(Arc); #[allow(dead_code)] #[derive(Debug)] pub(crate) struct ModuleInstanceInner { + pub(crate) failed_to_instantiate: bool, + pub(crate) store_id: usize, pub(crate) idx: ModuleInstanceAddr, pub(crate) types: Box<[FuncType]>, - pub(crate) func_addrs: Vec, - pub(crate) table_addrs: Vec, - pub(crate) mem_addrs: Vec, - pub(crate) global_addrs: Vec, - pub(crate) elem_addrs: Vec, - pub(crate) data_addrs: Vec, + + pub(crate) func_addrs: Box<[FuncAddr]>, + pub(crate) table_addrs: Box<[TableAddr]>, + pub(crate) mem_addrs: Box<[MemAddr]>, + pub(crate) global_addrs: Box<[GlobalAddr]>, + pub(crate) elem_addrs: Box<[ElemAddr]>, + pub(crate) data_addrs: Box<[DataAddr]>, pub(crate) func_start: Option, pub(crate) imports: Box<[Import]>, - pub(crate) exports: ExportInstance, + pub(crate) exports: Box<[Export]>, } impl ModuleInstance { + // drop the module instance reference and swap it with another one + pub(crate) fn swap(&mut self, other: Self) { + self.0 = other.0; + } + /// Get the module instance's address pub fn id(&self) -> ModuleInstanceAddr { self.0.idx @@ -50,61 +59,72 @@ impl ModuleInstance { // don't need to create a auxiliary frame etc. let idx = store.next_module_instance_idx(); + log::error!("Instantiating module at index {}", idx); let imports = imports.unwrap_or_default(); - // TODO: doesn't link other modules yet - let linked_imports = imports.link(store, &module)?; - let global_addrs = store.add_globals(module.data.globals.into(), &module.data.imports, &linked_imports, idx)?; - - // TODO: imported functions missing - let func_addrs = store.add_funcs(module.data.funcs.into(), idx); - - let table_addrs = store.add_tables(module.data.table_types.into(), idx); - let mem_addrs = store.add_mems(module.data.memory_types.into(), idx)?; + let mut addrs = imports.link(store, &module, idx)?; + let data = module.data; - // TODO: active/declared elems need to be initialized - let elem_addrs = store.add_elems(module.data.elements.into(), idx)?; + // TODO: check if the compiler correctly optimizes this to prevent wasted allocations + addrs.funcs.extend(store.init_funcs(data.funcs.into(), idx)?); + addrs.tables.extend(store.init_tables(data.table_types.into(), idx)?); + addrs.memories.extend(store.init_memories(data.memory_types.into(), idx)?); - // TODO: active data segments need to be initialized - let data_addrs = store.add_datas(module.data.data.into(), idx); + let global_addrs = store.init_globals(addrs.globals, data.globals.into(), &addrs.funcs, idx)?; + let (elem_addrs, elem_trapped) = + store.init_elements(&addrs.tables, &addrs.funcs, &global_addrs, data.elements.into(), idx)?; + let (data_addrs, data_trapped) = store.init_datas(&addrs.memories, data.data.into(), idx)?; let instance = ModuleInstanceInner { + failed_to_instantiate: elem_trapped.is_some() || data_trapped.is_some(), store_id: store.id(), idx, - - types: module.data.func_types, - func_addrs, - table_addrs, - mem_addrs, - global_addrs, + types: data.func_types, + func_addrs: addrs.funcs.into_boxed_slice(), + table_addrs: addrs.tables.into_boxed_slice(), + mem_addrs: addrs.memories.into_boxed_slice(), + global_addrs: global_addrs.into_boxed_slice(), elem_addrs, data_addrs, - - func_start: module.data.start_func, - imports: module.data.imports, - exports: crate::ExportInstance(module.data.exports), + func_start: data.start_func, + imports: data.imports, + exports: data.exports, }; let instance = ModuleInstance::new(instance); store.add_instance(instance.clone())?; + if let Some(trap) = elem_trapped { + return Err(trap.into()); + }; + + if let Some(trap) = data_trapped { + return Err(trap.into()); + }; + Ok(instance) } - /// Get the module's exports - pub fn exports(&self) -> &ExportInstance { - &self.0.exports + /// Get a export by name + pub fn export(&self, name: &str) -> Option { + let exports = self.0.exports.iter().find(|e| e.name == name.into())?; + let kind = exports.kind.clone(); + let addr = match kind { + ExternalKind::Func => self.0.func_addrs.get(exports.index as usize)?, + ExternalKind::Table => self.0.table_addrs.get(exports.index as usize)?, + ExternalKind::Memory => self.0.mem_addrs.get(exports.index as usize)?, + ExternalKind::Global => self.0.global_addrs.get(exports.index as usize)?, + }; + + Some(ExternVal::new(kind, *addr)) } pub(crate) fn func_addrs(&self) -> &[FuncAddr] { &self.0.func_addrs } - pub(crate) fn _global_addrs(&self) -> &[GlobalAddr] { - &self.0.global_addrs - } - - pub(crate) fn func_ty_addrs(&self) -> &[FuncType] { + /// Get the module's function types + pub fn func_tys(&self) -> &[FuncType] { &self.0.types } @@ -113,22 +133,27 @@ impl ModuleInstance { } pub(crate) fn func_ty(&self, addr: FuncAddr) -> &FuncType { - &self.0.types[addr as usize] + self.0.types.get(addr as usize).expect("No func type for func, this is a bug") } // resolve a function address to the global store address pub(crate) fn resolve_func_addr(&self, addr: FuncAddr) -> FuncAddr { - self.0.func_addrs[addr as usize] + *self.0.func_addrs.get(addr as usize).expect("No func addr for func, this is a bug") } // resolve a table address to the global store address - pub(crate) fn _resolve_table_addr(&self, addr: TableAddr) -> TableAddr { - self.0.table_addrs[addr as usize] + pub(crate) fn resolve_table_addr(&self, addr: TableAddr) -> TableAddr { + *self.0.table_addrs.get(addr as usize).expect("No table addr for table, this is a bug") } // resolve a memory address to the global store address pub(crate) fn resolve_mem_addr(&self, addr: MemAddr) -> MemAddr { - self.0.mem_addrs[addr as usize] + *self.0.mem_addrs.get(addr as usize).expect("No mem addr for mem, this is a bug") + } + + // resolve a memory address to the global store address + pub(crate) fn resolve_elem_addr(&self, addr: ElemAddr) -> ElemAddr { + *self.0.elem_addrs.get(addr as usize).expect("No elem addr for elem, this is a bug") } // resolve a global address to the global store address @@ -142,30 +167,25 @@ impl ModuleInstance { return Err(Error::InvalidStore); } - let export = self.0.exports.get(name, ExternalKind::Func)?; - let func_addr = self.0.func_addrs[export.index as usize]; - let func = store.get_func(func_addr as usize)?; - let ty = self.0.types[func.ty_addr() as usize].clone(); + let export = self.export(name).ok_or_else(|| Error::Other(format!("Export not found: {}", name)))?; + let ExternVal::Func(func_addr) = export else { + return Err(Error::Other(format!("Export is not a function: {}", name))); + }; - Ok(FuncHandle { - addr: export.index, - module: self.clone(), - name: Some(name.to_string()), - ty, - }) + let func_inst = store.get_func(func_addr as usize)?; + let ty = func_inst.func.ty(); + + Ok(FuncHandle { addr: func_addr, module: self.clone(), name: Some(name.to_string()), ty: ty.clone() }) } /// Get a typed exported function by name - pub fn typed_func(&self, store: &Store, name: &str) -> Result> + pub fn typed_func(&self, store: &Store, name: &str) -> Result> where P: IntoWasmValueTuple, R: FromWasmValueTuple, { let func = self.exported_func_by_name(store, name)?; - Ok(TypedFuncHandle { - func, - marker: core::marker::PhantomData, - }) + Ok(FuncHandleTyped { func, marker: core::marker::PhantomData }) } /// Get the start function of the module @@ -184,29 +204,19 @@ impl ModuleInstance { Some(func_index) => func_index, None => { // alternatively, check for a _start function in the exports - let Ok(start) = self.0.exports.get("_start", ExternalKind::Func) else { + let Some(ExternVal::Func(func_addr)) = self.export("_start") else { return Ok(None); }; - start.index + func_addr } }; - let func_addr = self - .0 - .func_addrs - .get(func_index as usize) - .expect("No func addr for start func, this is a bug"); - - let func = store.get_func(*func_addr as usize)?; - let ty = self.0.types[func.ty_addr() as usize].clone(); - - Ok(Some(FuncHandle { - module: self.clone(), - addr: *func_addr, - ty, - name: None, - })) + let func_addr = self.0.func_addrs.get(func_index as usize).expect("No func addr for start func, this is a bug"); + let func_inst = store.get_func(*func_addr as usize)?; + let ty = func_inst.func.ty(); + + Ok(Some(FuncHandle { module: self.clone(), addr: *func_addr, ty: ty.clone(), name: None })) } /// Invoke the start function of the module diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 0c1863f..36270a7 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -2,10 +2,7 @@ #![forbid(unsafe_code)] #![doc(test( no_crate_inject, - attr( - deny(warnings, rust_2018_idioms), - allow(dead_code, unused_assignments, unused_variables) - ) + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables)) ))] #![warn(missing_docs, missing_debug_implementations, rust_2018_idioms, unreachable_pub)] #![cfg_attr(nightly, feature(error_in_core))] @@ -17,12 +14,23 @@ //! to be useful for embedded systems and other environments where a full-featured //! runtime is not required. //! -//! ## Getting Started +//! ## Features +//! - `std` (default): Enables the use of `std` and `std::io` for parsing from files and streams. +//! - `logging` (default): Enables logging via the `log` crate. +//! - `parser` (default): Enables the `tinywasm_parser` crate for parsing WebAssembly modules. //! +//! ## No-std support +//! TinyWasm supports `no_std` environments by disabling the `std` feature and registering +//! a custom allocator. This removes support for parsing from files and streams, +//! but otherwise the API is the same. +//! Additionally, to have proper error types, you currently need a `nightly` compiler to have the error trait in core. +//! +//! ## Getting Started //! The easiest way to get started is to use the [`Module::parse_bytes`] function to load a //! WebAssembly module from bytes. This will parse the module and validate it, returning //! a [`Module`] that can be used to instantiate the module. //! +//! //! ```rust //! use tinywasm::{Store, Module}; //! @@ -42,25 +50,21 @@ //! //! // Get a typed handle to the exported "add" function //! // Alternatively, you can use `instance.get_func` to get an untyped handle -//! // that takes and returns WasmValue types -//! let func = instance.typed_func::<(i32, i32), (i32,)>(&mut store, "add")?; +//! // that takes and returns [`WasmValue`]s +//! let func = instance.typed_func::<(i32, i32), i32>(&mut store, "add")?; //! let res = func.call(&mut store, (1, 2))?; //! -//! assert_eq!(res, (3,)); +//! assert_eq!(res, 3); //! # Ok::<(), tinywasm::Error>(()) //! ``` //! -//! ## Features -//! - `std` (default): Enables the use of `std` and `std::io` for parsing from files and streams. -//! - `logging` (default): Enables logging via the `log` crate. -//! - `parser` (default): Enables the `tinywasm_parser` crate for parsing WebAssembly modules. +//! ## Imports //! -//! ## No-std support -//! TinyWasm supports `no_std` environments by disabling the `std` feature and registering -//! a custom allocator. This removes support for parsing from files and streams, -//! but otherwise the API is the same. +//! To provide imports to a module, you can use the [`Imports`] struct. +//! This struct allows you to register custom functions, globals, memories, tables, +//! and other modules to be linked into the module when it is instantiated. //! -//! Additionally, if you want proper error types, you must use a `nightly` compiler to have the error trait in core. +//! See the [`Imports`] documentation for more information. mod std; extern crate alloc; @@ -74,7 +78,13 @@ use log; #[cfg(not(feature = "logging"))] pub(crate) mod log { macro_rules! debug ( ($($tt:tt)*) => {{}} ); + macro_rules! info ( ($($tt:tt)*) => {{}} ); + macro_rules! trace ( ($($tt:tt)*) => {{}} ); + macro_rules! error ( ($($tt:tt)*) => {{}} ); pub(crate) use debug; + pub(crate) use error; + pub(crate) use info; + pub(crate) use trace; } mod error; @@ -89,17 +99,15 @@ pub use module::Module; mod instance; pub use instance::ModuleInstance; -mod export; -pub use export::ExportInstance; - mod func; -pub use func::{FuncHandle, TypedFuncHandle}; +pub use func::{FuncHandle, FuncHandleTyped}; mod imports; pub use imports::*; -mod runtime; -pub use runtime::*; +/// Runtime for executing WebAssembly modules. +pub mod runtime; +pub use runtime::InterpreterRuntime; #[cfg(feature = "parser")] /// Re-export of [`tinywasm_parser`]. Requires `parser` feature. diff --git a/crates/tinywasm/src/runtime/executor/macros.rs b/crates/tinywasm/src/runtime/interpreter/macros.rs similarity index 81% rename from crates/tinywasm/src/runtime/executor/macros.rs rename to crates/tinywasm/src/runtime/interpreter/macros.rs index 8a92ac2..909acb3 100644 --- a/crates/tinywasm/src/runtime/executor/macros.rs +++ b/crates/tinywasm/src/runtime/interpreter/macros.rs @@ -17,13 +17,25 @@ macro_rules! mem_load { let addr = $stack.values.pop()?.raw_value(); + let addr = $arg.offset.checked_add(addr).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: $arg.offset as usize, + len: core::mem::size_of::<$load_type>(), + max: mem.borrow().max_pages(), + }) + })?; + + let addr: usize = addr.try_into().ok().ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: $arg.offset as usize, + len: core::mem::size_of::<$load_type>(), + max: mem.borrow().max_pages(), + }) + })?; + let val: [u8; core::mem::size_of::<$load_type>()] = { let mem = mem.borrow_mut(); - let val = mem.load( - ($arg.offset + addr) as usize, - $arg.align as usize, - core::mem::size_of::<$load_type>(), - )?; + let val = mem.load(addr, $arg.align as usize, core::mem::size_of::<$load_type>())?; val.try_into().expect("slice with incorrect length") }; @@ -51,15 +63,16 @@ macro_rules! mem_store { let val = val as $store_type; let val = val.to_le_bytes(); - mem.borrow_mut() - .store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; + mem.borrow_mut().store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; }}; } /// Doing the actual conversion from float to int is a bit tricky, because /// we need to check for overflow. This macro generates the min/max values /// for a specific conversion, which are then used in the actual conversion. -/// Rust sadly doesn't have wrapping casts for floats (yet) +/// Rust sadly doesn't have wrapping casts for floats yet, maybe never. +/// Alternatively, https://crates.io/crates/az could be used for this but +/// it's not worth the dependency. macro_rules! float_min_max { (f32, i32) => { (-2147483904.0_f32, 2147483648.0_f32) @@ -186,16 +199,22 @@ macro_rules! arithmetic_single { let result = a.$op(); $stack.values.push((result as $ty).into()); }}; + + ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ + let a: $from = $stack.values.pop()?.into(); + let result = a.$op(); + $stack.values.push((result as $to).into()); + }}; } /// Apply an arithmetic operation to two values on the stack with error checking -macro_rules! checked_arithmetic { +macro_rules! checked_int_arithmetic { // Direct conversion with error checking (two types) - ($from:tt, $to:tt, $stack:ident, $trap:expr) => {{ - checked_arithmetic!($from, $to, $to, $stack, $trap) + ($from:tt, $to:tt, $stack:ident) => {{ + checked_int_arithmetic!($from, $to, $to, $stack) }}; - ($op:ident, $from:ty, $to:ty, $stack:ident, $trap:expr) => {{ + ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ let [a, b] = $stack.values.pop_n_const::<2>()?; let a: $from = a.into(); let b: $from = b.into(); @@ -203,7 +222,11 @@ macro_rules! checked_arithmetic { let a_casted: $to = a as $to; let b_casted: $to = b as $to; - let result = a_casted.$op(b_casted).ok_or_else(|| Error::Trap($trap))?; + if b_casted == 0 { + return Err(Error::Trap(crate::Trap::DivisionByZero)); + } + + let result = a_casted.$op(b_casted).ok_or_else(|| Error::Trap(crate::Trap::IntegerOverflow))?; // Cast back to original type if different $stack.values.push((result as $from).into()); @@ -212,8 +235,8 @@ macro_rules! checked_arithmetic { pub(super) use arithmetic; pub(super) use arithmetic_single; -pub(super) use checked_arithmetic; pub(super) use checked_conv_float; +pub(super) use checked_int_arithmetic; pub(super) use comp; pub(super) use comp_zero; pub(super) use conv; diff --git a/crates/tinywasm/src/runtime/executor/mod.rs b/crates/tinywasm/src/runtime/interpreter/mod.rs similarity index 63% rename from crates/tinywasm/src/runtime/executor/mod.rs rename to crates/tinywasm/src/runtime/interpreter/mod.rs index 541c0a7..db013a0 100644 --- a/crates/tinywasm/src/runtime/executor/mod.rs +++ b/crates/tinywasm/src/runtime/interpreter/mod.rs @@ -1,43 +1,63 @@ -use core::ops::{BitAnd, BitOr, BitXor, Neg}; - -use super::{DefaultRuntime, Stack}; +use super::{InterpreterRuntime, Stack}; +use crate::log; use crate::{ log::debug, - runtime::{BlockType, LabelFrame}, - CallFrame, Error, LabelArgs, ModuleInstance, Result, Store, + runtime::{BlockType, CallFrame, LabelArgs, LabelFrame}, + Error, FuncContext, ModuleInstance, Result, Store, Trap, }; -use alloc::vec::Vec; -use log::info; -use tinywasm_types::Instruction; +use alloc::{string::ToString, vec::Vec}; +use core::ops::{BitAnd, BitOr, BitXor, Neg}; +use tinywasm_types::{ElementKind, Instruction, ValType}; + +#[cfg(not(feature = "std"))] +mod no_std_floats; + +#[cfg(not(feature = "std"))] +#[allow(unused_imports)] +use no_std_floats::FExt; mod macros; mod traits; + use macros::*; use traits::*; -impl DefaultRuntime { - pub(crate) fn exec(&self, store: &mut Store, stack: &mut Stack, module: ModuleInstance) -> Result<()> { - log::info!("exports: {:?}", module.exports()); - log::info!("func_addrs: {:?}", module.func_addrs()); - log::info!("func_ty_addrs: {:?}", module.func_ty_addrs().len()); - log::info!("store funcs: {:?}", store.data.funcs.len()); - +impl InterpreterRuntime { + pub(crate) fn exec(&self, store: &mut Store, stack: &mut Stack) -> Result<()> { // The current call frame, gets updated inside of exec_one let mut cf = stack.call_stack.pop()?; + let mut func_inst = cf.func_instance.clone(); + let mut wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); + // The function to execute, gets updated from ExecResult::Call - let mut func = store.get_func(cf.func_ptr)?.clone(); - let mut instrs = func.instructions(); + let mut instrs = &wasm_func.instructions; + + let mut current_module = store.get_module_instance(func_inst.owner).unwrap().clone(); - // TODO: we might be able to index into the instructions directly - // since the instruction pointer should always be in bounds while let Some(instr) = instrs.get(cf.instr_ptr) { - match exec_one(&mut cf, instr, instrs, stack, store, &module)? { + match exec_one(&mut cf, instr, instrs, stack, store, ¤t_module)? { // Continue execution at the new top of the call stack ExecResult::Call => { cf = stack.call_stack.pop()?; - func = store.get_func(cf.func_ptr)?.clone(); - instrs = func.instructions(); + func_inst = cf.func_instance.clone(); + wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); + instrs = &wasm_func.instructions; + + if cf.func_instance.owner != current_module.id() { + current_module.swap( + store + .get_module_instance(cf.func_instance.owner) + .unwrap_or_else(|| { + panic!( + "exec expected module instance {} to exist for function", + cf.func_instance.owner + ) + }) + .clone(), + ); + } + continue; } @@ -54,7 +74,7 @@ impl DefaultRuntime { cf.instr_ptr += 1; // push the call frame back onto the stack so that it can be resumed // if the trap can be handled - stack.call_stack.push(cf); + stack.call_stack.push(cf)?; return Err(Error::Trap(trap)); } } @@ -104,18 +124,18 @@ fn exec_one( store: &mut Store, module: &ModuleInstance, ) -> Result { - info!("ptr: {} instr: {:?}", cf.instr_ptr, instr); + debug!("ptr: {} instr: {:?}", cf.instr_ptr, instr); use tinywasm_types::Instruction::*; match instr { Nop => { /* do nothing */ } Unreachable => return Ok(ExecResult::Trap(crate::Trap::Unreachable)), // we don't need to include the call frame here because it's already on the stack Drop => stack.values.pop().map(|_| ())?, - Select(t) => { - if t.is_some() { - unimplemented!("select with type"); - } + Select( + _valtype, // due to validation, we know that the type of the values on the stack are correct + ) => { + // due to validation, we know that the type of the values on the stack let cond: i32 = stack.values.pop()?.into(); let val2 = stack.values.pop()?; @@ -125,48 +145,94 @@ fn exec_one( stack.values.push(val2); } } + Call(v) => { - debug!("start call"); // prepare the call frame let func_idx = module.resolve_func_addr(*v); - let func = store.get_func(func_idx as usize)?; - let func_ty = module.func_ty(func.ty_addr()); + let func_inst = store.get_func(func_idx as usize)?.clone(); + + let (locals, ty) = match &func_inst.func { + crate::Function::Wasm(ref f) => (f.locals.to_vec(), f.ty.clone()), + crate::Function::Host(host_func) => { + let func = host_func.func.clone(); + let params = stack.values.pop_params(&host_func.ty.params)?; + let res = (func)(FuncContext { store, module }, ¶ms)?; + stack.values.extend_from_typed(&res); + return Ok(ExecResult::Ok); + } + }; - debug!("params: {:?}", func_ty.params); - debug!("stack: {:?}", stack.values); - let params = stack.values.pop_n(func_ty.params.len())?; - let call_frame = CallFrame::new_raw(*v as usize, ¶ms, func.locals().to_vec()); + let params = stack.values.pop_n_rev(ty.params.len())?; + let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); // push the call frame cf.instr_ptr += 1; // skip the call instruction - stack.call_stack.push(cf.clone()); - stack.call_stack.push(call_frame); + stack.call_stack.push(cf.clone())?; + stack.call_stack.push(call_frame)?; // call the function return Ok(ExecResult::Call); } - If(args, else_offset, end_offset) => { - if stack.values.pop_t::()? == 0 { - if let Some(else_offset) = else_offset { - log::info!("entering else at {}", cf.instr_ptr + *else_offset); - cf.enter_label( - LabelFrame { - instr_ptr: cf.instr_ptr + *else_offset, - end_instr_ptr: cf.instr_ptr + *end_offset, - stack_ptr: stack.values.len(), // - params, - args: crate::LabelArgs::new(*args, module)?, - ty: BlockType::Else, - }, - &mut stack.values, - ); - cf.instr_ptr += *else_offset; - } else { - log::info!("skipping if"); - cf.instr_ptr += *end_offset + CallIndirect(type_addr, table_addr) => { + let table = store.get_table(module.resolve_table_addr(*table_addr) as usize)?; + let table_idx = stack.values.pop_t::()?; + + // verify that the table is of the right type, this should be validated by the parser already + assert!(table.borrow().kind.element_type == ValType::RefFunc, "table is not of type funcref"); + + let func_ref = { + table + .borrow() + .get(table_idx as usize)? + .addr() + .ok_or(Trap::UninitializedElement { index: table_idx as usize })? + }; + + let func_inst = store.get_func(func_ref as usize)?.clone(); + let func_ty = func_inst.func.ty(); + + log::info!("type_addr: {}", type_addr); + log::info!("types: {:?}", module.func_tys()); + let call_ty = module.func_ty(*type_addr); + + log::info!("call_indirect: current fn owner: {:?}", module.id()); + log::info!("call_indirect: func owner: {:?}", func_inst.owner); + + if func_ty != call_ty { + log::error!("indirect call type mismatch: {:?} != {:?}", func_ty, call_ty); + return Err( + Trap::IndirectCallTypeMismatch { actual: func_ty.clone(), expected: call_ty.clone() }.into() + ); + } + + let locals = match &func_inst.func { + crate::Function::Wasm(ref f) => f.locals.to_vec(), + crate::Function::Host(host_func) => { + let func = host_func.func.clone(); + let params = stack.values.pop_params(&func_ty.params)?; + let res = (func)(FuncContext { store, module }, ¶ms)?; + stack.values.extend_from_typed(&res); + return Ok(ExecResult::Ok); } - } else { - log::info!("entering then"); + }; + + let params = stack.values.pop_n_rev(func_ty.params.len())?; + let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); + + // push the call frame + cf.instr_ptr += 1; // skip the call instruction + stack.call_stack.push(cf.clone())?; + stack.call_stack.push(call_frame)?; + + // call the function + return Ok(ExecResult::Call); + } + + If(args, else_offset, end_offset) => { + // truthy value is on the top of the stack, so enter the then block + if stack.values.pop_t::()? != 0 { + log::trace!("entering then"); cf.enter_label( LabelFrame { instr_ptr: cf.instr_ptr, @@ -176,7 +242,26 @@ fn exec_one( ty: BlockType::If, }, &mut stack.values, - ) + ); + return Ok(ExecResult::Ok); + } + + // falsy value is on the top of the stack + if let Some(else_offset) = else_offset { + log::debug!("entering else at {}", cf.instr_ptr + *else_offset); + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr + *else_offset, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), // - params, + args: LabelArgs::new(*args, module)?, + ty: BlockType::Else, + }, + &mut stack.values, + ); + cf.instr_ptr += *else_offset; + } else { + cf.instr_ptr += *end_offset; } } @@ -217,20 +302,21 @@ fn exec_one( .collect::>>()?; if instr.len() != *len { - panic!("Expected {} BrLabel instructions, got {}", len, instr.len()); + panic!( + "Expected {} BrLabel instructions, got {}, this should have been validated by the parser", + len, + instr.len() + ); } let idx = stack.values.pop_t::()? as usize; - if let Some(label) = instr.get(idx) { - break_to!(cf, stack, label); - } else { - break_to!(cf, stack, default); - } + let to = instr.get(idx).unwrap_or(default); + break_to!(cf, stack, to); } Br(v) => break_to!(cf, stack, v), BrIf(v) => { - if stack.values.pop_t::()? > 0 { + if stack.values.pop_t::()? != 0 { break_to!(cf, stack, v); } } @@ -241,7 +327,7 @@ fn exec_one( }, EndFunc => { - debug_assert!( + assert!( cf.labels.len() == 0, "endfunc: block frames not empty, this should have been validated by the parser" ); @@ -298,12 +384,12 @@ fn exec_one( let mem_idx = module.resolve_mem_addr(*addr); let mem = store.get_mem(mem_idx as usize)?; - stack.values.push(mem.borrow().size().into()); + stack.values.push((mem.borrow().page_count() as i32).into()); } MemoryGrow(addr, byte) => { if *byte != 0 { - unimplemented!("memory.grow with byte != 0"); + return Err(Error::UnsupportedFeature("memory.grow with byte != 0".to_string())); } let mem_idx = module.resolve_mem_addr(*addr); @@ -311,13 +397,13 @@ fn exec_one( let (res, prev_size) = { let mut mem = mem.borrow_mut(); - let prev_size = mem.size(); + let prev_size = mem.page_count() as i32; (mem.grow(stack.values.pop_t::()?), prev_size) }; match res { - Ok(_) => stack.values.push(prev_size.into()), - Err(_) => stack.values.push((-1).into()), + Some(_) => stack.values.push(prev_size.into()), + None => stack.values.push((-1).into()), } } @@ -406,15 +492,15 @@ fn exec_one( F64Mul => arithmetic!(*, f64, stack), // these can trap - I32DivS => checked_arithmetic!(checked_div, i32, stack, crate::Trap::DivisionByZero), - I64DivS => checked_arithmetic!(checked_div, i64, stack, crate::Trap::DivisionByZero), - I32DivU => checked_arithmetic!(checked_div, i32, u32, stack, crate::Trap::DivisionByZero), - I64DivU => checked_arithmetic!(checked_div, i64, u64, stack, crate::Trap::DivisionByZero), + I32DivS => checked_int_arithmetic!(checked_div, i32, stack), + I64DivS => checked_int_arithmetic!(checked_div, i64, stack), + I32DivU => checked_int_arithmetic!(checked_div, i32, u32, stack), + I64DivU => checked_int_arithmetic!(checked_div, i64, u64, stack), - I32RemS => checked_arithmetic!(checked_wrapping_rem, i32, stack, crate::Trap::DivisionByZero), - I64RemS => checked_arithmetic!(checked_wrapping_rem, i64, stack, crate::Trap::DivisionByZero), - I32RemU => checked_arithmetic!(checked_wrapping_rem, i32, u32, stack, crate::Trap::DivisionByZero), - I64RemU => checked_arithmetic!(checked_wrapping_rem, i64, u64, stack, crate::Trap::DivisionByZero), + I32RemS => checked_int_arithmetic!(checked_wrapping_rem, i32, stack), + I64RemS => checked_int_arithmetic!(checked_wrapping_rem, i64, stack), + I32RemU => checked_int_arithmetic!(checked_wrapping_rem, i32, u32, stack), + I64RemU => checked_int_arithmetic!(checked_wrapping_rem, i64, u64, stack), I32And => arithmetic!(bitand, i32, stack), I64And => arithmetic!(bitand, i64, stack), @@ -497,9 +583,58 @@ fn exec_one( I64TruncF32U => checked_conv_float!(f32, u64, i64, stack), I64TruncF64U => checked_conv_float!(f64, u64, i64, stack), + TableGet(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + let idx = stack.values.pop_t::()? as usize; + let v = table.borrow().get_wasm_val(idx)?; + stack.values.push(v.into()); + } + + TableSet(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + let val = stack.values.pop_t::()?; + let idx = stack.values.pop_t::()? as usize; + table.borrow_mut().set(idx, val)?; + } + + TableSize(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + stack.values.push(table.borrow().size().into()); + } + + TableInit(table_index, elem_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + + let elem_idx = module.resolve_elem_addr(*elem_index); + let elem = store.get_elem(elem_idx as usize)?; + + if elem.kind != ElementKind::Passive { + return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); + } + + let Some(items) = elem.items.as_ref() else { + return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); + }; + + table.borrow_mut().init(module.func_addrs(), 0, items)?; + } + + I32TruncSatF32S => arithmetic_single!(trunc, f32, i32, stack), + I32TruncSatF32U => arithmetic_single!(trunc, f32, u32, stack), + I32TruncSatF64S => arithmetic_single!(trunc, f64, i32, stack), + I32TruncSatF64U => arithmetic_single!(trunc, f64, u32, stack), + I64TruncSatF32S => arithmetic_single!(trunc, f32, i64, stack), + I64TruncSatF32U => arithmetic_single!(trunc, f32, u64, stack), + I64TruncSatF64S => arithmetic_single!(trunc, f64, i64, stack), + I64TruncSatF64U => arithmetic_single!(trunc, f64, u64, stack), + i => { log::error!("unimplemented instruction: {:?}", i); - panic!("Unimplemented instruction: {:?}", i) + return Err(Error::UnsupportedFeature(alloc::format!("unimplemented instruction: {:?}", i))); } }; diff --git a/crates/tinywasm/src/runtime/interpreter/no_std_floats.rs b/crates/tinywasm/src/runtime/interpreter/no_std_floats.rs new file mode 100644 index 0000000..095fe9f --- /dev/null +++ b/crates/tinywasm/src/runtime/interpreter/no_std_floats.rs @@ -0,0 +1,77 @@ +pub(super) trait FExt { + fn round(self) -> Self; + fn abs(self) -> Self; + fn signum(self) -> Self; + fn ceil(self) -> Self; + fn floor(self) -> Self; + fn trunc(self) -> Self; + fn sqrt(self) -> Self; + fn copysign(self, other: Self) -> Self; +} + +impl FExt for f64 { + fn round(self) -> Self { + libm::round(self) + } + + fn abs(self) -> Self { + libm::fabs(self) + } + + fn signum(self) -> Self { + libm::copysign(1.0, self) + } + + fn ceil(self) -> Self { + libm::ceil(self) + } + + fn floor(self) -> Self { + libm::floor(self) + } + + fn trunc(self) -> Self { + libm::trunc(self) + } + + fn sqrt(self) -> Self { + libm::sqrt(self) + } + + fn copysign(self, other: Self) -> Self { + libm::copysign(self, other) + } +} +impl FExt for f32 { + fn round(self) -> Self { + libm::roundf(self) + } + + fn abs(self) -> Self { + libm::fabsf(self) + } + + fn signum(self) -> Self { + libm::copysignf(1.0, self) + } + + fn ceil(self) -> Self { + libm::ceilf(self) + } + + fn floor(self) -> Self { + libm::floorf(self) + } + + fn trunc(self) -> Self { + libm::truncf(self) + } + + fn sqrt(self) -> Self { + libm::sqrtf(self) + } + + fn copysign(self, other: Self) -> Self { + libm::copysignf(self, other) + } +} diff --git a/crates/tinywasm/src/runtime/executor/traits.rs b/crates/tinywasm/src/runtime/interpreter/traits.rs similarity index 78% rename from crates/tinywasm/src/runtime/executor/traits.rs rename to crates/tinywasm/src/runtime/interpreter/traits.rs index 9bddba1..06a97e3 100644 --- a/crates/tinywasm/src/runtime/executor/traits.rs +++ b/crates/tinywasm/src/runtime/interpreter/traits.rs @@ -11,18 +11,37 @@ pub(crate) trait WasmFloatOps { fn wasm_nearest(self) -> Self; } +#[cfg(not(feature = "std"))] +use super::no_std_floats::FExt; + macro_rules! impl_wasm_float_ops { ($($t:ty)*) => ($( impl WasmFloatOps for $t { // https://webassembly.github.io/spec/core/exec/numerics.html#op-fnearest fn wasm_nearest(self) -> Self { - log::info!("wasm_nearest: {}", self); match self { - x if x.is_nan() => x, - x if x.is_infinite() || x == 0.0 => x, + x if x.is_nan() => x, // preserve NaN + x if x.is_infinite() || x == 0.0 => x, // preserve infinities and zeros x if (0.0..=0.5).contains(&x) => 0.0, x if (-0.5..0.0).contains(&x) => -0.0, - x => x.round(), + // x => x.round(), + x => { + // Handle normal and halfway cases + let rounded = x.round(); + let diff = (x - rounded).abs(); + + if diff == 0.5 { + // Halfway case: round to even + if rounded % 2.0 == 0.0 { + rounded // Already even + } else { + rounded - x.signum() // Make even + } + } else { + // Normal case + rounded + } + } } } diff --git a/crates/tinywasm/src/runtime/mod.rs b/crates/tinywasm/src/runtime/mod.rs index 610810f..3b9a57c 100644 --- a/crates/tinywasm/src/runtime/mod.rs +++ b/crates/tinywasm/src/runtime/mod.rs @@ -1,19 +1,23 @@ -mod executor; +mod interpreter; mod stack; mod value; pub use stack::*; pub(crate) use value::RawWasmValue; +use crate::Result; + #[allow(rustdoc::private_intra_doc_links)] -/// A WebAssembly Runtime. -/// -/// Generic over `CheckTypes` to enable type checking at runtime. -/// This is useful for debugging, but should be disabled if you know -/// that the module is valid. +/// A WebAssembly runtime. /// /// See +pub trait Runtime { + /// Execute all call-frames on the stack until the stack is empty. + fn exec(&self, store: &mut crate::Store, stack: &mut crate::runtime::Stack) -> Result<()>; +} + +/// The main TinyWasm runtime. /// -/// Execution is implemented in the [`crate::runtime::executor`] module +/// This is the default runtime used by TinyWasm. #[derive(Debug, Default)] -pub struct DefaultRuntime {} +pub struct InterpreterRuntime {} diff --git a/crates/tinywasm/src/runtime/stack/blocks.rs b/crates/tinywasm/src/runtime/stack/blocks.rs index f5a0d8d..76252b5 100644 --- a/crates/tinywasm/src/runtime/stack/blocks.rs +++ b/crates/tinywasm/src/runtime/stack/blocks.rs @@ -72,10 +72,9 @@ impl LabelArgs { Ok(match args { BlockArgs::Empty => LabelArgs { params: 0, results: 0 }, BlockArgs::Type(_) => LabelArgs { params: 0, results: 1 }, - BlockArgs::FuncType(t) => LabelArgs { - params: module.func_ty(t).params.len(), - results: module.func_ty(t).results.len(), - }, + BlockArgs::FuncType(t) => { + LabelArgs { params: module.func_ty(t).params.len(), results: module.func_ty(t).results.len() } + } }) } } diff --git a/crates/tinywasm/src/runtime/stack/call_stack.rs b/crates/tinywasm/src/runtime/stack/call_stack.rs index 239659c..20de728 100644 --- a/crates/tinywasm/src/runtime/stack/call_stack.rs +++ b/crates/tinywasm/src/runtime/stack/call_stack.rs @@ -1,12 +1,16 @@ -use crate::{runtime::RawWasmValue, BlockType, Error, LabelFrame, Result}; -use alloc::{boxed::Box, vec::Vec}; +use crate::log; +use crate::{ + runtime::{BlockType, RawWasmValue}, + Error, FunctionInstance, Result, Trap, +}; +use alloc::{boxed::Box, rc::Rc, vec::Vec}; use tinywasm_types::{ValType, WasmValue}; -use super::blocks::Labels; +use super::{blocks::Labels, LabelFrame}; // minimum call stack size -const CALL_STACK_SIZE: usize = 1024; -const CALL_STACK_MAX_SIZE: usize = 1024 * 1024; +const CALL_STACK_SIZE: usize = 128; +const CALL_STACK_MAX_SIZE: usize = 1024; #[derive(Debug)] pub(crate) struct CallStack { @@ -16,10 +20,7 @@ pub(crate) struct CallStack { impl Default for CallStack { fn default() -> Self { - Self { - stack: Vec::with_capacity(CALL_STACK_SIZE), - top: 0, - } + Self { stack: Vec::with_capacity(CALL_STACK_SIZE), top: 0 } } } @@ -39,38 +40,25 @@ impl CallStack { } #[inline] - pub(crate) fn _top(&self) -> Result<&CallFrame> { - assert!(self.top <= self.stack.len()); - if self.top == 0 { - return Err(Error::CallStackEmpty); - } - Ok(&self.stack[self.top - 1]) - } + pub(crate) fn push(&mut self, call_frame: CallFrame) -> Result<()> { + assert!(self.top <= self.stack.len(), "stack is too small"); - #[inline] - pub(crate) fn _top_mut(&mut self) -> Result<&mut CallFrame> { - assert!(self.top <= self.stack.len()); - if self.top == 0 { - return Err(Error::CallStackEmpty); + log::debug!("stack size: {}", self.stack.len()); + if self.stack.len() >= CALL_STACK_MAX_SIZE { + return Err(Trap::CallStackOverflow.into()); } - Ok(&mut self.stack[self.top - 1]) - } - - #[inline] - pub(crate) fn push(&mut self, call_frame: CallFrame) { - assert!(self.top <= self.stack.len()); - assert!(self.stack.len() <= CALL_STACK_MAX_SIZE); self.top += 1; self.stack.push(call_frame); + Ok(()) } } #[derive(Debug, Clone)] pub(crate) struct CallFrame { - // having real pointers here would be nice :( but we can't really do that in safe rust pub(crate) instr_ptr: usize, - pub(crate) func_ptr: usize, + // pub(crate) module: ModuleInstanceAddr, + pub(crate) func_instance: Rc, pub(crate) labels: Labels, pub(crate) locals: Box<[RawWasmValue]>, @@ -92,62 +80,63 @@ impl CallFrame { /// Returns `None` if there is no block at the given index (e.g. if we need to return, this is handled by the caller) #[inline] pub(crate) fn break_to(&mut self, break_to_relative: u32, value_stack: &mut super::ValueStack) -> Option<()> { + log::debug!("break_to_relative: {}", break_to_relative); let break_to = self.labels.get_relative_to_top(break_to_relative as usize)?; - value_stack.break_to(break_to.stack_ptr, break_to.args.results); // instr_ptr points to the label instruction, but the next step // will increment it by 1 since we're changing the "current" instr_ptr match break_to.ty { BlockType::Loop => { // this is a loop, so we want to jump back to the start of the loop + // We also want to push the params to the stack + value_stack.break_to(break_to.stack_ptr, break_to.args.params); + self.instr_ptr = break_to.instr_ptr; // we also want to trim the label stack to the loop (but not including the loop) self.labels.truncate(self.labels.len() - break_to_relative as usize); } BlockType::Block | BlockType::If | BlockType::Else => { - // this is a block, so we want to jump to the next instruction after the block ends (the inst_ptr will be incremented by 1 before the next instruction is executed) + // this is a block, so we want to jump to the next instruction after the block ends + // We also want to push the block's results to the stack + value_stack.break_to(break_to.stack_ptr, break_to.args.results); + + // (the inst_ptr will be incremented by 1 before the next instruction is executed) self.instr_ptr = break_to.end_instr_ptr; // we also want to trim the label stack, including the block - self.labels - .truncate(self.labels.len() - (break_to_relative as usize + 1)); + self.labels.truncate(self.labels.len() - (break_to_relative as usize + 1)); } } - // self.instr_ptr = block_frame.instr_ptr; - // value_stack.trim(block_frame.stack_ptr); - - // // // Adjusting how to trim the blocks stack based on the block type - // // let trim_index = match block_frame.block { - // // // if we are breaking to a loop, we want to jump back to the start of the loop - // // BlockFrameInner::Loop => block_index as usize - 1, - // // // if we are breaking to any other block, we want to jump to the end of the block - // // // TODO: check if this is correct - // // BlockFrameInner::If | BlockFrameInner::Else | BlockFrameInner::Block => block_index as usize - 1, - // // }; - - // self.block_frames.trim(block_index as usize); Some(()) } - pub(crate) fn new_raw(func_ptr: usize, params: &[RawWasmValue], local_types: Vec) -> Self { + pub(crate) fn new_raw( + func_instance_ptr: Rc, + params: &[RawWasmValue], + local_types: Vec, + ) -> Self { let mut locals = Vec::with_capacity(local_types.len() + params.len()); locals.extend(params.iter().cloned()); locals.extend(local_types.iter().map(|_| RawWasmValue::default())); Self { instr_ptr: 0, - func_ptr, + func_instance: func_instance_ptr, local_count: locals.len(), locals: locals.into_boxed_slice(), labels: Labels::default(), } } - pub(crate) fn new(func_ptr: usize, params: &[WasmValue], local_types: Vec) -> Self { + pub(crate) fn new( + func_instance_ptr: Rc, + params: &[WasmValue], + local_types: Vec, + ) -> Self { CallFrame::new_raw( - func_ptr, + func_instance_ptr, ¶ms.iter().map(|v| RawWasmValue::from(*v)).collect::>(), local_types, ) @@ -155,19 +144,13 @@ impl CallFrame { #[inline] pub(crate) fn set_local(&mut self, local_index: usize, value: RawWasmValue) { - if local_index >= self.local_count { - panic!("Invalid local index"); - } - + assert!(local_index < self.local_count, "Invalid local index"); self.locals[local_index] = value; } #[inline] pub(crate) fn get_local(&self, local_index: usize) -> RawWasmValue { - if local_index >= self.local_count { - panic!("Invalid local index"); - } - + assert!(local_index < self.local_count, "Invalid local index"); self.locals[local_index] } } diff --git a/crates/tinywasm/src/runtime/stack/value_stack.rs b/crates/tinywasm/src/runtime/stack/value_stack.rs index 10054bc..bb4e398 100644 --- a/crates/tinywasm/src/runtime/stack/value_stack.rs +++ b/crates/tinywasm/src/runtime/stack/value_stack.rs @@ -1,7 +1,9 @@ use core::ops::Range; +use crate::log; use crate::{runtime::RawWasmValue, Error, Result}; use alloc::vec::Vec; +use tinywasm_types::{ValType, WasmValue}; // minimum stack size pub(crate) const STACK_SIZE: usize = 1024; @@ -9,17 +11,12 @@ pub(crate) const STACK_SIZE: usize = 1024; #[derive(Debug)] pub(crate) struct ValueStack { stack: Vec, - - // TODO: don't pop the stack, just keep track of the top for better performance top: usize, } impl Default for ValueStack { fn default() -> Self { - Self { - stack: Vec::with_capacity(STACK_SIZE), - top: 0, - } + Self { stack: Vec::with_capacity(STACK_SIZE), top: 0 } } } @@ -30,6 +27,16 @@ impl ValueStack { self.stack.extend_from_within(range); } + #[inline] + pub(crate) fn extend_from_typed(&mut self, values: &[WasmValue]) { + if values.is_empty() { + return; + } + + self.top += values.len(); + self.stack.extend(values.iter().map(|v| RawWasmValue::from(*v))); + } + #[inline] pub(crate) fn len(&self) -> usize { assert!(self.top <= self.stack.len()); @@ -38,10 +45,7 @@ impl ValueStack { pub(crate) fn truncate_keep(&mut self, n: usize, end_keep: usize) { let total_to_keep = n + end_keep; - assert!( - self.top >= total_to_keep, - "Total to keep should be less than or equal to self.top" - ); + assert!(self.top >= total_to_keep, "Total to keep should be less than or equal to self.top"); let current_size = self.stack.len(); if current_size <= total_to_keep { @@ -79,9 +83,22 @@ impl ValueStack { self.stack.pop().ok_or(Error::StackUnderflow) } + #[inline] + pub(crate) fn pop_params(&mut self, types: &[ValType]) -> Result> { + log::info!("pop_params: types={:?}", types); + log::info!("stack={:?}", self.stack); + + let mut res = Vec::with_capacity(types.len()); + for ty in types.iter() { + res.push(self.pop()?.attach_type(*ty)); + } + + Ok(res) + } + pub(crate) fn break_to(&mut self, new_stack_size: usize, result_count: usize) { - self.stack - .copy_within((self.top - result_count)..self.top, new_stack_size); + assert!(self.top >= result_count); + self.stack.copy_within((self.top - result_count)..self.top, new_stack_size); self.top = new_stack_size + result_count; self.stack.truncate(self.top); } @@ -95,12 +112,12 @@ impl ValueStack { } #[inline] - pub(crate) fn pop_n(&mut self, n: usize) -> Result> { + pub(crate) fn pop_n_rev(&mut self, n: usize) -> Result> { if self.top < n { return Err(Error::StackUnderflow); } self.top -= n; - let res = self.stack.drain(self.top..).rev().collect::>(); + let res = self.stack.drain(self.top..).collect::>(); Ok(res) } diff --git a/crates/tinywasm/src/runtime/value.rs b/crates/tinywasm/src/runtime/value.rs index b25f634..7230830 100644 --- a/crates/tinywasm/src/runtime/value.rs +++ b/crates/tinywasm/src/runtime/value.rs @@ -27,9 +27,20 @@ impl RawWasmValue { ValType::I64 => WasmValue::I64(self.0 as i64), ValType::F32 => WasmValue::F32(f32::from_bits(self.0 as u32)), ValType::F64 => WasmValue::F64(f64::from_bits(self.0)), - ValType::ExternRef => todo!("externref"), - ValType::FuncRef => todo!("funcref"), - ValType::V128 => todo!("v128"), + ValType::RefExtern => { + if self.0 == -1i64 as u64 { + WasmValue::RefNull(ValType::RefExtern) + } else { + WasmValue::RefExtern(self.0 as u32) + } + } + ValType::RefFunc => { + if self.0 == -1i64 as u64 { + WasmValue::RefNull(ValType::RefFunc) + } else { + WasmValue::RefFunc(self.0 as u32) + } + } } } } @@ -41,7 +52,9 @@ impl From for RawWasmValue { WasmValue::I64(i) => Self(i as u64), WasmValue::F32(i) => Self(i.to_bits() as u64), WasmValue::F64(i) => Self(i.to_bits()), - WasmValue::RefNull(_) => Self(0), + WasmValue::RefExtern(v) => Self(v as i64 as u64), + WasmValue::RefFunc(v) => Self(v as i64 as u64), + WasmValue::RefNull(_) => Self(-1i64 as u64), } } } @@ -71,5 +84,8 @@ impl_from_raw_wasm_value!(i64, |x| x as u64, |x| x as i64); impl_from_raw_wasm_value!(f32, |x| f32::to_bits(x) as u64, |x| f32::from_bits(x as u32)); impl_from_raw_wasm_value!(f64, f64::to_bits, f64::from_bits); +// convenience impls (not actually part of the spec) impl_from_raw_wasm_value!(i8, |x| x as u64, |x| x as i8); impl_from_raw_wasm_value!(i16, |x| x as u64, |x| x as i16); +impl_from_raw_wasm_value!(u32, |x| x as u64, |x| x as u32); +impl_from_raw_wasm_value!(u64, |x| x, |x| x); diff --git a/crates/tinywasm/src/store.rs b/crates/tinywasm/src/store.rs index 564232d..d281b5c 100644 --- a/crates/tinywasm/src/store.rs +++ b/crates/tinywasm/src/store.rs @@ -1,19 +1,14 @@ -#![allow(dead_code)] // TODO: remove this - +use crate::log; +use alloc::{boxed::Box, format, rc::Rc, string::ToString, vec, vec::Vec}; use core::{ cell::RefCell, sync::atomic::{AtomicUsize, Ordering}, }; - -use alloc::{format, rc::Rc, string::ToString, vec, vec::Vec}; -use tinywasm_types::{ - Addr, Data, Element, ElementKind, FuncAddr, Function, Global, GlobalType, Import, Instruction, MemAddr, MemoryArch, - MemoryType, ModuleInstanceAddr, TableAddr, TableType, TypeAddr, ValType, -}; +use tinywasm_types::*; use crate::{ - runtime::{self, DefaultRuntime}, - Error, Extern, LinkedImports, ModuleInstance, RawWasmValue, Result, + runtime::{self, InterpreterRuntime, RawWasmValue}, + Error, Function, ModuleInstance, Result, Trap, }; // global store id counter @@ -49,10 +44,16 @@ impl Store { Self::default() } + /// Get a module instance by the internal id + pub fn get_module_instance(&self, addr: ModuleInstanceAddr) -> Option<&ModuleInstance> { + log::debug!("existing module instances: {:?}", self.module_instances.len()); + self.module_instances.get(addr as usize) + } + /// Create a new store with the given runtime - pub(crate) fn runtime(&self) -> runtime::DefaultRuntime { + pub(crate) fn runtime(&self) -> runtime::InterpreterRuntime { match self.runtime { - Runtime::Default => DefaultRuntime::default(), + Runtime::Default => InterpreterRuntime::default(), } } } @@ -82,13 +83,12 @@ impl Default for Store { /// /// Data should only be addressable by the module that owns it /// See -// TODO: Arena allocate these? pub(crate) struct StoreData { pub(crate) funcs: Vec>, - pub(crate) tables: Vec, - pub(crate) mems: Vec>>, + pub(crate) tables: Vec>>, + pub(crate) memories: Vec>>, pub(crate) globals: Vec>>, - pub(crate) elems: Vec, + pub(crate) elements: Vec, pub(crate) datas: Vec, } @@ -102,46 +102,55 @@ impl Store { self.module_instance_count as ModuleInstanceAddr } - /// Initialize the store with global state from the given module pub(crate) fn add_instance(&mut self, instance: ModuleInstance) -> Result<()> { + assert!(instance.id() == self.module_instance_count as ModuleInstanceAddr); self.module_instances.push(instance); self.module_instance_count += 1; Ok(()) } /// Add functions to the store, returning their addresses in the store - pub(crate) fn add_funcs(&mut self, funcs: Vec, idx: ModuleInstanceAddr) -> Vec { + pub(crate) fn init_funcs( + &mut self, + funcs: Vec<(u32, WasmFunction)>, + idx: ModuleInstanceAddr, + ) -> Result> { let func_count = self.data.funcs.len(); let mut func_addrs = Vec::with_capacity(func_count); - for (i, func) in funcs.into_iter().enumerate() { - self.data.funcs.push(Rc::new(FunctionInstance { func, owner: idx })); + + for (i, (type_idx, func)) in funcs.into_iter().enumerate() { + self.data.funcs.push(Rc::new(FunctionInstance { + func: Function::Wasm(func), + _type_idx: type_idx, + owner: idx, + })); func_addrs.push((i + func_count) as FuncAddr); } - func_addrs + + Ok(func_addrs) } /// Add tables to the store, returning their addresses in the store - pub(crate) fn add_tables(&mut self, tables: Vec, idx: ModuleInstanceAddr) -> Vec { + pub(crate) fn init_tables(&mut self, tables: Vec, idx: ModuleInstanceAddr) -> Result> { let table_count = self.data.tables.len(); let mut table_addrs = Vec::with_capacity(table_count); for (i, table) in tables.into_iter().enumerate() { - self.data.tables.push(TableInstance::new(table, idx)); + self.data.tables.push(Rc::new(RefCell::new(TableInstance::new(table, idx)))); table_addrs.push((i + table_count) as TableAddr); } - table_addrs + Ok(table_addrs) } /// Add memories to the store, returning their addresses in the store - pub(crate) fn add_mems(&mut self, mems: Vec, idx: ModuleInstanceAddr) -> Result> { - let mem_count = self.data.mems.len(); + pub(crate) fn init_memories(&mut self, memories: Vec, idx: ModuleInstanceAddr) -> Result> { + let mem_count = self.data.memories.len(); let mut mem_addrs = Vec::with_capacity(mem_count); - for (i, mem) in mems.into_iter().enumerate() { + for (i, mem) in memories.into_iter().enumerate() { if let MemoryArch::I64 = mem.arch { return Err(Error::UnsupportedFeature("64-bit memories".to_string())); } - self.data - .mems - .push(Rc::new(RefCell::new(MemoryInstance::new(mem, idx)))); + log::info!("adding memory: {:?}", mem); + self.data.memories.push(Rc::new(RefCell::new(MemoryInstance::new(mem, idx)))); mem_addrs.push((i + mem_count) as MemAddr); } @@ -149,51 +158,21 @@ impl Store { } /// Add globals to the store, returning their addresses in the store - pub(crate) fn add_globals( + pub(crate) fn init_globals( &mut self, - globals: Vec, - wasm_imports: &[Import], - user_imports: &LinkedImports, + mut imported_globals: Vec, + new_globals: Vec, + func_addrs: &[FuncAddr], idx: ModuleInstanceAddr, ) -> Result> { - // TODO: initialize imported globals - #![allow(clippy::unnecessary_filter_map)] // this is cleaner - let imported_globals = wasm_imports - .iter() - .filter_map(|import| match &import.kind { - tinywasm_types::ImportKind::Global(_) => Some(import), - _ => None, - }) - .map(|import| { - let Some(global) = user_imports.get(&import.module, &import.name) else { - return Err(Error::Other(format!( - "global import not found for {}::{}", - import.module, import.name - ))); - }; - match global { - Extern::Global(global) => Ok(global), - - #[allow(unreachable_patterns)] // this is non-exhaustive - _ => Err(Error::Other(format!( - "expected global import for {}::{}", - import.module, import.name - ))), - } - }) - .collect::>>()?; - let global_count = self.data.globals.len(); - let mut global_addrs = Vec::with_capacity(global_count); - - log::debug!("globals: {:?}", globals); - let globals = globals.into_iter(); - let iterator = imported_globals.into_iter().chain(globals.as_ref()); + imported_globals.reserve_exact(new_globals.len()); + let mut global_addrs = imported_globals; - for (i, global) in iterator.enumerate() { + for (i, global) in new_globals.iter().enumerate() { self.data.globals.push(Rc::new(RefCell::new(GlobalInstance::new( global.ty, - self.eval_const(&global.init)?, + self.eval_const(&global.init, &global_addrs, func_addrs)?, idx, )))); global_addrs.push((i + global_count) as Addr); @@ -202,101 +181,246 @@ impl Store { Ok(global_addrs) } - pub(crate) fn eval_const(&self, const_instr: &tinywasm_types::ConstInstruction) -> Result { - use tinywasm_types::ConstInstruction::*; - let val = match const_instr { - F32Const(f) => RawWasmValue::from(*f), - F64Const(f) => RawWasmValue::from(*f), - I32Const(i) => RawWasmValue::from(*i), - I64Const(i) => RawWasmValue::from(*i), - GlobalGet(addr) => { - let addr = *addr as usize; - let global = self.data.globals[addr].clone(); - let val = global.borrow().value; - val + fn elem_addr(&self, item: &ElementItem, globals: &[Addr], funcs: &[FuncAddr]) -> Result> { + let res = match item { + ElementItem::Func(addr) | ElementItem::Expr(ConstInstruction::RefFunc(addr)) => { + Some(funcs.get(*addr as usize).copied().ok_or_else(|| { + Error::Other(format!("function {} not found. This should have been caught by the validator", addr)) + })?) } - RefNull(_) => RawWasmValue::default(), - RefFunc(idx) => RawWasmValue::from(*idx as i64), + ElementItem::Expr(ConstInstruction::RefNull(_ty)) => None, + ElementItem::Expr(ConstInstruction::GlobalGet(addr)) => { + let addr = globals.get(*addr as usize).copied().ok_or_else(|| { + Error::Other(format!("global {} not found. This should have been caught by the validator", addr)) + })?; + + let global = self.data.globals[addr as usize].clone(); + let val = i64::from(global.borrow().value); + log::error!("global: {}", val); + if val < 0 { + // the global is actually a null reference + None + } else { + Some(val as u32) + } + } + _ => return Err(Error::UnsupportedFeature(format!("const expression other than ref: {:?}", item))), }; - Ok(val) + + Ok(res) } /// Add elements to the store, returning their addresses in the store /// Should be called after the tables have been added - pub(crate) fn add_elems(&mut self, elems: Vec, idx: ModuleInstanceAddr) -> Result> { - let elem_count = self.data.elems.len(); + pub(crate) fn init_elements( + &mut self, + table_addrs: &[TableAddr], + func_addrs: &[FuncAddr], + global_addrs: &[Addr], + elements: Vec, + idx: ModuleInstanceAddr, + ) -> Result<(Box<[Addr]>, Option)> { + let elem_count = self.data.elements.len(); let mut elem_addrs = Vec::with_capacity(elem_count); - for (i, elem) in elems.into_iter().enumerate() { - match elem.kind { - // doesn't need to be initialized, can be initialized lazily using the `table.init` instruction - ElementKind::Passive => {} + for (i, element) in elements.into_iter().enumerate() { + let init = element + .items + .iter() + .map(|item| Ok(TableElement::from(self.elem_addr(item, global_addrs, func_addrs)?))) + .collect::>>()?; - // this one is active, so we need to initialize it (essentially a `table.init` instruction) - ElementKind::Active { .. } => { - // a. Let n be the length of the vector elem[i].init - // b. Execute the instruction sequence einstrs - // c. Execute the instruction i32.const 0 - // d. Execute the instruction i32.const n - // e. Execute the instruction table.init tableidx i - // f. Execute the instruction elm.drop i - } + log::error!("element kind: {:?}", element.kind); + + let items = match element.kind { + // doesn't need to be initialized, can be initialized lazily using the `table.init` instruction + ElementKind::Passive => Some(init), // this one is not available to the runtime but needs to be initialized to declare references ElementKind::Declared => { // a. Execute the instruction elm.drop i + None } - } - self.data.elems.push(ElemInstance::new(elem.kind, idx)); + // this one is active, so we need to initialize it (essentially a `table.init` instruction) + ElementKind::Active { offset, table } => { + let offset = self.eval_i32_const(&offset)?; + let table_addr = table_addrs + .get(table as usize) + .copied() + .ok_or_else(|| Error::Other(format!("table {} not found for element {}", table, i)))?; + + if let Some(table) = self.data.tables.get_mut(table_addr as usize) { + // In wasm 2.0, it's possible to call a function that hasn't been instantiated yet, + // when using a partially initialized active element segments. + // This isn't mentioned in the spec, but the "unofficial" testsuite has a test for it: + // https://github.com/WebAssembly/testsuite/blob/5a1a590603d81f40ef471abba70a90a9ae5f4627/linking.wast#L264-L276 + // I have NO IDEA why this is allowed, but it is. + if let Err(Error::Trap(trap)) = table.borrow_mut().init_raw(offset, &init) { + return Ok((elem_addrs.into_boxed_slice(), Some(trap))); + } + } else { + return Err(Error::Other(format!("table {} not found for element {}", table, i))); + } + + // f. Execute the instruction elm.drop i + None + } + }; + + self.data.elements.push(ElementInstance::new(element.kind, idx, items)); elem_addrs.push((i + elem_count) as Addr); } - Ok(elem_addrs) + // this should be optimized out by the compiler + Ok((elem_addrs.into_boxed_slice(), None)) } /// Add data to the store, returning their addresses in the store - pub(crate) fn add_datas(&mut self, datas: Vec, idx: ModuleInstanceAddr) -> Vec { + pub(crate) fn init_datas( + &mut self, + mem_addrs: &[MemAddr], + datas: Vec, + idx: ModuleInstanceAddr, + ) -> Result<(Box<[Addr]>, Option)> { let data_count = self.data.datas.len(); let mut data_addrs = Vec::with_capacity(data_count); for (i, data) in datas.into_iter().enumerate() { - self.data.datas.push(DataInstance::new(data.data.to_vec(), idx)); - data_addrs.push((i + data_count) as Addr); - use tinywasm_types::DataKind::*; match data.kind { - Active { .. } => { + Active { mem: mem_addr, offset } => { // a. Assert: memidx == 0 - // b. Let n be the length of the vector - // c. Execute the instruction sequence - // d. Execute the instruction - // e. Execute the instruction - // f. Execute the instruction - // g. Execute the instruction + if mem_addr != 0 { + return Err(Error::UnsupportedFeature("data segments for non-zero memories".to_string())); + } + + let mem_addr = mem_addrs + .get(mem_addr as usize) + .copied() + .ok_or_else(|| Error::Other(format!("memory {} not found for data segment {}", mem_addr, i)))?; + + let offset = self.eval_i32_const(&offset)?; + + let mem = + self.data.memories.get_mut(mem_addr as usize).ok_or_else(|| { + Error::Other(format!("memory {} not found for data segment {}", mem_addr, i)) + })?; + + // See comment for active element sections in the function above why we need to do this here + if let Err(Error::Trap(trap)) = mem.borrow_mut().store(offset as usize, 0, &data.data) { + return Ok((data_addrs.into_boxed_slice(), Some(trap))); + } + + // drop the data + continue; } Passive => {} } + + self.data.datas.push(DataInstance::new(data.data.to_vec(), idx)); + data_addrs.push((i + data_count) as Addr); + } + + // this should be optimized out by the compiler + Ok((data_addrs.into_boxed_slice(), None)) + } + + pub(crate) fn add_global(&mut self, ty: GlobalType, value: RawWasmValue, idx: ModuleInstanceAddr) -> Result { + self.data.globals.push(Rc::new(RefCell::new(GlobalInstance::new(ty, value, idx)))); + Ok(self.data.globals.len() as Addr - 1) + } + + pub(crate) fn add_table(&mut self, table: TableType, idx: ModuleInstanceAddr) -> Result { + self.data.tables.push(Rc::new(RefCell::new(TableInstance::new(table, idx)))); + Ok(self.data.tables.len() as TableAddr - 1) + } + + pub(crate) fn add_mem(&mut self, mem: MemoryType, idx: ModuleInstanceAddr) -> Result { + if let MemoryArch::I64 = mem.arch { + return Err(Error::UnsupportedFeature("64-bit memories".to_string())); } - data_addrs + self.data.memories.push(Rc::new(RefCell::new(MemoryInstance::new(mem, idx)))); + Ok(self.data.memories.len() as MemAddr - 1) + } + + pub(crate) fn add_func(&mut self, func: Function, type_idx: TypeAddr, idx: ModuleInstanceAddr) -> Result { + self.data.funcs.push(Rc::new(FunctionInstance { func, _type_idx: type_idx, owner: idx })); + Ok(self.data.funcs.len() as FuncAddr - 1) + } + + /// Evaluate a constant expression, only supporting i32 globals and i32.const + pub(crate) fn eval_i32_const(&self, const_instr: &tinywasm_types::ConstInstruction) -> Result { + use tinywasm_types::ConstInstruction::*; + let val = match const_instr { + I32Const(i) => *i, + GlobalGet(addr) => { + let addr = *addr as usize; + let global = self.data.globals[addr].clone(); + let val = global.borrow().value; + i32::from(val) + } + _ => return Err(Error::Other("expected i32".to_string())), + }; + Ok(val) + } + + /// Evaluate a constant expression + pub(crate) fn eval_const( + &self, + const_instr: &tinywasm_types::ConstInstruction, + module_global_addrs: &[Addr], + module_func_addrs: &[FuncAddr], + ) -> Result { + use tinywasm_types::ConstInstruction::*; + let val = match const_instr { + F32Const(f) => RawWasmValue::from(*f), + F64Const(f) => RawWasmValue::from(*f), + I32Const(i) => RawWasmValue::from(*i), + I64Const(i) => RawWasmValue::from(*i), + GlobalGet(addr) => { + let addr = module_global_addrs.get(*addr as usize).copied().ok_or_else(|| { + Error::Other(format!("global {} not found. This should have been caught by the validator", addr)) + })?; + + let global = + self.data.globals.get(addr as usize).expect("global not found. This should be unreachable"); + + global.borrow().value + } + RefNull(t) => RawWasmValue::from(t.default_value()), + RefFunc(idx) => RawWasmValue::from(module_func_addrs.get(*idx as usize).copied().ok_or_else(|| { + Error::Other(format!("function {} not found. This should have been caught by the validator", idx)) + })?), + }; + Ok(val) } /// Get the function at the actual index in the store pub(crate) fn get_func(&self, addr: usize) -> Result<&Rc> { - self.data - .funcs - .get(addr) - .ok_or_else(|| Error::Other(format!("function {} not found", addr))) + self.data.funcs.get(addr).ok_or_else(|| Error::Other(format!("function {} not found", addr))) } /// Get the memory at the actual index in the store pub(crate) fn get_mem(&self, addr: usize) -> Result<&Rc>> { - self.data - .mems - .get(addr) - .ok_or_else(|| Error::Other(format!("memory {} not found", addr))) + self.data.memories.get(addr).ok_or_else(|| Error::Other(format!("memory {} not found", addr))) + } + + /// Get the table at the actual index in the store + pub(crate) fn get_table(&self, addr: usize) -> Result<&Rc>> { + self.data.tables.get(addr).ok_or_else(|| Error::Other(format!("table {} not found", addr))) + } + + /// Get the element at the actual index in the store + pub(crate) fn get_elem(&self, addr: usize) -> Result<&ElementInstance> { + self.data.elements.get(addr).ok_or_else(|| Error::Other(format!("element {} not found", addr))) + } + + /// Get the global at the actual index in the store + pub(crate) fn get_global(&self, addr: usize) -> Result<&Rc>> { + self.data.globals.get(addr).ok_or_else(|| Error::Other(format!("global {} not found", addr))) } /// Get the global at the actual index in the store - pub(crate) fn get_global_val(&self, addr: usize) -> Result { + pub fn get_global_val(&self, addr: usize) -> Result { self.data .globals .get(addr) @@ -317,52 +441,147 @@ impl Store { /// A WebAssembly Function Instance /// /// See -pub struct FunctionInstance { +pub(crate) struct FunctionInstance { pub(crate) func: Function, - pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances + pub(crate) _type_idx: TypeAddr, + pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances, none for host functions } +// TODO: check if this actually helps +#[inline(always)] +#[cold] +const fn cold() {} + impl FunctionInstance { - pub(crate) fn _module_instance_addr(&self) -> ModuleInstanceAddr { - self.owner + pub(crate) fn assert_wasm(&self) -> Result<&WasmFunction> { + match &self.func { + Function::Wasm(w) => Ok(w), + Function::Host(_) => { + cold(); + Err(Error::Other("expected wasm function".to_string())) + } + } } +} - pub(crate) fn locals(&self) -> &[ValType] { - &self.func.locals +#[derive(Debug, Clone, Copy)] +pub(crate) enum TableElement { + Uninitialized, + Initialized(Addr), +} + +impl From> for TableElement { + fn from(addr: Option) -> Self { + match addr { + None => TableElement::Uninitialized, + Some(addr) => TableElement::Initialized(addr), + } } +} - pub(crate) fn instructions(&self) -> &[Instruction] { - &self.func.instructions +impl TableElement { + pub(crate) fn addr(&self) -> Option { + match self { + TableElement::Uninitialized => None, + TableElement::Initialized(addr) => Some(*addr), + } } - pub(crate) fn ty_addr(&self) -> TypeAddr { - self.func.ty + pub(crate) fn map Addr>(self, f: F) -> Self { + match self { + TableElement::Uninitialized => TableElement::Uninitialized, + TableElement::Initialized(addr) => TableElement::Initialized(f(addr)), + } } } +const MAX_TABLE_SIZE: u32 = 10000000; + /// A WebAssembly Table Instance /// /// See #[derive(Debug)] pub(crate) struct TableInstance { + pub(crate) elements: Vec, pub(crate) kind: TableType, - pub(crate) elements: Vec, - pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances + pub(crate) _owner: ModuleInstanceAddr, // index into store.module_instances } impl TableInstance { pub(crate) fn new(kind: TableType, owner: ModuleInstanceAddr) -> Self { - Self { - elements: vec![0; kind.size_initial as usize], - kind, - owner, + Self { elements: vec![TableElement::Uninitialized; kind.size_initial as usize], kind, _owner: owner } + } + + pub(crate) fn get_wasm_val(&self, addr: usize) -> Result { + let val = self.get(addr)?.addr(); + + Ok(match self.kind.element_type { + ValType::RefFunc => val.map(WasmValue::RefFunc).unwrap_or(WasmValue::RefNull(ValType::RefFunc)), + ValType::RefExtern => val.map(WasmValue::RefExtern).unwrap_or(WasmValue::RefNull(ValType::RefExtern)), + _ => unimplemented!("unsupported table type: {:?}", self.kind.element_type), + }) + } + + pub(crate) fn get(&self, addr: usize) -> Result<&TableElement> { + self.elements.get(addr).ok_or_else(|| Error::Trap(Trap::UndefinedElement { index: addr })) + } + + pub(crate) fn set(&mut self, table_idx: usize, value: Addr) -> Result<()> { + self.grow_to_fit(table_idx + 1).map(|_| self.elements[table_idx] = TableElement::Initialized(value)) + } + + pub(crate) fn grow_to_fit(&mut self, new_size: usize) -> Result<()> { + if new_size > self.elements.len() { + if new_size > self.kind.size_max.unwrap_or(MAX_TABLE_SIZE) as usize { + return Err(crate::Trap::TableOutOfBounds { offset: new_size, len: 1, max: self.elements.len() }.into()); + } + + self.elements.resize(new_size, TableElement::Uninitialized); } + Ok(()) + } + + pub(crate) fn size(&self) -> i32 { + self.elements.len() as i32 + } + + fn resolve_func_ref(&self, func_addrs: &[u32], addr: Addr) -> Addr { + if self.kind.element_type != ValType::RefFunc { + return addr; + } + + *func_addrs + .get(addr as usize) + .expect("error initializing table: function not found. This should have been caught by the validator") + } + + // Initialize the table with the given elements + pub(crate) fn init_raw(&mut self, offset: i32, init: &[TableElement]) -> Result<()> { + let offset = offset as usize; + let end = offset.checked_add(init.len()).ok_or_else(|| { + Error::Trap(crate::Trap::TableOutOfBounds { offset, len: init.len(), max: self.elements.len() }) + })?; + + if end > self.elements.len() || end < offset { + return Err(crate::Trap::TableOutOfBounds { offset, len: init.len(), max: self.elements.len() }.into()); + } + + self.elements[offset..end].copy_from_slice(init); + log::debug!("table: {:?}", self.elements); + Ok(()) + } + + // Initialize the table with the given elements (resolves function references) + pub(crate) fn init(&mut self, func_addrs: &[u32], offset: i32, init: &[TableElement]) -> Result<()> { + let init = init.iter().map(|item| item.map(|addr| self.resolve_func_ref(func_addrs, addr))).collect::>(); + + self.init_raw(offset, &init) } } pub(crate) const PAGE_SIZE: usize = 65536; pub(crate) const MAX_PAGES: usize = 65536; -pub(crate) const MAX_SIZE: usize = PAGE_SIZE * MAX_PAGES; +pub(crate) const MAX_SIZE: u64 = PAGE_SIZE as u64 * MAX_PAGES as u64; /// A WebAssembly Memory Instance /// @@ -372,29 +591,25 @@ pub(crate) struct MemoryInstance { pub(crate) kind: MemoryType, pub(crate) data: Vec, pub(crate) page_count: usize, - pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances + pub(crate) _owner: ModuleInstanceAddr, // index into store.module_instances } impl MemoryInstance { pub(crate) fn new(kind: MemoryType, owner: ModuleInstanceAddr) -> Self { - debug_assert!(kind.page_count_initial <= kind.page_count_max.unwrap_or(MAX_PAGES as u64)); + assert!(kind.page_count_initial <= kind.page_count_max.unwrap_or(MAX_PAGES as u64)); log::debug!("initializing memory with {} pages", kind.page_count_initial); Self { kind, data: vec![0; PAGE_SIZE * kind.page_count_initial as usize], page_count: kind.page_count_initial as usize, - owner, + _owner: owner, } } pub(crate) fn store(&mut self, addr: usize, _align: usize, data: &[u8]) -> Result<()> { let end = addr.checked_add(data.len()).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset: addr, - len: data.len(), - max: self.data.len(), - }) + Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len: data.len(), max: self.data.len() }) })?; if end > self.data.len() || end < addr { @@ -410,43 +625,46 @@ impl MemoryInstance { Ok(()) } + pub(crate) fn max_pages(&self) -> usize { + self.kind.page_count_max.unwrap_or(MAX_PAGES as u64) as usize + } + pub(crate) fn load(&self, addr: usize, _align: usize, len: usize) -> Result<&[u8]> { - let end = addr.checked_add(len).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset: addr, - len, - max: self.data.len(), - }) - })?; + let end = addr + .checked_add(len) + .ok_or_else(|| Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len, max: self.max_pages() }))?; if end > self.data.len() { - return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { - offset: addr, - len, - max: self.data.len(), - })); + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { offset: addr, len, max: self.data.len() })); } // WebAssembly doesn't require alignment for loads Ok(&self.data[addr..end]) } - pub(crate) fn size(&self) -> i32 { - log::debug!("memory pages: {}", self.page_count); - log::debug!("memory size: {}", self.page_count * PAGE_SIZE); - self.page_count as i32 + pub(crate) fn page_count(&self) -> usize { + self.page_count } - pub(crate) fn grow(&mut self, delta: i32) -> Result { - let current_pages = self.size(); - let new_pages = current_pages + delta; - if new_pages < 0 || new_pages > MAX_PAGES as i32 { - return Err(Error::Other(format!("memory size out of bounds: {}", new_pages))); + pub(crate) fn grow(&mut self, delta: i32) -> Option { + let current_pages = self.page_count(); + let new_pages = current_pages as i64 + delta as i64; + + if new_pages < 0 || new_pages > MAX_PAGES as i64 { + return None; + } + + if new_pages as usize > self.max_pages() { + log::info!("memory size out of bounds: {}", new_pages); + return None; } + let new_size = new_pages as usize * PAGE_SIZE; - if new_size > MAX_SIZE { - return Err(Error::Other(format!("memory size out of bounds: {}", new_size))); + if new_size as u64 > MAX_SIZE { + return None; } + + // Zero initialize the new pages self.data.resize(new_size, 0); self.page_count = new_pages as usize; @@ -454,7 +672,7 @@ impl MemoryInstance { log::debug!("memory grown by {} pages", delta); log::debug!("memory grown to {} pages", self.page_count); - Ok(current_pages) + Some(current_pages.try_into().expect("memory size out of bounds, this should have been caught earlier")) } } @@ -463,14 +681,14 @@ impl MemoryInstance { /// See #[derive(Debug)] pub(crate) struct GlobalInstance { - pub(crate) ty: GlobalType, pub(crate) value: RawWasmValue, - owner: ModuleInstanceAddr, // index into store.module_instances + pub(crate) ty: GlobalType, + pub(crate) _owner: ModuleInstanceAddr, // index into store.module_instances } impl GlobalInstance { pub(crate) fn new(ty: GlobalType, value: RawWasmValue, owner: ModuleInstanceAddr) -> Self { - Self { ty, value, owner } + Self { ty, value, _owner: owner } } } @@ -478,14 +696,15 @@ impl GlobalInstance { /// /// See #[derive(Debug)] -pub(crate) struct ElemInstance { - kind: ElementKind, - owner: ModuleInstanceAddr, // index into store.module_instances +pub(crate) struct ElementInstance { + pub(crate) kind: ElementKind, + pub(crate) items: Option>, // none is the element was dropped + _owner: ModuleInstanceAddr, // index into store.module_instances } -impl ElemInstance { - pub(crate) fn new(kind: ElementKind, owner: ModuleInstanceAddr) -> Self { - Self { kind, owner } +impl ElementInstance { + pub(crate) fn new(kind: ElementKind, owner: ModuleInstanceAddr, items: Option>) -> Self { + Self { kind, _owner: owner, items } } } @@ -494,12 +713,12 @@ impl ElemInstance { /// See #[derive(Debug)] pub(crate) struct DataInstance { - pub(crate) data: Vec, - owner: ModuleInstanceAddr, // index into store.module_instances + pub(crate) _data: Vec, + pub(crate) _owner: ModuleInstanceAddr, // index into store.module_instances } impl DataInstance { pub(crate) fn new(data: Vec, owner: ModuleInstanceAddr) -> Self { - Self { data, owner } + Self { _data: data, _owner: owner } } } diff --git a/crates/tinywasm/tests/generated/2.0.csv b/crates/tinywasm/tests/generated/2.0.csv new file mode 100644 index 0000000..0d233ee --- /dev/null +++ b/crates/tinywasm/tests/generated/2.0.csv @@ -0,0 +1,2 @@ +0.3.0,26722,1161,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":174,"failed":0},{"name":"bulk.wast","passed":8,"failed":109},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":170,"failed":0},{"name":"comments.wast","passed":8,"failed":0},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":619,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":61,"failed":0},{"name":"elem.wast","passed":98,"failed":1},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":96,"failed":0},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":8,"failed":0},{"name":"float_exprs.wast","passed":900,"failed":0},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":441,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":172,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":110,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":171,"failed":12},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":29,"failed":0},{"name":"left-to-right.wast","passed":96,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":97,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":79,"failed":0},{"name":"memory_copy.wast","passed":3928,"failed":522},{"name":"memory_fill.wast","passed":64,"failed":36},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":177,"failed":63},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":42,"failed":0},{"name":"memory_trap.wast","passed":182,"failed":0},{"name":"names.wast","passed":486,"failed":0},{"name":"nop.wast","passed":88,"failed":0},{"name":"ref_func.wast","passed":9,"failed":8},{"name":"ref_is_null.wast","passed":4,"failed":12},{"name":"ref_null.wast","passed":1,"failed":2},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":148,"failed":0},{"name":"skip-stack-guard-page.wast","passed":11,"failed":0},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":20,"failed":0},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1613,"failed":115},{"name":"table_fill.wast","passed":23,"failed":22},{"name":"table_get.wast","passed":10,"failed":6},{"name":"table_grow.wast","passed":21,"failed":29},{"name":"table_init.wast","passed":594,"failed":186},{"name":"table_set.wast","passed":19,"failed":7},{"name":"table_size.wast","passed":8,"failed":31},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unreached-valid.wast","passed":7,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.4.0-alpha.0,26841,1042,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":174,"failed":0},{"name":"bulk.wast","passed":8,"failed":109},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":170,"failed":0},{"name":"comments.wast","passed":8,"failed":0},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":619,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":61,"failed":0},{"name":"elem.wast","passed":99,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":96,"failed":0},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":8,"failed":0},{"name":"float_exprs.wast","passed":900,"failed":0},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":441,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":172,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":110,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":183,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":29,"failed":0},{"name":"left-to-right.wast","passed":96,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":97,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":79,"failed":0},{"name":"memory_copy.wast","passed":3928,"failed":522},{"name":"memory_fill.wast","passed":64,"failed":36},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":177,"failed":63},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":42,"failed":0},{"name":"memory_trap.wast","passed":182,"failed":0},{"name":"names.wast","passed":486,"failed":0},{"name":"nop.wast","passed":88,"failed":0},{"name":"ref_func.wast","passed":9,"failed":8},{"name":"ref_is_null.wast","passed":4,"failed":12},{"name":"ref_null.wast","passed":1,"failed":2},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":148,"failed":0},{"name":"skip-stack-guard-page.wast","passed":11,"failed":0},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":20,"failed":0},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1613,"failed":115},{"name":"table_fill.wast","passed":23,"failed":22},{"name":"table_get.wast","passed":10,"failed":6},{"name":"table_grow.wast","passed":21,"failed":29},{"name":"table_init.wast","passed":700,"failed":80},{"name":"table_set.wast","passed":19,"failed":7},{"name":"table_size.wast","passed":8,"failed":31},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unreached-valid.wast","passed":7,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] diff --git a/crates/tinywasm/tests/generated/mvp.csv b/crates/tinywasm/tests/generated/mvp.csv index a238586..23c080d 100644 --- a/crates/tinywasm/tests/generated/mvp.csv +++ b/crates/tinywasm/tests/generated/mvp.csv @@ -1,5 +1,5 @@ -0.0.3,9258,7567,[{"name":"address.wast","passed":0,"failed":54},{"name":"align.wast","passed":0,"failed":109},{"name":"binary-leb128.wast","passed":66,"failed":25},{"name":"binary.wast","passed":104,"failed":8},{"name":"block.wast","passed":0,"failed":171},{"name":"br.wast","passed":0,"failed":21},{"name":"br_if.wast","passed":0,"failed":30},{"name":"br_table.wast","passed":0,"failed":25},{"name":"call.wast","passed":0,"failed":22},{"name":"call_indirect.wast","passed":0,"failed":56},{"name":"comments.wast","passed":4,"failed":4},{"name":"const.wast","passed":702,"failed":76},{"name":"conversions.wast","passed":0,"failed":93},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":0,"failed":61},{"name":"elem.wast","passed":0,"failed":76},{"name":"endianness.wast","passed":0,"failed":1},{"name":"exports.wast","passed":21,"failed":73},{"name":"f32.wast","passed":1005,"failed":1509},{"name":"f32_bitwise.wast","passed":1,"failed":363},{"name":"f32_cmp.wast","passed":2401,"failed":6},{"name":"f64.wast","passed":1005,"failed":1509},{"name":"f64_bitwise.wast","passed":1,"failed":363},{"name":"f64_cmp.wast","passed":2401,"failed":6},{"name":"fac.wast","passed":0,"failed":2},{"name":"float_exprs.wast","passed":269,"failed":591},{"name":"float_literals.wast","passed":34,"failed":129},{"name":"float_memory.wast","passed":0,"failed":6},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":4,"failed":75},{"name":"func_ptrs.wast","passed":0,"failed":16},{"name":"global.wast","passed":4,"failed":49},{"name":"i32.wast","passed":0,"failed":96},{"name":"i64.wast","passed":0,"failed":42},{"name":"if.wast","passed":0,"failed":118},{"name":"imports.wast","passed":1,"failed":156},{"name":"inline-module.wast","passed":0,"failed":1},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":5,"failed":46},{"name":"labels.wast","passed":1,"failed":28},{"name":"left-to-right.wast","passed":0,"failed":1},{"name":"linking.wast","passed":1,"failed":66},{"name":"load.wast","passed":0,"failed":60},{"name":"local_get.wast","passed":2,"failed":34},{"name":"local_set.wast","passed":5,"failed":48},{"name":"local_tee.wast","passed":0,"failed":42},{"name":"loop.wast","passed":0,"failed":43},{"name":"memory.wast","passed":0,"failed":34},{"name":"memory_grow.wast","passed":0,"failed":19},{"name":"memory_redundancy.wast","passed":0,"failed":1},{"name":"memory_size.wast","passed":0,"failed":6},{"name":"memory_trap.wast","passed":0,"failed":172},{"name":"names.wast","passed":484,"failed":1},{"name":"nop.wast","passed":0,"failed":5},{"name":"return.wast","passed":0,"failed":21},{"name":"select.wast","passed":0,"failed":32},{"name":"skip-stack-guard-page.wast","passed":0,"failed":11},{"name":"stack.wast","passed":0,"failed":2},{"name":"start.wast","passed":0,"failed":10},{"name":"store.wast","passed":0,"failed":59},{"name":"switch.wast","passed":1,"failed":27},{"name":"token.wast","passed":16,"failed":42},{"name":"traps.wast","passed":3,"failed":33},{"name":"type.wast","passed":1,"failed":2},{"name":"unreachable.wast","passed":0,"failed":59},{"name":"unreached-invalid.wast","passed":0,"failed":118},{"name":"unwind.wast","passed":1,"failed":49},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":0,"failed":176}] 0.0.4,9258,10909,[{"name":"address.wast","passed":0,"failed":54},{"name":"align.wast","passed":0,"failed":109},{"name":"binary-leb128.wast","passed":66,"failed":25},{"name":"binary.wast","passed":104,"failed":8},{"name":"block.wast","passed":0,"failed":171},{"name":"br.wast","passed":0,"failed":21},{"name":"br_if.wast","passed":0,"failed":30},{"name":"br_table.wast","passed":0,"failed":25},{"name":"call.wast","passed":0,"failed":22},{"name":"call_indirect.wast","passed":0,"failed":56},{"name":"comments.wast","passed":4,"failed":4},{"name":"const.wast","passed":702,"failed":76},{"name":"conversions.wast","passed":0,"failed":93},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":0,"failed":61},{"name":"elem.wast","passed":0,"failed":76},{"name":"endianness.wast","passed":0,"failed":1},{"name":"exports.wast","passed":21,"failed":73},{"name":"f32.wast","passed":1005,"failed":1509},{"name":"f32_bitwise.wast","passed":1,"failed":363},{"name":"f32_cmp.wast","passed":2401,"failed":6},{"name":"f64.wast","passed":1005,"failed":1509},{"name":"f64_bitwise.wast","passed":1,"failed":363},{"name":"f64_cmp.wast","passed":2401,"failed":6},{"name":"fac.wast","passed":0,"failed":2},{"name":"float_exprs.wast","passed":269,"failed":591},{"name":"float_literals.wast","passed":34,"failed":129},{"name":"float_memory.wast","passed":0,"failed":6},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":4,"failed":75},{"name":"func_ptrs.wast","passed":0,"failed":16},{"name":"global.wast","passed":4,"failed":49},{"name":"i32.wast","passed":0,"failed":96},{"name":"i64.wast","passed":0,"failed":42},{"name":"if.wast","passed":0,"failed":118},{"name":"imports.wast","passed":1,"failed":156},{"name":"inline-module.wast","passed":0,"failed":1},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":5,"failed":46},{"name":"labels.wast","passed":1,"failed":28},{"name":"left-to-right.wast","passed":0,"failed":1},{"name":"linking.wast","passed":1,"failed":66},{"name":"load.wast","passed":0,"failed":60},{"name":"local_get.wast","passed":2,"failed":34},{"name":"local_set.wast","passed":5,"failed":48},{"name":"local_tee.wast","passed":0,"failed":42},{"name":"loop.wast","passed":0,"failed":43},{"name":"memory.wast","passed":0,"failed":34},{"name":"memory_grow.wast","passed":0,"failed":19},{"name":"memory_redundancy.wast","passed":0,"failed":1},{"name":"memory_size.wast","passed":0,"failed":6},{"name":"memory_trap.wast","passed":0,"failed":172},{"name":"names.wast","passed":484,"failed":1},{"name":"nop.wast","passed":0,"failed":5},{"name":"return.wast","passed":0,"failed":21},{"name":"select.wast","passed":0,"failed":32},{"name":"skip-stack-guard-page.wast","passed":0,"failed":11},{"name":"stack.wast","passed":0,"failed":2},{"name":"start.wast","passed":0,"failed":10},{"name":"store.wast","passed":0,"failed":59},{"name":"switch.wast","passed":1,"failed":27},{"name":"token.wast","passed":16,"failed":42},{"name":"traps.wast","passed":3,"failed":33},{"name":"type.wast","passed":1,"failed":2},{"name":"unreachable.wast","passed":0,"failed":59},{"name":"unreached-invalid.wast","passed":0,"failed":118},{"name":"unwind.wast","passed":1,"failed":49},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":0,"failed":176}] 0.0.5,11135,9093,[{"name":"address.wast","passed":1,"failed":259},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":78,"failed":13},{"name":"binary.wast","passed":107,"failed":5},{"name":"block.wast","passed":170,"failed":53},{"name":"br.wast","passed":20,"failed":77},{"name":"br_if.wast","passed":29,"failed":89},{"name":"br_table.wast","passed":24,"failed":150},{"name":"call.wast","passed":18,"failed":73},{"name":"call_indirect.wast","passed":34,"failed":136},{"name":"comments.wast","passed":5,"failed":3},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":25,"failed":594},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":22,"failed":39},{"name":"elem.wast","passed":27,"failed":72},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":90,"failed":6},{"name":"f32.wast","passed":1018,"failed":1496},{"name":"f32_bitwise.wast","passed":4,"failed":360},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":1018,"failed":1496},{"name":"f64_bitwise.wast","passed":4,"failed":360},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":1,"failed":7},{"name":"float_exprs.wast","passed":275,"failed":625},{"name":"float_literals.wast","passed":112,"failed":51},{"name":"float_memory.wast","passed":0,"failed":90},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":81,"failed":91},{"name":"func_ptrs.wast","passed":7,"failed":29},{"name":"global.wast","passed":50,"failed":60},{"name":"i32.wast","passed":85,"failed":375},{"name":"i64.wast","passed":31,"failed":385},{"name":"if.wast","passed":116,"failed":125},{"name":"imports.wast","passed":23,"failed":160},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":25,"failed":26},{"name":"labels.wast","passed":13,"failed":16},{"name":"left-to-right.wast","passed":0,"failed":96},{"name":"linking.wast","passed":5,"failed":127},{"name":"load.wast","passed":59,"failed":38},{"name":"local_get.wast","passed":18,"failed":18},{"name":"local_set.wast","passed":38,"failed":15},{"name":"local_tee.wast","passed":41,"failed":56},{"name":"loop.wast","passed":42,"failed":78},{"name":"memory.wast","passed":30,"failed":49},{"name":"memory_grow.wast","passed":11,"failed":85},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":1,"failed":181},{"name":"names.wast","passed":484,"failed":2},{"name":"nop.wast","passed":4,"failed":84},{"name":"return.wast","passed":20,"failed":64},{"name":"select.wast","passed":28,"failed":120},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":4,"failed":16},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":39,"failed":19},{"name":"traps.wast","passed":4,"failed":32},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":0,"failed":64},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":9,"failed":41},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] 0.1.0,17630,2598,[{"name":"address.wast","passed":5,"failed":255},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":110,"failed":2},{"name":"block.wast","passed":193,"failed":30},{"name":"br.wast","passed":84,"failed":13},{"name":"br_if.wast","passed":90,"failed":28},{"name":"br_table.wast","passed":25,"failed":149},{"name":"call.wast","passed":29,"failed":62},{"name":"call_indirect.wast","passed":36,"failed":134},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":371,"failed":248},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":50,"failed":49},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":2,"failed":6},{"name":"float_exprs.wast","passed":761,"failed":139},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":6,"failed":84},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":124,"failed":48},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":51,"failed":59},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":120,"failed":121},{"name":"imports.wast","passed":74,"failed":109},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":14,"failed":15},{"name":"left-to-right.wast","passed":1,"failed":95},{"name":"linking.wast","passed":21,"failed":111},{"name":"load.wast","passed":60,"failed":37},{"name":"local_get.wast","passed":32,"failed":4},{"name":"local_set.wast","passed":50,"failed":3},{"name":"local_tee.wast","passed":68,"failed":29},{"name":"loop.wast","passed":93,"failed":27},{"name":"memory.wast","passed":34,"failed":45},{"name":"memory_grow.wast","passed":12,"failed":84},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":2,"failed":180},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":46,"failed":42},{"name":"return.wast","passed":73,"failed":11},{"name":"select.wast","passed":86,"failed":62},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":9,"failed":11},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":22,"failed":14},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":50,"failed":14},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":35,"failed":15},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] -0.2.0-alpha.0,19344,884,[{"name":"address.wast","passed":181,"failed":79},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":220,"failed":3},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":171,"failed":3},{"name":"call.wast","passed":73,"failed":18},{"name":"call_indirect.wast","passed":50,"failed":120},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":439,"failed":180},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":56,"failed":43},{"name":"endianness.wast","passed":29,"failed":40},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":6,"failed":2},{"name":"float_exprs.wast","passed":890,"failed":10},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":78,"failed":12},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":168,"failed":4},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":103,"failed":7},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":231,"failed":10},{"name":"imports.wast","passed":80,"failed":103},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":26,"failed":3},{"name":"left-to-right.wast","passed":92,"failed":4},{"name":"linking.wast","passed":29,"failed":103},{"name":"load.wast","passed":93,"failed":4},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":93,"failed":4},{"name":"loop.wast","passed":116,"failed":4},{"name":"memory.wast","passed":78,"failed":1},{"name":"memory_grow.wast","passed":91,"failed":5},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":35,"failed":7},{"name":"memory_trap.wast","passed":180,"failed":2},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":78,"failed":10},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":114,"failed":34},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":11,"failed":9},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.2.0,19344,884,[{"name":"address.wast","passed":181,"failed":79},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":220,"failed":3},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":171,"failed":3},{"name":"call.wast","passed":73,"failed":18},{"name":"call_indirect.wast","passed":50,"failed":120},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":439,"failed":180},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":56,"failed":43},{"name":"endianness.wast","passed":29,"failed":40},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":6,"failed":2},{"name":"float_exprs.wast","passed":890,"failed":10},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":78,"failed":12},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":168,"failed":4},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":103,"failed":7},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":231,"failed":10},{"name":"imports.wast","passed":80,"failed":103},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":26,"failed":3},{"name":"left-to-right.wast","passed":92,"failed":4},{"name":"linking.wast","passed":29,"failed":103},{"name":"load.wast","passed":93,"failed":4},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":93,"failed":4},{"name":"loop.wast","passed":116,"failed":4},{"name":"memory.wast","passed":78,"failed":1},{"name":"memory_grow.wast","passed":91,"failed":5},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":35,"failed":7},{"name":"memory_trap.wast","passed":180,"failed":2},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":78,"failed":10},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":114,"failed":34},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":11,"failed":9},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.3.0,20254,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":174,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":170,"failed":0},{"name":"comments.wast","passed":8,"failed":0},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":619,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":61,"failed":0},{"name":"elem.wast","passed":99,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":96,"failed":0},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":8,"failed":0},{"name":"float_exprs.wast","passed":900,"failed":0},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":441,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":172,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":110,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":183,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":29,"failed":0},{"name":"left-to-right.wast","passed":96,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":97,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":79,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":42,"failed":0},{"name":"memory_trap.wast","passed":182,"failed":0},{"name":"names.wast","passed":486,"failed":0},{"name":"nop.wast","passed":88,"failed":0},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":148,"failed":0},{"name":"skip-stack-guard-page.wast","passed":11,"failed":0},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":20,"failed":0},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unreached-valid.wast","passed":7,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] diff --git a/crates/tinywasm/tests/generated/progress-mvp.svg b/crates/tinywasm/tests/generated/progress-mvp.svg index e1fd709..c1200f5 100644 --- a/crates/tinywasm/tests/generated/progress-mvp.svg +++ b/crates/tinywasm/tests/generated/progress-mvp.svg @@ -12,7 +12,7 @@ TinyWasm Version - + @@ -27,38 +27,38 @@ TinyWasm Version 10000 - + 15000 - + 20000 -v0.0.3 (9258) +v0.0.4 (9258) -v0.0.4 (9258) +v0.0.5 (11135) -v0.0.5 (11135) +v0.1.0 (17630) -v0.1.0 (17630) +v0.2.0 (19344) -v0.2.0-alpha.0 (19344) +v0.3.0 (20254) - - - - + + + + diff --git a/crates/tinywasm/tests/test-mvp.rs b/crates/tinywasm/tests/test-mvp.rs index 5107551..445b7fa 100644 --- a/crates/tinywasm/tests/test-mvp.rs +++ b/crates/tinywasm/tests/test-mvp.rs @@ -17,16 +17,11 @@ fn test_mvp() -> Result<()> { TestSuite::set_log_level(log::LevelFilter::Off); test_suite.run_spec_group(wasm_testsuite::MVP_TESTS)?; - test_suite.save_csv("./tests/generated/mvp.csv", env!("CARGO_PKG_VERSION"))?; if test_suite.failed() { println!(); - Err(eyre!(format!( - "{}:\n{:#?}", - "failed one or more tests".red().bold(), - test_suite, - ))) + Err(eyre!(format!("{}:\n{:#?}", "failed one or more tests".red().bold(), test_suite,))) } else { println!("\n\npassed all tests:\n{:#?}", test_suite); Ok(()) diff --git a/crates/tinywasm/tests/test-two.rs b/crates/tinywasm/tests/test-two.rs new file mode 100644 index 0000000..e710d1a --- /dev/null +++ b/crates/tinywasm/tests/test-two.rs @@ -0,0 +1,29 @@ +mod testsuite; +use eyre::{eyre, Result}; +use owo_colors::OwoColorize; +use testsuite::TestSuite; + +fn main() -> Result<()> { + let args = std::env::args().collect::>(); + if args.len() < 2 || args[1] != "--enable" { + return Ok(()); + } + + test_2() +} + +fn test_2() -> Result<()> { + let mut test_suite = TestSuite::new(); + + TestSuite::set_log_level(log::LevelFilter::Off); + test_suite.run_spec_group(wasm_testsuite::V2_DRAFT_1_TESTS)?; + test_suite.save_csv("./tests/generated/2.0.csv", env!("CARGO_PKG_VERSION"))?; + + if test_suite.failed() { + println!(); + Err(eyre!(format!("{}:\n{:#?}", "failed one or more tests".red().bold(), test_suite,))) + } else { + println!("\n\npassed all tests:\n{:#?}", test_suite); + Ok(()) + } +} diff --git a/crates/tinywasm/tests/test-wast.rs b/crates/tinywasm/tests/test-wast.rs index 42f7091..a50a612 100644 --- a/crates/tinywasm/tests/test-wast.rs +++ b/crates/tinywasm/tests/test-wast.rs @@ -20,11 +20,7 @@ fn main() -> Result<()> { let cwd = std::env::current_dir()?; // if current dir is crates/tinywasm, then we want to go up 2 levels - let mut wast_file = if cwd.ends_with("crates/tinywasm") { - PathBuf::from("../../") - } else { - PathBuf::from("./") - }; + let mut wast_file = if cwd.ends_with("crates/tinywasm") { PathBuf::from("../../") } else { PathBuf::from("./") }; wast_file.push(&args[2]); let wast_file = cwd.join(wast_file); @@ -48,11 +44,7 @@ fn test_wast(wast_file: &str) -> Result<()> { println!(); test_suite.print_errors(); println!(); - Err(eyre!(format!( - "{}:\n{:#?}", - "failed one or more tests".red().bold(), - test_suite, - ))) + Err(eyre!(format!("{}:\n{:#?}", "failed one or more tests".red().bold(), test_suite,))) } else { println!("\n\npassed all tests:\n{:#?}", test_suite); Ok(()) diff --git a/crates/tinywasm/tests/testsuite/indexmap.rs b/crates/tinywasm/tests/testsuite/indexmap.rs index d4e3869..1642ce2 100644 --- a/crates/tinywasm/tests/testsuite/indexmap.rs +++ b/crates/tinywasm/tests/testsuite/indexmap.rs @@ -8,10 +8,7 @@ where K: std::cmp::Eq + std::hash::Hash + Clone, { pub fn new() -> Self { - Self { - map: std::collections::HashMap::new(), - keys: Vec::new(), - } + Self { map: std::collections::HashMap::new(), keys: Vec::new() } } pub fn insert(&mut self, key: K, value: V) -> Option { diff --git a/crates/tinywasm/tests/testsuite/mod.rs b/crates/tinywasm/tests/testsuite/mod.rs index 36fc727..2019c04 100644 --- a/crates/tinywasm/tests/testsuite/mod.rs +++ b/crates/tinywasm/tests/testsuite/mod.rs @@ -46,9 +46,7 @@ impl TestSuite { if let Err(e) = &test.result { eprintln!( "{} {} failed: {:?}", - link(group_name, &group.file, Some(test.linecol.0 + 1)) - .bold() - .underline(), + link(group_name, &group.file, Some(test.linecol.0 + 1)).bold().underline(), test_name.bold(), e.to_string().bright_red() ); @@ -98,11 +96,7 @@ impl TestSuite { passed += group_passed; failed += group_failed; - groups.push(TestGroupResult { - name: name.to_string(), - passed: group_passed, - failed: group_failed, - }); + groups.push(TestGroupResult { name: name.to_string(), passed: group_passed, failed: group_failed }); } let groups = serde_json::to_string(&groups)?; @@ -134,7 +128,9 @@ impl Debug for TestSuite { writeln!(f, "{}", link(group_name, &group.file, None).bold().underline())?; writeln!(f, " Tests Passed: {}", group_passed.to_string().green())?; - writeln!(f, " Tests Failed: {}", group_failed.to_string().red())?; + if group_failed != 0 { + writeln!(f, " Tests Failed: {}", group_failed.to_string().red())?; + } // for (test_name, test) in &group.tests { // write!(f, " {}: ", test_name.bold())?; @@ -166,10 +162,7 @@ struct TestGroup { impl TestGroup { fn new(file: &str) -> Self { - Self { - tests: IndexMap::new(), - file: file.to_string(), - } + Self { tests: IndexMap::new(), file: file.to_string() } } fn stats(&self) -> (usize, usize) { diff --git a/crates/tinywasm/tests/testsuite/run.rs b/crates/tinywasm/tests/testsuite/run.rs index 8a1ddf4..79d9acc 100644 --- a/crates/tinywasm/tests/testsuite/run.rs +++ b/crates/tinywasm/tests/testsuite/run.rs @@ -1,15 +1,72 @@ use crate::testsuite::util::*; -use std::{ - borrow::Cow, - panic::{catch_unwind, AssertUnwindSafe}, -}; +use std::{borrow::Cow, collections::HashMap}; use super::TestSuite; use eyre::{eyre, Result}; use log::{debug, error, info}; use tinywasm::{Extern, Imports, ModuleInstance}; -use tinywasm_types::{ModuleInstanceAddr, WasmValue}; -use wast::{lexer::Lexer, parser::ParseBuffer, Wast}; +use tinywasm_types::{ExternVal, MemoryType, ModuleInstanceAddr, TableType, ValType, WasmValue}; +use wast::{lexer::Lexer, parser::ParseBuffer, QuoteWat, Wast}; + +#[derive(Default)] +struct RegisteredModules { + modules: HashMap, + + named_modules: HashMap, + last_module: Option, +} + +impl RegisteredModules { + fn modules(&self) -> &HashMap { + &self.modules + } + + fn update_last_module(&mut self, addr: ModuleInstanceAddr, name: Option) { + self.last_module = Some(addr); + if let Some(name) = name { + self.named_modules.insert(name, addr); + } + } + fn register(&mut self, name: String, addr: ModuleInstanceAddr) { + log::debug!("registering module: {}", name); + self.modules.insert(name.clone(), addr); + + self.last_module = Some(addr); + self.named_modules.insert(name, addr); + } + + fn get_idx(&self, module_id: Option>) -> Option<&ModuleInstanceAddr> { + match module_id { + Some(module) => { + log::debug!("getting module: {}", module.name()); + + if let Some(addr) = self.modules.get(module.name()) { + return Some(addr); + } + + if let Some(addr) = self.named_modules.get(module.name()) { + return Some(addr); + } + + None + } + None => self.last_module.as_ref(), + } + } + + fn get<'a>( + &self, + module_id: Option>, + store: &'a tinywasm::Store, + ) -> Option<&'a ModuleInstance> { + let addr = self.get_idx(module_id)?; + store.get_module_instance(*addr) + } + + fn last<'a>(&self, store: &'a tinywasm::Store) -> Option<&'a ModuleInstance> { + store.get_module_instance(*self.last_module.as_ref()?) + } +} impl TestSuite { pub fn run_paths(&mut self, tests: &[&str]) -> Result<()> { @@ -22,17 +79,65 @@ impl TestSuite { Ok(()) } - fn imports(registered_modules: Vec<(String, ModuleInstanceAddr)>) -> Result { + fn imports(modules: &HashMap) -> Result { let mut imports = Imports::new(); + let table = + Extern::table(TableType::new(ValType::RefFunc, 10, Some(20)), WasmValue::default_for(ValType::RefFunc)); + + let print = Extern::typed_func(|_ctx: tinywasm::FuncContext, _: ()| { + log::debug!("print"); + Ok(()) + }); + + let print_i32 = Extern::typed_func(|_ctx: tinywasm::FuncContext, arg: i32| { + log::debug!("print_i32: {}", arg); + Ok(()) + }); + + let print_i64 = Extern::typed_func(|_ctx: tinywasm::FuncContext, arg: i64| { + log::debug!("print_i64: {}", arg); + Ok(()) + }); + + let print_f32 = Extern::typed_func(|_ctx: tinywasm::FuncContext, arg: f32| { + log::debug!("print_f32: {}", arg); + Ok(()) + }); + + let print_f64 = Extern::typed_func(|_ctx: tinywasm::FuncContext, arg: f64| { + log::debug!("print_f64: {}", arg); + Ok(()) + }); + + let print_i32_f32 = Extern::typed_func(|_ctx: tinywasm::FuncContext, args: (i32, f32)| { + log::debug!("print_i32_f32: {}, {}", args.0, args.1); + Ok(()) + }); + + let print_f64_f64 = Extern::typed_func(|_ctx: tinywasm::FuncContext, args: (f64, f64)| { + log::debug!("print_f64_f64: {}, {}", args.0, args.1); + Ok(()) + }); + imports + .define("spectest", "memory", Extern::memory(MemoryType::new_32(1, Some(2))))? + .define("spectest", "table", table)? .define("spectest", "global_i32", Extern::global(WasmValue::I32(666), false))? .define("spectest", "global_i64", Extern::global(WasmValue::I64(666), false))? .define("spectest", "global_f32", Extern::global(WasmValue::F32(666.0), false))? - .define("spectest", "global_f64", Extern::global(WasmValue::F64(666.0), false))?; - - for (name, addr) in registered_modules { - imports.link_module(&name, addr)?; + .define("spectest", "global_f64", Extern::global(WasmValue::F64(666.0), false))? + .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)?; + + for (name, addr) in modules { + log::debug!("registering module: {}", name); + imports.link_module(&name, *addr)?; } Ok(imports) @@ -66,8 +171,7 @@ impl TestSuite { let wast_data = wast::parser::parse::(&buf).expect("failed to parse wat"); let mut store = tinywasm::Store::default(); - let mut registered_modules = Vec::new(); - let mut last_module: Option = None; + let mut registered_modules = RegisteredModules::default(); println!("running {} tests for group: {}", wast_data.directives.len(), group_name); for (i, directive) in wast_data.directives.into_iter().enumerate() { @@ -76,7 +180,7 @@ impl TestSuite { match directive { Register { span, name, .. } => { - let Some(last) = &last_module else { + let Some(last) = registered_modules.last(&store) else { test_group.add_result( &format!("Register({})", i), span.linecol_in(wast), @@ -84,45 +188,57 @@ impl TestSuite { ); continue; }; - - registered_modules.push((name.to_string(), last.id())); + registered_modules.register(name.to_string(), last.id()); test_group.add_result(&format!("Register({})", i), span.linecol_in(wast), Ok(())); } - Wat(mut module) => { - // TODO: modules are not properly isolated from each other - tests fail because of this otherwise - store = tinywasm::Store::default(); + Wat(module) => { debug!("got wat module"); - let result = catch_unwind(AssertUnwindSafe(|| { - let m = parse_module_bytes(&module.encode().expect("failed to encode module")) - .expect("failed to parse module bytes"); - tinywasm::Module::from(m) - .instantiate(&mut store, Some(Self::imports(registered_modules.clone()).unwrap())) - .map_err(|e| { - println!("failed to instantiate module: {:?}", e); - e - }) - .expect("failed to instantiate module") - })) + let result = catch_unwind_silent(|| { + let (name, bytes) = 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::>() + .join("\n"); + + let lexer = Lexer::new(&wat); + let buf = ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer"); + let mut wat_data = wast::parser::parse::(&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"), + ) + } + _ => unimplemented!("Not supported"), + }; + + let m = parse_module_bytes(&bytes).expect("failed to parse module bytes"); + + let module_instance = tinywasm::Module::from(m) + .instantiate(&mut store, Some(Self::imports(registered_modules.modules()).unwrap())) + .expect("failed to instantiate module"); + + (name, module_instance) + }) .map_err(|e| eyre!("failed to parse wat module: {:?}", try_downcast_panic(e))); match &result { - Err(_) => last_module = None, - Ok(m) => last_module = Some(m.clone()), - } - - if let Err(err) = &result { - debug!("failed to parse module: {:?}", err) - } + Err(err) => debug!("failed to parse module: {:?}", err), + Ok((name, module)) => registered_modules.update_last_module(module.id(), name.clone()), + }; test_group.add_result(&format!("Wat({})", i), span.linecol_in(wast), result.map(|_| ())); } - AssertMalformed { - span, - mut module, - message: _, - } => { + AssertMalformed { span, mut module, message: _ } => { let Ok(module) = module.encode() else { test_group.add_result(&format!("AssertMalformed({})", i), span.linecol_in(wast), Ok(())); continue; @@ -142,11 +258,7 @@ impl TestSuite { ); } - AssertInvalid { - span, - mut module, - message: _, - } => { + AssertInvalid { span, mut module, message: _ } => { 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))) .and_then(|res| res); @@ -161,31 +273,55 @@ impl TestSuite { ); } - AssertTrap { exec, message: _, span } => { + AssertExhaustion { call, message, span } => { + let module = registered_modules.get_idx(call.module); + let args = convert_wastargs(call.args).expect("failed to convert 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), + span.linecol_in(wast), + Err(eyre!("expected trap")), + ); + continue; + }; + + if trap.message() != message { + test_group.add_result( + &format!("AssertExhaustion({})", i), + span.linecol_in(wast), + Err(eyre!("expected trap: {}, got: {}", message, trap.message())), + ); + continue; + } + + test_group.add_result(&format!("AssertExhaustion({})", i), span.linecol_in(wast), Ok(())); + } + + AssertTrap { exec, message, span } => { let res: Result, _> = catch_unwind_silent(|| { - let (module, name, args) = match exec { + let invoke = match exec { wast::WastExecute::Wat(mut wat) => { let module = parse_module_bytes(&wat.encode().expect("failed to encode module")) .expect("failed to parse module"); let module = tinywasm::Module::from(module); module.instantiate( &mut store, - Some(Self::imports(registered_modules.clone()).unwrap()), + Some(Self::imports(registered_modules.modules()).unwrap()), )?; return Ok(()); } wast::WastExecute::Get { module: _, global: _ } => { panic!("get not supported"); } - wast::WastExecute::Invoke(invoke) => (last_module.as_ref(), invoke.name, invoke.args), + wast::WastExecute::Invoke(invoke) => invoke, }; - let args = args - .into_iter() - .map(wastarg2tinywasmvalue) - .collect::>>() - .expect("failed to convert args"); - exec_fn_instance(module, &mut store, name, &args).map(|_| ()) + let module = registered_modules.get_idx(invoke.module); + let args = convert_wastargs(invoke.args).expect("failed to convert args"); + exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) }); match res { @@ -194,54 +330,141 @@ impl TestSuite { span.linecol_in(wast), Err(eyre!("test panicked: {:?}", try_downcast_panic(err))), ), - Ok(Err(tinywasm::Error::Trap(_))) => { + Ok(Err(tinywasm::Error::Trap(trap))) => { + if trap.message() != message { + test_group.add_result( + &format!("AssertTrap({})", i), + span.linecol_in(wast), + Err(eyre!("expected trap: {}, got: {}", message, trap.message())), + ); + continue; + } + test_group.add_result(&format!("AssertTrap({})", i), span.linecol_in(wast), Ok(())) } Ok(Err(err)) => test_group.add_result( &format!("AssertTrap({})", i), span.linecol_in(wast), - Err(eyre!("expected trap, got error: {:?}", err,)), + Err(eyre!("expected trap, {}, got: {:?}", message, err)), ), Ok(Ok(())) => test_group.add_result( &format!("AssertTrap({})", i), span.linecol_in(wast), - Err(eyre!("expected trap, got ok")), + Err(eyre!("expected trap {}, got Ok", message)), + ), + } + } + + AssertUnlinkable { mut module, span, message } => { + let res = catch_unwind_silent(|| { + let module = parse_module_bytes(&module.encode().expect("failed to encode module")) + .expect("failed to parse module"); + let module = tinywasm::Module::from(module); + module.instantiate(&mut store, Some(Self::imports(registered_modules.modules()).unwrap())) + }); + + match res { + Err(err) => test_group.add_result( + &format!("AssertUnlinkable({})", i), + span.linecol_in(wast), + Err(eyre!("test panicked: {:?}", try_downcast_panic(err))), + ), + Ok(Err(tinywasm::Error::Linker(err))) => { + if err.message() != message { + test_group.add_result( + &format!("AssertUnlinkable({})", i), + span.linecol_in(wast), + Err(eyre!("expected linker error: {}, got: {}", message, err.message())), + ); + continue; + } + + test_group.add_result(&format!("AssertUnlinkable({})", i), span.linecol_in(wast), Ok(())) + } + Ok(Err(err)) => test_group.add_result( + &format!("AssertUnlinkable({})", i), + span.linecol_in(wast), + Err(eyre!("expected linker error, {}, got: {:?}", message, err)), + ), + Ok(Ok(_)) => test_group.add_result( + &format!("AssertUnlinkable({})", i), + span.linecol_in(wast), + Err(eyre!("expected linker error {}, got Ok", message)), ), } } Invoke(invoke) => { let name = invoke.name; + let res: Result, _> = catch_unwind_silent(|| { - let args = invoke - .args - .into_iter() - .map(wastarg2tinywasmvalue) - .collect::>>() - .map_err(|e| { - error!("failed to convert args: {:?}", e); - e - })?; - - exec_fn_instance(last_module.as_ref(), &mut store, invoke.name, &args).map_err(|e| { + let args = convert_wastargs(invoke.args)?; + let module = registered_modules.get_idx(invoke.module); + exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| { error!("failed to execute function: {:?}", e); e })?; 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), res); } AssertReturn { span, exec, results } => { info!("AssertReturn: {:?}", exec); + let expected = convert_wastret(results)?; + let invoke = match match exec { wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")), - wast::WastExecute::Get { module: _, global: _ } => Err(eyre!("get not supported")), + wast::WastExecute::Get { module: module_id, global } => { + let module = registered_modules.get(module_id, &store); + let Some(module) = module else { + test_group.add_result( + &format!("AssertReturn(unsupported-{})", i), + span.linecol_in(wast), + Err(eyre!("no module to get global from")), + ); + continue; + }; + + let module_global = match match module.export(global) { + Some(ExternVal::Global(addr)) => { + store.get_global_val(addr as usize).map_err(|_| eyre!("failed to get global")) + } + _ => Err(eyre!("no module to get global from")), + } { + Ok(module_global) => module_global, + Err(err) => { + test_group.add_result( + &format!("AssertReturn(unsupported-{})", i), + span.linecol_in(wast), + Err(eyre!("failed to get global: {:?}", err)), + ); + continue; + } + }; + let expected = expected.get(0).expect("expected global value"); + let module_global = module_global.attach_type(expected.val_type()); + + if !module_global.eq_loose(expected) { + test_group.add_result( + &format!("AssertReturn(unsupported-{})", i), + span.linecol_in(wast), + Err(eyre!("global value did not match: {:?} != {:?}", module_global, expected)), + ); + continue; + } + + test_group.add_result( + &format!("AssertReturn({}-{})", global, i), + span.linecol_in(wast), + Ok(()), + ); + + continue; + // check if module_global matches the expected results + } wast::WastExecute::Invoke(invoke) => Ok(invoke), } { Ok(invoke) => invoke, @@ -254,37 +477,19 @@ impl TestSuite { continue; } }; - let invoke_name = invoke.name; + let invoke_name = invoke.name; let res: Result, _> = catch_unwind_silent(|| { debug!("invoke: {:?}", invoke); - let args = invoke - .args - .into_iter() - .map(wastarg2tinywasmvalue) - .collect::>>() - .map_err(|e| { - error!("failed to convert args: {:?}", e); - e - })?; - - let outcomes = - exec_fn_instance(last_module.as_ref(), &mut store, invoke.name, &args).map_err(|e| { - error!("failed to execute function: {:?}", e); - e - })?; + let args = convert_wastargs(invoke.args)?; + let module = registered_modules.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 + })?; debug!("outcomes: {:?}", outcomes); - let expected = results - .into_iter() - .map(wastret2tinywasmvalue) - .collect::>>() - .map_err(|e| { - error!("failed to convert expected results: {:?}", e); - e - })?; - debug!("expected: {:?}", expected); if outcomes.len() != expected.len() { @@ -296,26 +501,18 @@ impl TestSuite { )); } - outcomes - .iter() - .zip(expected) - .enumerate() - .try_for_each(|(i, (outcome, exp))| { - (outcome.eq_loose(&exp)) - .then_some(()) - .ok_or_else(|| eyre!(" result {} did not match: {:?} != {:?}", i, outcome, exp)) - }) + log::debug!("outcomes: {:?}", outcomes); + + outcomes.iter().zip(expected).enumerate().try_for_each(|(i, (outcome, exp))| { + (outcome.eq_loose(&exp)) + .then_some(()) + .ok_or_else(|| eyre!(" result {} did not match: {:?} != {:?}", i, outcome, exp)) + }) }); - 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), - res, - ); + test_group.add_result(&format!("AssertReturn({}-{})", invoke_name, i), span.linecol_in(wast), res); } _ => test_group.add_result( &format!("Unknown({})", i), diff --git a/crates/tinywasm/tests/testsuite/util.rs b/crates/tinywasm/tests/testsuite/util.rs index 9ba7934..b74eec5 100644 --- a/crates/tinywasm/tests/testsuite/util.rs +++ b/crates/tinywasm/tests/testsuite/util.rs @@ -1,26 +1,18 @@ use std::panic::{self, AssertUnwindSafe}; use eyre::{eyre, Result}; -use tinywasm_types::{TinyWasmModule, WasmValue}; +use tinywasm_types::{ModuleInstanceAddr, TinyWasmModule, ValType, WasmValue}; pub fn try_downcast_panic(panic: Box) -> String { - let info = panic - .downcast_ref::() - .or(None) - .map(|p| p.to_string()) - .clone(); + let info = panic.downcast_ref::().or(None).map(|p| p.to_string()).clone(); let info_string = panic.downcast_ref::().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(), - ) + info.unwrap_or(info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string()) } pub fn exec_fn_instance( - instance: Option<&tinywasm::ModuleInstance>, + instance: Option<&ModuleInstanceAddr>, store: &mut tinywasm::Store, name: &str, args: &[tinywasm_types::WasmValue], @@ -29,6 +21,10 @@ pub fn exec_fn_instance( 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.exported_func_by_name(store, name)?; func.call(store, args) } @@ -62,7 +58,15 @@ pub fn parse_module_bytes(bytes: &[u8]) -> Result { Ok(parser.parse_module_bytes(bytes)?) } -pub fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { +pub fn convert_wastargs(args: Vec) -> Result> { + args.into_iter().map(|a| wastarg2tinywasmvalue(a)).collect() +} + +pub fn convert_wastret(args: Vec) -> Result> { + args.into_iter().map(|a| wastret2tinywasmvalue(a)).collect() +} + +fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { let wast::WastArg::Core(arg) = arg else { return Err(eyre!("unsupported arg type: Component")); }; @@ -73,17 +77,17 @@ pub fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result WasmValue::F64(f64::from_bits(f.bits)), I32(i) => WasmValue::I32(i), I64(i) => WasmValue::I64(i), - // RefExtern(v) => WasmValue::RefExtern(v), - RefNull(t) => WasmValue::RefNull(match t { - wast::core::HeapType::Func => tinywasm_types::ValType::FuncRef, - wast::core::HeapType::Extern => tinywasm_types::ValType::ExternRef, + RefExtern(v) => WasmValue::RefExtern(v), + RefNull(t) => match t { + wast::core::HeapType::Func => WasmValue::RefNull(ValType::RefFunc), + wast::core::HeapType::Extern => WasmValue::RefNull(ValType::RefExtern), _ => return Err(eyre!("unsupported arg type: refnull: {:?}", t)), - }), + }, v => return Err(eyre!("unsupported arg type: {:?}", v)), }) } -pub fn wastret2tinywasmvalue(arg: wast::WastRet) -> Result { +fn wastret2tinywasmvalue(arg: wast::WastRet) -> Result { let wast::WastRet::Core(arg) = arg else { return Err(eyre!("unsupported arg type")); }; @@ -94,7 +98,20 @@ pub fn wastret2tinywasmvalue(arg: wast::WastRet) -> Result nanpattern2tinywasmvalue(f)?, I32(i) => WasmValue::I32(i), I64(i) => WasmValue::I64(i), - _ => return Err(eyre!("unsupported arg type")), + RefNull(t) => match t { + Some(wast::core::HeapType::Func) => WasmValue::RefNull(ValType::RefFunc), + Some(wast::core::HeapType::Extern) => WasmValue::RefNull(ValType::RefExtern), + _ => return Err(eyre!("unsupported arg type: refnull: {:?}", t)), + }, + RefExtern(v) => match v { + Some(v) => WasmValue::RefExtern(v), + _ => return Err(eyre!("unsupported arg type: refextern: {:?}", v)), + }, + RefFunc(v) => match v { + Some(wast::token::Index::Num(n, _)) => WasmValue::RefFunc(n), + _ => return Err(eyre!("unsupported arg type: reffunc: {:?}", v)), + }, + a => return Err(eyre!("unsupported arg type {:?}", a)), }) } diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index 01c5b6e..d5de50a 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -1,4 +1,4 @@ -use crate::MemAddr; +use crate::{ElemAddr, MemAddr}; use super::{FuncAddr, GlobalAddr, LabelAddr, LocalAddr, TableAddr, TypeAddr, ValType}; @@ -11,7 +11,7 @@ pub enum BlockArgs { /// Represents a memory immediate in a WebAssembly memory instruction. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct MemArg { +pub struct MemoryArg { pub mem_addr: MemAddr, pub align: u8, pub align_max: u8, @@ -82,29 +82,29 @@ pub enum Instruction { GlobalSet(GlobalAddr), // Memory Instructions - I32Load(MemArg), - I64Load(MemArg), - F32Load(MemArg), - F64Load(MemArg), - I32Load8S(MemArg), - I32Load8U(MemArg), - I32Load16S(MemArg), - I32Load16U(MemArg), - I64Load8S(MemArg), - I64Load8U(MemArg), - I64Load16S(MemArg), - I64Load16U(MemArg), - I64Load32S(MemArg), - I64Load32U(MemArg), - I32Store(MemArg), - I64Store(MemArg), - F32Store(MemArg), - F64Store(MemArg), - I32Store8(MemArg), - I32Store16(MemArg), - I64Store8(MemArg), - I64Store16(MemArg), - I64Store32(MemArg), + I32Load(MemoryArg), + I64Load(MemoryArg), + F32Load(MemoryArg), + F64Load(MemoryArg), + I32Load8S(MemoryArg), + I32Load8U(MemoryArg), + I32Load16S(MemoryArg), + I32Load16U(MemoryArg), + I64Load8S(MemoryArg), + I64Load8U(MemoryArg), + I64Load16S(MemoryArg), + I64Load16U(MemoryArg), + I64Load32S(MemoryArg), + I64Load32U(MemoryArg), + I32Store(MemoryArg), + I64Store(MemoryArg), + F32Store(MemoryArg), + F64Store(MemoryArg), + I32Store8(MemoryArg), + I32Store16(MemoryArg), + I64Store8(MemoryArg), + I64Store16(MemoryArg), + I64Store32(MemoryArg), MemorySize(MemAddr, u8), MemoryGrow(MemAddr, u8), @@ -257,4 +257,13 @@ pub enum Instruction { I64TruncSatF32U, I64TruncSatF64S, I64TruncSatF64U, + + // Table Instructions + TableInit(TableAddr, ElemAddr), + TableGet(TableAddr), + TableSet(TableAddr), + TableCopy { from: TableAddr, to: TableAddr }, + TableGrow(TableAddr), + TableSize(TableAddr), + TableFill(TableAddr), } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index d3bf81a..d0d854c 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -2,10 +2,7 @@ #![forbid(unsafe_code)] #![doc(test( no_crate_inject, - attr( - deny(warnings, rust_2018_idioms), - allow(dead_code, unused_assignments, unused_variables) - ) + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables)) ))] #![warn(missing_debug_implementations, rust_2018_idioms, unreachable_pub)] @@ -14,16 +11,15 @@ extern crate alloc; // log for logging (optional). -// #[cfg(feature = "logging")] -// #[allow(unused_imports)] -// use log; +#[cfg(feature = "logging")] +use log; -// #[cfg(not(feature = "logging"))] -// #[macro_use] -// pub(crate) mod log { -// // macro_rules! debug ( ($($tt:tt)*) => {{}} ); -// // pub(crate) use debug; -// } +#[cfg(not(feature = "logging"))] +#[macro_use] +pub(crate) mod log { + macro_rules! error ( ($($tt:tt)*) => {{}} ); + pub(crate) use error; +} mod instructions; use core::{fmt::Debug, ops::Range}; @@ -45,7 +41,7 @@ pub struct TinyWasmModule { pub start_func: Option, /// The functions of the WebAssembly module. - pub funcs: Box<[Function]>, + pub funcs: Box<[(u32, WasmFunction)]>, /// The types of the WebAssembly module. pub func_types: Box<[FuncType]>, @@ -86,10 +82,9 @@ pub enum WasmValue { F32(f32), /// A 64-bit float. F64(f64), - // Vec types - // V128(i128), - // RefExtern(ExternAddr), - // RefHost(FuncAddr), + + RefExtern(ExternAddr), + RefFunc(FuncAddr), RefNull(ValType), } @@ -100,9 +95,12 @@ impl WasmValue { Self::I64(i) => ConstInstruction::I64Const(*i), Self::F32(i) => ConstInstruction::F32Const(*i), Self::F64(i) => ConstInstruction::F64Const(*i), + + Self::RefFunc(i) => ConstInstruction::RefFunc(*i), Self::RefNull(ty) => ConstInstruction::RefNull(*ty), + // Self::RefExtern(addr) => ConstInstruction::RefExtern(*addr), - // _ => unimplemented!("const_instr for {:?}", self), + _ => unimplemented!("no const_instr for {:?}", self), } } @@ -113,9 +111,8 @@ impl WasmValue { ValType::I64 => Self::I64(0), ValType::F32 => Self::F32(0.0), ValType::F64 => Self::F64(0.0), - ValType::V128 => unimplemented!("V128 is not yet supported"), - ValType::FuncRef => unimplemented!("FuncRef is not yet supported"), - ValType::ExternRef => unimplemented!("ExternRef is not yet supported"), + ValType::RefFunc => Self::RefNull(ValType::RefFunc), + ValType::RefExtern => Self::RefNull(ValType::RefExtern), } } @@ -123,6 +120,9 @@ impl WasmValue { match (self, other) { (Self::I32(a), Self::I32(b)) => a == b, (Self::I64(a), Self::I64(b)) => a == b, + (Self::RefNull(v), Self::RefNull(v2)) => v == v2, + (Self::RefExtern(addr), Self::RefExtern(addr2)) => addr == addr2, + (Self::RefFunc(addr), Self::RefFunc(addr2)) => addr == addr2, (Self::F32(a), Self::F32(b)) => { if a.is_nan() && b.is_nan() { true // Both are NaN, treat them as equal @@ -166,19 +166,16 @@ impl From for WasmValue { } } -// impl From for WasmValue { -// fn from(i: i128) -> Self { -// Self::V128(i) -// } -// } - impl TryFrom for i32 { type Error = (); fn try_from(value: WasmValue) -> Result { match value { WasmValue::I32(i) => Ok(i), - _ => Err(()), + _ => { + log::error!("i32: try_from failed: {:?}", value); + Err(()) + } } } } @@ -189,7 +186,10 @@ impl TryFrom for i64 { fn try_from(value: WasmValue) -> Result { match value { WasmValue::I64(i) => Ok(i), - _ => Err(()), + _ => { + log::error!("i64: try_from failed: {:?}", value); + Err(()) + } } } } @@ -200,7 +200,10 @@ impl TryFrom for f32 { fn try_from(value: WasmValue) -> Result { match value { WasmValue::F32(i) => Ok(i), - _ => Err(()), + _ => { + log::error!("f32: try_from failed: {:?}", value); + Err(()) + } } } } @@ -211,7 +214,10 @@ impl TryFrom for f64 { fn try_from(value: WasmValue) -> Result { match value { WasmValue::F64(i) => Ok(i), - _ => Err(()), + _ => { + log::error!("f64: try_from failed: {:?}", value); + Err(()) + } } } } @@ -223,6 +229,8 @@ impl Debug for WasmValue { WasmValue::I64(i) => write!(f, "i64({})", i), WasmValue::F32(i) => write!(f, "f32({})", i), WasmValue::F64(i) => write!(f, "f64({})", i), + WasmValue::RefExtern(addr) => write!(f, "ref.extern({:?})", addr), + WasmValue::RefFunc(addr) => write!(f, "ref.func({:?})", addr), WasmValue::RefNull(ty) => write!(f, "ref.null({:?})", ty), // WasmValue::V128(i) => write!(f, "v128({})", i), } @@ -237,8 +245,9 @@ impl WasmValue { Self::I64(_) => ValType::I64, Self::F32(_) => ValType::F32, Self::F64(_) => ValType::F64, + Self::RefExtern(_) => ValType::RefExtern, + Self::RefFunc(_) => ValType::RefFunc, Self::RefNull(ty) => *ty, - // Self::V128(_) => ValType::V128, } } } @@ -254,12 +263,10 @@ pub enum ValType { F32, /// A 64-bit float. F64, - /// A 128-bit vector. - V128, /// A reference to a function. - FuncRef, + RefFunc, /// A reference to an external value. - ExternRef, + RefExtern, } impl ValType { @@ -293,7 +300,7 @@ pub type FuncAddr = Addr; pub type TableAddr = Addr; pub type MemAddr = Addr; pub type GlobalAddr = Addr; -pub type ElmAddr = Addr; +pub type ElemAddr = Addr; pub type DataAddr = Addr; pub type ExternAddr = Addr; // additional internal addresses @@ -305,7 +312,7 @@ pub type ModuleInstanceAddr = Addr; /// A WebAssembly External Value. /// /// See -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ExternVal { Func(FuncAddr), Table(TableAddr), @@ -313,6 +320,26 @@ pub enum ExternVal { Global(GlobalAddr), } +impl ExternVal { + pub fn kind(&self) -> ExternalKind { + match self { + Self::Func(_) => ExternalKind::Func, + Self::Table(_) => ExternalKind::Table, + Self::Mem(_) => ExternalKind::Memory, + Self::Global(_) => ExternalKind::Global, + } + } + + pub fn new(kind: ExternalKind, addr: Addr) -> Self { + match kind { + ExternalKind::Func => Self::Func(addr), + ExternalKind::Table => Self::Table(addr), + ExternalKind::Memory => Self::Mem(addr), + ExternalKind::Global => Self::Global(addr), + } + } +} + /// The type of a WebAssembly Function. /// /// See @@ -325,19 +352,15 @@ pub struct FuncType { impl FuncType { /// Get the number of parameters of a function type. pub fn empty() -> Self { - Self { - params: Box::new([]), - results: Box::new([]), - } + Self { params: Box::new([]), results: Box::new([]) } } } -/// A WebAssembly Function #[derive(Debug, Clone)] -pub struct Function { - pub ty: TypeAddr, - pub locals: Box<[ValType]>, +pub struct WasmFunction { pub instructions: Box<[Instruction]>, + pub locals: Box<[ValType]>, + pub ty: FuncType, } /// A WebAssembly Module Export @@ -363,13 +386,23 @@ pub struct GlobalType { pub ty: ValType, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TableType { pub element_type: ValType, pub size_initial: u32, pub size_max: Option, } +impl TableType { + pub fn empty() -> Self { + Self { element_type: ValType::RefFunc, size_initial: 0, size_max: None } + } + + pub fn new(element_type: ValType, size_initial: u32, size_max: Option) -> Self { + Self { element_type, size_initial, size_max } + } +} + #[derive(Debug, Clone)] /// Represents a memory's type. @@ -380,6 +413,20 @@ pub struct MemoryType { pub page_count_max: Option, } +impl MemoryType { + pub fn new_32(page_count_initial: u64, page_count_max: Option) -> Self { + Self { arch: MemoryArch::I32, page_count_initial, page_count_max } + } + + // pub fn new_64(page_count_initial: u64, page_count_max: Option) -> Self { + // Self { + // arch: MemoryArch::I64, + // page_count_initial, + // page_count_max, + // } + // } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum MemoryArch { I32, @@ -395,12 +442,23 @@ pub struct Import { #[derive(Debug, Clone)] pub enum ImportKind { - Func(TypeAddr), + Function(TypeAddr), Table(TableType), - Mem(MemoryType), + Memory(MemoryType), Global(GlobalType), } +impl From<&ImportKind> for ExternalKind { + fn from(kind: &ImportKind) -> Self { + match kind { + ImportKind::Function(_) => Self::Func, + ImportKind::Table(_) => Self::Table, + ImportKind::Memory(_) => Self::Memory, + ImportKind::Global(_) => Self::Global, + } + } +} + #[derive(Debug, Clone)] pub struct Data { pub data: Box<[u8]>, @@ -422,7 +480,7 @@ pub struct Element { pub ty: ValType, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ElementKind { Passive, Active { table: TableAddr, offset: ConstInstruction }, diff --git a/crates/wasm-testsuite/Cargo.toml b/crates/wasm-testsuite/Cargo.toml index 3e6d647..04f478c 100644 --- a/crates/wasm-testsuite/Cargo.toml +++ b/crates/wasm-testsuite/Cargo.toml @@ -1,6 +1,6 @@ [package] name="wasm-testsuite" -version="0.2.0" +version="0.2.1" description="Mirror of the WebAssembly core testsuite for use in testing WebAssembly implementations" license="Apache-2.0" readme="README.md" diff --git a/crates/wasm-testsuite/lib.rs b/crates/wasm-testsuite/lib.rs index 9e38bfc..e3352fb 100644 --- a/crates/wasm-testsuite/lib.rs +++ b/crates/wasm-testsuite/lib.rs @@ -2,10 +2,7 @@ #![forbid(unsafe_code)] #![doc(test( no_crate_inject, - attr( - deny(warnings, rust_2018_idioms), - allow(dead_code, unused_assignments, unused_variables) - ) + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_assignments, unused_variables)) ))] #![warn(missing_docs, missing_debug_implementations, rust_2018_idioms, unreachable_pub)] @@ -26,7 +23,7 @@ pub const PROPOSALS: &[&str] = &["annotations", "exception-handling", "memory64" /// List of all tests that apply to the MVP (V1) spec. /// Note that the tests are still for the latest spec, so the latest version of Wast is used. #[rustfmt::skip] // removed: "break-drop.wast", -pub const MVP_TESTS: &[&str] = &["address.wast","align.wast","binary-leb128.wast","binary.wast","block.wast","br.wast","br_if.wast","br_table.wast","call.wast","call_indirect.wast","comments.wast","const.wast","conversions.wast","custom.wast","data.wast","elem.wast","endianness.wast","exports.wast","f32.wast","f32_bitwise.wast","f32_cmp.wast","f64.wast","f64_bitwise.wast","f64_cmp.wast","fac.wast","float_exprs.wast","float_literals.wast","float_memory.wast","float_misc.wast","forward.wast","func.wast","func_ptrs.wast","global.wast","i32.wast","i64.wast","if.wast","imports.wast","inline-module.wast","int_exprs.wast","int_literals.wast","labels.wast","left-to-right.wast","linking.wast","load.wast","local_get.wast","local_set.wast","local_tee.wast","loop.wast","memory.wast","memory_grow.wast","memory_redundancy.wast","memory_size.wast","memory_trap.wast","names.wast","nop.wast","return.wast","select.wast","skip-stack-guard-page.wast","stack.wast","start.wast","store.wast","switch.wast","token.wast","traps.wast","type.wast","unreachable.wast","unreached-invalid.wast","unwind.wast","utf8-custom-section-id.wast","utf8-import-field.wast","utf8-import-module.wast","utf8-invalid-encoding.wast"]; +pub const MVP_TESTS: &[&str] = &["address.wast","align.wast","binary-leb128.wast","binary.wast","block.wast","br.wast","br_if.wast","br_table.wast","call.wast","call_indirect.wast","comments.wast","const.wast","conversions.wast","custom.wast","data.wast","elem.wast","endianness.wast","exports.wast","f32.wast","f32_bitwise.wast","f32_cmp.wast","f64.wast","f64_bitwise.wast","f64_cmp.wast","fac.wast","float_exprs.wast","float_literals.wast","float_memory.wast","float_misc.wast","forward.wast","func.wast","func_ptrs.wast","global.wast","i32.wast","i64.wast","if.wast","imports.wast","inline-module.wast","int_exprs.wast","int_literals.wast","labels.wast","left-to-right.wast","linking.wast","load.wast","local_get.wast","local_set.wast","local_tee.wast","loop.wast","memory.wast","memory_grow.wast","memory_redundancy.wast","memory_size.wast","memory_trap.wast","names.wast","nop.wast","return.wast","select.wast","skip-stack-guard-page.wast","stack.wast","start.wast","store.wast","switch.wast","table.wast","token.wast","traps.wast","type.wast","unreachable.wast","unreached-valid.wast","unreached-invalid.wast","unwind.wast","utf8-custom-section-id.wast","utf8-import-field.wast","utf8-import-module.wast","utf8-invalid-encoding.wast"]; /// List of all tests that apply to the V2 draft 1 spec. #[rustfmt::skip] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ce47073 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +# Examples + +## WasmRust + +These are examples using WebAssembly generated from Rust code. +To run these, you first need to build the Rust code into WebAssembly, since the wasm files are not included in the repository to keep it small. +This requires the `wasm32-unknown-unknown` target and `wasm-opt` to be installed (available via Binaryen). + +```bash +$ ./examples/rust/build.sh +``` + +Then you can run the examples: + +```bash +$ cargo run --example wasm-rust +``` + +Where `` is one of the following: + +- `hello`: A simple example that prints a number to the console. +- `tinywasm`: Runs `hello` using TinyWasm - inside of TinyWasm itself! diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml new file mode 100644 index 0000000..9ff80dc --- /dev/null +++ b/examples/rust/Cargo.toml @@ -0,0 +1,18 @@ +cargo-features=["per-package-target"] + +[package] +publish=false +name="rust-wasm-examples" +forced-target="wasm32-unknown-unknown" +edition="2021" + +[dependencies] +tinywasm={path="../../crates/tinywasm", features=["parser", "std"]} + +[[bin]] +name="hello" +path="src/hello.rs" + +[[bin]] +name="tinywasm" +path="src/tinywasm.rs" diff --git a/examples/rust/README.md b/examples/rust/README.md new file mode 100644 index 0000000..8ecac52 --- /dev/null +++ b/examples/rust/README.md @@ -0,0 +1 @@ +# Examples using Rust compiled to WebAssembly diff --git a/examples/rust/build.sh b/examples/rust/build.sh new file mode 100755 index 0000000..2c8069a --- /dev/null +++ b/examples/rust/build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +cd "$(dirname "$0")" + +bins=("hello" "tinywasm") +exclude_wat=("tinywasm") +out_dir="../../target/wasm32-unknown-unknown/wasm" +dest_dir="out" + +for bin in "${bins[@]}"; do + cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" + + cp "$out_dir/$bin.wasm" "$dest_dir/" + wasm-opt "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.wasm" -O --intrinsic-lowering -O + + if [[ ! " ${exclude_wat[@]} " =~ " $bin " ]]; then + wasm2wat "$dest_dir/$bin.wasm" -o "$dest_dir/$bin.wat" + fi +done diff --git a/examples/rust/src/hello.rs b/examples/rust/src/hello.rs new file mode 100644 index 0000000..34f3c7f --- /dev/null +++ b/examples/rust/src/hello.rs @@ -0,0 +1,18 @@ +#![no_std] +#![no_main] + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} + +#[link(wasm_import_module = "env")] +extern "C" { + fn printi32(x: i32); +} + +#[no_mangle] +pub unsafe extern "C" fn add_and_print(lh: i32, rh: i32) { + printi32(lh + rh); +} diff --git a/examples/rust/src/tinywasm.rs b/examples/rust/src/tinywasm.rs new file mode 100644 index 0000000..0f18ab9 --- /dev/null +++ b/examples/rust/src/tinywasm.rs @@ -0,0 +1,32 @@ +#![no_main] +use tinywasm::{Extern, FuncContext}; + +#[link(wasm_import_module = "env")] +extern "C" { + fn printi32(x: i32); +} + +#[no_mangle] +pub extern "C" fn hello() { + let _ = run(); +} + +fn run() -> tinywasm::Result<()> { + let module = tinywasm::Module::parse_bytes(include_bytes!("../out/hello.wasm"))?; + let mut store = tinywasm::Store::default(); + let mut imports = tinywasm::Imports::new(); + + imports.define( + "env", + "printi32", + Extern::typed_func(|_: FuncContext<'_>, v: i32| { + unsafe { printi32(v) } + Ok(()) + }), + )?; + let instance = module.instantiate(&mut store, Some(imports))?; + + let add_and_print = instance.typed_func::<(i32, i32), ()>(&mut store, "add_and_print")?; + add_and_print.call(&mut store, (1, 2))?; + Ok(()) +} diff --git a/examples/wasm-rust.rs b/examples/wasm-rust.rs new file mode 100644 index 0000000..3b8877e --- /dev/null +++ b/examples/wasm-rust.rs @@ -0,0 +1,65 @@ +use color_eyre::eyre::Result; +use tinywasm::{Extern, FuncContext, Imports, Module, Store}; + +fn main() -> Result<()> { + let args = std::env::args().collect::>(); + if args.len() < 2 { + println!("Usage: cargo run --example wasm-rust "); + println!("Available examples:"); + println!(" hello"); + println!(" tinywasm"); + return Ok(()); + } + + match args[1].as_str() { + "hello" => hello()?, + "tinywasm" => tinywasm()?, + _ => {} + } + + Ok(()) +} + +fn tinywasm() -> Result<()> { + const TINYWASM: &[u8] = include_bytes!("./rust/out/tinywasm.wasm"); + let module = Module::parse_bytes(&TINYWASM)?; + let mut store = Store::default(); + + let mut imports = Imports::new(); + imports.define( + "env", + "printi32", + Extern::typed_func(|_: FuncContext<'_>, x: i32| { + println!("{}", x); + Ok(()) + }), + )?; + let instance = module.instantiate(&mut store, Some(imports))?; + + let hello = instance.typed_func::<(), ()>(&mut store, "hello")?; + hello.call(&mut store, ())?; + + Ok(()) +} + +fn hello() -> Result<()> { + const HELLO_WASM: &[u8] = include_bytes!("./rust/out/hello.wasm"); + let module = Module::parse_bytes(&HELLO_WASM)?; + let mut store = Store::default(); + + let mut imports = Imports::new(); + imports.define( + "env", + "printi32", + Extern::typed_func(|_: FuncContext<'_>, x: i32| { + println!("{}", x); + Ok(()) + }), + )?; + + let instance = module.instantiate(&mut store, Some(imports))?; + let add_and_print = instance.typed_func::<(i32, i32), ()>(&mut store, "add_and_print")?; + add_and_print.call(&mut store, (1, 2))?; + + Ok(()) +} diff --git a/examples/wasm/add.wasm b/examples/wasm/add.wasm deleted file mode 100644 index 92e3432..0000000 Binary files a/examples/wasm/add.wasm and /dev/null differ diff --git a/examples/wasm/add.wat b/examples/wasm/add.wat deleted file mode 100644 index 4976689..0000000 --- a/examples/wasm/add.wat +++ /dev/null @@ -1,16 +0,0 @@ -(module - (func $add (export "add") (param $a i32) (param $b i32) (result i32) - local.get $a - local.get $b - i32.add) - - (func $sub (export "sub") (param $a i32) (param $b i32) (result i32) - local.get $a - local.get $b - i32.sub) - - (func $add_64 (export "add_64") (param $a i64) (param $b i64) (result i64) - local.get $a - local.get $b - i64.add) -) diff --git a/examples/wasm/call.wat b/examples/wasm/call.wat deleted file mode 100644 index d515e78..0000000 --- a/examples/wasm/call.wat +++ /dev/null @@ -1,42 +0,0 @@ -(module - (func (export "check") (param i32) (result i32) - i64.const 0 ;; Set 0 to the stack - local.get 0 - i32.const 10 - i32.lt_s ;; Check if input is less than 10 - if (param i64) (result i32) ;; If so, - i32.const 1 ;; Set 1 to the stack - return ;; And return immediately - else ;; Otherwise, - i32.const 0 ;; Set 0 to the stack - return ;; And return immediately - end) ;; End of the if/else block - - (func (export "simple_block") (result i32) - (block (result i32) - (i32.const 0) - (i32.const 1) - (i32.add) - ) - ) - - (func (export "checkloop") (result i32) - (block (result i32) - (i32.const 0) - (loop (param i32) - (block (br 2 (i32.const 18))) - (br 0 (i32.const 20)) - ) - (i32.const 19) - ) - ) - - - (func (export "param") (result i32) - (i32.const 1) - (loop (param i32) (result i32) - (i32.const 2) - (i32.add) - ) - ) -) \ No newline at end of file diff --git a/examples/wasm/global.wat b/examples/wasm/global.wat deleted file mode 100644 index 22bde45..0000000 --- a/examples/wasm/global.wat +++ /dev/null @@ -1 +0,0 @@ -(module (global i32 (i32.const 0))) \ No newline at end of file diff --git a/examples/wasm/helloworld.wasm b/examples/wasm/helloworld.wasm deleted file mode 100644 index a5c95d0..0000000 Binary files a/examples/wasm/helloworld.wasm and /dev/null differ diff --git a/examples/wasm/helloworld.wat b/examples/wasm/helloworld.wat deleted file mode 100644 index b74c98f..0000000 --- a/examples/wasm/helloworld.wat +++ /dev/null @@ -1,15 +0,0 @@ -(module - ;; Imports from JavaScript namespace - (import "console" "log" (func $log (param i32 i32))) ;; Import log function - (import "js" "mem" (memory 1)) ;; Import 1 page of memory (54kb) - - ;; Data section of our module - (data (i32.const 0) "Hello World from WebAssembly!") - - ;; Function declaration: Exported as helloWorld(), no arguments - (func (export "helloWorld") - i32.const 0 ;; pass offset 0 to log - i32.const 29 ;; pass length 29 to log (strlen of sample text) - call $log - ) -) \ No newline at end of file diff --git a/examples/wasm/loop.wat b/examples/wasm/loop.wat deleted file mode 100644 index 0dcd191..0000000 --- a/examples/wasm/loop.wat +++ /dev/null @@ -1,84 +0,0 @@ -(module - (func $loop_test (export "loop_test") (result i32) - (local i32) ;; Local 0: Counter - - ;; Initialize the counter - (local.set 0 (i32.const 0)) - - ;; Loop starts here - (loop $my_loop - ;; Increment the counter - (local.set 0 (i32.add (local.get 0) (i32.const 1))) - - ;; Exit condition: break out of the loop if counter >= 10 - (br_if $my_loop (i32.lt_s (local.get 0) (i32.const 10))) - ) - - ;; Return the counter value - (local.get 0) - ) - - (func $loop_test3 (export "loop_test3") (result i32) - (local i32) ;; Local 0: Counter - - ;; Initialize the counter - (local.set 0 (i32.const 0)) - - ;; Loop starts here - (block $exit_loop ;; Label for exiting the loop - (loop $my_loop - ;; Increment the counter - (local.set 0 (i32.add (local.get 0) (i32.const 1))) - - ;; Prepare an index for br_table - ;; Here, we use the counter, but you could modify this - ;; For simplicity, 0 will continue the loop, any other value will exit - (local.get 0) - (i32.const 10) - (i32.lt_s) - (br_table $my_loop $exit_loop) - ) - ) - - ;; Return the counter value - (local.get 0) - ) - - (func $calculate (export "loop_test2") (result i32) - (local i32) ;; Local 0: Counter for the outer loop - (local i32) ;; Local 1: Counter for the inner loop - (local i32) ;; Local 2: Result variable - - ;; Initialize variables - (local.set 0 (i32.const 0)) ;; Initialize outer loop counter - (local.set 1 (i32.const 0)) ;; Initialize inner loop counter - (local.set 2 (i32.const 0)) ;; Initialize result variable - - (block $outer ;; Outer loop label - (loop $outer_loop - (local.set 1 (i32.const 5)) ;; Reset inner loop counter for each iteration of the outer loop - - (block $inner ;; Inner loop label - (loop $inner_loop - (br_if $inner (i32.eqz (local.get 1))) ;; Break to $inner if inner loop counter is zero - - ;; Computation: Adding product of counters to the result - (local.set 2 (i32.add (local.get 2) (i32.mul (local.get 0) (local.get 1)))) - - ;; Decrement inner loop counter - (local.set 1 (i32.sub (local.get 1) (i32.const 1))) - ) - ) - - ;; Increment outer loop counter - (local.set 0 (i32.add (local.get 0) (i32.const 1))) - - ;; Break condition for outer loop: break if outer loop counter >= 5 - (br_if $outer (i32.ge_s (local.get 0) (i32.const 5))) - ) - ) - - ;; Return the result - (local.get 2) - ) -) \ No newline at end of file diff --git a/examples/wasm/test.wat b/examples/wasm/test.wat deleted file mode 100644 index 563f382..0000000 --- a/examples/wasm/test.wat +++ /dev/null @@ -1,7 +0,0 @@ -(module - (func (export "test") (result i32) - (i32.const 1) - ;; comment - (return (i32.const 2)) - ) -) \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml index 94ac875..589b2d2 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ max_width=120 +use_small_heuristics="Max"