Skip to content

wasm-opt: prove the local cleanup rules in Rocq - #287

Open
rvcas wants to merge 2 commits into
mainfrom
rvcas/wasm-opt-rocq-proofs
Open

rvcas wants to merge 2 commits into
mainfrom
rvcas/wasm-opt-rocq-proofs

Conversation

@rvcas

@rvcas rvcas commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Rocq proofs that the five local-cleanup rules from #284 add no behaviour:

  1. local.set N; local.get N → local.tee N
  2. local.tee N; drop → local.set N
  3. local.tee N, where N is never read → nothing
  4. local.set N, where N is never read → drop
  5. local.get, global.get or a constant, then drop → nothing

They are proved against SpecTec's Wasm 2.0 semantics. That is the same Wasm2.v that Nightstream's formal/wasm-rocq proves the Neo-Wasm circuit against, copied verbatim; the copy is 18k of the added lines.

The main theorem is invoke_refines in starstream-wasm-opt/rocq/Nested.v.

  • Setup: take a store and rewrite its function bodies with these rules anywhere, in any mix, at any depth of blocks, loops and ifs. Rules 3 and 4 may only delete writes to locals the function never reads.
  • Claim: call any function in the rewritten store. Every run that ends in values or a trap has either:
    • a run of the same call in the original store with the same result, where the final stores agree except for function bodies; or
    • a run of the original that gets stuck before the end.

That is the direction that matters here, because the prover proves the rewritten code.

The second outcome is there because of rule 3. Deleting local.tee removes an instruction that needs a value under it. Without Wasm's type system, the proof cannot rule out an original that has no value there. Wasm's type soundness rules that out for validated modules, but that theorem isn't proved against these semantics.

How the proof works

  • LocalRewrites.v: each rule on its own. From the same state, the original instructions and their rewrite run to the same configuration.
  • StepInversion.v: every step happens at a configuration's first non-value instruction. This is checked against all of SpecTec's rules.
  • Nested.v: a simulation.
    • Before each step of the rewritten program, the original runs any code the rewrite deleted, then takes the matching step.
    • Inside labels and frames the simulation recurses, by induction on nesting depth.
  • What the relation tracks:
    • The two stores differ in function bodies, and the two frames differ in locals the function never reads.
    • Only the two call rules read code. A call enters each side's own body, and the bodies stay related.
    • Every local.get the two programs share reads a local whose value is the same on both sides.

Not yet proved

  • Removing and renumbering locals.
  • That the Rust code applies exactly these rules.
  • That instantiating the two modules gives stores related as the theorem assumes.

CI doesn't build the proofs yet.

Testing

  • A clean make builds everything in about 40s with the pinned Nightstream toolchain: OCaml 5.3.0, Rocq 9.0.1, MathComp ssreflect 2.4.0, RecordUpdate 0.3.6. The README has the setup.
  • There is no Admitted, admit or Axiom in our files.
  • Print Assumptions invoke_refines lists 55 axioms, all abstract numeric primitives in Wasm2.v. The names are identical to Nightstream's formal/wasm-rocq/scripts/audit.expected.

rvcas added 2 commits October 28, 2026 21:45
Adds Rocq proofs, against SpecTec's Wasm 2.0 semantics (the same
Wasm2.v Nightstream's formal/wasm-rocq uses), that the local-cleanup
rewrites local.set/get -> local.tee, local.tee;drop -> local.set and
get/global.get/const;drop -> nothing add no behaviour.

The main theorem, invoke_refines in Nested.v: rewrite a store's
function bodies with these rules anywhere, at any block depth. Then
every run of a call into the rewritten store that ends in values or a
trap has a run of the same call into the original store ending in the
same result, with final stores equal except for code.

The only assumptions are the 55 numeric primitives Wasm2.v leaves
abstract, identical to Nightstream's audit.expected. Rules 3, 4
(dead stores) and 6 (local removal) are not yet covered.

Signed-off-by: rvcas <x@rvcas.dev>
Extends invoke_refines to all five local-cleanup rules, applied
anywhere in function bodies and in any mix. Rules 3 and 4 delete writes
to locals the function never reads, so the simulation now relates
frames that differ in those locals, and checks that every local.get
the two programs share reads one they agree on.

Rule 3 deletes a local.tee, which needs a value under it. Without
Wasm's type system the proof cannot rule out an original with nothing
there, so the theorem now allows a second outcome: the original gets
stuck before the end. Validated modules never do.

The only assumptions are still the 55 numeric primitives Wasm2.v leaves
abstract, identical to Nightstream's audit.expected.

Signed-off-by: rvcas <x@rvcas.dev>
@rvcas rvcas changed the title wasm-opt: prove local cleanup rules 1, 2 and 5 in Rocq wasm-opt: prove the local cleanup rules in Rocq Oct 29, 2026
the prover proves the rewritten code. The second case is there because of
rule 3. Deleting `local.tee` drops an instruction that needs a value under
it, and without Wasm's type system the proof cannot rule out an original
with nothing there. Wasm's type soundness rules it out for validated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think spectec does have proofs of proofs of progress, btw: https://github.com/Wasm-DSL/spectec/blob/95c256c2c/spectec/test-rocq/theories/type_progress.v#L5853

(with some limitations apparently, but may not matter for our supported subset)

I didn't pull them because I didn't need them. But I think probably we don't need them here neither tbh, I think the property as stated is fine for our case. We don't really care about what the optimizer would do to non validated wasm.

(* In particular: invoking a function of the rewritten store gives the same
result as invoking it in the original store, unless the original goes
wrong, which validated code does not. *)
Corollary invoke_refines : forall s1 s2 f vs a z2f res,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should also prove that the optimization preserves validation?

So validated wasm -> opt -> validated wasm

Maybe it is trivial/not a concern though.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants