diff --git a/.cargo/config.toml b/.cargo/config.toml index 743421a..3e15584 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,6 +2,6 @@ version-dev="workspaces version --no-git-commit --force tinywasm*" dev="run -- -l debug run" -test-mvp="test --package tinywasm --test test-mvp" -test-wast="test --package tinywasm --test test-wast --" -generate-charts="test --package tinywasm --test generate-charts" +test-mvp="test --package tinywasm --test test-mvp --release -- --enable " +test-wast="test --package tinywasm --test test-wast -- --enable " +generate-charts="test --package tinywasm --test generate-charts -- --enable " diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c4ae296..0baaba9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Install stable Rust toolchain run: | @@ -20,10 +22,10 @@ jobs: rustup default stable - name: Build (stable) - run: cargo +stable build --verbose + run: cargo +stable build --workspace --exclude wasm-testsuite - name: Run tests (stable) - run: cargo +stable test --verbose + run: cargo +stable test --workspace --exclude wasm-testsuite test-no-std: name: Test without default features on nightly Rust @@ -37,7 +39,7 @@ jobs: rustup default nightly - name: Build (nightly, no default features) - run: cargo +nightly build --verbose --no-default-features + run: cargo +nightly build --workspace --exclude wasm-testsuite --no-default-features - name: Run tests (nightly, no default features) - run: cargo +nightly test --verbose --no-default-features + run: cargo +nightly test --workspace --exclude wasm-testsuite --no-default-features diff --git a/.gitignore b/.gitignore index 1e56606..7bb669d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target notes.md examples/tinywasm.wat -examples/wast/* \ No newline at end of file +examples/wast/* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4f9768f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "search.exclude": { + "**/wasm-testsuite/data": true + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index acee43d..0bbe617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", "serde", @@ -310,9 +310,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -525,9 +525,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -586,9 +586,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -636,13 +636,13 @@ checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -680,9 +680,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -719,9 +719,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miniz_oxide" @@ -886,9 +886,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -915,9 +915,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1017,9 +1017,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.1.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810294a8a4a0853d4118e3b94bb079905f2107c7fe979d8f0faae98765eb6378" +checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -1028,22 +1028,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.1.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc144a1273124a67b8c1d7cd19f5695d1878b31569c0512f6086f0f4676604e" +checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.43", + "syn 2.0.48", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.1.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ccd4875431253d6bb54b804bcff4369cbde9bae33defde25fdf6c2ef91d40" +checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" dependencies = [ "globset", "sha2", @@ -1101,35 +1101,35 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1172,9 +1172,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1198,22 +1198,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", ] [[package]] @@ -1233,7 +1233,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tinywasm" -version = "0.1.0" +version = "0.2.0" dependencies = [ "eyre", "log", @@ -1250,7 +1250,7 @@ dependencies = [ [[package]] name = "tinywasm-cli" -version = "0.1.0" +version = "0.2.0" dependencies = [ "argh", "color-eyre", @@ -1262,7 +1262,7 @@ dependencies = [ [[package]] name = "tinywasm-parser" -version = "0.1.0" +version = "0.2.0" dependencies = [ "log", "tinywasm-types", @@ -1271,7 +1271,7 @@ dependencies = [ [[package]] name = "tinywasm-types" -version = "0.1.0" +version = "0.2.0" dependencies = [ "log", "rkyv", @@ -1350,7 +1350,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -1372,7 +1372,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1385,9 +1385,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-encoder" -version = "0.38.1" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad2b51884de9c7f4fe2fd1043fccb8dcad4b1e29558146ee57a144d15779f3f" +checksum = "111495d6204760238512f57a9af162f45086504da332af210f2f75dd80b34f1d" dependencies = [ "leb128", ] @@ -1410,9 +1410,9 @@ dependencies = [ [[package]] name = "wast" -version = "69.0.1" +version = "70.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ee37317321afde358e4d7593745942c48d6d17e0e6e943704de9bbee121e7a" +checksum = "2ee4bc54bbe1c6924160b9f75e374a1d07532e7580eb632c0ee6cdd109bb217e" dependencies = [ "leb128", "memchr", @@ -1469,11 +1469,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bf078c5..90f4e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members=["crates/cli"] resolver="2" [workspace.package] -version="0.1.0" +version="0.2.0" edition="2021" license="MIT OR Apache-2.0" authors=["Henry Gressmann "] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1a23d16..65a9e72 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,12 +14,12 @@ path="src/bin.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tinywasm={version="0.1.0", path="../tinywasm", features=["std", "parser"]} +tinywasm={version="0.2.0-alpha.0", path="../tinywasm", features=["std", "parser"]} argh="0.1" color-eyre={version="0.6", default-features=false} log="0.4" pretty_env_logger="0.5" -wast={version="69.0", optional=true} +wast={version="70.0", optional=true} [features] default=["wat"] diff --git a/crates/cli/src/bin.rs b/crates/cli/src/bin.rs index 2712751..c8ab379 100644 --- a/crates/cli/src/bin.rs +++ b/crates/cli/src/bin.rs @@ -114,10 +114,10 @@ fn main() -> Result<()> { fn run(module: Module, func: Option, args: Vec) -> Result<()> { let mut store = tinywasm::Store::default(); - let instance = module.instantiate(&mut store)?; + let instance = module.instantiate(&mut store, None)?; if let Some(func) = func { - let func = instance.get_func(&store, &func)?; + let func = instance.exported_func_by_name(&store, &func)?; let res = func.call(&mut store, &args)?; info!("{res:?}"); } diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index 29908b6..9e6dc71 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -9,10 +9,9 @@ repository.workspace=true [dependencies] # fork of wasmparser with no_std support, see https://github.com/bytecodealliance/wasmtime/issues/3495 -# TODO: create dependency free parser wasmparser={version="0.100", package="wasmparser-nostd", default-features=false} log={version="0.4", optional=true} -tinywasm-types={version="0.1.0", path="../types"} +tinywasm-types={version="0.2.0-alpha.0", path="../types"} [features] default=["std", "logging"] diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index b962ff4..b9e674e 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -280,6 +280,8 @@ pub(crate) fn convert_memarg(memarg: wasmparser::MemArg) -> MemArg { MemArg { offset: memarg.offset, align: memarg.align, + align_max: memarg.max_align, + mem_addr: memarg.memory, } } @@ -296,6 +298,9 @@ pub(crate) fn process_const_operators(ops: OperatorsReader) -> Result Result { match op { + wasmparser::Operator::RefNull { ty } => Ok(ConstInstruction::RefNull(convert_valtype(&ty))), + wasmparser::Operator::RefFunc { function_index } => Ok(ConstInstruction::RefFunc(function_index)), + wasmparser::Operator::I32Const { value } => Ok(ConstInstruction::I32Const(value)), wasmparser::Operator::I64Const { value } => Ok(ConstInstruction::I64Const(value)), wasmparser::Operator::F32Const { value } => Ok(ConstInstruction::F32Const(f32::from_bits(value.bits()))), // TODO: check if this is correct @@ -414,12 +419,15 @@ pub fn process_operators<'a>( LocalTee { local_index } => Instruction::LocalTee(local_index), GlobalGet { global_index } => Instruction::GlobalGet(global_index), GlobalSet { global_index } => Instruction::GlobalSet(global_index), - MemorySize { .. } => Instruction::MemorySize, - MemoryGrow { .. } => Instruction::MemoryGrow, + MemorySize { mem, mem_byte } => Instruction::MemorySize(mem, mem_byte), + MemoryGrow { mem, mem_byte } => Instruction::MemoryGrow(mem, mem_byte), I32Const { value } => Instruction::I32Const(value), I64Const { value } => Instruction::I64Const(value), - F32Const { value } => Instruction::F32Const(f32::from_bits(value.bits())), // TODO: check if this is correct - F64Const { value } => Instruction::F64Const(f64::from_bits(value.bits())), // TODO: check if this is correct + F32Const { value } => Instruction::F32Const(f32::from_bits(value.bits())), + F64Const { value } => Instruction::F64Const(f64::from_bits(value.bits())), + RefNull { ty } => Instruction::RefNull(convert_valtype(&ty)), + RefIsNull => Instruction::RefIsNull, + RefFunc { function_index } => Instruction::RefFunc(function_index), I32Load { memarg } => Instruction::I32Load(convert_memarg(memarg)), I64Load { memarg } => Instruction::I64Load(convert_memarg(memarg)), F32Load { memarg } => Instruction::F32Load(convert_memarg(memarg)), @@ -580,10 +588,11 @@ pub fn process_operators<'a>( I64TruncSatF64S => Instruction::I64TruncSatF64S, I64TruncSatF64U => Instruction::I64TruncSatF64U, op => { + log::error!("Unsupported instruction: {:?}", op); return Err(crate::ParseError::UnsupportedOperator(format!( "Unsupported instruction: {:?}", op - ))) + ))); } }; diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index 22e2661..561d5c6 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -10,6 +10,7 @@ extern crate alloc; #[allow(clippy::single_component_path_imports)] use log; +// noop fallback if logging is disabled. #[cfg(not(feature = "logging"))] mod log { macro_rules! debug ( ($($tt:tt)*) => {{}} ); diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 3f97364..931bb45 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -14,12 +14,12 @@ path="src/lib.rs" [dependencies] log={version="0.4", optional=true} -tinywasm-parser={version="0.1.0", path="../parser", default-features=false, optional=true} -tinywasm-types={version="0.1.0", path="../types", default-features=false} +tinywasm-parser={version="0.2.0-alpha.0", path="../parser", default-features=false, optional=true} +tinywasm-types={version="0.2.0-alpha.0", path="../types", default-features=false} [dev-dependencies] wasm-testsuite={path="../wasm-testsuite"} -wast={version="69.0"} +wast={version="70.0"} owo-colors={version="4.0"} eyre={version="0.6"} serde_json={version="1.0"} diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index 208dab1..a7dac81 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -13,7 +13,14 @@ pub enum Trap { Unreachable, /// An out-of-bounds memory access occurred - MemoryOutOfBounds, + MemoryOutOfBounds { + /// The offset of the access + offset: usize, + /// The size of the access + len: usize, + /// The maximum size of the memory + max: usize, + }, /// A division by zero occurred DivisionByZero, @@ -54,6 +61,9 @@ pub enum Error { /// The label stack is empty LabelStackUnderflow, + /// An invalid label type was encountered + InvalidLabelType, + /// The call stack is empty CallStackEmpty, @@ -72,6 +82,7 @@ impl Display for Error { Self::Trap(trap) => write!(f, "trap: {:?}", trap), + Self::InvalidLabelType => write!(f, "invalid label type"), Self::Other(message) => write!(f, "unknown error: {}", message), Self::UnsupportedFeature(feature) => write!(f, "unsupported feature: {}", feature), Self::FuncDidNotReturn => write!(f, "function did not return"), diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs index 58890f6..b0b359d 100644 --- a/crates/tinywasm/src/func.rs +++ b/crates/tinywasm/src/func.rs @@ -19,7 +19,7 @@ pub struct FuncHandle { } impl FuncHandle { - /// Call a function + /// Call a function (Invocation) /// /// See pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { @@ -57,18 +57,23 @@ impl FuncHandle { let call_frame = CallFrame::new(self.addr as usize, params, func_inst.locals().to_vec()); // 7. Push the frame f to the call stack + // & 8. Push the values to the stack (Not needed since the call frame owns the values) stack.call_stack.push(call_frame); - // 8. Push the values to the stack (Not needed since the call frame owns the values) - // 9. Invoke the function instance let runtime = store.runtime(); runtime.exec(store, &mut stack, self.module.clone())?; // Once the function returns: let result_m = func_ty.results.len(); - let res = stack.values.pop_n(result_m)?; + // 1. Assert: m values are on the top of the stack (Ensured by validation) + debug_assert!(stack.values.len() >= result_m); + + // 2. Pop m values from the stack + let res = stack.values.last_n(result_m)?; + + // The values are returned as the results of the invocation. Ok(res .iter() .zip(func_ty.results.iter()) diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs new file mode 100644 index 0000000..1b3a9cf --- /dev/null +++ b/crates/tinywasm/src/imports.rs @@ -0,0 +1,92 @@ +use crate::Result; +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, +}; +use tinywasm_types::{Global, GlobalType, ModuleInstanceAddr, WasmValue}; + +#[derive(Debug)] +#[non_exhaustive] +/// An external value +pub enum Extern { + /// A global value + Global(Global), + // Func(HostFunc), + // Table(Table), +} + +impl Extern { + /// Create a new global import + pub fn global(val: WasmValue, mutable: bool) -> Self { + Self::Global(Global { + ty: GlobalType { + ty: val.val_type(), + mutable, + }, + init: val.const_instr(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +/// Name of an import +pub struct ExternName { + module: String, + name: String, +} + +#[derive(Debug, Default)] +/// Imports for a module instance +pub struct Imports { + values: BTreeMap, + modules: BTreeMap, +} + +pub(crate) struct LinkedImports { + pub(crate) values: BTreeMap, +} + +impl LinkedImports { + pub(crate) fn get(&self, module: &str, name: &str) -> Option<&Extern> { + self.values.get(&ExternName { + module: module.to_string(), + name: name.to_string(), + }) + } +} + +impl Imports { + /// Create a new empty import set + pub fn new() -> Self { + Imports { + values: BTreeMap::new(), + modules: BTreeMap::new(), + } + } + + /// Link a module + /// + /// This will automatically link all imported values + pub fn link_module(&mut self, name: &str, addr: ModuleInstanceAddr) -> Result<&mut Self> { + self.modules.insert(name.to_string(), addr); + Ok(self) + } + + /// Define an import + pub fn define(&mut self, module: &str, name: &str, value: Extern) -> Result<&mut Self> { + self.values.insert( + ExternName { + module: module.to_string(), + name: name.to_string(), + }, + value, + ); + Ok(self) + } + + pub(crate) fn link(self, _store: &mut crate::Store, _module: &crate::Module) -> Result { + // TODO: link to other modules (currently only direct imports are supported) + let values = self.values; + Ok(LinkedImports { values }) + } +} diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index 2fa9cf3..ad5e937 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -1,9 +1,11 @@ use alloc::{boxed::Box, string::ToString, sync::Arc, vec::Vec}; -use tinywasm_types::{Export, ExternalKind, FuncAddr, FuncType, ModuleInstanceAddr}; +use tinywasm_types::{ + DataAddr, ElmAddr, ExternalKind, FuncAddr, FuncType, GlobalAddr, Import, MemAddr, ModuleInstanceAddr, TableAddr, +}; use crate::{ func::{FromWasmValueTuple, IntoWasmValueTuple}, - Error, ExportInstance, FuncHandle, Result, Store, TypedFuncHandle, + Error, ExportInstance, FuncHandle, Imports, Module, Result, Store, TypedFuncHandle, }; /// A WebAssembly Module Instance @@ -14,67 +16,136 @@ use crate::{ #[derive(Debug, Clone)] pub struct ModuleInstance(Arc); +#[allow(dead_code)] #[derive(Debug)] -struct ModuleInstanceInner { +pub(crate) struct ModuleInstanceInner { pub(crate) store_id: usize, - pub(crate) _idx: ModuleInstanceAddr, - pub(crate) func_start: Option, - pub(crate) types: Box<[FuncType]>, - pub(crate) exports: ExportInstance, + pub(crate) idx: ModuleInstanceAddr, + pub(crate) types: Box<[FuncType]>, pub(crate) func_addrs: Vec, - // pub table_addrs: Vec, - // pub mem_addrs: Vec, - // pub global_addrs: Vec, - // pub elem_addrs: Vec, - // pub data_addrs: Vec, + pub(crate) table_addrs: Vec, + pub(crate) mem_addrs: Vec, + pub(crate) global_addrs: Vec, + pub(crate) elem_addrs: Vec, + pub(crate) data_addrs: Vec, + + pub(crate) func_start: Option, + pub(crate) imports: Box<[Import]>, + pub(crate) exports: ExportInstance, } impl ModuleInstance { + /// Get the module instance's address + pub fn id(&self) -> ModuleInstanceAddr { + self.0.idx + } + + /// Instantiate the module in the given store + /// + /// See + pub fn instantiate(store: &mut Store, module: Module, imports: Option) -> Result { + // This doesn't completely follow the steps in the spec, but the end result is the same + // Constant expressions are evaluated directly where they are used, so we + // don't need to create a auxiliary frame etc. + + let idx = store.next_module_instance_idx(); + let imports = imports.unwrap_or_default(); + + // TODO: doesn't link other modules yet + let linked_imports = imports.link(store, &module)?; + let global_addrs = store.add_globals(module.data.globals.into(), &module.data.imports, &linked_imports, idx)?; + + // TODO: imported functions missing + let func_addrs = store.add_funcs(module.data.funcs.into(), idx); + + let table_addrs = store.add_tables(module.data.table_types.into(), idx); + let mem_addrs = store.add_mems(module.data.memory_types.into(), idx)?; + + // TODO: active/declared elems need to be initialized + let elem_addrs = store.add_elems(module.data.elements.into(), idx)?; + + // TODO: active data segments need to be initialized + let data_addrs = store.add_datas(module.data.data.into(), idx); + + let instance = ModuleInstanceInner { + store_id: store.id(), + idx, + + types: module.data.func_types, + func_addrs, + table_addrs, + mem_addrs, + global_addrs, + elem_addrs, + data_addrs, + + func_start: module.data.start_func, + imports: module.data.imports, + exports: crate::ExportInstance(module.data.exports), + }; + + let instance = ModuleInstance::new(instance); + store.add_instance(instance.clone())?; + + Ok(instance) + } + /// Get the module's exports pub fn exports(&self) -> &ExportInstance { &self.0.exports } - pub(crate) fn new( - types: Box<[FuncType]>, - func_start: Option, - exports: Box<[Export]>, - func_addrs: Vec, - idx: ModuleInstanceAddr, - store_id: usize, - ) -> Self { - Self(Arc::new(ModuleInstanceInner { - store_id, - _idx: idx, - types, - func_start, - func_addrs, - exports: ExportInstance(exports), - })) + pub(crate) fn func_addrs(&self) -> &[FuncAddr] { + &self.0.func_addrs + } + + pub(crate) fn _global_addrs(&self) -> &[GlobalAddr] { + &self.0.global_addrs + } + + pub(crate) fn func_ty_addrs(&self) -> &[FuncType] { + &self.0.types + } + + pub(crate) fn new(inner: ModuleInstanceInner) -> Self { + Self(Arc::new(inner)) } pub(crate) fn func_ty(&self, addr: FuncAddr) -> &FuncType { &self.0.types[addr as usize] } + // resolve a function address to the global store address + pub(crate) fn resolve_func_addr(&self, addr: FuncAddr) -> FuncAddr { + self.0.func_addrs[addr as usize] + } + + // resolve a table address to the global store address + pub(crate) fn _resolve_table_addr(&self, addr: TableAddr) -> TableAddr { + self.0.table_addrs[addr as usize] + } + + // resolve a memory address to the global store address + pub(crate) fn resolve_mem_addr(&self, addr: MemAddr) -> MemAddr { + self.0.mem_addrs[addr as usize] + } + + // resolve a global address to the global store address + pub(crate) fn resolve_global_addr(&self, addr: GlobalAddr) -> GlobalAddr { + self.0.global_addrs[addr as usize] + } + /// Get an exported function by name - pub fn get_func(&self, store: &Store, name: &str) -> Result { + pub fn exported_func_by_name(&self, store: &Store, name: &str) -> Result { if self.0.store_id != store.id() { return Err(Error::InvalidStore); } let export = self.0.exports.get(name, ExternalKind::Func)?; - log::debug!("get_func: export: {:?}", export); - - log::debug!("{:?}", self.0.func_addrs); let func_addr = self.0.func_addrs[export.index as usize]; - log::debug!("get_func: func index: {}", export.index); let func = store.get_func(func_addr as usize)?; - log::debug!("get_func: func_addr: {}, func: {:?}", func_addr, func); let ty = self.0.types[func.ty_addr() as usize].clone(); - log::debug!("get_func: ty: {:?}", ty); - log::debug!("types: {:?}", self.0.types); Ok(FuncHandle { addr: export.index, @@ -85,12 +156,12 @@ impl ModuleInstance { } /// Get a typed exported function by name - pub fn get_typed_func(&self, store: &Store, name: &str) -> Result> + pub fn typed_func(&self, store: &Store, name: &str) -> Result> where P: IntoWasmValueTuple, R: FromWasmValueTuple, { - let func = self.get_func(store, name)?; + let func = self.exported_func_by_name(store, name)?; Ok(TypedFuncHandle { func, marker: core::marker::PhantomData, @@ -104,7 +175,7 @@ impl ModuleInstance { /// (which is not part of the spec, but used by llvm) /// /// See - pub fn get_start_func(&mut self, store: &Store) -> Result> { + pub fn start_func(&self, store: &Store) -> Result> { if self.0.store_id != store.id() { return Err(Error::InvalidStore); } @@ -121,13 +192,18 @@ impl ModuleInstance { } }; - let func_addr = self.0.func_addrs[func_index as usize]; - let func = store.get_func(func_addr as usize)?; + let func_addr = self + .0 + .func_addrs + .get(func_index as usize) + .expect("No func addr for start func, this is a bug"); + + let func = store.get_func(*func_addr as usize)?; let ty = self.0.types[func.ty_addr() as usize].clone(); Ok(Some(FuncHandle { module: self.clone(), - addr: func_addr, + addr: *func_addr, ty, name: None, })) @@ -138,8 +214,8 @@ impl ModuleInstance { /// Returns None if the module has no start function /// /// See - pub fn start(&mut self, store: &mut Store) -> Result> { - let Some(func) = self.get_start_func(store)? else { + pub fn start(&self, store: &mut Store) -> Result> { + let Some(func) = self.start_func(store)? else { return Ok(None); }; diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index ec48acf..0c1863f 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -38,12 +38,12 @@ //! // This will allocate the module and its globals into the store //! // and execute the module's start function. //! // Every ModuleInstance has its own ID space for functions, globals, etc. -//! let instance = module.instantiate(&mut store)?; +//! let instance = module.instantiate(&mut store, None)?; //! //! // Get a typed handle to the exported "add" function //! // Alternatively, you can use `instance.get_func` to get an untyped handle //! // that takes and returns WasmValue types -//! let func = instance.get_typed_func::<(i32, i32), (i32,)>(&mut store, "add")?; +//! let func = instance.typed_func::<(i32, i32), (i32,)>(&mut store, "add")?; //! let res = func.call(&mut store, (1, 2))?; //! //! assert_eq!(res, (3,)); @@ -60,7 +60,7 @@ //! a custom allocator. This removes support for parsing from files and streams, //! but otherwise the API is the same. //! -//! Additionally, if you want proper error types, you must use a `nightly` compiler. +//! Additionally, if you want proper error types, you must use a `nightly` compiler to have the error trait in core. mod std; extern crate alloc; @@ -70,6 +70,7 @@ extern crate alloc; #[allow(clippy::single_component_path_imports)] use log; +// noop fallback if logging is disabled. #[cfg(not(feature = "logging"))] pub(crate) mod log { macro_rules! debug ( ($($tt:tt)*) => {{}} ); @@ -94,6 +95,9 @@ pub use export::ExportInstance; mod func; pub use func::{FuncHandle, TypedFuncHandle}; +mod imports; +pub use imports::*; + mod runtime; pub use runtime::*; diff --git a/crates/tinywasm/src/module.rs b/crates/tinywasm/src/module.rs index 4f3575d..5a2c4f7 100644 --- a/crates/tinywasm/src/module.rs +++ b/crates/tinywasm/src/module.rs @@ -1,13 +1,13 @@ use tinywasm_types::TinyWasmModule; -use crate::{ModuleInstance, Result, Store}; +use crate::{Imports, ModuleInstance, Result, Store}; #[derive(Debug)] /// A WebAssembly Module /// /// See pub struct Module { - data: TinyWasmModule, + pub(crate) data: TinyWasmModule, } impl From<&TinyWasmModule> for Module { @@ -50,28 +50,12 @@ impl Module { /// Instantiate the module in the given store /// /// Runs the start function if it exists - /// If you want to run the start function yourself, use `ModuleInstance::new` + /// If you want to run the start function yourself, use `ModuleInstance::instantiate` /// /// See - pub fn instantiate( - self, - store: &mut Store, - // imports: Option<()>, - ) -> Result { - let idx = store.next_module_instance_idx(); - let func_addrs = store.add_funcs(self.data.funcs.into(), idx); - - let instance = ModuleInstance::new( - self.data.func_types, - self.data.start_func, - self.data.exports, - func_addrs, - idx, - store.id(), - ); - - store.add_instance(instance.clone())?; - // let _ = instance.start(store)?; + pub fn instantiate(self, store: &mut Store, imports: Option) -> Result { + let instance = ModuleInstance::instantiate(store, self, imports)?; + let _ = instance.start(store)?; Ok(instance) } } diff --git a/crates/tinywasm/src/runtime/executor/macros.rs b/crates/tinywasm/src/runtime/executor/macros.rs index 61678f2..8a92ac2 100644 --- a/crates/tinywasm/src/runtime/executor/macros.rs +++ b/crates/tinywasm/src/runtime/executor/macros.rs @@ -1,15 +1,65 @@ //! More generic macros for various instructions //! //! These macros are used to generate the actual instruction implementations. +//! In some basic tests this generated better assembly than using generic functions, even when inlined. +//! (Something to revisit in the future) -/// Convert the top value on the stack to a specific type -macro_rules! conv_1 { - ($from:ty, $to:ty, $stack:ident) => {{ - let a: $from = $stack.values.pop()?.into(); - $stack.values.push((a as $to).into()); +/// Load a value from memory +macro_rules! mem_load { + ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + mem_load!($type, $type, $arg, $stack, $store, $module) + }}; + + ($load_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + // TODO: there could be a lot of performance improvements here + let mem_idx = $module.resolve_mem_addr($arg.mem_addr); + let mem = $store.get_mem(mem_idx as usize)?; + + let addr = $stack.values.pop()?.raw_value(); + + let val: [u8; core::mem::size_of::<$load_type>()] = { + let mem = mem.borrow_mut(); + let val = mem.load( + ($arg.offset + addr) as usize, + $arg.align as usize, + core::mem::size_of::<$load_type>(), + )?; + val.try_into().expect("slice with incorrect length") + }; + + let loaded_value = <$load_type>::from_le_bytes(val); + $stack.values.push((loaded_value as $target_type).into()); }}; } +/// Store a value to memory +macro_rules! mem_store { + ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + log::debug!("mem_store!({}, {:?})", stringify!($type), $arg); + + mem_store!($type, $type, $arg, $stack, $store, $module) + }}; + + ($store_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + // likewise, there could be a lot of performance improvements here + let mem_idx = $module.resolve_mem_addr($arg.mem_addr); + let mem = $store.get_mem(mem_idx as usize)?; + + let val = $stack.values.pop_t::<$store_type>()?; + let addr = $stack.values.pop()?.raw_value(); + + let val = val as $store_type; + let val = val.to_le_bytes(); + + mem.borrow_mut() + .store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; + }}; +} + +/// Doing the actual conversion from float to int is a bit tricky, because +/// we need to check for overflow. This macro generates the min/max values +/// for a specific conversion, which are then used in the actual conversion. +/// Rust sadly doesn't have wrapping casts for floats (yet) macro_rules! float_min_max { (f32, i32) => { (-2147483904.0_f32, 2147483648.0_f32) @@ -18,22 +68,22 @@ macro_rules! float_min_max { (-2147483649.0_f64, 2147483648.0_f64) }; (f32, u32) => { - (-1.0_f32, 4294967296.0_f32) + (-1.0_f32, 4294967296.0_f32) // 2^32 }; (f64, u32) => { - (-1.0_f64, 4294967296.0_f64) + (-1.0_f64, 4294967296.0_f64) // 2^32 }; (f32, i64) => { - (-9223373136366403584.0_f32, 9223372036854775808.0_f32) + (-9223373136366403584.0_f32, 9223372036854775808.0_f32) // 2^63 + 2^40 | 2^63 }; (f64, i64) => { - (-9223372036854777856.0_f64, 9223372036854775808.0_f64) + (-9223372036854777856.0_f64, 9223372036854775808.0_f64) // 2^63 + 2^40 | 2^63 }; (f32, u64) => { - (-1.0_f32, 18446744073709551616.0_f32) + (-1.0_f32, 18446744073709551616.0_f32) // 2^64 }; (f64, u64) => { - (-1.0_f64, 18446744073709551616.0_f64) + (-1.0_f64, 18446744073709551616.0_f64) // 2^64 }; // other conversions are not allowed ($from:ty, $to:ty) => { @@ -41,75 +91,56 @@ macro_rules! float_min_max { }; } -// Convert a float to an int, checking for overflow -macro_rules! checked_float_conv_1 { - ($from:tt, $to:tt, $stack:ident) => {{ - let (min, max) = float_min_max!($from, $to); +/// Convert a value on the stack +macro_rules! conv { + ($from:ty, $intermediate:ty, $to:ty, $stack:ident) => {{ + let a: $from = $stack.values.pop()?.into(); + $stack.values.push((a as $intermediate as $to).into()); + }}; + ($from:ty, $to:ty, $stack:ident) => {{ let a: $from = $stack.values.pop()?.into(); - - if a.is_nan() { - return Err(Error::Trap(crate::Trap::InvalidConversionToInt)); - } - - if a <= min || a >= max { - return Err(Error::Trap(crate::Trap::IntegerOverflow)); - } - $stack.values.push((a as $to).into()); }}; } -// Convert a float to an int, checking for overflow -macro_rules! checked_float_conv_2 { - ($from:tt, $uty:tt, $to:tt, $stack:ident) => {{ - let (min, max) = float_min_max!($from, $uty); +/// Convert a value on the stack with error checking +macro_rules! checked_conv_float { + // Direct conversion with error checking (two types) + ($from:tt, $to:tt, $stack:ident) => {{ + checked_conv_float!($from, $to, $to, $stack) + }}; + // Conversion with an intermediate unsigned type and error checking (three types) + ($from:tt, $intermediate:tt, $to:tt, $stack:ident) => {{ + let (min, max) = float_min_max!($from, $intermediate); let a: $from = $stack.values.pop()?.into(); if a.is_nan() { return Err(Error::Trap(crate::Trap::InvalidConversionToInt)); } - log::info!("a: {}", a); - log::info!("min: {}", min); - log::info!("max: {}", max); - if a <= min || a >= max { return Err(Error::Trap(crate::Trap::IntegerOverflow)); } - $stack.values.push((a as $uty as $to).into()); - }}; -} - -/// Convert the unsigned value on the top of the stack to a specific type -macro_rules! conv_2 { - ($ty:ty, $uty:ty, $to:ty, $stack:ident) => {{ - let a: $ty = $stack.values.pop()?.into(); - $stack.values.push((a as $uty as $to).into()); + $stack.values.push((a as $intermediate as $to).into()); }}; } /// Compare two values on the stack macro_rules! comp { ($op:tt, $ty:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); - $stack.values.push(((a $op b) as i32).into()); + comp!($op, $ty, $ty, $stack) }}; -} -/// Compare two values on the stack (cast to ty2 before comparison) -macro_rules! comp_cast { - ($op:tt, $ty:ty, $ty2:ty, $stack:ident) => {{ + ($op:tt, $intermediate:ty, $to:ty, $stack:ident) => {{ let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); + let a: $intermediate = a.into(); + let b: $intermediate = b.into(); // Cast to unsigned type before comparison - let a_unsigned: $ty2 = a as $ty2; - let b_unsigned: $ty2 = b as $ty2; - $stack.values.push(((a_unsigned $op b_unsigned) as i32).into()); + let a = a as $to; + let b = b as $to; + $stack.values.push(((a $op b) as i32).into()); }}; } @@ -121,27 +152,35 @@ macro_rules! comp_zero { }}; } -/// Apply an arithmetic operation to two values on the stack -macro_rules! arithmetic_op { +/// Apply an arithmetic method to two values on the stack +macro_rules! arithmetic { + ($op:ident, $ty:ty, $stack:ident) => {{ + arithmetic!($op, $ty, $ty, $stack) + }}; + + // also allow operators such as +, - ($op:tt, $ty:ty, $stack:ident) => {{ let [a, b] = $stack.values.pop_n_const::<2>()?; let a: $ty = a.into(); let b: $ty = b.into(); $stack.values.push((a $op b).into()); }}; -} -macro_rules! arithmetic_method { - ($op:ident, $ty:ty, $stack:ident) => {{ + ($op:ident, $intermediate:ty, $to:ty, $stack:ident) => {{ let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); + let a: $to = a.into(); + let b: $to = b.into(); + + let a = a as $intermediate; + let b = b as $intermediate; + let result = a.$op(b); - $stack.values.push(result.into()); + $stack.values.push((result as $to).into()); }}; } -macro_rules! arithmetic_method_self { +/// Apply an arithmetic method to a single value on the stack +macro_rules! arithmetic_single { ($op:ident, $ty:ty, $stack:ident) => {{ let a: $ty = $stack.values.pop()?.into(); let result = a.$op(); @@ -149,66 +188,35 @@ macro_rules! arithmetic_method_self { }}; } -macro_rules! arithmetic_method_cast { - ($op:ident, $ty:ty, $ty2:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); - - // Cast to unsigned type before operation - let a_unsigned: $ty2 = a as $ty2; - let b_unsigned: $ty2 = b as $ty2; - - let result = a_unsigned.$op(b_unsigned); - $stack.values.push((result as $ty).into()); +/// Apply an arithmetic operation to two values on the stack with error checking +macro_rules! checked_arithmetic { + // Direct conversion with error checking (two types) + ($from:tt, $to:tt, $stack:ident, $trap:expr) => {{ + checked_arithmetic!($from, $to, $to, $stack, $trap) }}; -} -/// Apply an arithmetic operation to two values on the stack -macro_rules! checked_arithmetic_method { - ($op:ident, $ty:ty, $stack:ident, $trap:expr) => {{ + ($op:ident, $from:ty, $to:ty, $stack:ident, $trap:expr) => {{ let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); - let result = a.$op(b).ok_or_else(|| Error::Trap($trap))?; - debug!( - "checked_arithmetic_method: {}, a: {}, b: {}, res: {}", - stringify!($op), - a, - b, - result - ); - $stack.values.push(result.into()); - }}; -} + let a: $from = a.into(); + let b: $from = b.into(); -/// Apply an arithmetic operation to two values on the stack (cast to ty2 before operation) -macro_rules! checked_arithmetic_method_cast { - ($op:ident, $ty:ty, $ty2:ty, $stack:ident, $trap:expr) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); + let a_casted: $to = a as $to; + let b_casted: $to = b as $to; - // Cast to unsigned type before operation - let a_unsigned: $ty2 = a as $ty2; - let b_unsigned: $ty2 = b as $ty2; + let result = a_casted.$op(b_casted).ok_or_else(|| Error::Trap($trap))?; - let result = a_unsigned.$op(b_unsigned).ok_or_else(|| Error::Trap($trap))?; - $stack.values.push((result as $ty).into()); + // Cast back to original type if different + $stack.values.push((result as $from).into()); }}; } -pub(super) use arithmetic_method; -pub(super) use arithmetic_method_cast; -pub(super) use arithmetic_method_self; -pub(super) use arithmetic_op; -pub(super) use checked_arithmetic_method; -pub(super) use checked_arithmetic_method_cast; -pub(super) use checked_float_conv_1; -pub(super) use checked_float_conv_2; +pub(super) use arithmetic; +pub(super) use arithmetic_single; +pub(super) use checked_arithmetic; +pub(super) use checked_conv_float; pub(super) use comp; -pub(super) use comp_cast; pub(super) use comp_zero; -pub(super) use conv_1; -pub(super) use conv_2; +pub(super) use conv; pub(super) use float_min_max; +pub(super) use mem_load; +pub(super) use mem_store; diff --git a/crates/tinywasm/src/runtime/executor/mod.rs b/crates/tinywasm/src/runtime/executor/mod.rs index 6486b38..541c0a7 100644 --- a/crates/tinywasm/src/runtime/executor/mod.rs +++ b/crates/tinywasm/src/runtime/executor/mod.rs @@ -2,10 +2,9 @@ use core::ops::{BitAnd, BitOr, BitXor, Neg}; use super::{DefaultRuntime, Stack}; use crate::{ - get_label_args, log::debug, runtime::{BlockType, LabelFrame}, - CallFrame, Error, ModuleInstance, Result, Store, + CallFrame, Error, LabelArgs, ModuleInstance, Result, Store, }; use alloc::vec::Vec; use log::info; @@ -18,6 +17,11 @@ use traits::*; impl DefaultRuntime { pub(crate) fn exec(&self, store: &mut Store, stack: &mut Stack, module: ModuleInstance) -> Result<()> { + log::info!("exports: {:?}", module.exports()); + log::info!("func_addrs: {:?}", module.func_addrs()); + log::info!("func_ty_addrs: {:?}", module.func_ty_addrs().len()); + log::info!("store funcs: {:?}", store.data.funcs.len()); + // The current call frame, gets updated inside of exec_one let mut cf = stack.call_stack.pop()?; @@ -31,6 +35,7 @@ impl DefaultRuntime { match exec_one(&mut cf, instr, instrs, stack, store, &module)? { // Continue execution at the new top of the call stack ExecResult::Call => { + cf = stack.call_stack.pop()?; func = store.get_func(cf.func_ptr)?.clone(); instrs = func.instructions(); continue; @@ -70,6 +75,23 @@ enum ExecResult { Trap(crate::Trap), } +// Break to a block at the given index (relative to the current frame) +// If there is no block at the given index, return or call the parent function +// +// This is a bit hard to see from the spec, but it's vaild to use breaks to return +// from a function, so we need to check if the label stack is empty +macro_rules! break_to { + ($cf:ident, $stack:ident, $break_to_relative:ident) => {{ + if $cf.break_to(*$break_to_relative, &mut $stack.values).is_none() { + if $stack.call_stack.is_empty() { + return Ok(ExecResult::Return); + } else { + return Ok(ExecResult::Call); + } + } + }}; +} + /// Run a single step of the interpreter /// A seperate function is used so later, we can more easily implement /// a step-by-step debugger (using generators once they're stable?) @@ -106,8 +128,9 @@ fn exec_one( Call(v) => { debug!("start call"); // prepare the call frame - let func = store.get_func(*v as usize)?; - let func_ty = module.func_ty(*v); + let func_idx = module.resolve_func_addr(*v); + let func = store.get_func(func_idx as usize)?; + let func_ty = module.func_ty(func.ty_addr()); debug!("params: {:?}", func_ty.params); debug!("stack: {:?}", stack.values); @@ -120,36 +143,36 @@ fn exec_one( stack.call_stack.push(call_frame); // call the function - *cf = stack.call_stack.pop()?; - debug!("calling: {:?}", func); return Ok(ExecResult::Call); } If(args, else_offset, end_offset) => { - let end_instr_ptr = cf.instr_ptr + *end_offset; - - info!( - "if it's true, we'll jump to the next instruction (@{})", - cf.instr_ptr + 1 - ); - - if let Some(else_offset) = else_offset { - info!( - "else: {:?} (@{})", - instrs[cf.instr_ptr + else_offset], - cf.instr_ptr + else_offset - ); - }; - - info!("end: {:?} (@{})", instrs[end_instr_ptr], end_instr_ptr); - - if stack.values.pop_t::()? != 0 { + if stack.values.pop_t::()? == 0 { + if let Some(else_offset) = else_offset { + log::info!("entering else at {}", cf.instr_ptr + *else_offset); + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr + *else_offset, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), // - params, + args: crate::LabelArgs::new(*args, module)?, + ty: BlockType::Else, + }, + &mut stack.values, + ); + cf.instr_ptr += *else_offset; + } else { + log::info!("skipping if"); + cf.instr_ptr += *end_offset + } + } else { + log::info!("entering then"); cf.enter_label( LabelFrame { instr_ptr: cf.instr_ptr, end_instr_ptr: cf.instr_ptr + *end_offset, stack_ptr: stack.values.len(), // - params, - args: get_label_args(*args, module)?, + args: LabelArgs::new(*args, module)?, ty: BlockType::If, }, &mut stack.values, @@ -164,7 +187,7 @@ fn exec_one( instr_ptr: cf.instr_ptr, end_instr_ptr: cf.instr_ptr + *end_offset, stack_ptr: stack.values.len(), // - params, - args: get_label_args(*args, module)?, + args: LabelArgs::new(*args, module)?, ty: BlockType::Loop, }, &mut stack.values, @@ -177,14 +200,14 @@ fn exec_one( instr_ptr: cf.instr_ptr, end_instr_ptr: cf.instr_ptr + *end_offset, stack_ptr: stack.values.len(), //- params, - args: get_label_args(*args, module)?, + args: LabelArgs::new(*args, module)?, ty: BlockType::Block, }, &mut stack.values, ); } - BrTable(_default, len) => { + BrTable(default, len) => { let instr = instrs[cf.instr_ptr + 1..cf.instr_ptr + 1 + *len] .iter() .map(|i| match i { @@ -197,64 +220,132 @@ fn exec_one( panic!("Expected {} BrLabel instructions, got {}", len, instr.len()); } - todo!() + let idx = stack.values.pop_t::()? as usize; + if let Some(label) = instr.get(idx) { + break_to!(cf, stack, label); + } else { + break_to!(cf, stack, default); + } } - Br(v) => cf.break_to(*v, &mut stack.values)?, + Br(v) => break_to!(cf, stack, v), BrIf(v) => { if stack.values.pop_t::()? > 0 { - cf.break_to(*v, &mut stack.values)? - }; + break_to!(cf, stack, v); + } } Return => match stack.call_stack.is_empty() { true => return Ok(ExecResult::Return), - false => { - *cf = stack.call_stack.pop()?; - return Ok(ExecResult::Call); - } + false => return Ok(ExecResult::Call), }, EndFunc => { - if cf.labels.len() > 0 { - panic!("endfunc: block frames not empty, this should have been validated by the parser"); - } + debug_assert!( + cf.labels.len() == 0, + "endfunc: block frames not empty, this should have been validated by the parser" + ); match stack.call_stack.is_empty() { true => return Ok(ExecResult::Return), - false => { - *cf = stack.call_stack.pop()?; - return Ok(ExecResult::Call); - } + false => return Ok(ExecResult::Call), } } - EndBlockFrame => { - let blocks = &mut cf.labels; - - // remove the label from the label stack - let Some(block) = blocks.pop() else { - panic!("end: no label to end, this should have been validated by the parser"); + // We're essentially using else as a EndBlockFrame instruction for if blocks + Else(end_offset) => { + let Some(block) = cf.labels.pop() else { + panic!("else: no label to end, this should have been validated by the parser"); }; let res_count = block.args.results; - info!("we want to keep {} values on the stack", res_count); - info!("current block stack ptr: {}", block.stack_ptr); - info!("stack: {:?}", stack.values); + stack.values.truncate_keep(block.stack_ptr, res_count); + cf.instr_ptr += *end_offset; + } - // trim the lable's stack from the stack - stack.values.truncate_keep(block.stack_ptr, res_count) + EndBlockFrame => { + // remove the label from the label stack + let Some(block) = cf.labels.pop() else { + panic!("end: no label to end, this should have been validated by the parser"); + }; + stack.values.truncate_keep(block.stack_ptr, block.args.results) } LocalGet(local_index) => stack.values.push(cf.get_local(*local_index as usize)), LocalSet(local_index) => cf.set_local(*local_index as usize, stack.values.pop()?), LocalTee(local_index) => cf.set_local(*local_index as usize, *stack.values.last()?), + GlobalGet(global_index) => { + let idx = module.resolve_global_addr(*global_index); + let global = store.get_global_val(idx as usize)?; + stack.values.push(global); + } + + GlobalSet(global_index) => { + let idx = module.resolve_global_addr(*global_index); + store.set_global_val(idx as usize, stack.values.pop()?)?; + } + I32Const(val) => stack.values.push((*val).into()), I64Const(val) => stack.values.push((*val).into()), F32Const(val) => stack.values.push((*val).into()), F64Const(val) => stack.values.push((*val).into()), + MemorySize(addr, byte) => { + if *byte != 0 { + unimplemented!("memory.size with byte != 0"); + } + + let mem_idx = module.resolve_mem_addr(*addr); + let mem = store.get_mem(mem_idx as usize)?; + stack.values.push(mem.borrow().size().into()); + } + + MemoryGrow(addr, byte) => { + if *byte != 0 { + unimplemented!("memory.grow with byte != 0"); + } + + let mem_idx = module.resolve_mem_addr(*addr); + let mem = store.get_mem(mem_idx as usize)?; + + let (res, prev_size) = { + let mut mem = mem.borrow_mut(); + let prev_size = mem.size(); + (mem.grow(stack.values.pop_t::()?), prev_size) + }; + + match res { + Ok(_) => stack.values.push(prev_size.into()), + Err(_) => stack.values.push((-1).into()), + } + } + + I32Store(arg) => mem_store!(i32, arg, stack, store, module), + I64Store(arg) => mem_store!(i64, arg, stack, store, module), + F32Store(arg) => mem_store!(f32, arg, stack, store, module), + F64Store(arg) => mem_store!(f64, arg, stack, store, module), + I32Store8(arg) => mem_store!(i8, i32, arg, stack, store, module), + I32Store16(arg) => mem_store!(i16, i32, arg, stack, store, module), + I64Store8(arg) => mem_store!(i8, i64, arg, stack, store, module), + I64Store16(arg) => mem_store!(i16, i64, arg, stack, store, module), + I64Store32(arg) => mem_store!(i32, i64, arg, stack, store, module), + + I32Load(arg) => mem_load!(i32, arg, stack, store, module), + I64Load(arg) => mem_load!(i64, arg, stack, store, module), + F32Load(arg) => mem_load!(f32, arg, stack, store, module), + F64Load(arg) => mem_load!(f64, arg, stack, store, module), + I32Load8S(arg) => mem_load!(i8, i32, arg, stack, store, module), + I32Load8U(arg) => mem_load!(u8, i32, arg, stack, store, module), + I32Load16S(arg) => mem_load!(i16, i32, arg, stack, store, module), + I32Load16U(arg) => mem_load!(u16, i32, arg, stack, store, module), + I64Load8S(arg) => mem_load!(i8, i64, arg, stack, store, module), + I64Load8U(arg) => mem_load!(u8, i64, arg, stack, store, module), + I64Load16S(arg) => mem_load!(i16, i64, arg, stack, store, module), + I64Load16U(arg) => mem_load!(u16, i64, arg, stack, store, module), + I64Load32S(arg) => mem_load!(i32, i64, arg, stack, store, module), + I64Load32U(arg) => mem_load!(u32, i64, arg, stack, store, module), + I64Eqz => comp_zero!(==, i64, stack), I32Eqz => comp_zero!(==, i32, stack), @@ -270,122 +361,125 @@ fn exec_one( I32LtS => comp!(<, i32, stack), I64LtS => comp!(<, i64, stack), - I32LtU => comp_cast!(<, i32, u32, stack), - I64LtU => comp_cast!(<, i64, u64, stack), + I32LtU => comp!(<, i32, u32, stack), + I64LtU => comp!(<, i64, u64, stack), F32Lt => comp!(<, f32, stack), F64Lt => comp!(<, f64, stack), I32LeS => comp!(<=, i32, stack), I64LeS => comp!(<=, i64, stack), - I32LeU => comp_cast!(<=, i32, u32, stack), - I64LeU => comp_cast!(<=, i64, u64, stack), + I32LeU => comp!(<=, i32, u32, stack), + I64LeU => comp!(<=, i64, u64, stack), F32Le => comp!(<=, f32, stack), F64Le => comp!(<=, f64, stack), I32GeS => comp!(>=, i32, stack), I64GeS => comp!(>=, i64, stack), - I32GeU => comp_cast!(>=, i32, u32, stack), - I64GeU => comp_cast!(>=, i64, u64, stack), + I32GeU => comp!(>=, i32, u32, stack), + I64GeU => comp!(>=, i64, u64, stack), F32Ge => comp!(>=, f32, stack), F64Ge => comp!(>=, f64, stack), I32GtS => comp!(>, i32, stack), I64GtS => comp!(>, i64, stack), - I32GtU => comp_cast!(>, i32, u32, stack), - I64GtU => comp_cast!(>, i64, u64, stack), + I32GtU => comp!(>, i32, u32, stack), + I64GtU => comp!(>, i64, u64, stack), F32Gt => comp!(>, f32, stack), F64Gt => comp!(>, f64, stack), - I64Add => arithmetic_method!(wrapping_add, i64, stack), - I32Add => arithmetic_method!(wrapping_add, i32, stack), - F32Add => arithmetic_op!(+, f32, stack), - F64Add => arithmetic_op!(+, f64, stack), + I64Add => arithmetic!(wrapping_add, i64, stack), + I32Add => arithmetic!(wrapping_add, i32, stack), + F32Add => arithmetic!(+, f32, stack), + F64Add => arithmetic!(+, f64, stack), - I32Sub => arithmetic_method!(wrapping_sub, i32, stack), - I64Sub => arithmetic_method!(wrapping_sub, i64, stack), - F32Sub => arithmetic_op!(-, f32, stack), - F64Sub => arithmetic_op!(-, f64, stack), + I32Sub => arithmetic!(wrapping_sub, i32, stack), + I64Sub => arithmetic!(wrapping_sub, i64, stack), + F32Sub => arithmetic!(-, f32, stack), + F64Sub => arithmetic!(-, f64, stack), - F32Div => arithmetic_op!(/, f32, stack), - F64Div => arithmetic_op!(/, f64, stack), + F32Div => arithmetic!(/, f32, stack), + F64Div => arithmetic!(/, f64, stack), - I32Mul => arithmetic_method!(wrapping_mul, i32, stack), - I64Mul => arithmetic_method!(wrapping_mul, i64, stack), - F32Mul => arithmetic_op!(*, f32, stack), - F64Mul => arithmetic_op!(*, f64, stack), + I32Mul => arithmetic!(wrapping_mul, i32, stack), + I64Mul => arithmetic!(wrapping_mul, i64, stack), + F32Mul => arithmetic!(*, f32, stack), + F64Mul => arithmetic!(*, f64, stack), // these can trap - I32DivS => checked_arithmetic_method!(checked_div, i32, stack, crate::Trap::DivisionByZero), - I64DivS => checked_arithmetic_method!(checked_div, i64, stack, crate::Trap::DivisionByZero), - I32DivU => checked_arithmetic_method_cast!(checked_div, i32, u32, stack, crate::Trap::DivisionByZero), - I64DivU => checked_arithmetic_method_cast!(checked_div, i64, u64, stack, crate::Trap::DivisionByZero), - - I32RemS => checked_arithmetic_method!(checked_wrapping_rem, i32, stack, crate::Trap::DivisionByZero), - I64RemS => checked_arithmetic_method!(checked_wrapping_rem, i64, stack, crate::Trap::DivisionByZero), - I32RemU => checked_arithmetic_method_cast!(checked_wrapping_rem, i32, u32, stack, crate::Trap::DivisionByZero), - I64RemU => checked_arithmetic_method_cast!(checked_wrapping_rem, i64, u64, stack, crate::Trap::DivisionByZero), - - I32And => arithmetic_method!(bitand, i32, stack), - I64And => arithmetic_method!(bitand, i64, stack), - I32Or => arithmetic_method!(bitor, i32, stack), - I64Or => arithmetic_method!(bitor, i64, stack), - I32Xor => arithmetic_method!(bitxor, i32, stack), - I64Xor => arithmetic_method!(bitxor, i64, stack), - I32Shl => arithmetic_method!(wasm_shl, i32, stack), - I64Shl => arithmetic_method!(wasm_shl, i64, stack), - I32ShrS => arithmetic_method!(wasm_shr, i32, stack), - I64ShrS => arithmetic_method!(wasm_shr, i64, stack), - I32ShrU => arithmetic_method_cast!(wasm_shr, i32, u32, stack), - I64ShrU => arithmetic_method_cast!(wasm_shr, i64, u64, stack), - I32Rotl => arithmetic_method!(wasm_rotl, i32, stack), - I64Rotl => arithmetic_method!(wasm_rotl, i64, stack), - I32Rotr => arithmetic_method!(wasm_rotr, i32, stack), - I64Rotr => arithmetic_method!(wasm_rotr, i64, stack), - - I32Clz => arithmetic_method_self!(leading_zeros, i32, stack), - I64Clz => arithmetic_method_self!(leading_zeros, i64, stack), - I32Ctz => arithmetic_method_self!(trailing_zeros, i32, stack), - I64Ctz => arithmetic_method_self!(trailing_zeros, i64, stack), - I32Popcnt => arithmetic_method_self!(count_ones, i32, stack), - I64Popcnt => arithmetic_method_self!(count_ones, i64, stack), - - F32ConvertI32S => conv_1!(i32, f32, stack), - F32ConvertI64S => conv_1!(i64, f32, stack), - F64ConvertI32S => conv_1!(i32, f64, stack), - F64ConvertI64S => conv_1!(i64, f64, stack), - F32ConvertI32U => conv_2!(i32, u32, f32, stack), - F32ConvertI64U => conv_2!(i64, u64, f32, stack), - F64ConvertI32U => conv_2!(i32, u32, f64, stack), - F64ConvertI64U => conv_2!(i64, u64, f64, stack), - I32Extend8S => conv_2!(i32, i8, i32, stack), - I32Extend16S => conv_2!(i32, i16, i32, stack), - I64Extend8S => conv_2!(i64, i8, i64, stack), - I64Extend16S => conv_2!(i64, i16, i64, stack), - I64Extend32S => conv_2!(i64, i32, i64, stack), - I64ExtendI32U => conv_2!(i32, u32, i64, stack), - I64ExtendI32S => conv_1!(i32, i64, stack), - I32WrapI64 => conv_1!(i64, i32, stack), - - F32Abs => arithmetic_method_self!(abs, f32, stack), - F64Abs => arithmetic_method_self!(abs, f64, stack), - F32Neg => arithmetic_method_self!(neg, f32, stack), - F64Neg => arithmetic_method_self!(neg, f64, stack), - F32Ceil => arithmetic_method_self!(ceil, f32, stack), - F64Ceil => arithmetic_method_self!(ceil, f64, stack), - F32Floor => arithmetic_method_self!(floor, f32, stack), - F64Floor => arithmetic_method_self!(floor, f64, stack), - F32Trunc => arithmetic_method_self!(trunc, f32, stack), - F64Trunc => arithmetic_method_self!(trunc, f64, stack), - F32Nearest => arithmetic_method_self!(wasm_nearest, f32, stack), - F64Nearest => arithmetic_method_self!(wasm_nearest, f64, stack), - F32Sqrt => arithmetic_method_self!(sqrt, f32, stack), - F64Sqrt => arithmetic_method_self!(sqrt, f64, stack), - F32Min => arithmetic_method!(wasm_min, f32, stack), - F64Min => arithmetic_method!(wasm_min, f64, stack), - F32Max => arithmetic_method!(wasm_max, f32, stack), - F64Max => arithmetic_method!(wasm_max, f64, stack), - F32Copysign => arithmetic_method!(copysign, f32, stack), - F64Copysign => arithmetic_method!(copysign, f64, stack), + I32DivS => checked_arithmetic!(checked_div, i32, stack, crate::Trap::DivisionByZero), + I64DivS => checked_arithmetic!(checked_div, i64, stack, crate::Trap::DivisionByZero), + I32DivU => checked_arithmetic!(checked_div, i32, u32, stack, crate::Trap::DivisionByZero), + I64DivU => checked_arithmetic!(checked_div, i64, u64, stack, crate::Trap::DivisionByZero), + + I32RemS => checked_arithmetic!(checked_wrapping_rem, i32, stack, crate::Trap::DivisionByZero), + I64RemS => checked_arithmetic!(checked_wrapping_rem, i64, stack, crate::Trap::DivisionByZero), + I32RemU => checked_arithmetic!(checked_wrapping_rem, i32, u32, stack, crate::Trap::DivisionByZero), + I64RemU => checked_arithmetic!(checked_wrapping_rem, i64, u64, stack, crate::Trap::DivisionByZero), + + I32And => arithmetic!(bitand, i32, stack), + I64And => arithmetic!(bitand, i64, stack), + I32Or => arithmetic!(bitor, i32, stack), + I64Or => arithmetic!(bitor, i64, stack), + I32Xor => arithmetic!(bitxor, i32, stack), + I64Xor => arithmetic!(bitxor, i64, stack), + I32Shl => arithmetic!(wasm_shl, i32, stack), + I64Shl => arithmetic!(wasm_shl, i64, stack), + I32ShrS => arithmetic!(wasm_shr, i32, stack), + I64ShrS => arithmetic!(wasm_shr, i64, stack), + I32ShrU => arithmetic!(wasm_shr, u32, i32, stack), + I64ShrU => arithmetic!(wasm_shr, u64, i64, stack), + I32Rotl => arithmetic!(wasm_rotl, i32, stack), + I64Rotl => arithmetic!(wasm_rotl, i64, stack), + I32Rotr => arithmetic!(wasm_rotr, i32, stack), + I64Rotr => arithmetic!(wasm_rotr, i64, stack), + + I32Clz => arithmetic_single!(leading_zeros, i32, stack), + I64Clz => arithmetic_single!(leading_zeros, i64, stack), + I32Ctz => arithmetic_single!(trailing_zeros, i32, stack), + I64Ctz => arithmetic_single!(trailing_zeros, i64, stack), + I32Popcnt => arithmetic_single!(count_ones, i32, stack), + I64Popcnt => arithmetic_single!(count_ones, i64, stack), + + F32ConvertI32S => conv!(i32, f32, stack), + F32ConvertI64S => conv!(i64, f32, stack), + F64ConvertI32S => conv!(i32, f64, stack), + F64ConvertI64S => conv!(i64, f64, stack), + F32ConvertI32U => conv!(i32, u32, f32, stack), + F32ConvertI64U => conv!(i64, u64, f32, stack), + F64ConvertI32U => conv!(i32, u32, f64, stack), + F64ConvertI64U => conv!(i64, u64, f64, stack), + I32Extend8S => conv!(i32, i8, i32, stack), + I32Extend16S => conv!(i32, i16, i32, stack), + I64Extend8S => conv!(i64, i8, i64, stack), + I64Extend16S => conv!(i64, i16, i64, stack), + I64Extend32S => conv!(i64, i32, i64, stack), + I64ExtendI32U => conv!(i32, u32, i64, stack), + I64ExtendI32S => conv!(i32, i64, stack), + I32WrapI64 => conv!(i64, i32, stack), + + F32DemoteF64 => conv!(f64, f32, stack), + F64PromoteF32 => conv!(f32, f64, stack), + + F32Abs => arithmetic_single!(abs, f32, stack), + F64Abs => arithmetic_single!(abs, f64, stack), + F32Neg => arithmetic_single!(neg, f32, stack), + F64Neg => arithmetic_single!(neg, f64, stack), + F32Ceil => arithmetic_single!(ceil, f32, stack), + F64Ceil => arithmetic_single!(ceil, f64, stack), + F32Floor => arithmetic_single!(floor, f32, stack), + F64Floor => arithmetic_single!(floor, f64, stack), + F32Trunc => arithmetic_single!(trunc, f32, stack), + F64Trunc => arithmetic_single!(trunc, f64, stack), + F32Nearest => arithmetic_single!(wasm_nearest, f32, stack), + F64Nearest => arithmetic_single!(wasm_nearest, f64, stack), + F32Sqrt => arithmetic_single!(sqrt, f32, stack), + F64Sqrt => arithmetic_single!(sqrt, f64, stack), + F32Min => arithmetic!(wasm_min, f32, stack), + F64Min => arithmetic!(wasm_min, f64, stack), + F32Max => arithmetic!(wasm_max, f32, stack), + F64Max => arithmetic!(wasm_max, f64, stack), + F32Copysign => arithmetic!(copysign, f32, stack), + F64Copysign => arithmetic!(copysign, f64, stack), // no-op instructions since types are erased at runtime I32ReinterpretF32 => {} @@ -394,14 +488,14 @@ fn exec_one( F64ReinterpretI64 => {} // unsigned versions of these are a bit broken atm - I32TruncF32S => checked_float_conv_1!(f32, i32, stack), - I32TruncF64S => checked_float_conv_1!(f64, i32, stack), - I32TruncF32U => checked_float_conv_2!(f32, u32, i32, stack), - I32TruncF64U => checked_float_conv_2!(f64, u32, i32, stack), - I64TruncF32S => checked_float_conv_1!(f32, i64, stack), - I64TruncF64S => checked_float_conv_1!(f64, i64, stack), - I64TruncF32U => checked_float_conv_2!(f32, u64, i64, stack), - I64TruncF64U => checked_float_conv_2!(f64, u64, i64, stack), + I32TruncF32S => checked_conv_float!(f32, i32, stack), + I32TruncF64S => checked_conv_float!(f64, i32, stack), + I32TruncF32U => checked_conv_float!(f32, u32, i32, stack), + I32TruncF64U => checked_conv_float!(f64, u32, i32, stack), + I64TruncF32S => checked_conv_float!(f32, i64, stack), + I64TruncF64S => checked_conv_float!(f64, i64, stack), + I64TruncF32U => checked_conv_float!(f32, u64, i64, stack), + I64TruncF64U => checked_conv_float!(f64, u64, i64, stack), i => { log::error!("unimplemented instruction: {:?}", i); diff --git a/crates/tinywasm/src/runtime/stack.rs b/crates/tinywasm/src/runtime/stack.rs index 0912640..07d9316 100644 --- a/crates/tinywasm/src/runtime/stack.rs +++ b/crates/tinywasm/src/runtime/stack.rs @@ -3,16 +3,12 @@ mod call_stack; mod value_stack; use self::{call_stack::CallStack, value_stack::ValueStack}; -pub(crate) use blocks::{get_label_args, BlockType, LabelFrame}; +pub(crate) use blocks::{BlockType, LabelArgs, LabelFrame}; pub(crate) use call_stack::CallFrame; /// A WebAssembly Stack #[derive(Debug, Default)] pub struct Stack { - // keeping this typed for now to make it easier to debug - // TODO: Maybe split into Vec and Vec for better memory usage? pub(crate) values: ValueStack, - - /// The call stack pub(crate) call_stack: CallStack, } diff --git a/crates/tinywasm/src/runtime/stack/blocks.rs b/crates/tinywasm/src/runtime/stack/blocks.rs index 05c9043..f5a0d8d 100644 --- a/crates/tinywasm/src/runtime/stack/blocks.rs +++ b/crates/tinywasm/src/runtime/stack/blocks.rs @@ -1,10 +1,9 @@ use alloc::vec::Vec; -use log::info; use tinywasm_types::BlockArgs; use crate::{ModuleInstance, Result}; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone, Default)] pub(crate) struct Labels(Vec); impl Labels { @@ -13,20 +12,18 @@ impl Labels { } #[inline] - pub(crate) fn push(&mut self, block: LabelFrame) { - self.0.push(block); + pub(crate) fn push(&mut self, label: LabelFrame) { + self.0.push(label); } #[inline] - pub(crate) fn top(&self) -> Option<&LabelFrame> { - self.0.last() - } - - #[inline] - /// get the block at the given index, where 0 is the top of the stack + /// get the label at the given index, where 0 is the top of the stack pub(crate) fn get_relative_to_top(&self, index: usize) -> Option<&LabelFrame> { - info!("get block: {}", index); - info!("blocks: {:?}", self.0); + let len = self.0.len(); + if index >= len { + return None; + } + self.0.get(self.0.len() - index - 1) } @@ -64,19 +61,21 @@ pub(crate) enum BlockType { Block, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub(crate) struct LabelArgs { pub(crate) params: usize, pub(crate) results: usize, } -pub(crate) fn get_label_args(args: BlockArgs, module: &ModuleInstance) -> Result { - Ok(match args { - BlockArgs::Empty => LabelArgs { params: 0, results: 0 }, - BlockArgs::Type(_) => LabelArgs { params: 0, results: 1 }, - BlockArgs::FuncType(t) => LabelArgs { - params: module.func_ty(t).params.len(), - results: module.func_ty(t).results.len(), - }, - }) +impl LabelArgs { + pub(crate) fn new(args: BlockArgs, module: &ModuleInstance) -> Result { + Ok(match args { + BlockArgs::Empty => LabelArgs { params: 0, results: 0 }, + BlockArgs::Type(_) => LabelArgs { params: 0, results: 1 }, + BlockArgs::FuncType(t) => LabelArgs { + params: module.func_ty(t).params.len(), + results: module.func_ty(t).results.len(), + }, + }) + } } diff --git a/crates/tinywasm/src/runtime/stack/call_stack.rs b/crates/tinywasm/src/runtime/stack/call_stack.rs index bf45753..239659c 100644 --- a/crates/tinywasm/src/runtime/stack/call_stack.rs +++ b/crates/tinywasm/src/runtime/stack/call_stack.rs @@ -89,16 +89,11 @@ impl CallFrame { } /// Break to a block at the given index (relative to the current frame) + /// Returns `None` if there is no block at the given index (e.g. if we need to return, this is handled by the caller) #[inline] - pub(crate) fn break_to(&mut self, break_to_relative: u32, value_stack: &mut super::ValueStack) -> Result<()> { - let current_label = self.labels.top().ok_or(Error::LabelStackUnderflow)?; - let break_to = self - .labels - .get_relative_to_top(break_to_relative as usize) - .ok_or(Error::LabelStackUnderflow)?; - - // trim the lable's stack from the stack - value_stack.truncate_keep(break_to.stack_ptr, break_to.args.results); + pub(crate) fn break_to(&mut self, break_to_relative: u32, value_stack: &mut super::ValueStack) -> Option<()> { + let break_to = self.labels.get_relative_to_top(break_to_relative as usize)?; + value_stack.break_to(break_to.stack_ptr, break_to.args.results); // instr_ptr points to the label instruction, but the next step // will increment it by 1 since we're changing the "current" instr_ptr @@ -110,7 +105,7 @@ impl CallFrame { // we also want to trim the label stack to the loop (but not including the loop) self.labels.truncate(self.labels.len() - break_to_relative as usize); } - BlockType::Block => { + BlockType::Block | BlockType::If | BlockType::Else => { // this is a block, so we want to jump to the next instruction after the block ends (the inst_ptr will be incremented by 1 before the next instruction is executed) self.instr_ptr = break_to.end_instr_ptr; @@ -118,7 +113,6 @@ impl CallFrame { self.labels .truncate(self.labels.len() - (break_to_relative as usize + 1)); } - _ => unimplemented!("break to block type: {:?}", current_label.ty), } // self.instr_ptr = block_frame.instr_ptr; @@ -134,7 +128,7 @@ impl CallFrame { // // }; // self.block_frames.trim(block_index as usize); - Ok(()) + Some(()) } pub(crate) fn new_raw(func_ptr: usize, params: &[RawWasmValue], local_types: Vec) -> Self { diff --git a/crates/tinywasm/src/runtime/stack/value_stack.rs b/crates/tinywasm/src/runtime/stack/value_stack.rs index 694eb29..10054bc 100644 --- a/crates/tinywasm/src/runtime/stack/value_stack.rs +++ b/crates/tinywasm/src/runtime/stack/value_stack.rs @@ -2,7 +2,6 @@ use core::ops::Range; use crate::{runtime::RawWasmValue, Error, Result}; use alloc::vec::Vec; -use log::info; // minimum stack size pub(crate) const STACK_SIZE: usize = 1024; @@ -25,11 +24,6 @@ impl Default for ValueStack { } impl ValueStack { - #[cfg(test)] - pub(crate) fn data(&self) -> &[RawWasmValue] { - &self.stack - } - #[inline] pub(crate) fn extend_from_within(&mut self, range: Range) { self.top += range.len(); @@ -42,31 +36,24 @@ impl ValueStack { self.top } - #[inline] - pub(crate) fn _truncate(&mut self, n: usize) { - assert!(self.top <= self.stack.len()); - self.top -= n; - self.stack.truncate(self.top); - } - - #[inline] - // example: [1, 2, 3] n=1, end_keep=1 => [1, 3] - // example: [1] n=1, end_keep=1 => [1] pub(crate) fn truncate_keep(&mut self, n: usize, end_keep: usize) { - if n == end_keep || n == 0 { - return; + let total_to_keep = n + end_keep; + assert!( + self.top >= total_to_keep, + "Total to keep should be less than or equal to self.top" + ); + + let current_size = self.stack.len(); + if current_size <= total_to_keep { + return; // No need to truncate if the current size is already less than or equal to total_to_keep } - assert!(self.top <= self.stack.len()); - info!("removing from {} to {}", self.top - n, self.top - end_keep); - self.stack.drain(self.top - n..self.top - end_keep); - self.top -= n - end_keep; - } + let items_to_remove = current_size - total_to_keep; + let remove_start_index = self.top - items_to_remove - end_keep; + let remove_end_index = self.top - end_keep; - #[inline] - pub(crate) fn _extend(&mut self, values: impl IntoIterator + ExactSizeIterator) { - self.top += values.len(); - self.stack.extend(values); + self.stack.drain(remove_start_index..remove_end_index); + self.top = total_to_keep; // Update top to reflect the new size } #[inline] @@ -92,6 +79,21 @@ impl ValueStack { self.stack.pop().ok_or(Error::StackUnderflow) } + pub(crate) fn break_to(&mut self, new_stack_size: usize, result_count: usize) { + self.stack + .copy_within((self.top - result_count)..self.top, new_stack_size); + self.top = new_stack_size + result_count; + self.stack.truncate(self.top); + } + + #[inline] + pub(crate) fn last_n(&self, n: usize) -> Result<&[RawWasmValue]> { + if self.top < n { + return Err(Error::StackUnderflow); + } + Ok(&self.stack[self.top - n..self.top]) + } + #[inline] pub(crate) fn pop_n(&mut self, n: usize) -> Result> { if self.top < n { @@ -120,39 +122,6 @@ impl ValueStack { #[cfg(test)] mod tests { use super::*; - use crate::std::panic; - - fn crate_stack + Copy>(data: &[T]) -> ValueStack { - let mut stack = ValueStack::default(); - stack._extend(data.iter().map(|v| (*v).into())); - stack - } - - fn assert_truncate_keep + Copy>(data: &[T], n: usize, end_keep: usize, expected: &[T]) { - let mut stack = crate_stack(data); - stack.truncate_keep(n, end_keep); - assert_eq!( - stack.data(), - expected.iter().map(|v| (*v).into()).collect::>().as_slice() - ); - } - - fn catch_unwind_silent R + panic::UnwindSafe, R>(f: F) -> crate::std::thread::Result { - let prev_hook = panic::take_hook(); - panic::set_hook(alloc::boxed::Box::new(|_| {})); - let result = panic::catch_unwind(f); - panic::set_hook(prev_hook); - result - } - - #[test] - fn test_truncate_keep() { - assert_truncate_keep(&[1, 2, 3], 1, 1, &[1, 2, 3]); - assert_truncate_keep(&[1], 1, 1, &[1]); - assert_truncate_keep(&[1, 2, 3], 2, 1, &[1, 3]); - assert_truncate_keep::(&[], 0, 0, &[]); - catch_unwind_silent(|| assert_truncate_keep(&[1, 2, 3], 4, 1, &[1, 3])).expect_err("should panic"); - } #[test] fn test_value_stack() { diff --git a/crates/tinywasm/src/runtime/value.rs b/crates/tinywasm/src/runtime/value.rs index 16fb42c..b25f634 100644 --- a/crates/tinywasm/src/runtime/value.rs +++ b/crates/tinywasm/src/runtime/value.rs @@ -17,15 +17,19 @@ impl Debug for RawWasmValue { } impl RawWasmValue { + pub fn raw_value(&self) -> u64 { + self.0 + } + pub fn attach_type(self, ty: ValType) -> WasmValue { match ty { ValType::I32 => WasmValue::I32(self.0 as i32), ValType::I64 => WasmValue::I64(self.0 as i64), ValType::F32 => WasmValue::F32(f32::from_bits(self.0 as u32)), ValType::F64 => WasmValue::F64(f64::from_bits(self.0)), - ValType::ExternRef => todo!(), - ValType::FuncRef => todo!(), - ValType::V128 => todo!(), + ValType::ExternRef => todo!("externref"), + ValType::FuncRef => todo!("funcref"), + ValType::V128 => todo!("v128"), } } } @@ -37,6 +41,7 @@ impl From for RawWasmValue { WasmValue::I64(i) => Self(i as u64), WasmValue::F32(i) => Self(i.to_bits() as u64), WasmValue::F64(i) => Self(i.to_bits()), + WasmValue::RefNull(_) => Self(0), } } } @@ -65,3 +70,6 @@ impl_from_raw_wasm_value!(i32, |x| x as u64, |x| x as i32); impl_from_raw_wasm_value!(i64, |x| x as u64, |x| x as i64); impl_from_raw_wasm_value!(f32, |x| f32::to_bits(x) as u64, |x| f32::from_bits(x as u32)); impl_from_raw_wasm_value!(f64, f64::to_bits, f64::from_bits); + +impl_from_raw_wasm_value!(i8, |x| x as u64, |x| x as i8); +impl_from_raw_wasm_value!(i16, |x| x as u64, |x| x as i16); diff --git a/crates/tinywasm/src/store.rs b/crates/tinywasm/src/store.rs index ff44d0d..564232d 100644 --- a/crates/tinywasm/src/store.rs +++ b/crates/tinywasm/src/store.rs @@ -1,11 +1,19 @@ -use core::sync::atomic::{AtomicUsize, Ordering}; +#![allow(dead_code)] // TODO: remove this -use alloc::{format, rc::Rc, vec::Vec}; -use tinywasm_types::{FuncAddr, Function, Instruction, ModuleInstanceAddr, TypeAddr, ValType}; +use core::{ + cell::RefCell, + sync::atomic::{AtomicUsize, Ordering}, +}; + +use alloc::{format, rc::Rc, string::ToString, vec, vec::Vec}; +use tinywasm_types::{ + Addr, Data, Element, ElementKind, FuncAddr, Function, Global, GlobalType, Import, Instruction, MemAddr, MemoryArch, + MemoryType, ModuleInstanceAddr, TableAddr, TableType, TypeAddr, ValType, +}; use crate::{ runtime::{self, DefaultRuntime}, - Error, ModuleInstance, Result, + Error, Extern, LinkedImports, ModuleInstance, RawWasmValue, Result, }; // global store id counter @@ -69,18 +77,254 @@ impl Default for Store { } } +#[derive(Debug, Default)] +/// Global state that can be manipulated by WebAssembly programs +/// +/// Data should only be addressable by the module that owns it +/// See +// TODO: Arena allocate these? +pub(crate) struct StoreData { + pub(crate) funcs: Vec>, + pub(crate) tables: Vec, + pub(crate) mems: Vec>>, + pub(crate) globals: Vec>>, + pub(crate) elems: Vec, + pub(crate) datas: Vec, +} + +impl Store { + /// Get the store's ID (unique per process) + pub fn id(&self) -> usize { + self.id + } + + pub(crate) fn next_module_instance_idx(&self) -> ModuleInstanceAddr { + self.module_instance_count as ModuleInstanceAddr + } + + /// Initialize the store with global state from the given module + pub(crate) fn add_instance(&mut self, instance: ModuleInstance) -> Result<()> { + self.module_instances.push(instance); + self.module_instance_count += 1; + Ok(()) + } + + /// Add functions to the store, returning their addresses in the store + pub(crate) fn add_funcs(&mut self, funcs: Vec, idx: ModuleInstanceAddr) -> Vec { + let func_count = self.data.funcs.len(); + let mut func_addrs = Vec::with_capacity(func_count); + for (i, func) in funcs.into_iter().enumerate() { + self.data.funcs.push(Rc::new(FunctionInstance { func, owner: idx })); + func_addrs.push((i + func_count) as FuncAddr); + } + func_addrs + } + + /// Add tables to the store, returning their addresses in the store + pub(crate) fn add_tables(&mut self, tables: Vec, idx: ModuleInstanceAddr) -> Vec { + let table_count = self.data.tables.len(); + let mut table_addrs = Vec::with_capacity(table_count); + for (i, table) in tables.into_iter().enumerate() { + self.data.tables.push(TableInstance::new(table, idx)); + table_addrs.push((i + table_count) as TableAddr); + } + table_addrs + } + + /// Add memories to the store, returning their addresses in the store + pub(crate) fn add_mems(&mut self, mems: Vec, idx: ModuleInstanceAddr) -> Result> { + let mem_count = self.data.mems.len(); + let mut mem_addrs = Vec::with_capacity(mem_count); + for (i, mem) in mems.into_iter().enumerate() { + if let MemoryArch::I64 = mem.arch { + return Err(Error::UnsupportedFeature("64-bit memories".to_string())); + } + self.data + .mems + .push(Rc::new(RefCell::new(MemoryInstance::new(mem, idx)))); + + mem_addrs.push((i + mem_count) as MemAddr); + } + Ok(mem_addrs) + } + + /// Add globals to the store, returning their addresses in the store + pub(crate) fn add_globals( + &mut self, + globals: Vec, + wasm_imports: &[Import], + user_imports: &LinkedImports, + idx: ModuleInstanceAddr, + ) -> Result> { + // TODO: initialize imported globals + #![allow(clippy::unnecessary_filter_map)] // this is cleaner + let imported_globals = wasm_imports + .iter() + .filter_map(|import| match &import.kind { + tinywasm_types::ImportKind::Global(_) => Some(import), + _ => None, + }) + .map(|import| { + let Some(global) = user_imports.get(&import.module, &import.name) else { + return Err(Error::Other(format!( + "global import not found for {}::{}", + import.module, import.name + ))); + }; + match global { + Extern::Global(global) => Ok(global), + + #[allow(unreachable_patterns)] // this is non-exhaustive + _ => Err(Error::Other(format!( + "expected global import for {}::{}", + import.module, import.name + ))), + } + }) + .collect::>>()?; + + let global_count = self.data.globals.len(); + let mut global_addrs = Vec::with_capacity(global_count); + + log::debug!("globals: {:?}", globals); + let globals = globals.into_iter(); + let iterator = imported_globals.into_iter().chain(globals.as_ref()); + + for (i, global) in iterator.enumerate() { + self.data.globals.push(Rc::new(RefCell::new(GlobalInstance::new( + global.ty, + self.eval_const(&global.init)?, + idx, + )))); + global_addrs.push((i + global_count) as Addr); + } + + Ok(global_addrs) + } + + pub(crate) fn eval_const(&self, const_instr: &tinywasm_types::ConstInstruction) -> Result { + use tinywasm_types::ConstInstruction::*; + let val = match const_instr { + F32Const(f) => RawWasmValue::from(*f), + F64Const(f) => RawWasmValue::from(*f), + I32Const(i) => RawWasmValue::from(*i), + I64Const(i) => RawWasmValue::from(*i), + GlobalGet(addr) => { + let addr = *addr as usize; + let global = self.data.globals[addr].clone(); + let val = global.borrow().value; + val + } + RefNull(_) => RawWasmValue::default(), + RefFunc(idx) => RawWasmValue::from(*idx as i64), + }; + Ok(val) + } + + /// Add elements to the store, returning their addresses in the store + /// Should be called after the tables have been added + pub(crate) fn add_elems(&mut self, elems: Vec, idx: ModuleInstanceAddr) -> Result> { + let elem_count = self.data.elems.len(); + let mut elem_addrs = Vec::with_capacity(elem_count); + for (i, elem) in elems.into_iter().enumerate() { + match elem.kind { + // doesn't need to be initialized, can be initialized lazily using the `table.init` instruction + ElementKind::Passive => {} + + // this one is active, so we need to initialize it (essentially a `table.init` instruction) + ElementKind::Active { .. } => { + // a. Let n be the length of the vector elem[i].init + // b. Execute the instruction sequence einstrs + // c. Execute the instruction i32.const 0 + // d. Execute the instruction i32.const n + // e. Execute the instruction table.init tableidx i + // f. Execute the instruction elm.drop i + } + + // this one is not available to the runtime but needs to be initialized to declare references + ElementKind::Declared => { + // a. Execute the instruction elm.drop i + } + } + + self.data.elems.push(ElemInstance::new(elem.kind, idx)); + elem_addrs.push((i + elem_count) as Addr); + } + + Ok(elem_addrs) + } + + /// Add data to the store, returning their addresses in the store + pub(crate) fn add_datas(&mut self, datas: Vec, idx: ModuleInstanceAddr) -> Vec { + let data_count = self.data.datas.len(); + let mut data_addrs = Vec::with_capacity(data_count); + for (i, data) in datas.into_iter().enumerate() { + self.data.datas.push(DataInstance::new(data.data.to_vec(), idx)); + data_addrs.push((i + data_count) as Addr); + + use tinywasm_types::DataKind::*; + match data.kind { + Active { .. } => { + // a. Assert: memidx == 0 + // b. Let n be the length of the vector + // c. Execute the instruction sequence + // d. Execute the instruction + // e. Execute the instruction + // f. Execute the instruction + // g. Execute the instruction + } + Passive => {} + } + } + data_addrs + } + + /// Get the function at the actual index in the store + pub(crate) fn get_func(&self, addr: usize) -> Result<&Rc> { + self.data + .funcs + .get(addr) + .ok_or_else(|| Error::Other(format!("function {} not found", addr))) + } + + /// Get the memory at the actual index in the store + pub(crate) fn get_mem(&self, addr: usize) -> Result<&Rc>> { + self.data + .mems + .get(addr) + .ok_or_else(|| Error::Other(format!("memory {} not found", addr))) + } + + /// Get the global at the actual index in the store + pub(crate) fn get_global_val(&self, addr: usize) -> Result { + self.data + .globals + .get(addr) + .ok_or_else(|| Error::Other(format!("global {} not found", addr))) + .map(|global| global.borrow().value) + } + + pub(crate) fn set_global_val(&mut self, addr: usize, value: RawWasmValue) -> Result<()> { + self.data + .globals + .get(addr) + .ok_or_else(|| Error::Other(format!("global {} not found", addr))) + .map(|global| global.borrow_mut().value = value) + } +} + #[derive(Debug)] /// A WebAssembly Function Instance /// /// See pub struct FunctionInstance { pub(crate) func: Function, - pub(crate) _module_instance: ModuleInstanceAddr, // index into store.module_instances + pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances } impl FunctionInstance { pub(crate) fn _module_instance_addr(&self) -> ModuleInstanceAddr { - self._module_instance + self.owner } pub(crate) fn locals(&self) -> &[ValType] { @@ -96,50 +340,166 @@ impl FunctionInstance { } } -#[derive(Debug, Default)] -/// Global state that can be manipulated by WebAssembly programs -pub(crate) struct StoreData { - pub(crate) funcs: Vec>, - // pub tables: Vec, - // pub mems: Vec, - // pub globals: Vec, - // pub elems: Vec, - // pub datas: Vec, +/// A WebAssembly Table Instance +/// +/// See +#[derive(Debug)] +pub(crate) struct TableInstance { + pub(crate) kind: TableType, + pub(crate) elements: Vec, + pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances } -impl Store { - /// Get the store's ID (unique per process) - pub fn id(&self) -> usize { - self.id +impl TableInstance { + pub(crate) fn new(kind: TableType, owner: ModuleInstanceAddr) -> Self { + Self { + elements: vec![0; kind.size_initial as usize], + kind, + owner, + } } +} - pub(crate) fn next_module_instance_idx(&self) -> ModuleInstanceAddr { - self.module_instance_count as ModuleInstanceAddr +pub(crate) const PAGE_SIZE: usize = 65536; +pub(crate) const MAX_PAGES: usize = 65536; +pub(crate) const MAX_SIZE: usize = PAGE_SIZE * MAX_PAGES; + +/// A WebAssembly Memory Instance +/// +/// See +#[derive(Debug)] +pub(crate) struct MemoryInstance { + pub(crate) kind: MemoryType, + pub(crate) data: Vec, + pub(crate) page_count: usize, + pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances +} + +impl MemoryInstance { + pub(crate) fn new(kind: MemoryType, owner: ModuleInstanceAddr) -> Self { + debug_assert!(kind.page_count_initial <= kind.page_count_max.unwrap_or(MAX_PAGES as u64)); + log::debug!("initializing memory with {} pages", kind.page_count_initial); + + Self { + kind, + data: vec![0; PAGE_SIZE * kind.page_count_initial as usize], + page_count: kind.page_count_initial as usize, + owner, + } } - /// Initialize the store with global state from the given module - pub(crate) fn add_instance(&mut self, instance: ModuleInstance) -> Result<()> { - self.module_instances.push(instance); - self.module_instance_count += 1; + pub(crate) fn store(&mut self, addr: usize, _align: usize, data: &[u8]) -> Result<()> { + let end = addr.checked_add(data.len()).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: addr, + len: data.len(), + max: self.data.len(), + }) + })?; + + if end > self.data.len() || end < addr { + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: addr, + len: data.len(), + max: self.data.len(), + })); + } + + // WebAssembly doesn't require alignment for stores + self.data[addr..end].copy_from_slice(data); Ok(()) } - pub(crate) fn add_funcs(&mut self, funcs: Vec, idx: ModuleInstanceAddr) -> Vec { - let mut func_addrs = Vec::with_capacity(funcs.len()); - for (i, func) in funcs.into_iter().enumerate() { - self.data.funcs.push(Rc::new(FunctionInstance { - func, - _module_instance: idx, + pub(crate) fn load(&self, addr: usize, _align: usize, len: usize) -> Result<&[u8]> { + let end = addr.checked_add(len).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: addr, + len, + max: self.data.len(), + }) + })?; + + if end > self.data.len() { + return Err(Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: addr, + len, + max: self.data.len(), })); - func_addrs.push(i as FuncAddr); } - func_addrs + + // WebAssembly doesn't require alignment for loads + Ok(&self.data[addr..end]) } - pub(crate) fn get_func(&self, addr: usize) -> Result<&Rc> { - self.data - .funcs - .get(addr) - .ok_or_else(|| Error::Other(format!("function {} not found", addr))) + pub(crate) fn size(&self) -> i32 { + log::debug!("memory pages: {}", self.page_count); + log::debug!("memory size: {}", self.page_count * PAGE_SIZE); + self.page_count as i32 + } + + pub(crate) fn grow(&mut self, delta: i32) -> Result { + let current_pages = self.size(); + let new_pages = current_pages + delta; + if new_pages < 0 || new_pages > MAX_PAGES as i32 { + return Err(Error::Other(format!("memory size out of bounds: {}", new_pages))); + } + let new_size = new_pages as usize * PAGE_SIZE; + if new_size > MAX_SIZE { + return Err(Error::Other(format!("memory size out of bounds: {}", new_size))); + } + self.data.resize(new_size, 0); + self.page_count = new_pages as usize; + + log::debug!("memory was {} pages", current_pages); + log::debug!("memory grown by {} pages", delta); + log::debug!("memory grown to {} pages", self.page_count); + + Ok(current_pages) + } +} + +/// A WebAssembly Global Instance +/// +/// See +#[derive(Debug)] +pub(crate) struct GlobalInstance { + pub(crate) ty: GlobalType, + pub(crate) value: RawWasmValue, + owner: ModuleInstanceAddr, // index into store.module_instances +} + +impl GlobalInstance { + pub(crate) fn new(ty: GlobalType, value: RawWasmValue, owner: ModuleInstanceAddr) -> Self { + Self { ty, value, owner } + } +} + +/// A WebAssembly Element Instance +/// +/// See +#[derive(Debug)] +pub(crate) struct ElemInstance { + kind: ElementKind, + owner: ModuleInstanceAddr, // index into store.module_instances +} + +impl ElemInstance { + pub(crate) fn new(kind: ElementKind, owner: ModuleInstanceAddr) -> Self { + Self { kind, owner } + } +} + +/// A WebAssembly Data Instance +/// +/// See +#[derive(Debug)] +pub(crate) struct DataInstance { + pub(crate) data: Vec, + owner: ModuleInstanceAddr, // index into store.module_instances +} + +impl DataInstance { + pub(crate) fn new(data: Vec, owner: ModuleInstanceAddr) -> Self { + Self { data, owner } } } diff --git a/crates/tinywasm/tests/generate-charts.rs b/crates/tinywasm/tests/generate-charts.rs index f80b092..e8e323d 100644 --- a/crates/tinywasm/tests/generate-charts.rs +++ b/crates/tinywasm/tests/generate-charts.rs @@ -6,11 +6,18 @@ fn main() -> Result<()> { } fn generate_charts() -> Result<()> { + let args = std::env::args().collect::>(); + if args.len() < 2 || args[1] != "--enable" { + return Ok(()); + } + // Create a line chart charts::create_progress_chart( std::path::Path::new("./tests/generated/mvp.csv"), std::path::Path::new("./tests/generated/progress-mvp.svg"), )?; + println!("created progress chart: ./tests/generated/progress-mvp.svg"); + Ok(()) } diff --git a/crates/tinywasm/tests/generated/mvp.csv b/crates/tinywasm/tests/generated/mvp.csv index c18acc5..a238586 100644 --- a/crates/tinywasm/tests/generated/mvp.csv +++ b/crates/tinywasm/tests/generated/mvp.csv @@ -1,4 +1,5 @@ 0.0.3,9258,7567,[{"name":"address.wast","passed":0,"failed":54},{"name":"align.wast","passed":0,"failed":109},{"name":"binary-leb128.wast","passed":66,"failed":25},{"name":"binary.wast","passed":104,"failed":8},{"name":"block.wast","passed":0,"failed":171},{"name":"br.wast","passed":0,"failed":21},{"name":"br_if.wast","passed":0,"failed":30},{"name":"br_table.wast","passed":0,"failed":25},{"name":"call.wast","passed":0,"failed":22},{"name":"call_indirect.wast","passed":0,"failed":56},{"name":"comments.wast","passed":4,"failed":4},{"name":"const.wast","passed":702,"failed":76},{"name":"conversions.wast","passed":0,"failed":93},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":0,"failed":61},{"name":"elem.wast","passed":0,"failed":76},{"name":"endianness.wast","passed":0,"failed":1},{"name":"exports.wast","passed":21,"failed":73},{"name":"f32.wast","passed":1005,"failed":1509},{"name":"f32_bitwise.wast","passed":1,"failed":363},{"name":"f32_cmp.wast","passed":2401,"failed":6},{"name":"f64.wast","passed":1005,"failed":1509},{"name":"f64_bitwise.wast","passed":1,"failed":363},{"name":"f64_cmp.wast","passed":2401,"failed":6},{"name":"fac.wast","passed":0,"failed":2},{"name":"float_exprs.wast","passed":269,"failed":591},{"name":"float_literals.wast","passed":34,"failed":129},{"name":"float_memory.wast","passed":0,"failed":6},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":4,"failed":75},{"name":"func_ptrs.wast","passed":0,"failed":16},{"name":"global.wast","passed":4,"failed":49},{"name":"i32.wast","passed":0,"failed":96},{"name":"i64.wast","passed":0,"failed":42},{"name":"if.wast","passed":0,"failed":118},{"name":"imports.wast","passed":1,"failed":156},{"name":"inline-module.wast","passed":0,"failed":1},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":5,"failed":46},{"name":"labels.wast","passed":1,"failed":28},{"name":"left-to-right.wast","passed":0,"failed":1},{"name":"linking.wast","passed":1,"failed":66},{"name":"load.wast","passed":0,"failed":60},{"name":"local_get.wast","passed":2,"failed":34},{"name":"local_set.wast","passed":5,"failed":48},{"name":"local_tee.wast","passed":0,"failed":42},{"name":"loop.wast","passed":0,"failed":43},{"name":"memory.wast","passed":0,"failed":34},{"name":"memory_grow.wast","passed":0,"failed":19},{"name":"memory_redundancy.wast","passed":0,"failed":1},{"name":"memory_size.wast","passed":0,"failed":6},{"name":"memory_trap.wast","passed":0,"failed":172},{"name":"names.wast","passed":484,"failed":1},{"name":"nop.wast","passed":0,"failed":5},{"name":"return.wast","passed":0,"failed":21},{"name":"select.wast","passed":0,"failed":32},{"name":"skip-stack-guard-page.wast","passed":0,"failed":11},{"name":"stack.wast","passed":0,"failed":2},{"name":"start.wast","passed":0,"failed":10},{"name":"store.wast","passed":0,"failed":59},{"name":"switch.wast","passed":1,"failed":27},{"name":"token.wast","passed":16,"failed":42},{"name":"traps.wast","passed":3,"failed":33},{"name":"type.wast","passed":1,"failed":2},{"name":"unreachable.wast","passed":0,"failed":59},{"name":"unreached-invalid.wast","passed":0,"failed":118},{"name":"unwind.wast","passed":1,"failed":49},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":0,"failed":176}] 0.0.4,9258,10909,[{"name":"address.wast","passed":0,"failed":54},{"name":"align.wast","passed":0,"failed":109},{"name":"binary-leb128.wast","passed":66,"failed":25},{"name":"binary.wast","passed":104,"failed":8},{"name":"block.wast","passed":0,"failed":171},{"name":"br.wast","passed":0,"failed":21},{"name":"br_if.wast","passed":0,"failed":30},{"name":"br_table.wast","passed":0,"failed":25},{"name":"call.wast","passed":0,"failed":22},{"name":"call_indirect.wast","passed":0,"failed":56},{"name":"comments.wast","passed":4,"failed":4},{"name":"const.wast","passed":702,"failed":76},{"name":"conversions.wast","passed":0,"failed":93},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":0,"failed":61},{"name":"elem.wast","passed":0,"failed":76},{"name":"endianness.wast","passed":0,"failed":1},{"name":"exports.wast","passed":21,"failed":73},{"name":"f32.wast","passed":1005,"failed":1509},{"name":"f32_bitwise.wast","passed":1,"failed":363},{"name":"f32_cmp.wast","passed":2401,"failed":6},{"name":"f64.wast","passed":1005,"failed":1509},{"name":"f64_bitwise.wast","passed":1,"failed":363},{"name":"f64_cmp.wast","passed":2401,"failed":6},{"name":"fac.wast","passed":0,"failed":2},{"name":"float_exprs.wast","passed":269,"failed":591},{"name":"float_literals.wast","passed":34,"failed":129},{"name":"float_memory.wast","passed":0,"failed":6},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":4,"failed":75},{"name":"func_ptrs.wast","passed":0,"failed":16},{"name":"global.wast","passed":4,"failed":49},{"name":"i32.wast","passed":0,"failed":96},{"name":"i64.wast","passed":0,"failed":42},{"name":"if.wast","passed":0,"failed":118},{"name":"imports.wast","passed":1,"failed":156},{"name":"inline-module.wast","passed":0,"failed":1},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":5,"failed":46},{"name":"labels.wast","passed":1,"failed":28},{"name":"left-to-right.wast","passed":0,"failed":1},{"name":"linking.wast","passed":1,"failed":66},{"name":"load.wast","passed":0,"failed":60},{"name":"local_get.wast","passed":2,"failed":34},{"name":"local_set.wast","passed":5,"failed":48},{"name":"local_tee.wast","passed":0,"failed":42},{"name":"loop.wast","passed":0,"failed":43},{"name":"memory.wast","passed":0,"failed":34},{"name":"memory_grow.wast","passed":0,"failed":19},{"name":"memory_redundancy.wast","passed":0,"failed":1},{"name":"memory_size.wast","passed":0,"failed":6},{"name":"memory_trap.wast","passed":0,"failed":172},{"name":"names.wast","passed":484,"failed":1},{"name":"nop.wast","passed":0,"failed":5},{"name":"return.wast","passed":0,"failed":21},{"name":"select.wast","passed":0,"failed":32},{"name":"skip-stack-guard-page.wast","passed":0,"failed":11},{"name":"stack.wast","passed":0,"failed":2},{"name":"start.wast","passed":0,"failed":10},{"name":"store.wast","passed":0,"failed":59},{"name":"switch.wast","passed":1,"failed":27},{"name":"token.wast","passed":16,"failed":42},{"name":"traps.wast","passed":3,"failed":33},{"name":"type.wast","passed":1,"failed":2},{"name":"unreachable.wast","passed":0,"failed":59},{"name":"unreached-invalid.wast","passed":0,"failed":118},{"name":"unwind.wast","passed":1,"failed":49},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":0,"failed":176}] 0.0.5,11135,9093,[{"name":"address.wast","passed":1,"failed":259},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":78,"failed":13},{"name":"binary.wast","passed":107,"failed":5},{"name":"block.wast","passed":170,"failed":53},{"name":"br.wast","passed":20,"failed":77},{"name":"br_if.wast","passed":29,"failed":89},{"name":"br_table.wast","passed":24,"failed":150},{"name":"call.wast","passed":18,"failed":73},{"name":"call_indirect.wast","passed":34,"failed":136},{"name":"comments.wast","passed":5,"failed":3},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":25,"failed":594},{"name":"custom.wast","passed":10,"failed":1},{"name":"data.wast","passed":22,"failed":39},{"name":"elem.wast","passed":27,"failed":72},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":90,"failed":6},{"name":"f32.wast","passed":1018,"failed":1496},{"name":"f32_bitwise.wast","passed":4,"failed":360},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":1018,"failed":1496},{"name":"f64_bitwise.wast","passed":4,"failed":360},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":1,"failed":7},{"name":"float_exprs.wast","passed":275,"failed":625},{"name":"float_literals.wast","passed":112,"failed":51},{"name":"float_memory.wast","passed":0,"failed":90},{"name":"float_misc.wast","passed":138,"failed":303},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":81,"failed":91},{"name":"func_ptrs.wast","passed":7,"failed":29},{"name":"global.wast","passed":50,"failed":60},{"name":"i32.wast","passed":85,"failed":375},{"name":"i64.wast","passed":31,"failed":385},{"name":"if.wast","passed":116,"failed":125},{"name":"imports.wast","passed":23,"failed":160},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":38,"failed":70},{"name":"int_literals.wast","passed":25,"failed":26},{"name":"labels.wast","passed":13,"failed":16},{"name":"left-to-right.wast","passed":0,"failed":96},{"name":"linking.wast","passed":5,"failed":127},{"name":"load.wast","passed":59,"failed":38},{"name":"local_get.wast","passed":18,"failed":18},{"name":"local_set.wast","passed":38,"failed":15},{"name":"local_tee.wast","passed":41,"failed":56},{"name":"loop.wast","passed":42,"failed":78},{"name":"memory.wast","passed":30,"failed":49},{"name":"memory_grow.wast","passed":11,"failed":85},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":1,"failed":181},{"name":"names.wast","passed":484,"failed":2},{"name":"nop.wast","passed":4,"failed":84},{"name":"return.wast","passed":20,"failed":64},{"name":"select.wast","passed":28,"failed":120},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":4,"failed":16},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":39,"failed":19},{"name":"traps.wast","passed":4,"failed":32},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":0,"failed":64},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":9,"failed":41},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] -0.0.6-alpha.0,17630,2598,[{"name":"address.wast","passed":5,"failed":255},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":110,"failed":2},{"name":"block.wast","passed":193,"failed":30},{"name":"br.wast","passed":84,"failed":13},{"name":"br_if.wast","passed":90,"failed":28},{"name":"br_table.wast","passed":25,"failed":149},{"name":"call.wast","passed":29,"failed":62},{"name":"call_indirect.wast","passed":36,"failed":134},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":371,"failed":248},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":50,"failed":49},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":2,"failed":6},{"name":"float_exprs.wast","passed":761,"failed":139},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":6,"failed":84},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":124,"failed":48},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":51,"failed":59},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":120,"failed":121},{"name":"imports.wast","passed":74,"failed":109},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":14,"failed":15},{"name":"left-to-right.wast","passed":1,"failed":95},{"name":"linking.wast","passed":21,"failed":111},{"name":"load.wast","passed":60,"failed":37},{"name":"local_get.wast","passed":32,"failed":4},{"name":"local_set.wast","passed":50,"failed":3},{"name":"local_tee.wast","passed":68,"failed":29},{"name":"loop.wast","passed":93,"failed":27},{"name":"memory.wast","passed":34,"failed":45},{"name":"memory_grow.wast","passed":12,"failed":84},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":2,"failed":180},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":46,"failed":42},{"name":"return.wast","passed":73,"failed":11},{"name":"select.wast","passed":86,"failed":62},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":9,"failed":11},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":22,"failed":14},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":50,"failed":14},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":35,"failed":15},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.1.0,17630,2598,[{"name":"address.wast","passed":5,"failed":255},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":110,"failed":2},{"name":"block.wast","passed":193,"failed":30},{"name":"br.wast","passed":84,"failed":13},{"name":"br_if.wast","passed":90,"failed":28},{"name":"br_table.wast","passed":25,"failed":149},{"name":"call.wast","passed":29,"failed":62},{"name":"call_indirect.wast","passed":36,"failed":134},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":371,"failed":248},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":50,"failed":49},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":2,"failed":6},{"name":"float_exprs.wast","passed":761,"failed":139},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":6,"failed":84},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":124,"failed":48},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":51,"failed":59},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":120,"failed":121},{"name":"imports.wast","passed":74,"failed":109},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":14,"failed":15},{"name":"left-to-right.wast","passed":1,"failed":95},{"name":"linking.wast","passed":21,"failed":111},{"name":"load.wast","passed":60,"failed":37},{"name":"local_get.wast","passed":32,"failed":4},{"name":"local_set.wast","passed":50,"failed":3},{"name":"local_tee.wast","passed":68,"failed":29},{"name":"loop.wast","passed":93,"failed":27},{"name":"memory.wast","passed":34,"failed":45},{"name":"memory_grow.wast","passed":12,"failed":84},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":2,"failed":180},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":46,"failed":42},{"name":"return.wast","passed":73,"failed":11},{"name":"select.wast","passed":86,"failed":62},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":9,"failed":11},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":22,"failed":14},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":50,"failed":14},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":35,"failed":15},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.2.0-alpha.0,19344,884,[{"name":"address.wast","passed":181,"failed":79},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":220,"failed":3},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":171,"failed":3},{"name":"call.wast","passed":73,"failed":18},{"name":"call_indirect.wast","passed":50,"failed":120},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":439,"failed":180},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":56,"failed":43},{"name":"endianness.wast","passed":29,"failed":40},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":6,"failed":2},{"name":"float_exprs.wast","passed":890,"failed":10},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":78,"failed":12},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":168,"failed":4},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":103,"failed":7},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":231,"failed":10},{"name":"imports.wast","passed":80,"failed":103},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":26,"failed":3},{"name":"left-to-right.wast","passed":92,"failed":4},{"name":"linking.wast","passed":29,"failed":103},{"name":"load.wast","passed":93,"failed":4},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":93,"failed":4},{"name":"loop.wast","passed":116,"failed":4},{"name":"memory.wast","passed":78,"failed":1},{"name":"memory_grow.wast","passed":91,"failed":5},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":35,"failed":7},{"name":"memory_trap.wast","passed":180,"failed":2},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":78,"failed":10},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":114,"failed":34},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":11,"failed":9},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] diff --git a/crates/tinywasm/tests/generated/progress-mvp.svg b/crates/tinywasm/tests/generated/progress-mvp.svg index 331b0f8..e1fd709 100644 --- a/crates/tinywasm/tests/generated/progress-mvp.svg +++ b/crates/tinywasm/tests/generated/progress-mvp.svg @@ -36,24 +36,29 @@ TinyWasm Version - + v0.0.3 (9258) - - + + v0.0.4 (9258) - - + + v0.0.5 (11135) - - -v0.0.6-alpha.0 (17630) + + +v0.1.0 (17630) - - - - - + + +v0.2.0-alpha.0 (19344) + + + + + + + diff --git a/crates/tinywasm/tests/test-mvp.rs b/crates/tinywasm/tests/test-mvp.rs index fdd043d..5107551 100644 --- a/crates/tinywasm/tests/test-mvp.rs +++ b/crates/tinywasm/tests/test-mvp.rs @@ -1,20 +1,32 @@ mod testsuite; use eyre::{eyre, Result}; +use owo_colors::OwoColorize; use testsuite::TestSuite; fn main() -> Result<()> { + let args = std::env::args().collect::>(); + if args.len() < 2 || args[1] != "--enable" { + return Ok(()); + } + test_mvp() } fn test_mvp() -> Result<()> { let mut test_suite = TestSuite::new(); + TestSuite::set_log_level(log::LevelFilter::Off); test_suite.run_spec_group(wasm_testsuite::MVP_TESTS)?; + test_suite.save_csv("./tests/generated/mvp.csv", env!("CARGO_PKG_VERSION"))?; if test_suite.failed() { - eprintln!("\n\nfailed one or more tests:\n{:#?}", test_suite); - Err(eyre!("failed one or more tests")) + println!(); + Err(eyre!(format!( + "{}:\n{:#?}", + "failed one or more tests".red().bold(), + test_suite, + ))) } else { println!("\n\npassed all tests:\n{:#?}", test_suite); Ok(()) diff --git a/crates/tinywasm/tests/test-wast.rs b/crates/tinywasm/tests/test-wast.rs index 68372a0..42f7091 100644 --- a/crates/tinywasm/tests/test-wast.rs +++ b/crates/tinywasm/tests/test-wast.rs @@ -1,14 +1,19 @@ use std::path::PathBuf; -use eyre::{bail, Result}; +use eyre::{bail, eyre, Result}; +use owo_colors::OwoColorize; use testsuite::TestSuite; mod testsuite; fn main() -> Result<()> { let args = std::env::args().collect::>(); - if args.len() < 2 { - bail!("usage: cargo test-wast ") + if args.len() < 2 || args[1] != "--enable" { + return Ok(()); + } + + if args.len() < 3 { + bail!("usage: cargo test-wast "); } // cwd for relative paths, absolute paths are kept as-is @@ -21,7 +26,7 @@ fn main() -> Result<()> { PathBuf::from("./") }; - wast_file.push(&args[1]); + wast_file.push(&args[2]); let wast_file = cwd.join(wast_file); test_wast(wast_file.to_str().expect("wast_file is not a valid path"))?; @@ -40,9 +45,14 @@ fn test_wast(wast_file: &str) -> Result<()> { test_suite.run_paths(&[wast_file])?; if test_suite.failed() { - eprintln!("\n\nfailed one or more tests:\n{:#?}", test_suite); + println!(); test_suite.print_errors(); - bail!("failed one or more tests") + println!(); + Err(eyre!(format!( + "{}:\n{:#?}", + "failed one or more tests".red().bold(), + test_suite, + ))) } else { println!("\n\npassed all tests:\n{:#?}", test_suite); Ok(()) diff --git a/crates/tinywasm/tests/testsuite/indexmap.rs b/crates/tinywasm/tests/testsuite/indexmap.rs new file mode 100644 index 0000000..d4e3869 --- /dev/null +++ b/crates/tinywasm/tests/testsuite/indexmap.rs @@ -0,0 +1,53 @@ +pub struct IndexMap { + map: std::collections::HashMap, + keys: Vec, +} + +impl IndexMap +where + K: std::cmp::Eq + std::hash::Hash + Clone, +{ + pub fn new() -> Self { + Self { + map: std::collections::HashMap::new(), + keys: Vec::new(), + } + } + + pub fn insert(&mut self, key: K, value: V) -> Option { + if let std::collections::hash_map::Entry::Occupied(mut e) = self.map.entry(key.clone()) { + return Some(e.insert(value)); + } + + self.keys.push(key.clone()); + self.map.insert(key, value) + } + + pub fn get(&self, key: &K) -> Option<&V> { + self.map.get(key) + } + + pub fn get_mut(&mut self, key: &K) -> Option<&mut V> { + self.map.get_mut(key) + } + + pub fn iter(&self) -> impl Iterator { + self.keys.iter().map(move |k| (k, self.map.get(k).unwrap())) + } + + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn keys(&self) -> impl Iterator { + self.keys.iter() + } + + pub fn values(&self) -> impl Iterator { + self.map.values() + } + + pub fn values_mut(&mut self) -> impl Iterator { + self.map.values_mut() + } +} diff --git a/crates/tinywasm/tests/testsuite/mod.rs b/crates/tinywasm/tests/testsuite/mod.rs index fd3e39f..36fc727 100644 --- a/crates/tinywasm/tests/testsuite/mod.rs +++ b/crates/tinywasm/tests/testsuite/mod.rs @@ -1,6 +1,7 @@ #![allow(dead_code)] // rust analyzer doesn't recognize that code is used by tests without harness use eyre::Result; +use owo_colors::OwoColorize; use std::io::{BufRead, Seek, SeekFrom}; use std::{ collections::BTreeMap, @@ -8,11 +9,14 @@ use std::{ io::BufReader, }; +mod indexmap; mod run; mod util; use serde::{Deserialize, Serialize}; +use self::indexmap::IndexMap; + #[derive(Serialize, Deserialize)] pub struct TestGroupResult { pub name: String, @@ -20,33 +24,49 @@ pub struct TestGroupResult { pub failed: usize, } -pub struct TestSuite(BTreeMap); +fn format_linecol(linecol: (usize, usize)) -> String { + format!("{}:{}", linecol.0 + 1, linecol.1 + 1) +} + +pub struct TestSuite(BTreeMap, Vec); impl TestSuite { + pub fn skip(&mut self, groups: &[&str]) { + self.1.extend(groups.iter().map(|s| s.to_string())); + } + pub fn set_log_level(level: log::LevelFilter) { pretty_env_logger::formatted_builder().filter_level(level).init(); } pub fn print_errors(&self) { for (group_name, group) in &self.0 { - for (test_name, test) in &group.tests { + let tests = &group.tests; + for (test_name, test) in tests.iter() { if let Err(e) = &test.result { - eprintln!("{}: {} failed: {:?}", group_name, test_name, e); + eprintln!( + "{} {} failed: {:?}", + link(group_name, &group.file, Some(test.linecol.0 + 1)) + .bold() + .underline(), + test_name.bold(), + e.to_string().bright_red() + ); } } } } pub fn new() -> Self { - Self(BTreeMap::new()) + Self(BTreeMap::new(), Vec::new()) } pub fn failed(&self) -> bool { self.0.values().any(|group| group.stats().1 > 0) } - fn test_group(&mut self, name: &str) -> &mut TestGroup { - self.0.entry(name.to_string()).or_insert_with(TestGroup::new) + fn test_group(&mut self, name: &str, file: &str) -> &mut TestGroup { + self.0.entry(name.to_string()).or_insert_with(|| TestGroup::new(file)) } // create or add to a test result file @@ -93,9 +113,17 @@ impl TestSuite { } } +fn link(name: &str, file: &str, line: Option) -> String { + let (path, name) = match line { + None => (file.to_string(), name.to_owned()), + Some(line) => (format!("{}:{}:0", file, line), (format!("{}:{}", name, line))), + }; + + format!("\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", path, name) +} + impl Debug for TestSuite { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - use owo_colors::OwoColorize; let mut total_passed = 0; let mut total_failed = 0; @@ -104,7 +132,7 @@ impl Debug for TestSuite { total_passed += group_passed; total_failed += group_failed; - writeln!(f, "{}", group_name.bold().underline())?; + writeln!(f, "{}", link(group_name, &group.file, None).bold().underline())?; writeln!(f, " Tests Passed: {}", group_passed.to_string().green())?; writeln!(f, " Tests Failed: {}", group_failed.to_string().red())?; @@ -132,12 +160,16 @@ impl Debug for TestSuite { } struct TestGroup { - tests: BTreeMap, + tests: IndexMap, + file: String, } impl TestGroup { - fn new() -> Self { - Self { tests: BTreeMap::new() } + fn new(file: &str) -> Self { + Self { + tests: IndexMap::new(), + file: file.to_string(), + } } fn stats(&self) -> (usize, usize) { @@ -154,12 +186,13 @@ impl TestGroup { (passed_count, failed_count) } - fn add_result(&mut self, name: &str, span: wast::token::Span, result: Result<()>) { - self.tests.insert(name.to_string(), TestCase { result, _span: span }); + fn add_result(&mut self, name: &str, linecol: (usize, usize), result: Result<()>) { + self.tests.insert(name.to_string(), TestCase { result, linecol }); } } +#[derive(Debug)] struct TestCase { result: Result<()>, - _span: wast::token::Span, + linecol: (usize, usize), } diff --git a/crates/tinywasm/tests/testsuite/run.rs b/crates/tinywasm/tests/testsuite/run.rs index fc3d7bc..8a1ddf4 100644 --- a/crates/tinywasm/tests/testsuite/run.rs +++ b/crates/tinywasm/tests/testsuite/run.rs @@ -1,10 +1,14 @@ use crate::testsuite::util::*; -use std::borrow::Cow; +use std::{ + borrow::Cow, + panic::{catch_unwind, AssertUnwindSafe}, +}; use super::TestSuite; use eyre::{eyre, Result}; use log::{debug, error, info}; -use tinywasm_types::TinyWasmModule; +use tinywasm::{Extern, Imports, ModuleInstance}; +use tinywasm_types::{ModuleInstanceAddr, WasmValue}; use wast::{lexer::Lexer, parser::ParseBuffer, Wast}; impl TestSuite { @@ -12,16 +16,37 @@ impl TestSuite { tests.iter().for_each(|group| { let group_wast = std::fs::read(group).expect("failed to read test wast"); let group_wast = Cow::Owned(group_wast); - debug!("running group: {}", group); self.run_group(group, group_wast).expect("failed to run group"); }); Ok(()) } + fn imports(registered_modules: Vec<(String, ModuleInstanceAddr)>) -> Result { + let mut imports = Imports::new(); + + imports + .define("spectest", "global_i32", Extern::global(WasmValue::I32(666), false))? + .define("spectest", "global_i64", Extern::global(WasmValue::I64(666), false))? + .define("spectest", "global_f32", Extern::global(WasmValue::F32(666.0), false))? + .define("spectest", "global_f64", Extern::global(WasmValue::F64(666.0), false))?; + + for (name, addr) in registered_modules { + imports.link_module(&name, addr)?; + } + + Ok(imports) + } + pub fn run_spec_group(&mut self, tests: &[&str]) -> Result<()> { tests.iter().for_each(|group| { let group_wast = wasm_testsuite::get_test_wast(group).expect("failed to get test wast"); + if self.1.contains(&group.to_string()) { + info!("skipping group: {}", group); + self.test_group(&format!("{} (skipped)", group), group); + return; + } + self.run_group(group, group_wast).expect("failed to run group"); }); @@ -29,7 +54,8 @@ impl TestSuite { } pub fn run_group(&mut self, group_name: &str, group_wast: Cow<'_, [u8]>) -> Result<()> { - let test_group = self.test_group(group_name); + let file_name = group_name.split('/').last().unwrap_or(group_name); + let test_group = self.test_group(file_name, group_name); let wast = std::str::from_utf8(&group_wast).expect("failed to convert wast to utf8"); let mut lexer = Lexer::new(wast); @@ -39,22 +65,46 @@ impl TestSuite { let buf = ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer"); let wast_data = wast::parser::parse::(&buf).expect("failed to parse wat"); - let mut last_module: Option = None; - debug!("running {} tests for group: {}", wast_data.directives.len(), group_name); + let mut store = tinywasm::Store::default(); + let mut registered_modules = Vec::new(); + let mut last_module: Option = None; + + println!("running {} tests for group: {}", wast_data.directives.len(), group_name); for (i, directive) in wast_data.directives.into_iter().enumerate() { let span = directive.span(); use wast::WastDirective::*; - let name = format!("{}-{}", group_name, i); - - debug!("directive: {:?}", directive); match directive { + Register { span, name, .. } => { + let Some(last) = &last_module else { + test_group.add_result( + &format!("Register({})", i), + span.linecol_in(wast), + Err(eyre!("no module to register")), + ); + continue; + }; + + registered_modules.push((name.to_string(), last.id())); + test_group.add_result(&format!("Register({})", i), span.linecol_in(wast), Ok(())); + } + Wat(mut module) => { + // TODO: modules are not properly isolated from each other - tests fail because of this otherwise + store = tinywasm::Store::default(); debug!("got wat module"); - - let result = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap())) - .map_err(|e| eyre!("failed to parse module: {:?}", e)) - .and_then(|res| res); + let result = catch_unwind(AssertUnwindSafe(|| { + let m = parse_module_bytes(&module.encode().expect("failed to encode module")) + .expect("failed to parse module bytes"); + tinywasm::Module::from(m) + .instantiate(&mut store, Some(Self::imports(registered_modules.clone()).unwrap())) + .map_err(|e| { + println!("failed to instantiate module: {:?}", e); + e + }) + .expect("failed to instantiate module") + })) + .map_err(|e| eyre!("failed to parse wat module: {:?}", try_downcast_panic(e))); match &result { Err(_) => last_module = None, @@ -65,7 +115,7 @@ impl TestSuite { debug!("failed to parse module: {:?}", err) } - test_group.add_result(&format!("{}-parse", name), span, result.map(|_| ())); + test_group.add_result(&format!("Wat({})", i), span.linecol_in(wast), result.map(|_| ())); } AssertMalformed { @@ -74,17 +124,17 @@ impl TestSuite { message: _, } => { let Ok(module) = module.encode() else { - test_group.add_result(&format!("{}-malformed", name), span, Ok(())); + test_group.add_result(&format!("AssertMalformed({})", i), span.linecol_in(wast), Ok(())); continue; }; let res = catch_unwind_silent(|| parse_module_bytes(&module)) - .map_err(|e| eyre!("failed to parse module: {:?}", e)) + .map_err(|e| eyre!("failed to parse module (expected): {:?}", try_downcast_panic(e))) .and_then(|res| res); test_group.add_result( - &format!("{}-malformed", name), - span, + &format!("AssertMalformed({})", i), + span.linecol_in(wast), match res { Ok(_) => Err(eyre!("expected module to be malformed")), Err(_) => Ok(()), @@ -98,12 +148,12 @@ impl TestSuite { message: _, } => { let res = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap())) - .map_err(|e| eyre!("failed to parse module: {:?}", e)) + .map_err(|e| eyre!("failed to parse module (invalid): {:?}", try_downcast_panic(e))) .and_then(|res| res); test_group.add_result( - &format!("{}-invalid", name), - span, + &format!("AssertInvalid({})", i), + span.linecol_in(wast), match res { Ok(_) => Err(eyre!("expected module to be invalid")), Err(_) => Ok(()), @@ -114,11 +164,18 @@ impl TestSuite { AssertTrap { exec, message: _, span } => { let res: Result, _> = catch_unwind_silent(|| { let (module, name, args) = match exec { - wast::WastExecute::Wat(_wat) => { - panic!("wat not supported"); + wast::WastExecute::Wat(mut wat) => { + let module = parse_module_bytes(&wat.encode().expect("failed to encode module")) + .expect("failed to parse module"); + let module = tinywasm::Module::from(module); + module.instantiate( + &mut store, + Some(Self::imports(registered_modules.clone()).unwrap()), + )?; + return Ok(()); } wast::WastExecute::Get { module: _, global: _ } => { - panic!("wat not supported"); + panic!("get not supported"); } wast::WastExecute::Invoke(invoke) => (last_module.as_ref(), invoke.name, invoke.args), }; @@ -128,48 +185,34 @@ impl TestSuite { .collect::>>() .expect("failed to convert args"); - exec_fn(module, name, &args).map(|_| ()) + exec_fn_instance(module, &mut store, name, &args).map(|_| ()) }); match res { Err(err) => test_group.add_result( - &format!("{}-trap", name), - span, - Err(eyre!("test panicked: {:?}, span: {:?}", err, span.linecol_in(&wast))), + &format!("AssertTrap({})", i), + span.linecol_in(wast), + Err(eyre!("test panicked: {:?}", try_downcast_panic(err))), ), Ok(Err(tinywasm::Error::Trap(_))) => { - test_group.add_result(&format!("{}-trap", name), span, Ok(())) + test_group.add_result(&format!("AssertTrap({})", i), span.linecol_in(wast), Ok(())) } Ok(Err(err)) => test_group.add_result( - &format!("{}-trap", name), - span, - Err(eyre!( - "expected trap, got error: {:?}, span: {:?}", - err, - span.linecol_in(&wast) - )), + &format!("AssertTrap({})", i), + span.linecol_in(wast), + Err(eyre!("expected trap, got error: {:?}", err,)), ), Ok(Ok(())) => test_group.add_result( - &format!("{}-trap", name), - span, - Err(eyre!("expected trap, got ok, span: {:?}", span.linecol_in(&wast))), + &format!("AssertTrap({})", i), + span.linecol_in(wast), + Err(eyre!("expected trap, got ok")), ), } } - AssertReturn { span, exec, results } => { - info!("AssertReturn: {:?}", exec); + Invoke(invoke) => { + let name = invoke.name; let res: Result, _> = catch_unwind_silent(|| { - let invoke = match exec { - wast::WastExecute::Wat(_) => { - error!("wat not supported"); - return Err(eyre!("wat not supported")); - } - wast::WastExecute::Get { module: _, global: _ } => return Err(eyre!("get not supported")), - wast::WastExecute::Invoke(invoke) => invoke, - }; - - debug!("invoke: {:?}", invoke); let args = invoke .args .into_iter() @@ -180,10 +223,56 @@ impl TestSuite { e })?; - let outcomes = exec_fn(last_module.as_ref(), invoke.name, &args).map_err(|e| { + exec_fn_instance(last_module.as_ref(), &mut store, invoke.name, &args).map_err(|e| { error!("failed to execute function: {:?}", e); e })?; + Ok(()) + }); + + let res = res + .map_err(|e| eyre!("test panicked: {:?}", try_downcast_panic(e))) + .and_then(|r| r); + + test_group.add_result(&format!("Invoke({}-{})", name, i), span.linecol_in(wast), res); + } + + AssertReturn { span, exec, results } => { + info!("AssertReturn: {:?}", exec); + let invoke = match match exec { + wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")), + wast::WastExecute::Get { module: _, global: _ } => Err(eyre!("get not supported")), + wast::WastExecute::Invoke(invoke) => Ok(invoke), + } { + Ok(invoke) => invoke, + Err(err) => { + test_group.add_result( + &format!("AssertReturn(unsupported-{})", i), + span.linecol_in(wast), + Err(eyre!("unsupported directive: {:?}", err)), + ); + continue; + } + }; + let invoke_name = invoke.name; + + let res: Result, _> = catch_unwind_silent(|| { + debug!("invoke: {:?}", invoke); + let args = invoke + .args + .into_iter() + .map(wastarg2tinywasmvalue) + .collect::>>() + .map_err(|e| { + error!("failed to convert args: {:?}", e); + e + })?; + + let outcomes = + exec_fn_instance(last_module.as_ref(), &mut store, invoke.name, &args).map_err(|e| { + error!("failed to execute function: {:?}", e); + e + })?; debug!("outcomes: {:?}", outcomes); @@ -212,25 +301,27 @@ impl TestSuite { .zip(expected) .enumerate() .try_for_each(|(i, (outcome, exp))| { - (outcome.eq_loose(&exp)).then_some(()).ok_or_else(|| { - eyre!( - "span: {:?}: result {} did not match: {:?} != {:?}", - span.linecol_in(&wast), - i, - outcome, - exp - ) - }) + (outcome.eq_loose(&exp)) + .then_some(()) + .ok_or_else(|| eyre!(" result {} did not match: {:?} != {:?}", i, outcome, exp)) }) }); let res = res - .map_err(|e| eyre!("test panicked: {:?}", e.downcast_ref::<&str>())) + .map_err(|e| eyre!("test panicked: {:?}", try_downcast_panic(e))) .and_then(|r| r); - test_group.add_result(&format!("{}-return", name), span, res); + test_group.add_result( + &format!("AssertReturn({}-{})", invoke_name, i), + span.linecol_in(wast), + res, + ); } - _ => test_group.add_result(&format!("{}-unknown", name), span, Err(eyre!("unsupported directive"))), + _ => test_group.add_result( + &format!("Unknown({})", i), + span.linecol_in(wast), + Err(eyre!("unsupported directive")), + ), } } diff --git a/crates/tinywasm/tests/testsuite/util.rs b/crates/tinywasm/tests/testsuite/util.rs index 85b2a06..9ba7934 100644 --- a/crates/tinywasm/tests/testsuite/util.rs +++ b/crates/tinywasm/tests/testsuite/util.rs @@ -1,12 +1,43 @@ -use std::panic; +use std::panic::{self, AssertUnwindSafe}; use eyre::{eyre, Result}; use tinywasm_types::{TinyWasmModule, WasmValue}; +pub fn try_downcast_panic(panic: Box) -> String { + let info = panic + .downcast_ref::() + .or(None) + .map(|p| p.to_string()) + .clone(); + let info_string = panic.downcast_ref::().cloned(); + let info_str = panic.downcast::<&str>().ok().map(|s| *s); + + info.unwrap_or( + info_str + .unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())) + .to_string(), + ) +} + +pub fn exec_fn_instance( + instance: Option<&tinywasm::ModuleInstance>, + store: &mut tinywasm::Store, + name: &str, + args: &[tinywasm_types::WasmValue], +) -> Result, tinywasm::Error> { + let Some(instance) = instance else { + return Err(tinywasm::Error::Other("no instance found".to_string())); + }; + + let func = instance.exported_func_by_name(store, name)?; + func.call(store, args) +} + pub fn exec_fn( module: Option<&TinyWasmModule>, name: &str, args: &[tinywasm_types::WasmValue], + imports: Option, ) -> Result, tinywasm::Error> { let Some(module) = module else { return Err(tinywasm::Error::Other("no module found".to_string())); @@ -14,14 +45,14 @@ pub fn exec_fn( let mut store = tinywasm::Store::new(); let module = tinywasm::Module::from(module); - let instance = module.instantiate(&mut store)?; - instance.get_func(&store, name)?.call(&mut store, args) + let instance = module.instantiate(&mut store, imports)?; + instance.exported_func_by_name(&store, name)?.call(&mut store, args) } -pub fn catch_unwind_silent R + panic::UnwindSafe, R>(f: F) -> std::thread::Result { +pub fn catch_unwind_silent R, R>(f: F) -> std::thread::Result { let prev_hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); - let result = panic::catch_unwind(f); + let result = panic::catch_unwind(AssertUnwindSafe(f)); panic::set_hook(prev_hook); result } @@ -33,7 +64,7 @@ pub fn parse_module_bytes(bytes: &[u8]) -> Result { pub fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { let wast::WastArg::Core(arg) = arg else { - return Err(eyre!("unsupported arg type")); + return Err(eyre!("unsupported arg type: Component")); }; use wast::core::WastArgCore::*; @@ -42,7 +73,13 @@ pub fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result WasmValue::F64(f64::from_bits(f.bits)), I32(i) => WasmValue::I32(i), I64(i) => WasmValue::I64(i), - _ => return Err(eyre!("unsupported arg type")), + // RefExtern(v) => WasmValue::RefExtern(v), + RefNull(t) => WasmValue::RefNull(match t { + wast::core::HeapType::Func => tinywasm_types::ValType::FuncRef, + wast::core::HeapType::Extern => tinywasm_types::ValType::ExternRef, + _ => return Err(eyre!("unsupported arg type: refnull: {:?}", t)), + }), + v => return Err(eyre!("unsupported arg type: {:?}", v)), }) } diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index 95fddc5..01c5b6e 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -1,3 +1,5 @@ +use crate::MemAddr; + use super::{FuncAddr, GlobalAddr, LabelAddr, LocalAddr, TableAddr, TypeAddr, ValType}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -10,7 +12,9 @@ pub enum BlockArgs { /// Represents a memory immediate in a WebAssembly memory instruction. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MemArg { + pub mem_addr: MemAddr, pub align: u8, + pub align_max: u8, pub offset: u64, } @@ -26,6 +30,8 @@ pub enum ConstInstruction { F32Const(f32), F64Const(f64), GlobalGet(GlobalAddr), + RefNull(ValType), + RefFunc(FuncAddr), } /// A WebAssembly Instruction @@ -99,8 +105,8 @@ pub enum Instruction { I64Store8(MemArg), I64Store16(MemArg), I64Store32(MemArg), - MemorySize, - MemoryGrow, + MemorySize(MemAddr, u8), + MemoryGrow(MemAddr, u8), // Constants I32Const(i32), @@ -108,6 +114,11 @@ pub enum Instruction { F32Const(f32), F64Const(f64), + // Reference Types + RefNull(ValType), + RefFunc(FuncAddr), + RefIsNull, + // Numeric Instructions // See I32Eqz, diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index ad82bd4..d3bf81a 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -88,9 +88,24 @@ pub enum WasmValue { F64(f64), // Vec types // V128(i128), + // RefExtern(ExternAddr), + // RefHost(FuncAddr), + RefNull(ValType), } impl WasmValue { + pub fn const_instr(&self) -> ConstInstruction { + match self { + Self::I32(i) => ConstInstruction::I32Const(*i), + Self::I64(i) => ConstInstruction::I64Const(*i), + Self::F32(i) => ConstInstruction::F32Const(*i), + Self::F64(i) => ConstInstruction::F64Const(*i), + Self::RefNull(ty) => ConstInstruction::RefNull(*ty), + // Self::RefExtern(addr) => ConstInstruction::RefExtern(*addr), + // _ => unimplemented!("const_instr for {:?}", self), + } + } + /// Get the default value for a given type. pub fn default_for(ty: ValType) -> Self { match ty { @@ -208,6 +223,7 @@ impl Debug for WasmValue { WasmValue::I64(i) => write!(f, "i64({})", i), WasmValue::F32(i) => write!(f, "f32({})", i), WasmValue::F64(i) => write!(f, "f64({})", i), + WasmValue::RefNull(ty) => write!(f, "ref.null({:?})", ty), // WasmValue::V128(i) => write!(f, "v128({})", i), } } @@ -221,13 +237,14 @@ impl WasmValue { Self::I64(_) => ValType::I64, Self::F32(_) => ValType::F32, Self::F64(_) => ValType::F64, + Self::RefNull(ty) => *ty, // Self::V128(_) => ValType::V128, } } } /// Type of a WebAssembly value. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ValType { /// A 32-bit integer. I32, @@ -245,6 +262,12 @@ pub enum ValType { ExternRef, } +impl ValType { + pub fn default_value(&self) -> WasmValue { + WasmValue::default_for(*self) + } +} + /// A WebAssembly External Kind. /// /// See @@ -334,7 +357,7 @@ pub struct Global { pub init: ConstInstruction, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct GlobalType { pub mutable: bool, pub ty: ValType,