From cf97a195abd55780d906e6168add37b6315840f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 00:25:10 +0200 Subject: [PATCH 1/2] fix(gc): float arithmetic proves a value is not a NaN-boxed pointer (#7738) Nine false positives turned main red on a gate that no longer has a budget. A load from a module global whose only use is fadd/ordered-fcmp cannot be a heap reference -- codegen emits those only on proven numbers. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- changelog.d/7738-dominance-numeric-proof.md | 18 +++++++++ scripts/gc_root_dominance_check.py | 44 +++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 changelog.d/7738-dominance-numeric-proof.md diff --git a/changelog.d/7738-dominance-numeric-proof.md b/changelog.d/7738-dominance-numeric-proof.md new file mode 100644 index 0000000000..cfd3d541b3 --- /dev/null +++ b/changelog.d/7738-dominance-numeric-proof.md @@ -0,0 +1,18 @@ +### Fixed + +- **`gc-root-dominance-statepoints` was red on `main`, on nine false positives (#7738).** #7732 drove the native lowering's unrooted count to 0 and — correctly, per #7706's precedent — **deleted** `--max-unrooted` rather than setting it to 0. With no budget, any spurious hit is a red build, and nine arrived. + + Every one had the same shape: a `load double` from a module global (`@perry_global_…`), reported as a stale register because the load crosses a statepoint, whose **only use is float arithmetic**: + + ``` + source (global): %r226 = load double, ptr @perry_global_…_ts__1, align 8 + stale use : %r309 = fadd double %r226, %r308 + ``` + + The global is `let churnAcc = 0` in `test_gap_repsel_gc_stress` — a number. Perry represents every JS value as a NaN-boxed double, so "it is a double" says nothing about whether it is a pointer; but float **arithmetic** does. A NaN-box carries its tag in the exponent/mantissa bits and `fadd` destroys it, so codegen emits one only where it has already proven the operand numeric. **The instruction is the proof.** A stale copy of a number is just a number: nothing to rewrite, nothing to dereference. + + `fcmp` gets exactly the **ordered** predicates (`oeq ogt oge olt ole one ord`). An ordered comparison is false unless both operands are non-NaN, and every NaN-boxed reference is a NaN — so an ordered predicate on a boxed pointer is a constant `false` that codegen has no reason to emit. The **unordered** ones are excluded deliberately: `fcmp uno` is precisely how a NaN-box tag check is written, and treating it as numeric proof would blind the checker to the pointer case it exists for. + + The filter is **per-use, not per-source**. A register with one arithmetic use and one dereference is still reported for the dereference — the case that matters, and the one a source-level filter would have silently dropped. + + Verified in both directions rather than assumed. `--self-test` still passes, so the checker can still fail. And the predicate was exercised against ten instruction shapes: `fadd` / ordered `fcmp` / `fmul fast` are filtered; `js_object_get_field_by_name_f64`, `bitcast … to i64`, `store`, `fcmp uno`, `fcmp une`, `js_nanbox_get_pointer` and `inttoptr` are all still reported. Corpus: `131/131 sources compiled, 0 skipped` → `within budget: unrooted 0 <= 0`. diff --git a/scripts/gc_root_dominance_check.py b/scripts/gc_root_dominance_check.py index 8afbeda67e..e5d998ebb2 100755 --- a/scripts/gc_root_dominance_check.py +++ b/scripts/gc_root_dominance_check.py @@ -2119,6 +2119,46 @@ def seeded_violation_test(paths, moving_only, anchor, want_sites, verbose=False) ) +# Uses that PROVE the value is a raw number, not a NaN-boxed reference. +# +# Perry represents every JS value as a NaN-boxed double, so "it is a double" +# says nothing about whether it is a pointer. But float ARITHMETIC does: a +# NaN-box carries its tag in the exponent/mantissa bits, and `fadd`-ing one +# destroys it. Codegen therefore emits these only where it has already proven +# the operand numeric — the instruction is the proof, not a heuristic about it. +# +# #7738: without this, every module-level `let n = 0` read inside a loop was +# reported as an unrooted stale register. `test_gap_repsel_gc_stress`'s +# `churnAcc` produced eight such hits (`taSum`, `taMix`, `churn`, +# `shapeAndI32` and their `$spec` clones), all with `fadd` as the "stale use". +# A stale NUMBER is just a number; there is nothing for the collector to +# rewrite and nothing to dereference. +# +# Deliberately per-USE, not per-source. A register with one arithmetic use and +# one dereference is still reported for the dereference — which is the case +# that matters, and which a source-level filter would have silently dropped. +# `fcmp` needs care and gets exactly the ORDERED predicates. An ordered +# comparison (`oeq ogt oge olt ole one ord`) is false unless BOTH operands are +# non-NaN — and every NaN-boxed reference is, by construction, a NaN. So an +# ordered predicate on a boxed pointer is a constant `false`, which codegen has +# no reason to emit; seeing one is proof the operand is a real number. +# +# The UNORDERED predicates (`une uno ueq ugt uge ult ule`) are excluded on +# purpose: `fcmp uno` is exactly how a NaN-box tag check is written, so treating +# it as numeric proof would blind the checker to the pointer case it exists for. +NUMERIC_PROOF_USE_RE = re.compile( + r"=\s*(?:fadd|fsub|fmul|fdiv|frem" + r"|fcmp\s+(?:fast\s+|nnan\s+|ninf\s+|nsz\s+|arcp\s+|contract\s+|afn\s+|reassoc\s+)*" + r"(?:oeq|ogt|oge|olt|ole|one|ord)\s" + r")" +) + + +def use_proves_numeric(text): + """Does this instruction prove its operands are raw numbers?""" + return NUMERIC_PROOF_USE_RE.search(text) is not None + + def rewritten_load_kind(text): """Which collector-rewritten global does this load read, if any? @@ -3575,6 +3615,10 @@ def check_func_statepoints(module, f, want_moving_only=False, continue if not uses(use.text, chain): continue + # #7738: float arithmetic proves the operand is a raw + # number, so a stale copy of it is harmless. + if use_proves_numeric(use.text): + continue if use.block == src.block and use.idx <= src.idx: continue sps = window_hits_generic(f, src, use, pred=_is_statepoint) From 63a3a17f7b84e9bf73e54429488cf9642c7c4c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 00:25:24 +0200 Subject: [PATCH 2/2] chore: bump version to 0.5.1430 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 21359a573e..9eb6fdbe0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1429 +**Current Version:** 0.5.1430 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 1e85d2779d..4fc65cba83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1429" +version = "0.5.1430" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1429" +version = "0.5.1430" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1429" +version = "0.5.1430" [[package]] name = "perry-ui-tvos" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1429" +version = "0.5.1430" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 9b8ba1178c..567df66aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1429" +version = "0.5.1430" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"