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.
This commit is contained in:
Alex Crichton
2022-07-27 16:02:16 -05:00
committed by GitHub
parent 0508932174
commit 174b60dcf7
25 changed files with 1154 additions and 468 deletions

View File

@@ -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<Val> {
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::<Result<Vec<_>>>()?;
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::<Result<Vec<_>>>()?
.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::<BTreeSet<_>>();
let actual = a.flags().collect::<BTreeSet<_>>();
match_debug(&actual, &expected)
}
_ => mismatch(expected, actual),
},
}
}
fn match_debug<T>(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}`")
}

346
crates/wast/src/core.rs Normal file
View File

@@ -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<Val> {
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::<u32>()
.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::<u32>()
.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<T>(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<Float32>) -> 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<Float64>) -> 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(())
}
}
}

View File

@@ -21,6 +21,9 @@
)
)]
#[cfg(feature = "component-model")]
mod component;
mod core;
mod spectest;
mod wast;

View File

@@ -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<Val> {
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<T> {
/// Wast files have a concept of a "current" module, which is the most
/// recently defined.
current: Option<Instance>,
current: Option<InstanceKind>,
core_linker: Linker<T>,
#[cfg(feature = "component-model")]
component_linker: component::Linker<T>,
store: Store<T>,
}
enum Outcome<T = Vec<Val>> {
enum Outcome<T = Results> {
Ok(T),
Trap(Trap),
}
@@ -66,6 +43,25 @@ impl<T> Outcome<T> {
}
}
#[derive(Debug)]
enum Results {
Core(Vec<Val>),
#[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<T> WastContext<T> {
/// Construct a new instance of `WastContext`.
pub fn new(store: Store<T>) -> Self {
@@ -87,19 +83,30 @@ impl<T> WastContext<T> {
}
}
fn get_export(&mut self, module: Option<&str>, name: &str) -> Result<Extern> {
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<Export> {
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<Outcome<Instance>> {
@@ -134,30 +141,67 @@ impl<T> WastContext<T> {
fn perform_execute(&mut self, exec: WastExecute<'_>) -> Result<Outcome> {
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<Outcome> {
let values = exec
.args
.iter()
.map(|v| runtime_value(v))
.collect::<Result<Vec<_>>>()?;
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::<Result<Vec<_>>>()?;
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::<Result<Vec<_>>>()?;
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<T> WastContext<T> {
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<T> WastContext<T> {
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<T> WastContext<T> {
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<Outcome> {
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<Outcome> {
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::<u32>()
.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::<u32>()
.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<T>(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<Float32>) -> 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<Float64>) -> 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(())
}
}
}