Repository navigation
codegen: type-directed specialization — emit native ops where the type is statically known (#5334 Tier 3 / Lever E) #5497
Description
Follow-on to #5334. That issue tracked making perry's IR efficient for large/untyped modules; its Tier 1 (outline the dynamic machinery) and Tier 2 (emit less scaffolding) levers all shipped:
- Lever A — outline field-SET guard-miss arm (perf(codegen): outline class-field-SET guard-miss arm to one call (#5334 lever A) #5351)
- Lever B — full-outline IC diamond for oversized modules, SET + GET (perf(codegen): full-outline class-field IC diamond for oversized modules (#5334 lever B) #5385, perf(codegen): full-outline the class-field-GET diamond for oversized modules (#5391 path 2) #5410)
- Lever C — fold nan-box bitcast round-trips at emit time (perf(codegen): fold nan-box bitcast round-trips at emit time (#5334 lever C) #5350)
- Lever D — elide GC write-barrier for non-pointer field stores (perf(codegen): elide GC write barrier for non-pointer class-field stores (#5334 lever D) #5381)
- (extra) outline array-literal construction (perf(codegen): outline array-literal construction for oversized modules (#5391) #5412)
This issue carries the one remaining lever, Tier 3 / Lever E: type-directed specialization — the strategic, long-term piece that #5334 explicitly deferred.
Goal
Where a value is statically a number / string / known-shape (real .ts, tsgo, or local inference), emit native ops instead of the dynamic form:
fadd/fmul/fcmpinstead ofjs_add/js_rel_lt/ IC guards- direct slot load/store instead of the inline-cache diamond
- skip the nan-box round-trip entirely when the value never leaves a typed register
This attacks the two largest line-counts from #5334's measurement at the source — the 4.4M call @js_* and 365K js_typed_feedback_*_guard sites — rather than outlining them after the fact.
Why separate from #5334
- It is a performance win, not just a size win — typed programs get both smaller and faster.
- It is orthogonal to minified bundles: minified JS is
Anyalmost everywhere, sostatic_type_ofrarely resolves, and Lever E barely moves the needle there. The bundle-compilability goal of codegen: IR is ~96x bloated for large/untyped modules (1.25GB / ~15GB clang RSS for a 13MB bundle) — outline dynamic machinery + specialize #5334 is already met by Tiers 1–2. - It is a larger, architectural change (type propagation through HIR → codegen) that deserves its own tracking and sequencing.
Sketch
- Thread a static-type signal to codegen op-emit sites (extend the existing
static_type_of/class_field_declared_typemachinery). - Number/boolean arithmetic + comparisons → native
f64ops, guarded only where the static type isnumber | undefined-ish. - Known-shape field access → direct slot, bypassing the IC diamond.
- String concat / comparison on statically-
stringoperands → direct runtime calls without the dynamic dispatch. - Measure on a typed hot-loop benchmark (IR-line reduction + wall-clock) and on a real
.tsapp; hard constraint carried over from codegen: IR is ~96x bloated for large/untyped modules (1.25GB / ~15GB clang RSS for a 13MB bundle) — outline dynamic machinery + specialize #5334: no runtime perf regression on real code.
Context: surfaced taking the 13 MB @anthropic-ai/claude-code cli.js through perry. Tiers 1–2 made untyped bundles compilable; this is the typed-code performance frontier.
Activity
perf(codegen): #5497 Lever E — unary -/+/~ and NumberCoerce are stati…
proggeramlug commentedon Aug 13, 2026
I’m working on a focused first slice of this tracker: threading existing static type information into a native-op specialization path, with IR/codegen regression coverage. I’ll keep the PR scoped and leave release/version metadata to maintainers.
proggeramlug commentedon Aug 13, 2026
Opened #8031 with the focused Boolean/native-numeric Lever E slice. Proven i1 booleans now feed native arithmetic/fcmp; annotation-only and invalidated facts retain the dynamic fallback.
proggeramlug commentedon Aug 15, 2026
Status: this is the surviving umbrella for Lever E. Keeping it open, with the current slice map.
Worth stating explicitly because #7294 (the engine plan tracker) is closed, so if this were closed too, type-directed specialization would have no strategic tracker — only scattered slices.
The architectural direction now lives in two merged documents:
docs/representation-selection-rfc.md(416 lines) anddocs/engine-plan.md(690 lines, canonical).What has shipped against Lever E
fcmp, with annotation-only and invalidated facts retaining the dynamic fallbackfib(40)211.54 G → 4.66 G instructions (45.5x)What is still open under it
forEachgapfib40IPC collapsed 6.30 → 1.92; instruction wins are not converting to timeTwo measured results that should shape the sequencing
interpandiso_misseach lost 22 sites in the last round and got no faster (0.8528 → 0.8524, 0.9896 → 0.9901). The residual is concentrated —parseApplyalone holds 30 of ~43 sites in both — so site-count sweeps are the wrong instrument here.This issue's original framing still holds: minified bundles are
Anyalmost everywhere so Lever E barely moves them, and the win is for typed programs. The corpus rows above are the measurable proxy.