Squashed from work; the fine-grained history is under tag archive/work-2026-08-29: - mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix - mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo - work: land the sources the last commits reference - route: the assistant icon the committed UI references - fast_inflate: the benches name their dev-deps - gif/weezl: drop the vendored benches nobody can run - weezl: the decode tests generate their own LZW fixture
482 lines
17 KiB
Rust
482 lines
17 KiB
Rust
//! Layer: stitch-op.
|
|
//!
|
|
//! Per-opcode tests for the NONSTANDARD float math opcodes (prefix 0xE0,
|
|
//! opt-in via `Extensions::ext_math`): scalar f32/f64 and packed f32x4
|
|
//! sin, cos, tan, asin, acos, atan, exp, ln, atan2, pow, rmin, rmax, rem.
|
|
//!
|
|
//! Every op is compared bit-for-bit against the host Rust function it is
|
|
//! specified to match, over an edge corpus including NaN, infinities,
|
|
//! signed zeros and denormals. A wrong lane here points at exactly one
|
|
//! handler in exec.rs/simd.rs.
|
|
|
|
use makepad_stitch::{Engine, Extensions, Linker, Module, Store, V128, Val};
|
|
|
|
// A tiny Wasm binary emitter, just enough for one-function modules.
|
|
|
|
fn leb(mut val: u32, out: &mut Vec<u8>) {
|
|
loop {
|
|
let byte = (val & 0x7F) as u8;
|
|
val >>= 7;
|
|
if val == 0 {
|
|
out.push(byte);
|
|
break;
|
|
}
|
|
out.push(byte | 0x80);
|
|
}
|
|
}
|
|
|
|
fn section(id: u8, payload: &[u8], out: &mut Vec<u8>) {
|
|
out.push(id);
|
|
leb(payload.len() as u32, out);
|
|
out.extend_from_slice(payload);
|
|
}
|
|
|
|
/// Builds a module with a single exported function "f" with the given
|
|
/// param/result types (value type bytes) and raw body code (without the
|
|
/// trailing `end`).
|
|
fn build_module(params: &[u8], results: &[u8], body: &[u8]) -> Vec<u8> {
|
|
let mut out = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
|
|
// Type section
|
|
let mut payload = Vec::new();
|
|
leb(1, &mut payload);
|
|
payload.push(0x60);
|
|
leb(params.len() as u32, &mut payload);
|
|
payload.extend_from_slice(params);
|
|
leb(results.len() as u32, &mut payload);
|
|
payload.extend_from_slice(results);
|
|
section(1, &payload, &mut out);
|
|
// Function section
|
|
section(3, &[1, 0], &mut out);
|
|
// Export section
|
|
let mut payload = Vec::new();
|
|
leb(1, &mut payload);
|
|
leb(1, &mut payload);
|
|
payload.push(b'f');
|
|
payload.push(0x00); // func export
|
|
leb(0, &mut payload);
|
|
section(7, &payload, &mut out);
|
|
// Code section
|
|
let mut func = Vec::new();
|
|
leb(0, &mut func); // no locals
|
|
func.extend_from_slice(body);
|
|
func.push(0x0B); // end
|
|
let mut payload = Vec::new();
|
|
leb(1, &mut payload);
|
|
leb(func.len() as u32, &mut payload);
|
|
payload.extend_from_slice(&func);
|
|
section(10, &payload, &mut out);
|
|
out
|
|
}
|
|
|
|
struct Runner {
|
|
store: Store,
|
|
instance: makepad_stitch::Instance,
|
|
}
|
|
|
|
impl Runner {
|
|
fn new(bytes: &[u8]) -> Runner {
|
|
let engine = Engine::new_with_extensions(Extensions { ext_math: true });
|
|
let mut store = Store::new(engine.clone());
|
|
let module = Module::new(&engine, bytes).unwrap();
|
|
let instance = Linker::new().instantiate(&mut store, &module).unwrap();
|
|
Runner { store, instance }
|
|
}
|
|
|
|
fn call(&mut self, args: &[Val], results: &mut [Val]) {
|
|
let func = self.instance.exported_func("f").unwrap();
|
|
func.call(&mut self.store, args, results).unwrap();
|
|
}
|
|
}
|
|
|
|
const F32_EDGES: &[f32] = &[
|
|
0.0,
|
|
-0.0,
|
|
1.0,
|
|
-1.0,
|
|
0.5,
|
|
-0.75,
|
|
2.5,
|
|
-7.25,
|
|
std::f32::consts::PI,
|
|
-std::f32::consts::PI,
|
|
1.0e-40, // denormal
|
|
-1.0e-40, // negative denormal
|
|
f32::MIN_POSITIVE,
|
|
f32::MAX,
|
|
f32::MIN,
|
|
1.0e10,
|
|
f32::INFINITY,
|
|
f32::NEG_INFINITY,
|
|
f32::NAN,
|
|
100.5,
|
|
];
|
|
|
|
const F64_EDGES: &[f64] = &[
|
|
0.0,
|
|
-0.0,
|
|
1.0,
|
|
-1.0,
|
|
0.5,
|
|
-0.75,
|
|
2.5,
|
|
-7.25,
|
|
std::f64::consts::PI,
|
|
-std::f64::consts::PI,
|
|
1.0e-310, // denormal
|
|
-1.0e-310,
|
|
f64::MIN_POSITIVE,
|
|
f64::MAX,
|
|
f64::MIN,
|
|
1.0e10,
|
|
f64::INFINITY,
|
|
f64::NEG_INFINITY,
|
|
f64::NAN,
|
|
100.5,
|
|
];
|
|
|
|
// Prefix and subopcodes (see decode_ext_math_instr in code.rs).
|
|
const EXT: u8 = 0xE0;
|
|
|
|
fn scalar_f32_un_op(sub: u8, host: fn(f32) -> f32, name: &str) {
|
|
// Stack-operand variant: f (param f32) (result f32) = op(x)
|
|
let bytes = build_module(&[0x7D], &[0x7D], &[0x20, 0x00, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
// Register-operand variant: op(op(x)) makes the inner result flow
|
|
// through the float register into the outer op.
|
|
let bytes_r = build_module(&[0x7D], &[0x7D], &[0x20, 0x00, EXT, sub, EXT, sub]);
|
|
let mut runner_r = Runner::new(&bytes_r);
|
|
for &x in F32_EDGES {
|
|
let mut results = [Val::F32(0.0)];
|
|
runner.call(&[Val::F32(x)], &mut results);
|
|
let actual = results[0].to_f32().unwrap();
|
|
let expected = host(x);
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_s({x:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
runner_r.call(&[Val::F32(x)], &mut results);
|
|
let actual = results[0].to_f32().unwrap();
|
|
let expected = host(host(x));
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_r({x:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn scalar_f64_un_op(sub: u8, host: fn(f64) -> f64, name: &str) {
|
|
let bytes = build_module(&[0x7C], &[0x7C], &[0x20, 0x00, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
let bytes_r = build_module(&[0x7C], &[0x7C], &[0x20, 0x00, EXT, sub, EXT, sub]);
|
|
let mut runner_r = Runner::new(&bytes_r);
|
|
for &x in F64_EDGES {
|
|
let mut results = [Val::F64(0.0)];
|
|
runner.call(&[Val::F64(x)], &mut results);
|
|
let actual = results[0].to_f64().unwrap();
|
|
let expected = host(x);
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_s({x:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
runner_r.call(&[Val::F64(x)], &mut results);
|
|
let actual = results[0].to_f64().unwrap();
|
|
let expected = host(host(x));
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_r({x:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn scalar_f32_bin_op(sub: u8, host: fn(f32, f32) -> f32, name: &str) {
|
|
// ss variant
|
|
let bytes = build_module(&[0x7D, 0x7D], &[0x7D], &[0x20, 0x00, 0x20, 0x01, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
// rs variant: first operand comes out of the float register.
|
|
let bytes_rs = build_module(
|
|
&[0x7D, 0x7D],
|
|
&[0x7D],
|
|
&[0x20, 0x00, EXT, sub_id_f32_neg_free(), 0x20, 0x01, EXT, sub],
|
|
);
|
|
let mut runner_rs = Runner::new(&bytes_rs);
|
|
// sr variant: second operand comes out of the float register.
|
|
let bytes_sr = build_module(
|
|
&[0x7D, 0x7D],
|
|
&[0x7D],
|
|
&[0x20, 0x00, 0x20, 0x01, EXT, sub_id_f32_neg_free(), EXT, sub],
|
|
);
|
|
let mut runner_sr = Runner::new(&bytes_sr);
|
|
for &a in F32_EDGES {
|
|
for &b in F32_EDGES {
|
|
let mut results = [Val::F32(0.0)];
|
|
runner.call(&[Val::F32(a), Val::F32(b)], &mut results);
|
|
let actual = results[0].to_f32().unwrap();
|
|
let expected = host(a, b);
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_ss({a:?}, {b:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
runner_rs.call(&[Val::F32(a), Val::F32(b)], &mut results);
|
|
let actual = results[0].to_f32().unwrap();
|
|
let expected = host(a.sin(), b);
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_rs({a:?}, {b:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
runner_sr.call(&[Val::F32(a), Val::F32(b)], &mut results);
|
|
let actual = results[0].to_f32().unwrap();
|
|
let expected = host(a, b.sin());
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_sr({a:?}, {b:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The "free" unary op used to force a value into the float register in the
|
|
/// bin-op variant tests: f32.sin (subopcode 0x00).
|
|
fn sub_id_f32_neg_free() -> u8 {
|
|
0x00
|
|
}
|
|
|
|
/// Emits `f32.const val`.
|
|
fn f32_const(val: f32, out: &mut Vec<u8>) {
|
|
out.push(0x43);
|
|
out.extend_from_slice(&val.to_le_bytes());
|
|
}
|
|
|
|
/// Covers the immediate-operand variants (is, ir, si, ri) of a scalar f32
|
|
/// binary op, with 2.5 as the immediate.
|
|
fn scalar_f32_bin_op_imm(sub: u8, host: fn(f32, f32) -> f32, name: &str) {
|
|
const IMM: f32 = 2.5;
|
|
// is: (op (f32.const IMM) (local.get 0))
|
|
let mut body = Vec::new();
|
|
f32_const(IMM, &mut body);
|
|
body.extend_from_slice(&[0x20, 0x00, EXT, sub]);
|
|
let mut runner_is = Runner::new(&build_module(&[0x7D], &[0x7D], &body));
|
|
// si: (op (local.get 0) (f32.const IMM))
|
|
let mut body = Vec::new();
|
|
body.extend_from_slice(&[0x20, 0x00]);
|
|
f32_const(IMM, &mut body);
|
|
body.extend_from_slice(&[EXT, sub]);
|
|
let mut runner_si = Runner::new(&build_module(&[0x7D], &[0x7D], &body));
|
|
// ir: (op (f32.const IMM) (f32.sin (local.get 0))) - second operand in reg
|
|
let mut body = Vec::new();
|
|
f32_const(IMM, &mut body);
|
|
body.extend_from_slice(&[0x20, 0x00, EXT, 0x00, EXT, sub]);
|
|
let mut runner_ir = Runner::new(&build_module(&[0x7D], &[0x7D], &body));
|
|
// ri: (op (f32.sin (local.get 0)) (f32.const IMM)) - first operand in reg
|
|
let mut body = Vec::new();
|
|
body.extend_from_slice(&[0x20, 0x00, EXT, 0x00]);
|
|
f32_const(IMM, &mut body);
|
|
body.extend_from_slice(&[EXT, sub]);
|
|
let mut runner_ri = Runner::new(&build_module(&[0x7D], &[0x7D], &body));
|
|
for &x in F32_EDGES {
|
|
let mut results = [Val::F32(0.0)];
|
|
runner_is.call(&[Val::F32(x)], &mut results);
|
|
assert_eq!(
|
|
results[0].to_f32().unwrap().to_bits(),
|
|
host(IMM, x).to_bits(),
|
|
"{name}_is({IMM:?}, {x:?})"
|
|
);
|
|
runner_si.call(&[Val::F32(x)], &mut results);
|
|
assert_eq!(
|
|
results[0].to_f32().unwrap().to_bits(),
|
|
host(x, IMM).to_bits(),
|
|
"{name}_si({x:?}, {IMM:?})"
|
|
);
|
|
runner_ir.call(&[Val::F32(x)], &mut results);
|
|
assert_eq!(
|
|
results[0].to_f32().unwrap().to_bits(),
|
|
host(IMM, x.sin()).to_bits(),
|
|
"{name}_ir({IMM:?}, sin({x:?}))"
|
|
);
|
|
runner_ri.call(&[Val::F32(x)], &mut results);
|
|
assert_eq!(
|
|
results[0].to_f32().unwrap().to_bits(),
|
|
host(x.sin(), IMM).to_bits(),
|
|
"{name}_ri(sin({x:?}), {IMM:?})"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn scalar_f64_bin_op(sub: u8, host: fn(f64, f64) -> f64, name: &str) {
|
|
let bytes = build_module(&[0x7C, 0x7C], &[0x7C], &[0x20, 0x00, 0x20, 0x01, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
for &a in F64_EDGES {
|
|
for &b in F64_EDGES {
|
|
let mut results = [Val::F64(0.0)];
|
|
runner.call(&[Val::F64(a), Val::F64(b)], &mut results);
|
|
let actual = results[0].to_f64().unwrap();
|
|
let expected = host(a, b);
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}_ss({a:?}, {b:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn packed_un_op(sub: u8, host: fn(f32) -> f32, name: &str) {
|
|
// f (param v128) (result v128)
|
|
let bytes = build_module(&[0x7B], &[0x7B], &[0x20, 0x00, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
for chunk in F32_EDGES.chunks(4) {
|
|
let mut lanes = [0f32; 4];
|
|
lanes[..chunk.len()].copy_from_slice(chunk);
|
|
let mut results = [Val::V128(V128::ZERO)];
|
|
runner.call(&[Val::V128(V128::from_f32x4(lanes))], &mut results);
|
|
let actual = results[0].to_v128().unwrap().to_f32x4();
|
|
for lane in 0..4 {
|
|
let expected = host(lanes[lane]);
|
|
assert_eq!(
|
|
actual[lane].to_bits(),
|
|
expected.to_bits(),
|
|
"{name} lane {lane} of {lanes:?}: got {:?}, want {expected:?}",
|
|
actual[lane]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn packed_bin_op(sub: u8, host: fn(f32, f32) -> f32, name: &str) {
|
|
let bytes = build_module(&[0x7B, 0x7B], &[0x7B], &[0x20, 0x00, 0x20, 0x01, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
for chunk_a in F32_EDGES.chunks(4) {
|
|
for chunk_b in F32_EDGES.chunks(4) {
|
|
let mut a = [0f32; 4];
|
|
a[..chunk_a.len()].copy_from_slice(chunk_a);
|
|
let mut b = [0f32; 4];
|
|
b[..chunk_b.len()].copy_from_slice(chunk_b);
|
|
let mut results = [Val::V128(V128::ZERO)];
|
|
runner.call(
|
|
&[Val::V128(V128::from_f32x4(a)), Val::V128(V128::from_f32x4(b))],
|
|
&mut results,
|
|
);
|
|
let actual = results[0].to_v128().unwrap().to_f32x4();
|
|
for lane in 0..4 {
|
|
let expected = host(a[lane], b[lane]);
|
|
assert_eq!(
|
|
actual[lane].to_bits(),
|
|
expected.to_bits(),
|
|
"{name} lane {lane} of {a:?}, {b:?}: got {:?}, want {expected:?}",
|
|
actual[lane]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_f32_ops() {
|
|
scalar_f32_un_op(0x00, f32::sin, "f32_sin");
|
|
scalar_f32_un_op(0x01, f32::cos, "f32_cos");
|
|
scalar_f32_un_op(0x02, f32::tan, "f32_tan");
|
|
scalar_f32_un_op(0x03, f32::asin, "f32_asin");
|
|
scalar_f32_un_op(0x04, f32::acos, "f32_acos");
|
|
scalar_f32_un_op(0x05, f32::atan, "f32_atan");
|
|
scalar_f32_un_op(0x06, f32::exp, "f32_exp");
|
|
scalar_f32_un_op(0x07, f32::ln, "f32_ln");
|
|
scalar_f32_bin_op(0x08, f32::atan2, "f32_atan2");
|
|
scalar_f32_bin_op(0x09, f32::powf, "f32_pow");
|
|
scalar_f32_bin_op(0x0A, f32::min, "f32_rmin");
|
|
scalar_f32_bin_op(0x0B, f32::max, "f32_rmax");
|
|
scalar_f32_bin_op(0x0C, |a, b| a % b, "f32_rem");
|
|
scalar_f32_bin_op_imm(0x08, f32::atan2, "f32_atan2");
|
|
scalar_f32_bin_op_imm(0x09, f32::powf, "f32_pow");
|
|
scalar_f32_bin_op_imm(0x0A, f32::min, "f32_rmin");
|
|
scalar_f32_bin_op_imm(0x0B, f32::max, "f32_rmax");
|
|
scalar_f32_bin_op_imm(0x0C, |a, b| a % b, "f32_rem");
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_f64_ops() {
|
|
scalar_f64_un_op(0x10, f64::sin, "f64_sin");
|
|
scalar_f64_un_op(0x11, f64::cos, "f64_cos");
|
|
scalar_f64_un_op(0x12, f64::tan, "f64_tan");
|
|
scalar_f64_un_op(0x13, f64::asin, "f64_asin");
|
|
scalar_f64_un_op(0x14, f64::acos, "f64_acos");
|
|
scalar_f64_un_op(0x15, f64::atan, "f64_atan");
|
|
scalar_f64_un_op(0x16, f64::exp, "f64_exp");
|
|
scalar_f64_un_op(0x17, f64::ln, "f64_ln");
|
|
scalar_f64_bin_op(0x18, f64::atan2, "f64_atan2");
|
|
scalar_f64_bin_op(0x19, f64::powf, "f64_pow");
|
|
scalar_f64_bin_op(0x1A, f64::min, "f64_rmin");
|
|
scalar_f64_bin_op(0x1B, f64::max, "f64_rmax");
|
|
scalar_f64_bin_op(0x1C, |a, b| a % b, "f64_rem");
|
|
}
|
|
|
|
#[test]
|
|
fn packed_f32x4_ops() {
|
|
packed_un_op(0x20, f32::sin, "f32x4_sin");
|
|
packed_un_op(0x21, f32::cos, "f32x4_cos");
|
|
packed_un_op(0x22, f32::tan, "f32x4_tan");
|
|
packed_un_op(0x23, f32::asin, "f32x4_asin");
|
|
packed_un_op(0x24, f32::acos, "f32x4_acos");
|
|
packed_un_op(0x25, f32::atan, "f32x4_atan");
|
|
packed_un_op(0x26, f32::exp, "f32x4_exp");
|
|
packed_un_op(0x27, f32::ln, "f32x4_ln");
|
|
packed_bin_op(0x28, f32::atan2, "f32x4_atan2");
|
|
packed_bin_op(0x29, f32::powf, "f32x4_pow");
|
|
packed_bin_op(0x2A, f32::min, "f32x4_rmin");
|
|
packed_bin_op(0x2B, f32::max, "f32x4_rmax");
|
|
packed_bin_op(0x2C, |a, b| a % b, "f32x4_rem");
|
|
}
|
|
|
|
fn packed_dot_op(sub: u8, w: usize, name: &str) {
|
|
let bytes = build_module(&[0x7B, 0x7B], &[0x7D], &[0x20, 0x00, 0x20, 0x01, EXT, sub]);
|
|
let mut runner = Runner::new(&bytes);
|
|
for chunk_a in F32_EDGES.chunks(4) {
|
|
for chunk_b in F32_EDGES.chunks(4) {
|
|
let mut a = [0f32; 4];
|
|
a[..chunk_a.len()].copy_from_slice(chunk_a);
|
|
let mut b = [0f32; 4];
|
|
b[..chunk_b.len()].copy_from_slice(chunk_b);
|
|
let mut results = [Val::F32(0.0)];
|
|
runner.call(
|
|
&[Val::V128(V128::from_f32x4(a)), Val::V128(V128::from_f32x4(b))],
|
|
&mut results,
|
|
);
|
|
let actual = results[0].to_f32().unwrap();
|
|
// Left-associated host reference.
|
|
let mut expected = a[0] * b[0];
|
|
for lane in 1..w {
|
|
expected += a[lane] * b[lane];
|
|
}
|
|
assert_eq!(
|
|
actual.to_bits(),
|
|
expected.to_bits(),
|
|
"{name}({a:?}, {b:?}): got {actual:?}, want {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn packed_dot_reductions() {
|
|
packed_dot_op(0x2D, 2, "f32x4_dot2");
|
|
packed_dot_op(0x2E, 3, "f32x4_dot3");
|
|
packed_dot_op(0x2F, 4, "f32x4_dot4");
|
|
}
|
|
|
|
/// A module using a 0xE0 opcode must be rejected by an engine without
|
|
/// `ext_math` — standard Wasm behavior is unchanged.
|
|
#[test]
|
|
fn ext_math_is_gated() {
|
|
let bytes = build_module(&[0x7D], &[0x7D], &[0x20, 0x00, EXT, 0x00]);
|
|
// With the extension: fine.
|
|
let engine = Engine::new_with_extensions(Extensions { ext_math: true });
|
|
assert!(Module::new(&engine, &bytes).is_ok());
|
|
// Without: "illegal opcode".
|
|
let engine = Engine::new();
|
|
assert!(Module::new(&engine, &bytes).is_err());
|
|
}
|