Skip to content

ltm: classify_axis_access resolves a subscript index dimension-name-first, opposite to the compiler's element-first precedence #986

Description

@bpowers

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    engineIssues with the rust-based simulation engineltmLoops that Matter (LTM) analysis subsystemrustPull requests that update Rust code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions