Skip to content

repsel: canonical-i32 requires index-use or a proven i32 bound, so a bare integer accumulator never promotes anywhere #7110

Closed

Description

@proggeramlug

Follow-up to #7106 (promotion census).

A bare integer accumulator or loop counter never gets canonical-i32 storage
anywhere
— function body or module top level — because the eligibility rule
requires the local to be used as an array index or to be provably i32-bounded.
Being proven integer-valued is not enough.

Mechanism

crates/perry-codegen/src/stmt/let_stmt.rs:1224:

let i32_safe_local = ctx.index_used_locals.contains(&id)
    || ctx.strictly_i32_bounded_locals.contains(&id)
    || is_unsigned_i32_local;

and at :1250 the canonical gate widens it only by int_valued_ta_locals:

let canonical_safe_local =
    i32_safe_local || ctx.native_facts.int_valued_ta_locals().contains(&id);

So a local in integer_locals that is never indexed with, never proven
i32-bounded, not >>> 0-written and not an int-typed-array read falls through
to the boxed/parallel-shadow model.

Evidence

--opt-report=json --no-link, --profile perry-dev, macOS arm64. This is in a
function body, so the module-init exclusion (separate issue) is not in play:

function run(): number {
    let sum = 0;
    for (let i = 0; i < ITERATIONS; i++) { sum = sum + 1; }
    return sum;
}

Report: no canonical-slot entry — neither i nor sum. Add a single
a[i] read to the body and i is immediately canonical-slot selected … rep=I32. The only difference is index use.

This is the shape of benchmarks/suite/02_loop_overhead.ts (definitionally a
loop counter), 06_math_intensive.ts, 13_factorial.ts, 14_closure.ts, and
the iter/px/py counters in 15_mandelbrot.ts.

Why the rule is this narrow

The audit note in crates/perry-codegen/src/expr/slot_rep.rs (Range soundness,
2026-07-27) explains it: admission via integer_locals ∩ index_used_locals
accepts Add/Sub/Mul chains that can in principle exceed i32, and it is sound
today only because the pre-phase shadow model already read the i32 slot for
exactly that set of locals. So the precondition is not a range proof — it is a
"this set already behaved this way" compatibility bound. Widening it needs a
real range argument (or a wrap-on-overflow contract like the one
int_valued_ta_locals carries), not just dropping the term.

Now visible

#7106 follow-up (this PR) records this as a denial with rule
not_index_used_or_bounded, tier compiler-limitation, instead of no entry at
all. --opt-report on 02_loop_overhead.ts now names the rule for i and
sum rather than showing an empty report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions