## v0.5.847 — fix(runtime,codegen): two numeric-coercion-at-the-boundary bugs surfaced together while probing real-app patterns. **Bug 1 — `isNaN("1")` returned `true`, `Number.isFinite("1")` returned `true`.** Both forms of these globals exist (`isNaN` / `Number.isNaN` and `isFinite` / `Number.isFinite`); they differ in coercion: the bare global coerces via ToNumber, the `Number.`-prefixed variant is strict (any non-Number → false). Pre-fix the codegen at `crates/perry-codegen/src/expr.rs:5186` lowered `Expr::IsNaN` to an inline `fcmp uno x, x` (true iff either operand is NaN by IEEE 754 unordered compare). That idiom is correct for raw numbers, but every NaN-boxed Perry value (strings, pointers, undefined, null) HAS a NaN bit pattern by construction — `isNaN(any-non-number)` was unconditionally true. Separately the `Expr::IsFinite | Expr::NumberIsFinite` arm at line 5594 merged both forms and called `js_is_finite` (coercing), even though the runtime already had a separate strict `js_number_is_finite` (`builtins.rs:1755-1769`) that codegen never called. **Bug 2 — `"abc".padStart(-1, "_")` allocated 4+ GiB of underscores before OOM-killing the process.** Same shape, different boundary: the codegen at `crates/perry-codegen/src/lower_string_method.rs:387` lowered the target-length f64 via `fptosi(DOUBLE → I32)` — LLVM's `fptosi` is undefined behavior on NaN/Infinity and varies by platform; on macOS aarch64 a literal `NaN` constant-folded to a positive INT_MAX-ish value. The runtime signature took the result as `u32`, so any negative i32 (`-1`) reinterpreted to `0xFFFFFFFF` (~4 billion), the `current_len >= target_len` guard fell through unconditionally, and the loop ran 4 billion iterations of `push_char`. **Fixes.** (a) Split the merged `Expr::IsFinite | Expr::NumberIsFinite` arm into two arms; route `IsFinite` → `js_is_finite` (coercing), `NumberIsFinite` → `js_number_is_finite` (strict). (b) Rewrite the `Expr::IsNaN` arm to call `js_is_nan` (coercing) instead of the inline `fcmp uno`. `Expr::NumberIsNaN` was already correct. (c) Change `js_string_pad_start` and `js_string_pad_end` runtime sigs from `u32` to `f64`; drop the codegen `fptosi`+I32 step and forward the DOUBLE through. New runtime helper `to_length_clamped` applies ToLength semantics: NaN / ≤ 0 → 0, Infinity / huge values → a 1 MiB cap (saner than the spec's 2^53-1 — protects against pathological inputs without breaking real-app use). **Validation.** Probes byte-identical to Bun: 15-case `/tmp/probe_isfinite.ts` (all four isNaN/isFinite variants × number/NaN/Infinity/string/bool/null/undefined inputs); 7-case `/tmp/probe_pad_neg.ts` + 3-case `/tmp/probe_pad_nan.ts` (literal/dynamic neg/NaN/Infinity for both padStart and padEnd). 26/28 gap parity tests pass. Same worktree branch as v0.5.838 (`worktree-claude-array-followups`); renumbered through v0.5.842→843→844→845→847 on successive rebases as Ralph's CI / perf / msg_send! / cargo-fmt commits kept landing in parallel.
0 commit comments