Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7242-i64-spec-exactness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Removed the unproven i64 function specialization (#7238).** `emit_i64_specializations` re-emitted any `number`-typed function body in i64 arithmetic behind an f64 shim that `fptosi`'d every argument and `sitofp`'d the result. Two halves of its contract were unchecked, and they failed independently. **Overflow**: `add`/`sub`/`mul i64` are exact, while JS rounds to the nearest double at *every* operator, so the two agree only while each intermediate satisfies `|v| <= 2^53` — `grow(40, 1)` with `grow = (n, acc) => n === 0 ? acc : grow(n - 1, acc * 3 + 1)` wrapped past 2^63 and printed `-210245885124158400` where Node prints `18236498188585394000`. **Argument truncation**: a `number` parameter is a double, and `fptosi double %arg to i64` truncated a fractional one on entry — `frac(3, 0.5)` printed `0` instead of `4`, and `apply2(mulAdd, 1.5, 2.5)` printed `3` instead of `4.75`. Neither hole is repairable by narrowing the admission rule: #7237's `i32_chain_magnitude_bits` composes *bounded leaves* (an i32 slot, a literal's own width, a masked or shifted value, a `const`'s magnitude), and this pass has none — its leaves are `number` parameters, and it is only observable through self-recursion (where a parameter fed by its own recursive-call argument is unbounded by construction; even `fib(79)` crosses 2^53) or through an indirect call that HIR inlining does not flatten. A sound version needs a runtime guard plus a deopt edge to an f64 body, which the pass deliberately did not emit. Per CLAUDE.md's kill-policy it is removed rather than left as an unprovable mode. Removal also unblocks the specializers it was displacing — `typed_f64_functions`/`typed_i32_functions`/`typed_i1_functions` and the Phase-2 specialized ABI were all retained *minus* the i64-specialized set — so `benchmarks/suite/14_closure.ts`'s `compute` now takes a call-site-guarded `__typed_f64` clone instead of an assumed integer body. Verified by byte-for-byte LLVM IR comparison across all 30 `benchmarks/suite/` programs: 28 unchanged, and the two movers (`05_fibonacci`, `14_closure`) are exactly the two that carried a specialization. `compiler_output_regression.py census --gate` green on both compilers with byte-identical per-workload per-representation tables. New parity case `test-files/test_gap_7238_i64_specialization_exactness.ts` covers both shapes from the issue, an overflow landing between 2^53 and 2^63, fractional arguments, the 2^53 boundary from both sides, an indirect non-recursive call, and the chains that must stay exact — 11 lines diverge from Node 26.5.1 on unfixed `main`, byte-identical after. Cost, measured and stated: `05_fibonacci` is ~20% slower (`fib(40)` 450 ms → 555 ms, fixed arm slower in 9/9 interleaved pairs on a non-quiet host); `14_closure` is within noise. A sound guarded-plus-deopt recursive numeric specialization is tracked as a follow-up.
86 changes: 0 additions & 86 deletions crates/perry-codegen/src/codegen/i64_spec.rs

This file was deleted.

94 changes: 7 additions & 87 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ mod function;
// `pub(crate)` so `crate::linker` can read the inline-hot-small policy
// (`inline_hot_small_enabled` / `inline_hot_small_hint_threshold`).
pub(crate) mod helpers;
mod i64_spec;
mod method;
mod method_registry;
mod module_globals_emit;
#[cfg(test)]
mod number_exactness_tests;
mod opts;
mod spec_abi;
mod string_pool;
Expand Down Expand Up @@ -2152,82 +2153,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
}
}

// Integer-specialization pass. See `i64_spec::emit_i64_specializations`.
let i64_specialized =
i64_spec::emit_i64_specializations(&mut llmod, hir, &func_names, &module_globals);

// From here on, this set means "a typed-f64 clone is present in the
// module", not just "the HIR body was eligible." The i64 specializer owns
// its public wrapper and may skip the ordinary f64 body entirely, so direct
// call lowering must not branch to an unemitted typed-f64 clone.
for f in &hir.functions {
if i64_specialized.contains(&f.id) && cross_module.typed_f64_functions.contains(&f.id) {
record_typed_clone_rejection(
&mut typed_clone_rejection_records,
f.name.clone(),
"typed_f64_function_clone_decision",
typed_abi::TypedCloneRejectionReason::I64Specialized,
vec![
"typed_clone_kind=typed_f64_function".to_string(),
format!("function_id={}", f.id),
format!(
"symbol={}",
func_names.get(&f.id).map(String::as_str).unwrap_or(&f.name)
),
],
);
}
if i64_specialized.contains(&f.id) && cross_module.typed_i32_functions.contains(&f.id) {
record_typed_clone_rejection(
&mut typed_clone_rejection_records,
f.name.clone(),
"typed_i32_function_clone_decision",
typed_abi::TypedCloneRejectionReason::I64Specialized,
vec![
"typed_clone_kind=typed_i32_function".to_string(),
format!("function_id={}", f.id),
format!(
"symbol={}",
func_names.get(&f.id).map(String::as_str).unwrap_or(&f.name)
),
],
);
}
if i64_specialized.contains(&f.id) && cross_module.typed_i1_functions.contains(&f.id) {
record_typed_clone_rejection(
&mut typed_clone_rejection_records,
f.name.clone(),
"typed_i1_function_clone_decision",
typed_abi::TypedCloneRejectionReason::I64Specialized,
vec![
"typed_clone_kind=typed_i1_function".to_string(),
format!("function_id={}", f.id),
format!(
"symbol={}",
func_names.get(&f.id).map(String::as_str).unwrap_or(&f.name)
),
],
);
}
}
cross_module
.typed_f64_functions
.retain(|id| !i64_specialized.contains(id));
cross_module
.typed_i32_functions
.retain(|id| !i64_specialized.contains(id));
cross_module
.typed_i1_functions
.retain(|id| !i64_specialized.contains(id));
cross_module
.typed_i1_function_param_reps
.retain(|id, _| !i64_specialized.contains(id));

// ---- Representation-selection Phase 2: specialized-ABI plan selection.
// Runs AFTER the i64-specialization pass and the typed_abi clone sets so
// mutual exclusion is decidable; the entries themselves are emitted below
// in the pre-public loop. Bounded: one entry per function (the dominant
// tuple), `PERRY_SPECIALIZED_ABI_MAX` per module.
// Runs AFTER the typed_abi clone sets so mutual exclusion is decidable;
// the entries themselves are emitted below in the pre-public loop.
// Bounded: one entry per function (the dominant tuple),
// `PERRY_SPECIALIZED_ABI_MAX` per module.
if spec_abi::spec_abi_enabled() {
let spec_facts = crate::collectors::collect_spec_abi_facts(hir);
let spec_budget = spec_abi::spec_abi_max();
Expand Down Expand Up @@ -2295,13 +2225,6 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
);
continue;
}
if i64_specialized.contains(&f.id) {
reject(
typed_abi::TypedCloneRejectionReason::I64Specialized,
&mut typed_clone_rejection_records,
);
continue;
}
if cross_module.typed_f64_functions.contains(&f.id)
|| cross_module.typed_i32_functions.contains(&f.id)
|| cross_module.typed_i1_functions.contains(&f.id)
Expand Down Expand Up @@ -2434,11 +2357,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
.with_context(|| format!("lowering specialized entry for function '{}'", f.name))?;
}

// Lower each user function into the module (skip i64-specialized ones).
// Lower each user function into the module.
for f in &hir.functions {
if i64_specialized.contains(&f.id) {
continue;
}
let typed_public_trampoline = if cross_module.typed_f64_functions.contains(&f.id) {
Some(typed_abi::TypedFunctionTrampolineKind::F64)
} else if cross_module.typed_i32_functions.contains(&f.id) {
Expand Down
Loading
Loading