Skip to content

ltm: the ceteris-paribus wrap leaves a LOOKUP table argument's runtime index live in every link-score partial #984

Description

@bpowers

Problem

wrap_non_matching_in_previous (src/simlin-engine/src/ltm_augment.rs, the LOOKUP / LOOKUP_FORWARD / LOOKUP_BACKWARD arm around line 685) holds the TABLE argument (child 0) verbatim and only wraps the index argument(s).

Holding the table reference verbatim is correct and deliberate: a graphical-function table is static data with no value slot, so lookup(PREVIOUS(table), ...) cannot compile, and wrapping it silently zeroes the whole link-score fragment. That is the failure mode behind WRLD3's identically-zero table-mediated link scores, and the in-code comment says so.

The defect is that the arm holds the whole argument verbatim, including any subscript index expressions inside it -- and those are genuine runtime value reads:

LOOKUP(g[idx], x)                  -- `idx` is a variable read
LOOKUP(pop[Region, other], x)      -- likewise

compiler::codegen::extract_table_info (src/simlin-engine/src/compiler/codegen.rs:610) evaluates such an index to select the table element at runtime: its Expr::Subscript arm computes an element offset from the index expressions (offset = sum(index_i * stride_i), built out of the live index Exprs), while its Expr::Var / Expr::StaticSubscript arms resolve a static one to a constant.

So for a target whose equation contains LOOKUP(g[idx], x), every link-score partial of that target varies with the current-step value of idx, even though ceteris paribus requires everything except the live source occurrence to be frozen. The score misattributes idx's influence to whatever source is being isolated.

Scope

This affects all link-score partials, not just the PerElement ones. For the non-PerElement callers the wrap's ctx.pin is None, so the table argument is returned completely untouched (the None => a branch) -- no pin descent, no freeze, nothing.

It predates branch roundtrips-track-a2.

How it was found

A codex review pass on roundtrips-track-a2 flagged the same defect in the narrow PerElement path, where that branch had just made the fragment reach compilability. Commit fb0863b0 ("engine: gate the LTM table-index refusal on the enclosing freeze") fixes only the PerElement path: it declines a bare table argument carrying a runtime index (pin_dimension_name_indices returns discharged = false, which becomes WrapOutcome::missing_occurrence, i.e. a warned skip) while keeping one that is already inside a freeze (a pre-existing PREVIOUS / INIT, a whole-frozen reducer), since a freeze lags the index read too.

The general case outside that path is untouched -- hence this issue.

Why it matters

Correctness of link scores, and silently so: the partial simply reports a wrong number. There is no compile failure and no diagnostic; the score just attributes the table index's per-step movement to the isolated source. This is the "degraded score masquerades as a real one" hazard class (#546 / #548 / #587 / #759).

Components affected

  • src/simlin-engine/src/ltm_augment.rs -- wrap_non_matching_in_previous (the LOOKUP arm), wrap_index_non_matching_in_previous, qualify_element_index
  • src/simlin-engine/src/compiler/codegen.rs -- extract_table_info (why the index is a runtime read)
  • src/simlin-engine/src/db/ltm_char_golden/ -- 16 char goldens that may need regeneration

Suggested fix direction

Give the wrap's own index pass (wrap_index_non_matching_in_previous) the table argument's index expressions, instead of holding the entire argument verbatim. That keeps the table head static (still no PREVIOUS around the table itself) while freezing the index reads like any other other-dep.

Two things a fix must handle:

  1. The index pass already carries the element / dimension-name guards (qualify_element_index, the element-of-any-dimension and dimension-name arms), so a static element selector must stay static -- reuse those guards rather than reimplementing them.
  2. The change could alter generated equation text, so the 16 char goldens in src/simlin-engine/src/db/ltm_char_golden/ should be checked. None currently contains a LOOKUP inside a partial, so it may well be a no-op for them.

Note on the deliberate decision

"Hold the table argument verbatim" is a deliberate decision with a real motivation (it fixed WRLD3's identically-zero table-mediated link scores). It should therefore be revisited on its own terms -- with a test that pins both halves (the table head stays unwrapped AND the index gets frozen) -- rather than changed as a side effect of some other refactor.

Related: #983 (sibling LTM finding filed from the same branch). Mechanically adjacent but distinct from #759, which is the opposite error (over-wrapping a dimension name inside a subscript index); this one is under-wrapping a genuine runtime index because it never reaches the index pass at all.

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