You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ltm: two different reducer sets answer "is this reference inside a reducer?" (SIZE/RANK disagree between the #779 decline and OccurrenceSite::in_reducer) #982
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.
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:
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 outin_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.
src/simlin-engine/src/db/ltm_ir.rs -- OccurrenceSite::in_reducer (the occurrence walker)
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).
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,
SIZEandRANK. 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
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 vialtm_agg::reducer_collapses_to_scalar, which includesSIZEand excludesRANK(ltm_agg.rs:211:reducer_kind_from_name(name, arity).is_some() && name != "rank").db::ltm_ir's occurrence walker -- setsOccurrenceSite::in_reducer(src/simlin-engine/src/db/ltm_ir.rs:1276) vialtm_agg::builtin_routes_through_agg(ltm_agg.rs:259:reducer_is_hoistable(builtin) || matches!(builtin, BuiltinFn::Rank(_, _))), which excludesSIZE(SIZEisReducerKind::Constant, never hoisted) and includesRANK(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
SIZEandRANKthe two answers are exactly inverted.Concrete consequence
OccurrenceSite::in_reduceris 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:RANKis the blocker. A bare arrayed source read insideRANK(...)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 consumedin_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.SIZEis 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 asPREVIOUS(size(...))), so it does not reach this gate."OccurrenceSite::in_reducer's rustdoc onroundtrips-track-a2already 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 outin_reducersites 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
src/simlin-engine/src/ltm_agg.rs--reducer_collapses_to_scalar/reducer_is_hoistable/builtin_routes_through_aggsrc/simlin-engine/src/ltm_augment.rs--references_bare_source_inside_reducer(the ltm: bare-spelled feeder of un-hoisted multi-source reducer gets silent wrong changed-last loop score (per-element partial vs broadcast execution) #779 decline)src/simlin-engine/src/db/ltm_ir.rs--OccurrenceSite::in_reducer(the occurrence walker)Suggested fix
Decide which set is correct for the #779 decline and make it a single named predicate in
ltm_aggconsumed 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
RANKbehavior change needs its own reasoning and its own test -- probably a char fixture pinning what a bare arrayed source insideRANK(...)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 theRANKdirection 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
RANKis excluded fromreducer_collapses_to_scalarbut included inbuiltin_routes_through_agg), #520 (the original single-classification-IR unification this is the residual of).