Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// `dyn Trait` dispatch: the vtables live in `.rodata` as arrays of funcref
// table indices, and each method call loads its slot and dispatches via
// `call_indirect`. The concrete impl is picked through a runtime-indexed array
// of trait-object references so LLVM cannot devirtualize, and two methods per
// trait exercise two distinct vtable slots per object.

trait Mix {
fn scale(&self, x: u32) -> u32;
fn fold(&self, x: u32, y: u32) -> u32;
}

struct Affine(u32);
struct Xor(u32);

impl Mix for Affine {
#[inline(never)]
fn scale(&self, x: u32) -> u32 {
x.wrapping_mul(self.0 | 1).wrapping_add(0x9e37)
}

#[inline(never)]
fn fold(&self, x: u32, y: u32) -> u32 {
x.rotate_left(self.0 & 31) ^ y
}
}

impl Mix for Xor {
#[inline(never)]
fn scale(&self, x: u32) -> u32 {
(x ^ self.0).wrapping_sub(x >> 5)
}

#[inline(never)]
fn fold(&self, x: u32, y: u32) -> u32 {
(x | y).wrapping_mul(2654435761).wrapping_add(self.0)
}
}

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let a = Affine(input2);
let b = Xor(input1.wrapping_add(input2));
// Runtime-indexed fat-pointer loads defeat devirtualization, the same way
// the fn-pointer array idiom does.
let objs: [&dyn Mix; 2] = [&a, &b];
let first = objs[(input1 & 1) as usize];
let second = objs[((input2 >> 2) & 1) as usize];
let scaled = first.scale(input1);
second.fold(scaled, input2).wrapping_add(first.fold(input2, input1))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Function pointers as first-class VALUES: returned from and passed to
// `#[inline(never)]` helpers, mutated across loop iterations (state-machine
// style), coerced from a non-capturing closure, and compared with `==`. At the
// Wasm level a fn pointer is its funcref-table index (an i32), so all of this
// exercises table-index data flow between functions, across loop-carried
// locals, and through an integer comparison — while every actual dispatch
// stays a runtime-indexed `call_indirect`.

type Op = fn(u32, u32) -> u32;

#[inline(never)]
fn op_add(a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}

#[inline(never)]
fn op_shear(a: u32, b: u32) -> u32 {
(a ^ b).rotate_left(11)
}

#[inline(never)]
fn op_scale(a: u32, b: u32) -> u32 {
a.wrapping_mul(b | 1)
}

// Returns a fn pointer picked by runtime data: the caller receives a table
// index it cannot devirtualize through the noinline boundary.
#[inline(never)]
fn pick(sel: u32) -> Op {
match sel % 3 {
0 => op_add,
1 => op_shear,
_ => op_scale,
}
}

// Takes a fn pointer as a parameter and dispatches through it.
#[inline(never)]
fn apply(f: Op, a: u32, b: u32) -> u32 {
f(a, b)
}

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let mut f = pick(input1);
let g = pick(input2 >> 3);
// fn-pointer equality compares funcref-table indices in wasm and host
// addresses natively; both are injective over these distinct functions.
let same = (f == g) as u32;
// Non-capturing closure coerced to `fn`: an anonymous table entry.
let h: Op = |a, b| (a | 3).wrapping_sub(b >> 2);
let mut acc = apply(h, input2, input1);
// Loop-carried fn-pointer state machine: f changes each iteration based
// on data computed through the previous pointer.
let mut i = 0u32;
while i < 4 {
acc = apply(f, acc, input1.rotate_left(i));
f = pick(acc ^ i);
i += 1;
}
acc.wrapping_add(g(acc, input2)).wrapping_add(same)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Chained indirect dispatch: the runtime-selected stage functions themselves
// dispatch through a second fn-pointer array, so a `dynexec` callee performs
// another `dynexec` (nested dispatch frames on the VM). The outer dispatch
// also sits inside a loop, and one arm of a conditional dispatches while the
// other computes directly — call_indirect in every control-flow position.

type Leaf = fn(u32) -> u32;

#[inline(never)]
fn leaf_gray(x: u32) -> u32 {
x ^ (x >> 1)
}

#[inline(never)]
fn leaf_spread(x: u32) -> u32 {
x.wrapping_mul(0x8100_0101).rotate_right(3)
}

static LEAVES: [Leaf; 2] = [leaf_gray, leaf_spread];

// Each stage dispatches through LEAVES with a runtime index: an indirect
// callee that itself calls indirectly.
#[inline(never)]
fn stage_mask(x: u32) -> u32 {
LEAVES[(x & 1) as usize](x).wrapping_add(0x5a5a)
}

#[inline(never)]
fn stage_swap(x: u32) -> u32 {
LEAVES[((x >> 2) & 1) as usize](x.swap_bytes())
}

type Stage = fn(u32) -> u32;
static STAGES: [Stage; 2] = [stage_mask, stage_swap];

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let mut acc = input1;
// Indirect dispatch inside a loop, index depending on the loop-carried value
let mut k = 0u32;
while k < 3 {
acc = STAGES[((acc ^ k) & 1) as usize](acc.wrapping_add(input2));
k += 1;
}
// Indirect dispatch in one branch arm only
if input2 & 4 == 0 {
acc = LEAVES[(acc & 1) as usize](acc);
} else {
acc = acc.wrapping_mul(3).wrapping_sub(input1);
}
acc
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// A user function deliberately named `__indirect_function_table_0` — the exact
// symbol the frontend generates for the lowered funcref table of table 0. Every
// module symbol is a producer-controlled string, so the table-lowering probes
// the symbol table and bumps a counter until the generated name is free; this
// case forces that collision-rename path while still dispatching indirectly.

#[unsafe(no_mangle)]
pub extern "C" fn __indirect_function_table_0(x: u32) -> u32 {
x.wrapping_mul(0x0101_0101).rotate_left(7)
}

#[inline(never)]
fn op_gray(a: u32, b: u32) -> u32 {
(a ^ (a >> 1)).wrapping_add(b)
}

#[inline(never)]
fn op_lerp(a: u32, b: u32) -> u32 {
a.wrapping_add(b.wrapping_sub(a) >> 3)
}

// Runtime-indexed fn-pointer load: survives as `call_indirect`, which lazily
// lowers the funcref table and hits the reserved-name collision.
static OPS: [fn(u32, u32) -> u32; 2] = [op_gray, op_lerp];

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let f = OPS[(input1 & 1) as usize];
let mixed = f(input1, input2);
mixed.wrapping_add(__indirect_function_table_0(input2))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Two function-pointer tables with DIFFERENT function types. All address-taken
// functions land in the single wasm funcref table, but the two dispatch sites
// carry distinct signature type indices — so the lowered `builtin.function_table`
// holds entries with two different signature tags, and each `hir.exec_indirect`
// call site must skip (tag-filter) the other signature's entries while the
// runtime tag check accepts only its own.

#[inline(never)]
fn un_not(a: u32) -> u32 {
!a
}

#[inline(never)]
fn un_rev(a: u32) -> u32 {
a.swap_bytes().rotate_left(9)
}

#[inline(never)]
fn wi_fold(a: u64, b: u32) -> u64 {
a.wrapping_mul(0x9e3779b97f4a7c15).wrapping_add(b as u64)
}

#[inline(never)]
fn wi_shear(a: u64, b: u32) -> u64 {
(a ^ ((b as u64) << 17)).rotate_right(23)
}

// Runtime-indexed loads of fn pointers from static arrays are not
// devirtualized by LLVM without PGO, so both dispatches survive as
// `call_indirect` with distinct type indices.
static UNARY: [fn(u32) -> u32; 2] = [un_not, un_rev];
static WIDE: [fn(u64, u32) -> u64; 2] = [wi_fold, wi_shear];

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let u = UNARY[(input1 & 1) as usize];
let w = WIDE[((input2 >> 1) & 1) as usize];
let narrow = u(input1.wrapping_add(input2));
let wide = w(((input1 as u64) << 32) | input2 as u64, narrow);
(wide as u32).wrapping_add((wide >> 32) as u32)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// The widest indirect signature the lowering accepts: 7 u64 parameters are 14
// stack felts, plus the table index = 15 of Miden's 16-element operand-stack
// window (16 argument felts + the index would be diagnosed at translation).
// Dispatching it exercises `dynexec` with a full argument window and u64
// (two-felt) values crossing the dispatch boundary in both directions.

type Wide = fn(u64, u64, u64, u64, u64, u64, u64) -> u64;

#[inline(never)]
fn w_fold(a: u64, b: u64, c: u64, d: u64, e: u64, f: u64, g: u64) -> u64 {
a.wrapping_add(b)
.wrapping_mul(c | 1)
.wrapping_sub(d)
.rotate_left((e & 63) as u32)
^ f.wrapping_add(g)
}

#[inline(never)]
fn w_zip(a: u64, b: u64, c: u64, d: u64, e: u64, f: u64, g: u64) -> u64 {
(a ^ b.rotate_right(17))
.wrapping_add(c.wrapping_mul(c))
.wrapping_add(d >> 3)
.wrapping_add(e << 5)
.wrapping_add(f ^ g.swap_bytes())
}

static WIDES: [Wide; 2] = [w_fold, w_zip];

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let x = ((input1 as u64) << 32) | input2 as u64;
let y = ((input2 as u64) << 32) | input1 as u64;
let f = WIDES[(input1 & 1) as usize];
let r = f(
x,
y,
x.wrapping_add(y),
x ^ 0x00ff_00ff_00ff_00ff,
y.wrapping_mul(3),
x.rotate_left(9),
y ^ x,
);
(r as u32).wrapping_add((r >> 32) as u32)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Local2Reg gap shapes. Every wasm function parameter gets an unconditional
// `hir.store_local` at entry (frontend/wasm `declare_parameters`), so:
// (1) `lsh_unused`'s ignored second parameter is a stored-but-never-loaded
// local, reaching the dead-store-erasure arm of the Local2Reg pass
// (`#[no_mangle]` gives the helper external linkage so LLVM's dead-arg
// elimination cannot drop the parameter; `#[inline(never)]` keeps the
// call);
// (2) `lsh_konst` has no parameters and no wasm locals at all, reaching the
// pass's `locals.is_empty()` early return;
// (3) `lsh_pick` takes a by-value array, which Rust passes indirectly — the
// incoming pointer travels through a single-use local (promotable), and
// with debug info the aggregate's `di.debug_declare` references that
// local, reaching the declare-conversion path of
// `convert_debug_references_for_local`.

#[inline(never)]
#[unsafe(no_mangle)]
extern "C" fn lsh_unused(a: u32, _dead: u32) -> u32 {
a.wrapping_mul(2654435761).rotate_left(5)
}

#[inline(never)]
#[unsafe(no_mangle)]
extern "C" fn lsh_konst() -> u32 {
40507
}

#[inline(never)]
#[unsafe(no_mangle)]
fn lsh_pick(arr: [u32; 4], i: u32) -> u32 {
arr[(i & 3) as usize]
}

#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let arr = [input1, input2, input1 ^ input2, input1.wrapping_add(input2)];
let a = lsh_unused(input1, input2);
let b = lsh_konst();
let c = lsh_pick(arr, input2 >> 7);
a ^ b.wrapping_add(c)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Minimal reproducer for the arity-2 operand-scheduler NoSolution panic that
// does NOT involve invalid IR: the ten-count variant of case_spill_switch.
// Ten shared masked rotate-count bands keep 15 felts live at an
// `arith.rotl` whose count operand is Copy-constrained; the only tactic the
// solver tries for binary ops (TwoArgs) emits dup-then-movup, which needs a
// 17-felt stack access, so the solution is rejected by the MASM 16-felt
// window check and no fallback tactic exists. See the `rotl_window` test for
// the full finding notes; the six-count `spill_switch` twin passes.
#[unsafe(no_mangle)]
pub extern "C" fn entrypoint(input1: u32, input2: u32) -> u32 {
let m = (input1 | 1) as u64;
let n = ((input2 ^ 0x9e37_79b9) as u64) | 2;
// First uses of the ten shared counts.
let mut acc = (m ^ n) | 1;
acc ^= m.rotate_left(1);
acc = acc.wrapping_add(n.rotate_left(3));
acc ^= m.rotate_left(5);
acc = acc.wrapping_sub(n.rotate_left(7));
acc ^= m.rotate_left(9);
acc = acc.wrapping_add(n.rotate_left(11));
acc ^= m.rotate_left(13);
acc = acc.wrapping_sub(n.rotate_left(15));
acc ^= m.rotate_left(17);
acc = acc.wrapping_add(n.rotate_left(19));
let iters = (input2 % 97) + 3;
let mut i: u32 = 0;
while i < iters {
// Dense 6-way dispatch with structurally-distinct arms.
acc = match (acc as u32) & 7 {
0 => (acc ^ 0x9e37_79b9).wrapping_mul(129),
1 => acc.rotate_left(2).wrapping_add(0x85eb_ca6b),
2 => acc.wrapping_sub(0xc2b2_ae35) ^ (acc >> 5),
3 => acc.rotate_left(6) ^ acc.wrapping_mul(65),
4 => acc.wrapping_add(acc.rotate_left(10)) | 1,
_ => acc ^ (acc << 3) ^ 0x27d4_eb2f,
};
i = i.wrapping_add(1);
}
// Post-loop partners of the ten crossing counts.
let mut r = acc;
r ^= acc.rotate_left(1);
r = r.wrapping_add(acc.rotate_left(3));
r ^= acc.rotate_left(5);
r = r.wrapping_sub(acc.rotate_left(7));
r ^= acc.rotate_left(9);
r = r.wrapping_add(acc.rotate_left(11));
r ^= acc.rotate_left(13);
r = r.wrapping_sub(acc.rotate_left(15));
r ^= acc.rotate_left(17);
r = r.wrapping_add(acc.rotate_left(19));
(r as u32) ^ ((r >> 32) as u32)
}
Loading
Loading