You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #8033 correctly stopped treating erased TypeScript binding annotations as runtime representation proofs, but it also removed ordinary function-parameter evidence from generic function bodies. That causes a large, reproducible specialization cliff across the current performance corpus.
Do not revert #8033 or seed the generic body from declared parameter types. The correctness constraint from #7846 is real. We need a guarded parameter-specialized clone (or equivalent entry guard) with the unchanged generic body as fallback.
Current-main regression
Measured at exact main 601a02d235af2d9af1841089426a1da379edac0a.
The clean M1 mini sweep regressed from the previously recorded 843ef621f sweep as follows:
Perry/Node wins: 10/19 -> 2/19
Perry/scriptc wins: 14/19 -> 8/18
Perry geomean across all 19 rows: 3.480x slower
clean and separately dirty corroboration agreed within 0.2%
Largest wall-time changes on the clean mini:
benchmark
843ef621f
current main
change
pipeline
0.1630 s
3.3585 s
+1960%
asyncpipe
0.0980 s
1.1216 s
+1045%
retain_wide
0.1840 s
1.0826 s
+488%
shapes
0.0550 s
0.3183 s
+479%
interp
0.8090 s
3.9931 s
+394%
iso_miss
0.9380 s
4.4743 s
+377%
The current timings also reproduce the older conservative-codegen regbase across 15 common rows: geomean current/regbase is 1.00003x, with a maximum row difference of 1.24%.
Causal A/B
I built a diagnostic-only compiler from exact current main that changed only ordinary compile_function admission:
local_types remained unchanged;
proven_local_types was seeded from f.params only;
module globals and local initializers were excluded;
runtime, stdlib, benchmark input, compile flags, and execution environment were otherwise held fixed.
This control is intentionally unsound for lying annotations and is not a proposed patch. It exists only to identify the lost optimization surface.
Every tested benchmark retained byte-exact output. Same-host retired instructions were:
benchmark
current main
parameter-hint control
reduction
current/control
asyncpipe
17,054,037,641
1,279,430,024
92.5%
13.329x
interp
64,455,747,802
12,683,767,308
80.3%
5.082x
iso_miss
72,572,449,776
15,332,837,352
78.9%
4.733x
pipeline
52,150,730,952
3,714,983,318
92.9%
14.038x
tree
31,782,383,093
8,896,702,263
72.0%
3.572x
interp was repeated three times in each arm. Current was 64.455-64.478B instructions; the parameter-only control was 12.684-12.699B. The control returns to the previously recorded roughly 11.5-14.1B instruction band.
This proves that lost ordinary-parameter type evidence is the dominant cause of the headline regression.
Mechanism
In current codegen/function.rs:
f.params still populate local_types.
The generic FnCtx initializes proven_local_types empty.
stable_local_type_proof() deliberately reads only proven_local_types and rejects reassigned locals.
Therefore an ordinary typed parameter is unproven throughout the generic body unless a separate guarded clone supplies evidence.
This is especially costly for the interpreter corpus. Hot recursive functions such as evalNode(n: Node, env: Env), lookup(env: Env, name: string), parser functions taking Parser, and lex(src: string) lose typed property, array, string, and numeric lowering at every use.
Current interp --opt-report=json records 8 selections and 64 denials, including:
pointer-shape: 0 selected / 49 denied
canonical-slot: 7 selected / 3 denied
specialized ABI: 1 selected / 12 denied
The diagnostic control does not repair pointer-shape clone routing (it remains 0/49). Its performance recovery comes from letting the ordinary body lowering consume the parameter type again.
Ruled out
Mini noise: independent clean/dirty sweeps agree within 0.2%.
Native-root GC overhead / Propagate native-root decisions to codegen-unit workers #8071: a compile-time PERRY_RS4GC=0 shadow-root control did not recover performance. asyncpipe retired 17.08B vs 17.07B instructions; interp worsened to 71.30B from 64.63B.
Output/correctness differences: Perry remained byte-exact on all 19 sweep rows; the five causal-control programs above were byte-exact in both arms.
Required soundness boundary
TypeScript annotations are erased and may lie. #7846 demonstrates that a declared scalar, class, array, or capture type cannot by itself select an unguarded representation or behavior. The generic body and GC rooting must remain conservative.
A sound recovery should therefore build on guarded clone-and-route machinery:
Select eligible ordinary parameter tuples without changing the generic entry/body semantics.
Emit runtime guards for the complete tuple before entering a specialized body clone.
Populate proven_local_types only inside the successfully guarded clone.
Preserve the generic boxed fallback for failed guards and indirect/unknown callers.
Route direct call sites only when their current argument facts prove the same tuple.
Support recursive calls from a guarded clone without reintroducing annotation trust.
Add object/array/discriminated-union parameter coverage; existing specialized ABI coverage is primarily scalar/string/typed-array and does not cover the hot Node/Env shapes here.
This likely belongs with the clone-and-route work already referenced by the pointer-shape report as #7034 section 1.
Acceptance
Lying-annotation regressions, including test_gap_7846_local_binding_type_proofs.ts, pass through the generic fallback with Node-equivalent output.
Positive tests prove the runtime guard is present and the specialized clone receives parameter proofs only after that guard.
Negative tests prove a wrong scalar/object/array/union value cannot enter the clone.
Direct, indirect, and recursive call paths retain a valid generic fallback.
Precise-root/forced-moving verification remains green for both clone and fallback paths.
interp, iso_miss, pipeline, asyncpipe, and tree are remeasured on the clean mini with exact-output gates.
The recovered instruction count is structurally pinned or otherwise guarded so this cliff cannot silently recur.
Measurement provenance
Perry was built with:
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
Compiles pinned the exact release directory with PERRY_RUNTIME_DIR, PERRY_NO_AUTO_OPTIMIZE=1, PERRY_NO_CACHE=1, and --no-cache. Nothing was built on the mini. The accepted mini cells were warmed and then measured best-of-five in interleaved order with exit/output validation on every run.
Summary
PR #8033 correctly stopped treating erased TypeScript binding annotations as runtime representation proofs, but it also removed ordinary function-parameter evidence from generic function bodies. That causes a large, reproducible specialization cliff across the current performance corpus.
Do not revert #8033 or seed the generic body from declared parameter types. The correctness constraint from #7846 is real. We need a guarded parameter-specialized clone (or equivalent entry guard) with the unchanged generic body as fallback.
Current-main regression
Measured at exact main
601a02d235af2d9af1841089426a1da379edac0a.The clean M1 mini sweep regressed from the previously recorded
843ef621fsweep as follows:Largest wall-time changes on the clean mini:
843ef621fThe current timings also reproduce the older conservative-codegen
regbaseacross 15 common rows: geomean current/regbase is1.00003x, with a maximum row difference of 1.24%.Causal A/B
I built a diagnostic-only compiler from exact current main that changed only ordinary
compile_functionadmission:local_typesremained unchanged;proven_local_typeswas seeded fromf.paramsonly;This control is intentionally unsound for lying annotations and is not a proposed patch. It exists only to identify the lost optimization surface.
Every tested benchmark retained byte-exact output. Same-host retired instructions were:
interpwas repeated three times in each arm. Current was 64.455-64.478B instructions; the parameter-only control was 12.684-12.699B. The control returns to the previously recorded roughly 11.5-14.1B instruction band.This proves that lost ordinary-parameter type evidence is the dominant cause of the headline regression.
Mechanism
In current
codegen/function.rs:f.paramsstill populatelocal_types.FnCtxinitializesproven_local_typesempty.stable_local_type_proof()deliberately reads onlyproven_local_typesand rejects reassigned locals.This is especially costly for the interpreter corpus. Hot recursive functions such as
evalNode(n: Node, env: Env),lookup(env: Env, name: string), parser functions takingParser, andlex(src: string)lose typed property, array, string, and numeric lowering at every use.Current
interp --opt-report=jsonrecords 8 selections and 64 denials, including:The diagnostic control does not repair pointer-shape clone routing (it remains 0/49). Its performance recovery comes from letting the ordinary body lowering consume the parameter type again.
Ruled out
PERRY_RS4GC=0shadow-root control did not recover performance.asyncpiperetired 17.08B vs 17.07B instructions;interpworsened to 71.30B from 64.63B.Required soundness boundary
TypeScript annotations are erased and may lie. #7846 demonstrates that a declared scalar, class, array, or capture type cannot by itself select an unguarded representation or behavior. The generic body and GC rooting must remain conservative.
A sound recovery should therefore build on guarded clone-and-route machinery:
proven_local_typesonly inside the successfully guarded clone.Node/Envshapes here.This likely belongs with the clone-and-route work already referenced by the pointer-shape report as #7034 section 1.
Acceptance
test_gap_7846_local_binding_type_proofs.ts, pass through the generic fallback with Node-equivalent output.interp,iso_miss,pipeline,asyncpipe, andtreeare remeasured on the clean mini with exact-output gates.Measurement provenance
Perry was built with:
Compiles pinned the exact release directory with
PERRY_RUNTIME_DIR,PERRY_NO_AUTO_OPTIMIZE=1,PERRY_NO_CACHE=1, and--no-cache. Nothing was built on the mini. The accepted mini cells were warmed and then measured best-of-five in interleaved order with exit/output validation on every run.