The problem
ltm_agg::classify_axis_access (src/simlin-engine/src/ltm_agg.rs, the IndexExpr2::Expr(Expr2::Var(name, ..)) arm around line 1522) resolves a subscript index by checking whether the name is one of the target equation's iterated dimensions FIRST, and only falls back to resolve_literal_axis_index (the axis's own literal element) if that check misses:
if target_iterated_dims.iter().any(|t| t == name_str) {
// ... Iterated, or a mapped remap, or None
} else {
resolve_literal_axis_index(idx, axis_dim).map(AxisRead::Pinned)
}
The compiler resolves the same index the other way round. compiler::subscript::normalize_subscripts3's IndexExpr3::Expr / Expr3::Var arm (src/simlin-engine/src/compiler/subscript.rs:188-198) looks the name up in the axis's own indexed_elements first -- the comment there reads "First check if it's a named dimension element (takes priority)" -- and only if that misses does it treat the name as a dimension name and emit IndexOp::ActiveDimRef.
The two orders disagree whenever a dimension declares an element whose name is ALSO the name of a dimension the target equation iterates. XMILE permits that collision. Example: dimensions Region = [nyc, boston] and Category = [Region, x] (an element literally named Region), source effect[Region, Category], target target[Region] = ... effect[Region, Region] .... The compiler reads the second index as Category's Region ELEMENT; classify_axis_access reads it as an iteration over the Region dimension.
Why it matters
This is primarily a PRECISION gap. Having taken the dimension-name branch, the classifier looks for a Region/Category positional mapping, finds none, and returns None -- "not statically describable". The reference therefore falls to the conservative path rather than being mis-resolved:
- the enclosing reducer is not hoisted into a
$⁚ltm⁚agg⁚{n} node (ltm_agg::compute_read_slice declines), and
db::ltm_ir::classify_iterated_dim_shape does not classify the reference as Bare/PerElement, so its element edges become the conservative cross-product instead of the diagonal.
Both outcomes are over-broad, not wrong.
But if a Region/Category mapping did exist, the mapped branch would be taken and the classifier would describe the axis as Iterated over a dimension the compiler never iterates there -- at which point the read slice and the emitted element edges would name rows the simulation does not read. So the collision-plus-mapping case is a genuine correctness hazard, not just imprecision.
Components affected
src/simlin-engine/src/ltm_agg.rs -- classify_axis_access (the shared per-axis classifier), compute_read_slice (reducer hoisting)
src/simlin-engine/src/db/ltm_ir.rs -- classify_iterated_dim_shape (reference-shape IR, hence element-edge emission)
src/simlin-engine/src/db/ltm/link_scores.rs -- link-score naming/shaping downstream of the shape
Suggested fix
Try resolve_literal_axis_index(idx, axis_dim) BEFORE the target_iterated_dims check in classify_axis_access, matching normalize_subscripts3's precedence.
Note the blast radius is why this was not done inline where it was found: classify_axis_access is the single shared per-axis classifier feeding reducer hoisting, the reference-shape IR, element-edge emission, and link-score naming, so the change can move LTM char goldens and needs its own verification pass.
How it was found
Found while fixing the sibling defect in pin_dimension_name_indices (src/simlin-engine/src/ltm_augment_post_transform.rs) on branch roundtrips-track-a2 / PR #985. That rule had the same inverted precedence and it was fixed there, using the compiler's order as the reference, with a per_element_pin_colliding_element_name_verdict_enumeration test pinning it. The rustdoc on that rule cross-references this issue as the reason the two rules now differ deliberately.
Deliberately NOT fixed in the shared classifier on that branch -- well outside its scope.
The problem
ltm_agg::classify_axis_access(src/simlin-engine/src/ltm_agg.rs, theIndexExpr2::Expr(Expr2::Var(name, ..))arm around line 1522) resolves a subscript index by checking whether the name is one of the target equation's iterated dimensions FIRST, and only falls back toresolve_literal_axis_index(the axis's own literal element) if that check misses:The compiler resolves the same index the other way round.
compiler::subscript::normalize_subscripts3'sIndexExpr3::Expr/Expr3::Vararm (src/simlin-engine/src/compiler/subscript.rs:188-198) looks the name up in the axis's ownindexed_elementsfirst -- the comment there reads "First check if it's a named dimension element (takes priority)" -- and only if that misses does it treat the name as a dimension name and emitIndexOp::ActiveDimRef.The two orders disagree whenever a dimension declares an element whose name is ALSO the name of a dimension the target equation iterates. XMILE permits that collision. Example: dimensions
Region = [nyc, boston]andCategory = [Region, x](an element literally namedRegion), sourceeffect[Region, Category], targettarget[Region] = ... effect[Region, Region] .... The compiler reads the second index asCategory'sRegionELEMENT;classify_axis_accessreads it as an iteration over theRegiondimension.Why it matters
This is primarily a PRECISION gap. Having taken the dimension-name branch, the classifier looks for a
Region/Categorypositional mapping, finds none, and returnsNone-- "not statically describable". The reference therefore falls to the conservative path rather than being mis-resolved:$⁚ltm⁚agg⁚{n}node (ltm_agg::compute_read_slicedeclines), anddb::ltm_ir::classify_iterated_dim_shapedoes not classify the reference asBare/PerElement, so its element edges become the conservative cross-product instead of the diagonal.Both outcomes are over-broad, not wrong.
But if a
Region/Categorymapping did exist, the mapped branch would be taken and the classifier would describe the axis asIteratedover a dimension the compiler never iterates there -- at which point the read slice and the emitted element edges would name rows the simulation does not read. So the collision-plus-mapping case is a genuine correctness hazard, not just imprecision.Components affected
src/simlin-engine/src/ltm_agg.rs--classify_axis_access(the shared per-axis classifier),compute_read_slice(reducer hoisting)src/simlin-engine/src/db/ltm_ir.rs--classify_iterated_dim_shape(reference-shape IR, hence element-edge emission)src/simlin-engine/src/db/ltm/link_scores.rs-- link-score naming/shaping downstream of the shapeSuggested fix
Try
resolve_literal_axis_index(idx, axis_dim)BEFORE thetarget_iterated_dimscheck inclassify_axis_access, matchingnormalize_subscripts3's precedence.Note the blast radius is why this was not done inline where it was found:
classify_axis_accessis the single shared per-axis classifier feeding reducer hoisting, the reference-shape IR, element-edge emission, and link-score naming, so the change can move LTM char goldens and needs its own verification pass.How it was found
Found while fixing the sibling defect in
pin_dimension_name_indices(src/simlin-engine/src/ltm_augment_post_transform.rs) on branchroundtrips-track-a2/ PR #985. That rule had the same inverted precedence and it was fixed there, using the compiler's order as the reference, with aper_element_pin_colliding_element_name_verdict_enumerationtest pinning it. The rustdoc on that rule cross-references this issue as the reason the two rules now differ deliberately.Deliberately NOT fixed in the shared classifier on that branch -- well outside its scope.