From 174b60dcf7e4277b17359be0823e911cd2530c70 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 27 Jul 2022 16:02:16 -0500 Subject: [PATCH] Add `*.wast` support for invoking components (#4526) This commit builds on bytecodealliance/wasm-tools#690 to add support to testing of the component model to execute functions when running `*.wast` files. This support is all built on #4442 as functions are invoked through a "dynamic" API. Right now the testing and integration is fairly crude but I'm hoping that we can try to improve it over time as necessary. For now this should provide a hopefully more convenient syntax for unit tests and the like. --- Cargo.lock | 34 +- Cargo.toml | 4 +- cranelift/Cargo.toml | 2 +- cranelift/wasm/Cargo.toml | 4 +- crates/c-api/Cargo.toml | 2 +- crates/cranelift/Cargo.toml | 2 +- crates/environ/Cargo.toml | 6 +- crates/environ/fuzz/Cargo.toml | 2 +- crates/fuzzing/Cargo.toml | 12 +- crates/fuzzing/src/generators/table_ops.rs | 7 +- .../fuzzing/wasm-spec-interpreter/Cargo.toml | 2 +- crates/test-programs/Cargo.toml | 2 +- crates/types/Cargo.toml | 2 +- crates/wasmtime/Cargo.toml | 4 +- crates/wasmtime/src/component/types.rs | 2 +- crates/wasmtime/src/component/values.rs | 126 ++++ crates/wast/Cargo.toml | 2 +- crates/wast/src/component.rs | 364 +++++++++++ crates/wast/src/core.rs | 346 +++++++++++ crates/wast/src/lib.rs | 3 + crates/wast/src/wast.rs | 569 +++++------------- supply-chain/audits.toml | 42 ++ .../misc_testsuite/component-model/fused.wast | 50 +- .../component-model/simple.wast | 8 + .../misc_testsuite/component-model/types.wast | 25 + 25 files changed, 1154 insertions(+), 468 deletions(-) create mode 100644 crates/wast/src/component.rs create mode 100644 crates/wast/src/core.rs diff --git a/Cargo.lock b/Cargo.lock index 2c6b5766b5..3fd232bf39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3216,18 +3216,18 @@ checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "wasm-encoder" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f76068e87fe9b837a6bc2ccded66784173eadb828c4168643e9fddf6f9ed2e61" +checksum = "8905fd25fdadeb0e7e8bf43a9f46f9f972d6291ad0c7a32573b88dd13a6cfa6b" dependencies = [ "leb128", ] [[package]] name = "wasm-mutate" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0df58bcb700325907d1b024b58de418bd9c48abf5de6f802ad63d28c7d08d" +checksum = "80e6de18ed96f27d3942041e5ae02177aff18e4425196a3d4b1f14145d027f71" dependencies = [ "egg", "log", @@ -3239,9 +3239,9 @@ dependencies = [ [[package]] name = "wasm-smith" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73250e61e41d0e467b78559c7d761841005d724384bb0b78d52ff974acf5520" +checksum = "d54f72dd89c036847831ef4d3b8f7fd8618d87509422728f12b0937f96d6dd04" dependencies = [ "arbitrary", "flagset", @@ -3286,18 +3286,18 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.87.0" +version = "0.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c04e207cd2e8ecb6f9bd28a2cf3119b4c6bfeee6fe3a25cc1daf8041d00a875" +checksum = "fb8cf7dd82407fe68161bedcd57fde15596f32ebf6e9b3bdbf3ae1da20e38e5e" dependencies = [ "indexmap", ] [[package]] name = "wasmprinter" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "550bde1d5aec6aa1584c9f227ca2ab60621e002a4b15b8bee83f92c7c516db87" +checksum = "04f2786f19a25211ddfa331e28b7579a6d6880f5f4b18d21253cd90274aa4c21" dependencies = [ "anyhow", "wasmparser", @@ -3441,7 +3441,7 @@ dependencies = [ "wasmtime-wasi-crypto", "wasmtime-wasi-nn", "wasmtime-wast", - "wast 44.0.0", + "wast 45.0.0", "wat", "windows-sys", ] @@ -3681,7 +3681,7 @@ version = "0.40.0" dependencies = [ "anyhow", "wasmtime", - "wast 44.0.0", + "wast 45.0.0", ] [[package]] @@ -3695,9 +3695,9 @@ dependencies = [ [[package]] name = "wast" -version = "44.0.0" +version = "45.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f474d1b1cb7d92e5360b293f28e8bc9b2d115197a5bbf76bdbfba9161cf9cdc" +checksum = "186c474c4f9bb92756b566d592a16591b4526b1a4841171caa3f31d7fe330d96" dependencies = [ "leb128", "memchr", @@ -3707,11 +3707,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d002ce2eca0730c6df2c21719e9c4d8d0cafe74fb0cb8ff137c0774b8e4ed1" +checksum = "c2d4bc4724b4f02a482c8cab053dac5ef26410f264c06ce914958f9a42813556" dependencies = [ - "wast 44.0.0", + "wast 45.0.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 558528a410..dfc8c0b9f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,12 +51,12 @@ test-programs = { path = "crates/test-programs" } wasmtime-runtime = { path = "crates/runtime" } tokio = { version = "1.8.0", features = ["rt", "time", "macros", "rt-multi-thread"] } tracing-subscriber = "0.3.1" -wast = "44.0.0" +wast = "45.0.0" criterion = "0.3.4" num_cpus = "1.13.0" memchr = "2.4" async-trait = "0.1" -wat = "1.0.46" +wat = "1.0.47" once_cell = "1.9.0" rayon = "1.5.0" component-macro-test = { path = "crates/misc/component-macro-test" } diff --git a/cranelift/Cargo.toml b/cranelift/Cargo.toml index e33125667c..445a2ffa31 100644 --- a/cranelift/Cargo.toml +++ b/cranelift/Cargo.toml @@ -32,7 +32,7 @@ filecheck = "0.5.0" log = "0.4.8" termcolor = "1.1.2" capstone = { version = "0.9.0", optional = true } -wat = { version = "1.0.45", optional = true } +wat = { version = "1.0.47", optional = true } target-lexicon = { version = "0.12", features = ["std"] } pretty_env_logger = "0.4.0" rayon = { version = "1", optional = true } diff --git a/cranelift/wasm/Cargo.toml b/cranelift/wasm/Cargo.toml index 413e364a90..ac79503708 100644 --- a/cranelift/wasm/Cargo.toml +++ b/cranelift/wasm/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["webassembly", "wasm"] edition = "2021" [dependencies] -wasmparser = { version = "0.87.0", default-features = false } +wasmparser = { version = "0.88.0", default-features = false } cranelift-codegen = { path = "../codegen", version = "0.87.0", default-features = false } cranelift-entity = { path = "../entity", version = "0.87.0" } cranelift-frontend = { path = "../frontend", version = "0.87.0", default-features = false } @@ -24,7 +24,7 @@ serde = { version = "1.0.94", features = ["derive"], optional = true } smallvec = "1.6.1" [dev-dependencies] -wat = "1.0.45" +wat = "1.0.47" target-lexicon = "0.12" cranelift-codegen = { path = "../codegen", version = "0.87.0", default-features = false } diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index 67491e966c..12a39a5864 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -24,7 +24,7 @@ wasmtime = { path = "../wasmtime", default-features = false, features = ['cranel wasmtime-c-api-macros = { path = "macros" } # Optional dependency for the `wat2wasm` API -wat = { version = "1.0.45", optional = true } +wat = { version = "1.0.47", optional = true } # Optional dependencies for the `wasi` feature wasi-cap-std-sync = { path = "../wasi-common/cap-std-sync", optional = true } diff --git a/crates/cranelift/Cargo.toml b/crates/cranelift/Cargo.toml index 8f14c9dd54..bc69e7b7e4 100644 --- a/crates/cranelift/Cargo.toml +++ b/crates/cranelift/Cargo.toml @@ -19,7 +19,7 @@ cranelift-codegen = { path = "../../cranelift/codegen", version = "0.87.0" } cranelift-frontend = { path = "../../cranelift/frontend", version = "0.87.0" } cranelift-entity = { path = "../../cranelift/entity", version = "0.87.0" } cranelift-native = { path = "../../cranelift/native", version = "0.87.0" } -wasmparser = "0.87.0" +wasmparser = "0.88.0" target-lexicon = "0.12" gimli = { version = "0.26.0", default-features = false, features = ['read', 'std'] } object = { version = "0.29.0", default-features = false, features = ['write'] } diff --git a/crates/environ/Cargo.toml b/crates/environ/Cargo.toml index 2f8622ec95..9c4e152981 100644 --- a/crates/environ/Cargo.toml +++ b/crates/environ/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" anyhow = "1.0" cranelift-entity = { path = "../../cranelift/entity", version = "0.87.0" } wasmtime-types = { path = "../types", version = "0.40.0" } -wasmparser = "0.87.0" +wasmparser = "0.88.0" indexmap = { version = "1.0.2", features = ["serde-1"] } thiserror = "1.0.4" serde = { version = "1.0.94", features = ["derive"] } @@ -22,8 +22,8 @@ log = { version = "0.4.8", default-features = false } gimli = { version = "0.26.0", default-features = false, features = ['read'] } object = { version = "0.29.0", default-features = false, features = ['read_core', 'write_core', 'elf'] } target-lexicon = "0.12" -wasm-encoder = { version = "0.14.0", optional = true } -wasmprinter = { version = "0.2.37", optional = true } +wasm-encoder = { version = "0.15.0", optional = true } +wasmprinter = { version = "0.2.38", optional = true } wasmtime-component-util = { path = "../component-util", version = "=0.40.0", optional = true } [badges] diff --git a/crates/environ/fuzz/Cargo.toml b/crates/environ/fuzz/Cargo.toml index c4cd8a6c3a..90086f36f2 100644 --- a/crates/environ/fuzz/Cargo.toml +++ b/crates/environ/fuzz/Cargo.toml @@ -12,7 +12,7 @@ cargo-fuzz = true arbitrary = { version = "1.1.0", features = ["derive"] } env_logger = "0.9.0" libfuzzer-sys = "0.4" -wasmparser = "0.87.0" +wasmparser = "0.88.0" wasmprinter = "0.2.37" wasmtime-environ = { path = ".." } diff --git a/crates/fuzzing/Cargo.toml b/crates/fuzzing/Cargo.toml index 3b67013f24..76ae1d269c 100644 --- a/crates/fuzzing/Cargo.toml +++ b/crates/fuzzing/Cargo.toml @@ -15,13 +15,13 @@ log = "0.4.8" rayon = "1.2.1" target-lexicon = "0.12.3" tempfile = "3.3.0" -wasmparser = "0.87.0" -wasmprinter = "0.2.37" +wasmparser = "0.88.0" +wasmprinter = "0.2.38" wasmtime = { path = "../wasmtime" } wasmtime-wast = { path = "../wast" } -wasm-encoder = "0.14.0" -wasm-smith = "0.11.2" -wasm-mutate = "0.2.5" +wasm-encoder = "0.15.0" +wasm-smith = "0.11.3" +wasm-mutate = "0.2.6" wasm-spec-interpreter = { path = "./wasm-spec-interpreter", optional = true } wasmi = "0.11.0" @@ -33,7 +33,7 @@ wasmi = "0.11.0" v8 = "0.44.3" [dev-dependencies] -wat = "1.0.45" +wat = "1.0.47" rand = { version = "0.8.0", features = ["small_rng"] } # Only enable the `build-libinterpret` feature when fuzzing is enabled, enabling diff --git a/crates/fuzzing/src/generators/table_ops.rs b/crates/fuzzing/src/generators/table_ops.rs index 07f19e38c2..21b13e1fdc 100644 --- a/crates/fuzzing/src/generators/table_ops.rs +++ b/crates/fuzzing/src/generators/table_ops.rs @@ -3,8 +3,9 @@ use arbitrary::{Arbitrary, Result, Unstructured}; use std::ops::RangeInclusive; use wasm_encoder::{ - CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, GlobalSection, - ImportSection, Instruction, Module, TableSection, TableType, TypeSection, ValType, + CodeSection, ConstExpr, EntityType, ExportKind, ExportSection, Function, FunctionSection, + GlobalSection, ImportSection, Instruction, Module, TableSection, TableType, TypeSection, + ValType, }; /// A description of a Wasm module that makes a series of `externref` table @@ -94,7 +95,7 @@ impl TableOps { val_type: wasm_encoder::ValType::ExternRef, mutable: true, }, - &Instruction::RefNull(wasm_encoder::ValType::ExternRef), + &ConstExpr::ref_null(wasm_encoder::ValType::ExternRef), ); } diff --git a/crates/fuzzing/wasm-spec-interpreter/Cargo.toml b/crates/fuzzing/wasm-spec-interpreter/Cargo.toml index 707451f16e..35c36cca09 100644 --- a/crates/fuzzing/wasm-spec-interpreter/Cargo.toml +++ b/crates/fuzzing/wasm-spec-interpreter/Cargo.toml @@ -16,7 +16,7 @@ ocaml-interop = { version = "0.8", optional = true } once_cell = { version = "1.12.0", optional = true } [dev-dependencies] -wat = "1.0.45" +wat = "1.0.47" [features] build-libinterpret = ["ocaml-interop", "once_cell"] diff --git a/crates/test-programs/Cargo.toml b/crates/test-programs/Cargo.toml index 12b3119117..b292ef9362 100644 --- a/crates/test-programs/Cargo.toml +++ b/crates/test-programs/Cargo.toml @@ -20,7 +20,7 @@ pretty_env_logger = "0.4.0" tempfile = "3.1.0" os_pipe = "0.9" anyhow = "1.0.19" -wat = "1.0.45" +wat = "1.0.47" cap-std = "0.25.0" tokio = { version = "1.8.0", features = ["rt-multi-thread"] } diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index a0f88e9ba2..1471aec55d 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -12,4 +12,4 @@ edition = "2021" cranelift-entity = { path = "../../cranelift/entity", version = "0.87.0", features = ['enable-serde'] } serde = { version = "1.0.94", features = ["derive"] } thiserror = "1.0.4" -wasmparser = { version = "0.87.0", default-features = false } +wasmparser = { version = "0.88.0", default-features = false } diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 325f595b7c..76af8c1094 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -22,13 +22,13 @@ wasmtime-cranelift = { path = "../cranelift", version = "=0.40.0", optional = tr wasmtime-component-macro = { path = "../component-macro", version = "=0.40.0", optional = true } wasmtime-component-util = { path = "../component-util", version = "=0.40.0", optional = true } target-lexicon = { version = "0.12.0", default-features = false } -wasmparser = "0.87.0" +wasmparser = "0.88.0" anyhow = "1.0.19" libc = "0.2" cfg-if = "1.0" backtrace = { version = "0.3.61" } log = "0.4.8" -wat = { version = "1.0.45", optional = true } +wat = { version = "1.0.47", optional = true } serde = { version = "1.0.94", features = ["derive"] } bincode = "1.2.1" indexmap = "1.6" diff --git a/crates/wasmtime/src/component/types.rs b/crates/wasmtime/src/component/types.rs index adf992acc3..9d05866125 100644 --- a/crates/wasmtime/src/component/types.rs +++ b/crates/wasmtime/src/component/types.rs @@ -73,7 +73,7 @@ impl Record { } /// Retrieve the fields of this `record` in declaration order. - pub fn fields(&self) -> impl ExactSizeIterator { + pub fn fields(&self) -> impl ExactSizeIterator> { self.0.types[self.0.index].fields.iter().map(|field| Field { name: &field.name, ty: Type::from(&field.ty, &self.0.types), diff --git a/crates/wasmtime/src/component/values.rs b/crates/wasmtime/src/component/values.rs index 066174d257..4588c08a58 100644 --- a/crates/wasmtime/src/component/values.rs +++ b/crates/wasmtime/src/component/values.rs @@ -30,6 +30,19 @@ impl List { values, }) } + + /// Returns the corresponding type of this list + pub fn ty(&self) -> &types::List { + &self.ty + } +} + +impl Deref for List { + type Target = [Val]; + + fn deref(&self) -> &[Val] { + &self.values + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -76,6 +89,20 @@ impl Record { values: values.into(), }) } + + /// Returns the corresponding type of this record. + pub fn ty(&self) -> &types::Record { + &self.ty + } + + /// Gets the value of the specified field `name` from this record. + pub fn fields(&self) -> impl Iterator { + assert_eq!(self.values.len(), self.ty.fields().len()); + self.ty + .fields() + .zip(self.values.iter()) + .map(|(ty, val)| (ty.name, val)) + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -105,6 +132,16 @@ impl Tuple { values, }) } + + /// Returns the type of this tuple. + pub fn ty(&self) -> &types::Tuple { + &self.ty + } + + /// Returns the list of values that this tuple contains. + pub fn values(&self) -> &[Val] { + &self.values + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -139,6 +176,25 @@ impl Variant { value: Box::new(value), }) } + + /// Returns the type of this variant. + pub fn ty(&self) -> &types::Variant { + &self.ty + } + + /// Returns name of the discriminant of this value within the variant type. + pub fn discriminant(&self) -> &str { + self.ty + .cases() + .nth(self.discriminant as usize) + .unwrap() + .name + } + + /// Returns the payload value for this variant. + pub fn payload(&self) -> &Val { + &self.value + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -161,6 +217,16 @@ impl Enum { discriminant, }) } + + /// Returns the type of this value. + pub fn ty(&self) -> &types::Enum { + &self.ty + } + + /// Returns name of this enum value. + pub fn discriminant(&self) -> &str { + self.ty.names().nth(self.discriminant as usize).unwrap() + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -190,6 +256,21 @@ impl Union { )) } } + + /// Returns the type of this value. + pub fn ty(&self) -> &types::Union { + &self.ty + } + + /// Returns name of the discriminant of this value within the union type. + pub fn discriminant(&self) -> u32 { + self.discriminant + } + + /// Returns the payload value for this union. + pub fn payload(&self) -> &Val { + &self.value + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -216,6 +297,20 @@ impl Option { value: Box::new(value.unwrap_or(Val::Unit)), }) } + + /// Returns the type of this value. + pub fn ty(&self) -> &types::Option { + &self.ty + } + + /// Returns the optional value contained within. + pub fn value(&self) -> std::option::Option<&Val> { + if self.discriminant == 0 { + None + } else { + Some(&self.value) + } + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -247,6 +342,20 @@ impl Expected { }), }) } + + /// Returns the type of this value. + pub fn ty(&self) -> &types::Expected { + &self.ty + } + + /// Returns the result value contained within. + pub fn value(&self) -> Result<&Val, &Val> { + if self.discriminant == 0 { + Ok(&self.value) + } else { + Err(&self.value) + } + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -280,6 +389,23 @@ impl Flags { value: values.into(), }) } + + /// Returns the type of this value. + pub fn ty(&self) -> &types::Flags { + &self.ty + } + + /// Returns an iterator over the set of names that this flags set contains. + pub fn flags(&self) -> impl Iterator { + (0..self.count).filter_map(|i| { + let (idx, bit) = ((i / 32) as usize, i % 32); + if self.value[idx] & (1 << bit) != 0 { + Some(self.ty.names().nth(i as usize).unwrap()) + } else { + None + } + }) + } } /// Represents possible runtime values which a component function can either consume or produce diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml index c61578ee34..6356363d7a 100644 --- a/crates/wast/Cargo.toml +++ b/crates/wast/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" [dependencies] anyhow = "1.0.19" wasmtime = { path = "../wasmtime", version = "0.40.0", default-features = false, features = ['cranelift'] } -wast = "44.0.0" +wast = "45.0.0" [badges] maintenance = { status = "actively-developed" } diff --git a/crates/wast/src/component.rs b/crates/wast/src/component.rs new file mode 100644 index 0000000000..d2ee07c705 --- /dev/null +++ b/crates/wast/src/component.rs @@ -0,0 +1,364 @@ +use crate::core; +use anyhow::{anyhow, bail, Context, Result}; +use std::collections::{BTreeSet, HashMap}; +use std::fmt::Debug; +use wast::component::WastVal; +use wast::core::NanPattern; + +pub use wasmtime::component::*; + +pub fn val(v: &WastVal<'_>, ty: &Type) -> Result { + Ok(match v { + WastVal::Unit => Val::Unit, + WastVal::Bool(b) => Val::Bool(*b), + WastVal::U8(b) => Val::U8(*b), + WastVal::S8(b) => Val::S8(*b), + WastVal::U16(b) => Val::U16(*b), + WastVal::S16(b) => Val::S16(*b), + WastVal::U32(b) => Val::U32(*b), + WastVal::S32(b) => Val::S32(*b), + WastVal::U64(b) => Val::U64(*b), + WastVal::S64(b) => Val::S64(*b), + WastVal::Float32(b) => Val::Float32(b.bits), + WastVal::Float64(b) => Val::Float64(b.bits), + WastVal::Char(b) => Val::Char(*b), + WastVal::String(s) => Val::String(s.to_string().into()), + WastVal::List(vals) => match ty { + Type::List(t) => { + let element = t.ty(); + let vals = vals + .iter() + .map(|v| val(v, &element)) + .collect::>>()?; + t.new_val(vals.into())? + } + _ => bail!("expected a list value"), + }, + WastVal::Record(fields) => match ty { + Type::Record(t) => { + let mut fields_by_name = HashMap::new(); + for (name, val) in fields { + let prev = fields_by_name.insert(*name, val); + if prev.is_some() { + bail!("field `{name}` specified twice"); + } + } + let mut values = Vec::new(); + for field in t.fields() { + let name = field.name; + let v = fields_by_name + .remove(name) + .ok_or_else(|| anyhow!("field `{name}` not specified"))?; + values.push((name, val(v, &field.ty)?)); + } + if let Some((field, _)) = fields_by_name.iter().next() { + bail!("extra field `{field}` specified"); + } + t.new_val(values)? + } + _ => bail!("expected a record value"), + }, + WastVal::Tuple(vals) => match ty { + Type::Tuple(t) => { + if vals.len() != t.types().len() { + bail!("expected {} values got {}", t.types().len(), vals.len()); + } + t.new_val( + vals.iter() + .zip(t.types()) + .map(|(v, ty)| val(v, &ty)) + .collect::>>()? + .into(), + )? + } + _ => bail!("expected a tuple value"), + }, + WastVal::Enum(name) => match ty { + Type::Enum(t) => t.new_val(name)?, + _ => bail!("expected an enum value"), + }, + WastVal::Variant(name, payload) => match ty { + Type::Variant(t) => { + let case = match t.cases().find(|c| c.name == *name) { + Some(case) => case.ty, + None => bail!("no case named `{}", name), + }; + let payload = val(payload, &case)?; + t.new_val(name, payload)? + } + _ => bail!("expected a variant value"), + }, + WastVal::Union(idx, payload) => match ty { + Type::Union(t) => { + let case = match t.types().nth(*idx as usize) { + Some(case) => case, + None => bail!("case {idx} too large"), + }; + let payload = val(payload, &case)?; + t.new_val(*idx, payload)? + } + _ => bail!("expected a union value"), + }, + WastVal::Option(v) => match ty { + Type::Option(t) => { + let v = match v { + Some(v) => Some(val(v, &t.ty())?), + None => None, + }; + t.new_val(v)? + } + _ => bail!("expected an option value"), + }, + WastVal::Expected(v) => match ty { + Type::Expected(t) => { + let v = match v { + Ok(v) => Ok(val(v, &t.ok())?), + Err(v) => Err(val(v, &t.err())?), + }; + t.new_val(v)? + } + _ => bail!("expected an expected value"), + }, + WastVal::Flags(v) => match ty { + Type::Flags(t) => t.new_val(v)?, + _ => bail!("expected a flags value"), + }, + }) +} + +pub fn match_val(expected: &WastVal<'_>, actual: &Val) -> Result<()> { + match expected { + WastVal::Unit => match actual { + Val::Unit => Ok(()), + _ => mismatch(expected, actual), + }, + WastVal::Bool(e) => match actual { + Val::Bool(a) => match_debug(a, e), + _ => mismatch(expected, actual), + }, + WastVal::U8(e) => match actual { + Val::U8(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::S8(e) => match actual { + Val::S8(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::U16(e) => match actual { + Val::U16(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::S16(e) => match actual { + Val::S16(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::U32(e) => match actual { + Val::U32(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::S32(e) => match actual { + Val::S32(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::U64(e) => match actual { + Val::U64(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::S64(e) => match actual { + Val::S64(a) => core::match_int(a, e), + _ => mismatch(expected, actual), + }, + WastVal::Float32(e) => match actual { + Val::Float32(a) => core::match_f32(*a, &NanPattern::Value(*e)), + _ => mismatch(expected, actual), + }, + WastVal::Float64(e) => match actual { + Val::Float64(a) => core::match_f64(*a, &NanPattern::Value(*e)), + _ => mismatch(expected, actual), + }, + WastVal::Char(e) => match actual { + Val::Char(a) => match_debug(a, e), + _ => mismatch(expected, actual), + }, + WastVal::String(e) => match actual { + Val::String(a) => match_debug(&a[..], *e), + _ => mismatch(expected, actual), + }, + WastVal::List(e) => match actual { + Val::List(a) => { + if e.len() != a.len() { + bail!("expected {} values got {}", e.len(), a.len()); + } + for (i, (expected, actual)) in e.iter().zip(a.iter()).enumerate() { + match_val(expected, actual) + .with_context(|| format!("failed to match list element {i}"))?; + } + Ok(()) + } + _ => mismatch(expected, actual), + }, + WastVal::Record(e) => match actual { + Val::Record(a) => { + let mut fields_by_name = HashMap::new(); + for (name, val) in e { + let prev = fields_by_name.insert(*name, val); + if prev.is_some() { + bail!("field `{name}` specified twice"); + } + } + for (name, actual) in a.fields() { + let expected = fields_by_name + .remove(name) + .ok_or_else(|| anyhow!("field `{name}` not specified"))?; + match_val(expected, actual) + .with_context(|| format!("failed to match field `{name}`"))?; + } + if let Some((field, _)) = fields_by_name.iter().next() { + bail!("extra field `{field}` specified"); + } + Ok(()) + } + _ => mismatch(expected, actual), + }, + WastVal::Tuple(e) => match actual { + Val::Tuple(a) => { + if e.len() != a.values().len() { + bail!( + "expected {}-tuple, found {}-tuple", + e.len(), + a.values().len() + ); + } + for (i, (expected, actual)) in e.iter().zip(a.values()).enumerate() { + match_val(expected, actual) + .with_context(|| format!("failed to match tuple element {i}"))?; + } + Ok(()) + } + _ => mismatch(expected, actual), + }, + WastVal::Variant(name, e) => match actual { + Val::Variant(a) => { + if a.discriminant() != *name { + bail!("expected discriminant `{name}` got `{}`", a.discriminant()); + } + match_val(e, a.payload()) + } + _ => mismatch(expected, actual), + }, + WastVal::Enum(name) => match actual { + Val::Enum(a) => { + if a.discriminant() != *name { + bail!("expected discriminant `{name}` got `{}`", a.discriminant()); + } else { + Ok(()) + } + } + _ => mismatch(expected, actual), + }, + WastVal::Union(idx, e) => match actual { + Val::Union(a) => { + if a.discriminant() != *idx { + bail!("expected discriminant `{idx}` got `{}`", a.discriminant()); + } + match_val(e, a.payload()) + } + _ => mismatch(expected, actual), + }, + WastVal::Option(e) => match actual { + Val::Option(a) => match (e, a.value()) { + (None, None) => Ok(()), + (Some(expected), Some(actual)) => match_val(expected, actual), + (None, Some(_)) => bail!("expected `none`, found `some`"), + (Some(_), None) => bail!("expected `some`, found `none`"), + }, + _ => mismatch(expected, actual), + }, + WastVal::Expected(e) => match actual { + Val::Expected(a) => match (e, a.value()) { + (Ok(_), Err(_)) => bail!("expected `ok`, found `err`"), + (Err(_), Ok(_)) => bail!("expected `err`, found `ok`"), + (Err(e), Err(a)) => match_val(e, a), + (Ok(e), Ok(a)) => match_val(e, a), + }, + _ => mismatch(expected, actual), + }, + WastVal::Flags(e) => match actual { + Val::Flags(a) => { + let expected = e.iter().copied().collect::>(); + let actual = a.flags().collect::>(); + match_debug(&actual, &expected) + } + _ => mismatch(expected, actual), + }, + } +} + +fn match_debug(actual: &T, expected: &T) -> Result<()> +where + T: Eq + Debug + ?Sized, +{ + if actual == expected { + Ok(()) + } else { + bail!( + " + expected {expected:?} + actual {actual:?}" + ) + } +} + +fn mismatch(expected: &WastVal<'_>, actual: &Val) -> Result<()> { + let expected = match expected { + WastVal::Unit => "unit", + WastVal::Bool(..) => "bool", + WastVal::U8(..) => "u8", + WastVal::S8(..) => "s8", + WastVal::U16(..) => "u16", + WastVal::S16(..) => "s16", + WastVal::U32(..) => "u32", + WastVal::S32(..) => "s32", + WastVal::U64(..) => "u64", + WastVal::S64(..) => "s64", + WastVal::Float32(..) => "float32", + WastVal::Float64(..) => "float64", + WastVal::Char(..) => "char", + WastVal::String(..) => "string", + WastVal::List(..) => "list", + WastVal::Record(..) => "record", + WastVal::Tuple(..) => "tuple", + WastVal::Enum(..) => "enum", + WastVal::Variant(..) => "variant", + WastVal::Union(..) => "union", + WastVal::Option(..) => "option", + WastVal::Expected(..) => "expected", + WastVal::Flags(..) => "flags", + }; + let actual = match actual { + Val::Unit => "unit", + Val::Bool(..) => "bool", + Val::U8(..) => "u8", + Val::S8(..) => "s8", + Val::U16(..) => "u16", + Val::S16(..) => "s16", + Val::U32(..) => "u32", + Val::S32(..) => "s32", + Val::U64(..) => "u64", + Val::S64(..) => "s64", + Val::Float32(..) => "float32", + Val::Float64(..) => "float64", + Val::Char(..) => "char", + Val::String(..) => "string", + Val::List(..) => "list", + Val::Record(..) => "record", + Val::Tuple(..) => "tuple", + Val::Enum(..) => "enum", + Val::Variant(..) => "variant", + Val::Union(..) => "union", + Val::Option(..) => "option", + Val::Expected(..) => "expected", + Val::Flags(..) => "flags", + }; + bail!("expected `{expected}` got `{actual}`") +} diff --git a/crates/wast/src/core.rs b/crates/wast/src/core.rs new file mode 100644 index 0000000000..e87d0b1f7e --- /dev/null +++ b/crates/wast/src/core.rs @@ -0,0 +1,346 @@ +use anyhow::{bail, Context, Result}; +use std::fmt::{Display, LowerHex}; +use wasmtime::{ExternRef, Val}; +use wast::core::{HeapType, NanPattern, V128Pattern, WastArgCore, WastRetCore}; +use wast::token::{Float32, Float64}; + +/// Translate from a `script::Value` to a `RuntimeValue`. +pub fn val(v: &WastArgCore<'_>) -> Result { + use wast::core::WastArgCore::*; + + Ok(match v { + I32(x) => Val::I32(*x), + I64(x) => Val::I64(*x), + F32(x) => Val::F32(x.bits), + F64(x) => Val::F64(x.bits), + V128(x) => Val::V128(u128::from_le_bytes(x.to_le_bytes())), + RefNull(HeapType::Extern) => Val::ExternRef(None), + RefNull(HeapType::Func) => Val::FuncRef(None), + RefExtern(x) => Val::ExternRef(Some(ExternRef::new(*x))), + other => bail!("couldn't convert {:?} to a runtime value", other), + }) +} + +fn extract_lane_as_i8(bytes: u128, lane: usize) -> i8 { + (bytes >> (lane * 8)) as i8 +} + +fn extract_lane_as_i16(bytes: u128, lane: usize) -> i16 { + (bytes >> (lane * 16)) as i16 +} + +fn extract_lane_as_i32(bytes: u128, lane: usize) -> i32 { + (bytes >> (lane * 32)) as i32 +} + +fn extract_lane_as_i64(bytes: u128, lane: usize) -> i64 { + (bytes >> (lane * 64)) as i64 +} + +pub fn match_val(actual: &Val, expected: &WastRetCore) -> Result<()> { + match (actual, expected) { + (Val::I32(a), WastRetCore::I32(b)) => match_int(a, b), + (Val::I64(a), WastRetCore::I64(b)) => match_int(a, b), + // Note that these float comparisons are comparing bits, not float + // values, so we're testing for bit-for-bit equivalence + (Val::F32(a), WastRetCore::F32(b)) => match_f32(*a, b), + (Val::F64(a), WastRetCore::F64(b)) => match_f64(*a, b), + (Val::V128(a), WastRetCore::V128(b)) => match_v128(*a, b), + (Val::ExternRef(x), WastRetCore::RefNull(Some(HeapType::Extern))) => { + if let Some(x) = x { + let x = x + .data() + .downcast_ref::() + .expect("only u32 externrefs created in wast test suites"); + bail!("expected null externref, found {}", x); + } else { + Ok(()) + } + } + (Val::ExternRef(x), WastRetCore::RefExtern(y)) => { + if let Some(x) = x { + let x = x + .data() + .downcast_ref::() + .expect("only u32 externrefs created in wast test suites"); + if x == y { + Ok(()) + } else { + bail!("expected {} found {}", y, x); + } + } else { + bail!("expected non-null externref, found null") + } + } + (Val::FuncRef(x), WastRetCore::RefNull(_)) => { + if x.is_none() { + Ok(()) + } else { + bail!("expected null funcref, found non-null") + } + } + _ => bail!( + "don't know how to compare {:?} and {:?} yet", + actual, + expected + ), + } +} + +pub fn match_int(actual: &T, expected: &T) -> Result<()> +where + T: Eq + Display + LowerHex, +{ + if actual == expected { + Ok(()) + } else { + bail!( + "expected {:18} / {0:#018x}\n\ + actual {:18} / {1:#018x}", + expected, + actual + ) + } +} + +pub fn match_f32(actual: u32, expected: &NanPattern) -> Result<()> { + match expected { + // Check if an f32 (as u32 bits to avoid possible quieting when moving values in registers, e.g. + // https://developer.arm.com/documentation/ddi0344/i/neon-and-vfp-programmers-model/modes-of-operation/default-nan-mode?lang=en) + // is a canonical NaN: + // - the sign bit is unspecified, + // - the 8-bit exponent is set to all 1s + // - the MSB of the payload is set to 1 (a quieted NaN) and all others to 0. + // See https://webassembly.github.io/spec/core/syntax/values.html#floating-point. + NanPattern::CanonicalNan => { + let canon_nan = 0x7fc0_0000; + if (actual & 0x7fff_ffff) == canon_nan { + Ok(()) + } else { + bail!( + "expected {:10} / {:#010x}\n\ + actual {:10} / {:#010x}", + "canon-nan", + canon_nan, + f32::from_bits(actual), + actual, + ) + } + } + + // Check if an f32 (as u32, see comments above) is an arithmetic NaN. + // This is the same as a canonical NaN including that the payload MSB is + // set to 1, but one or more of the remaining payload bits MAY BE set to + // 1 (a canonical NaN specifies all 0s). See + // https://webassembly.github.io/spec/core/syntax/values.html#floating-point. + NanPattern::ArithmeticNan => { + const AF32_NAN: u32 = 0x7f80_0000; + let is_nan = actual & AF32_NAN == AF32_NAN; + const AF32_PAYLOAD_MSB: u32 = 0x0040_0000; + let is_msb_set = actual & AF32_PAYLOAD_MSB == AF32_PAYLOAD_MSB; + if is_nan && is_msb_set { + Ok(()) + } else { + bail!( + "expected {:>10} / {:>10}\n\ + actual {:10} / {:#010x}", + "arith-nan", + "0x7fc*****", + f32::from_bits(actual), + actual, + ) + } + } + NanPattern::Value(expected_value) => { + if actual == expected_value.bits { + Ok(()) + } else { + bail!( + "expected {:10} / {:#010x}\n\ + actual {:10} / {:#010x}", + f32::from_bits(expected_value.bits), + expected_value.bits, + f32::from_bits(actual), + actual, + ) + } + } + } +} + +pub fn match_f64(actual: u64, expected: &NanPattern) -> Result<()> { + match expected { + // Check if an f64 (as u64 bits to avoid possible quieting when moving values in registers, e.g. + // https://developer.arm.com/documentation/ddi0344/i/neon-and-vfp-programmers-model/modes-of-operation/default-nan-mode?lang=en) + // is a canonical NaN: + // - the sign bit is unspecified, + // - the 11-bit exponent is set to all 1s + // - the MSB of the payload is set to 1 (a quieted NaN) and all others to 0. + // See https://webassembly.github.io/spec/core/syntax/values.html#floating-point. + NanPattern::CanonicalNan => { + let canon_nan = 0x7ff8_0000_0000_0000; + if (actual & 0x7fff_ffff_ffff_ffff) == canon_nan { + Ok(()) + } else { + bail!( + "expected {:18} / {:#018x}\n\ + actual {:18} / {:#018x}", + "canon-nan", + canon_nan, + f64::from_bits(actual), + actual, + ) + } + } + + // Check if an f64 (as u64, see comments above) is an arithmetic NaN. This is the same as a + // canonical NaN including that the payload MSB is set to 1, but one or more of the remaining + // payload bits MAY BE set to 1 (a canonical NaN specifies all 0s). See + // https://webassembly.github.io/spec/core/syntax/values.html#floating-point. + NanPattern::ArithmeticNan => { + const AF64_NAN: u64 = 0x7ff0_0000_0000_0000; + let is_nan = actual & AF64_NAN == AF64_NAN; + const AF64_PAYLOAD_MSB: u64 = 0x0008_0000_0000_0000; + let is_msb_set = actual & AF64_PAYLOAD_MSB == AF64_PAYLOAD_MSB; + if is_nan && is_msb_set { + Ok(()) + } else { + bail!( + "expected {:>18} / {:>18}\n\ + actual {:18} / {:#018x}", + "arith-nan", + "0x7ff8************", + f64::from_bits(actual), + actual, + ) + } + } + NanPattern::Value(expected_value) => { + if actual == expected_value.bits { + Ok(()) + } else { + bail!( + "expected {:18} / {:#018x}\n\ + actual {:18} / {:#018x}", + f64::from_bits(expected_value.bits), + expected_value.bits, + f64::from_bits(actual), + actual, + ) + } + } + } +} + +fn match_v128(actual: u128, expected: &V128Pattern) -> Result<()> { + match expected { + V128Pattern::I8x16(expected) => { + let actual = [ + extract_lane_as_i8(actual, 0), + extract_lane_as_i8(actual, 1), + extract_lane_as_i8(actual, 2), + extract_lane_as_i8(actual, 3), + extract_lane_as_i8(actual, 4), + extract_lane_as_i8(actual, 5), + extract_lane_as_i8(actual, 6), + extract_lane_as_i8(actual, 7), + extract_lane_as_i8(actual, 8), + extract_lane_as_i8(actual, 9), + extract_lane_as_i8(actual, 10), + extract_lane_as_i8(actual, 11), + extract_lane_as_i8(actual, 12), + extract_lane_as_i8(actual, 13), + extract_lane_as_i8(actual, 14), + extract_lane_as_i8(actual, 15), + ]; + if actual == *expected { + return Ok(()); + } + bail!( + "expected {:4?}\n\ + actual {:4?}\n\ + \n\ + expected (hex) {0:02x?}\n\ + actual (hex) {1:02x?}", + expected, + actual, + ) + } + V128Pattern::I16x8(expected) => { + let actual = [ + extract_lane_as_i16(actual, 0), + extract_lane_as_i16(actual, 1), + extract_lane_as_i16(actual, 2), + extract_lane_as_i16(actual, 3), + extract_lane_as_i16(actual, 4), + extract_lane_as_i16(actual, 5), + extract_lane_as_i16(actual, 6), + extract_lane_as_i16(actual, 7), + ]; + if actual == *expected { + return Ok(()); + } + bail!( + "expected {:6?}\n\ + actual {:6?}\n\ + \n\ + expected (hex) {0:04x?}\n\ + actual (hex) {1:04x?}", + expected, + actual, + ) + } + V128Pattern::I32x4(expected) => { + let actual = [ + extract_lane_as_i32(actual, 0), + extract_lane_as_i32(actual, 1), + extract_lane_as_i32(actual, 2), + extract_lane_as_i32(actual, 3), + ]; + if actual == *expected { + return Ok(()); + } + bail!( + "expected {:11?}\n\ + actual {:11?}\n\ + \n\ + expected (hex) {0:08x?}\n\ + actual (hex) {1:08x?}", + expected, + actual, + ) + } + V128Pattern::I64x2(expected) => { + let actual = [ + extract_lane_as_i64(actual, 0), + extract_lane_as_i64(actual, 1), + ]; + if actual == *expected { + return Ok(()); + } + bail!( + "expected {:20?}\n\ + actual {:20?}\n\ + \n\ + expected (hex) {0:016x?}\n\ + actual (hex) {1:016x?}", + expected, + actual, + ) + } + V128Pattern::F32x4(expected) => { + for (i, expected) in expected.iter().enumerate() { + let a = extract_lane_as_i32(actual, i) as u32; + match_f32(a, expected).with_context(|| format!("difference in lane {}", i))?; + } + Ok(()) + } + V128Pattern::F64x2(expected) => { + for (i, expected) in expected.iter().enumerate() { + let a = extract_lane_as_i64(actual, i) as u64; + match_f64(a, expected).with_context(|| format!("difference in lane {}", i))?; + } + Ok(()) + } + } +} diff --git a/crates/wast/src/lib.rs b/crates/wast/src/lib.rs index ca38818045..f6ae34f895 100644 --- a/crates/wast/src/lib.rs +++ b/crates/wast/src/lib.rs @@ -21,6 +21,9 @@ ) )] +#[cfg(feature = "component-model")] +mod component; +mod core; mod spectest; mod wast; diff --git a/crates/wast/src/wast.rs b/crates/wast/src/wast.rs index a88583c892..102e4f7abd 100644 --- a/crates/wast/src/wast.rs +++ b/crates/wast/src/wast.rs @@ -1,51 +1,28 @@ +#[cfg(feature = "component-model")] +use crate::component; +use crate::core; use crate::spectest::*; use anyhow::{anyhow, bail, Context as _, Result}; -use std::fmt::{Display, LowerHex}; use std::path::Path; use std::str; use wasmtime::*; -use wast::core::{Expression, HeapType}; use wast::lexer::Lexer; use wast::parser::{self, ParseBuffer}; -use wast::token::{Float32, Float64}; -use wast::{ - AssertExpression, NanPattern, QuoteWat, V128Pattern, Wast, WastDirective, WastExecute, - WastInvoke, Wat, -}; - -/// Translate from a `script::Value` to a `RuntimeValue`. -fn runtime_value(v: &Expression<'_>) -> Result { - use wast::core::Instruction::*; - - if v.instrs.len() != 1 { - bail!("too many instructions in {:?}", v); - } - Ok(match &v.instrs[0] { - I32Const(x) => Val::I32(*x), - I64Const(x) => Val::I64(*x), - F32Const(x) => Val::F32(x.bits), - F64Const(x) => Val::F64(x.bits), - V128Const(x) => Val::V128(u128::from_le_bytes(x.to_le_bytes())), - RefNull(HeapType::Extern) => Val::ExternRef(None), - RefNull(HeapType::Func) => Val::FuncRef(None), - RefExtern(x) => Val::ExternRef(Some(ExternRef::new(*x))), - other => bail!("couldn't convert {:?} to a runtime value", other), - }) -} +use wast::{QuoteWat, Wast, WastArg, WastDirective, WastExecute, WastInvoke, WastRet, Wat}; /// The wast test script language allows modules to be defined and actions /// to be performed on them. pub struct WastContext { /// Wast files have a concept of a "current" module, which is the most /// recently defined. - current: Option, + current: Option, core_linker: Linker, #[cfg(feature = "component-model")] component_linker: component::Linker, store: Store, } -enum Outcome> { +enum Outcome { Ok(T), Trap(Trap), } @@ -66,6 +43,25 @@ impl Outcome { } } +#[derive(Debug)] +enum Results { + Core(Vec), + #[cfg(feature = "component-model")] + Component(component::Val), +} + +enum InstanceKind { + Core(Instance), + #[cfg(feature = "component-model")] + Component(component::Instance), +} + +enum Export { + Core(Extern), + #[cfg(feature = "component-model")] + Component(component::Func), +} + impl WastContext { /// Construct a new instance of `WastContext`. pub fn new(store: Store) -> Self { @@ -87,19 +83,30 @@ impl WastContext { } } - fn get_export(&mut self, module: Option<&str>, name: &str) -> Result { - match module { - Some(module) => self - .core_linker - .get(&mut self.store, module, name) - .ok_or_else(|| anyhow!("no item named `{}::{}` found", module, name)), - None => self - .current - .as_ref() - .ok_or_else(|| anyhow!("no previous instance found"))? - .get_export(&mut self.store, name) - .ok_or_else(|| anyhow!("no item named `{}` found", name)), + fn get_export(&mut self, module: Option<&str>, name: &str) -> Result { + if let Some(module) = module { + return Ok(Export::Core( + self.core_linker + .get(&mut self.store, module, name) + .ok_or_else(|| anyhow!("no item named `{}::{}` found", module, name))?, + )); } + + let cur = self + .current + .as_ref() + .ok_or_else(|| anyhow!("no previous instance found"))?; + Ok(match cur { + InstanceKind::Core(i) => Export::Core( + i.get_export(&mut self.store, name) + .ok_or_else(|| anyhow!("no item named `{}` found", name))?, + ), + #[cfg(feature = "component-model")] + InstanceKind::Component(i) => Export::Component( + i.get_func(&mut self.store, name) + .ok_or_else(|| anyhow!("no func named `{}` found", name))?, + ), + }) } fn instantiate_module(&mut self, module: &[u8]) -> Result> { @@ -134,30 +141,67 @@ impl WastContext { fn perform_execute(&mut self, exec: WastExecute<'_>) -> Result { match exec { WastExecute::Invoke(invoke) => self.perform_invoke(invoke), - WastExecute::Wat(mut module) => { - let result = match &mut module { - Wat::Module(m) => self.instantiate_module(&m.encode()?)?.map(|_| ()), - #[cfg(feature = "component-model")] - Wat::Component(m) => self.instantiate_component(&m.encode()?)?.map(|_| ()), - #[cfg(not(feature = "component-model"))] - Wat::Component(_) => bail!("component-model support not enabled"), - }; - Ok(match result { - Outcome::Ok(_) => Outcome::Ok(Vec::new()), - Outcome::Trap(e) => Outcome::Trap(e), - }) - } + WastExecute::Wat(mut module) => Ok(match &mut module { + Wat::Module(m) => self + .instantiate_module(&m.encode()?)? + .map(|_| Results::Core(Vec::new())), + #[cfg(feature = "component-model")] + Wat::Component(m) => self + .instantiate_component(&m.encode()?)? + .map(|_| Results::Component(component::Val::Unit)), + #[cfg(not(feature = "component-model"))] + Wat::Component(_) => bail!("component-model support not enabled"), + }), WastExecute::Get { module, global } => self.get(module.map(|s| s.name()), global), } } fn perform_invoke(&mut self, exec: WastInvoke<'_>) -> Result { - let values = exec - .args - .iter() - .map(|v| runtime_value(v)) - .collect::>>()?; - self.invoke(exec.module.map(|i| i.name()), exec.name, &values) + match self.get_export(exec.module.map(|i| i.name()), exec.name)? { + Export::Core(export) => { + let func = export + .into_func() + .ok_or_else(|| anyhow!("no function named `{}`", exec.name))?; + let values = exec + .args + .iter() + .map(|v| match v { + WastArg::Core(v) => core::val(v), + WastArg::Component(_) => bail!("expected component function, found core"), + }) + .collect::>>()?; + + let mut results = vec![Val::null(); func.ty(&self.store).results().len()]; + Ok(match func.call(&mut self.store, &values, &mut results) { + Ok(()) => Outcome::Ok(Results::Core(results.into())), + Err(e) => Outcome::Trap(e.downcast()?), + }) + } + #[cfg(feature = "component-model")] + Export::Component(func) => { + let params = func.params(&self.store); + if exec.args.len() != params.len() { + bail!("mismatched number of parameters") + } + let values = exec + .args + .iter() + .zip(params.iter()) + .map(|(v, t)| match v { + WastArg::Component(v) => component::val(v, t), + WastArg::Core(_) => bail!("expected core function, found component"), + }) + .collect::>>()?; + + Ok(match func.call(&mut self.store, &values) { + Ok(results) => { + func.post_return(&mut self.store)?; + Outcome::Ok(Results::Component(results.into())) + } + Err(e) => Outcome::Trap(e.downcast()?), + }) + } + } } /// Define a module and register it. @@ -178,7 +222,7 @@ impl WastContext { self.core_linker .instance(&mut self.store, name.name(), instance)?; } - self.current = Some(instance); + self.current = Some(InstanceKind::Core(instance)); } else { #[cfg(feature = "component-model")] { @@ -197,6 +241,7 @@ impl WastContext { linker.module(name, module)?; } } + self.current = Some(InstanceKind::Component(instance)); } #[cfg(not(feature = "component-model"))] bail!("component-model support not enabled"); @@ -209,49 +254,65 @@ impl WastContext { match name { Some(name) => self.core_linker.alias_module(name, as_name), None => { - let current = *self + let current = self .current .as_ref() .ok_or(anyhow!("no previous instance"))?; - self.core_linker - .instance(&mut self.store, as_name, current)?; + match current { + InstanceKind::Core(current) => { + self.core_linker + .instance(&mut self.store, as_name, *current)?; + } + #[cfg(feature = "component-model")] + InstanceKind::Component(_) => { + bail!("register not implemented for components"); + } + } Ok(()) } } } - /// Invoke an exported function from an instance. - fn invoke( - &mut self, - instance_name: Option<&str>, - field: &str, - args: &[Val], - ) -> Result { - let func = self - .get_export(instance_name, field)? - .into_func() - .ok_or_else(|| anyhow!("no function named `{}`", field))?; - - let mut results = vec![Val::null(); func.ty(&self.store).results().len()]; - Ok(match func.call(&mut self.store, args, &mut results) { - Ok(()) => Outcome::Ok(results.into()), - Err(e) => Outcome::Trap(e.downcast()?), - }) - } - /// Get the value of an exported global from an instance. fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result { - let global = self - .get_export(instance_name, field)? - .into_global() - .ok_or_else(|| anyhow!("no global named `{}`", field))?; - Ok(Outcome::Ok(vec![global.get(&mut self.store)])) + let global = match self.get_export(instance_name, field)? { + Export::Core(e) => e + .into_global() + .ok_or_else(|| anyhow!("no global named `{field}`"))?, + #[cfg(feature = "component-model")] + Export::Component(_) => bail!("no global named `{field}`"), + }; + Ok(Outcome::Ok(Results::Core( + vec![global.get(&mut self.store)], + ))) } - fn assert_return(&self, result: Outcome, results: &[AssertExpression]) -> Result<()> { - let values = result.into_result()?; - for (i, (v, e)) in values.iter().zip(results).enumerate() { - match_val(v, e).with_context(|| format!("result {} didn't match", i))?; + fn assert_return(&self, result: Outcome, results: &[WastRet<'_>]) -> Result<()> { + match result.into_result()? { + Results::Core(values) => { + for (i, (v, e)) in values.iter().zip(results).enumerate() { + let e = match e { + WastRet::Core(core) => core, + WastRet::Component(_) => { + bail!("expected component value found core value") + } + }; + core::match_val(v, e).with_context(|| format!("result {} didn't match", i))?; + } + } + #[cfg(feature = "component-model")] + Results::Component(value) => { + if results.len() != 1 { + bail!("expected one result value assertion"); + } + let result = match &results[0] { + WastRet::Component(ret) => ret, + WastRet::Core(_) => { + bail!("expected core value found component value") + } + }; + component::match_val(&result, &value)?; + } } Ok(()) } @@ -414,327 +475,3 @@ fn is_matching_assert_invalid_error_message(expected: &str, actual: &str) -> boo // the memory64 tests to pass. || (expected.contains("memory size must be at most 65536 pages") && actual.contains("invalid u32 number")) } - -fn extract_lane_as_i8(bytes: u128, lane: usize) -> i8 { - (bytes >> (lane * 8)) as i8 -} - -fn extract_lane_as_i16(bytes: u128, lane: usize) -> i16 { - (bytes >> (lane * 16)) as i16 -} - -fn extract_lane_as_i32(bytes: u128, lane: usize) -> i32 { - (bytes >> (lane * 32)) as i32 -} - -fn extract_lane_as_i64(bytes: u128, lane: usize) -> i64 { - (bytes >> (lane * 64)) as i64 -} - -fn match_val(actual: &Val, expected: &AssertExpression) -> Result<()> { - match (actual, expected) { - (Val::I32(a), AssertExpression::I32(b)) => match_int(a, b), - (Val::I64(a), AssertExpression::I64(b)) => match_int(a, b), - // Note that these float comparisons are comparing bits, not float - // values, so we're testing for bit-for-bit equivalence - (Val::F32(a), AssertExpression::F32(b)) => match_f32(*a, b), - (Val::F64(a), AssertExpression::F64(b)) => match_f64(*a, b), - (Val::V128(a), AssertExpression::V128(b)) => match_v128(*a, b), - (Val::ExternRef(x), AssertExpression::RefNull(Some(HeapType::Extern))) => { - if let Some(x) = x { - let x = x - .data() - .downcast_ref::() - .expect("only u32 externrefs created in wast test suites"); - bail!("expected null externref, found {}", x); - } else { - Ok(()) - } - } - (Val::ExternRef(x), AssertExpression::RefExtern(y)) => { - if let Some(x) = x { - let x = x - .data() - .downcast_ref::() - .expect("only u32 externrefs created in wast test suites"); - if x == y { - Ok(()) - } else { - bail!("expected {} found {}", y, x); - } - } else { - bail!("expected non-null externref, found null") - } - } - (Val::FuncRef(x), AssertExpression::RefNull(_)) => { - if x.is_none() { - Ok(()) - } else { - bail!("expected null funcref, found non-null") - } - } - _ => bail!( - "don't know how to compare {:?} and {:?} yet", - actual, - expected - ), - } -} - -fn match_int(actual: &T, expected: &T) -> Result<()> -where - T: Eq + Display + LowerHex, -{ - if actual == expected { - Ok(()) - } else { - bail!( - "expected {:18} / {0:#018x}\n\ - actual {:18} / {1:#018x}", - expected, - actual - ) - } -} - -fn match_f32(actual: u32, expected: &NanPattern) -> Result<()> { - match expected { - // Check if an f32 (as u32 bits to avoid possible quieting when moving values in registers, e.g. - // https://developer.arm.com/documentation/ddi0344/i/neon-and-vfp-programmers-model/modes-of-operation/default-nan-mode?lang=en) - // is a canonical NaN: - // - the sign bit is unspecified, - // - the 8-bit exponent is set to all 1s - // - the MSB of the payload is set to 1 (a quieted NaN) and all others to 0. - // See https://webassembly.github.io/spec/core/syntax/values.html#floating-point. - NanPattern::CanonicalNan => { - let canon_nan = 0x7fc0_0000; - if (actual & 0x7fff_ffff) == canon_nan { - Ok(()) - } else { - bail!( - "expected {:10} / {:#010x}\n\ - actual {:10} / {:#010x}", - "canon-nan", - canon_nan, - f32::from_bits(actual), - actual, - ) - } - } - - // Check if an f32 (as u32, see comments above) is an arithmetic NaN. - // This is the same as a canonical NaN including that the payload MSB is - // set to 1, but one or more of the remaining payload bits MAY BE set to - // 1 (a canonical NaN specifies all 0s). See - // https://webassembly.github.io/spec/core/syntax/values.html#floating-point. - NanPattern::ArithmeticNan => { - const AF32_NAN: u32 = 0x7f80_0000; - let is_nan = actual & AF32_NAN == AF32_NAN; - const AF32_PAYLOAD_MSB: u32 = 0x0040_0000; - let is_msb_set = actual & AF32_PAYLOAD_MSB == AF32_PAYLOAD_MSB; - if is_nan && is_msb_set { - Ok(()) - } else { - bail!( - "expected {:>10} / {:>10}\n\ - actual {:10} / {:#010x}", - "arith-nan", - "0x7fc*****", - f32::from_bits(actual), - actual, - ) - } - } - NanPattern::Value(expected_value) => { - if actual == expected_value.bits { - Ok(()) - } else { - bail!( - "expected {:10} / {:#010x}\n\ - actual {:10} / {:#010x}", - f32::from_bits(expected_value.bits), - expected_value.bits, - f32::from_bits(actual), - actual, - ) - } - } - } -} - -fn match_f64(actual: u64, expected: &NanPattern) -> Result<()> { - match expected { - // Check if an f64 (as u64 bits to avoid possible quieting when moving values in registers, e.g. - // https://developer.arm.com/documentation/ddi0344/i/neon-and-vfp-programmers-model/modes-of-operation/default-nan-mode?lang=en) - // is a canonical NaN: - // - the sign bit is unspecified, - // - the 11-bit exponent is set to all 1s - // - the MSB of the payload is set to 1 (a quieted NaN) and all others to 0. - // See https://webassembly.github.io/spec/core/syntax/values.html#floating-point. - NanPattern::CanonicalNan => { - let canon_nan = 0x7ff8_0000_0000_0000; - if (actual & 0x7fff_ffff_ffff_ffff) == canon_nan { - Ok(()) - } else { - bail!( - "expected {:18} / {:#018x}\n\ - actual {:18} / {:#018x}", - "canon-nan", - canon_nan, - f64::from_bits(actual), - actual, - ) - } - } - - // Check if an f64 (as u64, see comments above) is an arithmetic NaN. This is the same as a - // canonical NaN including that the payload MSB is set to 1, but one or more of the remaining - // payload bits MAY BE set to 1 (a canonical NaN specifies all 0s). See - // https://webassembly.github.io/spec/core/syntax/values.html#floating-point. - NanPattern::ArithmeticNan => { - const AF64_NAN: u64 = 0x7ff0_0000_0000_0000; - let is_nan = actual & AF64_NAN == AF64_NAN; - const AF64_PAYLOAD_MSB: u64 = 0x0008_0000_0000_0000; - let is_msb_set = actual & AF64_PAYLOAD_MSB == AF64_PAYLOAD_MSB; - if is_nan && is_msb_set { - Ok(()) - } else { - bail!( - "expected {:>18} / {:>18}\n\ - actual {:18} / {:#018x}", - "arith-nan", - "0x7ff8************", - f64::from_bits(actual), - actual, - ) - } - } - NanPattern::Value(expected_value) => { - if actual == expected_value.bits { - Ok(()) - } else { - bail!( - "expected {:18} / {:#018x}\n\ - actual {:18} / {:#018x}", - f64::from_bits(expected_value.bits), - expected_value.bits, - f64::from_bits(actual), - actual, - ) - } - } - } -} - -fn match_v128(actual: u128, expected: &V128Pattern) -> Result<()> { - match expected { - V128Pattern::I8x16(expected) => { - let actual = [ - extract_lane_as_i8(actual, 0), - extract_lane_as_i8(actual, 1), - extract_lane_as_i8(actual, 2), - extract_lane_as_i8(actual, 3), - extract_lane_as_i8(actual, 4), - extract_lane_as_i8(actual, 5), - extract_lane_as_i8(actual, 6), - extract_lane_as_i8(actual, 7), - extract_lane_as_i8(actual, 8), - extract_lane_as_i8(actual, 9), - extract_lane_as_i8(actual, 10), - extract_lane_as_i8(actual, 11), - extract_lane_as_i8(actual, 12), - extract_lane_as_i8(actual, 13), - extract_lane_as_i8(actual, 14), - extract_lane_as_i8(actual, 15), - ]; - if actual == *expected { - return Ok(()); - } - bail!( - "expected {:4?}\n\ - actual {:4?}\n\ - \n\ - expected (hex) {0:02x?}\n\ - actual (hex) {1:02x?}", - expected, - actual, - ) - } - V128Pattern::I16x8(expected) => { - let actual = [ - extract_lane_as_i16(actual, 0), - extract_lane_as_i16(actual, 1), - extract_lane_as_i16(actual, 2), - extract_lane_as_i16(actual, 3), - extract_lane_as_i16(actual, 4), - extract_lane_as_i16(actual, 5), - extract_lane_as_i16(actual, 6), - extract_lane_as_i16(actual, 7), - ]; - if actual == *expected { - return Ok(()); - } - bail!( - "expected {:6?}\n\ - actual {:6?}\n\ - \n\ - expected (hex) {0:04x?}\n\ - actual (hex) {1:04x?}", - expected, - actual, - ) - } - V128Pattern::I32x4(expected) => { - let actual = [ - extract_lane_as_i32(actual, 0), - extract_lane_as_i32(actual, 1), - extract_lane_as_i32(actual, 2), - extract_lane_as_i32(actual, 3), - ]; - if actual == *expected { - return Ok(()); - } - bail!( - "expected {:11?}\n\ - actual {:11?}\n\ - \n\ - expected (hex) {0:08x?}\n\ - actual (hex) {1:08x?}", - expected, - actual, - ) - } - V128Pattern::I64x2(expected) => { - let actual = [ - extract_lane_as_i64(actual, 0), - extract_lane_as_i64(actual, 1), - ]; - if actual == *expected { - return Ok(()); - } - bail!( - "expected {:20?}\n\ - actual {:20?}\n\ - \n\ - expected (hex) {0:016x?}\n\ - actual (hex) {1:016x?}", - expected, - actual, - ) - } - V128Pattern::F32x4(expected) => { - for (i, expected) in expected.iter().enumerate() { - let a = extract_lane_as_i32(actual, i) as u32; - match_f32(a, expected).with_context(|| format!("difference in lane {}", i))?; - } - Ok(()) - } - V128Pattern::F64x2(expected) => { - for (i, expected) in expected.iter().enumerate() { - let a = extract_lane_as_i64(actual, i) as u64; - match_f64(a, expected).with_context(|| format!("difference in lane {}", i))?; - } - Ok(()) - } - } -} diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 6dd1ce715a..dd5fc4f036 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -31,30 +31,60 @@ criteria = "safe-to-deploy" version = "0.14.0" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wasm-encoder]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.15.0" +notes = "The Bytecode Alliance is the author of this crate." + [[audits.wasm-mutate]] who = "Alex Crichton " criteria = "safe-to-deploy" version = "0.2.5" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wasm-mutate]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.2.6" +notes = "The Bytecode Alliance is the author of this crate." + [[audits.wasm-smith]] who = "Alex Crichton " criteria = "safe-to-deploy" version = "0.11.2" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wasm-smith]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.11.3" +notes = "The Bytecode Alliance is the author of this crate." + [[audits.wasmparser]] who = "Alex Crichton " criteria = "safe-to-deploy" version = "0.87.0" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wasmparser]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.88.0" +notes = "The Bytecode Alliance is the author of this crate." + [[audits.wasmprinter]] who = "Alex Crichton " criteria = "safe-to-deploy" version = "0.2.37" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wasmprinter]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.2.38" +notes = "The Bytecode Alliance is the author of this crate." + [[audits.wast]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -67,9 +97,21 @@ criteria = "safe-to-deploy" version = "44.0.0" notes = "The Bytecode Alliance is the author of this crate" +[[audits.wast]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "45.0.0" +notes = "The Bytecode Alliance is the author of this crate" + [[audits.wat]] who = "Alex Crichton " criteria = "safe-to-deploy" version = "1.0.46" notes = "The Bytecode Alliance is the author of this crate." +[[audits.wat]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.0.47" +notes = "The Bytecode Alliance is the author of this crate." + diff --git a/tests/misc_testsuite/component-model/fused.wast b/tests/misc_testsuite/component-model/fused.wast index 6faf0b8496..b311178430 100644 --- a/tests/misc_testsuite/component-model/fused.wast +++ b/tests/misc_testsuite/component-model/fused.wast @@ -160,9 +160,17 @@ (component $c (import "roundtrip" (func $roundtrip (type $roundtrip))) - (core module $libc (memory (export "memory") 1)) + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) unreachable) + ) (core instance $libc (instantiate $libc)) - (core func $roundtrip (canon lower (func $roundtrip) (memory $libc "memory"))) + (core func $roundtrip + (canon lower (func $roundtrip) + (memory $libc "memory") + (realloc (func $libc "realloc")) ;; FIXME(wasm-tools#693) should not be necessary + ) + ) (core module $m2 (import "libc" "memory" (memory 1)) @@ -312,9 +320,18 @@ (component $c (import "foo" (func $foo (param $tuple20))) - (core module $libc (memory (export "memory") 1)) + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + unreachable) + ) (core instance $libc (instantiate $libc)) - (core func $foo (canon lower (func $foo) (memory $libc "memory"))) + (core func $foo + (canon lower (func $foo) + (memory $libc "memory") + (realloc (func $libc "realloc")) ;; FIXME(wasm-tools#693) should not be necessary + ) + ) (core module $something (import "" "foo" (func (param i32))) ) @@ -620,9 +637,17 @@ ) (component $c2 (import "r" (func $r (param $big))) - (core module $libc (memory (export "memory") 1)) + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) unreachable) + ) (core instance $libc (instantiate $libc)) - (core func $r (canon lower (func $r) (memory $libc "memory"))) + (core func $r + (canon lower (func $r) + (memory $libc "memory") + (realloc (func $libc "realloc")) ;; FIXME(wasm-tools#693) should not be necessary + ) + ) (core module $m (import "" "r" (func $r (param i32))) @@ -660,9 +685,18 @@ ) (component $c2 (import "r" (func $r (param $big))) - (core module $libc (memory (export "memory") 1)) + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) unreachable) + ) (core instance $libc (instantiate $libc)) - (core func $r (canon lower (func $r) (memory $libc "memory"))) + (core func $r + (canon lower (func $r) + (memory $libc "memory") + (realloc (func $libc "realloc")) ;; FIXME(wasm-tools#693) should not be necessary + ) + ) + (core module $m (import "" "r" (func $r (param i32))) diff --git a/tests/misc_testsuite/component-model/simple.wast b/tests/misc_testsuite/component-model/simple.wast index 7901fb87f4..df1c05b536 100644 --- a/tests/misc_testsuite/component-model/simple.wast +++ b/tests/misc_testsuite/component-model/simple.wast @@ -32,3 +32,11 @@ (component (export "")) ) "exporting a component from the root component is not supported") + +(component + (core module $m (func (export ""))) + (core instance $m (instantiate $m)) + (func (export "") (canon lift (core func $m ""))) +) + +(assert_return (invoke "") (unit.const)) diff --git a/tests/misc_testsuite/component-model/types.wast b/tests/misc_testsuite/component-model/types.wast index 358ef35571..845005358e 100644 --- a/tests/misc_testsuite/component-model/types.wast +++ b/tests/misc_testsuite/component-model/types.wast @@ -101,3 +101,28 @@ )) ) ) + +(component + (core module $m (func (export "") (param i32) (result i32) local.get 0)) + (core instance $m (instantiate $m)) + (func (export "i-to-b") (param u32) (result bool) (canon lift (core func $m ""))) + (func (export "i-to-u8") (param u32) (result u8) (canon lift (core func $m ""))) + (func (export "i-to-s8") (param u32) (result s8) (canon lift (core func $m ""))) + (func (export "i-to-u16") (param u32) (result u16) (canon lift (core func $m ""))) + (func (export "i-to-s16") (param u32) (result s16) (canon lift (core func $m ""))) +) +(assert_return (invoke "i-to-b" (u32.const 0)) (bool.const false)) +(assert_return (invoke "i-to-b" (u32.const 1)) (bool.const true)) +(assert_return (invoke "i-to-b" (u32.const 2)) (bool.const true)) +(assert_return (invoke "i-to-u8" (u32.const 0x00)) (u8.const 0)) +(assert_return (invoke "i-to-u8" (u32.const 0x01)) (u8.const 1)) +(assert_return (invoke "i-to-u8" (u32.const 0xf01)) (u8.const 1)) +(assert_return (invoke "i-to-u8" (u32.const 0xf00)) (u8.const 0)) +(assert_return (invoke "i-to-s8" (u32.const 0xffffffff)) (s8.const -1)) +(assert_return (invoke "i-to-s8" (u32.const 127)) (s8.const 127)) +(assert_return (invoke "i-to-u16" (u32.const 0)) (u16.const 0)) +(assert_return (invoke "i-to-u16" (u32.const 1)) (u16.const 1)) +(assert_return (invoke "i-to-u16" (u32.const 0xffffffff)) (u16.const 0xffff)) +(assert_return (invoke "i-to-s16" (u32.const 0)) (s16.const 0)) +(assert_return (invoke "i-to-s16" (u32.const 1)) (s16.const 1)) +(assert_return (invoke "i-to-s16" (u32.const 0xffffffff)) (s16.const -1))