Context and credit
WasmBounds (Emma Sudo & Keith Winstein, Stanford — paper) applies abstract interpretation to elide Wasm memory bounds checks, and reports a measured 1.21× speedup on a dot-product microbenchmark. It is an early-stage project (their words), but it does two things we have not: it states the motivation crisply and it measures the payoff end to end.
Their motivation section is the clearest statement I've seen of why this matters for our embedded target: hardware guard pages need virtual memory, so on no-MMU embedded — and, increasingly, under the Memory64 and Custom Page Sizes proposals — runtimes fall back to a software bounds check on every memory access. synth's own SoftwareBoundsChecker documents that cost as "~25-40% overhead".
scry already computes the verdict (TrapCheck{kind: OutOfBounds, verdict: ProvenSafe} via region_in_bounds). What's missing is an export a compiler can consume, and one guard idiom that matters for real loop code.
1. Export: safe-accesses.json (schema v1)
Keyed on the wasmparser operator index space — the same space synth's frontend consumes, so no source re-mapping step.
{
"schema": "scry/safe-accesses/v1",
"scry_version": "3.2.4",
"module_sha256": "<hex>",
"memory_min_bytes": 65536,
"premises": { "bounded_memory": true },
"proven_safe": [ { "func": 4, "pc": 41, "op": "i32.load", "width": 4 } ],
"counts": { "access_sites": 0, "proven_safe": 0 }
}
Soundness contract (belongs in the schema doc):
module_sha256 binds the verdicts to one exact module; the consumer fails closed on mismatch. Eliding on a stale analysis is a memory-safety hole, not a stale optimisation.
- Verdicts are proven against
memory_min_bytes, the guaranteed floor. Wasm memory only grows, so a site proven in-bounds against the floor stays in-bounds under memory.grow.
- Absence from
proven_safe means "not proven", never "unsafe".
This mirrors WasmBounds' output contract — "a non-exhaustive list of provably safe memory instruction offsets" that any runtime can consume — which is a good, simple interface worth copying.
2. Precision: br_if on a bare value
Our guard refinement recognises local.get X; i32.const c; <cmp>; br_if, local.get X; i32.eqz; br_if, and the two-local compare (try_guard_brif / try_guard_brif_rel). It does not yet handle local.tee $x; br_if — a branch on the raw value's truthiness (implicit ≠ 0), which is what LLVM emits for count-to-zero loops.
WasmBounds' BrIf rule covers this case: the fall-through edge gets the local = {0}, the taken edge gets ≠ 0. It's the dual of the i32.eqz shape we already support, and it's worth adding.
Their mechanism for it generalises better than ours, too: they pair each operand-stack entry with the expression that produced it, so br_if knows the popped value is local $x. Ours is a syntactic peephole over ops[pc..pc+4]. A light provenance tag (Local(i) | Const | Mem | Other) on the abstract stack would make refinement general rather than idiom-matched.
3. Domain choice: keep intervals + congruence
WasmBounds tracks sets of intervals, which keeps branch-disjointness precise and supports their fixpoint without a widening heuristic (they note widening as future work).
For scry we should keep our current stack, for a specific reason: for the in-bounds decision a disjunctive set and its hull are equivalent — proving every element in bounds is the same as proving the hull in bounds, since the hull's endpoints are attained by elements. So our single interval already answers this question as precisely, and our threshold widening + narrowing (FEAT-016/042) reaches the fixpoint without unrolling. Where a set would otherwise help — stride/alignment — we have the known-bits congruence domain (FEAT-037).
Worth revisiting if we find a case where branch-disjointness matters for a decision other than in-bounds.
4. Measure it
We have never published "% of i32.load/i32.store sites proven safe". That's the number that makes the capability legible, and the input to the synth-side benchmark.
Related: pulseengine/synth#901 (consumer side), FEAT-046 (OOB verdicts), FEAT-061 (loop-range memory content), REQ-004 (invariants for consumers).
Context and credit
WasmBounds (Emma Sudo & Keith Winstein, Stanford — paper) applies abstract interpretation to elide Wasm memory bounds checks, and reports a measured 1.21× speedup on a dot-product microbenchmark. It is an early-stage project (their words), but it does two things we have not: it states the motivation crisply and it measures the payoff end to end.
Their motivation section is the clearest statement I've seen of why this matters for our embedded target: hardware guard pages need virtual memory, so on no-MMU embedded — and, increasingly, under the Memory64 and Custom Page Sizes proposals — runtimes fall back to a software bounds check on every memory access. synth's own
SoftwareBoundsCheckerdocuments that cost as "~25-40% overhead".scry already computes the verdict (
TrapCheck{kind: OutOfBounds, verdict: ProvenSafe}viaregion_in_bounds). What's missing is an export a compiler can consume, and one guard idiom that matters for real loop code.1. Export:
safe-accesses.json(schema v1)Keyed on the wasmparser operator index space — the same space synth's frontend consumes, so no source re-mapping step.
{ "schema": "scry/safe-accesses/v1", "scry_version": "3.2.4", "module_sha256": "<hex>", "memory_min_bytes": 65536, "premises": { "bounded_memory": true }, "proven_safe": [ { "func": 4, "pc": 41, "op": "i32.load", "width": 4 } ], "counts": { "access_sites": 0, "proven_safe": 0 } }Soundness contract (belongs in the schema doc):
module_sha256binds the verdicts to one exact module; the consumer fails closed on mismatch. Eliding on a stale analysis is a memory-safety hole, not a stale optimisation.memory_min_bytes, the guaranteed floor. Wasm memory only grows, so a site proven in-bounds against the floor stays in-bounds undermemory.grow.proven_safemeans "not proven", never "unsafe".This mirrors WasmBounds' output contract — "a non-exhaustive list of provably safe memory instruction offsets" that any runtime can consume — which is a good, simple interface worth copying.
2. Precision:
br_ifon a bare valueOur guard refinement recognises
local.get X; i32.const c; <cmp>; br_if,local.get X; i32.eqz; br_if, and the two-local compare (try_guard_brif/try_guard_brif_rel). It does not yet handlelocal.tee $x; br_if— a branch on the raw value's truthiness (implicit≠ 0), which is what LLVM emits for count-to-zero loops.WasmBounds'
BrIfrule covers this case: the fall-through edge gets the local= {0}, the taken edge gets≠ 0. It's the dual of thei32.eqzshape we already support, and it's worth adding.Their mechanism for it generalises better than ours, too: they pair each operand-stack entry with the expression that produced it, so
br_ifknows the popped value is local$x. Ours is a syntactic peephole overops[pc..pc+4]. A light provenance tag (Local(i) | Const | Mem | Other) on the abstract stack would make refinement general rather than idiom-matched.3. Domain choice: keep intervals + congruence
WasmBounds tracks sets of intervals, which keeps branch-disjointness precise and supports their fixpoint without a widening heuristic (they note widening as future work).
For scry we should keep our current stack, for a specific reason: for the in-bounds decision a disjunctive set and its hull are equivalent — proving every element in bounds is the same as proving the hull in bounds, since the hull's endpoints are attained by elements. So our single interval already answers this question as precisely, and our threshold widening + narrowing (FEAT-016/042) reaches the fixpoint without unrolling. Where a set would otherwise help — stride/alignment — we have the known-bits congruence domain (FEAT-037).
Worth revisiting if we find a case where branch-disjointness matters for a decision other than in-bounds.
4. Measure it
We have never published "% of
i32.load/i32.storesites proven safe". That's the number that makes the capability legible, and the input to the synth-side benchmark.Related: pulseengine/synth#901 (consumer side), FEAT-046 (OOB verdicts), FEAT-061 (loop-range memory content), REQ-004 (invariants for consumers).