From cfe7cc5c513b356df4bd879545f23292ce23942c Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Sun, 6 Sep 2026 21:46:28 -0400 Subject: [PATCH] testgen: emit the upstream json-from-wast schema Replace testgen's hand-written wast->JSON emitter (~700 lines, its own `kind` tag and component-value encoding) with the upstream `json-from-wast` crate at the pinned wasm-tools release train (0.258) - the implementation of `wasm-tools json-from-wast` and the same conversion wasmtime's wast runner performs in-process. The reasons recorded for owning the emitter no longer hold: the parser lag was version skew (fixed by linking the crate, not owning a copy), and upstream's value encoding is specified by its serde derives. Harness migrates to upstream's schema unmodified: `schema.ts` mirrors the exercised subset of json-from-wast's lib.rs; `value-mapping.ts` handles bool-as-boolean, record `[name, v][]`, variant `{case, payload?}`, result `{Ok|Err}`; the runner classifies core module vs component from the binary preamble (`artifactKind`) since the JSON no longer says. Upstream's `line` for module-/action-bearing asserts is the inner form's line, so the (file, line)-keyed xfail entries and the webkit lane overlay were remapped by command index against the previous output (same directive order; 509 of 1511 lines moved). Ten xfail entries in async/trap-if-sync-and-waitable-set.json referenced lines that had not existed in the corpus since the file grew upstream; deleted. --- Cargo.lock | 40 +- crates/testgen/Cargo.toml | 2 +- crates/testgen/src/convert.rs | 520 -------------------- crates/testgen/src/json.rs | 190 ------- crates/testgen/src/main.rs | 47 +- docs/architecture.md | 9 +- docs/references.md | 2 + harness/README.md | 105 +--- harness/browser/expectations/webkit.ts | 102 ++-- harness/src/executor.ts | 3 +- harness/src/runner.ts | 26 +- harness/src/schema.ts | 73 +-- harness/src/value-mapping.ts | 118 +++-- harness/src/xfail.ts | 130 +---- harness/tests/runner_unit_test.ts | 17 +- runtime/tests/integration/e2e_suite_test.ts | 34 +- upstream-component-model-repo-findings.md | 7 +- 17 files changed, 339 insertions(+), 1086 deletions(-) delete mode 100644 crates/testgen/src/convert.rs delete mode 100644 crates/testgen/src/json.rs diff --git a/Cargo.lock b/Cargo.lock index 229c4c1a..3f48d98f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,6 +253,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fnv" version = "1.0.7" @@ -275,6 +281,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "gimli" version = "0.33.0" @@ -339,6 +356,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "json-from-wast" +version = "0.258.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c74cef75a485ea9c18b6f7c1928bda2ef3a5727227fe7ebab4260626bc75f98e" +dependencies = [ + "anyhow", + "serde", + "serde_derive", + "wast", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -497,6 +526,12 @@ dependencies = [ "serde", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" @@ -534,7 +569,7 @@ name = "testgen" version = "0.0.0" dependencies = [ "anyhow", - "serde", + "json-from-wast", "serde_json", "wast", ] @@ -646,7 +681,7 @@ dependencies = [ "cranelift-bforest", "cranelift-bitset", "cranelift-entity", - "gimli", + "gimli 0.33.0", "hashbrown 0.17.1", "indexmap", "log", @@ -689,6 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97f7defc7ecca8b19ac7f824598eadd0c53985ee00c74060d65051e9da5b58a1" dependencies = [ "bumpalo", + "gimli 0.32.3", "leb128fmt", "memchr", "unicode-width", diff --git a/crates/testgen/Cargo.toml b/crates/testgen/Cargo.toml index 0ee35e19..61030495 100644 --- a/crates/testgen/Cargo.toml +++ b/crates/testgen/Cargo.toml @@ -10,6 +10,6 @@ path = "src/main.rs" [dependencies] anyhow = "1" -serde = { version = "1", features = ["derive"] } +json-from-wast = "0.258.0" serde_json = { version = "1", features = ["preserve_order"] } wast = "258.0.0" diff --git a/crates/testgen/src/convert.rs b/crates/testgen/src/convert.rs deleted file mode 100644 index ad4a2db2..00000000 --- a/crates/testgen/src/convert.rs +++ /dev/null @@ -1,520 +0,0 @@ -//! Conversion of one parsed `.wast` file into JSON commands + artifact files. - -use anyhow::{bail, Context, Result}; -use wast::core::{AbstractHeapType, HeapType, NanPattern, V128Const, V128Pattern}; -use wast::core::{WastArgCore, WastRetCore}; -use wast::component::WastVal; -use wast::token::{Id, Span}; -use wast::{QuoteWat, QuoteWatTest, Wast, WastArg, WastDirective, WastExecute, WastInvoke, WastRet, Wat}; - -use crate::json::{Action, Command, Kind, ModuleType, Value, WastJson}; - -/// Result of converting a single `.wast` file: the JSON document plus the -/// artifact files (`..wasm` / `.wat`) it references. -pub struct Converted { - pub json: WastJson, - pub artifacts: Vec<(String, Vec)>, -} - -pub fn convert_wast(source_rel: &str, stem: &str, text: &str) -> Result { - let mut lexer = wast::lexer::Lexer::new(text); - lexer.allow_confusing_unicode(true); - let buf = wast::parser::ParseBuffer::new_with_lexer(lexer) - .map_err(|e| pretty(e, source_rel, text))?; - let wast: Wast = wast::parser::parse(&buf).map_err(|e| pretty(e, source_rel, text))?; - - let mut cx = Converter { - text, - stem, - next_idx: 0, - commands: Vec::new(), - artifacts: Vec::new(), - }; - for directive in wast.directives { - let line = cx.line_of(directive.span()); - cx.directive(directive) - .with_context(|| format!("{source_rel}:{line}: failed to convert directive"))?; - } - Ok(Converted { - json: WastJson { - source_filename: source_rel.to_string(), - commands: cx.commands, - }, - artifacts: cx.artifacts, - }) -} - -fn pretty(mut e: wast::Error, path: &str, text: &str) -> anyhow::Error { - e.set_path(std::path::Path::new(path)); - e.set_text(text); - anyhow::anyhow!("{e}") -} - -struct Converter<'a> { - text: &'a str, - stem: &'a str, - next_idx: usize, - commands: Vec, - artifacts: Vec<(String, Vec)>, -} - -/// An extracted module/component artifact, ready to reference from a command. -struct Artifact { - name: Option, - filename: String, - module_type: ModuleType, - kind: Kind, -} - -impl<'a> Converter<'a> { - fn line_of(&self, span: Span) -> usize { - span.linecol_in(self.text).0 + 1 - } - - fn directive(&mut self, directive: WastDirective<'a>) -> Result<()> { - let line = self.line_of(directive.span()); - match directive { - WastDirective::Module(qw) => { - let a = self.emit_quote_wat(qw, false)?; - self.commands.push(Command::Module { - line, - name: a.name, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - }); - } - WastDirective::ModuleDefinition(qw) => { - let a = self.emit_quote_wat(qw, false)?; - self.commands.push(Command::ModuleDefinition { - line, - name: a.name, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - }); - } - WastDirective::ModuleInstance { - instance, module, .. - } => { - self.commands.push(Command::ModuleInstance { - line, - instance: instance.map(id_name), - module: module.map(id_name), - }); - } - WastDirective::AssertMalformed { module, message, .. } => { - let a = self.emit_quote_wat(module, true)?; - self.commands.push(Command::AssertMalformed { - line, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - text: message.to_string(), - }); - } - WastDirective::AssertInvalid { module, message, .. } => { - let a = self.emit_quote_wat(module, false)?; - self.commands.push(Command::AssertInvalid { - line, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - text: message.to_string(), - }); - } - WastDirective::AssertMalformedCustom { .. } | WastDirective::AssertInvalidCustom { .. } => { - bail!("assert_malformed_custom / assert_invalid_custom are not supported") - } - WastDirective::Register { name, module, .. } => { - self.commands.push(Command::Register { - line, - as_: name.to_string(), - name: module.map(id_name), - }); - } - WastDirective::Invoke(invoke) => { - let action = self.invoke_action(invoke)?; - self.commands.push(Command::Action { line, action }); - } - WastDirective::AssertTrap { exec, message, .. } => match exec { - // `(assert_trap (component ...) "...")`: instantiation trap. - WastExecute::Wat(wat) => { - let a = self.emit_wat(wat)?; - self.commands.push(Command::AssertUninstantiable { - line, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - text: message.to_string(), - }); - } - exec => { - let action = self.execute_action(exec)?; - self.commands.push(Command::AssertTrap { - line, - action, - text: message.to_string(), - }); - } - }, - WastDirective::AssertReturn { exec, results, .. } => { - let action = self.execute_action(exec)?; - let expected = results - .into_iter() - .map(|r| self.ret_value(r)) - .collect::>>()?; - self.commands.push(Command::AssertReturn { - line, - action, - expected, - }); - } - WastDirective::AssertExhaustion { call, message, .. } => { - let action = self.invoke_action(call)?; - self.commands.push(Command::AssertExhaustion { - line, - action, - text: message.to_string(), - }); - } - WastDirective::AssertUnlinkable { module, message, .. } => { - let a = self.emit_wat(module)?; - self.commands.push(Command::AssertUnlinkable { - line, - filename: a.filename, - module_type: a.module_type, - kind: a.kind, - text: message.to_string(), - }); - } - WastDirective::AssertException { exec, .. } => { - let action = self.execute_action(exec)?; - self.commands.push(Command::AssertException { line, action }); - } - WastDirective::AssertSuspension { exec, message, .. } => { - let action = self.execute_action(exec)?; - self.commands.push(Command::AssertSuspension { - line, - action, - text: message.to_string(), - }); - } - WastDirective::Thread(_) | WastDirective::Wait { .. } => { - bail!("thread/wait directives are not supported") - } - } - Ok(()) - } - - /// Encode a module/component to an artifact file. - /// - /// `force_text_for_quote`: `assert_malformed` quote forms are always kept - /// as text (malformedness must be judged from the source text, and the - /// text generally cannot be encoded anyway). Everything else is encoded - /// to binary, falling back to text only for quote forms that fail to - /// parse. - fn emit_quote_wat(&mut self, mut qw: QuoteWat<'a>, force_text_for_quote: bool) -> Result { - let kind = match &qw { - QuoteWat::Wat(Wat::Module(_)) | QuoteWat::QuoteModule(..) => Kind::Module, - QuoteWat::Wat(Wat::Component(_)) | QuoteWat::QuoteComponent(..) => Kind::Component, - }; - let name = qw.name().map(id_name); - let is_quote = !matches!(qw, QuoteWat::Wat(_)); - - if is_quote && force_text_for_quote { - match qw.to_test().map_err(|e| pretty(e, "", self.text))? { - QuoteWatTest::Text(t) => return Ok(self.push_artifact(name, ModuleType::Text, kind, t)), - QuoteWatTest::Binary(_) => unreachable!("quote forms convert to text"), - } - } - - match qw.encode() { - Ok(bytes) => Ok(self.push_artifact(name, ModuleType::Binary, kind, bytes)), - Err(_) if is_quote => { - // Quote form whose text does not parse: keep it as text so a - // text-capable consumer could still run the assertion. - match qw.to_test().map_err(|e| pretty(e, "", self.text))? { - QuoteWatTest::Text(t) => Ok(self.push_artifact(name, ModuleType::Text, kind, t)), - QuoteWatTest::Binary(_) => unreachable!("quote forms convert to text"), - } - } - Err(e) => Err(pretty(e, "", self.text)).context("failed to encode module"), - } - } - - fn emit_wat(&mut self, wat: Wat<'a>) -> Result { - self.emit_quote_wat(QuoteWat::Wat(wat), false) - } - - fn push_artifact( - &mut self, - name: Option, - module_type: ModuleType, - kind: Kind, - bytes: Vec, - ) -> Artifact { - let ext = match module_type { - ModuleType::Binary => "wasm", - ModuleType::Text => "wat", - }; - let filename = format!("{}.{}.{}", self.stem, self.next_idx, ext); - self.next_idx += 1; - self.artifacts.push((filename.clone(), bytes)); - Artifact { - name, - filename, - module_type, - kind, - } - } - - fn execute_action(&mut self, exec: WastExecute<'a>) -> Result { - match exec { - WastExecute::Invoke(i) => self.invoke_action(i), - WastExecute::Get { module, global, .. } => Ok(Action::Get { - module: module.map(id_name), - field: global.to_string(), - }), - WastExecute::Wat(_) => bail!("inline module in action position is not supported here"), - } - } - - fn invoke_action(&mut self, invoke: WastInvoke<'a>) -> Result { - let args = invoke - .args - .into_iter() - .map(|a| self.arg_value(a)) - .collect::>>()?; - Ok(Action::Invoke { - module: invoke.module.map(id_name), - field: invoke.name.to_string(), - args, - }) - } - - fn arg_value(&self, arg: WastArg<'a>) -> Result { - match arg { - WastArg::Core(c) => core_arg_value(c), - WastArg::Component(v) => component_value(v), - _ => bail!("unsupported argument value"), - } - } - - fn ret_value(&self, ret: WastRet<'a>) -> Result { - match ret { - WastRet::Core(c) => core_ret_value(c), - WastRet::Component(v) => component_value(v), - _ => bail!("unsupported result value"), - } - } -} - -fn id_name(id: Id<'_>) -> String { - id.name().to_string() -} - -// ---------------------------------------------------------------- values --- - -fn core_arg_value(arg: WastArgCore<'_>) -> Result { - use WastArgCore::*; - Ok(match arg { - I32(v) => Value::simple("i32", (v as u32).to_string()), - I64(v) => Value::simple("i64", (v as u64).to_string()), - F32(f) => Value::simple("f32", f.bits.to_string()), - F64(f) => Value::simple("f64", f.bits.to_string()), - V128(v) => v128_const_value(v), - RefNull(ht) => Value::simple(heap_type_name(&ht)?, "null"), - RefExtern(v) => Value::simple("externref", v.to_string()), - RefHost(v) => Value::simple("hostref", v.to_string()), - }) -} - -fn core_ret_value(ret: WastRetCore<'_>) -> Result { - use WastRetCore::*; - Ok(match ret { - I32(v) => Value::simple("i32", (v as u32).to_string()), - I64(v) => Value::simple("i64", (v as u64).to_string()), - F32(f) => Value::simple("f32", nan_pattern_string(f, |f| f.bits.to_string())), - F64(f) => Value::simple("f64", nan_pattern_string(f, |f| f.bits.to_string())), - V128(v) => v128_pattern_value(v), - RefNull(ht) => match ht { - Some(ht) => Value::simple(heap_type_name(&ht)?, "null"), - None => Value::simple("refnull", "null"), - }, - RefExtern(v) => match v { - Some(v) => Value::simple("externref", v.to_string()), - None => Value::simple("externref", serde_json::Value::Null), - }, - RefHost(v) => Value::simple("hostref", v.to_string()), - RefFunc(_) => Value::simple("funcref", serde_json::Value::Null), - RefAny => Value::simple("anyref", serde_json::Value::Null), - RefEq => Value::simple("eqref", serde_json::Value::Null), - RefArray => Value::simple("arrayref", serde_json::Value::Null), - RefStruct => Value::simple("structref", serde_json::Value::Null), - RefI31 | RefI31Shared => Value::simple("i31ref", serde_json::Value::Null), - Either(alternatives) => { - let vals = alternatives - .into_iter() - .map(core_ret_value) - .collect::>>()?; - Value::simple("either", serde_json::to_value(vals)?) - } - }) -} - -fn nan_pattern_string(p: NanPattern, fmt: impl Fn(T) -> String) -> String { - match p { - NanPattern::CanonicalNan => "nan:canonical".to_string(), - NanPattern::ArithmeticNan => "nan:arithmetic".to_string(), - NanPattern::Value(v) => fmt(v), - } -} - -fn heap_type_name(ht: &HeapType<'_>) -> Result<&'static str> { - use AbstractHeapType::*; - match ht { - HeapType::Abstract { shared: false, ty } => Ok(match ty { - Func => "funcref", - Extern => "externref", - Any => "anyref", - Eq => "eqref", - Array => "arrayref", - Struct => "structref", - I31 => "i31ref", - Exn => "exnref", - None => "nullref", - NoFunc => "nullfuncref", - NoExtern => "nullexternref", - NoExn => "nullexnref", - _ => bail!("unsupported abstract heap type"), - }), - _ => bail!("unsupported heap type in wast value"), - } -} - -fn v128_lanes(lanes: &[T]) -> serde_json::Value { - lanes - .iter() - .map(|l| serde_json::Value::from(l.to_string())) - .collect::>() - .into() -} - -fn v128_value(lane_type: &str, lanes: serde_json::Value) -> Value { - Value { - ty: "v128".to_string(), - lane_type: Some(lane_type.to_string()), - case: None, - status: None, - value: lanes, - } -} - -fn v128_const_value(v: V128Const) -> Value { - use V128Const::*; - match &v { - I8x16(l) => v128_value("i8", v128_lanes(&l.map(|x| x as u8))), - I16x8(l) => v128_value("i16", v128_lanes(&l.map(|x| x as u16))), - I32x4(l) => v128_value("i32", v128_lanes(&l.map(|x| x as u32))), - I64x2(l) => v128_value("i64", v128_lanes(&l.map(|x| x as u64))), - F32x4(l) => v128_value("f32", v128_lanes(&l.map(|x| x.bits))), - F64x2(l) => v128_value("f64", v128_lanes(&l.map(|x| x.bits))), - } -} - -fn v128_pattern_value(v: V128Pattern) -> Value { - use V128Pattern::*; - match v { - I8x16(l) => v128_value("i8", v128_lanes(&l.map(|x| x as u8))), - I16x8(l) => v128_value("i16", v128_lanes(&l.map(|x| x as u16))), - I32x4(l) => v128_value("i32", v128_lanes(&l.map(|x| x as u32))), - I64x2(l) => v128_value("i64", v128_lanes(&l.map(|x| x as u64))), - F32x4(l) => v128_value( - "f32", - l.map(|x| nan_pattern_string(x, |f| f.bits.to_string())) - .to_vec() - .into(), - ), - F64x2(l) => v128_value( - "f64", - l.map(|x| nan_pattern_string(x, |f| f.bits.to_string())) - .to_vec() - .into(), - ), - } -} - -/// Component-level value encoding; see harness/README.md. -fn component_value(v: WastVal<'_>) -> Result { - Ok(match v { - WastVal::Bool(b) => Value::simple("bool", b.to_string()), - WastVal::U8(v) => Value::simple("u8", v.to_string()), - WastVal::S8(v) => Value::simple("s8", v.to_string()), - WastVal::U16(v) => Value::simple("u16", v.to_string()), - WastVal::S16(v) => Value::simple("s16", v.to_string()), - WastVal::U32(v) => Value::simple("u32", v.to_string()), - WastVal::S32(v) => Value::simple("s32", v.to_string()), - WastVal::U64(v) => Value::simple("u64", v.to_string()), - WastVal::S64(v) => Value::simple("s64", v.to_string()), - WastVal::F32(f) => Value::simple("f32", f.bits.to_string()), - WastVal::F64(f) => Value::simple("f64", f.bits.to_string()), - WastVal::Char(c) => Value::simple("char", c.to_string()), - WastVal::String(s) => Value::simple("string", s.to_string()), - WastVal::List(items) => Value::simple("list", nested(items)?), - WastVal::Tuple(items) => Value::simple("tuple", nested(items)?), - WastVal::Record(fields) => { - let fields = fields - .into_iter() - .map(|(name, v)| { - Ok(serde_json::json!({ - "name": name, - "value": serde_json::to_value(component_value(v)?)?, - })) - }) - .collect::>>()?; - Value::simple("record", fields) - } - WastVal::Variant(case, payload) => Value { - ty: "variant".to_string(), - lane_type: None, - case: Some(case.to_string()), - status: None, - value: optional_nested(payload)?, - }, - WastVal::Enum(case) => Value::simple("enum", case.to_string()), - WastVal::Option(payload) => Value::simple("option", optional_nested(payload)?), - WastVal::Result(r) => { - let (status, payload) = match r { - Ok(p) => ("ok", p), - Err(p) => ("err", p), - }; - Value { - ty: "result".to_string(), - lane_type: None, - case: None, - status: Some(status.to_string()), - value: optional_nested(payload)?, - } - } - WastVal::Flags(names) => Value::simple( - "flags", - names.iter().map(|n| n.to_string()).collect::>(), - ), - }) -} - -fn nested(items: Vec>) -> Result { - let vals = items - .into_iter() - .map(component_value) - .collect::>>()?; - Ok(serde_json::to_value(vals)?) -} - -fn optional_nested(payload: Option>>) -> Result { - match payload { - Some(v) => Ok(serde_json::to_value(component_value(*v)?)?), - None => Ok(serde_json::Value::Null), - } -} diff --git a/crates/testgen/src/json.rs b/crates/testgen/src/json.rs deleted file mode 100644 index c7513e4c..00000000 --- a/crates/testgen/src/json.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! JSON schema for converted `.wast` files. -//! -//! The shape deliberately mirrors `wasm-tools json-from-wast` (which itself -//! mirrors WABT's `wast2json`, see -//! ), with -//! two extensions: -//! -//! - every command that references an on-disk artifact carries a `kind` -//! field: `"module"` (core wasm) or `"component"`, so consumers never have -//! to sniff binary layer preambles; -//! - component-level values (`WastVal`) have a documented encoding covering -//! compound types (list/record/tuple/variant/enum/option/result/flags). -//! -//! See harness/README.md for the full schema documentation. - -use serde::Serialize; - -#[derive(Serialize)] -pub struct WastJson { - pub source_filename: String, - pub commands: Vec, -} - -/// Which layer an extracted artifact belongs to. -#[derive(Serialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum Kind { - /// A core wasm module (binary version 0x1, layer absent). - Module, - /// A component (binary layer 0x1). - Component, -} - -/// Whether the artifact file is a binary (`.wasm`) or raw text (`.wat`). -/// -/// Text artifacts only occur for `(... quote "...")` forms whose text cannot -/// be (or, for `assert_malformed`, must not be) converted to a binary. -#[derive(Serialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ModuleType { - Binary, - Text, -} - -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Command { - /// Define, validate, and instantiate a top-level module/component. Its - /// exports become the default target for subsequent actions. - Module { - line: usize, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - filename: String, - module_type: ModuleType, - kind: Kind, - }, - /// `(component definition ...)` / `(module definition ...)`: define and - /// validate only; instantiated later by `module_instance`. - ModuleDefinition { - line: usize, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - filename: String, - module_type: ModuleType, - kind: Kind, - }, - /// `(component instance $i $M)`: instantiate a prior `module_definition` - /// (`module == None` means the most recent definition). The new instance - /// becomes the default action target. - ModuleInstance { - line: usize, - #[serde(skip_serializing_if = "Option::is_none")] - instance: Option, - #[serde(skip_serializing_if = "Option::is_none")] - module: Option, - }, - /// Make an instance's exports importable by later modules under `as`. - Register { - line: usize, - #[serde(rename = "as")] - as_: String, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - }, - /// Perform an action, ignore results (must not trap). - Action { line: usize, action: Action }, - AssertReturn { - line: usize, - action: Action, - expected: Vec, - }, - AssertTrap { - line: usize, - action: Action, - text: String, - }, - AssertExhaustion { - line: usize, - action: Action, - text: String, - }, - AssertException { line: usize, action: Action }, - AssertSuspension { - line: usize, - action: Action, - text: String, - }, - /// Module/component must fail *validation* with an error matching `text`. - AssertInvalid { - line: usize, - filename: String, - module_type: ModuleType, - kind: Kind, - text: String, - }, - /// Module/component must fail *decoding* (binary) or *parsing* (text). - AssertMalformed { - line: usize, - filename: String, - module_type: ModuleType, - kind: Kind, - text: String, - }, - /// Module/component validates but must trap during instantiation. - /// (Produced from `(assert_trap (component ...) "...")`.) - AssertUninstantiable { - line: usize, - filename: String, - module_type: ModuleType, - kind: Kind, - text: String, - }, - /// Module/component validates but must fail to link. - AssertUnlinkable { - line: usize, - filename: String, - module_type: ModuleType, - kind: Kind, - text: String, - }, -} - -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Action { - Invoke { - /// Named instance to invoke on; `None` = current default instance. - #[serde(skip_serializing_if = "Option::is_none")] - module: Option, - field: String, - args: Vec, - }, - Get { - #[serde(skip_serializing_if = "Option::is_none")] - module: Option, - field: String, - }, -} - -/// A core or component value. `type` discriminates; the payload fields are -/// documented in harness/README.md. Scalars are decimal strings (floats are -/// *bit patterns*); compound component values nest `Value`s. -#[derive(Serialize)] -pub struct Value { - #[serde(rename = "type")] - pub ty: String, - /// v128 only: the lane interpretation used in the source text. - #[serde(skip_serializing_if = "Option::is_none")] - pub lane_type: Option, - /// variant only: case name. - #[serde(skip_serializing_if = "Option::is_none")] - pub case: Option, - /// result only: "ok" | "err". - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - pub value: serde_json::Value, -} - -impl Value { - pub fn simple(ty: impl Into, value: impl Into) -> Value { - Value { - ty: ty.into(), - lane_type: None, - case: None, - status: None, - value: value.into(), - } - } -} diff --git a/crates/testgen/src/main.rs b/crates/testgen/src/main.rs index 525ddde7..3e28db0d 100644 --- a/crates/testgen/src/main.rs +++ b/crates/testgen/src/main.rs @@ -1,7 +1,7 @@ //! testgen: convert the official Component Model `.wast` test suite into -//! JSON command files + extracted `.wasm`/`.wat` artifacts, following the -//! core-spec `wast2json` model. See harness/README.md for the pipeline and -//! schema documentation. +//! JSON command files + extracted `.wasm`/`.wat` artifacts, by driving +//! `wast` and `json-from-wast` (the `wasm-tools json-from-wast` implementation) +//! as libraries. See harness/README.md for the pipeline and schema documentation. //! //! Usage: //! testgen [--test-dir DIR] [--out-dir DIR] [SUBDIR...] @@ -14,9 +14,6 @@ //! SUBDIR arguments (e.g. `binary validation`) restrict conversion to those //! test suite subdirectories; the default is everything. -mod convert; -mod json; - use anyhow::{bail, Context, Result}; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -123,24 +120,40 @@ fn run() -> Result { let text = std::fs::read_to_string(&wast_path) .with_context(|| format!("reading {}", wast_path.display()))?; - match convert::convert_wast(&source_rel, &stem, &text) { - Ok(out) => { - let n_artifacts = out.artifacts.len(); - for (filename, bytes) in out.artifacts { - std::fs::write(out_sub.join(&filename), bytes) + let mut lexer = wast::lexer::Lexer::new(&text); + lexer.allow_confusing_unicode(true); + let buf = match wast::parser::ParseBuffer::new_with_lexer(lexer) { + Ok(buf) => buf, + Err(e) => { + failures.push((wast_path, pretty(e, &source_rel, &text))); + continue; + } + }; + let ast: wast::Wast = match wast::parser::parse(&buf) { + Ok(ast) => ast, + Err(e) => { + failures.push((wast_path, pretty(e, &source_rel, &text))); + continue; + } + }; + match json_from_wast::Opts::default().convert(&source_rel, &text, ast) { + Ok(wast) => { + let n_artifacts = wast.wasms.len(); + for (filename, bytes) in &wast.wasms { + std::fs::write(out_sub.join(filename), bytes) .with_context(|| format!("writing {sub}/{filename}"))?; } - let mut json = serde_json::to_string_pretty(&out.json)?; + let mut json = serde_json::to_string_pretty(&wast)?; json.push('\n'); std::fs::write(out_sub.join(format!("{stem}.json")), json) .with_context(|| format!("writing {sub}/{stem}.json"))?; println!( "converted {source_rel}: {} commands, {} artifacts", - out.json.commands.len(), + wast.commands.len(), n_artifacts ); converted += 1; - total_commands += out.json.commands.len(); + total_commands += wast.commands.len(); } Err(e) => failures.push((wast_path, e)), } @@ -175,3 +188,9 @@ fn run() -> Result { } Ok(failures.len()) } + +fn pretty(mut e: wast::Error, path: &str, text: &str) -> anyhow::Error { + e.set_path(std::path::Path::new(path)); + e.set_text(text); + anyhow::anyhow!("{e}") +} diff --git a/docs/architecture.md b/docs/architecture.md index d0ca900c..068dc1ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -618,10 +618,11 @@ There is no single official conformance suite; the corpus is assembled: | experiment-mosh gates + minimized repros (`compose-async-tdz`) | composed 3-component client: mixed sync/async exports, background pumps, resources re-exported across interfaces, componentize-go guest | strongest known real-workload exercisers — this family surfaced ≥5 distinct jco defect classes no WAST corpus expresses (`tools/smoke-c0/`) | Harness pipeline: an offline Rust step (`crates/testgen`) converts `.wast` -into JSON commands + `.wasm` binaries — the core-spec `wast2json` model. It -uses the `wast` crate directly (the pinned wasm-tools CLI's bundled parser -predates current suite syntax; owning the emitter also lets us tag every -artifact `core` vs `component`, which the harness needs since V8 cannot even +into JSON commands + `.wasm` binaries by driving the `wast` and +`json-from-wast` crates (the `wasm-tools json-from-wast` implementation) as +libraries — the same conversion wasmtime's own wast runner performs +in-process; the schema is upstream's, unmodified. The harness classifies +each binary as core module vs component from its preamble (V8 cannot even validate component binaries). The TS harness executes the JSON identically under `deno test` and in browsers (`tools/browser/run-lane.ts`: static server + automated Chromium / Firefox-with-pref / WebKit, with per-lane diff --git a/docs/references.md b/docs/references.md index 015c7ace..a037c149 100644 --- a/docs/references.md +++ b/docs/references.md @@ -55,6 +55,8 @@ the dependency. - wasm-tools repo (CLI + crates): https://github.com/bytecodealliance/wasm-tools - `wast` crate (component-aware wast parsing, used by testgen): https://docs.rs/wast/ +- `json-from-wast` crate (wast → JSON commands + binaries, used by testgen; + the `wasm-tools json-from-wast` implementation): https://docs.rs/json-from-wast/ - `wasmparser` (0.258.x — must match wasmtime-environ): https://docs.rs/wasmparser/ - `wasm-encoder`: https://docs.rs/wasm-encoder/ - `wit-parser` (bindgen input): https://docs.rs/wit-parser/ diff --git a/harness/README.md b/harness/README.md index 4c2f4d30..93881d4e 100644 --- a/harness/README.md +++ b/harness/README.md @@ -31,18 +31,6 @@ Output is deterministic: same suite + same testgen build → byte-identical `harness/generated/` (sorted traversal, stable JSON field order, no timestamps or absolute paths). -## Why our own converter instead of `wasm-tools json-from-wast` - -`json-from-wast` (wasm-tools 1.247.0) *does* model component directives in -its JSON — the schema below is essentially its schema — but its bundled -`wast` parser predates the current test suite's text syntax -(`(dtor (core func ...))`, `async`/`(callback ...)` canon-lift options, -`(implements ...)` import attributes): 40 of 57 suite files fail to parse -with that CLI. testgen therefore uses the `wast` crate directly (pinned in -`Cargo.lock`, currently 255.0.0), which parses the entire suite, and emits -the same JSON model with two extensions noted below. This is the exact -fallback docs/architecture.md §11 anticipated. - ## Generated layout ``` @@ -50,76 +38,37 @@ harness/generated/ manifest.json # {"files": ["async/cancel-stream.json", ...]} /.json # command file, one per .wast /..wasm # extracted binary (module or component) - /..wat # only for quote forms kept as text + /..wat # quote forms kept as text ``` ## JSON schema -Follows WABT's `wast2json` model -() as -emitted by `wasm-tools json-from-wast`. A file is -`{source_filename, commands: [...]}`; each command has a `type` and the -1-based source `line`. TypeScript definitions: `src/schema.ts`. - -Command types and their fields: - -| type | fields | meaning | -|---|---|---| -| `module` | `name?, filename, module_type, kind` | define+validate+instantiate; becomes default action target | -| `module_definition` | `name?, filename, module_type, kind` | define+validate only (`(component definition ...)`) | -| `module_instance` | `instance?, module?` | instantiate a prior definition (`module` absent = most recent); becomes default target | -| `register` | `as, name?` | make instance importable (unused by this suite) | -| `action` | `action` | run action, ignore results | -| `assert_return` | `action, expected` | action returns `expected` values | -| `assert_trap` | `action, text` | action traps, message matches `text` | -| `assert_invalid` | `filename, module_type, kind, text` | binary decodes but fails validation | -| `assert_malformed` | `filename, module_type, kind, text` | binary fails decoding (or text fails parsing) | -| `assert_uninstantiable` | `filename, module_type, kind, text` | valid, but instantiation traps (from `(assert_trap (component ...))`) | -| `assert_unlinkable` | `filename, module_type, kind, text` | valid, but linking fails (unused by this suite) | -| `assert_exhaustion` / `assert_exception` / `assert_suspension` | `action, text?` | core-suite directives, unused here | - -Actions: `{"type":"invoke", "module"?, "field", "args":[Value]}` and -`{"type":"get", "module"?, "field"}`. `module` names a **named instance**; -absent means the current default instance (wast2json convention). - -Extensions over `json-from-wast`: - -1. **`kind`: `"module" | "component"`** on every artifact-bearing command, so - consumers never sniff the 8-byte preamble to tell layers apart. (In the - official suite every top-level artifact is a component; core modules only - appear nested inside them.) -2. **Documented component value encoding** (below); `json-from-wast`'s - encoding for compound values is unspecified. - -### Values - -Every value is `{"type": ..., "value": ...}` plus type-specific fields. -Scalars encode as decimal **strings**; floats are IEEE754 **bit patterns** -(f32→u32, f64→u64), with `nan:canonical` / `nan:arithmetic` allowed in -expectations; core `i32`/`i64` are printed unsigned (wast2json convention), -component `s8..s64` are signed. - -| type | encoding | -|---|---| -| `bool` | `"true"` / `"false"` | -| `u8 s8 u16 s16 u32 s32 u64 s64 i32 i64 f32 f64` | decimal string | -| `char` | the single Unicode scalar value as a string | -| `string` | the string | -| `list`, `tuple` | `value` = array of Values | -| `record` | `value` = array of `{name, value: Value}` | -| `variant` | `case` = case name, `value` = payload Value or `null` | -| `enum` | `value` = case name | -| `option` | `value` = payload Value or `null` (= none) | -| `result` | `status` = `"ok"`/`"err"`, `value` = payload Value or `null` | -| `flags` | `value` = array of set flag names | -| `v128` | `lane_type` set, `value` = array of lane strings (core-suite only) | - -### Text artifacts - -`(component quote "...")` forms whose malformedness lives at the text level -are written as `.wat` with `module_type: "text"` (5 in the current suite, -all `assert_malformed`). They are only executable by a host with a text -parser; the runner records them as skip(`unsupported-directive`). +testgen drives the `wast` parser and the `json-from-wast` crate (the +implementation of `wasm-tools json-from-wast`, itself the component-aware +successor of WABT's `wast2json`) as libraries, so the schema is exactly +upstream's: the serde derives in `json-from-wast`'s `src/lib.rs` +(`Wast`, `Command`, `WasmFile`, `Action`, `Const`) are the specification; +`src/schema.ts` mirrors the subset this suite exercises. Points worth knowing +when reading the files or writing xfail entries: + +- `line` is 1-based and, for the module- and action-bearing asserts + (`assert_invalid/malformed/unlinkable/uninstantiable/trap/return`), is the + line of the *inner form* (`(component ...)` / `(invoke ...)`), not the + `(assert_...` line. It is the key used by `src/xfail.ts` and the lane + expectation overlays. +- Artifacts are `filename` + `module_type: "binary" | "text"`; nothing in + the JSON says whether a binary is a core module or a component. The runner + classifies from the preamble (`artifactKind` in `src/runner.ts`: exact + core preamble → module, anything else → component; in this suite every + top-level artifact is a component). +- Values are `{"type": ..., "value": ...}`. Scalars are decimal strings + (floats as IEEE754 bit patterns; core-value expectations may also be + `nan:canonical` / `nan:arithmetic`), `bool` is a JSON boolean, `record` is `[name, value][]`, + `variant` is `{case, payload?}`, `result` is `{Ok: v|null}` / + `{Err: v|null}`, `option` is `v | null`, `flags` is `string[]`. +- `(component quote "...")` forms are written as `.wat` with + `module_type: "text"` (5 in the current suite, all `assert_malformed`); + the runner records them as skip(`unsupported-directive`). ## Executor contract (provisional) diff --git a/harness/browser/expectations/webkit.ts b/harness/browser/expectations/webkit.ts index d5c8b8c1..fe965240 100644 --- a/harness/browser/expectations/webkit.ts +++ b/harness/browser/expectations/webkit.ts @@ -89,14 +89,14 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 827, + line: 828, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 836, + line: 837, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -110,49 +110,49 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 844, + line: 845, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 856, + line: 857, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 863, + line: 864, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 873, + line: 874, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 884, + line: 885, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 896, + line: 897, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 906, + line: 907, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -166,98 +166,98 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 914, + line: 915, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 934, + line: 935, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 946, + line: 947, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 964, + line: 965, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1024, + line: 1025, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1058, + line: 1059, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1104, + line: 1105, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1132, + line: 1133, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1160, + line: 1161, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1206, + line: 1207, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1256, + line: 1257, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1288, + line: 1289, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1344, + line: 1345, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1392, + line: 1393, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -271,7 +271,7 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1407, + line: 1408, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -285,14 +285,14 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1417, + line: 1418, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1427, + line: 1428, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -306,7 +306,7 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1438, + line: 1439, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -320,84 +320,84 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1448, + line: 1449, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1457, + line: 1458, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1469, + line: 1470, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1481, + line: 1482, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1491, + line: 1492, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1504, + line: 1505, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1520, + line: 1521, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1533, + line: 1534, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1544, + line: 1545, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1555, + line: 1556, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1568, + line: 1569, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1584, + line: 1585, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -411,7 +411,7 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1594, + line: 1595, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -425,28 +425,28 @@ export const webkit: LaneExpectation = { }, { file: "async/big-interleaving-test.json", - line: 1603, + line: 1604, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1614, + line: 1615, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1633, + line: 1634, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "async/big-interleaving-test.json", - line: 1644, + line: 1645, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -1118,7 +1118,7 @@ export const webkit: LaneExpectation = { }, { file: "values/concat.json", - line: 722, + line: 723, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", @@ -1132,42 +1132,42 @@ export const webkit: LaneExpectation = { }, { file: "values/concat.json", - line: 731, + line: 732, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "values/concat.json", - line: 740, + line: 741, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "values/concat.json", - line: 747, + line: 748, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "values/concat.json", - line: 754, + line: 755, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "values/concat.json", - line: 761, + line: 762, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", }, { file: "values/concat.json", - line: 769, + line: 770, kind: "expected-fail", reason: "CASCADE: an earlier command in this same file failed, leaving the component definition / instance state wrong for every later command. Root cause = the first non-CASCADE delta listed above it in this file.", diff --git a/harness/src/executor.ts b/harness/src/executor.ts index 1ddeb6d6..6ec429d2 100644 --- a/harness/src/executor.ts +++ b/harness/src/executor.ts @@ -12,7 +12,8 @@ import type { Kind, ModuleType, Value } from "./schema.ts"; -/** An artifact extracted by testgen, loaded into memory. */ +/** An artifact extracted by testgen, loaded into memory (`kind` is derived + * by the runner at load time via `artifactKind`, not carried in the JSON). */ export interface Artifact { filename: string; kind: Kind; diff --git a/harness/src/runner.ts b/harness/src/runner.ts index 5ea3f809..5b534bd0 100644 --- a/harness/src/runner.ts +++ b/harness/src/runner.ts @@ -5,6 +5,7 @@ import type { Action, ArtifactRef, Command, + Kind, WastJson, } from "./schema.ts"; import { @@ -18,6 +19,26 @@ import { } from "./executor.ts"; import { compareValues as compareComponentValues } from "./value-mapping.ts"; +/** + * Sniffs the binary preamble to classify an artifact — upstream's JSON + * carries no `kind` field, only `filename`/`module_type`/`binary_filename` + * (json-from-wast's `WasmFile`). Only the unambiguous core-module preamble + * (`\0asm` + version `01 00 00 00`) returns "module"; everything else, + * including bytes that are not a valid preamble at all, is handed to the + * component pipeline — the strict one, which rejects garbage, and also + * where the suite's assert_malformed `(component binary ...)` cases belong. + * Residual: a `(component binary ...)` whose bytes happen to be a valid core + * preamble would be misclassified as a module and fail visibly (the + * component pipeline never even sees it) — the suite has no such case. + */ +export function artifactKind(bytes: Uint8Array): Kind { + const isModule = bytes.length >= 8 && + bytes[0] === 0x00 && bytes[1] === 0x61 && bytes[2] === 0x73 && + bytes[3] === 0x6d && + bytes[4] === 0x01 && bytes[5] === 0x00 && bytes[6] === 0x00 && bytes[7] === 0x00; + return isModule ? "module" : "component"; +} + /** * `pending-capability` is a precise, named-in-report subset of * `pending-runtime`: the executor understood the command but a specific @@ -242,11 +263,12 @@ class FileRunner { // level; executable only by a host with a text parser. throw new UnsupportedDirective(`text artifact ${ref.filename}`); } + const bytes = await this.loadArtifact(ref.filename); return { filename: ref.filename, - kind: ref.kind, + kind: artifactKind(bytes), moduleType: ref.module_type, - bytes: await this.loadArtifact(ref.filename), + bytes, }; } diff --git a/harness/src/schema.ts b/harness/src/schema.ts index a5ea0d7d..ee206290 100644 --- a/harness/src/schema.ts +++ b/harness/src/schema.ts @@ -1,8 +1,12 @@ // JSON schema for testgen-generated command files (see harness/README.md). -// Mirrors `wasm-tools json-from-wast` / wast2json, with a `kind` extension -// distinguishing core modules from components. - -/** Layer of an extracted artifact. */ +// Mirrors the subset of upstream `json-from-wast`'s schema (the +// `wasm-tools json-from-wast` subcommand) this suite exercises — no local +// extensions. Authority: `json-from-wast-0.258.0/src/lib.rs` +// (Command/WasmFile/Action/CoreConst/ComponentConst serde derives). + +/** Layer of an extracted artifact, sniffed from the binary preamble at load + * time (upstream's JSON carries no `kind` field — see `runner.ts` + * `artifactKind`). */ export type Kind = "module" | "component"; /** Artifact file flavor: `.wasm` binary or raw `.wat` text. */ @@ -13,10 +17,13 @@ export interface WastJson { commands: Command[]; } +/** `WasmFile` (lib.rs): `#[serde(flatten)]`ed into artifact-bearing + * commands. `binary_filename` is only present for quote forms that also + * encode (never for `assert_malformed` quote forms). */ export interface ArtifactRef { filename: string; module_type: ModuleType; - kind: Kind; + binary_filename?: string; } export interface ModuleCommand extends ArtifactRef { @@ -143,35 +150,33 @@ export interface GetAction { export type Action = InvokeAction | GetAction; /** - * A core or component value. - * - * - scalars (`bool,u8..s64,i32,i64,f32,f64,char,string,enum`): `value` is a - * string; floats are decimal *bit patterns* (or `nan:canonical` / - * `nan:arithmetic` in expectations); - * - `list`/`tuple`: `value` is `Value[]`; - * - `record`: `value` is `{name, value}[]`; - * - `variant`: `case` is set, `value` is payload `Value` or null; - * - `option`: `value` is payload `Value` or null (none); - * - `result`: `status` is "ok"|"err", `value` is payload `Value` or null; - * - `flags`: `value` is `string[]`; - * - `v128`: `lane_type` is set, `value` is `string[]` of lanes. + * A component-model value (`ComponentConst`, `#[serde(tag="type", + * content="value")]`, lib.rs). This is the only `Const` variant this harness + * executes today (see `CoreValue` below for the executed subset of the + * core-wasm side of the untagged `Const` union). */ -export interface Value { - type: string; - lane_type?: string; - case?: string; - status?: "ok" | "err"; - value: ValuePayload; -} +export type ComponentValue = + | { type: "bool"; value: boolean } + | { type: "u8" | "s8" | "u16" | "s16" | "u32" | "s32" | "u64" | "s64"; value: string } + | { type: "f32" | "f64"; value: string } + | { type: "char" | "string" | "enum"; value: string } + | { type: "list" | "tuple"; value: Value[] } + | { type: "record"; value: [name: string, value: Value][] } + | { type: "variant"; value: { case: string; payload?: Value } } + | { type: "option"; value: Value | null } + | { type: "result"; value: { Ok: Value | null } | { Err: Value | null } } + | { type: "flags"; value: string[] }; -export type ValuePayload = - | string - | null - | string[] - | Value[] - | RecordField[]; +/** + * The executed subset of `CoreConst` (`#[serde(tag="type", + * rename_all="lowercase")]`, lib.rs): plain numeric/float scalars. The + * harness does not execute core-wasm actions with ref-typed + * (funcref/externref/anyref/v128/...) arguments or results — keep parity + * with today's supported set, do not expand without a corpus need. + */ +export type CoreValue = + | { type: "i32" | "i64"; value: string } + | { type: "f32" | "f64"; value: string }; -export interface RecordField { - name: string; - value: Value; -} +/** Untagged `Const` union (lib.rs): `Core(CoreConst) | Component(ComponentConst)`. */ +export type Value = CoreValue | ComponentValue; diff --git a/harness/src/value-mapping.ts b/harness/src/value-mapping.ts index 19d2d197..9cb15063 100644 --- a/harness/src/value-mapping.ts +++ b/harness/src/value-mapping.ts @@ -1,13 +1,17 @@ -// Bidirectional mapping between wast-JSON `Value` (harness/src/schema.ts — -// scalars as decimal strings, floats as bit patterns) and the runtime's -// `ComponentValue` (runtime/src/cabi/types.ts — definitions.py's semantics in -// our own representation, contracts/descriptor-ir.md §"Host value shapes": -// variant/enum/option/result as `{kind: label, value: payload}` objects with -// despecialized labels `none`/`some`/`ok`/`error`, tuple as despecialized -// record `{"0": v, ...}`, flags as `{label: boolean}`). +// Bidirectional mapping between wast-JSON `Value` (harness/src/schema.ts, +// mirroring upstream `json-from-wast`'s `ComponentConst`/`CoreConst` — +// scalars as decimal strings, floats as bit patterns, bool as a JSON +// boolean, record as `[name, Value][]`, variant as `{case, payload?}`, +// option as `Value | null`, result as `{Ok: Value|null} | {Err: Value|null}`) +// and the runtime's `ComponentValue` (runtime/src/cabi/types.ts — +// definitions.py's semantics in our own representation, +// contracts/descriptor-ir.md §"Host value shapes": variant/enum/option/result +// as `{kind: label, value: payload}` objects with despecialized labels +// `none`/`some`/`ok`/`error`, tuple as despecialized record `{"0": v, ...}`, +// flags as `{label: boolean}`). // -// Every wast-JSON `Value` is self-describing (`type`/`case`/`status` fields -// carry the type), so converting an *argument* list needs no separate +// Every wast-JSON `Value` is self-describing (the `type` field carries the +// tag — nested inside `value` for compound cases, e.g. `variant`'s `case`), so converting an *argument* list needs no separate // FuncType — the value's own tag says how to build the ComponentValue. // Comparing an *actual* ComponentValue against an `assert_return` expected // value works the same way: the expected value's tag drives the comparison @@ -17,7 +21,7 @@ // ever seeing a `FuncType` itself (see `runtime/src/exec/boundary.ts` // `createLiftedFunction`, which resolves it internally). -import type { RecordField, Value } from "./schema.ts"; +import type { Value } from "./schema.ts"; const scratch = new DataView(new ArrayBuffer(8)); @@ -66,7 +70,7 @@ function isArithmeticNan64(bits: bigint): boolean { export function toComponentValue(v: Value): any { switch (v.type) { case "bool": - return v.value === "true"; + return v.value; case "u8": case "u16": case "u32": @@ -74,23 +78,23 @@ export function toComponentValue(v: Value): any { case "s16": case "s32": case "i32": - return Number(v.value as string); + return Number(v.value); case "u64": case "s64": case "i64": - return BigInt(v.value as string); + return BigInt(v.value); case "f32": - return f32FromBits(BigInt(v.value as string)); + return f32FromBits(BigInt(v.value)); case "f64": - return f64FromBits(BigInt(v.value as string)); + return f64FromBits(BigInt(v.value)); case "char": case "string": - return v.value as string; + return v.value; case "enum": - return { kind: v.value as string, value: null }; + return { kind: v.value, value: null }; case "list": case "tuple": { - const items = (v.value as Value[]).map(toComponentValue); + const items = v.value.map(toComponentValue); if (v.type === "tuple") { const record: Record = {}; items.forEach((it, i) => record[String(i)] = it); @@ -100,37 +104,45 @@ export function toComponentValue(v: Value): any { } case "record": { const record: Record = {}; - for (const f of v.value as RecordField[]) { - record[f.name] = toComponentValue(f.value); + for (const [name, value] of v.value) { + record[name] = toComponentValue(value); } return record; } case "variant": { - const payload = v.value === null + const payload = v.value.payload === undefined ? null - : toComponentValue(v.value as unknown as Value); - return { kind: v.case as string, value: payload }; + : toComponentValue(v.value.payload); + return { kind: v.value.case, value: payload }; } case "option": return v.value === null ? { kind: "none", value: null } - : { kind: "some", value: toComponentValue(v.value as unknown as Value) }; + : { kind: "some", value: toComponentValue(v.value) }; case "result": { - const payload = v.value === null - ? null - : toComponentValue(v.value as unknown as Value); // Internal spelling of the error case is "error", not "err" // (contracts/descriptor-ir.md §"Host value shapes"). - return { kind: v.status === "ok" ? "ok" : "error", value: payload }; + if ("Ok" in v.value) { + return { + kind: "ok", + value: v.value.Ok === null ? null : toComponentValue(v.value.Ok), + }; + } + return { + kind: "error", + value: v.value.Err === null ? null : toComponentValue(v.value.Err), + }; } case "flags": { - const set = new Set(v.value as string[]); + const set = new Set(v.value); const record: Record = {}; for (const label of set) record[label] = true; return record; } default: - throw new Error(`toComponentValue: unsupported value type '${v.type}'`); + throw new Error( + `toComponentValue: unsupported value type '${(v as { type: string }).type}'`, + ); } } @@ -148,7 +160,7 @@ export function compareValue( const where = path || ""; switch (expected.type) { case "bool": { - const want = expected.value === "true"; + const want = expected.value; return actual === want ? undefined : `${where}: expected bool ${want}, got ${JSON.stringify(actual)}`; @@ -160,7 +172,7 @@ export function compareValue( case "s16": case "s32": case "i32": { - const want = Number(expected.value as string); + const want = Number(expected.value); return actual === want ? undefined : `${where}: expected ${expected.type} ${want}, got ${ @@ -170,7 +182,7 @@ export function compareValue( case "u64": case "s64": case "i64": { - const want = BigInt(expected.value as string); + const want = BigInt(expected.value); return actual === want ? undefined : `${where}: expected ${expected.type} ${want}, got ${ @@ -182,7 +194,7 @@ export function compareValue( return `${where}: expected f32 number, got ${typeof actual}`; } const bits = f32ToBits(actual); - const raw = expected.value as string; + const raw = expected.value; if (raw === "nan:canonical") { return isCanonicalNan32(bits) ? undefined @@ -209,7 +221,7 @@ export function compareValue( return `${where}: expected f64 number, got ${typeof actual}`; } const bits = f64ToBits(actual); - const raw = expected.value as string; + const raw = expected.value; if (raw === "nan:canonical") { return isCanonicalNan64(bits) ? undefined @@ -249,7 +261,7 @@ export function compareValue( : `${where}: expected enum '${expected.value}', got '${label}'`; } case "list": { - const items = expected.value as Value[]; + const items = expected.value; // list lifts as a Uint8Array (docs/architecture.md §7); accept both. const arr = actual instanceof Uint8Array ? Array.from(actual) @@ -267,7 +279,7 @@ export function compareValue( return undefined; } case "tuple": { - const items = expected.value as Value[]; + const items = expected.value; if (typeof actual !== "object" || actual === null) { return `${where}: expected tuple record, got ${JSON.stringify(actual)}`; } @@ -279,13 +291,13 @@ export function compareValue( return undefined; } case "record": { - const fields = expected.value as RecordField[]; + const fields = expected.value; if (typeof actual !== "object" || actual === null) { return `${where}: expected record object, got ${JSON.stringify(actual)}`; } const rec = actual as Record; - for (const f of fields) { - const m = compareValue(f.value, rec[f.name], `${where}.${f.name}`); + for (const [name, value] of fields) { + const m = compareValue(value, rec[name], `${where}.${name}`); if (m !== undefined) return m; } return undefined; @@ -301,12 +313,12 @@ export function compareValue( JSON.stringify(actual) }`; } - if (label !== expected.case) { - return `${where}: expected variant case '${expected.case}', got '${label}'`; + if (label !== expected.value.case) { + return `${where}: expected variant case '${expected.value.case}', got '${label}'`; } - if (expected.value === null) return undefined; + if (expected.value.payload === undefined) return undefined; return compareValue( - expected.value as unknown as Value, + expected.value.payload, rec.value, `${where}.${label}`, ); @@ -324,32 +336,32 @@ export function compareValue( if (rec.kind !== "some") { return `${where}: expected some(...), got ${JSON.stringify(actual)}`; } - return compareValue(expected.value as unknown as Value, rec.value, `${where}.some`); + return compareValue(expected.value, rec.value, `${where}.some`); } case "result": { if (typeof actual !== "object" || actual === null) { return `${where}: expected result object, got ${JSON.stringify(actual)}`; } const rec = actual as Record; - if (expected.status === "ok") { + if ("Ok" in expected.value) { if (rec.kind !== "ok") { return `${where}: expected ok(...), got ${JSON.stringify(actual)}`; } - if (expected.value === null) return undefined; - return compareValue(expected.value as unknown as Value, rec.value, `${where}.ok`); + if (expected.value.Ok === null) return undefined; + return compareValue(expected.value.Ok, rec.value, `${where}.ok`); } if (rec.kind !== "error") { return `${where}: expected error(...), got ${JSON.stringify(actual)}`; } - if (expected.value === null) return undefined; + if (expected.value.Err === null) return undefined; return compareValue( - expected.value as unknown as Value, + expected.value.Err, rec.value, `${where}.error`, ); } case "flags": { - const want = new Set(expected.value as string[]); + const want = new Set(expected.value); if (typeof actual !== "object" || actual === null) { return `${where}: expected flags object, got ${JSON.stringify(actual)}`; } @@ -365,7 +377,9 @@ export function compareValue( return undefined; } default: - return `${where}: unsupported expected value type '${expected.type}'`; + return `${where}: unsupported expected value type '${ + (expected as { type: string }).type + }'`; } } diff --git a/harness/src/xfail.ts b/harness/src/xfail.ts index f8da6202..c13902c1 100644 --- a/harness/src/xfail.ts +++ b/harness/src/xfail.ts @@ -36,7 +36,7 @@ export const XFAIL: XfailEntry[] = [ // upstream-component-model-repo-findings.md (#246/#247). --- { file: "validation/kebab.json", - line: 149, + line: 150, reason: 'expected assert_invalid ("import name `foobar` conflicts with ' + 'previous name `foo-bar`"), but it validated — wasmtime does not ' + @@ -47,35 +47,35 @@ export const XFAIL: XfailEntry[] = [ }, { file: "validation/kebab.json", - line: 154, + line: 155, reason: 'expected assert_invalid ("import name `FOOBAR` conflicts with ' + 'previous name `foo-bar`"), but it validated — same name-rules-nyi ' + - "gap as line 149, https://github.com/polymorph-components/polyengine/issues/248", + "gap as line 150, https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/kebab.json", - line: 159, + line: 160, reason: 'expected assert_invalid ("import name `foob-ar` conflicts with ' + 'previous name `foo-bar`"), but it validated — same name-rules-nyi ' + - "gap as line 149, https://github.com/polymorph-components/polyengine/issues/248", + "gap as line 150, https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/kebab.json", - line: 164, + line: 165, reason: 'expected assert_invalid ("import name `[static]foo-bar.FO-ob-AR` ' + 'conflicts with previous name `foo-bar`"), but it validated — same ' + - "name-rules-nyi gap as line 149, https://github.com/polymorph-components/polyengine/issues/248", + "name-rules-nyi gap as line 150, https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/kebab.json", - line: 169, + line: 170, reason: 'expected assert_invalid ("import name `[method]foo-bar.foobar` ' + 'conflicts with previous name `foo-bar`"), but it validated — same ' + - "name-rules-nyi gap as line 149, https://github.com/polymorph-components/polyengine/issues/248", + "name-rules-nyi gap as line 150, https://github.com/polymorph-components/polyengine/issues/248", }, // --- validation/max-value-size.json: CM#688 ("max-value-size", pulled in // by the CM#705 pin advance polyengine#173) added the elem_size(t, i64) < @@ -92,7 +92,7 @@ export const XFAIL: XfailEntry[] = [ // tied to pointer width. --- { file: "validation/max-value-size.json", - line: 25, + line: 26, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + "validated — wasmtime does not implement CM#688's elem_size < 2^28 " + @@ -103,52 +103,52 @@ export const XFAIL: XfailEntry[] = [ }, { file: "validation/max-value-size.json", - line: 31, + line: 32, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25 (fixed list " + + "validated — same max-value-size-nyi gap as line 26 (fixed list " + "whose product exceeds MAX: `(list u64 33554432)`), " + "https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/max-value-size.json", - line: 37, + line: 38, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25 (u32-wrap class: " + + "validated — same max-value-size-nyi gap as line 26 (u32-wrap class: " + "real byte size is 2^32 but a naive u32 multiply wraps to 0: " + "`(list u64 536870912)`), https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/max-value-size.json", - line: 43, + line: 44, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25 (compound sum " + + "validated — same max-value-size-nyi gap as line 26 (compound sum " + "exceeds MAX via a tuple), https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/max-value-size.json", - line: 48, + line: 49, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25 (compound sum " + + "validated — same max-value-size-nyi gap as line 26 (compound sum " + "exceeds MAX via a record), https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/max-value-size.json", - line: 57, + line: 58, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25 (nested fixed " + + "validated — same max-value-size-nyi gap as line 26 (nested fixed " + "list), https://github.com/polymorph-components/polyengine/issues/248", }, { file: "validation/max-value-size.json", - line: 63, + line: 64, reason: 'expected assert_invalid ("exceeds maximum byte size"), but it ' + - "validated — same max-value-size-nyi gap as line 25; this is the " + + "validated — same max-value-size-nyi gap as line 26; this is the " + "dispatch-flagged pointer-width-sensitive row (`(list string " + "16777216)`, the i32-vs-i64 elem-size boundary noted in the wast " + "source comment) — observed identically to the other rows on this " + @@ -934,90 +934,8 @@ export const XFAIL: XfailEntry[] = [ // needs a host trampoline for `thread-new-indirect`, which this executor // does not implement yet, so every `module_instance` command against it // is pending-capability/SKIPPED (no xfail entry needed) and every assert - // cascades with "no current instance". The file grew (CM#715) so the - // cascade now extends past the pre-existing 281-305 entries below to - // 307-327 (new entries added at the end of this block). Also listed in - // upstream's own third_party/component-model/test/nyi.txt at this pin. --- - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 281, - reason: - "cascade: this file's component was declined earlier, so " + - "every later command against the instance fails; see the " + - "first entry for this file", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 283, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 285, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 287, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 289, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 291, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 293, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 295, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 297, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, - { - file: "async/trap-if-sync-and-waitable-set.json", - line: 299, - reason: - "cascade of this file's first failure: the component was " + - "declined at instantiation, so no instance exists for this " + - "command", - }, + // cascades with "no current instance". Also listed in upstream's own + // third_party/component-model/test/nyi.txt at this pin. --- { file: "async/trap-if-sync-and-waitable-set.json", line: 301, diff --git a/harness/tests/runner_unit_test.ts b/harness/tests/runner_unit_test.ts index d138a9b4..5e88462b 100644 --- a/harness/tests/runner_unit_test.ts +++ b/harness/tests/runner_unit_test.ts @@ -11,15 +11,17 @@ import { runWastJson, trapMatches } from "../src/runner.ts"; // (module) - the empty core module, hand-encoded. const EMPTY_CORE_MODULE = new Uint8Array([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]); -// Core preamble with a bogus version. -const BAD_VERSION_MODULE = new Uint8Array([0, 0x61, 0x73, 0x6d, 9, 0, 0, 0]); +// A core module with a valid preamble (sniffs as kind "module") but an +// invalid trailing section byte (0xff is not a valid section id), so +// WebAssembly.validate rejects it on content, not preamble. +const INVALID_SECTION_MODULE = new Uint8Array([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0, 0xff]); // (component) - the empty component: core preamble with version 0x0d, // layer 0x0001. const EMPTY_COMPONENT = new Uint8Array([0, 0x61, 0x73, 0x6d, 0x0d, 0, 1, 0]); const artifacts = new Map>([ ["ok.0.wasm", EMPTY_CORE_MODULE], - ["bad.0.wasm", BAD_VERSION_MODULE], + ["bad.0.wasm", INVALID_SECTION_MODULE], ["comp.0.wasm", EMPTY_COMPONENT], ]); @@ -41,7 +43,7 @@ function assertEq(actual: unknown, expected: unknown, what: string) { Deno.test("environment: V8 validates core modules but no component binaries", () => { assertEq(WebAssembly.validate(EMPTY_CORE_MODULE), true, "core valid"); - assertEq(WebAssembly.validate(BAD_VERSION_MODULE), false, "bad version"); + assertEq(WebAssembly.validate(INVALID_SECTION_MODULE), false, "invalid section"); // The load-bearing fact behind skip("pending-runtime"): the JS API rejects // the component layer preamble outright, so `validate === false` carries // no information about a component's actual validity. @@ -56,14 +58,12 @@ Deno.test("core module command executes via the JS WebAssembly API", async () => line: 1, filename: "ok.0.wasm", module_type: "binary", - kind: "module", }, { type: "assert_invalid", line: 2, filename: "bad.0.wasm", module_type: "binary", - kind: "module", text: "whatever", }, ]), @@ -85,7 +85,6 @@ Deno.test("core invoke and definition instantiation are pending-runtime", async line: 1, filename: "ok.0.wasm", module_type: "binary", - kind: "module", }, { type: "assert_return", @@ -116,14 +115,12 @@ Deno.test("component-layer commands are pending-runtime", async () => { line: 1, filename: "comp.0.wasm", module_type: "binary", - kind: "component", }, { type: "assert_invalid", line: 2, filename: "comp.0.wasm", module_type: "binary", - kind: "component", text: "whatever", }, ]), @@ -145,7 +142,6 @@ Deno.test("text artifacts are unsupported-directive", async () => { line: 1, filename: "x.0.wat", module_type: "text", - kind: "component", text: "whatever", }, ]), @@ -167,7 +163,6 @@ Deno.test("a genuinely invalid core module fails assert-free module command", as line: 1, filename: "bad.0.wasm", module_type: "binary", - kind: "module", }, ]), load, diff --git a/runtime/tests/integration/e2e_suite_test.ts b/runtime/tests/integration/e2e_suite_test.ts index 47310a55..bb8e6955 100644 --- a/runtime/tests/integration/e2e_suite_test.ts +++ b/runtime/tests/integration/e2e_suite_test.ts @@ -18,6 +18,7 @@ import { isInstancePoisoned } from "../../src/task/scheduler.ts"; import { Translator } from "../../src/shim/mod.ts"; import { instantiateComponent } from "../../src/exec/mod.ts"; import { TranslateError } from "../../src/plan/mod.ts"; +import { artifactKind } from "../../../harness/src/runner.ts"; const root = new URL("../../../", import.meta.url); @@ -384,7 +385,6 @@ interface WastCommand { type: string; line: number; filename?: string; - kind?: string; module_type?: string; } @@ -423,20 +423,20 @@ async function commandsOf(dir: string): Promise<[string, WastCommand[]][]> { const KNOWN_ACCEPTANCE_GAPS: ReadonlySet = new Set([ // name-rules-nyi (https://github.com/polymorph-components/polyengine/issues/248): kebab-case name-folding import // conflicts wasmparser 0.258 does not detect. - "kebab.json:149", - "kebab.json:154", - "kebab.json:159", - "kebab.json:164", - "kebab.json:169", + "kebab.json:150", + "kebab.json:155", + "kebab.json:160", + "kebab.json:165", + "kebab.json:170", // max-value-size-nyi (https://github.com/polymorph-components/polyengine/issues/248): the elem_size(t, i64) < 2^28 // check wasmparser 0.258 does not enforce. - "max-value-size.json:25", - "max-value-size.json:31", - "max-value-size.json:37", - "max-value-size.json:43", - "max-value-size.json:48", - "max-value-size.json:57", - "max-value-size.json:63", + "max-value-size.json:26", + "max-value-size.json:32", + "max-value-size.json:38", + "max-value-size.json:44", + "max-value-size.json:49", + "max-value-size.json:58", + "max-value-size.json:64", ]); /** @@ -462,9 +462,10 @@ Deno.test({ if (cmd.type !== "assert_invalid" && cmd.type !== "assert_malformed") { continue; } - if (cmd.kind !== "component" || cmd.module_type !== "binary") continue; - if (KNOWN_ACCEPTANCE_GAPS.has(`${file}:${cmd.line}`)) continue; + if (cmd.module_type !== "binary") continue; const bytes = await Deno.readFile(new URL(cmd.filename!, base)); + if (artifactKind(bytes) !== "component") continue; + if (KNOWN_ACCEPTANCE_GAPS.has(`${file}:${cmd.line}`)) continue; checked++; try { translator!.translate(bytes); @@ -507,8 +508,9 @@ Deno.test({ if (cmd.type !== "module" && cmd.type !== "module_definition") { continue; } - if (cmd.kind !== "component" || cmd.module_type !== "binary") continue; + if (cmd.module_type !== "binary") continue; const bytes = await Deno.readFile(new URL(cmd.filename!, base)); + if (artifactKind(bytes) !== "component") continue; try { translator!.translate(bytes); ok++; diff --git a/upstream-component-model-repo-findings.md b/upstream-component-model-repo-findings.md index 7b4c128e..42ffc2ec 100644 --- a/upstream-component-model-repo-findings.md +++ b/upstream-component-model-repo-findings.md @@ -116,10 +116,9 @@ entry on resolution — see git history.) ## Out of scope (tracked elsewhere, listed so they aren't lost) - **wasm-tools CLI 1.247 `json-from-wast` parser lag** (15/59 suite files - parse; current `wast` crate parses 59/59): version-skew, resolved on our - side by owning the emitter (`crates/testgen`, docs/architecture.md §11). Only worth - upstream traffic (bytecodealliance/wasm-tools) if still true at a current - CLI release. + parse; current `wast` crate parses 59/59): version-skew, resolved by + version — `crates/testgen` links the `json-from-wast` crate at the pinned + wasm-tools release train (0.258 parses 67/67). No upstream traffic needed. - **wasmparser 0.252 requires async function types for async lifts; wasm-tools 1.247's validator predates the rule**: spec-tracking drift between released versions, not a component-model repo defect. Handled by docs/architecture.md §4.1/§9