Found by the package audit (while fixing #10418) on Perry 7661bc0 (v0.5.1589), Linux x64. When a module is compiled
as more than one codegen unit, BigInt literals that don't fit in 64 bits lose their high bits: a 71-bit literal
becomes 0n, a 97-bit negative literal becomes a different 64-bit value. Single-unit builds are correct. Large real
modules are split automatically, so this silently changes the constants of big BigInt-heavy libraries (crypto curve
parameters, field moduli), not just forced splits.
Reproduction
main.ts:
function f() { const x = 2n ** 70n; return x === 1180591620717411303424n; }
function g() { return 1180591620717411303424n; }
console.log(f(), g() === 2n ** 70n, String(g()));
const small = 12345678901234567890n;
const neg = -98765432109876543210987654321n;
console.log(String(small), String(neg), 2n ** 64n === 18446744073709551616n);
node main.ts
PERRY_NO_AUTO_OPTIMIZE=1 perry compile main.ts -o one && ./one # 1 unit
PERRY_CODEGEN_UNITS=2 PERRY_NO_AUTO_OPTIMIZE=1 perry compile main.ts -o two && ./two # forced split
Expected (Node 26.5.1, and Perry with 1 unit)
true true 1180591620717411303424
12345678901234567890 -98765432109876543210987654321 true
Actual (Perry, PERRY_CODEGEN_UNITS=2 or 4)
false false 0
12345678901234567890 -18444665141527514289 false
The 64-bit literal 12345678901234567890n survives; anything wider is truncated.
Impact
- Every automatically split module containing a >64-bit BigInt literal computes with wrong constants — silently.
Real candidates: @noble/curves (secp256k1/ed25519/BLS field primes and group orders are 255–381-bit literals),
@noble/hashes constants, ethers, big-number libraries, and any large bundle that happens to include them.
- The gap corpus is all single-unit, so no existing test covers this.
Notes
Found by the package audit (while fixing #10418) on Perry 7661bc0 (v0.5.1589), Linux x64. When a module is compiled
as more than one codegen unit, BigInt literals that don't fit in 64 bits lose their high bits: a 71-bit literal
becomes
0n, a 97-bit negative literal becomes a different 64-bit value. Single-unit builds are correct. Large realmodules are split automatically, so this silently changes the constants of big BigInt-heavy libraries (crypto curve
parameters, field moduli), not just forced splits.
Reproduction
main.ts:Expected (Node 26.5.1, and Perry with 1 unit)
Actual (Perry,
PERRY_CODEGEN_UNITS=2or4)The 64-bit literal
12345678901234567890nsurvives; anything wider is truncated.Impact
Real candidates:
@noble/curves(secp256k1/ed25519/BLS field primes and group orders are 255–381-bit literals),@noble/hashesconstants, ethers, big-number libraries, and any large bundle that happens to include them.Notes
(
crates/perry-codegen/src/dialect/), which is the default only for split modules; the wide BigInt literal'sconstant form (large integer / byte-array global) is probably parsed into a 64-bit field or an unrecognized arm.
The precedent for a closed-set reader breaking only split modules is [codegen] In-process LLVM backend fails the 5 biggest Next App Route modules on current main (silent per-module failure; regression in 3c95020f8..07c8040bf) #8228/fix(codegen): teach the in-process IR reader the vector instructions codegen emits (#8228) #8241. An A/B with
PERRY_LLVM_INPROCESS=0(external clang) failed to compile on this host for an unrelated reason, so the readerhypothesis is not yet confirmed.
PERRY_CODEGEN_UNITS=2andchecks wide literals) would catch the whole class of split-only IR regressions.