//! 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 { 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)> { use T::*; let mut v: Vec<(&'static str, Vec, 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, Vec<(String, Vec)>) { 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 = 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> { 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)]) -> Vec<(String, Result, 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, 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 { 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) -> (usize, Vec) { 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)> { 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 { 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| { 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"); }