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.
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(themainentry context) and:1460(the module-init context) both hard-code:Those two flags are the sole context gate consulted at the
Stmt::Leteligibility site (
crates/perry-codegen/src/stmt/let_stmt.rs), so everytop-level local stays boxed.
Evidence
Two programs with a byte-identical loop,
--opt-report=json --no-link,--profile perry-dev, macOS arm64: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:That loop is Phase 3a's motivating pattern (
+=self-append), and it isexactly
benchmarks/suite/08_string_concat.ts. CanonicalStrpromotes in0 of 18 census workloads, and this is why.
benchmarks/suite/16_matrix_multiply.tsis the control: its 3 canonical-i32promotions are
matmul::i/j/k, all inside the function. Its two top-levelloops 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/suiteworkloads put their entirehot loop at module top level, and idiomatic single-file TypeScript does the
same. In
benchmarks/suite/11_prime_sieve.tsthe blocked localsiandjare 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 themodule-init shadow-frame path (
enable_module_init_shadow_frame) has its ownslot 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_contextinstead of an absent entry, so the cost of this issue isreadable straight out of
--opt-reportper workload. It does not changeselection behaviour.