Summary
When LTM (Loops That Matter) is enabled, an apply-to-all (A2A) equation that references another arrayed variable with an explicit dimension subscript -- e.g. growth[D1,D2] = row_sum[D1] * c where row_sum[D1] carries the literal [D1] subscript inside the A2A equation -- breaks LTM compilation with:
PREVIOUS requires a variable reference after helper rewriting
(error originates at src/simlin-engine/src/compiler/codegen.rs:712). Without LTM the same model compiles and simulates fine.
Failure chain
- The element-graph reference-shape walker (
collect_reference_sites / classify_subscript_shape in src/simlin-engine/src/db_analysis.rs) classifies row_sum[D1] -- a Subscript over the iterated dimension D1 -- as RefShape::DynamicIndex rather than a same-element / Bare reference. (Per the doc comments at db_analysis.rs:120-136, 452-466, a literal index that doesn't resolve via resolve_literal_index falls back to DynamicIndex; the iterated-dimension index D1 is not a literal element name, so it falls through.)
- The LTM link-score partial generator (
build_partial_equation_shaped / the broadcast-A2A path through shape_aware_source_ref in src/simlin-engine/src/ltm_augment.rs) emits a ceteris-paribus partial equation for that edge that wraps the non-target form in PREVIOUS(...).
- After helper rewriting desugars
PREVIOUS(x), codegen requires the argument to be a plain Expr::Var (codegen.rs:702-714). But row_sum[D1] compiles down to an Expr::Subscript (Op2-shaped) expression, not Expr::Var -- so the assertion fires and the model is NotSimulatable.
So a valid model construct -- an A2A equation that explicitly subscripts an arrayed dependency by the iterated dimension -- breaks LTM compilation.
Why this matters
A model author who writes x[D1,D2] = some_arrayed_aux[D1] * ... (perfectly legal XMILE/Vensim) and then turns on LTM gets a hard compile error with a message that points at internal helper-rewriting machinery, with no indication that the trigger is the subscripted reference. The same model is fine without LTM. Correctness/usability gap, not a regression.
Locations
src/simlin-engine/src/compiler/codegen.rs:702-714 -- the BuiltinFn::Previous arm that requires Expr::Var and emits "PREVIOUS requires a variable reference after helper rewriting"
src/simlin-engine/src/db_analysis.rs -- collect_reference_sites, walk_reference_sites, classify_subscript_shape (RefShape::Bare / FixedIndex / Wildcard / DynamicIndex); the iterated-dimension subscript on an A2A dependency lands in DynamicIndex
src/simlin-engine/src/ltm_augment.rs -- build_partial_equation_shaped, shape_aware_source_ref (the broadcast-A2A / dynamic-index source-ref path); related comment about the stub-dep limitation around ltm_augment.rs:1317-1324
src/simlin-engine/tests/simulate_ltm.rs::build_partial_reduce_model -- comment (~lines 5397-5402) documenting that the fixture deliberately uses bare references to avoid this
Possible approaches
- Teach the reference-shape classifier that a subscript whose indices are exactly the target's iterated dimension(s) (in the A2A case) is a same-element /
Bare reference, not DynamicIndex -- row_sum[D1] inside growth[D1,D2] = ... is just row_sum evaluated at the current D1 element. That would let the existing Bare partial path handle it.
- Alternatively, in the partial generator, when the source reference resolves to a per-element slot of an arrayed var, emit a synthetic scalar "previous of this element" helper var (so
PREVIOUS(...) gets a real Expr::Var), instead of wrapping the Subscript expression directly.
- At minimum, if the construct genuinely can't be scored, emit a clear compile-time diagnostic ("LTM cannot score the link from
row_sum to growth because row_sum is referenced with an explicit dimension subscript; rewrite as a bare reference") instead of the internal "after helper rewriting" assertion.
This is adjacent to AC5 of the "LTM Cross-Element Aggregate Scoring" design (retire the :wildcard / :dynamic link-score path) and to the LTM array umbrella #273, and Phase 5's agg-node rerouting might incidentally fix it, but no existing issue or design-plan section enumerates this specific PREVIOUS-arg-must-be-Var failure mode for subscripted-A2A-reference link-score partials.
Discovery context
Confirmed by both an implementer and a code reviewer during Phase 4 of the "LTM Cross-Element Aggregate Scoring" implementation plan (branch ltm-503-cross-element-agg; design plan under docs/implementation-plans/2026-05-09-ltm-503-cross-element-agg/). Phase 4 sidestepped it -- the test fixture (build_partial_reduce_model in tests/simulate_ltm.rs) was restructured to use bare references rather than fixing the underlying bug.
Tracking
Part of LTM tracking epic: #488. Related to #273 (LTM array support umbrella) and #510 (degenerate link score for disjoint-dimension arrayed->arrayed edges) -- but distinct: #510 is about a degenerate scalar score for per-element target equations with disjoint dims; this is a hard compile error triggered by an explicit dimension subscript inside an apply-to-all equation.
Summary
When LTM (Loops That Matter) is enabled, an apply-to-all (A2A) equation that references another arrayed variable with an explicit dimension subscript -- e.g.
growth[D1,D2] = row_sum[D1] * cwhererow_sum[D1]carries the literal[D1]subscript inside the A2A equation -- breaks LTM compilation with:(error originates at
src/simlin-engine/src/compiler/codegen.rs:712). Without LTM the same model compiles and simulates fine.Failure chain
collect_reference_sites/classify_subscript_shapeinsrc/simlin-engine/src/db_analysis.rs) classifiesrow_sum[D1]-- aSubscriptover the iterated dimensionD1-- asRefShape::DynamicIndexrather than a same-element /Barereference. (Per the doc comments atdb_analysis.rs:120-136, 452-466, a literal index that doesn't resolve viaresolve_literal_indexfalls back toDynamicIndex; the iterated-dimension indexD1is not a literal element name, so it falls through.)build_partial_equation_shaped/ the broadcast-A2A path throughshape_aware_source_refinsrc/simlin-engine/src/ltm_augment.rs) emits a ceteris-paribus partial equation for that edge that wraps the non-target form inPREVIOUS(...).PREVIOUS(x), codegen requires the argument to be a plainExpr::Var(codegen.rs:702-714). Butrow_sum[D1]compiles down to anExpr::Subscript(Op2-shaped) expression, notExpr::Var-- so the assertion fires and the model isNotSimulatable.So a valid model construct -- an A2A equation that explicitly subscripts an arrayed dependency by the iterated dimension -- breaks LTM compilation.
Why this matters
A model author who writes
x[D1,D2] = some_arrayed_aux[D1] * ...(perfectly legal XMILE/Vensim) and then turns on LTM gets a hard compile error with a message that points at internal helper-rewriting machinery, with no indication that the trigger is the subscripted reference. The same model is fine without LTM. Correctness/usability gap, not a regression.Locations
src/simlin-engine/src/compiler/codegen.rs:702-714-- theBuiltinFn::Previousarm that requiresExpr::Varand emits "PREVIOUS requires a variable reference after helper rewriting"src/simlin-engine/src/db_analysis.rs--collect_reference_sites,walk_reference_sites,classify_subscript_shape(RefShape::Bare/FixedIndex/Wildcard/DynamicIndex); the iterated-dimension subscript on an A2A dependency lands inDynamicIndexsrc/simlin-engine/src/ltm_augment.rs--build_partial_equation_shaped,shape_aware_source_ref(the broadcast-A2A / dynamic-index source-ref path); related comment about the stub-dep limitation aroundltm_augment.rs:1317-1324src/simlin-engine/tests/simulate_ltm.rs::build_partial_reduce_model-- comment (~lines 5397-5402) documenting that the fixture deliberately uses bare references to avoid thisPossible approaches
Barereference, notDynamicIndex--row_sum[D1]insidegrowth[D1,D2] = ...is justrow_sumevaluated at the currentD1element. That would let the existing Bare partial path handle it.PREVIOUS(...)gets a realExpr::Var), instead of wrapping theSubscriptexpression directly.row_sumtogrowthbecauserow_sumis referenced with an explicit dimension subscript; rewrite as a bare reference") instead of the internal "after helper rewriting" assertion.This is adjacent to AC5 of the "LTM Cross-Element Aggregate Scoring" design (retire the
:wildcard/:dynamiclink-score path) and to the LTM array umbrella #273, and Phase 5's agg-node rerouting might incidentally fix it, but no existing issue or design-plan section enumerates this specificPREVIOUS-arg-must-be-Varfailure mode for subscripted-A2A-reference link-score partials.Discovery context
Confirmed by both an implementer and a code reviewer during Phase 4 of the "LTM Cross-Element Aggregate Scoring" implementation plan (branch
ltm-503-cross-element-agg; design plan underdocs/implementation-plans/2026-05-09-ltm-503-cross-element-agg/). Phase 4 sidestepped it -- the test fixture (build_partial_reduce_modelintests/simulate_ltm.rs) was restructured to use bare references rather than fixing the underlying bug.Tracking
Part of LTM tracking epic: #488. Related to #273 (LTM array support umbrella) and #510 (degenerate link score for disjoint-dimension arrayed->arrayed edges) -- but distinct: #510 is about a degenerate scalar score for per-element target equations with disjoint dims; this is a hard compile error triggered by an explicit dimension subscript inside an apply-to-all equation.