From 6b3640863a62f3094ccd1efd5f4aecf33096b7dd Mon Sep 17 00:00:00 2001 From: Oliver Horn <1029691+ohorn@users.noreply.github.com> Date: Sun, 3 Mar 2024 21:33:55 +0100 Subject: [PATCH] First release --- .github/workflows/release.yml | 38 ++ .gitignore | 2 + .gitmodules | 3 + CHANGELOG.md | 5 + Cargo.lock | 567 ++++++++++++++++ Cargo.toml | 32 +- LICENSE | 201 ++++++ README.md | 23 + src/error.rs | 156 +++++ src/json.rs | 632 ++++++++++++++++++ src/lib.rs | 606 ++++++++++++++++- src/main.rs | 85 +++ src/registry.rs | 125 ++++ tests/local/module-binary.txt | 13 + .../parse/assert/assert-after-module.txt | 23 + .../assert/assertinvalid-binary-module.txt | 15 + tests/local/parse/assert/assertinvalid.txt | 21 + tests/local/parse/assert/assertmalformed.txt | 13 + .../assert/assertreturn-arithmetic-nan.txt | 26 + .../assert/assertreturn-canonical-nan.txt | 26 + tests/local/parse/assert/assertreturn.txt | 30 + .../parse/assert/bad-assert-before-module.txt | 8 + .../assert/bad-assertreturn-non-const.txt | 17 + .../parse/assert/bad-assertreturn-too-few.txt | 12 + .../assert/bad-assertreturn-too-many.txt | 12 + .../bad-assertreturn-unknown-function.txt | 9 + .../parse/assert/bad-invoke-no-module.txt | 8 + .../local/parse/assert/bad-invoke-too-few.txt | 11 + .../parse/assert/bad-invoke-too-many.txt | 11 + .../assert/bad-invoke-unknown-function.txt | 12 + tests/local/parse/assert/invoke.txt | 30 + tests/local/parse/bad-input-command.txt | 10 + tests/local/parse/bad-output-command.txt | 12 + .../bad-assertreturn-invoke-type-mismatch.txt | 11 + .../bad-assertreturn-type-mismatch.txt | 11 + .../typecheck/bad-invoke-type-mismatch.txt | 11 + tests/tests.rs | 150 +++++ 37 files changed, 2971 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .gitmodules create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/error.rs create mode 100644 src/json.rs create mode 100644 src/main.rs create mode 100644 src/registry.rs create mode 100644 tests/local/module-binary.txt create mode 100644 tests/local/parse/assert/assert-after-module.txt create mode 100644 tests/local/parse/assert/assertinvalid-binary-module.txt create mode 100644 tests/local/parse/assert/assertinvalid.txt create mode 100644 tests/local/parse/assert/assertmalformed.txt create mode 100644 tests/local/parse/assert/assertreturn-arithmetic-nan.txt create mode 100644 tests/local/parse/assert/assertreturn-canonical-nan.txt create mode 100644 tests/local/parse/assert/assertreturn.txt create mode 100644 tests/local/parse/assert/bad-assert-before-module.txt create mode 100644 tests/local/parse/assert/bad-assertreturn-non-const.txt create mode 100644 tests/local/parse/assert/bad-assertreturn-too-few.txt create mode 100644 tests/local/parse/assert/bad-assertreturn-too-many.txt create mode 100644 tests/local/parse/assert/bad-assertreturn-unknown-function.txt create mode 100644 tests/local/parse/assert/bad-invoke-no-module.txt create mode 100644 tests/local/parse/assert/bad-invoke-too-few.txt create mode 100644 tests/local/parse/assert/bad-invoke-too-many.txt create mode 100644 tests/local/parse/assert/bad-invoke-unknown-function.txt create mode 100644 tests/local/parse/assert/invoke.txt create mode 100644 tests/local/parse/bad-input-command.txt create mode 100644 tests/local/parse/bad-output-command.txt create mode 100644 tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt create mode 100644 tests/local/typecheck/bad-assertreturn-type-mismatch.txt create mode 100644 tests/local/typecheck/bad-invoke-type-mismatch.txt create mode 100644 tests/tests.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1b04d25 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +permissions: + contents: write + +on: + push: + tags: + - v[0-9]+.* + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/create-gh-release-action@v1 + with: + changelog: CHANGELOG.md + branch: main + draft: true + token: ${{ secrets.GITHUB_TOKEN }} + + upload-assets: + needs: create-release + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: taiki-e/upload-rust-binary-action@v1 + with: + bin: wast2json-rs + checksum: sha256 + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ea8c4bf..6a647be 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.vscode/ +.idea/ /target diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..91abefb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/testsuite"] + path = tests/testsuite + url = https://github.com/WebAssembly/testsuite diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bd88105 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 - 2024-03-03 + +- First release diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..70b4326 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,567 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap-verbosity-flag" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9b20c0dd58e4c2e991c8d203bbeb76c11304d1011659686b5b644bc29aa478" +dependencies = [ + "clap", + "log", +] + +[[package]] +name = "clap_builder" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "2.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasm-encoder" +version = "0.201.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasmparser" +version = "0.201.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.201.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a67e66da702706ba08729a78e3c0079085f6bfcb1a62e4799e97bbf728c2c265" +dependencies = [ + "anyhow", + "wasmparser", +] + +[[package]] +name = "wast" +version = "201.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef6e1ef34d7da3e2b374fd2b1a9c0227aff6cad596e1b24df9b58d0f6222faa" +dependencies = [ + "bumpalo", + "leb128", + "memchr", + "unicode-width", + "wasm-encoder", +] + +[[package]] +name = "wast2json" +version = "0.1.0" +dependencies = [ + "assert-json-diff", + "clap", + "clap-verbosity-flag", + "env_logger", + "log", + "serde", + "serde_json", + "tempfile", + "thiserror", + "wasmparser", + "wasmprinter", + "wast", + "wat", +] + +[[package]] +name = "wat" +version = "1.201.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453d5b37a45b98dee4f4cb68015fc73634d7883bbef1c65e6e9c78d454cf3f32" +dependencies = [ + "wast", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/Cargo.toml b/Cargo.toml index 986595c..3553301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,37 @@ [package] name = "wast2json" -version = "0.0.0" +version = "0.1.0" +authors = ["Oliver Horn "] edition = "2021" description = "A wast2json implementation written in Rust" +readme = "README.md" +homepage = "https://github.com/ohorn/wast2json-rs" repository = "https://github.com/ohorn/wast2json-rs" license = "Apache-2.0" +keywords = ["WebAssembly", "wasm", "wast"] +categories = ["wasm"] + +[[bin]] +name = "wast2json-rs" +path = "src/main.rs" + +[profile.release] +lto = true +strip = true + +[dependencies] +clap = { version = "4.5.1", features = ["derive"] } +clap-verbosity-flag = "2.2.0" +env_logger = "0.11.2" +log = "0.4.21" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = { version = "1.0.114", features = ["preserve_order"] } +thiserror = "1.0.57" +wasmparser = "0.201.0" +wast = "201.0.0" +wat = "1.201.0" + +[dev-dependencies] +assert-json-diff = "2.0.2" +wasmprinter = "0.201.0" +tempfile = "3.10.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1a8df3 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# wast2json-rs + +A [`wast2json`](https://github.com/WebAssembly/wabt/blob/main/docs/wast2json.md) implementation written in Rust. + +## Installation + +You can download prebuilt binaries from the [Release page](https://github.com/ohorn/wast2json-rs/releases). + +You can also install from source using Cargo: + +```sh +cargo install wast2json +``` + +## Usage + +The binary file is named `wast2json-rs` to allow installation alongside wabt's `wast2json`. + +```sh +$ wast2json-rs wast2json spec-test.wast -o spec-test.json +``` + +Details about the output JSON format can be found on the wabt [`wast2json` page](https://github.com/WebAssembly/wabt/blob/main/docs/wast2json.md#json-format). diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..859e20a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,156 @@ +use crate::json::WastJsonValueType; +use std::{fmt, io, path::Path, path::PathBuf, result}; +use wast::token::Span; + +pub(crate) type Result = result::Result; + +#[derive(thiserror::Error)] +#[error(transparent)] +pub struct Error { + inner: Box, +} + +impl Error { + pub(crate) fn new(kind: ErrorKind) -> Self { + Self { + inner: Box::new(ErrorInner { + kind, + span: None, + path: None, + pos: None, + source_line: None, + }), + } + } + + pub(crate) fn set_span(&mut self, offset: Span) { + if self.inner.span.is_none() { + self.inner.span = Some(offset); + } + } + + pub(crate) fn set_source(&mut self, path: &Path, source: &str) { + self.inner.path = Some(path.to_owned()); + if let Some(span) = self.inner.span { + let pos = span.linecol_in(source); + self.inner.pos = Some(pos); + self.inner.source_line = source.lines().nth(pos.0).map(|s| s.to_string()); + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +#[derive(thiserror::Error, Debug)] +struct ErrorInner { + #[source] + kind: ErrorKind, + span: Option, + path: Option, + pos: Option<(usize, usize)>, + source_line: Option, +} + +impl fmt::Display for ErrorInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(ref path) = self.path { + write!(f, "{}", path.to_string_lossy())?; + if let Some((line, col)) = self.pos { + write!(f, ":{}:{}", line + 1, col + 1)?; + } + f.write_str(": ")?; + } + write!(f, "error: {}", self.kind)?; + if let Some(ref source_line) = self.source_line { + writeln!(f)?; + f.write_str(source_line)?; + if let Some((_, col)) = self.pos { + writeln!(f)?; + write!(f, "{}^", " ".repeat(col))?; + } + } + Ok(()) + } +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum ErrorKind { + #[error("unknown module")] + UnknownModule, + #[error("unknown function export \"{0}\"")] + UnknownFuncExport(String), + #[error("unknown global export \"{0}\"")] + UnknownGlobalExport(String), + #[error("too {} {0}s to function. got {1}, expected {2}", if .1 < .2 { "few" } else { "many" })] + WrongNumberOfTypes(TypeContext, usize, usize), + #[error("type mismatch for {1} {2} of {0}. got {3}, expected {4}")] + WrongType( + String, + TypeContext, + usize, + WastJsonValueType, + WastJsonValueType, + ), + #[error("unsupported feature: {0}")] + UnsupportedFeature(&'static str), + #[error("{}", _0.message())] + WastError(#[source] wast::Error), + #[error("{0}")] + IoError(#[source] io::Error), + #[error("{0}")] + JsonError(#[source] serde_json::Error), + #[error("{0}")] + BinaryReaderError(#[source] wasmparser::BinaryReaderError), +} + +#[derive(Debug)] +pub enum TypeContext { + Argument, + Result, +} + +impl fmt::Display for TypeContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Argument => "argument", + Self::Result => "result", + }) + } +} + +impl From for Error { + fn from(source: wasmparser::BinaryReaderError) -> Self { + Error::new(ErrorKind::BinaryReaderError(source)) + } +} + +impl From for Error { + fn from(source: wast::Error) -> Self { + let span = source.span(); + let mut error = Error::new(ErrorKind::WastError(source)); + error.set_span(span); + error + } +} + +impl From for Error { + fn from(source: io::Error) -> Self { + Error::new(ErrorKind::IoError(source)) + } +} + +impl From for Error { + fn from(source: serde_json::Error) -> Self { + Error::new(ErrorKind::JsonError(source)) + } +} + +impl From for Error { + fn from(source: ErrorKind) -> Self { + Error::new(source) + } +} diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 0000000..43076d4 --- /dev/null +++ b/src/json.rs @@ -0,0 +1,632 @@ +//! Types for a `wast2json`-compatible JSON representation of `wast` scripts. +//! +//! The entry point is the [`WastJsonScript`] struct. +//! +//! Note: This crate reuses the [`Float32`] and [`Float32`] structs and the +//! [`NanPattern`] enum of the [`wast`] crate. +//! Due to some limitations, the `V128Const` and `V128Pattern` enums are +//! _not_ currently reused! + +use serde::Serialize; +use serde_json::ser::Formatter; +use std::{borrow::Cow, fmt, io}; +use wast::core::NanPattern; +use wast::token::{Float32, Float64}; + +#[derive(Serialize)] +pub struct WastJsonScript { + pub source_filename: String, + pub commands: Box<[WastJsonCommand]>, +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +pub enum WastJsonCommand { + Module { + line: usize, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + filename: String, + }, + Action { + line: usize, + action: WastJsonAction, + expected: Box<[WastJsonValueType]>, + }, + AssertReturn { + line: usize, + action: WastJsonAction, + expected: Box<[WastJsonExpected]>, + }, + AssertException { + line: usize, + action: WastJsonAction, + expected: Box<[WastJsonValueType]>, + }, + AssertMalformed { + line: usize, + filename: String, + text: String, + module_type: ModuleType, + }, + AssertInvalid { + line: usize, + filename: String, + text: String, + module_type: ModuleType, + }, + AssertUninstantiable { + line: usize, + filename: String, + text: String, + module_type: ModuleType, + }, + AssertUnlinkable { + line: usize, + filename: String, + text: String, + module_type: ModuleType, + }, + AssertTrap { + line: usize, + action: WastJsonAction, + text: String, + expected: Box<[WastJsonValueType]>, + }, + AssertExhaustion { + line: usize, + action: WastJsonAction, + text: String, + expected: Box<[WastJsonValueType]>, + }, + Register { + line: usize, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(rename = "as")] + alias: String, + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ModuleType { + Binary, + Text, +} + +impl ModuleType { + pub fn extension(self) -> &'static str { + match self { + ModuleType::Binary => "wasm", + ModuleType::Text => "wat", + } + } +} + +#[derive(Serialize, Debug)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum WastJsonAction { + Invoke { + #[serde(skip_serializing_if = "Option::is_none")] + module: Option, + field: String, + args: Box<[WastJsonConst]>, + }, + Get { + #[serde(skip_serializing_if = "Option::is_none")] + module: Option, + field: String, + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum WastJsonValueType { + I32, + I64, + F32, + F64, + V128, + FuncRef, + ExternRef, +} + +impl fmt::Display for WastJsonValueType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::I32 => "i32", + Self::I64 => "i64", + Self::F32 => "f32", + Self::F64 => "f64", + Self::V128 => "v128", + Self::FuncRef => "funcref", + Self::ExternRef => "externref", + }) + } +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum WastJsonConst { + I32 { + #[serde(with = "value_string")] + value: i32, + }, + I64 { + #[serde(with = "value_string")] + value: i64, + }, + F32 { + #[serde(with = "value_string")] + value: Float32, + }, + F64 { + #[serde(with = "value_string")] + value: Float64, + }, + V128(V128Const), + FuncRef { + #[serde(with = "value_string")] + value: RefIndex, + }, + ExternRef { + #[serde(with = "value_string")] + value: RefIndex, + }, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "lane_type", content = "value")] +pub enum V128Const { + #[serde(rename = "i8", with = "value_string_array")] + I8([i8; 16]), + #[serde(rename = "i16", with = "value_string_array")] + I16([i16; 8]), + #[serde(rename = "i32", with = "value_string_array")] + I32([i32; 4]), + #[serde(rename = "i64", with = "value_string_array")] + I64([i64; 2]), + #[serde(rename = "f32", with = "value_string_array")] + F32([Float32; 4]), + #[serde(rename = "f64", with = "value_string_array")] + F64([Float64; 2]), +} + +impl From<&wast::core::V128Const> for V128Const { + fn from(value: &wast::core::V128Const) -> V128Const { + match value { + wast::core::V128Const::I8x16(v) => Self::I8(*v), + wast::core::V128Const::I16x8(v) => Self::I16(*v), + wast::core::V128Const::I32x4(v) => Self::I32(*v), + wast::core::V128Const::I64x2(v) => Self::I64(*v), + wast::core::V128Const::F32x4(v) => Self::F32(*v), + wast::core::V128Const::F64x2(v) => Self::F64(*v), + } + } +} + +impl From<&V128Const> for wast::core::V128Const { + fn from(value: &V128Const) -> wast::core::V128Const { + match value { + V128Const::I8(v) => Self::I8x16(*v), + V128Const::I16(v) => Self::I16x8(*v), + V128Const::I32(v) => Self::I32x4(*v), + V128Const::I64(v) => Self::I64x2(*v), + V128Const::F32(v) => Self::F32x4(*v), + V128Const::F64(v) => Self::F64x2(*v), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum WastJsonExpected { + I32 { + #[serde(with = "value_string")] + value: i32, + }, + I64 { + #[serde(with = "value_string")] + value: i64, + }, + F32 { + #[serde(with = "value_string")] + value: NanPattern, + }, + F64 { + #[serde(with = "value_string")] + value: NanPattern, + }, + V128(V128Pattern), + FuncRef { + #[serde(with = "value_string")] + value: RefIndex, + }, + ExternRef { + #[serde(with = "value_string")] + value: RefIndex, + }, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "lane_type", content = "value")] +pub enum V128Pattern { + #[serde(rename = "i8", with = "value_string_array")] + I8([i8; 16]), + #[serde(rename = "i16", with = "value_string_array")] + I16([i16; 8]), + #[serde(rename = "i32", with = "value_string_array")] + I32([i32; 4]), + #[serde(rename = "i64", with = "value_string_array")] + I64([i64; 2]), + #[serde(rename = "f32", with = "value_string_array")] + F32([NanPattern; 4]), + #[serde(rename = "f64", with = "value_string_array")] + F64([NanPattern; 2]), +} + +pub(crate) trait Clone2 { + fn clone2(&self) -> Self; +} + +impl Clone2 for NanPattern { + fn clone2(&self) -> Self { + match self { + Self::CanonicalNan => Self::CanonicalNan, + Self::ArithmeticNan => Self::ArithmeticNan, + Self::Value(v) => Self::Value(v.clone()), + } + } +} + +impl Clone2 for [NanPattern; 4] { + fn clone2(&self) -> Self { + [ + self[0].clone2(), + self[1].clone2(), + self[2].clone2(), + self[3].clone2(), + ] + } +} + +impl Clone2 for [NanPattern; 2] { + fn clone2(&self) -> Self { + [self[0].clone2(), self[1].clone2()] + } +} + +impl From<&wast::core::V128Pattern> for V128Pattern { + fn from(value: &wast::core::V128Pattern) -> V128Pattern { + match value { + wast::core::V128Pattern::I8x16(v) => Self::I8(*v), + wast::core::V128Pattern::I16x8(v) => Self::I16(*v), + wast::core::V128Pattern::I32x4(v) => Self::I32(*v), + wast::core::V128Pattern::I64x2(v) => Self::I64(*v), + wast::core::V128Pattern::F32x4(v) => Self::F32(v.clone2()), + wast::core::V128Pattern::F64x2(v) => Self::F64(v.clone2()), + } + } +} + +impl From<&V128Pattern> for wast::core::V128Pattern { + fn from(value: &V128Pattern) -> wast::core::V128Pattern { + match value { + V128Pattern::I8(v) => Self::I8x16(*v), + V128Pattern::I16(v) => Self::I16x8(*v), + V128Pattern::I32(v) => Self::I32x4(*v), + V128Pattern::I64(v) => Self::I64x2(*v), + V128Pattern::F32(v) => Self::F32x4(v.clone2()), + V128Pattern::F64(v) => Self::F64x2(v.clone2()), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub enum RefIndex { + Null, + Index(u32), +} + +impl fmt::Display for RefIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Null => f.write_str("null"), + Self::Index(index) => index.fmt(f), + } + } +} + +mod value_string { + use super::ValueString; + + pub(crate) fn serialize(value: &V, serializer: S) -> std::result::Result + where + S: serde::Serializer, + V: ValueString, + { + serializer.serialize_str(&value.to_value_string()) + } +} + +mod value_string_array { + use super::ValueString; + use serde::ser::SerializeSeq; + + pub(crate) fn serialize( + value: &[V; N], + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + V: ValueString, + { + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for element in value { + seq.serialize_element(&element.to_value_string())?; + } + seq.end() + } +} + +/// Trait for converting values to their `wast2json` string representation. +/// +/// Note: Number values are always written as unsigned decimal numbers. +/// Floats are always written as the decimal encoding of their binary +/// representation. +pub trait ValueString: Sized + fmt::Debug { + const NAME: &'static str; + + fn to_value_string(&self) -> Cow<'static, str>; +} + +macro_rules! value_string { + ( $name:literal, $ty:ident, $val:ident => $to:expr, $str:ident => $parse:expr ) => { + impl ValueString for $ty { + const NAME: &'static str = concat!($name, " value"); + + fn to_value_string(&self) -> Cow<'static, str> { + let $val = self; + ($to).to_string().into() + } + } + }; +} + +macro_rules! nanpattern_string { + ( $name:literal, $ty:ident) => { + impl ValueString for NanPattern<$ty> { + const NAME: &'static str = concat!($name, " pattern"); + + fn to_value_string(&self) -> Cow<'static, str> { + match self { + Self::CanonicalNan => "nan:canonical".into(), + Self::ArithmeticNan => "nan:arithmetic".into(), + Self::Value(v) => v.to_value_string(), + } + } + } + }; +} + +value_string!("i8", i8, val => *val as u8, str => str.parse::()); +value_string!("i16", i16, val => *val as u16, str => str.parse::()); +value_string!("i32", i32, val => *val as u32, str => str.parse::()); +value_string!("i64", i64, val => *val as u64, str => str.parse::()); +value_string!("f32", Float32, val => val.bits, str => str.parse().map(|bits| Float32 { bits })); +value_string!("f64", Float64, val => val.bits, str => str.parse().map(|bits| Float64 { bits })); + +nanpattern_string!("f32", Float32); +nanpattern_string!("f64", Float64); + +impl ValueString for RefIndex { + const NAME: &'static str = "reference"; + + #[allow(clippy::cast_sign_loss)] + fn to_value_string(&self) -> Cow<'static, str> { + match self { + RefIndex::Null => "null".into(), + RefIndex::Index(idx) => idx.to_string().into(), + } + } +} + +/// A formatter that formats the JSON output similar to wabt. +#[derive(Clone, Debug)] +pub(crate) struct WabtFormatter { + current_indent: usize, +} + +impl WabtFormatter { + #[allow(clippy::new_without_default)] + pub fn new() -> WabtFormatter { + WabtFormatter { current_indent: 0 } + } +} + +impl Formatter for WabtFormatter { + #[inline] + fn begin_array(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + self.current_indent += 1; + if self.current_indent == 2 { + writer.write_all(b"[\n ") + } else { + writer.write_all(b"[") + } + } + + #[inline] + fn end_array(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + self.current_indent -= 1; + writer.write_all(b"]") + } + + #[inline] + fn begin_array_value(&mut self, writer: &mut W, first: bool) -> io::Result<()> + where + W: ?Sized + io::Write, + { + if first { + Ok(()) + } else if self.current_indent == 2 { + writer.write_all(b", \n ") + } else { + writer.write_all(b", ") + } + } + + #[inline] + fn begin_object(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + self.current_indent += 1; + writer.write_all(b"{") + } + + #[inline] + fn end_object(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + self.current_indent -= 1; + writer.write_all(b"}") + } + + #[inline] + fn begin_object_key(&mut self, writer: &mut W, first: bool) -> io::Result<()> + where + W: ?Sized + io::Write, + { + if first { + Ok(()) + } else if self.current_indent == 1 { + writer.write_all(b",\n ") + } else { + writer.write_all(b", ") + } + } + + #[inline] + fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> + where + W: ?Sized + io::Write, + { + writer.write_all(b": ") + } +} + +#[cfg(test)] +mod test { + use super::*; + use assert_json_diff::*; + use serde_json::json; + + #[test] + fn test_const_i32() { + assert_json_eq!( + WastJsonConst::I32 { value: -1 }, + json!({ "type": "i32", "value": "4294967295" }) + ); + } + + #[test] + fn test_const_i64() { + assert_json_eq!( + WastJsonConst::I64 { value: -1 }, + json!({ "type": "i64", "value": "18446744073709551615" }) + ); + } + + #[test] + fn test_const_f32() { + assert_json_eq!( + WastJsonConst::F32 { + value: Float32 { bits: 0xc0400000 } + }, + json!({ "type": "f32", "value": "3225419776" }) + ); + } + + #[test] + fn test_const_f64() { + assert_json_eq!( + WastJsonConst::F64 { + value: Float64 { + bits: 0xc008000000000000 + } + }, + json!({ "type": "f64", "value": "13837309855095848960" }) + ); + } + + #[test] + fn test_const_v128() { + assert_json_eq!( + WastJsonConst::V128(V128Const::F32([ + Float32 { bits: 1 }, + Float32 { bits: 2 }, + Float32 { bits: 3 }, + Float32 { bits: 4 }, + ])), + json!({ "type": "v128", "lane_type": "f32", "value": ["1", "2", "3", "4"] }) + ); + } + + #[test] + fn test_const_funcref() { + assert_json_eq!( + WastJsonConst::FuncRef { + value: RefIndex::Index(1) + }, + json!({ "type": "funcref", "value": "1" }) + ); + } + + #[test] + fn test_const_funcref_null() { + assert_json_eq!( + WastJsonConst::FuncRef { + value: RefIndex::Null + }, + json!({ "type": "funcref", "value": "null" }) + ); + } + + #[test] + fn test_const_externref() { + assert_json_eq!( + WastJsonConst::ExternRef { + value: RefIndex::Index(1), + }, + json!({ "type": "externref", "value": "1" }) + ); + } + + #[test] + fn test_const_externref_null() { + assert_json_eq!( + WastJsonConst::ExternRef { + value: RefIndex::Null + }, + json!({ "type": "externref", "value": "null" }) + ); + } + + #[test] + fn test_pattern_f64_nan_arithmtic() { + assert_json_eq!( + WastJsonExpected::F64 { + value: NanPattern::ArithmeticNan + }, + json!({ "type": "f64", "value": "nan:arithmetic" }) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 31e1bb2..27a7d19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,603 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +use std::{io::Write, iter::once, path::PathBuf}; + +use serde::Serialize; +use serde_json::ser::{CompactFormatter, PrettyFormatter}; + +mod error; +pub use error::Error; +use error::{ErrorKind, Result, TypeContext}; + +pub mod json; +mod registry; + +use json::{ + Clone2, ModuleType, RefIndex, WabtFormatter, WastJsonAction, WastJsonCommand, WastJsonConst, + WastJsonExpected, WastJsonScript, WastJsonValueType, +}; +use registry::ModuleRegistry; + +use wasmparser::ValType; +use wast::{ + core::{HeapType, WastArgCore, WastRetCore}, + parser::{self, ParseBuffer}, + token::{Id, Span}, + QuoteWat, Wast, WastArg, WastDirective, WastExecute, WastInvoke, WastRet, Wat, +}; + +#[derive(Default)] +pub struct Wast2JsonOptions {} + +impl WastJsonScript { + pub fn to_string() {} + + pub fn write(&self, writer: W) -> Result<()> { + self.write_formatted(writer, WabtFormatter::new()) + } + + pub fn write_pretty(&self, writer: W) -> Result<()> { + self.write_formatted(writer, PrettyFormatter::new()) + } + + pub fn write_compact(&self, writer: W) -> Result<()> { + self.write_formatted(writer, CompactFormatter) + } + + pub fn write_formatted(&self, writer: W, formatter: F) -> Result<()> + where + W: Write, + F: serde_json::ser::Formatter + std::fmt::Debug, + { + let mut serializer = serde_json::ser::Serializer::with_formatter(writer, formatter); + self.serialize(&mut serializer)?; + Ok(()) + } +} + +/// Trait for writing (or collecting) the `.wat` or `.wasm` modules files +/// associated with a [`WastJsonScript`]. +pub trait WriteModuleFile { + fn write(&mut self, module_fileme: &str, bytes: Vec) -> Result<()>; +} + +impl WriteModuleFile for T +where + T: Extend<(String, Vec)>, +{ + fn write(&mut self, module_filename: &str, bytes: Vec) -> Result<()> { + self.extend(Some((module_filename.to_string(), bytes))); + Ok(()) + } +} + +/// Converts a `wast` file to its `wast2json` representation. +pub fn wast2json( + source_filename: &PathBuf, + source: &str, + module_basename: &str, + module_files: &mut M, + options: &Wast2JsonOptions, +) -> Result { + let wast2json = Wast2JsonConverter::new( + source_filename, + source, + module_basename, + module_files, + options, + ); + wast2json.convert() +} + +struct Wast2JsonConverter<'a, M: WriteModuleFile> { + filename: &'a PathBuf, + text: &'a str, + line_offsets: Vec, + module_basename: &'a str, + module_files: &'a mut M, + counter: u32, + registry: ModuleRegistry, +} + +#[derive(Clone)] +struct ModuleFile { + pub filename: String, + pub module_type: ModuleType, + pub name: Option, +} + +macro_rules! unsupported { + ($str:expr) => { + return Err(Error::new(ErrorKind::UnsupportedFeature($str))) + }; +} + +impl<'a, M: WriteModuleFile> Wast2JsonConverter<'a, M> { + fn new( + source_filename: &'a PathBuf, + text: &'a str, + module_basename: &'a str, + module_files: &'a mut M, + _options: &Wast2JsonOptions, + ) -> Self { + let line_offsets = once(0) + .chain(text.match_indices('\n').map(|(i, _)| i + 1)) + .collect(); + Self { + filename: source_filename, + text, + line_offsets, + module_basename, + module_files, + counter: 0, + registry: ModuleRegistry::new(), + } + } + + fn span_linecol(&self, span: Span) -> (usize, usize) { + let offset = span.offset(); + let line = self.line_offsets.partition_point(|&i| i <= offset) - 1; + let line_offset = self.line_offsets.get(line).copied().unwrap_or(0); + (line, offset - line_offset) + } + + fn convert(mut self) -> Result { + let buf = ParseBuffer::new(self.text)?; + let mut wast = match parser::parse::(&buf) { + Ok(wast) => wast, + Err(err) => { + let mut err: Error = err.into(); + err.set_source(self.filename, self.text); + return Err(err); + } + }; + + let mut cmds = Vec::new(); + + for directive in &mut wast.directives { + let cmd = self.directive_to_command(directive).map_err(|mut err| { + err.set_span(directive.span()); + err.set_source(self.filename, self.text); + err + })?; + cmds.push(cmd); + } + + Ok(WastJsonScript { + source_filename: self.filename.to_string_lossy().to_string(), + commands: cmds.into_boxed_slice(), + }) + } + + #[allow(clippy::too_many_lines)] + fn directive_to_command(&mut self, directive: &mut WastDirective) -> Result { + let span = directive.span(); + let line = self.span_linecol(span).0 + 1; + + let cmd = match directive { + WastDirective::Wat(wat) => { + let file = self.write_quote_module(wat)?; + let name = file.name; + let filename = file.filename; + WastJsonCommand::Module { + line, + name, + filename, + } + } + + WastDirective::Invoke(invoke) => { + let result_types = self.check_types_invoke(invoke)?; + WastJsonCommand::Action { + line, + action: self.invoke_to_action(invoke)?, + expected: result_types, + } + } + + WastDirective::AssertReturn { exec, results, .. } => { + self.check_result_types(exec, results)?; + let expected = results + .iter() + .map(|v| v.try_into()) + .collect::>>()?; + WastJsonCommand::AssertReturn { + line, + action: self.exec_to_action(exec)?, + expected, + } + } + + WastDirective::AssertException { ref exec, .. } => { + let result_types = self.check_types(exec)?; + WastJsonCommand::AssertException { + line, + action: self.exec_to_action(exec)?, + expected: result_types, + } + } + + WastDirective::AssertMalformed { + module, message, .. + } => { + let file = self.write_quote_module(module)?; + WastJsonCommand::AssertMalformed { + line, + filename: file.filename.to_string(), + text: (*message).to_string(), + module_type: file.module_type, + } + } + + WastDirective::AssertInvalid { + module, message, .. + } => { + let file = self.write_quote_module(module)?; + WastJsonCommand::AssertInvalid { + line, + filename: file.filename.to_string(), + text: (*message).to_string(), + module_type: file.module_type, + } + } + + WastDirective::AssertTrap { + exec: WastExecute::Wat(wat), + message, + .. + } => { + let file = self.write_module(wat)?; + WastJsonCommand::AssertUninstantiable { + line, + filename: file.filename.to_string(), + text: (*message).to_string(), + module_type: file.module_type, + } + } + + WastDirective::AssertUnlinkable { + module, message, .. + } => { + let file = self.write_module(module)?; + WastJsonCommand::AssertUnlinkable { + line, + filename: file.filename.to_string(), + text: (*message).to_string(), + module_type: file.module_type, + } + } + + WastDirective::AssertTrap { exec, message, .. } => { + let result_types = self.check_types(exec)?; + WastJsonCommand::AssertTrap { + line, + action: self.exec_to_action(exec)?, + text: (*message).to_string(), + expected: result_types, + } + } + + WastDirective::AssertExhaustion { call, message, .. } => { + let result_types = self.check_types_invoke(call)?; + WastJsonCommand::AssertExhaustion { + line, + action: self.invoke_to_action(call)?, + text: (*message).to_string(), + expected: result_types, + } + } + + WastDirective::Register { + name: alias, + module, + .. + } => { + // Check that the module exists + self.registry.lookup_module(module)?; + WastJsonCommand::Register { + line, + name: module.map(|ref id| id_to_name(id)), + alias: (*alias).to_string(), + } + } + + WastDirective::Thread(..) | WastDirective::Wait { .. } => unsupported!("threads"), + }; + + Ok(cmd) + } + + fn invoke_to_action(&self, invoke: &WastInvoke) -> Result { + let module = invoke.module.map(|ref id| id_to_name(id)); + let field = invoke.name.to_string(); + let args = invoke + .args + .iter() + .map(|v| v.try_into()) + .collect::>>()?; + Ok(WastJsonAction::Invoke { + module, + field, + args, + }) + } + + fn exec_to_action(&self, exec: &WastExecute) -> Result { + match exec { + WastExecute::Invoke(invoke) => self.invoke_to_action(invoke), + WastExecute::Get { global, module } => { + let module = module.map(|ref id| id_to_name(id)); + let field = (*global).to_string(); + Ok(WastJsonAction::Get { module, field }) + } + WastExecute::Wat(..) => unsupported!("components"), + } + } + + fn write_quote_module(&mut self, module: &mut QuoteWat) -> Result { + match module { + QuoteWat::QuoteModule(_, source) => self.write_quote(source), + QuoteWat::QuoteComponent(..) => unsupported!("components"), + QuoteWat::Wat(wat) => self.write_module(wat), + } + } + + fn write_module(&mut self, module: &mut Wat) -> Result { + match module { + Wat::Module(module) => { + let bytes = module.encode()?; + let name = module.id.map(|ref id| id_to_name(id)); + self.registry.define(&module.id, &bytes)?; + self.write(ModuleType::Binary, name, bytes) + } + Wat::Component(..) => unsupported!("components"), + } + } + + fn write_quote(&mut self, source: &[(Span, &[u8])]) -> Result { + let len = source.iter().map(|&(_, buf)| buf.len()).sum(); + let mut bytes = Vec::with_capacity(len); + source.iter().for_each(|&(_, buf)| bytes.extend(buf)); + self.write(ModuleType::Text, None, bytes) + } + + fn write( + &mut self, + module_type: ModuleType, + name: Option, + bytes: Vec, + ) -> Result { + let filename = format!( + "{}.{}.{}", + self.module_basename, + self.counter, + module_type.extension() + ); + + self.counter += 1; + + let file = ModuleFile { + filename: filename.clone(), + module_type, + name, + }; + + self.module_files.write(&filename, bytes)?; + + Ok(file) + } + + fn check_types(&self, exec: &WastExecute) -> Result> { + match exec { + WastExecute::Invoke(invoke) => self.check_types_invoke(invoke), + WastExecute::Get { module, global } => { + let module = self.registry.lookup_module(module)?; + let global_type = module.get_global_type(global)?; + Ok(Box::new([global_type.try_into()?])) + } + WastExecute::Wat(..) => unsupported!("components"), + } + } + + fn check_types_invoke(&self, invoke: &WastInvoke) -> Result> { + let result = || -> Result<_> { + let module = self.registry.lookup_module(&invoke.module)?; + let func_type = module.get_func_type(invoke.name)?; + let params = func_type + .params() + .iter() + .map(|t| t.try_into()) + .collect::>>()?; + self.check_type_slices("invoke", TypeContext::Argument, ¶ms, &invoke.args)?; + func_type.results().iter().map(|t| t.try_into()).collect() + }; + result().map_err(|mut e| { + e.set_span(invoke.span); + e + }) + } + + fn check_result_types(&self, exec: &WastExecute, assert_exprs: &[wast::WastRet]) -> Result<()> { + let result_types = self.check_types(exec)?; + self.check_type_slices("action", TypeContext::Result, &result_types, assert_exprs) + } + + fn check_type_slices( + &self, + desc: &str, + context: TypeContext, + expected: &[WastJsonValueType], + exprs: &[T], + ) -> Result<()> { + if exprs.len() != expected.len() { + return Err(Error::new(ErrorKind::WrongNumberOfTypes( + context, + exprs.len(), + expected.len(), + ))); + } + for (index, (expr, &expected_type)) in exprs.iter().zip(expected.iter()).enumerate() { + let expr_type = expr.get_type()?; + if expr_type != expected_type { + return Err(Error::new(ErrorKind::WrongType( + desc.to_string(), + context, + index, + expr_type, + expected_type, + ))); + } + } + Ok(()) + } +} + +fn id_to_name(id: &Id) -> String { + format!("${}", id.name()) +} + +trait GetType { + fn get_type(&self) -> Result; +} + +impl GetType for WastRetCore<'_> { + fn get_type(&self) -> Result { + Ok(match self { + WastRetCore::I32(_) => WastJsonValueType::I32, + WastRetCore::I64(_) => WastJsonValueType::I64, + WastRetCore::F32(_) => WastJsonValueType::F32, + WastRetCore::F64(_) => WastJsonValueType::F64, + WastRetCore::V128(_) => WastJsonValueType::V128, + WastRetCore::RefFunc(_) => WastJsonValueType::FuncRef, + WastRetCore::RefNull(Some(HeapType::Func)) => WastJsonValueType::FuncRef, + WastRetCore::RefExtern(_) => WastJsonValueType::ExternRef, + WastRetCore::RefNull(Some(HeapType::Extern)) => WastJsonValueType::ExternRef, + WastRetCore::Either(_) => unsupported!("either"), + _ => unsupported!("gc"), + }) + } +} + +impl GetType for WastRet<'_> { + fn get_type(&self) -> Result { + match self { + WastRet::Core(core) => core.get_type(), + WastRet::Component(_) => unsupported!("components"), + } + } +} + +impl GetType for WastArgCore<'_> { + fn get_type(&self) -> Result { + Ok(match self { + WastArgCore::I32(_) => WastJsonValueType::I32, + WastArgCore::I64(_) => WastJsonValueType::I64, + WastArgCore::F32(_) => WastJsonValueType::F32, + WastArgCore::F64(_) => WastJsonValueType::F64, + WastArgCore::V128(_) => WastJsonValueType::V128, + WastArgCore::RefNull(HeapType::Func) => WastJsonValueType::FuncRef, + WastArgCore::RefExtern(_) => WastJsonValueType::ExternRef, + WastArgCore::RefNull(HeapType::Extern) => WastJsonValueType::ExternRef, + _ => unsupported!("gc"), + }) + } +} + +impl GetType for WastArg<'_> { + fn get_type(&self) -> Result { + match self { + WastArg::Core(core) => core.get_type(), + WastArg::Component(_) => unsupported!("components"), + } + } +} + +impl TryFrom<&ValType> for WastJsonValueType { + type Error = Error; + + fn try_from(valtype: &ValType) -> Result { + Ok(match valtype { + ValType::I32 => WastJsonValueType::I32, + ValType::I64 => WastJsonValueType::I64, + ValType::F32 => WastJsonValueType::F32, + ValType::F64 => WastJsonValueType::F64, + ValType::V128 => WastJsonValueType::V128, + ValType::Ref(r) if r.is_func_ref() => WastJsonValueType::FuncRef, + ValType::Ref(r) if r.is_extern_ref() => WastJsonValueType::ExternRef, + ValType::Ref(_) => unsupported!("reference types"), + }) + } +} + +impl<'a> TryFrom<&WastRet<'a>> for WastJsonExpected { + type Error = Error; + + fn try_from(value: &WastRet<'a>) -> Result { + match value { + WastRet::Core(ret) => ret.try_into(), + WastRet::Component(_) => unsupported!("components"), + } + } +} + +impl<'a> TryFrom<&WastArg<'a>> for WastJsonConst { + type Error = Error; + + fn try_from(value: &WastArg<'a>) -> Result { + match value { + WastArg::Core(arg) => arg.try_into(), + WastArg::Component(_) => unsupported!("components"), + } + } +} + +impl<'a> TryFrom<&WastRetCore<'a>> for WastJsonExpected { + type Error = Error; + + fn try_from(value: &WastRetCore<'a>) -> Result { + Ok(match value { + WastRetCore::I32(val) => WastJsonExpected::I32 { value: *val }, + WastRetCore::I64(val) => WastJsonExpected::I64 { value: *val }, + WastRetCore::F32(val) => WastJsonExpected::F32 { + value: val.clone2(), + }, + WastRetCore::F64(val) => WastJsonExpected::F64 { + value: val.clone2(), + }, + WastRetCore::V128(val) => WastJsonExpected::V128(val.into()), + WastRetCore::RefNull(Some(HeapType::Func)) => WastJsonExpected::FuncRef { + value: RefIndex::Null, + }, + WastRetCore::RefNull(Some(HeapType::Extern)) => WastJsonExpected::ExternRef { + value: RefIndex::Null, + }, + WastRetCore::RefExtern(Some(val)) => WastJsonExpected::ExternRef { + value: RefIndex::Index(*val), + }, + WastRetCore::Either(..) => unsupported!("either"), + _ => unsupported!("gc"), + }) + } +} + +impl<'a> TryFrom<&WastArgCore<'a>> for WastJsonConst { + type Error = Error; + + fn try_from(value: &WastArgCore<'a>) -> Result { + Ok(match value { + WastArgCore::I32(val) => WastJsonConst::I32 { value: *val }, + WastArgCore::I64(val) => WastJsonConst::I64 { value: *val }, + WastArgCore::F32(val) => WastJsonConst::F32 { value: *val }, + WastArgCore::F64(val) => WastJsonConst::F64 { value: *val }, + WastArgCore::V128(val) => WastJsonConst::V128(val.into()), + WastArgCore::RefNull(HeapType::Func) => WastJsonConst::FuncRef { + value: RefIndex::Null, + }, + WastArgCore::RefNull(HeapType::Extern) => WastJsonConst::ExternRef { + value: RefIndex::Null, + }, + WastArgCore::RefExtern(val) => WastJsonConst::ExternRef { + value: RefIndex::Index(*val), + }, + _ => unsupported!("gc"), + }) } } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..50a8db8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +use clap::Parser; +use std::{collections::HashMap, env, fs, path::PathBuf}; +use wast2json::{wast2json, Wast2JsonOptions}; + +#[derive(Parser, Debug)] +#[command(about, version)] +struct Args { + #[command(flatten)] + verbose: clap_verbosity_flag::Verbosity, + + #[arg( + short = 'p', + long = "pretty-print", + group = "json", + help = "Pretty-print JSON output" + )] + pretty_print: bool, + + #[arg( + short = 'c', + long = "compact", + group = "json", + help = "Compact JSON output" + )] + compact: bool, + + #[arg(short, long, value_parser, help = "Output .json file")] + output: Option, + + #[arg(value_parser, help = "Input .wast file")] + input: PathBuf, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + env_logger::builder() + .filter_level(args.verbose.log_level_filter()) + .parse_default_env() + .init(); + + log::debug!("Arguments: {:?}", args); + + let input = &args.input; + log::debug!("Input file: {:?}", input); + + let output = match args.output { + Some(output) => output, + None => env::current_dir()? + .join(input.file_name().unwrap_or_default()) + .with_extension("json"), + }; + log::debug!("Output file: {:?}", output); + + let module_basename = output.file_stem().unwrap_or_default().to_string_lossy(); + + let source = fs::read_to_string(input)?; + let options = Wast2JsonOptions::default(); + let mut module_files = HashMap::new(); + + let script = wast2json( + input, + &source, + &module_basename, + &mut module_files, + &options, + )?; + + log::debug!("Writing {:?}", output); + let file = std::fs::File::create(&output)?; + if args.compact { + script.write_compact(file)?; + } else if args.pretty_print { + script.write_pretty(file)?; + } else { + script.write(file)?; + } + + for (filename, bytes) in module_files.iter() { + let filename = output.with_file_name(filename); + fs::write(filename, bytes)?; + } + + Ok(()) +} diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..73381f5 --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,125 @@ +use crate::error::{Error, ErrorKind, Result}; +use std::{collections::HashMap, rc::Rc}; +use wasmparser::{ExternalKind, FuncType, Payload, ValType}; +use wast::token::Id; + +/// Module registry +pub struct ModuleRegistry { + named: HashMap>, + most_recent: Option>, +} + +/// A disassembled wasm module +pub struct ModuleExports(HashMap); + +#[derive(Clone)] +pub enum Export { + Global(ValType), + Func(FuncType), + Other, +} + +impl ModuleRegistry { + pub fn new() -> Self { + Self { + named: HashMap::new(), + most_recent: None, + } + } + + pub fn define(&mut self, name: &Option, bytes: &[u8]) -> Result<()> { + let exports = Rc::new(ModuleExports::parse(bytes)?); + if let Some(id) = name { + self.named.insert(id.name().into(), exports.clone()); + } + self.most_recent.replace(exports); + Ok(()) + } + + pub fn lookup_module(&self, name: &Option) -> Result<&ModuleExports> { + let module = match name { + Some(id) => self.named.get(id.name()), + None => self.most_recent.as_ref(), + }; + module + .map(|m| m.as_ref()) + .ok_or_else(|| Error::new(ErrorKind::UnknownModule)) + } +} + +impl ModuleExports { + pub fn parse(bytes: &[u8]) -> Result { + let mut types: Vec = Vec::new(); + let mut funcs: Vec = Vec::new(); + let mut globals: Vec = Vec::new(); + let mut exports: HashMap = HashMap::new(); + + for item in wasmparser::Parser::new(0).parse_all(bytes) { + match item? { + Payload::TypeSection(s) => { + types.reserve(s.count() as usize); + for ty in s.into_iter_err_on_gc_types() { + types.push(ty.map_or(Export::Other, Export::Func)); + } + } + Payload::ImportSection(s) => { + for import in s { + let ty = import?.ty; + if let wasmparser::TypeRef::Func(f) = ty { + funcs.push(f); + } else if let wasmparser::TypeRef::Global(g) = ty { + globals.push(g.content_type); + } + } + } + Payload::FunctionSection(s) => { + types.reserve_exact(s.count() as usize); + for f in s { + funcs.push(f?); + } + } + Payload::GlobalSection(s) => { + for g in s { + globals.push(g?.ty.content_type); + } + } + Payload::ExportSection(s) => { + for export in s { + let export = export?; + let x = match export.kind { + ExternalKind::Func => funcs + .get(export.index as usize) + .and_then(|&i| types.get(i as usize)) + .cloned() + .unwrap_or(Export::Other), + ExternalKind::Global => globals + .get(export.index as usize) + .map_or(Export::Other, |&t| Export::Global(t)), + _ => Export::Other, + }; + exports.insert(export.name.to_string(), x); + } + } + _ => (), + } + } + + Ok(ModuleExports(exports)) + } + + pub fn get_func_type(&self, name: &str) -> Result<&FuncType> { + if let Some(Export::Func(ref t)) = self.0.get(name) { + Ok(t) + } else { + Err(Error::new(ErrorKind::UnknownFuncExport(name.into()))) + } + } + + pub fn get_global_type(&self, name: &str) -> Result<&ValType> { + if let Some(Export::Global(ref t)) = self.0.get(name) { + Ok(t) + } else { + Err(Error::new(ErrorKind::UnknownGlobalExport(name.into()))) + } + } +} diff --git a/tests/local/module-binary.txt b/tests/local/module-binary.txt new file mode 100644 index 0000000..d1a310f --- /dev/null +++ b/tests/local/module-binary.txt @@ -0,0 +1,13 @@ +;;; TOOL: wast2json +(module binary + "\00asm" "\01\00\00\00" + "\04\04\01" ;; Table section with 1 entry + "\70\00\00" ;; no max, minimum 0, funcref + "\09\09\01" ;; Element section with 1 entry + "\02" ;; Element with explicit table index + "\80\00" ;; Table index 0, encoded with 2 bytes + "\41\00\0b\00\00" ;; (i32.const 0) with no elements +) +(;; BINARY module-binary.0.wasm ;;; +"\00asm\01\00\00\00\04\04\01\70\00\00\09\09\01\02\80\00\41\00\0b\00\00" +;;; BINARY module-binary.0.wasm ;;) diff --git a/tests/local/parse/assert/assert-after-module.txt b/tests/local/parse/assert/assert-after-module.txt new file mode 100644 index 0000000..cd20db9 --- /dev/null +++ b/tests/local/parse/assert/assert-after-module.txt @@ -0,0 +1,23 @@ +;;; TOOL: wast2json +(module + (export "f" (func 0)) + (func (result i32) + i32.const 0 + return)) +(assert_return (invoke "f") (i32.const 0)) +(;; OUTPUT assert-after-module.json ;;; +{"source_filename": "tests/local/parse/assert/assert-after-module.txt", + "commands": [ + {"type": "module", "line": 2, "filename": "assert-after-module.0.wasm"}, + {"type": "assert_return", "line": 7, "action": {"type": "invoke", "field": "f", "args": []}, "expected": [{"type": "i32", "value": "0"}]}]} +;;; OUTPUT assert-after-module.json ;;) +(;; DUMP assert-after-module.0.wasm ;;; +(module + (type (;0;) (func (result i32))) + (func (;0;) (type 0) (result i32) + i32.const 0 + return + ) + (export "f" (func 0)) +) +;;; DUMP assert-after-module.0.wasm ;;) diff --git a/tests/local/parse/assert/assertinvalid-binary-module.txt b/tests/local/parse/assert/assertinvalid-binary-module.txt new file mode 100644 index 0000000..79461d0 --- /dev/null +++ b/tests/local/parse/assert/assertinvalid-binary-module.txt @@ -0,0 +1,15 @@ +;;; TOOL: wast2json +(assert_invalid (module binary "\00ASM") "bad magic") +(module) +(;; OUTPUT assertinvalid-binary-module.json ;;; +{"source_filename": "tests/local/parse/assert/assertinvalid-binary-module.txt", + "commands": [ + {"type": "assert_invalid", "line": 2, "filename": "assertinvalid-binary-module.0.wasm", "text": "bad magic", "module_type": "binary"}, + {"type": "module", "line": 3, "filename": "assertinvalid-binary-module.1.wasm"}]} +;;; OUTPUT assertinvalid-binary-module.json ;;) +(;; BINARY assertinvalid-binary-module.0.wasm ;;; +"\00ASM" +;;; BINARY assertinvalid-binary-module.0.wasm ;;) +(;; DUMP assertinvalid-binary-module.1.wasm ;;; +(module) +;;; DUMP assertinvalid-binary-module.1.wasm ;;) diff --git a/tests/local/parse/assert/assertinvalid.txt b/tests/local/parse/assert/assertinvalid.txt new file mode 100644 index 0000000..0c093a6 --- /dev/null +++ b/tests/local/parse/assert/assertinvalid.txt @@ -0,0 +1,21 @@ +;;; TOOL: wast2json +(assert_invalid + (module + (func + i32.const 1 + drop) + (export "foo" (func 1))) + "unknown function 1") ;; string is ignored +(assert_invalid + (module + (func (result i32) + nop)) + "type mismatch") +(;; OUTPUT assertinvalid.json ;;; +{"source_filename": "tests/local/parse/assert/assertinvalid.txt", + "commands": [ + {"type": "assert_invalid", "line": 2, "filename": "assertinvalid.0.wasm", "text": "unknown function 1", "module_type": "binary"}, + {"type": "assert_invalid", "line": 9, "filename": "assertinvalid.1.wasm", "text": "type mismatch", "module_type": "binary"}]} +;;; OUTPUT assertinvalid.json ;;) +;;; IGNORE assertinvalid.0.wasm +;;; IGNORE assertinvalid.1.wasm diff --git a/tests/local/parse/assert/assertmalformed.txt b/tests/local/parse/assert/assertmalformed.txt new file mode 100644 index 0000000..0b1c024 --- /dev/null +++ b/tests/local/parse/assert/assertmalformed.txt @@ -0,0 +1,13 @@ +;;; TOOL: wast2json +(assert_malformed + (module binary + "\00asm\bc\0a\00\00") + "unknown binary version") +(;; OUTPUT assertmalformed.json ;;; +{"source_filename": "tests/local/parse/assert/assertmalformed.txt", + "commands": [ + {"type": "assert_malformed", "line": 2, "filename": "assertmalformed.0.wasm", "text": "unknown binary version", "module_type": "binary"}]} +;;; OUTPUT assertmalformed.json ;;) +(;; BINARY assertmalformed.0.wasm ;;; +"\00asm\bc\0a\00\00" +;;; BINARY assertmalformed.0.wasm ;;) diff --git a/tests/local/parse/assert/assertreturn-arithmetic-nan.txt b/tests/local/parse/assert/assertreturn-arithmetic-nan.txt new file mode 100644 index 0000000..ef3005e --- /dev/null +++ b/tests/local/parse/assert/assertreturn-arithmetic-nan.txt @@ -0,0 +1,26 @@ +;;; TOOL: wast2json +(module + (func $foo (param f32) (result f32) + local.get 0 + f32.const 0 + f32.div) + (export "foo" (func $foo))) + +(assert_return (invoke "foo" (f32.const 0)) (f32.const nan:arithmetic)) +(;; OUTPUT assertreturn-arithmetic-nan.json ;;; +{"source_filename": "tests/local/parse/assert/assertreturn-arithmetic-nan.txt", + "commands": [ + {"type": "module", "line": 2, "filename": "assertreturn-arithmetic-nan.0.wasm"}, + {"type": "assert_return", "line": 9, "action": {"type": "invoke", "field": "foo", "args": [{"type": "f32", "value": "0"}]}, "expected": [{"type": "f32", "value": "nan:arithmetic"}]}]} +;;; OUTPUT assertreturn-arithmetic-nan.json ;;) +(;; DUMP assertreturn-arithmetic-nan.0.wasm ;;; +(module + (type (;0;) (func (param f32) (result f32))) + (func $foo (;0;) (type 0) (param f32) (result f32) + local.get 0 + f32.const 0x0p+0 (;=0;) + f32.div + ) + (export "foo" (func $foo)) +) +;;; DUMP assertreturn-arithmetic-nan.0.wasm ;;) diff --git a/tests/local/parse/assert/assertreturn-canonical-nan.txt b/tests/local/parse/assert/assertreturn-canonical-nan.txt new file mode 100644 index 0000000..63be0f3 --- /dev/null +++ b/tests/local/parse/assert/assertreturn-canonical-nan.txt @@ -0,0 +1,26 @@ +;;; TOOL: wast2json +(module + (func $foo (param f32) (result f32) + local.get 0 + f32.const 0 + f32.div) + (export "foo" (func $foo))) + +(assert_return (invoke "foo" (f32.const 0)) (f32.const nan:canonical)) +(;; OUTPUT assertreturn-canonical-nan.json ;;; +{"source_filename": "tests/local/parse/assert/assertreturn-canonical-nan.txt", + "commands": [ + {"type": "module", "line": 2, "filename": "assertreturn-canonical-nan.0.wasm"}, + {"type": "assert_return", "line": 9, "action": {"type": "invoke", "field": "foo", "args": [{"type": "f32", "value": "0"}]}, "expected": [{"type": "f32", "value": "nan:canonical"}]}]} +;;; OUTPUT assertreturn-canonical-nan.json ;;) +(;; DUMP assertreturn-canonical-nan.0.wasm ;;; +(module + (type (;0;) (func (param f32) (result f32))) + (func $foo (;0;) (type 0) (param f32) (result f32) + local.get 0 + f32.const 0x0p+0 (;=0;) + f32.div + ) + (export "foo" (func $foo)) +) +;;; DUMP assertreturn-canonical-nan.0.wasm ;;) diff --git a/tests/local/parse/assert/assertreturn.txt b/tests/local/parse/assert/assertreturn.txt new file mode 100644 index 0000000..1883f93 --- /dev/null +++ b/tests/local/parse/assert/assertreturn.txt @@ -0,0 +1,30 @@ +;;; TOOL: wast2json +(module + (func $foo (result i32) i32.const 0) + (export "foo" (func $foo)) + (func $bar (param f32) (result f32) + local.get 0) + (export "bar" (func $bar))) + +(assert_return (invoke "foo") (i32.const 0)) +(assert_return (invoke "bar" (f32.const 0)) (f32.const 0)) +--- output: assertreturn.json +{"source_filename": "tests/local/parse/assert/assertreturn.txt", + "commands": [ + {"type": "module", "line": 2, "filename": "assertreturn.0.wasm"}, + {"type": "assert_return", "line": 9, "action": {"type": "invoke", "field": "foo", "args": []}, "expected": [{"type": "i32", "value": "0"}]}, + {"type": "assert_return", "line": 10, "action": {"type": "invoke", "field": "bar", "args": [{"type": "f32", "value": "0"}]}, "expected": [{"type": "f32", "value": "0"}]}]} +--- dump: assertreturn.0.wasm +(module + (type (;0;) (func (result i32))) + (type (;1;) (func (param f32) (result f32))) + (func $foo (;0;) (type 0) (result i32) + i32.const 0 + ) + (func $bar (;1;) (type 1) (param f32) (result f32) + local.get 0 + ) + (export "foo" (func $foo)) + (export "bar" (func $bar)) +) +--- diff --git a/tests/local/parse/assert/bad-assert-before-module.txt b/tests/local/parse/assert/bad-assert-before-module.txt new file mode 100644 index 0000000..b35d3d1 --- /dev/null +++ b/tests/local/parse/assert/bad-assert-before-module.txt @@ -0,0 +1,8 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(assert_return (invoke "f") (i32.const 0)) +(;; STDERR ;;; +tests/local/parse/assert/bad-assert-before-module.txt:3:17: error: unknown module +(assert_return (invoke "f") (i32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-assertreturn-non-const.txt b/tests/local/parse/assert/bad-assertreturn-non-const.txt new file mode 100644 index 0000000..b2d20bd --- /dev/null +++ b/tests/local/parse/assert/bad-assertreturn-non-const.txt @@ -0,0 +1,17 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func $bar (param f32) (result f32) + local.get 0) + (export "bar" (func $bar))) + +;; NOT ok to use more complex exprs +(assert_return + (invoke "bar" + (f32.add (f32.const 1) (f32.const 10))) + (f32.const 11)) +(;; STDERR ;;; +tests/local/parse/assert/bad-assertreturn-non-const.txt:11:6: error: expected a [type].const expression + (f32.add (f32.const 1) (f32.const 10))) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-assertreturn-too-few.txt b/tests/local/parse/assert/bad-assertreturn-too-few.txt new file mode 100644 index 0000000..97ea239 --- /dev/null +++ b/tests/local/parse/assert/bad-assertreturn-too-few.txt @@ -0,0 +1,12 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func $foo (param i32) (result i32) + local.get 0) + (export "foo" (func $foo))) +(assert_return (invoke "foo") (i32.const 0)) +(;; STDERR ;;; +tests/local/parse/assert/bad-assertreturn-too-few.txt:7:17: error: too few arguments to function. got 0, expected 1 +(assert_return (invoke "foo") (i32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-assertreturn-too-many.txt b/tests/local/parse/assert/bad-assertreturn-too-many.txt new file mode 100644 index 0000000..a203a63 --- /dev/null +++ b/tests/local/parse/assert/bad-assertreturn-too-many.txt @@ -0,0 +1,12 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func $foo (result i32) + i32.const 0) + (export "foo" (func $foo))) +(assert_return (invoke "foo" (i32.const 0)) (i32.const 0)) +(;; STDERR ;;; +tests/local/parse/assert/bad-assertreturn-too-many.txt:7:17: error: too many arguments to function. got 1, expected 0 +(assert_return (invoke "foo" (i32.const 0)) (i32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-assertreturn-unknown-function.txt b/tests/local/parse/assert/bad-assertreturn-unknown-function.txt new file mode 100644 index 0000000..18a4c8b --- /dev/null +++ b/tests/local/parse/assert/bad-assertreturn-unknown-function.txt @@ -0,0 +1,9 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module) +(assert_return (invoke "foo") (i32.const 0)) +(;; STDERR ;;; +tests/local/parse/assert/bad-assertreturn-unknown-function.txt:4:17: error: unknown function export "foo" +(assert_return (invoke "foo") (i32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-invoke-no-module.txt b/tests/local/parse/assert/bad-invoke-no-module.txt new file mode 100644 index 0000000..6a5ed65 --- /dev/null +++ b/tests/local/parse/assert/bad-invoke-no-module.txt @@ -0,0 +1,8 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(invoke "foo") +(;; STDERR ;;; +tests/local/parse/assert/bad-invoke-no-module.txt:3:2: error: unknown module +(invoke "foo") + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-invoke-too-few.txt b/tests/local/parse/assert/bad-invoke-too-few.txt new file mode 100644 index 0000000..ac0576f --- /dev/null +++ b/tests/local/parse/assert/bad-invoke-too-few.txt @@ -0,0 +1,11 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func (param i32)) + (export "foo" (func 0))) +(invoke "foo") +(;; STDERR ;;; +tests/local/parse/assert/bad-invoke-too-few.txt:6:2: error: too few arguments to function. got 0, expected 1 +(invoke "foo") + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-invoke-too-many.txt b/tests/local/parse/assert/bad-invoke-too-many.txt new file mode 100644 index 0000000..7a88018 --- /dev/null +++ b/tests/local/parse/assert/bad-invoke-too-many.txt @@ -0,0 +1,11 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func (param i32)) + (export "foo" (func 0))) +(invoke "foo" (i32.const 0) (i32.const 1)) +(;; STDERR ;;; +tests/local/parse/assert/bad-invoke-too-many.txt:6:2: error: too many arguments to function. got 2, expected 1 +(invoke "foo" (i32.const 0) (i32.const 1)) + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/bad-invoke-unknown-function.txt b/tests/local/parse/assert/bad-invoke-unknown-function.txt new file mode 100644 index 0000000..9d8e83c --- /dev/null +++ b/tests/local/parse/assert/bad-invoke-unknown-function.txt @@ -0,0 +1,12 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (export "foo" (func $foo)) + (func $foo)) + +(invoke "bar") +(;; STDERR ;;; +tests/local/parse/assert/bad-invoke-unknown-function.txt:7:2: error: unknown function export "bar" +(invoke "bar") + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/assert/invoke.txt b/tests/local/parse/assert/invoke.txt new file mode 100644 index 0000000..224eb27 --- /dev/null +++ b/tests/local/parse/assert/invoke.txt @@ -0,0 +1,30 @@ +;;; TOOL: wast2json +(module + (export "test" (func $test)) + (func $test (param i32) (result i32) + local.get 0 + i32.const 100 + i32.add)) + +(invoke "test" (i32.const 1)) +(invoke "test" (i32.const 100)) +(invoke "test" (i32.const -30)) +(;; OUTPUT invoke.json ;;; +{"source_filename": "tests/local/parse/assert/invoke.txt", + "commands": [ + {"type": "module", "line": 2, "filename": "invoke.0.wasm"}, + {"type": "action", "line": 9, "action": {"type": "invoke", "field": "test", "args": [{"type": "i32", "value": "1"}]}, "expected": [{"type": "i32"}]}, + {"type": "action", "line": 10, "action": {"type": "invoke", "field": "test", "args": [{"type": "i32", "value": "100"}]}, "expected": [{"type": "i32"}]}, + {"type": "action", "line": 11, "action": {"type": "invoke", "field": "test", "args": [{"type": "i32", "value": "4294967266"}]}, "expected": [{"type": "i32"}]}]} +;;; OUTPUT invoke.json ;;) +(;; DUMP invoke.0.wasm ;;; +(module + (type (;0;) (func (param i32) (result i32))) + (func $test (;0;) (type 0) (param i32) (result i32) + local.get 0 + i32.const 100 + i32.add + ) + (export "test" (func $test)) +) +;;; DUMP invoke.0.wasm ;;) \ No newline at end of file diff --git a/tests/local/parse/bad-input-command.txt b/tests/local/parse/bad-input-command.txt new file mode 100644 index 0000000..0886375 --- /dev/null +++ b/tests/local/parse/bad-input-command.txt @@ -0,0 +1,10 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +;; syntax is (input name? text) +(input "hello") +(input $var "hello") +(;; STDERR ;;; +tests/local/parse/bad-input-command.txt:4:2: error: expected valid module field +(input "hello") + ^ +;;; STDERR ;;) diff --git a/tests/local/parse/bad-output-command.txt b/tests/local/parse/bad-output-command.txt new file mode 100644 index 0000000..c7ab9d0 --- /dev/null +++ b/tests/local/parse/bad-output-command.txt @@ -0,0 +1,12 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +;; syntax is (output name? text?) +(output) +(output "hello") +(output $var) +(output $var "hello") +(;; STDERR ;;; +tests/local/parse/bad-output-command.txt:4:2: error: expected valid module field +(output) + ^ +;;; STDERR ;;) diff --git a/tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt b/tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt new file mode 100644 index 0000000..f6d1179 --- /dev/null +++ b/tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt @@ -0,0 +1,11 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func (param i32) (result i32) local.get 0) + (export "foo" (func 0))) +(assert_return (invoke "foo" (f32.const 0)) (i32.const 0)) +(;; STDERR ;;; +tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt:6:17: error: type mismatch for argument 0 of invoke. got f32, expected i32 +(assert_return (invoke "foo" (f32.const 0)) (i32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/typecheck/bad-assertreturn-type-mismatch.txt b/tests/local/typecheck/bad-assertreturn-type-mismatch.txt new file mode 100644 index 0000000..354394f --- /dev/null +++ b/tests/local/typecheck/bad-assertreturn-type-mismatch.txt @@ -0,0 +1,11 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func (param i32) (result i32) local.get 0) + (export "foo" (func 0))) +(assert_return (invoke "foo" (i32.const 0)) (f32.const 0)) +(;; STDERR ;;; +tests/local/typecheck/bad-assertreturn-type-mismatch.txt:6:2: error: type mismatch for result 0 of action. got f32, expected i32 +(assert_return (invoke "foo" (i32.const 0)) (f32.const 0)) + ^ +;;; STDERR ;;) diff --git a/tests/local/typecheck/bad-invoke-type-mismatch.txt b/tests/local/typecheck/bad-invoke-type-mismatch.txt new file mode 100644 index 0000000..99e8f8b --- /dev/null +++ b/tests/local/typecheck/bad-invoke-type-mismatch.txt @@ -0,0 +1,11 @@ +;;; TOOL: wast2json +;;; ERROR: 1 +(module + (func (param i32)) + (export "foo" 0)) +(invoke "foo" (f32.const 1.5)) +(;; STDERR ;;; +tests/local/typecheck/bad-invoke-type-mismatch.txt:5:17: error: expected `(` + (export "foo" 0)) + ^ +;;; STDERR ;;) diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..9b87f50 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,150 @@ +use assert_json_diff::assert_json_eq; +use std::{collections::HashMap, str::FromStr}; +use wast::lexer::{Lexer, TokenKind}; +use wast2json::{wast2json, Wast2JsonOptions}; + +macro_rules! test_wast { + ( $( $testfile:literal as $name:ident ,)* ) => { + $( + #[test] + fn $name() { + run_wast_test($testfile); + } + )* + }; +} + +test_wast! { + "tests/local/parse/assert/assert-after-module.txt" as assert_after_module, + "tests/local/parse/assert/assertreturn-arithmetic-nan.txt" as assertreturn_arithmetic_nan, + "tests/local/parse/assert/assertreturn-canonical-nan.txt" as assertreturn_canonical_nan, + "tests/local/parse/assert/assertinvalid-binary-module.txt" as assertinvalid_binary_module, + "tests/local/parse/assert/assertinvalid.txt" as assertinvalid, + "tests/local/parse/assert/assertmalformed.txt" as assertmalformed, + "tests/local/parse/assert/assertreturn.txt" as assertreturn, + "tests/local/parse/assert/bad-assert-before-module.txt" as bad_assert_before_module, + "tests/local/parse/assert/bad-assertreturn-non-const.txt" as bad_assertreturn_non_const, + "tests/local/parse/assert/bad-assertreturn-too-few.txt" as bad_assertreturn_too_few, + "tests/local/parse/assert/bad-assertreturn-too-many.txt" as bad_assertreturn_too_many, + "tests/local/parse/assert/bad-assertreturn-unknown-function.txt" as bad_assertreturn_unknown_function, + "tests/local/parse/assert/bad-invoke-no-module.txt" as bad_invoke_no_module, + "tests/local/parse/assert/bad-invoke-too-few.txt" as bad_invoke_too_few, + "tests/local/parse/assert/bad-invoke-too-many.txt" as bad_invoke_too_many, + "tests/local/parse/assert/bad-invoke-unknown-function.txt" as bad_invoke_unknown_function, + "tests/local/parse/assert/invoke.txt" as invoke, + "tests/local/parse/bad-input-command.txt" as bad_input_command, + "tests/local/parse/bad-output-command.txt" as bad_output_command, + "tests/local/typecheck/bad-assertreturn-invoke-type-mismatch.txt" as bad_assertreturn_invoke_type_mismatch, + "tests/local/typecheck/bad-assertreturn-type-mismatch.txt" as bad_assertreturn_type_mismatch, + "tests/local/typecheck/bad-invoke-type-mismatch.txt" as bad_invoke_type_mismatch, + "tests/local/module-binary.txt" as module_binary, +} + +#[derive(Debug)] +struct TestCommand { + key: String, + arg: Option, + payload: String, +} +fn run_wast_test(testfile: &str) { + let filename = std::path::PathBuf::from_str(testfile).expect("path"); + let content = std::fs::read_to_string(&filename).unwrap(); + let options = Wast2JsonOptions::default(); + let mut cmds = parse_test_commands(&content); + let mut module_files = HashMap::new(); + let mut result = None; + for cmd in cmds.iter_mut() { + match cmd.key.as_str() { + "TOOL" => { + assert_eq!(Some("wast2json"), cmd.arg.as_deref(), "tool"); + result = Some(wast2json( + &filename, + &content, + &filename.file_stem().unwrap_or_default().to_string_lossy(), + &mut module_files, + &options, + )); + } + "ERROR" => { + assert_eq!(Some("1"), cmd.arg.as_deref()); + assert!(result.as_ref().filter(|r| r.is_err()).is_some(), "error"); + } + "STDERR" => { + let result = result.as_ref().expect("result"); + assert!(result.is_err(), "error expected: {}", cmd.payload); + assert_eq!( + cmd.payload.trim_end(), + format!("{}", result.as_ref().err().unwrap()), + "error message" + ); + } + "OUTPUT" => { + let result = result.as_ref().expect("result"); + if let Err(ref e) = result { + panic!("unexpected error: {}", e); + } + eprintln!("{}", &cmd.payload); + let json = serde_json::from_str::(&cmd.payload).unwrap(); + assert_json_eq!(json, result.as_ref().unwrap()); + } + "IGNORE" | "BINARY" | "DUMP" => { + let arg = cmd.arg.as_ref().unwrap(); + let module = module_files.remove(arg).expect(arg); + if cmd.key == "DUMP" { + let dump = wasmprinter::print_bytes(module).unwrap(); + assert_eq!(&dump, &cmd.payload, "dump {}", arg); + } else if cmd.key == "BINARY" { + let lexer = Lexer::new(&cmd.payload); + let token = lexer.iter(0).next().expect("first token").expect("token"); + if token.kind != TokenKind::String { + panic!("Byte string expected"); + }; + let bytes = token.string(&cmd.payload); + assert_eq!(bytes, module, "binary {}", arg); + } + } + _ => panic!("{}: Unsupported cmd '{}'", testfile, cmd.key), + } + } + + if result.expect("result").is_ok() { + assert!(module_files.is_empty()); + } +} + +fn parse_test_commands(content: &str) -> Vec { + let mut cmds = Vec::new(); + + for token in Lexer::new(content).iter(0) { + let token = token.expect("token"); + let cmd = match token.kind { + TokenKind::LineComment => token.src(content).strip_prefix(";;;"), + TokenKind::BlockComment => token.src(content).strip_prefix("(;;"), + _ => None, + }; + if let Some(cmd) = cmd { + let mut lines = cmd.lines(); + let first_line = lines.next().unwrap(); + let mut payload: &str = &lines.collect::>().join("\n"); + + let first_line = first_line.trim_end_matches(";;;").trim(); + let (key, arg) = if let Some((k, a)) = first_line.split_once([':', ' ']) { + (k.trim_end().to_string(), Some(a.trim_start().to_string())) + } else { + (first_line.to_string(), None) + }; + + if payload.ends_with(";;)") { + let suffix = format!(";;; {} ;;)", first_line); + payload = payload.strip_suffix(&suffix).expect(&suffix); + } + cmds.push(TestCommand { + key, + arg, + payload: payload.to_string(), + }); + } + } + + cmds +}