Skip to content

wasm-backend plan: false 'CompiledSimulation is fully un-fused' premise misleads un-executed phases 2-8 #607

@bpowers

Description

@bpowers

Summary

The wasm-backend implementation plan (docs/implementation-plans/2026-05-20-wasm-backend/) and its design plan (docs/design-plans/2026-05-20-wasm-backend.md) state a load-bearing premise about the engine's opcode pipeline that is factually wrong. Phase 1 (the scalar core) has already corrected the code and its local docs, but the false premise still lives in the per-phase framing of the un-executed phases 2-8, where it will mislead the implementer of the array and module emitter work.

This is a documentation / premise-accuracy defect in an in-flight plan, not a user-facing bug. Phase 1 is not blocked.

The false premise

docs/implementation-plans/2026-05-20-wasm-backend/phase_01.md:56:

The opcode programs are un-fused. fuse_three_address runs inside Vm::new (vm.rs:397), after CompiledSimulation is produced, on the VM's private execution copy. A CompiledSimulation consumer only ever sees the plain opcode set above — never BinVarVar, AssignConstCurr, etc. The emitter does not need to handle the fused/superinstruction opcodes; if one is ever encountered, return WasmGenError::Unsupported.

The same claim is repeated in the design plan:

  • docs/design-plans/2026-05-20-wasm-backend.md:106 ("so the backend ... only ever sees the plain opcode set")
  • docs/design-plans/2026-05-20-wasm-backend.md:130 ("It is the un-fused form ... so the backend translates the plain opcode set only.")

Lumping AssignConstCurr (a peephole opcode) together with BinVarVar (a genuinely VM-private 3-address opcode) is the core error.

Why it is wrong (verified in-repo)

There are two fusion layers, and only the second is VM-private:

  1. Peephole passByteCode::peephole_optimize (src/simlin-engine/src/bytecode.rs:1792) runs inside ByteCodeBuilder::finish() (bytecode.rs:1769). The incremental salsa path invokes finish() per-variable-fragment before symbolization. Its three fused opcodes are:

    • AssignConstCurr (bytecode.rs:643)
    • BinOpAssignCurr (bytecode.rs:650)
    • BinOpAssignNext (bytecode.rs:657)

    These are first-class Opcode variants and first-class SymbolicOpcode variants with a full symbolize/resolve round-trip (src/simlin-engine/src/compiler/symbolic.rs:117/121/125, 509-517, 945-953). So they ride through the symbolic layer straight into CompiledSimulation.

  2. Late 3-address passfuse_three_address (src/simlin-engine/src/vm.rs, the fuse path producing BinVarVar and friends, e.g. vm.rs:1473) is the only pass that is genuinely VM-private (runs on the VM's private execution copy inside Vm::new).

Empirically, essentially every scalar Euler model carries the peephole opcodes — the engine's own VM tests assert it: a constant aux produces AssignConstCurr in flow bytecode (vm.rs:4336-4354) and a stock integration produces BinOpAssignNext in stock bytecode (vm.rs:4416-4433). A Phase 1 compile_simulation that treated these three as Unsupported would have rejected almost all real models.

What Phase 1 already fixed

  • src/simlin-engine/src/wasmgen/lower.rs now lowers all three peephole opcodes (AssignConstCurr at line 228, BinOpAssignCurr at 248, BinOpAssignNext at 252), mirroring the VM's handling at vm.rs:1453/1457/1463.
  • Its module doc (lower.rs:18-33) documents the corrected two-layer fusion model and notes that only the late 3-address pass is VM-private.
  • Stock-offset collection was fixed to include BinOpAssignNext, matching collect_stock_offsets (vm.rs:512, which reads BinOpAssignNext at vm.rs:524).

So the scalar core is correct and the corrected mental model exists in code — just not in the plan docs that drive the remaining phases.

Why it still needs tracking

Phases 2-8 inherit the false framing in their per-phase architecture sections and the "the emitter does not need to handle the fused/superinstruction opcodes" guidance. Any later phase that extends the opcode emitter — especially the array and module phases — must assume the three peephole superinstructions are present in CompiledSimulation. Additionally, if bytecode.rs::peephole_optimize ever grows new fusion patterns, those would also surface in CompiledSimulation and the emitter would need to handle them. A future reader/implementer following the current plan text would wrongly emit WasmGenError::Unsupported for opcodes that occur in nearly every model.

Suggested resolution

Annotate the plan with the correction so a future reader is not misled:

  • docs/implementation-plans/2026-05-20-wasm-backend/phase_01.md (the "Notes for the implementer" framing) — fix the premise at the source.
  • The shared/per-phase framing inherited by phase_02.md .. phase_08.md — anywhere the "un-fused / only the plain opcode set / emitter need not handle fused opcodes" guidance appears.
  • docs/design-plans/2026-05-20-wasm-backend.md:106 and :130.

The correction should state: there are two fusion layers; the peephole layer (AssignConstCurr, BinOpAssignCurr, BinOpAssignNext) runs in ByteCodeBuilder::finish() before symbolization and is present in CompiledSimulation, so the emitter must handle it; only the late 3-address pass (BinVarVar, etc., in fuse_three_address inside Vm::new) is VM-private and may legitimately be treated as Unsupported. src/simlin-engine/src/wasmgen/lower.rs:18-33 already has the correct wording to mirror.

Component(s) affected

  • docs/implementation-plans/2026-05-20-wasm-backend/ (phase_01 .. phase_08)
  • docs/design-plans/2026-05-20-wasm-backend.md
  • Forward-looking: src/simlin-engine/src/wasmgen/ array + module emitter phases

Severity

Medium. Does not block Phase 1 (corrected). It is a premise-accuracy / documentation risk for the remaining un-executed phases.

How it was discovered

Surfaced while executing Phase 1 of the wasm-backend implementation plan; the emitter would have rejected nearly all real models had the three peephole opcodes been treated as Unsupported.

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation enginehygieneToil, but its useful to get get too behind on itrustPull requests that update Rust code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions