You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LTM (Loops That Matter) is VM-only: Model.simulate({engine:'wasm', enableLtm:true}) is rejected up front and getLinks() throws on a wasm sim. The wasm backend lowers the salsa CompiledSimulation bytecode opcode-for-opcode, so the LTM synthetic equations (link/loop scores, $⁚ltm⁚agg nodes) are "just more equations" and should lower with little-to-no new codegen. Enable LTM on the wasm engine.
Why it isn't free today (three gaps)
LTM has a numeric half (synthetic equations run during simulation) and an analytic half (post-sim Rust: polarity, detected loops, relative-score normalization, discovery). The wasm gaps:
The wasm compile path never enables LTM.compile_datamodel_to_artifact (src/simlin-engine/src/wasmgen/module.rs:114) syncs a fresh SourceProject (default ltm_enabled = false) and never calls set_project_ltm_enabled, so the synthetic vars are never generated and the WasmLayout contains none of them.
Zero parity coverage.tests/simulate_ltm.rs is VM-only and the tests/simulate.rs parity hook runs the non-LTM compile. Nothing proves the LTM equations lower without WasmGenError::Unsupported (LTM augmentation multiplies array/var counts and can trip MAX_UNROLL_UNITS; the engine-wasm-sim contract is no fallback) or that they match the VM numerically.
The analytic half is bound to the VM's in-memory Results.simlin_analyze_get_links (src/libsimlin/src/analysis.rs:288) and simlin_analyze_get_relative_loop_score read $⁚ltm⁚* series out of sim_ref.state.results. The wasm blob writes results into its own JS-owned linear memory that is never handed back to libsimlin, so the existing analysis code structurally cannot see them. (Note: model_detected_loops, scoreless links, and polarity are backend-independent salsa structural queries — already reachable via the model handle.)
Proposed work
Thread LTM into the wasm compile. Add ltm_enabled/ltm_discovery_mode to compile_datamodel_to_artifact + simlin_model_compile_to_wasm; set them via set_project_ltm_enabled. Synthetic vars then land in the bytecode and the WasmLayout.
Add an LTM wasm-parity harness (TDD). A simulate_ltm.rs twin that runs LTM models through both backends under the DLR-FT interpreter, ratcheting a floor like simulate.rs; surfaces every Unsupported/divergence. Heavy models stay #[ignore]d to respect the 3-minute cargo test cap.
Keep the analytic half in Rust, fed by the blob's series (option 3a). Add a small, orthogonal FFI (e.g. simlin_analyze_links_from_series) that ingests the host-produced result slab / $⁚ltm⁚* columns and runs the existing ltm_post/ltm_finding/polarity code. Analysis is on-demand (not the per-frame scrub hot path), so handing the slab back across the FFI is acceptable. Do not reimplement the analysis in TypeScript — that invites the divergent-implementation bug class of ts: two divergent canonicalize implementations (@simlin/core incomplete vs @simlin/engine Rust-faithful) #624. Relative loop scores (post-sim, not synthetic) and discovery mode must go through this Rust path; only raw link/loop-score series come from the blob.
Wire the TS side to drop the up-front rejections (src/engine/src/direct-backend.ts:463, :670) for the now-supported path.
Done / acceptance criteria
AC1: Model.simulate({engine:'wasm', enableLtm:true}) succeeds; the blob's WasmLayout contains the $⁚ltm⁚* series.
AC2: getLinks() on a wasm sim returns links whose scores match the VM within existing tolerances; loop scores and relative loop scores match the VM; discovery mode (when enabled) matches.
AC3: an LTM model the wasm backend genuinely cannot lower returns an explicit WasmGenError (no silent VM fallback) — clean error, never a panic or silently-wrong result.
AC4: LTM parity harness runs LTM corpus models through both backends (heavy ones #[ignore]d); a regression that drops a previously-supported model fails the suite.
AC5: 95%+ coverage on new code via TDD; FFI surface stays small/orthogonal (no bulk/batch endpoint).
Summary
LTM (Loops That Matter) is VM-only:
Model.simulate({engine:'wasm', enableLtm:true})is rejected up front andgetLinks()throws on a wasm sim. The wasm backend lowers the salsaCompiledSimulationbytecode opcode-for-opcode, so the LTM synthetic equations (link/loop scores,$⁚ltm⁚aggnodes) are "just more equations" and should lower with little-to-no new codegen. Enable LTM on the wasm engine.Why it isn't free today (three gaps)
LTM has a numeric half (synthetic equations run during simulation) and an analytic half (post-sim Rust: polarity, detected loops, relative-score normalization, discovery). The wasm gaps:
compile_datamodel_to_artifact(src/simlin-engine/src/wasmgen/module.rs:114) syncs a freshSourceProject(defaultltm_enabled = false) and never callsset_project_ltm_enabled, so the synthetic vars are never generated and theWasmLayoutcontains none of them.tests/simulate_ltm.rsis VM-only and thetests/simulate.rsparity hook runs the non-LTM compile. Nothing proves the LTM equations lower withoutWasmGenError::Unsupported(LTM augmentation multiplies array/var counts and can tripMAX_UNROLL_UNITS; the engine-wasm-sim contract is no fallback) or that they match the VM numerically.Results.simlin_analyze_get_links(src/libsimlin/src/analysis.rs:288) andsimlin_analyze_get_relative_loop_scoreread$⁚ltm⁚*series out ofsim_ref.state.results. The wasm blob writes results into its own JS-owned linear memory that is never handed back to libsimlin, so the existing analysis code structurally cannot see them. (Note:model_detected_loops, scoreless links, and polarity are backend-independent salsa structural queries — already reachable via the model handle.)Proposed work
ltm_enabled/ltm_discovery_modetocompile_datamodel_to_artifact+simlin_model_compile_to_wasm; set them viaset_project_ltm_enabled. Synthetic vars then land in the bytecode and theWasmLayout.simulate_ltm.rstwin that runs LTM models through both backends under the DLR-FT interpreter, ratcheting a floor likesimulate.rs; surfaces everyUnsupported/divergence. Heavy models stay#[ignore]d to respect the 3-minutecargo testcap.simlin_analyze_links_from_series) that ingests the host-produced result slab /$⁚ltm⁚*columns and runs the existingltm_post/ltm_finding/polarity code. Analysis is on-demand (not the per-frame scrub hot path), so handing the slab back across the FFI is acceptable. Do not reimplement the analysis in TypeScript — that invites the divergent-implementation bug class of ts: two divergent canonicalize implementations (@simlin/core incomplete vs @simlin/engine Rust-faithful) #624. Relative loop scores (post-sim, not synthetic) and discovery mode must go through this Rust path; only raw link/loop-score series come from the blob.Wire the TS side to drop the up-front rejections (
src/engine/src/direct-backend.ts:463,:670) for the now-supported path.Done / acceptance criteria
Model.simulate({engine:'wasm', enableLtm:true})succeeds; the blob'sWasmLayoutcontains the$⁚ltm⁚*series.getLinks()on a wasm sim returns links whose scores match the VM within existing tolerances; loop scores and relative loop scores match the VM; discovery mode (when enabled) matches.WasmGenError(no silent VM fallback) — clean error, never a panic or silently-wrong result.#[ignore]d); a regression that drops a previously-supported model fails the suite.References
Run.linksnow[]), ts: two divergent canonicalize implementations (@simlin/core incomplete vs @simlin/engine Rust-faithful) #624 (divergent canonicalize), wasm-backend Phase 5: broadcast-iteration opcode family is unreached dead code; ViewRangeDynamic unsupported by design (neither in corpus) #612 (MAX_UNROLL_UNITS/ViewRangeDynamicunsupported).docs/design-plans/2026-05-20-wasm-backend.md:31(LTM out-of-scope rationale),docs/design-plans/2026-05-22-engine-wasm-sim.md:97(getLinks VM-only).