Skip to content

ltm-augment: subscripted-A2A-reference link-score partial fails to compile (PREVIOUS arg must be Var) #511

@bpowers

Description

@bpowers

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

  1. 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.)
  2. 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(...).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions