open-standards/wai/wai-wasmq/tests/engines.rs
Transaction Science 2593c1f945
Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 7s
wai-quantum wasmQ / does this change touch it (push) Successful in 10s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 28s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 54s
wai-quantum wasmQ / the reference modules rebuild to their modules of record · linux (push) Successful in 40s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Failing after 1m16s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
wai-app and Create / wai-app · does this change touch it (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-audit collector and player integration / does this change touch it (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
WAI operation catalogue / does this change touch it (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-fleet service / does this change touch it (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai-ledger service / does this change touch it (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-quantum for JavaScript / does this change touch it (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / does this change touch it (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / does this change touch it (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / does this change touch it (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / does this change touch it (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · macos (push) Successful in 2m13s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 2m33s
wai-quantum wasmQ / interpreter and vectors under wasm32-wasip2 (wasmtime) (push) Successful in 2m38s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 4m11s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · linux (push) Successful in 4m51s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 6m22s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 7m45s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 9m14s
wai-quantum command line / does this change touch it (push) Has been cancelled
wai-quantum command line / command line · linux (push) Has been cancelled
wai-quantum command line / command line · macos (push) Has been cancelled
wai-quantum contract / does this change touch it (push) Has been cancelled
wai-quantum contract / API compatibility · linux (push) Has been cancelled
wai-quantum contract / schemas and the committed tool lists · linux (push) Has been cancelled
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Has been cancelled
wai-quantum docs, course and locales / does this change touch it (push) Has been cancelled
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Has been cancelled
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Has been cancelled
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Has been cancelled
wai-quantum editors / does this change touch it (push) Has been cancelled
wai-quantum editors / editors · linux (push) Has been cancelled
wai-quantum editors / editors · macos (push) Has been cancelled
wai-quantum resource estimation / does this change touch it (push) Has been cancelled
wai-quantum resource estimation / estimation and calibration · linux, release (push) Has been cancelled
wai-quantum other formats / does this change touch it (push) Has been cancelled
wai-quantum other formats / readers, writers and the set · linux (push) Has been cancelled
wai-quantum other formats / the set under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum OpenQASM reader / does this change touch it (push) Has been cancelled
wai-quantum OpenQASM reader / reader and corpus · linux (push) Has been cancelled
wai-quantum OpenQASM reader / corpus under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum pulse and dynamics / does this change touch it (push) Has been cancelled
wai-quantum pulse and dynamics / reader, simulator and pulse jobs · linux, release (push) Has been cancelled
wai-quantum pulse and dynamics / the same bits · linux-arm64 (push) Has been cancelled
wai-quantum pulse and dynamics / the same bits · macos (push) Has been cancelled
wai-quantum pulse and dynamics / reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum for Python / does this change touch it (push) Has been cancelled
wai-quantum for Python / python · linux (push) Has been cancelled
wai-quantum for Python / python · macos (push) Has been cancelled
wai-quantum error correction and mitigation / does this change touch it (push) Has been cancelled
wai-quantum error correction and mitigation / codes, decoders, texts and mitigation · linux, release (push) Has been cancelled
wai-quantum extension points / does this change touch it (push) Has been cancelled
wai-quantum extension points / the registry and the example · linux (push) Has been cancelled
wai-quantum extension points / the registry and the example · linux-arm64 (push) Has been cancelled
wai-quantum extension points / the registry under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum service / does this change touch it (push) Has been cancelled
wai-quantum service / service · linux (push) Has been cancelled
wai-quantum service / service · macos (push) Has been cancelled
wai-quantum service / service energy · linux packages (push) Has been cancelled
wai-quantum variational and algorithm library / does this change touch it (push) Has been cancelled
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux, release (push) Has been cancelled
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (push) Has been cancelled
wai-quantum verdicts and repairs / does this change touch it (push) Has been cancelled
wai-quantum verdicts and repairs / verdicts, repairs and the intent set · linux (push) Has been cancelled
wai-quantum verdicts and repairs / the intent set and the verdict under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum views in agent hosts / does this change touch it (push) Has been cancelled
wai-quantum views in agent hosts / views · the page and its engine (push) Has been cancelled
wai-quantum views in agent hosts / views · a scripted host (push) Has been cancelled
wai-quantum-landscape-fpga reference design / does this change touch it (push) Has been cancelled
wai-quantum-landscape-fpga reference design / the member against the model of the logic, and the logic against the model · linux, release (push) Has been cancelled
wai-quantum-landscape-fpga reference design / the board's code on the board's processor's architecture · linux-arm64, release (push) Has been cancelled
wai: wasmQ, an OpenQASM 3 program and the WebAssembly its externs call, run the same everywhere, metered and receipted
Re-landed from PR #659 (branch wai/quantum-wasmq, head 39fdb517a) as one
commit on main, under the land-it model. Five review rounds; the last had
no high or critical finding, and its mediums were fixed on the branch.

A hybrid quantum program runs classical code between its gates. OpenQASM 3
declares such a function with `extern` and leaves its body and linking
outside the language. wasmQ (wai.quantum.wasmq) packs the program and the
WebAssembly that defines its externs into one content-addressed WAI
envelope; a run gives the same result bytes and fuel on every host and
seals the engine's job receipt.

- extensions/quantum-wasmq.md (normative) and quantum-wasmq.wit.
- wai/wai-wasmq: the profile (Lime1, nothing imported, bounded memory,
  refusal codes W001-W017); what a run asks of its host, bounded from the
  bundle alone before anything runs; fuel defined on the module as written;
  a weighted stack; two engines (the pinned wasmtime 50.0.1 and an
  interpreter of the crate's own that also runs on wasm32); the envelope,
  runner, receipts, transcript replay, reference programs and vectors.
- wai-quantum: engine::wasmq (built with quantum_receipt), the job form
  beside its receipts; the estimate verifier answers a request no host
  re-runs with a verdict carrying rerun_error, combined here with main's
  reaction clock (clock_of).
- wai/quantum-conformance/wasmq: envelopes, refusals and receipts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:22:09 -05:00

529 lines
26 KiB
Rust

//! The engines agree (extensions/quantum-wasmq.md §4.6): every instruction of the profile
//! on edge-case operands gives the same value or the same trap on the interpreter (which
//! meters and canonicalises itself) and on the pinned native engine (which runs the
//! prepared module); the fuel each counts for a run is the fuel the native engine's own
//! meter counts on the original module, but for a `memory.grow` that fails; the stack
//! ends at the same call on each; and a native frame takes at most eight bytes for each
//! unit of the weight the profile gives it (§4.3).
use wai_wasmq::engine::{Engine, Interpreter, Trap};
use wai_wasmq::interp::Metering;
use wai_wasmq::meter;
const I32: &[u64] = &[0, 1, 2, 7, 31, 32, 33, 0x7f, 0x80, 0xff, 0x7fff_ffff, 0x8000_0000, 0x8000_0001, 0xffff_ffff, 0xffff_fffe, 0x1234_5678];
const I64: &[u64] = &[0, 1, 2, 63, 64, 65, 0xffff_ffff, 0x1_0000_0000, 0x7fff_ffff_ffff_ffff, 0x8000_0000_0000_0000, 0xffff_ffff_ffff_ffff, 0x0123_4567_89ab_cdef];
const F32: &[u32] = &[
0x0000_0000, 0x8000_0000, 0x3f80_0000, 0xbf80_0000, 0x3f00_0000, 0x3fc0_0000, 0x4020_0000, 0xc020_0000, 0x7f80_0000, 0xff80_0000, 0x7fc0_0000, 0xffc0_0000,
0x7fa0_0001, 0x7f80_0001, 0x7f7f_ffff, 0x0000_0001, 0x4f00_0000, 0xcf00_0000, 0x4f80_0000, 0x5f00_0000, 0xdf00_0000, 0xbf66_6666, 0x3eff_ffff,
];
const F64: &[u64] = &[
0, 0x8000_0000_0000_0000, 0x3ff0_0000_0000_0000, 0xbff0_0000_0000_0000, 0x3fe0_0000_0000_0000, 0x3ff8_0000_0000_0000, 0x4004_0000_0000_0000,
0xc004_0000_0000_0000, 0x7ff0_0000_0000_0000, 0xfff0_0000_0000_0000, 0x7ff8_0000_0000_0000, 0xfff8_0000_0000_0000, 0x7ff4_0000_0000_0001,
0x7ff0_0000_0000_0001, 0x7fef_ffff_ffff_ffff, 1, 0x41e0_0000_0000_0000, 0xc1e0_0000_0000_0000, 0x41f0_0000_0000_0000, 0x43e0_0000_0000_0000,
0xc3e0_0000_0000_0000, 0x43f0_0000_0000_0000, 0xbfec_cccc_cccc_cccd, 0x41df_ffff_ffc0_0000,
];
#[derive(Clone, Copy)]
enum T {
I32,
I64,
F32,
F64,
}
impl T {
fn name(self) -> &'static str {
match self {
T::I32 => "i32",
T::I64 => "i64",
T::F32 => "f32",
T::F64 => "f64",
}
}
/// From the i64 parameter `k` to a value of this type.
fn load(self, k: usize) -> String {
match self {
T::I32 => format!("local.get {k} i32.wrap_i64"),
T::I64 => format!("local.get {k}"),
T::F32 => format!("local.get {k} i32.wrap_i64 f32.reinterpret_i32"),
T::F64 => format!("local.get {k} f64.reinterpret_i64"),
}
}
/// From a value of this type on the stack to the i64 result.
fn store(self) -> &'static str {
match self {
T::I32 => "i64.extend_i32_u",
T::I64 => "",
T::F32 => "i32.reinterpret_f32 i64.extend_i32_u",
T::F64 => "i64.reinterpret_f64",
}
}
fn values(self) -> Vec<u64> {
match self {
T::I32 => I32.to_vec(),
T::I64 => I64.to_vec(),
T::F32 => F32.iter().map(|&x| x as u64).collect(),
T::F64 => F64.to_vec(),
}
}
}
/// Every instruction under test: its text, its operand types and its result type.
fn ops() -> Vec<(&'static str, Vec<T>, T)> {
use T::*;
let mut v: Vec<(&'static str, Vec<T>, T)> = Vec::new();
for (t, names) in [
(I32, &["add", "sub", "mul", "div_s", "div_u", "rem_s", "rem_u", "and", "or", "xor", "shl", "shr_s", "shr_u", "rotl", "rotr"][..]),
(I64, &["add", "sub", "mul", "div_s", "div_u", "rem_s", "rem_u", "and", "or", "xor", "shl", "shr_s", "shr_u", "rotl", "rotr"][..]),
(F32, &["add", "sub", "mul", "div", "min", "max", "copysign"][..]),
(F64, &["add", "sub", "mul", "div", "min", "max", "copysign"][..]),
] {
for n in names {
v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t, t], t));
}
}
for (t, names) in [(I32, &["eq", "ne", "lt_s", "lt_u", "gt_s", "gt_u", "le_s", "le_u", "ge_s", "ge_u"][..]), (I64, &["eq", "ne", "lt_s", "lt_u", "gt_s", "gt_u", "le_s", "le_u", "ge_s", "ge_u"][..]), (F32, &["eq", "ne", "lt", "gt", "le", "ge"][..]), (F64, &["eq", "ne", "lt", "gt", "le", "ge"][..])] {
for n in names {
v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t, t], I32));
}
}
for (t, names) in [(I32, &["clz", "ctz", "popcnt", "extend8_s", "extend16_s"][..]), (I64, &["clz", "ctz", "popcnt", "extend8_s", "extend16_s", "extend32_s"][..]), (F32, &["abs", "neg", "ceil", "floor", "trunc", "nearest", "sqrt"][..]), (F64, &["abs", "neg", "ceil", "floor", "trunc", "nearest", "sqrt"][..])] {
for n in names {
v.push((Box::leak(format!("{}.{n}", t.name()).into_boxed_str()), vec![t], t));
}
}
v.push(("i32.eqz", vec![I32], I32));
v.push(("i64.eqz", vec![I64], I32));
for (name, from, to) in [
("i32.wrap_i64", I64, I32), ("i64.extend_i32_s", I32, I64), ("i64.extend_i32_u", I32, I64),
("i32.trunc_f32_s", F32, I32), ("i32.trunc_f32_u", F32, I32), ("i32.trunc_f64_s", F64, I32), ("i32.trunc_f64_u", F64, I32),
("i64.trunc_f32_s", F32, I64), ("i64.trunc_f32_u", F32, I64), ("i64.trunc_f64_s", F64, I64), ("i64.trunc_f64_u", F64, I64),
("i32.trunc_sat_f32_s", F32, I32), ("i32.trunc_sat_f32_u", F32, I32), ("i32.trunc_sat_f64_s", F64, I32), ("i32.trunc_sat_f64_u", F64, I32),
("i64.trunc_sat_f32_s", F32, I64), ("i64.trunc_sat_f32_u", F32, I64), ("i64.trunc_sat_f64_s", F64, I64), ("i64.trunc_sat_f64_u", F64, I64),
("f32.convert_i32_s", I32, F32), ("f32.convert_i32_u", I32, F32), ("f32.convert_i64_s", I64, F32), ("f32.convert_i64_u", I64, F32),
("f64.convert_i32_s", I32, F64), ("f64.convert_i32_u", I32, F64), ("f64.convert_i64_s", I64, F64), ("f64.convert_i64_u", I64, F64),
("f32.demote_f64", F64, F32), ("f64.promote_f32", F32, F64),
] {
v.push((name, vec![from], to));
}
v
}
fn module() -> (Vec<u8>, Vec<(String, Vec<T>)>) {
let mut wat = String::from("(module (memory 1 1)\n");
let mut names = Vec::new();
for (k, (op, args, ret)) in ops().into_iter().enumerate() {
let params = "i64 ".repeat(args.len());
let loads: Vec<String> = args.iter().enumerate().map(|(i, t)| t.load(i)).collect();
wat.push_str(&format!("(func (export \"op{k}\") (param {params}) (result i64) {} {op} {})\n", loads.join(" "), ret.store()));
names.push((format!("op{k}"), args));
}
wat.push(')');
(wat::parse_str(&wat).unwrap(), names)
}
fn cases(args: &[T]) -> Vec<Vec<u64>> {
match args {
[a] => a.values().into_iter().map(|x| vec![x]).collect(),
[a, b] => a.values().into_iter().flat_map(|x| b.values().into_iter().map(move |y| vec![x, y])).collect(),
_ => vec![],
}
}
fn run_all(engine: &dyn Engine, names: &[(String, Vec<T>)]) -> Vec<(String, Result<Vec<u64>, Trap>, i64)> {
let mut i = engine.instantiate().unwrap();
let mut out = Vec::new();
for (name, args) in names {
for c in cases(args) {
i.set_fuel(1_000_000);
i.set_stack(0);
let r = i.call(name, &c);
out.push((format!("{name}{c:x?}"), r, i.fuel()));
}
}
out
}
#[test]
fn every_instruction_gives_the_same_value_or_trap_on_both_engines() {
let (m, names) = module();
let prepared = meter::prepare(&m).unwrap();
let own = Interpreter::new(&m, Metering::Own).unwrap();
let a = run_all(&own, &names);
let via = Interpreter::new(&prepared, Metering::Module).unwrap();
let b = run_all(&via, &names);
assert_eq!(a.len(), b.len());
assert!(a.len() > 5000, "{} cases", a.len());
for (x, y) in a.iter().zip(&b) {
assert_eq!(x, y, "the interpreter on the original and on the prepared module");
}
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
{
let native = wai_wasmq::engine::Native::new(&prepared).unwrap();
let c = run_all(&native, &names);
for (x, y) in a.iter().zip(&c) {
assert_eq!(x, y, "the interpreter and the native engine");
}
}
// Every NaN that comes out of arithmetic is canonical.
let traps = a.iter().filter(|x| x.1.is_err()).count();
assert!(traps > 50, "{traps} traps: the edge cases reach the trapping paths");
}
/// Programs with every kind of control flow, memory traffic and calls.
const PROGRAMS: &str = r#"
(module (memory 1 4)
(table 3 funcref) (elem (i32.const 0) $sq $neg $sq)
(type $u (func (param i32) (result i32)))
(func $sq (type $u) local.get 0 local.get 0 i32.mul)
(func $neg (type $u) i32.const 0 local.get 0 i32.sub)
(func (export "loop") (param i64) (result i64) (local i32 i32)
local.get 0 i32.wrap_i64 local.set 1
block loop
local.get 1 i32.eqz br_if 1
local.get 2 local.get 1 local.get 1 i32.const 3 i32.rem_u call_indirect (type $u) i32.add local.set 2
local.get 1 i32.const 1 i32.sub local.set 1
br 0
end end
local.get 2 i64.extend_i32_u)
(func (export "table") (param i64) (result i64)
block block block
local.get 0 i32.wrap_i64 br_table 0 1 2
end i64.const 10 return
end i64.const 20 return
end i64.const 30)
(func $fib (param i32) (result i32)
local.get 0 i32.const 2 i32.lt_u if (result i32) local.get 0 else
local.get 0 i32.const 1 i32.sub call $fib local.get 0 i32.const 2 i32.sub call $fib i32.add end)
(func (export "fib") (param i64) (result i64) local.get 0 i32.wrap_i64 call $fib i64.extend_i32_u)
(func (export "mem") (param i64) (result i64)
i32.const 16 i32.const 7 local.get 0 i32.wrap_i64 memory.fill
i32.const 4096 i32.const 16 local.get 0 i32.wrap_i64 memory.copy
local.get 0 i32.wrap_i64 memory.grow drop
memory.size i64.extend_i32_u
i32.const 4100 i64.load8_u offset=0 i64.add)
(func $deep (param i32) (result i32)
local.get 0 i32.eqz if (result i32) i32.const 0 else local.get 0 i32.const 1 i32.sub call $deep i32.const 1 i32.add end)
(func (export "deep") (param i64) (result i64) local.get 0 i32.wrap_i64 call $deep i64.extend_i32_u)
(func (export "multi") (param i64) (result i64)
local.get 0 i64.const 1 block (param i64 i64) (result i64) i64.add end
i64.const 3 loop (param i64 i64) (result i64) i64.mul end))
"#;
fn fuel_of(engine: &dyn Engine, name: &str, arg: u64) -> (Result<Vec<u64>, Trap>, i64) {
let mut i = engine.instantiate().unwrap();
i.set_fuel(100_000_000);
i.set_stack(0);
let r = i.call(name, &[arg]);
(r, 100_000_000 - i.fuel())
}
#[test]
fn the_fuel_is_the_same_on_every_engine_and_is_the_native_engine_s_own_count_but_for_growth() {
let m = wat::parse_str(PROGRAMS).unwrap();
let prepared = meter::prepare(&m).unwrap();
let own = Interpreter::new(&m, Metering::Own).unwrap();
let via = Interpreter::new(&prepared, Metering::Module).unwrap();
// `mem(4)` asks a memory of one page and at most four for four more: the growth fails.
let calls: Vec<(&str, u64)> = vec![("loop", 0), ("loop", 1), ("loop", 50), ("table", 0), ("table", 1), ("table", 2), ("table", 9), ("fib", 0), ("fib", 15), ("mem", 0), ("mem", 1), ("mem", 3), ("mem", 4), ("deep", 10), ("deep", 1000), ("multi", 4)];
for (name, arg) in &calls {
let a = fuel_of(&own, name, *arg);
let b = fuel_of(&via, name, *arg);
assert_eq!(a, b, "{name}({arg}): the interpreter, original and prepared");
assert!(a.0.is_ok(), "{name}({arg}): {:?}", a.0);
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
{
let native = wai_wasmq::engine::Native::new(&prepared).unwrap();
assert_eq!(a, fuel_of(&native, name, *arg), "{name}({arg}): the native engine");
let theirs = wai_wasmq::engine::native_fuel(&m, &[(name, vec![*arg])]).unwrap();
// The profile charges `memory.grow` PAGE_FUEL for each page it asks for, whether
// or not it gets them (§4.2); the engine's own meter one for each page it gets.
let unmet = if *name == "mem" { meter::PAGE_FUEL * arg - if *arg < 4 { *arg } else { 0 } } else { 0 };
assert_eq!((a.0.clone().unwrap(), a.1 as u64 - unmet), theirs[0].clone(), "{name}({arg}): the engine's own meter on the original module");
}
}
}
#[test]
fn a_run_that_runs_out_stops_at_the_same_charge_everywhere() {
let m = wat::parse_str(PROGRAMS).unwrap();
let prepared = meter::prepare(&m).unwrap();
let own = Interpreter::new(&m, Metering::Own).unwrap();
let via = Interpreter::new(&prepared, Metering::Module).unwrap();
for budget in [0i64, 1, 2, 5, 17, 100, 1234, 5000] {
for (name, arg) in [("loop", 30u64), ("fib", 12), ("mem", 3), ("deep", 200)] {
let go = |e: &dyn Engine| {
let mut i = e.instantiate().unwrap();
i.set_fuel(budget);
i.set_stack(0);
let r = i.call(name, &[arg]);
(r, i.fuel())
};
let a = go(&own);
assert_eq!(a, go(&via), "{name}({arg}) at {budget}");
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
assert_eq!(a, go(&wai_wasmq::engine::Native::new(&prepared).unwrap()), "{name}({arg}) at {budget}, native");
}
}
}
/// The frame weights of a module's functions (§4.3).
fn weights(m: &[u8]) -> Vec<u64> {
wai_wasmq::profile::check(m).unwrap().weights.into_iter().map(u64::from).collect()
}
#[test]
fn the_stack_ends_at_the_same_call_on_every_engine() {
let m = wat::parse_str(PROGRAMS).unwrap();
let prepared = meter::prepare(&m).unwrap();
// `deep(n)` suspends its own frame and `n` frames of `$deep`, functions 8 and 7.
let w = weights(&m);
let most = (wai_wasmq::profile::MAX_STACK as u64 - w[8]) / w[7];
assert!(most > 10_000, "a small function recurses {most} deep");
for (arg, ok) in [(most, true), (most + 1, false), (most * 2, false)] {
let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "deep", arg);
assert_eq!(own.0.is_ok(), ok, "deep({arg}): {:?}", own.0);
if !ok {
assert_eq!(own.0, Err(Trap::Stack));
}
assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "deep", arg));
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "deep", arg));
}
}
/// The largest of `build(n)`, `n` below `past`, that the profile admits.
fn largest_admitted(past: usize, build: impl Fn(usize) -> Vec<u8>) -> (usize, Vec<u8>) {
let ok = |n: usize| wai_wasmq::profile::check(&build(n)).is_ok();
let (mut lo, mut hi) = (1, past);
assert!(ok(lo) && !ok(hi), "the profile admits {past}");
while hi - lo > 1 {
let mid = (lo + hi) / 2;
if ok(mid) { lo = mid } else { hi = mid }
}
(lo, build(lo))
}
/// Modules made to keep as much in a native frame as their bytes allow, each recursing
/// without end through its export `f`, with what each is: each at the largest body the
/// profile takes.
fn heavy_frames() -> Vec<(String, Vec<u8>)> {
let wasm = |wat: String| wat::parse_str(&wat).unwrap();
let mut out = Vec::new();
// Locals read from memory the callee goes on to write, all read again after the call:
// a compiler can neither read them again nor compute them there.
for ty in ["i64", "f64"] {
let (n, m) = largest_admitted(4000, |n| {
let mut body = String::from("i32.const 0 i32.const 0 i64.load i64.const 1 i64.add i64.store ");
for i in 0..n {
body.push_str(&format!("i32.const {} {ty}.load local.set {} ", 8 * (i % 1000), i + 1));
}
body.push_str("local.get 0 call $f drop ");
for i in 1..=n {
body.push_str(&format!("local.get {i} {} ", if ty == "i64" { "local.get 0 i64.add local.set 0" } else { "f64.sqrt drop" }));
}
body.push_str("local.get 0");
wasm(format!("(module (memory 1 1) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", format!("{ty} ").repeat(n)))
});
out.push((format!("{n} {ty} locals held across a call"), m));
}
// The review's module: forty parameters read at the end, and a new value held across
// two dozen calls at every step.
let (steps, m) = largest_admitted(8000, |steps| {
let mut body = String::from("local.get 0 i64.const 1 i64.add ");
for p in 1..40 {
body.push_str(&format!("local.get {p} "));
}
body.push_str("call $f drop local.get 0 i64.const 99999999 i64.eq if ");
for k in 0..steps {
body.push_str(&format!("i32.const 0 i64.load local.set {} call $g local.get {} local.get 64 i64.add local.set 64 ", 40 + k % 24, 40 + (k + 1) % 24));
}
body.push_str("end local.get 64 ");
for p in 0..40 {
body.push_str(&format!("local.get {p} i64.add "));
}
let consts: String = (1..40).map(|p| format!("i64.const {p} ")).collect();
wasm(format!(
"(module (memory 1 1) (func $g i32.const 0 i32.const 0 i64.load i64.const 1 i64.add i64.store) (func $f (param {}) (result i64) (local {}) {body}) (func (export \"f\") (param i64) (result i64) local.get 0 {consts} call $f))",
"i64 ".repeat(40),
"i64 ".repeat(25)
))
});
out.push((format!("a value held across two dozen calls at each of {steps} steps"), m));
// The cheapest value there is to make and keep: the memory's size, two bytes, held
// across calls in rotating locals.
let (steps, m) = largest_admitted(15_000, |steps| {
let mut body = String::from("local.get 0 call $f drop ");
for k in 0..steps {
body.push_str(&format!("memory.size local.set {} local.get {} call $h ", 1 + k % 24, 1 + (k + 1) % 24));
}
body.push_str("local.get 0");
wasm(format!("(module (memory 1 2) (func $h (param i32) local.get 0 memory.grow drop) (func $f (export \"f\") (param i64) (result i64) (local {}) {body}))", "i32 ".repeat(24)))
});
out.push((format!("the memory's size held across two dozen calls at each of {steps} steps"), m));
// Results a callee leaves on the caller's operand stack, held there across the
// recursive call: fifteen calls of 256 results each, two bytes apiece, and 3840 values
// a frame (§4.3 counts the operands a function's body can hold, not only its bytes).
let ps = "call $p ".repeat(15);
let cs = "call $c ".repeat(15);
out.push((
"3840 results of calls held across a call".to_string(),
wasm(format!(
"(module (func $p (result {0}) {1}) (func $c (param {0})) (func $f (export \"f\") (param i64) (result i64) {ps} local.get 0 i64.const 1 i64.add call $f drop {cs} local.get 0))",
"i64 ".repeat(256),
"i64.const 7 ".repeat(256)
)),
));
// As many arguments and results as a function type takes, passed straight through.
let all: String = (0..256).map(|p| format!("local.get {p} ")).collect();
out.push((
"256 arguments and 256 results at every call".to_string(),
wasm(format!(
"(module (func $p (param {0}) (result {0}) {all} call $p) (func (export \"f\") (param i64) (result i64) {1} call $p {2}))",
"i64 ".repeat(256),
"local.get 0 ".repeat(256),
"drop ".repeat(255)
)),
));
// The smallest frame there is.
out.push(("a function that only calls itself".to_string(), wasm("(module (func $f (export \"f\") (param i64) (result i64) local.get 0 call $f))".to_string())));
out
}
#[test]
fn a_native_frame_takes_at_most_eight_bytes_for_each_unit_of_its_weight() {
for (what, m) in heavy_frames() {
let prepared = meter::prepare(&m).unwrap();
// The recursion has no end of its own: it stops where the stack's weight does.
let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", 0);
assert_eq!(own.0, Err(Trap::Stack), "{what}");
assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", 0), "{what}: the interpreter on the prepared module");
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
{
// The frames suspended at calls weigh at most MAX_STACK, and one more frame
// runs on top of them. At eight bytes a unit, and 64 KiB for the engine's own
// entry, that is all the stack this engine is given here: half of what
// `Native::new` gives it.
let heaviest = *weights(&m).iter().max().unwrap() as usize;
let bytes = 8 * (wai_wasmq::profile::MAX_STACK as usize + heaviest) + (64 << 10);
let native = fuel_of(&wai_wasmq::engine::Native::with_stack(&prepared, bytes).unwrap(), "f", 0);
assert_eq!(own, native, "{what}: the native engine with {bytes} bytes of stack, where the frames weigh up to {heaviest}");
}
}
// This test can fail: the frames are real. At one byte for each unit of weight, an
// eighth of that stack, the module of 256 arguments stops at the engine's limit and
// not at the profile's.
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
{
let (what, m) = heavy_frames().into_iter().find(|(what, _)| what.starts_with("256 arguments")).unwrap();
let heaviest = *weights(&m).iter().max().unwrap() as usize;
let small = fuel_of(&wai_wasmq::engine::Native::with_stack(&meter::prepare(&m).unwrap(), wai_wasmq::profile::MAX_STACK as usize + heaviest + (64 << 10)).unwrap(), "f", 0);
assert!(matches!(small.0, Err(Trap::Host(_))), "{what}: {:?}", small.0);
}
}
/// A module as large as the profile takes, its items as many as fit beside the largest body
/// there is, all calls: what preparing grows most.
fn at_the_limits() -> Vec<u8> {
use wai_wasmq::profile::{MAX_BODY_BYTES, MAX_EXPORTS, MAX_MODULE_BYTES, MAX_NAME_BYTES};
let leb = |mut v: u64| {
let mut o = Vec::new();
loop {
let b = (v & 0x7f) as u8;
v >>= 7;
if v == 0 {
o.push(b);
return o;
}
o.push(b | 0x80);
}
};
let sec = |id: u8, body: Vec<u8>| {
let mut s = vec![id];
s.extend(leb(body.len() as u64));
s.extend(body);
s
};
let n: u64 = 1 << 13;
// Types: `() -> ()`, and `(i64) -> i64` last.
let mut types = leb(n);
for _ in 0..n - 1 {
types.extend_from_slice(&[0x60, 0x00, 0x00]);
}
types.extend_from_slice(&[0x60, 0x01, 0x7e, 0x01, 0x7e]);
// Functions: the big one, `f` of `(i64) -> i64` that calls it, and empty ones.
let mut funcs = leb(n);
funcs.extend(leb(0));
funcs.extend(leb(n - 1));
funcs.extend(std::iter::repeat_n(0u8, (n - 2) as usize));
let globals = {
let mut g = leb(n);
for _ in 0..n {
g.extend_from_slice(&[0x7f, 0x00, 0x41, 0x00, 0x0b]);
}
g
};
// Exports: `f`, `memory`, and names of the longest length up to the most exports.
let mut exports = leb(u64::from(MAX_EXPORTS));
exports.extend_from_slice(&[0x01, b'f', 0x00]);
exports.extend(leb(1));
exports.extend_from_slice(&[0x06]);
exports.extend_from_slice(b"memory");
exports.extend_from_slice(&[0x02, 0x00]);
for k in 0..u64::from(MAX_EXPORTS) - 2 {
let name = format!("{k:0>width$}", width = MAX_NAME_BYTES);
exports.extend(leb(name.len() as u64));
exports.extend_from_slice(name.as_bytes());
exports.extend_from_slice(&[0x00]);
exports.extend(leb(2 + k % (n - 2)));
}
// A table of the most entries, one segment of every function.
let table = vec![0x01, 0x70, 0x01, 0x80, 0x80, 0x04, 0x80, 0x80, 0x04];
let mut elems = vec![0x01, 0x00, 0x41, 0x00, 0x0b];
elems.extend(leb(n));
for k in 0..n {
elems.extend(leb(k));
}
// A memory of eight pages that may grow to the most.
let memory = vec![0x01, 0x01, 0x08, 0x80, 0x20];
// The body: calls of an empty function up to the most bytes a body takes.
let mut big = vec![0x00];
while (big.len() as u64) + 3 + 3 <= MAX_BODY_BYTES {
big.extend_from_slice(&[0x10, 0x02]);
}
big.push(0x0b);
let mut code = leb(n);
code.extend(leb(big.len() as u64));
code.extend_from_slice(&big);
let f = [0x00, 0x10, 0x00, 0x20, 0x00, 0x0b];
code.extend(leb(f.len() as u64));
code.extend_from_slice(&f);
for _ in 0..n - 2 {
code.extend_from_slice(&[0x02, 0x00, 0x0b]);
}
let mut m = b"\0asm\x01\0\0\0".to_vec();
for s in [sec(1, types), sec(3, funcs), sec(4, table), sec(5, memory), sec(6, globals), sec(7, exports), sec(9, elems), sec(10, code)] {
m.extend(s);
}
// The rest of the bytes a module takes, as data in its first pages.
let room = MAX_MODULE_BYTES - m.len() - 16;
let mut data = vec![0x01, 0x00, 0x41, 0x00, 0x0b];
data.extend(leb(room as u64));
data.extend(std::iter::repeat_n(0x5au8, room));
m.extend(sec(11, data));
m
}
#[test]
fn a_module_at_the_profile_s_limits_prepares_compiles_and_runs_the_same_on_every_engine() {
let m = at_the_limits();
assert!(m.len() > wai_wasmq::profile::MAX_MODULE_BYTES - 64, "{} bytes", m.len());
wai_wasmq::profile::check(&m).unwrap();
let prepared = meter::prepare(&m).unwrap();
let own = fuel_of(&Interpreter::new(&m, Metering::Own).unwrap(), "f", 5);
assert!(own.0.is_ok(), "{:?}", own.0);
assert_eq!(own, fuel_of(&Interpreter::new(&prepared, Metering::Module).unwrap(), "f", 5), "the interpreter on the prepared module");
// The native engine compiles the largest prepared body there is, and runs it alike.
#[cfg(all(feature = "wasmtime", not(target_arch = "wasm32")))]
assert_eq!(own, fuel_of(&wai_wasmq::engine::Native::new(&prepared).unwrap(), "f", 5), "the native engine");
}