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.
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 nestedPREVIOUSon scalars compiles fine -- so the defect is specifically: bare arrayed name + nestedPREVIOUS+ apply-to-all context.Minimal repro
In a model with an arrayed variable
pop[region]:p2baremakes the whole project non-simulatable -- it surfaces as aNotSimulatableerror / 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.rshandlesPREVIOUS()helper-aux rewriting.walk()(aroundbuiltins_visitor.rs:404-443) computesprevious_needs_temp_arg: forPREVIOUS(arg0, fallback), whenarg0is not a direct scalar slot -- which includes a nestedPREVIOUS(...), since that arg isApp(...), notVar-- it routesarg0throughmake_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)ofPREVIOUS(PREVIOUS(pop))gets desugared into a scalar helper aux holding a bare arrayed name, which is ill-typed. Themake_temp_argpath doessubstitute_dimension_refson the arg when anactive_subscriptis 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 surroundingactive_subscript/substitute_dimension_refsmachinery.)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 nestedPREVIOUS. The fix is in the core compiler'sPREVIOUS/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_equationinltm_augment.rs) was emitting exactly this problematic form -- a bare arrayed flow/stock name insidePREVIOUS(PREVIOUS(...))-- for arrayed flow-to-stock link scores. The LTM fragment compiler silently stubbed the failed fragment to a constant0, producing a wrong link score (e.g.1/9for the canonical pop/growth model instead of the isolated-loop invariant1).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-PREVIOUSform 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 aFIXMEreferencing 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()(theprevious_needs_temp_argdecision) andmake_temp_arg()(the scalar-only helper-aux synthesis atbuiltins_visitor.rs:351)src/simlin-engine/src/compiler/codegen.rs-- theBuiltinFn::Previousarm asserts the post-helper-rewriting arg is anExpr::Var; the broken helper fragment fails before/at this point in the LTM-fragment pathPossible approaches
make_temp_arg, when the captured argument is (or contains) a bare arrayed reference, synthesize an arrayed helper aux (Equation::ApplyToAllover the relevant dimensions) instead ofEquation::Scalar, so the helper has a well-defined array shape. The active-subscript context already threaded throughmake_temp_argis the natural place to determine those dimensions.8853e8dcgenerator-side workaround produces, but done generically in the compiler so all callers benefit).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 subscriptpop[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 nestedPREVIOUSthat hard-fails compilation.