Skip to content

fix(codegen): remove the unproven i64 function specialization (#7238) - #7242

Merged
proggeramlug merged 4 commits into
mainfrom
fix/7238-i64-spec-exactness
Aug 2, 2026
Merged

fix(codegen): remove the unproven i64 function specialization (#7238)#7242
proggeramlug merged 4 commits into
mainfrom
fix/7238-i64-spec-exactness

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #7238.

The bug

function grow(n: number, acc: number): number {
  return n === 0 ? acc : grow(n - 1, acc * 3 + 1);
}
function frac(n: number, acc: number): number {
  return n === 0 ? acc : frac(n - 1, acc * 2);
}
node 26.5.1 perry @ f8f1e7188
grow(40, 1) 18236498188585394000 -210245885124158400
frac(3, 0.5) 4 0

Two mechanisms, both in the same pass

emit_i64_specializations re-emitted a whole number-typed function body in
i64 arithmetic and wrapped it in an f64 shim.

1. Overflowcrates/perry-codegen/src/collectors/i64_emit.rs:148-158
(i64_val's Binary arm → add/sub/mul i64), admitted by
crates/perry-codegen/src/collectors/clamp_detect.rs:278 (i64s_expr).
Nothing bounded any intermediate. i64 arithmetic is exact; JS rounds to the
nearest double at every operator, so the two agree only while each
intermediate satisfies |v| <= 2^53. Between 2^53 and 2^63 the answers merely
differ; past 2^63 the chain wraps and the sign flips.

2. Argument truncationcrates/perry-codegen/src/codegen/i64_spec.rs:73
(blk.fptosi(DOUBLE, &format!("%arg{}", p.id), I64)). A number parameter is
a double. fptosi truncated a fractional argument on entry, and the sitofp
at line 79 could not represent a fractional result on the way out.

define double @perry_fn_probe_ts__frac(double %arg0, double %arg1) alwaysinline {
  %r1 = fptosi double %arg0 to i64          ; 0.5 -> 0
  %r2 = fptosi double %arg1 to i64
  %r3 = call i64 @perry_fn_probe_ts__frac_i64(i64 %r1, i64 %r2)
  %r4 = sitofp i64 %r3 to double
  ret double %r4
}

The two are independent: frac(3, 0.5) never leaves the exact range, and
grow(40, 1) is called with exact integers.

The invariant, and why #7237's machinery does not transfer

#7237 stated it for i32:

An integer-native chain computes the exact two's-complement result. JS
evaluates the same chain in doubles, rounding at every operator. They agree
only while every intermediate is exactly representable as a double
(|v| <= 2^53).

At i64 the same bound applies, plus an argument-domain proof the i32 path
did not need: i32_chain_magnitude_bits measures bounded leaves — an i32
slot, an integer literal's own width, a masked or shifted value, a const's
literal magnitude — and composes them (Add/Sub +1 bit, Mul sums widths).

Here there is no bounded leaf. The leaves are number parameters, and the
pass is only observable through two paths, both of which defeat the
measurement:

  • Self-recursion — a parameter fed by its own recursive-call argument is
    unbounded by construction. grow's accumulator, and fib's return value:
    fib(79) already crosses 2^53, so the pass's own motivating example
    diverges from Node.
  • An indirect call — straight-line callers are HIR-inlined before the
    wrapper matters, but a call through a function value is not.
    apply2(mulAdd, 1.5, 2.5) printed 3, not 4.75.

So the admission set under static proof is empty, and repairing it would need
a runtime guard plus a deopt path back to an f64 body — which the pass
deliberately did not emit (i64_specialized suppressed it). Per CLAUDE.md's
kill-policy the pass is removed rather than left as an unprovable mode, which
is option (b) in the issue.

Removal also unblocks the sound specializers the pass 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
(codegen/mod.rs:2213-2224, :2298). 14_closure's compute now takes a
__typed_f64 clone behind a guarded public wrapper — a call-site-proven
specialization instead of an assumed one.

Red then green

test-files/test_gap_7238_i64_specialization_exactness.ts — the issue's two
shapes, an overflow that lands between 2^53 and 2^63, fractional arguments
(positive, negative, and one that truncates to zero), the 2^53 boundary from
both sides (2^52/2^53/2^54, +1 across it, and the negative mirror), a
non-recursive function reached through a function value and through a
higher-order call, comparisons on fractional arguments, and the chains that
must stay exact (fib, fact, sumTo, tri).

  • Unfixed main: 11 lines diverge from node --experimental-strip-types
    26.5.1 — 5 from overflow, 6 from argument truncation.
  • Fixed: byte-identical. It passes, so it is correctly absent from
    test-parity/gap_snapshot.json (which lists only non-passing tests).

Evidence

IR, all 30 benchmarks/suite/ programs, byte-for-byte vs main: 28
unchanged.
The two that move are exactly the two that carried a
specialization:

  • 05_fibonaccifib loses its _i64 body and fptosi shim for an exact
    fcmp/fsub/fadd double body. It does not qualify for a typed-f64 clone
    (its body is not straight-line), so it keeps the ordinary f64 body.
  • 14_closurecompute trades its _i64 body for
    compute__typed_f64 + compute__generic behind js_typed_f64_arg_guard.

Census. compiler_output_regression.py census --gate green on both
compilers, and the per-workload per-representation tables are byte-identical
between arms. No floor lowered, no --update.

Corpus sweep. All 467 test-files/test_gap_*.ts compiled and run under
both compilers with program output compared. 459 ran in both arms; exactly two
differ. One is the new test. The other is test_gap_console_methods, whose
diff is entirely console.time durations (timer1: 0.025ms vs 0.024ms) —
the parity normalizer rewrites those to <timer>. The 9 that did not run
(test_gap_http*, test_gap_net*, test_gap_fetch_*,
test_gap_regex_replace_dyn_regex_with_http) failed to compile identically
in both arms
under the ad-hoc invocation; no error is asymmetric.

Sabotage. Restoring the pass (i64_spec.rs + i64_emit.rs +
is_integer_specializable + the codegen/mod.rs wiring) reds 3 of the 4 new
unit tests. The 4th (fractional_literal_body_stays_on_the_double_path) stays
green by design — the old gate already rejected fractional literals inside a
body (#6221); its point is that the parameter hole was the one left open.

Cost, stated plainly

05_fibonacci regresses. Interleaved A/B on a Mac mini, 9 pairs, fixed arm
slower in 9/9: median 450 ms → 555 ms for fib(40) (~20%). 14_closure is
within noise (59 → 63 ms median, both series drifting with host load).

Two caveats, both real. Neither host was quiet, so these are indicative rather
than controlled. And both arms were built --profile perry-dev (opt-level=1,
no LTO) — the arm-to-arm ratio is what the interleaving buys, but a release
build could narrow or widen it, and I did not measure that. For scale: Node
26.5.1 runs the same fib(40) in ~1500 ms on that host, so the fixed arm is
still ~2.7x faster than Node (the base arm was ~3.3x).

That is a real cost, and it is the right trade: the pass was computing
different numbers than the language specifies, on ordinary reachable code, with
no proof available to narrow it. A sound recursive-numeric specialization —
guarded entry plus a deopt edge to the f64 body — is worth building; it is a
different piece of work from this one; filed as #7244 with the measurement,
the design it needs (guarded entry, a preserved f64 body to deopt into, and a
checked interior), and an acceptance bar.

Gates

  • cargo test -p perry-codegen --lib — 544 pass (540 + 4 new).
  • Integration: i64_spec_ternary_recursion 2/2. native_proof_regressions
    235 pass / 16 fail — the same 16 at origin/main with an unmodified
    tree, so pre-existing and untouched by this diff (15 invalidation::*
    buffer-bounds tests plus
    typed_f64_receiver_method_clone_raw_loads_after_composed_guards).
  • cargo fmt --all; file-size gate 16 offenders on both arms, symmetric
    difference empty.
  • No GC-adjacent codegen touched — the diff is confined to the i64
    specialization pass, its admission predicate, and the rejection-reason
    variant that only it produced. The GC stress matrix does not apply.

`emit_i64_specializations` re-emitted a whole `number`-typed function body in
i64 arithmetic and wrapped it in an f64 shim that `fptosi`d every argument and
`sitofp`d the result. Two independent halves of its contract were unchecked:

  * overflow — i64 add/sub/mul are exact, JS rounds to the nearest double at
    every operator, so the two agree only while every intermediate satisfies
    |v| <= 2^53;
  * argument truncation — a `number` parameter is a double, and the wrapper's
    `fptosi double %arg to i64` truncated a fractional one on entry.

Neither is statically provable for the self-recursive bodies the pass existed
to serve: a parameter fed by its own recursive-call argument has no bound, so
#7237's `i32_chain_magnitude_bits` has no bounded leaf to measure from. Even
the motivating example diverges (`fib(79)` crosses 2^53). The pass also
suppressed the sound replacements — `typed_f64`/`typed_i32`/`typed_i1` clones
and the Phase-2 specialized ABI were all retained minus the i64-specialized
set — so removing it hands those functions to a specializer that proves what
this one assumed.

Claimed 2 of the 30 `benchmarks/suite/` programs (`05_fibonacci`, `14_closure`).

Fixes #7238.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR removes the unsound whole-function i64 specialization path. Normal double lowering remains active, while specialized-ABI and typed-ABI selection use updated exclusions. Tests cover overflow, fractional arguments, recursion, and exact double code generation.

Changes

Number exactness codegen

Layer / File(s) Summary
Remove i64 specialization pipeline
crates/perry-codegen/src/collectors/*, crates/perry-codegen/src/codegen/mod.rs
The i64 admission logic, emitter, module wiring, exports, and lowering skip are removed. Specialized-ABI planning remains bounded and coordinated with typed ABI eligibility.
Update typed ABI contracts
crates/perry-codegen/src/codegen/typed_abi.rs, crates/perry-codegen/src/codegen/typed_abi_opt_report.rs, crates/perry-codegen/src/codegen/opts.rs
The obsolete I64Specialized rejection case and report mapping are removed. The spec_abi_functions documentation reflects the updated exclusivity rules.
Validate double code generation
crates/perry-codegen/src/codegen/number_exactness_tests.rs, crates/perry-codegen/tests/*, test-files/test_gap_7238_i64_specialization_exactness.ts, changelog.d/7242-i64-spec-exactness.md
Regression coverage checks recursive, fractional, overflow, comparison, indirect-call, and ordinary numeric functions for double bodies without i64 wrappers or fptosi truncation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • PerryTS/perry issue 7244 — Proposes a guarded replacement for recursive numeric specialization after the i64 specialization removal.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR removes the unsound i64 specialization and adds tests for overflow, fractional arguments, and numeric boundaries required by issue #7238.
Out of Scope Changes check ✅ Passed The code, documentation, regression tests, and benchmark updates are directly related to removing and validating the i64 specialization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely identifies removal of the unproven i64 function specialization, which is the main change.
Description check ✅ Passed The description thoroughly covers the bug, rationale, changes, related issue, tests, benchmarks, regressions, and trade-offs, despite not using every template heading.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7238-i64-spec-exactness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
crates/perry-codegen/tests/native_proof_regressions.rs (1)

10039-10048: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the fixture to match the new test intent.

The test no longer covers an i64-specialization collision, but it still builds typed_f64_i64_specialized_collision_module(). A name such as number_add_module describes what the fixture now provides. This is optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/native_proof_regressions.rs` around lines 10039 -
10048, Rename the fixture used by
number_add_function_takes_the_typed_f64_clone_not_an_i64_body from
typed_f64_i64_specialized_collision_module to a name describing the number-add
module, such as number_add_module, and update its definition and all references
accordingly.
crates/perry-codegen/src/codegen/mod.rs (1)

2156-2160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the matching comment on the spec_abi_functions initializer.

This comment now correctly states that plan selection runs after the typed-ABI clone sets. The initializer of spec_abi_functions still carries the old wording, which names the removed i64-specialization pass as the ordering constraint. Align both so the ordering contract has one description.

♻️ Proposed comment fix at the initializer
-        // Phase 2 spec-ABI plans are selected AFTER the i64-specialization
-        // pass (mutual exclusion), below; start empty here.
+        // Phase 2 spec-ABI plans are selected AFTER the typed_abi clone sets
+        // (mutual exclusion), below; start empty here.
         spec_abi_functions: std::collections::HashMap::new(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/mod.rs` around lines 2156 - 2160, Update the
comment attached to the spec_abi_functions initializer to describe ordering in
terms of the typed_abi clone sets, matching the Representation-selection Phase 2
comment. Remove the outdated reference to the removed i64-specialization pass
while preserving the existing scope and bounded-selection details.
crates/perry-codegen/tests/i64_spec_ternary_recursion.rs (1)

207-214: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive double-body assertion here too.

Both assertions are negative. After the pass removal neither halfDown_i64 nor an entry fptosi can be emitted for any lowering of this shape, so the test cannot distinguish a correct double body from a broken one. The sibling test at Line 172 already slices the public body and asserts br i1 plus call double. Apply the same positive checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/i64_spec_ternary_recursion.rs` around lines 207 -
214, Add positive assertions in the i64 ternary recursion test alongside the
existing checks, verifying the public double body contains the expected br i1
and call double instructions. Follow the sibling test’s body-slicing approach
rather than relying only on absence of halfDown_i64 or entry fptosi.
crates/perry-codegen/src/codegen/number_exactness_tests.rs (1)

339-340: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive assertion so this test cannot pass vacuously.

The only assertion is that halfDown_i64 is absent. After the pass removal that symbol can never be emitted for any input, so the test holds even if halfDown were lowered incorrectly. The doc comment above claims coverage for "the whole class of reasons". Assert that the public body keeps the double path, as the sibling tests do.

♻️ Proposed strengthening
     let ir = emitted_ir(vec![f]);
     assert!(!ir.contains("halfDown_i64"), "{NO_I64_BODY}:\n{ir}");
+    let body = function_ir(&ir, "`@perry_fn_number_exactness_ts__halfDown`(")
+        .unwrap_or_else(|| panic!("public f64 body for `halfDown` must be emitted:\n{ir}"));
+    assert!(
+        body.contains("call double `@perry_fn_number_exactness_ts__`"),
+        "`halfDown` must recurse through a double-typed body:\n{body}"
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/codegen/number_exactness_tests.rs` around lines 339
- 340, Add a positive assertion in the test around emitted_ir so it verifies the
public halfDown body retains the expected double-path lowering, matching the
sibling tests. Keep the existing absence assertion for halfDown_i64, ensuring
the test checks both that the removed i64 helper is not emitted and that
halfDown itself is lowered correctly.
test-files/test_gap_7238_i64_specialization_exactness.ts (1)

67-68: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

fib(40) dominates the runtime of this parity case.

fib(40) is roughly 1.6e9 calls and, per the changelog, takes ~555 ms after the change. fib(25) on Line 67 already covers the exact-chain shape. If the gap suite runs on every PR, consider lowering the second bound (for example fib(32)) to keep the case cheap. Keep fib(40) if the crossing of 2^53 by intermediate values is the point being asserted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-files/test_gap_7238_i64_specialization_exactness.ts` around lines 67 -
68, Reduce the second Fibonacci workload in the parity case by changing the
fib(40) call near the existing fib(25) check to a lower bound such as fib(32),
since fib(25) already covers the exact-chain shape. Preserve fib(40) only if
this test explicitly asserts intermediate values crossing 2^53.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/codegen/number_exactness_tests.rs`:
- Around line 268-271: Update the assertion in the number exactness test to
check only that the IR does not contain “define i64 `@perry_fn_number_exactness`”.
Remove the unrelated “alwaysinline” disjunction while preserving the existing
failure message and IR context.
- Around line 286-289: Update the argument-truncation assertions in
crates/perry-codegen/src/codegen/number_exactness_tests.rs:286-289,
crates/perry-codegen/tests/i64_spec_ternary_recursion.rs:184-187 and 211-214,
and crates/perry-codegen/tests/native_proof_regressions.rs:10049-10052 to match
each function’s emitted SSA parameter names, such as %arg1, or inspect the
truncation opcode within the sliced public body. Ensure entry truncation using
the actual emitted arguments cannot satisfy these checks.

---

Nitpick comments:
In `@crates/perry-codegen/src/codegen/mod.rs`:
- Around line 2156-2160: Update the comment attached to the spec_abi_functions
initializer to describe ordering in terms of the typed_abi clone sets, matching
the Representation-selection Phase 2 comment. Remove the outdated reference to
the removed i64-specialization pass while preserving the existing scope and
bounded-selection details.

In `@crates/perry-codegen/src/codegen/number_exactness_tests.rs`:
- Around line 339-340: Add a positive assertion in the test around emitted_ir so
it verifies the public halfDown body retains the expected double-path lowering,
matching the sibling tests. Keep the existing absence assertion for
halfDown_i64, ensuring the test checks both that the removed i64 helper is not
emitted and that halfDown itself is lowered correctly.

In `@crates/perry-codegen/tests/i64_spec_ternary_recursion.rs`:
- Around line 207-214: Add positive assertions in the i64 ternary recursion test
alongside the existing checks, verifying the public double body contains the
expected br i1 and call double instructions. Follow the sibling test’s
body-slicing approach rather than relying only on absence of halfDown_i64 or
entry fptosi.

In `@crates/perry-codegen/tests/native_proof_regressions.rs`:
- Around line 10039-10048: Rename the fixture used by
number_add_function_takes_the_typed_f64_clone_not_an_i64_body from
typed_f64_i64_specialized_collision_module to a name describing the number-add
module, such as number_add_module, and update its definition and all references
accordingly.

In `@test-files/test_gap_7238_i64_specialization_exactness.ts`:
- Around line 67-68: Reduce the second Fibonacci workload in the parity case by
changing the fib(40) call near the existing fib(25) check to a lower bound such
as fib(32), since fib(25) already covers the exact-chain shape. Preserve fib(40)
only if this test explicitly asserts intermediate values crossing 2^53.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50c85ecd-fad5-44a9-ba07-17db3e701f57

📥 Commits

Reviewing files that changed from the base of the PR and between f8f1e71 and 5ee81fb.

📒 Files selected for processing (13)
  • changelog.d/7242-i64-spec-exactness.md
  • crates/perry-codegen/src/codegen/i64_spec.rs
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/number_exactness_tests.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/codegen/typed_abi.rs
  • crates/perry-codegen/src/codegen/typed_abi_opt_report.rs
  • crates/perry-codegen/src/collectors/clamp_detect.rs
  • crates/perry-codegen/src/collectors/i64_emit.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/tests/i64_spec_ternary_recursion.rs
  • crates/perry-codegen/tests/native_proof_regressions.rs
  • test-files/test_gap_7238_i64_specialization_exactness.ts
💤 Files with no reviewable changes (4)
  • crates/perry-codegen/src/codegen/typed_abi_opt_report.rs
  • crates/perry-codegen/src/codegen/typed_abi.rs
  • crates/perry-codegen/src/collectors/i64_emit.rs
  • crates/perry-codegen/src/codegen/i64_spec.rs

Comment thread crates/perry-codegen/src/codegen/number_exactness_tests.rs
Comment thread crates/perry-codegen/src/codegen/number_exactness_tests.rs
Ralph Küpper added 2 commits August 2, 2026 10:56
CodeRabbit review on #7242. Three assertions matched the literal
`fptosi double %arg`, which couples them to how parameters happen to be
named in the emitted IR. Match on the opcode alone, scoped to the public
function body, so a rename cannot turn them vacuous — none of these fixtures
has another reason to narrow a double to an integer. Also renames the
`native_proof_regressions` fixture to `number_add_module`, since it no longer
describes an i64-specialization collision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

i64 function specialization evaluates number arithmetic in i64 and truncates fractional arguments

1 participant