Skip to content

repsel: module-init / program-entry bodies are excluded from canonical i32+Str selection, so a top-level hot loop promotes nothing #7109

Description

@proggeramlug

Follow-up to #7106 (promotion census) and #7034.

The census recorded that 8 of its 18 real workloads produce zero
representation-selection candidates
. This issue is the largest single cause:
module-init and program-entry bodies are excluded from canonical (i32/u32/Str)
selection wholesale
, so a program whose hot loop lives at module top level
promotes nothing, no matter what the per-value rules would have said.

Mechanism

crates/perry-codegen/src/codegen/entry.rs:804 (the main entry context) and
:1460 (the module-init context) both hard-code:

// Representation-selection Phase 1: module-init contexts keep the
// boxed/parallel-shadow model (top-level locals interleave with
// import/init machinery; the win lives in function bodies).
repsel_context_allows_canonical_i32: false,
repsel_context_allows_canonical_str: false,

Those two flags are the sole context gate consulted at the Stmt::Let
eligibility site (crates/perry-codegen/src/stmt/let_stmt.rs), so every
top-level local stays boxed.

Evidence

Two programs with a byte-identical loop, --opt-report=json --no-link,
--profile perry-dev, macOS arm64:

// A — in a function: canonical-slot SELECTED for `i` (rep I32)
function f(a: number[], size: number): number {
    let sum = 0;
    for (let i = 0; i < size; i++) { sum = sum + a[i]; }
    return sum;
}

// B — same loop at module top level: ZERO entries. Not selected, not denied.
let sum = 0;
for (let i = 0; i < arr.length; i++) { sum = sum + arr[i]; }

Wrapping the top-level loop in a bare block (so the counter is not a module
global) does not change B. The gate is the context, not the binding kind.

The same holds for canonical Str, which is the more interesting loss:

// in a function: canonical-slot SELECTED for `result` (rep Str)
// at module top level, in a block:  ZERO entries
let result = "";
for (let i = 0; i < n; i++) { result = result + "x"; }

That loop is Phase 3a's motivating pattern (+= self-append), and it is
exactly benchmarks/suite/08_string_concat.ts. Canonical Str promotes in
0 of 18 census workloads, and this is why.

benchmarks/suite/16_matrix_multiply.ts is the control: its 3 canonical-i32
promotions are matmul::i/j/k, all inside the function. Its two top-level
loops over the same arrays promote nothing.

Why the premise in the comment does not hold

"the win lives in function bodies" is false for the corpus Perry's performance
story is measured on. 9 of the 17 benchmarks/suite workloads put their entire
hot loop at module top level, and idiomatic single-file TypeScript does the
same. In benchmarks/suite/11_prime_sieve.ts the blocked locals i and j
are genuine array-index counters — the exact shape Phase 1 exists for.

Scope / risk

Not a one-line flip. Module init interleaves with import and global-init
machinery, top-level bindings may be module globals held in
@perry_global_* (already excluded by a separate value-level rule), and the
module-init shadow-frame path (enable_module_init_shadow_frame) has its own
slot bookkeeping. It needs the same audit Phase 1 got for function bodies.

Now visible

#7106 follow-up (this PR) makes the exclusion a counted denial with rule
module_init_context instead of an absent entry, so the cost of this issue is
readable straight out of --opt-report per workload. It does not change
selection behaviour.

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