Skip to content

LTM: PREVIOUS-in-subscript arg not reduced to a bare var during helper rewriting -> LTM synthetic link/loop score silently drops to 0 #587

Description

@bpowers

Summary

A PREVIOUS(...) call whose argument sits inside a Subscript index (e.g. PREVIOUS(self[COP, tNext])) is not reduced to a bare Expr::Var by the PREVIOUS helper-rewrite pass. When such an expression appears in an LTM synthetic link-score / loop-score fragment (generated only when LTM discovery is enabled), codegen rejects it and the fragment is silently dropped, so that LTM link/loop score reads a constant 0 -- degrading LTM loop-discovery accuracy for that edge.

This is not a simulation-correctness bug: core simulation is unaffected. It is an LTM-accuracy gap on the loop-analysis path. It is also not a blocker for the element-cycle-resolution work -- C-LEARN compiles, runs, and the LTM-discovery-compiles checks all pass; this surfaces (loudly, via a Warning) once that work removed the masking failures.

Failure chain

  1. The PREVIOUS helper-rewrite (src/simlin-engine/src/builtins_visitor.rs, the scalar-helper synthesis for non-bare PREVIOUS args -- walk() / make_temp_arg) does not reduce a PREVIOUS argument that is an Expr::Subscript index to a bare per-element scalar Expr::Var. The argument survives helper rewriting still shaped as a Subscript.
  2. When this expression is part of an LTM synthetic fragment, codegen reaches the Expr::Subscript(off, indices, ...) -> SubscriptIndex::Single(expr) arm at src/simlin-engine/src/compiler/codegen.rs (~:489-:521, the self.walk_expr(expr)? at ~:493-:502). The inner walk returns Err(NotSimulatable, "PREVIOUS requires a variable reference after helper rewriting"), which -- as of commit 59f01126 -- is propagated via ? rather than panicking. (That ? propagation is exactly the Incremental compiler panics on C-LEARN model (catch_unwind workaround) #363 / AC7.5 fix: the in-tree comment at codegen.rs:494-501 documents "a PREVIOUS whose arg survived helper rewriting as a non-variable expression (NotSimulatable) ... must flow back to the caller (db_ltm.rs gracefully drops the un-compilable LTM synthetic fragment), never escalate to a panic (Incremental compiler panics on C-LEARN model (catch_unwind workaround) #363).")
  3. That Err flows back to compile_phase in src/simlin-engine/src/db_ltm.rs, where the Err(_) => None arm at db_ltm.rs:2083 (and the sibling Err(_) => None build_var fallbacks at ~:2093/:2099) gracefully drops the un-compilable LTM synthetic fragment.
  4. With no bytecode emitted for that synthetic variable, it keeps its layout slot and reads a constant 0 for the entire simulation. The loop score for any loop through that edge is the product of its link scores, so a zero link score collapses that loop's score.

The drop is surfaced, not silent: the existing model_ltm_fragment_diagnostics pass emits a Warning for every LTM synthetic variable whose fragment fails to compile (subject to the #466 caveat that the diagnostic-collection FFI paths leave ltm_enabled false).

Why it matters

Repro context

Surfaced and made loud during element-cycle-resolution Phase 6 Task 11 (#363 / AC7.5), commit 59f01126, on test/xmutil_test_models/C-LEARN v77 for Vensim.mdl with LTM discovery enabled.

Components affected

  • src/simlin-engine/src/builtins_visitor.rs -- the PREVIOUS helper-rewrite (walk() / make_temp_arg): a subscripted/expression PREVIOUS argument is not reduced to a bare per-element scalar Expr::Var.
  • src/simlin-engine/src/compiler/codegen.rs -- the Expr::Subscript -> SubscriptIndex::Single arm (~:489-:521) where the inner walk_expr(expr)? returns the NotSimulatable "PREVIOUS requires a variable reference after helper rewriting" error.
  • src/simlin-engine/src/db_ltm.rs -- db_ltm.rs:2083 (Err(_) => None, plus the sibling build_var Err(_) => None arms), the graceful-drop site that makes the LTM synthetic fragment read constant 0.

Possible fix direction

Extend the PREVIOUS helper-rewrite in builtins_visitor.rs to reduce a subscripted (or otherwise non-bare-expression) PREVIOUS argument to a bare Expr::Var -- i.e. synthesize the per-element scalar helper aux for the subscripted slice -- so the LTM synthetic fragment compiles instead of being dropped. This is the same structural direction proposed for the related PREVIOUS-arg cases (generalize the helper synthesis so PREVIOUS(expr) desugars for any expr, not just a bare Var). The fix should be paired with a regression test asserting the LTM link/loop score for an edge through a subscripted-PREVIOUS reference is non-zero (so the dropped-fragment-reads-0 behavior cannot silently return).

Linkage

Activity

  1. added
    bugSomething isn't working
    engineIssues with the rust-based simulation engine
    ltmLoops that Matter (LTM) analysis subsystem
    on May 19, 2026
  2. bpowers commented on Jun 2, 2026

    @bpowers
    OwnerAuthor

    Fixed by 865854c ("engine: compile static-subscript PREVIOUS/INIT directly, qualify LTM element refs" — its message names this bug) and f1ac6f6. PREVIOUS/INIT with statically-resolvable subscript indices (including A2A-iterated dimension elements, the SAMPLE IF TRUE self-reference) now compile to a direct LoadPrev/LoadInitial, and the ceteris-paribus wrapper no longer PREVIOUS-wraps an element index; the dynamic-index case keeps a helper aux that itself compiles.

    Verified empirically on main (521fc37): a minimal 2D-arrayed SAMPLE IF TRUE-in-loop model compiles with zero fragment-compile-failure warnings and a non-zero link/loop score through the construct; test_previous_a2a_iterated_dimension_no_helpers, test_ltm_bare_element_subscripts_no_helpers, and test_previous_dynamic_subscript_keeps_helper pass; and on C-LEARN no remaining fragment failure involves the SAMPLE IF TRUE / self-subscript shape (the remaining C-LEARN fragment failures belong to #528 and module-composite cases, tracked separately).

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

    bugSomething isn't workingengineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystem

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions