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:
-
Peephole pass — ByteCode::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.
-
Late 3-address pass — fuse_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.
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 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 withBinVarVar(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:
Peephole pass —
ByteCode::peephole_optimize(src/simlin-engine/src/bytecode.rs:1792) runs insideByteCodeBuilder::finish()(bytecode.rs:1769). The incremental salsa path invokesfinish()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
Opcodevariants and first-classSymbolicOpcodevariants 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 intoCompiledSimulation.Late 3-address pass —
fuse_three_address(src/simlin-engine/src/vm.rs, the fuse path producingBinVarVarand friends, e.g.vm.rs:1473) is the only pass that is genuinely VM-private (runs on the VM's private execution copy insideVm::new).Empirically, essentially every scalar Euler model carries the peephole opcodes — the engine's own VM tests assert it: a constant aux produces
AssignConstCurrin flow bytecode (vm.rs:4336-4354) and a stock integration producesBinOpAssignNextin stock bytecode (vm.rs:4416-4433). A Phase 1compile_simulationthat treated these three asUnsupportedwould have rejected almost all real models.What Phase 1 already fixed
src/simlin-engine/src/wasmgen/lower.rsnow lowers all three peephole opcodes (AssignConstCurrat line 228,BinOpAssignCurrat 248,BinOpAssignNextat 252), mirroring the VM's handling atvm.rs:1453/1457/1463.lower.rs:18-33) documents the corrected two-layer fusion model and notes that only the late 3-address pass is VM-private.BinOpAssignNext, matchingcollect_stock_offsets(vm.rs:512, which readsBinOpAssignNextatvm.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, ifbytecode.rs::peephole_optimizeever grows new fusion patterns, those would also surface inCompiledSimulationand the emitter would need to handle them. A future reader/implementer following the current plan text would wrongly emitWasmGenError::Unsupportedfor 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.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:106and:130.The correction should state: there are two fusion layers; the peephole layer (
AssignConstCurr,BinOpAssignCurr,BinOpAssignNext) runs inByteCodeBuilder::finish()before symbolization and is present inCompiledSimulation, so the emitter must handle it; only the late 3-address pass (BinVarVar, etc., infuse_three_addressinsideVm::new) is VM-private and may legitimately be treated asUnsupported.src/simlin-engine/src/wasmgen/lower.rs:18-33already 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.mdsrc/simlin-engine/src/wasmgen/array + module emitter phasesSeverity
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.