Skip to content

ltm: two different reducer sets answer "is this reference inside a reducer?" (SIZE/RANK disagree between the #779 decline and OccurrenceSite::in_reducer) #982

Description

@bpowers

Summary

Two adjacent LTM decisions each answer "is this reference inside an array reducer?", and each answers it with a different reducer set. They disagree on exactly two builtins, SIZE and RANK. Neither choice is a bug where it lives -- both are documented and defended in their own rustdoc -- but they are two implementations of one question, which is the same drift surface #965 exists to remove.

The two sets

  1. ltm_augment::references_bare_source_inside_reducer (src/simlin-engine/src/ltm_augment.rs:1404) -- the GH ltm: bare-spelled feeder of un-hoisted multi-source reducer gets silent wrong changed-last loop score (per-element partial vs broadcast execution) #779 bare-reducer-feeder decline. It marks a child in-reducer via ltm_agg::reducer_collapses_to_scalar, which includes SIZE and excludes RANK (ltm_agg.rs:211: reducer_kind_from_name(name, arity).is_some() && name != "rank").

  2. db::ltm_ir's occurrence walker -- sets OccurrenceSite::in_reducer (src/simlin-engine/src/db/ltm_ir.rs:1276) via ltm_agg::builtin_routes_through_agg (ltm_agg.rs:259: reducer_is_hoistable(builtin) || matches!(builtin, BuiltinFn::Rank(_, _))), which excludes SIZE (SIZE is ReducerKind::Constant, never hoisted) and includes RANK (array-valued, but still aggregate-routed per GH ltm: de-hoisted array-valued RANK gets diagonal-only element edges, so rank-mediated cross-element loops are not enumerated #776).

So on SIZE and RANK the two answers are exactly inverted.

Concrete consequence

OccurrenceSite::in_reducer is the natural IR-side signal for the #779 decline -- it is the same fact, already computed by the one occurrence walk the typed-IR work (#965) established. Consuming it there would be the right simplification, and it is exactly what the occurrence IR is for. But it is not behavior-neutral today:

  • RANK is the blocker. A bare arrayed source read inside RANK(...) currently classifies as not in a scalar reducer, so it is scored (via the GH ltm: PREVIOUS-captured array-valued non-reducing builtin (RANK) scalarizes into ill-typed stubbed helpers, corrupting link scores #742 arrayed-capture path). If the decline consumed in_reducer, that same site would flip to "loudly declined as a bare reducer feeder" -- a real behavior change, with a real user-visible score/diagnostic difference.
  • SIZE is argued harmless. references_bare_source_inside_reducer's own rustdoc states the case: "an equation whose only reducer is SIZE keeps the changed-first convention (the whole reducer is freezable as PREVIOUS(size(...))), so it does not reach this gate."

OccurrenceSite::in_reducer's rustdoc on roundtrips-track-a2 already records the divergence and says "Tracked separately" -- this issue is the thing that comment should be updated to reference.

Why it matters

Two tables answering one question is the exact failure mode of the pre-#965 string-backed design: nothing structurally forces them to agree, and the corpus agreement gate (src/simlin-engine/src/ltm_classifier_agreement_tests.rs) deliberately scopes out in_reducer sites from its strict per-edge comparison, so a future edit to either set can drift without a test going red. It also blocks a genuine simplification: the #779 gate re-walks the AST to re-derive a fact the occurrence IR already carries.

Components affected

Suggested fix

Decide which set is correct for the #779 decline and make it a single named predicate in ltm_agg consumed by both sites, rather than two call sites picking different tables.

If the answer is that the #779 gate genuinely needs a different question than "routes through an agg" -- e.g. "is this reference's value consumed by a scalar-collapsing reduction of the source's own axis?" -- then give that question its own named predicate in ltm_agg, and say so at both sites, so the difference is deliberate and visible rather than incidental. Either way the outcome should be: no call site chooses a reducer table by hand.

The RANK behavior change needs its own reasoning and its own test -- probably a char fixture pinning what a bare arrayed source inside RANK(...) scores today, so the flip (if taken) is a visible golden diff rather than an unnoticed one.

Discovery context

Found while working on the LTM occurrence IR (branch roundtrips-track-a2, part of #965 / epic #488). Filed rather than fixed because that PR's standing invariant is that all 16 LTM char goldens stay byte-identical, and the RANK direction is a deliberate behavior change that does not belong under that invariant.

Tracking

Part of #965 (replace string-backed synthetic equations with typed IR) and LTM epic #488. Related: #779 (the bare-reducer-feeder decline this gate implements), #771/#776/#742 (why RANK is excluded from reducer_collapses_to_scalar but included in builtin_routes_through_agg), #520 (the original single-classification-IR unification this is the residual of).

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystemtech debt

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions