Skip to content

engine: bare arrayed name inside nested PREVIOUS() in an apply-to-all equation fails to compile #541

Description

@bpowers

Summary

A plain (non-LTM) apply-to-all equation that nests PREVIOUS() around a bare arrayed variable name (the name with no subscript) fails to compile. The properly subscripted form compiles fine, and nested PREVIOUS on scalars compiles fine -- so the defect is specifically: bare arrayed name + nested PREVIOUS + apply-to-all context.

Minimal repro

In a model with an arrayed variable pop[region]:

p2bare[region] = PREVIOUS(PREVIOUS(pop))      # bare `pop`, no subscript -> NotSimulatable
p2sub[region]  = PREVIOUS(PREVIOUS(pop[region]))  # explicitly subscripted -> compiles fine

p2bare makes the whole project non-simulatable -- it surfaces as a NotSimulatable error / an LTM-style synthetic-fragment compile failure ("PREVIOUS requires a variable reference after helper rewriting" once the fragment path is involved).

Root cause (file-level pointer)

src/simlin-engine/src/builtins_visitor.rs handles PREVIOUS() helper-aux rewriting.

  • walk() (around builtins_visitor.rs:404-443) computes previous_needs_temp_arg: for PREVIOUS(arg0, fallback), when arg0 is not a direct scalar slot -- which includes a nested PREVIOUS(...), since that arg is App(...), not Var -- it routes arg0 through make_temp_arg.
  • make_temp_arg() (builtins_visitor.rs:333-362) synthesizes the helper as a scalar aux: datamodel::Equation::Scalar(eqn) (line 351). A bare arrayed name has no scalar meaning, so the synthesized helper fragment cannot be given an array shape -- it fails to compile.

So the inner PREVIOUS(pop) of PREVIOUS(PREVIOUS(pop)) gets desugared into a scalar helper aux holding a bare arrayed name, which is ill-typed. The make_temp_arg path does substitute_dimension_refs on the arg when an active_subscript is set, but a bare arrayed name carries no dimension reference to substitute, so the synthesized helper stays arrayed-bare-in-a-scalar-equation. The helper-aux synthesis needs to either (a) preserve the source variable's array dimensions when the argument is (or contains) a bare arrayed reference -- emitting an arrayed helper aux -- or (b) subscript the bare arrayed reference by the active apply-to-all dimensions before it lands in the scalar helper. (Worth verifying which is the cleaner fix against the surrounding active_subscript / substitute_dimension_refs machinery.)

Why it matters

A plain user model that writes aux[dim] = PREVIOUS(PREVIOUS(arr)) (a perfectly legal XMILE/Vensim construct -- bare arrayed names are valid apply-to-all references) gets a hard, whole-project compile failure with an error message that points at internal helper-rewriting machinery, with no hint that the trigger is the bare arrayed reference inside the nested PREVIOUS. The fix is in the core compiler's PREVIOUS/array handling, not in LTM-specific code -- LTM is just where it was discovered.

How it was discovered

This is the underlying engine limitation behind what the LTM-arrays-hardening effort (epic #488) called "Finding 2." The LTM equation generator (generate_flow_to_stock_equation in ltm_augment.rs) was emitting exactly this problematic form -- a bare arrayed flow/stock name inside PREVIOUS(PREVIOUS(...)) -- for arrayed flow-to-stock link scores. The LTM fragment compiler silently stubbed the failed fragment to a constant 0, producing a wrong link score (e.g. 1/9 for the canonical pop/growth model instead of the isolated-loop invariant 1).

Commit 8853e8dc ("engine: fix arrayed flow-to-stock LTM link score") fixed the LTM-generator symptom with a generator-side workaround: the generator now emits explicitly-subscripted references (stock[Dim], flow[Dim]). That commit message explicitly states the underlying engine limitation is "left in place." This issue tracks that engine-side root cause -- a plain user model that writes the bare-arrayed nested-PREVIOUS form directly still fails to compile.

Current behavior is test-pinned

As part of Piece 5 of the LTM-arrays-hardening effort, a permanent characterization test is being added to src/simlin-engine/tests/ltm_array_agg.rs: it passes today by asserting the current (broken) "fails to compile" behavior, with a FIXME referencing this issue. When the engine is fixed, that test flips to failing, forcing whoever fixes the engine to update the pin.

Components affected

  • src/simlin-engine/src/builtins_visitor.rs -- walk() (the previous_needs_temp_arg decision) and make_temp_arg() (the scalar-only helper-aux synthesis at builtins_visitor.rs:351)
  • Downstream: src/simlin-engine/src/compiler/codegen.rs -- the BuiltinFn::Previous arm asserts the post-helper-rewriting arg is an Expr::Var; the broken helper fragment fails before/at this point in the LTM-fragment path

Possible approaches

  • In make_temp_arg, when the captured argument is (or contains) a bare arrayed reference, synthesize an arrayed helper aux (Equation::ApplyToAll over the relevant dimensions) instead of Equation::Scalar, so the helper has a well-defined array shape. The active-subscript context already threaded through make_temp_arg is the natural place to determine those dimensions.
  • Alternatively, subscript a bare arrayed reference by the active apply-to-all dimensions before it is captured into the scalar helper aux -- making every occurrence a scalar per-element access (the same shape the 8853e8dc generator-side workaround produces, but done generically in the compiler so all callers benefit).
  • At minimum, if neither structural fix is taken immediately, surface a clear compile-time diagnostic naming the bare arrayed reference as the trigger, instead of the internal "after helper rewriting" assertion / opaque NotSimulatable.

Tracking

Discovered within the LTM arrays hardening epic (#488), but the root cause is in the core compiler's PREVIOUS/array handling, not LTM-specific code. Related to #525 (partially-iterated arrayed subscript pop[Region, young] in an LTM A2A link-score partial -- a subscripted, DynamicIndex-classified reference, distinct from this bare name) and the closed #517 (SUM(PREVIOUS(arr[*])) evaluating to 0 -- a wildcard-reducer numerator that silently produces a wrong result rather than failing to compile). Distinct from both: this is a bare arrayed name inside a nested PREVIOUS that hard-fails compilation.

Activity

  1. added
    ltmLoops that Matter (LTM) analysis subsystem
    on May 14, 2026
  2. bpowers commented on Jun 2, 2026

    @bpowers
    OwnerAuthor

    Fixed in PR #668 (merged as f9f850d). make_temp_arg now synthesizes an arrayed (Equation::ApplyToAll) helper when a PREVIOUS/INIT argument in A2A context carries a bare variable reference and no subscript — so PREVIOUS(PREVIOUS(arr)) over a bare arrayed name compiles and matches the subscripted form per element (multidim and transposed contexts included; name-based dimension matching). Subscripted arguments keep the proven per-element scalar path (substitute_dimension_refs translates active/mapped/literal subscripts — the C-LEARN idiom), and per-element (Ast::Arrayed) slot equations get element-suffixed helper identities with a hardened dedup that fails loudly on ident collisions with differing content. Pinned by bare_arrayed_nested_previous_matches_subscripted (the flipped characterization test), multidim/transposed variants, the mapped-dim regression fixture, the per-slot identity tests (all red-verified), and the three C-LEARN integration gates run explicitly in debug.

    Note for posterity: the first cut of this fix regressed C-LEARN compilation (caught by bisect, fixed in the same PR) and a PR review caught a per-slot helper-identity collision (also fixed in the PR) — the #657 release-mode CI lane would have caught the former automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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