Skip to content

ltm: enumerate_agg_nodes hoists whole-extent array-valued RANK into a scalar agg node that cannot compile (agg-routed loop scores zeroed) #771

Description

@bpowers

Summary

enumerate_agg_nodes (src/simlin-engine/src/ltm_agg.rs) hoists a whole-extent RANK(pop, 1) subexpression into a scalar synthetic aggregate node $⁚ltm⁚agg⁚0 -- but RANK is an order statistic that returns an ARRAY (the per-element ranks; Vensim's VECTOR RANK), so a scalar agg node is the wrong shape: the agg's own equation rank(pop, 1) is array-valued in a scalar context and its fragment cannot compile. Post-#741 the failure is loud (one synthetic-variable Assembly Warning naming the agg), and every agg-routed loop score through the node is stubbed to 0.

Demonstrated repro

With arrayed pop[Region] and grow[Region] = scale[Region] * RANK(pop, 1) closing a feedback loop, LTM compilation emits exactly one warning naming $⁚ltm⁚agg⁚0 and zeroes the agg-routed loop scores. Demonstrated and pinned loudly by rank_frozen_subtree_link_score_scores_correctly (src/simlin-engine/tests/integration/ltm_array_agg.rs, commit 01181a36 on branch ltm-fix-batch-2) -- the test asserts exactly 1 remaining warning whose variable is the RANK-hoisted agg, and documents this defect as the one degradation deliberately out of #742's scope.

Root cause

reducer_kind_from_name (src/simlin-engine/src/ltm_agg.rs ~139) maps "rank" to ReducerKind::Nonlinear, so reducer_is_hoistable includes RANK and the agg enumerator mints a synthetic node for it exactly as if it were SUM-like. A whole-extent reducer subexpression yields a scalar agg node -- correct for genuinely reducing builtins, wrong for RANK, whose result has the source's shape.

The #742 fix (commit 01181a36) introduced reducer_collapses_to_scalar (ltm_agg.rs ~167) precisely because RANK does not collapse to a scalar -- but only the PREVIOUS-capture consumers (builtins_visitor::arg_has_bare_var_ref, the GH #541 arrayed-capture gate; ltm_augment::expr_is_array_slice_valued, the GH #743 unfreezable-PREVIOUS detector) consult it. The hoisting gate (reducer_is_hoistable, used by reducer_source_vars / the reference-site IR walk to decide which subexpressions become aggregate nodes) still treats RANK as hoistable.

Why it matters

Low-medium. Loud (post-#741, which landed as cd96ffcf on ltm-fix-batch-2) rather than silent, but every loop routed through the RANK agg node scores a flat 0 -- wrong dominance attribution for any model with RANK on a loop edge. The shape is uncommon (no corpus model hits it), but it is one legal equation away.

Components affected

  • src/simlin-engine/src/ltm_agg.rs -- reducer_kind_from_name / reducer_is_hoistable (the hoisting gate), enumerate_agg_nodes / reducer_source_vars (the enumeration), reducer_collapses_to_scalar (the shape predicate that already encodes the right discrimination but is not consulted here)
  • src/simlin-engine/src/db/ltm/ -- agg-routed link/loop score emission downstream of the ill-shaped node

Possible approaches

Relationship to existing issues

Discovery context

Surfaced during the #742 fix on branch ltm-fix-batch-2 (commit 01181a36), in the GH #759/#742/#525 cluster work. Part of LTM tracking epic #488.

Activity

  1. added
    ltmLoops that Matter (LTM) analysis subsystem
    on Jun 10, 2026
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