Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
41ceba9
doc: ltm shape-expressiveness phase-1 design (epic #488)
bpowers Jun 11, 2026
e5006af
doc: revise ltm shape design per adversarial review
bpowers Jun 11, 2026
7a08588
doc: scope scalar-result gate admission to scalar owners
bpowers Jun 11, 2026
0b14a9e
engine: ltm subset-aware axis reads and rank de-hoist
bpowers Jun 11, 2026
3a96baf
engine: document de-hoisted rank wildcard-arg path, add subset pins
bpowers Jun 11, 2026
2b135b5
engine: per-source AggNode representation for ltm aggs
bpowers Jun 11, 2026
63a2b0c
engine: flag canonical_read_slice contract for t5
bpowers Jun 11, 2026
0a1f27f
engine: score variable-backed reduces by their read slice
bpowers Jun 11, 2026
148a17d
engine: correct dispatch-widening rationale, pin two flips
bpowers Jun 11, 2026
b7c559c
engine: synthetic aggs for non-aligned whole-rhs reduces
bpowers Jun 11, 2026
e872918
engine: pin mapped-broadcast intersection, exact loop census
bpowers Jun 11, 2026
bcb8956
engine: accept projection feeders in multi-source agg hoisting
bpowers Jun 11, 2026
ad6bdeb
engine: pin feeder deps positionally, amend feeder-clause docs
bpowers Jun 11, 2026
cbb26d3
engine: per-element refshape family for mixed subscripts
bpowers Jun 11, 2026
ff2d50d
engine: pin aliased-agg, other-dep, and mapped perelement corners
bpowers Jun 11, 2026
3779f38
engine: pin frozen co-aggs to their slots in agg-to-target partials
bpowers Jun 11, 2026
fa8fa77
engine: positional-correspondence gate for non-live-dep collapse
bpowers Jun 11, 2026
9c3a35c
doc: t7 as-landed amendments and changed-last third trigger
bpowers Jun 11, 2026
3864542
doc: cross-link phase residual issues in shape design
bpowers Jun 11, 2026
d26dfc3
engine: proptest mixed-subscript and subset-reducer shapes
bpowers Jun 11, 2026
6f606d1
engine: cover pinned-first axis order in mixed proptest
bpowers Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- [design-plans/2026-05-26-wasm-ltm.md](design-plans/2026-05-26-wasm-ltm.md) -- LTM on the wasm backend: thread `enableLtm` into `simlin_model_compile_to_wasm`, add Results-from-slab + from-wasm analyze FFI, and wire `Sim.getLinks` / `Run.links` to run the shared analytic core over a wasm sim so its links match the VM (no protocol change); 6 phases
- [design-plans/2026-06-01-ltm-653-pinned-loop-dimensions.md](design-plans/2026-06-01-ltm-653-pinned-loop-dimensions.md) -- Pinned-loop dimension classification (GH #653): route pinned cycles through the same classify_cycle machinery as enumerated loops, add slot-aware (`Equation::Arrayed`) loop-score generation shared by both the pin path and the enumerator's A2A-collapse, C-LEARN end-to-end validation; 6 phases
- [design-plans/2026-06-04-time-invariant-hoisting.md](design-plans/2026-06-04-time-invariant-hoisting.md) -- Time-invariant variable hoisting (GH #712): run-invariance classification, flow-runlist partition, split metadata (B1); B2 execution sketch
- [design-plans/2026-06-11-ltm-shape-expressiveness.md](design-plans/2026-06-11-ltm-shape-expressiveness.md) -- LTM shape expressiveness (epic #488 Phase 1): one per-axis access model (`AxisRead` with subset-Reduced, per-source `AggNode` slices, `RefShape::PerElement`) consumed by classification/expansion/emission/naming through one row derivation; resolves #765/#766/#767/#764/#771/#751/#757/#525/#526/#769 in 7 tasks
- [plans/](plans/README.md) -- Implementation plans (active and completed)
- [test-plans/](test-plans/) -- Human verification plans for completed features
- [test-plans/2026-05-22-engine-wasm-sim.md](test-plans/2026-05-22-engine-wasm-sim.md) -- Manual verification for the `@simlin/engine` selectable wasm engine (`Model.simulate({engine:'wasm'})`): re-running the automated gates, driving the gated/`#[ignore]`d heavy tests, and the human-judged extras (interactive scrubbing feel, VM-vs-wasm benchmark numbers); all 25 ACs already have automated coverage
Expand Down
711 changes: 711 additions & 0 deletions docs/design-plans/2026-06-11-ltm-shape-expressiveness.md

Large diffs are not rendered by default.

28 changes: 18 additions & 10 deletions docs/reference/ltm--loops-that-matter.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,23 +273,31 @@ re-evaluation per input per variable).
> **"changed-last"** attribution --
> `Delta_x(z) = z(x_current, w_current) - z(x_previous, w_current)`, only the isolated
> input frozen, every other reference left live -- whenever the changed-first partial
> cannot be rendered as a compilable equation. That happens exactly when freezing the
> "everything else" would put an **array slice inside `PREVIOUS()`**
> (`PREVIOUS(matrix[D1,*])`), which Simlin's equation compiler has no lagged-array-view
> path for; expressing changed-first there would require synthesizing a per-element
> frozen helper per arrayed reference (a cost and complexity tradeoff, not an
> impossibility). Two shapes hit the fallback today:
> cannot be rendered as a *correct*, compilable equation. Three shapes hit the
> fallback today:
>
> - the link score for a **scalar feeder of a hoisted array reducer** (`scale` in
> `SUM(pop[*] * scale)`, scored as the `scale -> $...agg...` half of the routed
> edge), the original changed-last case; and
> - freezing the "everything else" would put an **array slice inside `PREVIOUS()`**
> (`PREVIOUS(matrix[D1,*])`), which Simlin's equation compiler has no
> lagged-array-view path for (expressing changed-first there would require
> synthesizing a per-element frozen helper per arrayed reference -- a cost and
> complexity tradeoff, not an impossibility). This covers the link score for a
> **scalar feeder of a hoisted array reducer** (`scale` in `SUM(pop[*] * scale)`,
> scored as the `scale -> $...agg...` half of the routed edge), the original
> changed-last case;
> - the conservative (un-hoisted) per-shape link scores -- the aux-to-aux,
> stock-to-flow, and per-element-slot generators -- when the target's equation
> embeds a reducer the aggregate-node machinery declined to hoist and the isolated
> input sits *inside* it next to a sliced co-source, e.g. the **iterated-dim feeder**
> `frac` in `growth[D1] = SUM(matrix[D1,*] * frac[D1])` (GH #743): changed-first
> would freeze `PREVIOUS(matrix[D1,*])`, so Simlin emits
> `growth - SUM(matrix[D1,*] * PREVIOUS(frac))` instead.
> `growth - SUM(matrix[D1,*] * PREVIOUS(frac))` instead; and
> - an A2A partial whose body references a non-isolated array dep subscripted by the
> target's iterated dimension at a **known position-mismatched axis** (a transposed
> dep like `arr[d2, d1]` inside a body iterated over `d1`, GH #526): changed-first
> would have to collapse the dep to a single *wrong* element (the positional
> correspondence does not hold), so the changed-first leg is rejected as doomed
> rather than silently freezing the wrong slot, and the changed-last form -- which
> leaves the dep live and needs no collapse -- is used instead.
>
> If *both* legs are unrenderable -- the changed-last freeze would itself lag an array
> slice, or the isolated input has no matching occurrence to freeze (the frozen
Expand Down
52 changes: 36 additions & 16 deletions src/libsimlin/tests/integration/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,26 +576,46 @@ fn test_get_errors_no_ltm_diagnostics_when_ltm_never_requested() {
/// accumulator, so `simlin_project_get_errors` surfaces it after an
/// LTM-enabled sim was created.
///
/// The fixture is the GH #742 RANK shape (`grow[Region] = scale[Region] *
/// RANK(pop, 1)` in a feedback loop): the array-valued RANK is hoisted into
/// a scalar `$⁚ltm⁚agg⁚0` aggregate whose own fragment is ill-typed and
/// genuinely fails to compile today (a real, non-injected failure). The
/// previous fixture here was the GH #759 pinned-index repro, retired when
/// #759's fix made its helpers compile; the assertion matches the shared
/// "failed to compile" wording rather than one diagnostic sub-class, so the
/// test keeps covering the FFI harvest channel as individual shapes get
/// fixed (the engine-side guard-injected
/// `test_model_ltm_fragment_diagnostics_covers_implicit_helpers` covers the
/// implicit-helper diagnostic leg independently of any real-shape lifetime).
/// The fixture is a NON-PROJECTION-feeder co-source closure
/// (`growth[D1] = SUM(matrix[D1,*] * w[D1, c1])` with the feedback loop
/// closed through the wildcard-read co-source `matrix`): the Pinned-axis
/// no-`Reduced` source `w[D1, c1]` falls outside the GH #767 / T5
/// projection-feeder acceptance, so the reducer is not hoisted, the element
/// graph keeps the conservative cross-product, and the cross-row circuits'
/// loop scores reference per-`(row, slot)` link-score names the cartesian
/// emitters never produce -- those fragments genuinely fail to compile (a
/// real, non-injected failure; pinned engine-side by
/// `non_projection_feeder_co_source_closure_stays_loud`). The previous
/// fixtures here were the GH #759 pinned-index repro (retired when #759's
/// fix made its helpers compile), the GH #742 RANK-hoisted agg (retired
/// when the GH #771 de-hoist stopped minting the ill-shaped agg), the
/// GH #765 Pinned-bearing variable-backed mixed reduce (retired when T3 of
/// the shape-expressiveness design made its read-slice scores compile
/// cleanly), and the GH #743 iterated-dim-feeder closure (retired when
/// T5's I1 feeder clause hoisted it -- GH #767); the assertion matches the
/// shared "failed to compile" wording rather than one diagnostic
/// sub-class, so the test keeps covering the FFI harvest channel as
/// individual shapes get fixed. (The engine-side guard-injected
/// `test_model_ltm_fragment_diagnostics_covers_implicit_helpers` covers
/// the implicit-helper diagnostic leg independently of any real-shape
/// lifetime; if a future task fixes this shape too and no organic failure
/// shape remains, the engine-`pub(crate)` `LtmFragmentFailureGuard`
/// injection hook will need re-exporting for FFI tests.)
#[test]
fn test_get_errors_surfaces_ltm_fragment_failure_after_ltm_sim() {
let datamodel = TestProject::new("get_errors_fragment_fail")
.with_sim_time(0.0, 8.0, 1.0)
.named_dimension("Region", &["north", "south"])
.array_aux("scale[Region]", "pop[Region] * 0.01")
.array_aux("grow[Region]", "scale[Region] * RANK(pop, 1)")
.array_flow("inflow[Region]", "grow[Region]", None)
.array_stock("pop[Region]", "100", &["inflow"], &[], None)
.named_dimension("D1", &["r1", "r2"])
.named_dimension("D2", &["c1", "c2"])
.array_stock("pop[D1]", "100", &["growth"], &[], None)
.array_aux_direct(
"matrix",
vec!["D1".into(), "D2".into()],
"pop[D1] * 0.05",
None,
)
.array_aux_direct("w", vec!["D1".into(), "D2".into()], "0.5", None)
.array_flow("growth[D1]", "SUM(matrix[D1, *] * w[D1, c1])", None)
.build_datamodel();
let proj = open_project_from_datamodel(&datamodel);

Expand Down
12 changes: 6 additions & 6 deletions src/simlin-engine/CLAUDE.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/simlin-engine/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,9 @@ pub(crate) fn module_link_score_equation(
to_var,
&all_vars,
Some(dim_ctx),
// Module-link partials are scalar-context; the GH #526
// other-dep check keeps its permissive legacy collapse.
None,
) {
Ok(eqn) => return Some(ltm::scalarize_ltm_equation(eqn)),
// The target's equation couldn't be parsed for the
Expand Down Expand Up @@ -824,6 +827,10 @@ pub fn link_score_equation_text<'db>(
&to_var,
&all_vars,
None,
// Legacy (from, to)-keyed path: no dims context is threaded at
// all, so the GH #526 other-dep check keeps the permissive
// collapse here too.
None,
) {
Ok(eqn) => eqn,
Err(err) => {
Expand Down
Loading
Loading