Skip to content
11 changes: 11 additions & 0 deletions changelog.d/6905-repsel-p2-specialized-abi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Representation-selection Phase 2 (`docs/representation-selection-rfc.md` §5.4–§5.6): the specialized calling convention — bounded monomorphization. Statically-proven call sites now call a full-body specialized entry with raw-typed args (typed-array header ptr, raw i32/f64), chosen statically with no per-call guard.

- Full-body specialized entries: `compile_function` parameterized on a call-site-derived rep tuple (`Boxed`/`I32`/`F64`/`TaPtr{kind, const_len}`); internal `{public}__spec_<mangle>` symbols emitted before the public bodies; mutually exclusive with `i64_specialized` and the typed_abi clone families; boxed return. `I32` params bind into Phase 1 canonical slots; `TaPtr` params bind as proven `BufferViewSlot`s with data pointer/length hoisted once at entry (sound: `GC_TYPE_TYPED_ARRAY`/`GC_TYPE_BUFFER` are non-movable, non-view typed arrays cannot detach or resize).
- `collectors/spec_abi_sites.rs` pre-pass (ctx-free, two-phase): proves `TaPtr` bindings (single top-level binding, non-view construction form, never reassigned module-wide, never closure-referenced) and judges direct call sites; dominant-tuple selection with callee-side demotion. Tier A dispatch re-proves every slot at the call site by construction (direct call, no diamond); Tier B keeps the guarded-diamond shape for declaration-proven tuples. Closures/wrappers/cross-module paths only ever reach the public boxed symbol (source-ratchet test).
- Proven-region tier: masked-window regions over fully compile-time-proven, undowngraded views lower as one guard-free fast copy (no probes, no alternate copies), and proven-view element stores join the region. New checked proven-view element access (`expr/proven_view_access.rs`): inline `icmp ult` bounds + bare load/store for dynamic exact-i32 indices, bit-exact with the runtime helpers.
- wrap-i32 additive accumulators: `int_valued_ta_locals` admits straight-line `Add`/`Sub` chains over exact operands (in-bounds-proven int-TA reads via constant lengths, literals, bitwise results, sibling candidates) carrying the ToInt32 image in the canonical i32 slot; loop-carried additive writes, unproven operands, and index-position reads stay rejected.
- FEAT_JSCVT: `toint32_wrap` emits `@llvm.aarch64.fjcvtzs` (spec-exact single-instruction ECMAScript ToInt32) on arm64 macOS targets (`PERRY_JSCVT` kill switch; iOS/tvOS device targets keep the portable tower — A7–A11 lack the instruction).
- Audits: `clamp_detect` gains the missing `was_plain_async` gates; top-level `const` numeric module bindings fold into `compile_time_constants` (TDZ-flagged bindings excluded); typed-array-typed `PutValueSet` element writes route to `index_set`'s typed arm; never-bound shadow-slot clears elided; spec `TaPtr` params skip the redundant callee-side root bind.
- Flags `PERRY_SPECIALIZED_ABI` (default on) and `PERRY_SPECIALIZED_ABI_MAX` (default 64) + `PERRY_JSCVT`, all object-cache-keyed; rejections recorded via the typed-clone rejection vocabulary; RFC §5.4/§8 updated — anti-bloat is proven by the empirical flag-on/off corpus measurement (the `binary-size` CI job only sees the compiler), measured at +0.0063% aggregate over a 32-binary corpus.

RFC §7 acceptance met on the protocol box (min-of-9, cache-busted): real unrolled untyped bcryptjs `_encipher` (`enc_real.ts`) 834 → **126 ms vs Node 134 ms**, byte-exact; post-`-O3` specialized entry has zero `js_dyn_index_get`/`js_dynamic_*`/`js_typed_array_*` calls and zero kind guards (pure-i32 Feistel: `fadd 0`, `add i32 33`, `xor i32 50`). Four new routing gap tests (polymorphic coexistence, view+detach, reassignment, recursion/escape) pass byte-exact flag on/off and under `PERRY_GC_FORCE_EVACUATE=1`; full gap suite: no new untriaged failures.
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,18 @@ impl LlBlock {
/// (poison otherwise); every clamped case is mathematically 0 anyway.
pub fn toint32_wrap(&mut self, val: &str) -> String {
use crate::types::{I1, I32, I64};
// ARMv8.3 FEAT_JSCVT: `fjcvtzs` IS ECMAScript ToInt32 in one
// instruction — truncate toward zero, wrap modulo 2^32, NaN/±Inf/-0
// → 0. Replaces the ~25-op branchless tower below on targets that
// have it (all Apple Silicon); the tower remains the portable path.
if crate::codegen::helpers::jscvt_enabled() {
let r = self.reg();
self.emit(format!(
"{} = call i32 @llvm.aarch64.fjcvtzs(double {})",
r, val
));
return r;
}
let bits = self.bitcast_double_to_i64(val);
let exp_shifted = self.lshr(I64, &bits, "52");
let bexp = self.and(I64, &exp_shifted, "2047");
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,9 @@ pub(super) fn compile_closure(
integer_locals: native_facts.integer_locals(),
not_bigint_locals: native_facts.not_bigint_locals(),
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
// Conservative: treat every slot as possibly-bound (param binds are
// emitted before FnCtx exists here), so clears never get skipped.
shadow_slots_bound: shadow_slot_map.values().copied().collect(),
shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt,
Expand All @@ -866,6 +869,9 @@ pub(super) fn compile_closure(
local_slot_reps: HashMap::new(),
repsel_context_allows_canonical_i32: repsel_allows,
repsel_closure_ref_locals: repsel_closure_refs,
spec_abi_functions: &cross_module.spec_abi_functions,
spec_ta_bindings: &cross_module.spec_ta_bindings,
spec_ta_ready: std::collections::HashSet::new(),
i1_local_slots: HashMap::new(),
index_used_locals: native_facts.index_used_locals(),
strictly_i32_bounded_locals: native_facts.strictly_i32_bounded_locals(),
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ pub(super) fn compile_module_entry(
integer_locals: main_native_facts.integer_locals(),
not_bigint_locals: main_native_facts.not_bigint_locals(),
unsigned_i32_locals: main_native_facts.unsigned_i32_locals(),
shadow_slots_bound: main_shadow_slot_map.values().copied().collect(),
shadow_slot_map: main_shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt: main_shadow_slot_clears_after_stmt,
Expand All @@ -768,6 +769,9 @@ pub(super) fn compile_module_entry(
// import/init machinery; the win lives in function bodies).
repsel_context_allows_canonical_i32: false,
repsel_closure_ref_locals: std::collections::HashSet::new(),
spec_abi_functions: &cross_module.spec_abi_functions,
spec_ta_bindings: &cross_module.spec_ta_bindings,
spec_ta_ready: std::collections::HashSet::new(),
i1_local_slots: HashMap::new(),
index_used_locals: main_native_facts.index_used_locals(),
strictly_i32_bounded_locals: main_native_facts.strictly_i32_bounded_locals(),
Expand Down Expand Up @@ -1360,6 +1364,7 @@ pub(super) fn compile_module_entry(
integer_locals: init_native_facts.integer_locals(),
not_bigint_locals: init_native_facts.not_bigint_locals(),
unsigned_i32_locals: init_native_facts.unsigned_i32_locals(),
shadow_slots_bound: init_shadow_slot_map.values().copied().collect(),
shadow_slot_map: init_shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt: init_shadow_slot_clears_after_stmt,
Expand All @@ -1379,6 +1384,9 @@ pub(super) fn compile_module_entry(
// import/init machinery; the win lives in function bodies).
repsel_context_allows_canonical_i32: false,
repsel_closure_ref_locals: std::collections::HashSet::new(),
spec_abi_functions: &cross_module.spec_abi_functions,
spec_ta_bindings: &cross_module.spec_ta_bindings,
spec_ta_ready: std::collections::HashSet::new(),
i1_local_slots: HashMap::new(),
index_used_locals: init_native_facts.index_used_locals(),
strictly_i32_bounded_locals: init_native_facts.strictly_i32_bounded_locals(),
Expand Down
Loading
Loading