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
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]>
529 lines
26 KiB
Rust
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");
|
|
}
|