open-standards/wai/wai-int-module/host/src/main.rs
Transaction Science 98d646dc92 wai-int-module: the first WA²ISM module of record, the operator-set module held at the integer level on every engine
wai/wai-int-module is one WebAssembly module with the interface of waism §5 and nothing else:
no import, one memory with a declared maximum, and every allocation trapping where memory
cannot grow (R13). It decodes the integer payloads (WIV1, WIH1, WIA1, WIS1), int_synth,
int_mlicpp, opus.dred.int and avatar.face through wai-rs's integer decoders, whose networks
run on wai-ops/1; networks and weights arrive as each capability's set.

- record/: the scalar and SIMD128 builds (7,099 vector instructions), named by SHA-256 in
  manifest.json, each held at the integer level by the Rust checker and check-module.mjs.
  build.sh builds both with the machine's paths remapped and checks both with both checkers.
  A build on another host has other bytes (the compiler's crate hashes take in each path
  dependency's absolute path and the host) and gives every case the same outcome.
- corpus.json (corpus.py): every committed vector and refusal fixture of those
  capabilities, 120 cases, from the committed corpora.
- host/ (wasmtime 50.0.1 Cranelift and Winch, Wasmi 2.0.0 with floats off), run-node.mjs and
  browser/run-browser.mjs run every case as a sink does and print one run digest; every run,
  on Linux x86_64, Linux arm64 and macOS arm64, gives the same.
- plant.py plants a float or a relaxed vector instruction in a module, for the checkers.
- waism §5, The set: one file as its bytes, several as their file sequence. avatars §6 names
  generator.wfg and identity.wfi; avatars §7.8, opus-dred §4.8 and determinism-tiers §5 state
  the module as a fabric.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 16:45:56 -05:00

569 lines
24 KiB
Rust

//! `wai-int-module-host`: the module of record on native engines, as a sink runs it.
//!
//! wai-int-module-host [--engines cranelift,winch,wasmi] [--external DIR] [--only PREFIX]
//! CORPUS MODULE...
//!
//! For each module and each engine named, every case of the module corpus `CORPUS`
//! (`../corpus.json`, written by `../corpus.py`) on an instance of its own, as waism §5 and §6
//! checks 11 to 13 state: the module instantiated with no imports, `wai_abi`, the capability,
//! the payload and the set each written at an offset from `wai_alloc`, `wai_run` under a fuel
//! budget, then the output read back. Each output is held to the corpus: the SHA-256 it
//! states, or the refusal code. `--external DIR` is the directory the corpus's external files
//! were fetched to (`live-conformance/dred/fetch.sh`); a case that names one is a failure
//! without it.
//!
//! The engines, each set as waism §7 sets an engine at the integer level:
//!
//! - `cranelift`, `winch`: wasmtime 50.0.1 with that compiler; shared memory and atomics off
//! (`threads-off`), relaxed vector instructions off (`relaxed-off`), fuel, and the memory
//! limited to the module's declared maximum. No engine setting fixes a NaN or a relaxed
//! instruction's behaviour: the integer level reaches neither (waism §7).
//! - `wasmi`: Wasmi 2.0.0, an interpreter, set the same way, with its floating-point
//! instructions and types turned off as well: a module with any floating point in it does
//! not compile, a second guard behind waism R3.
//!
//! Each run prints one line: its module, engine, the cases it held, and the **run digest**,
//! the SHA-256 of a line per case (`<name> <outcome>`, the outcome `sha256:<hex>` of the
//! output or `refuse:<code>`). The run digest of every run must be the same: a module's
//! scalar and SIMD128 builds, and every engine, give each case the same outcome.
//!
//! Exit status 0 when every case of every run is the corpus's and every run digest is the
//! same; 1 otherwise; 2 for a usage or input error.
//!
//! wai-int-module-host --count MODULE...
//!
//! For each module, how many instructions its function bodies hold, and how many of them
//! are 128-bit vector instructions: what tells the SIMD128 build from the scalar one.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Instant;
use serde_json::Value;
use sha2::{Digest, Sha256};
/// The instructions an engine may count for one run of `wai_run` before it is stopped
/// (`module_timeout`, waism §6 check 12).
const FUEL: u64 = 1 << 42;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn sha256(b: &[u8]) -> String {
hex(&Sha256::digest(b))
}
// ------------------------------------------------------------------------------ the corpus
enum Expect {
Sha256(String),
Refuse(String),
}
struct Case {
name: String,
cap: String,
payload: Vec<u8>,
set: Vec<u8>,
expect: Expect,
}
fn unhex(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("odd hexadecimal".into());
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())).collect()
}
/// A file a case names: a path below wai/, `{"hex"}`, or `{"external"}`.
fn file(v: &Value, wai: &Path, external: Option<&Path>) -> Result<Vec<u8>, String> {
if let Some(p) = v.as_str() {
return std::fs::read(wai.join(p)).map_err(|e| format!("{p}: {e}"));
}
if let Some(h) = v.get("hex").and_then(Value::as_str) {
return unhex(h);
}
if let Some(p) = v.get("file").and_then(Value::as_str) {
return std::fs::read(wai.join(p)).map_err(|e| format!("{p}: {e}"));
}
if let Some(n) = v.get("external").and_then(Value::as_str) {
let dir = external.ok_or_else(|| format!("{n} is an external file, and no --external directory was given"))?;
return std::fs::read(dir.join(n)).map_err(|e| format!("{n}: {e}"));
}
Err(format!("not a file: {v}"))
}
/// The set a case names, as waism §5 passes it: nothing, one file's bytes, or the file
/// sequence of a list of `[name, file]`.
fn set(v: &Value, wai: &Path, external: Option<&Path>) -> Result<Vec<u8>, String> {
match v {
Value::Null => Ok(Vec::new()),
Value::Array(files) => {
let mut out = Vec::new();
for f in files {
let (Some(name), Some(what)) = (f.get(0).and_then(Value::as_str), f.get(1)) else { return Err(format!("not [name, file]: {f}")) };
let b = file(what, wai, external)?;
out.push(u8::try_from(name.len()).map_err(|_| "a name past 255 bytes")?);
out.extend_from_slice(name.as_bytes());
out.extend_from_slice(&u32::try_from(b.len()).map_err(|_| "a file past 4 GiB")?.to_le_bytes());
out.extend_from_slice(&b);
}
Ok(out)
}
other => file(other, wai, external),
}
}
fn corpus(path: &Path, external: Option<&Path>, only: Option<&str>) -> Result<Vec<Case>, String> {
let text = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
let doc: Value = serde_json::from_slice(&text).map_err(|e| format!("{}: {e}", path.display()))?;
if doc["format"] != "waism-module-corpus/1" {
return Err(format!("{}: not a waism-module-corpus/1", path.display()));
}
let abs = std::fs::canonicalize(path).map_err(|e| e.to_string())?;
let wai = abs.parent().and_then(Path::parent).ok_or("the corpus is not two levels below wai/")?;
let mut cases = Vec::new();
for c in doc["cases"].as_array().ok_or("no cases")? {
let name = c["name"].as_str().ok_or("a case with no name")?.to_string();
if only.is_some_and(|o| !name.starts_with(o)) {
continue;
}
let expect = if let Some(s) = c["expect"]["sha256"].as_str() {
Expect::Sha256(s.to_string())
} else if let Some(r) = c["expect"]["refuse"].as_str() {
Expect::Refuse(r.to_string())
} else {
return Err(format!("{name}: no expect"));
};
let cx = |e: String| format!("{name}: {e}");
cases.push(Case {
cap: c["capability"].as_str().ok_or("a case with no capability")?.to_string(),
payload: file(&c["payload"], wai, external).map_err(cx)?,
set: set(&c["set"], wai, external).map_err(cx)?,
expect,
name,
});
}
Ok(cases)
}
// --------------------------------------------------------------------------- the sink
/// What a run of one case gave: the module's output or its refusal, or the sink's refusal
/// of the run (waism §8: `module_resources`, `module_trap`, `module_timeout`,
/// `module_result_invalid`).
enum Outcome {
Output(Vec<u8>),
Refused(String),
Sink(&'static str, String),
}
impl Outcome {
fn line(&self) -> String {
match self {
Outcome::Output(b) => format!("sha256:{}", sha256(b)),
Outcome::Refused(c) => format!("refuse:{c}"),
Outcome::Sink(c, _) => format!("sink:{c}"),
}
}
}
/// A refusal code a module returns: ASCII lowercase letters, digits and `_`.
fn code(b: &[u8]) -> Option<String> {
(!b.is_empty() && b.iter().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'_')).then(|| String::from_utf8_lossy(b).into_owned())
}
/// The calls of waism §5 on one instance, for any engine: `call` runs an export, `write`
/// and `read` reach the memory.
trait Instance {
fn call0(&mut self, name: &str) -> Result<i32, Outcome>;
fn call1(&mut self, name: &str, a: i32) -> Result<i32, Outcome>;
fn run(&mut self, args: [i32; 6]) -> Result<i32, Outcome>;
fn write(&mut self, at: usize, b: &[u8]) -> bool;
fn read(&mut self, at: usize, n: usize) -> Option<Vec<u8>>;
}
fn sink(inst: &mut dyn Instance, c: &Case) -> Outcome {
match inst.call0("wai_abi") {
Ok(1) => {}
Ok(v) => return Outcome::Sink("module_result_invalid", format!("wai_abi gave {v}")),
Err(o) => return o,
}
let mut args = [0i32; 6];
for (i, input) in [c.cap.as_bytes(), &c.payload, &c.set].into_iter().enumerate() {
if input.is_empty() {
continue;
}
let Ok(len) = i32::try_from(input.len()) else { return Outcome::Sink("module_resources", "an input past 2 GiB".into()) };
let at = match inst.call1("wai_alloc", len) {
Ok(0) => return Outcome::Sink("module_resources", "wai_alloc gave 0".into()),
Ok(at) => at,
Err(o) => return o,
};
if !inst.write(at as u32 as usize, input) {
return Outcome::Sink("module_result_invalid", "wai_alloc gave a region outside the memory".into());
}
args[2 * i] = at;
args[2 * i + 1] = len;
}
let status = match inst.run(args) {
Ok(s) => s,
Err(o) => return o,
};
let (ptr, len) = match (inst.call0("wai_out_ptr"), inst.call0("wai_out_len")) {
(Ok(p), Ok(l)) => (p as u32 as usize, l as u32 as usize),
(Err(o), _) | (_, Err(o)) => return o,
};
let Some(out) = inst.read(ptr, len) else { return Outcome::Sink("module_result_invalid", "the output lies outside the memory".into()) };
match status {
0 => Outcome::Output(out),
1 => match code(&out) {
Some(c) => Outcome::Refused(c),
None => Outcome::Sink("module_result_invalid", "a refusal that is not a code".into()),
},
s => Outcome::Sink("module_result_invalid", format!("wai_run gave {s}")),
}
}
// --------------------------------------------------------------------------- wasmtime
mod wt {
use super::{FUEL, Instance as Inst, Outcome};
use wasmtime::{Config, Engine, Instance, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, Strategy, Trap};
pub struct Data {
limits: StoreLimits,
}
pub fn engine(strategy: Strategy) -> wasmtime::Result<Engine> {
let mut c = Config::new();
c.strategy(strategy);
c.wasm_threads(false);
c.wasm_relaxed_simd(false);
c.consume_fuel(true);
Engine::new(&c)
}
pub struct Run {
store: Store<Data>,
inst: Instance,
mem: Memory,
}
fn fail(e: wasmtime::Error) -> Outcome {
match e.downcast_ref::<Trap>() {
Some(Trap::OutOfFuel) => Outcome::Sink("module_timeout", e.to_string()),
_ => Outcome::Sink("module_trap", format!("{e:#}")),
}
}
pub fn instantiate(engine: &Engine, module: &Module, max_memory: usize) -> Result<Run, Outcome> {
let mut store = Store::new(engine, Data { limits: StoreLimitsBuilder::new().memory_size(max_memory).build() });
store.limiter(|d| &mut d.limits);
store.set_fuel(FUEL).map_err(|e| Outcome::Sink("module_resources", e.to_string()))?;
let inst = Instance::new(&mut store, module, &[]).map_err(|e| Outcome::Sink("module_resources", format!("{e:#}")))?;
let mem = inst.get_memory(&mut store, "memory").ok_or_else(|| Outcome::Sink("module_result_invalid", "no memory".into()))?;
Ok(Run { store, inst, mem })
}
impl Inst for Run {
fn call0(&mut self, name: &str) -> Result<i32, Outcome> {
let f = self.inst.get_typed_func::<(), i32>(&mut self.store, name).map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, ()).map_err(fail)
}
fn call1(&mut self, name: &str, a: i32) -> Result<i32, Outcome> {
let f = self.inst.get_typed_func::<i32, i32>(&mut self.store, name).map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, a).map_err(fail)
}
fn run(&mut self, a: [i32; 6]) -> Result<i32, Outcome> {
let f = self
.inst
.get_typed_func::<(i32, i32, i32, i32, i32, i32), i32>(&mut self.store, "wai_run")
.map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, (a[0], a[1], a[2], a[3], a[4], a[5])).map_err(fail)
}
fn write(&mut self, at: usize, b: &[u8]) -> bool {
self.mem.write(&mut self.store, at, b).is_ok()
}
fn read(&mut self, at: usize, n: usize) -> Option<Vec<u8>> {
let mut out = vec![0u8; n];
self.mem.read(&self.store, at, &mut out).ok().map(|_| out)
}
}
}
// ------------------------------------------------------------------------------ wasmi
mod wi {
use super::{FUEL, Instance as Inst, Outcome};
use wasmi::{Config, Engine, Instance, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, TrapCode};
pub struct Data {
limits: StoreLimits,
}
pub fn engine() -> Engine {
let mut c = Config::default();
c.wasm_relaxed_simd(false);
c.floats(false);
c.consume_fuel(true);
Engine::new(&c)
}
pub struct Run {
store: Store<Data>,
inst: Instance,
mem: Memory,
}
fn fail(e: wasmi::Error) -> Outcome {
match e.as_trap_code() {
Some(TrapCode::OutOfFuel) => Outcome::Sink("module_timeout", e.to_string()),
_ => Outcome::Sink("module_trap", e.to_string()),
}
}
pub fn instantiate(engine: &Engine, module: &Module, max_memory: usize) -> Result<Run, Outcome> {
let mut store = Store::new(engine, Data { limits: StoreLimitsBuilder::new().memory_size(max_memory).build() });
store.limiter(|d| &mut d.limits);
store.set_fuel(FUEL).map_err(|e| Outcome::Sink("module_resources", e.to_string()))?;
let inst = Instance::new(&mut store, module, &[]).map_err(|e| Outcome::Sink("module_resources", e.to_string()))?;
let mem = inst.get_memory(&store, "memory").ok_or_else(|| Outcome::Sink("module_result_invalid", "no memory".into()))?;
Ok(Run { store, inst, mem })
}
impl Inst for Run {
fn call0(&mut self, name: &str) -> Result<i32, Outcome> {
let f = self.inst.get_typed_func::<(), i32>(&self.store, name).map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, ()).map_err(fail)
}
fn call1(&mut self, name: &str, a: i32) -> Result<i32, Outcome> {
let f = self.inst.get_typed_func::<i32, i32>(&self.store, name).map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, a).map_err(fail)
}
fn run(&mut self, a: [i32; 6]) -> Result<i32, Outcome> {
let f = self
.inst
.get_typed_func::<(i32, i32, i32, i32, i32, i32), i32>(&self.store, "wai_run")
.map_err(|e| Outcome::Sink("module_result_invalid", e.to_string()))?;
f.call(&mut self.store, (a[0], a[1], a[2], a[3], a[4], a[5])).map_err(fail)
}
fn write(&mut self, at: usize, b: &[u8]) -> bool {
self.mem.write(&mut self.store, at, b).is_ok()
}
fn read(&mut self, at: usize, n: usize) -> Option<Vec<u8>> {
let mut out = vec![0u8; n];
self.mem.read(&self.store, at, &mut out).ok().map(|_| out)
}
}
}
// ------------------------------------------------------------------------------- runs
/// The module's declared maximum memory, in bytes (waism R6), read from its memory section.
fn max_memory(bytes: &[u8]) -> Result<usize, String> {
use wasmparser::{Parser, Payload};
for p in Parser::new(0).parse_all(bytes) {
if let Payload::MemorySection(r) = p.map_err(|e| e.to_string())? {
let m = r.into_iter().next().ok_or("no memory")?.map_err(|e| e.to_string())?;
let pages = m.maximum.ok_or("the memory declares no maximum")?;
return usize::try_from(pages * 65_536).map_err(|e| e.to_string());
}
}
Err("no memory".into())
}
/// One run: every case on one engine, each on an instance of its own. Returns each case's
/// outcome line, and how many cases differ from the corpus.
fn run_all(cases: &[Case], mut make: impl FnMut() -> Result<Box<dyn Instance>, Outcome>) -> (Vec<String>, usize) {
let (mut lines, mut wrong) = (Vec::new(), 0);
for c in cases {
let got = match make() {
Ok(mut inst) => sink(inst.as_mut(), c),
Err(o) => o,
};
let ok = match (&c.expect, &got) {
(Expect::Sha256(want), Outcome::Output(b)) => sha256(b) == *want,
(Expect::Refuse(want), Outcome::Refused(code)) => code == want,
_ => false,
};
if !ok {
wrong += 1;
let want = match &c.expect {
Expect::Sha256(s) => format!("sha256:{s}"),
Expect::Refuse(r) => format!("refuse:{r}"),
};
let detail = match &got {
Outcome::Sink(_, d) => format!(" ({d})"),
_ => String::new(),
};
println!(" {}: {}{detail}, not {want}", c.name, got.line());
}
lines.push(format!("{} {}\n", c.name, got.line()));
}
(lines, wrong)
}
/// The instructions of a module's function bodies, and how many are 128-bit vector
/// instructions (those of WebAssembly's `0xfd` prefix).
fn count(bytes: &[u8]) -> Result<(u64, u64), String> {
use wasmparser::{Parser, Payload};
let (mut all, mut vector) = (0u64, 0u64);
for p in Parser::new(0).parse_all(bytes) {
if let Payload::CodeSectionEntry(body) = p.map_err(|e| e.to_string())? {
let mut ops = body.get_operators_reader().map_err(|e| e.to_string())?;
while !ops.eof() {
let at = usize::try_from(ops.original_position()).map_err(|e| e.to_string())?;
ops.read().map_err(|e| e.to_string())?;
all += 1;
if bytes.get(at) == Some(&0xfd) {
vector += 1;
}
}
}
}
Ok((all, vector))
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.first().map(String::as_str) == Some("--count") && args.len() > 1 {
for path in &args[1..] {
match std::fs::read(path).map_err(|e| e.to_string()).and_then(|b| count(&b).map(|c| (sha256(&b), c))) {
Ok((sum, (all, vector))) => println!("{path} (SHA-256 {sum}): {all} instructions, {vector} of them 128-bit vector instructions"),
Err(e) => {
eprintln!("wai-int-module-host: {path}: {e}");
return ExitCode::from(2);
}
}
}
return ExitCode::SUCCESS;
}
let usage = || {
eprintln!("usage: wai-int-module-host [--engines cranelift,winch,wasmi] [--external DIR] [--only PREFIX] CORPUS MODULE...");
ExitCode::from(2)
};
let (mut engines, mut external, mut only, mut rest) = ("cranelift,winch,wasmi".to_string(), None::<PathBuf>, None::<String>, Vec::new());
let mut it = args.into_iter();
while let Some(a) = it.next() {
match a.as_str() {
"--engines" => match it.next() {
Some(e) => engines = e,
None => return usage(),
},
"--external" => match it.next() {
Some(d) => external = Some(PathBuf::from(d)),
None => return usage(),
},
"--only" => match it.next() {
Some(p) => only = Some(p),
None => return usage(),
},
s if s.starts_with("--") => return usage(),
_ => rest.push(a),
}
}
if rest.len() < 2 {
return usage();
}
let cases = match corpus(Path::new(&rest[0]), external.as_deref(), only.as_deref()) {
Ok(c) => c,
Err(e) => {
eprintln!("wai-int-module-host: {e}");
return ExitCode::from(2);
}
};
let mut digests: BTreeMap<String, String> = BTreeMap::new();
let mut failed = false;
for path in &rest[1..] {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
eprintln!("wai-int-module-host: {path}: {e}");
return ExitCode::from(2);
}
};
let module_sha = sha256(&bytes);
for name in engines.split(',') {
let t0 = Instant::now();
let (lines, wrong) = match name {
"cranelift" | "winch" => {
let strategy = if name == "winch" { wasmtime::Strategy::Winch } else { wasmtime::Strategy::Cranelift };
let engine = match wt::engine(strategy) {
Ok(e) => e,
Err(e) => {
println!("{path} on wasmtime 50.0.1/{name}: the engine does not start: {e:#}");
failed = true;
continue;
}
};
let max = match max_memory(&bytes) {
Ok(m) => m,
Err(e) => {
println!("{path} on wasmtime 50.0.1/{name}: {e}");
failed = true;
continue;
}
};
let module = match wasmtime::Module::new(&engine, &bytes) {
Ok(m) => m,
Err(e) => {
println!("{path} on wasmtime 50.0.1/{name}: the module does not compile: {e:#}");
failed = true;
continue;
}
};
run_all(&cases, || wt::instantiate(&engine, &module, max).map(|r| Box::new(r) as Box<dyn Instance>))
}
"wasmi" => {
let max = match max_memory(&bytes) {
Ok(m) => m,
Err(e) => {
println!("{path} on wasmi 2.0.0: {e}");
failed = true;
continue;
}
};
let engine = wi::engine();
let module = match wasmi::Module::new(&engine, &bytes) {
Ok(m) => m,
Err(e) => {
println!("{path} on wasmi 2.0.0: the module does not compile: {e}");
failed = true;
continue;
}
};
run_all(&cases, || wi::instantiate(&engine, &module, max).map(|r| Box::new(r) as Box<dyn Instance>))
}
other => {
eprintln!("wai-int-module-host: no engine {other}");
return ExitCode::from(2);
}
};
let run = sha256(lines.concat().as_bytes());
let label = match name {
"wasmi" => "[email protected]".to_string(),
n => format!("[email protected]/{n}"),
};
println!(
"{path} (SHA-256 {module_sha}) on {label}, {}: {} of {} cases the corpus's; run digest {run}; {:.1} s",
std::env::consts::ARCH,
cases.len() - wrong,
cases.len(),
t0.elapsed().as_secs_f64()
);
failed |= wrong > 0;
digests.insert(format!("{path} {label}"), run);
}
}
let distinct: std::collections::BTreeSet<&String> = digests.values().collect();
if distinct.len() > 1 {
println!("wai-int-module-host: the runs differ: {} run digests", distinct.len());
failed = true;
} else if let Some(d) = distinct.iter().next() {
println!("wai-int-module-host: {} runs, one run digest {d}", digests.len());
}
if failed { ExitCode::FAILURE } else { ExitCode::SUCCESS }
}