From 5c987fa9118c7790995c9cef977f8ca9337e24e8 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 23 Aug 2026 23:27:23 -0400 Subject: [PATCH 1/6] fix: unstick main's red lint and cargo-test gates (stale census/ratchet assertions, unclassified codegen env vars) Three separate pre-existing failures on main's own latest commit, none caused by an in-flight PR: shape_descriptor_census.py asserted a select()-based fail-closed token shape that PR #8665 legitimately replaced with icmp_ne, and was missing two legitimate call sites PR #8680 added; addr_class_inventory.py's lone-valid-obj-ptr rule used a narrower ad hoc lookahead than the shared band_predicate_near helper, flagging a correctly-paired guard PR #8680 added as a false positive; and codegen_env_vars_are_build_cache_inputs failed because PERRY_CONST_ARRAY_DESCRIPTOR and PERRY_DIALECT_DUMP were never classified into BUILD_CACHE_ENV_VARS/BUILD_CACHE_ENV_EXCLUSIONS. --- CLAUDE.md | 2 +- Cargo.lock | 154 +++++++++--------- Cargo.toml | 2 +- .../8684-fix-main-red-lint-cargo-test.md | 5 + .../perry/src/commands/compile/build_cache.rs | 8 + scripts/addr_class_inventory.py | 6 +- scripts/addr_class_ratchet_baseline.txt | 41 +---- scripts/shape_descriptor_census.py | 2 +- scripts/shape_descriptor_census_baseline.json | 4 +- 9 files changed, 106 insertions(+), 118 deletions(-) create mode 100644 changelog.d/8684-fix-main-red-lint-cargo-test.md diff --git a/CLAUDE.md b/CLAUDE.md index 61bf8dcebb..94d8b53bf4 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.1519 +**Current Version:** 0.5.1520 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index c1f2dc8119..02412a7317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5624,7 +5624,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -5686,7 +5686,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-dispatch", "serde", @@ -5694,7 +5694,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "cc", "libc", @@ -5703,7 +5703,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "inkwell", @@ -5720,7 +5720,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5728,7 +5728,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-dispatch", @@ -5745,7 +5745,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5753,7 +5753,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -5765,7 +5765,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -5773,7 +5773,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "async-trait", @@ -5802,14 +5802,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "serde", "serde_json", @@ -5817,7 +5817,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5828,7 +5828,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "clap", @@ -5843,7 +5843,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "block2", "objc2", @@ -5853,7 +5853,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "argon2", "perry-ffi", @@ -5862,7 +5862,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "reqwest", @@ -5871,7 +5871,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bcrypt", "perry-ffi", @@ -5879,7 +5879,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rusqlite", @@ -5887,7 +5887,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "scraper", @@ -5895,7 +5895,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -5903,7 +5903,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "cron", @@ -5913,7 +5913,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rust_decimal", @@ -5929,7 +5929,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "serde_json", @@ -5937,7 +5937,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -5953,14 +5953,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "http-body-util", @@ -5978,7 +5978,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "lazy_static", @@ -5991,7 +5991,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "bytes", @@ -6023,7 +6023,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lazy_static", "perry-ffi", @@ -6033,7 +6033,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lru", "perry-ffi", @@ -6053,7 +6053,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bson", "futures-util", @@ -6073,7 +6073,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "chrono", "perry-ffi", @@ -6083,7 +6083,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "nanoid", "perry-ffi", @@ -6092,7 +6092,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "bytes", "perry-ffi", @@ -6106,7 +6106,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "const-oid 0.10.2", "der 0.8.0", @@ -6125,7 +6125,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lettre", "perry-ffi", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "fancy-regex", "notify", @@ -6147,7 +6147,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "printpdf", @@ -6155,7 +6155,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "sqlx", @@ -6164,7 +6164,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "governor", "perry-ffi", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "fast_image_resize", "image", @@ -6182,7 +6182,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "lazy_static", "perry-ffi", @@ -6191,7 +6191,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "serde", @@ -6207,7 +6207,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "perry-runtime", @@ -6216,7 +6216,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "uuid", @@ -6224,7 +6224,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ffi", "regex", @@ -6234,7 +6234,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "futures-util", "lazy_static", @@ -6247,7 +6247,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "brotli", "flate2", @@ -6257,7 +6257,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "dashmap", "once_cell", @@ -6266,7 +6266,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-api-manifest", @@ -6284,7 +6284,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-diagnostics", @@ -6296,7 +6296,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -6339,14 +6339,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6441,14 +6441,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "perry-hir", @@ -6457,14 +6457,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "itoa", @@ -6482,7 +6482,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "rand 0.10.1", "serde", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.0", @@ -6515,7 +6515,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6548,7 +6548,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-ui-test" @@ -6559,11 +6559,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1519" +version = "0.5.1520" [[package]] name = "perry-ui-tvos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6580,7 +6580,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "block2", @@ -6597,7 +6597,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "block2", "libc", @@ -6611,7 +6611,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "base64 0.22.1", "libc", @@ -6630,14 +6630,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "anyhow", "base64 0.22.1", @@ -6653,7 +6653,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1519" +version = "0.5.1520" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 0826ae1c39..ab2e5b9b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -316,7 +316,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1519" +version = "0.5.1520" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/8684-fix-main-red-lint-cargo-test.md b/changelog.d/8684-fix-main-red-lint-cargo-test.md new file mode 100644 index 0000000000..01b29b3f6e --- /dev/null +++ b/changelog.d/8684-fix-main-red-lint-cargo-test.md @@ -0,0 +1,5 @@ +Fixed three pre-existing `main` gate failures, none caused by an in-flight PR: + +- `scripts/shape_descriptor_census.py` still asserted the old `select(I1, &is_stamp, I64, &id_token, "0")` fail-closed shape in the generic property-read PIC. PR #8665 legitimately replaced it with `icmp_ne(I32, &pcid, "0")` (a documented six-instruction perf win) but never updated this assertion. It also missed the two new, legitimate `object_header_size_bytes(...)` call sites `crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs` gained in PR #8680 — the checked-in baseline is refreshed to include them. +- `scripts/addr_class_inventory.py`'s `lone-valid-obj-ptr` rule reimplemented a narrower, ad hoc lookahead window (1 code line) instead of reusing the already-tested `band_predicate_near` helper (6 code lines, comment/blank-aware) used by the `handle-floor` rule. That gap flagged a legitimately paired `is_valid_obj_ptr` + `try_read_gc_header` guard PR #8680 added in `crates/perry-runtime/src/array/subclass.rs` as a false positive. Fixing the rule to call the shared helper also cleared several other long-standing false positives across the tree, so the ratchet baseline is refreshed down to the now-accurate counts. +- `commands::compile::build_cache::tests::codegen_env_vars_are_build_cache_inputs` failed because two new codegen env vars were never classified: `PERRY_CONST_ARRAY_DESCRIPTOR` (PR #8583's array-literal const-descriptor gate — changes emitted IR, added to `BUILD_CACHE_ENV_VARS`) and `PERRY_DIALECT_DUMP` (only read on an already-fatal dialect-construction failure, so it can't affect a successful build's bytes — added to `BUILD_CACHE_ENV_EXCLUSIONS`). diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index 725ecfae31..0cced3d6dd 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -115,6 +115,10 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ // #8105 — number-by-construction locals (see the collector of the same // name); `=0` empties the fact and changes every affected function's IR. "PERRY_NUMBER_BY_CONSTRUCTION", + // #8583 follow-up gate: `=0/off/false` reverts every large constant array + // literal from the const-descriptor path to procedural construction — + // different emitted IR for the same source. + "PERRY_CONST_ARRAY_DESCRIPTOR", ]; /// #7183: codegen env vars that deliberately do NOT key the build cache. @@ -147,6 +151,10 @@ const BUILD_CACHE_ENV_EXCLUSIONS: &[&str] = &[ "PERRY_CODEGEN_UNIT_TIMINGS", // Entry outlining report output is observational only. "PERRY_OUTLINE_ENTRY_REPORT", + // Only read on an already-fatal dialect-construction failure (a unit that + // never parses); it writes a diagnostic IR dump to `/.ll` for + // triage and cannot affect the bytes of any build that actually succeeds. + "PERRY_DIALECT_DUMP", ]; #[cfg(test)] diff --git a/scripts/addr_class_inventory.py b/scripts/addr_class_inventory.py index d16f55e2b6..66a203ac2c 100644 --- a/scripts/addr_class_inventory.py +++ b/scripts/addr_class_inventory.py @@ -214,11 +214,7 @@ def scan_text(rel_path: str, text: str) -> list[Finding]: findings.append(Finding(rel_path, line_no, "handle-floor", raw)) if VALID_OBJ_PTR_RE.search(code) and "fn is_valid_obj_ptr" not in code: # A band predicate anywhere in the enclosing guard clears it. - start = max(0, idx - BAND_PREDICATE_LOOKBACK) - context = "\n".join( - strip_comment(l) for l in lines[start : idx + 2] - ) - if not BAND_PREDICATE_RE.search(context): + if not band_predicate_near(lines, idx): findings.append( Finding(rel_path, line_no, "lone-valid-obj-ptr", raw) ) diff --git a/scripts/addr_class_ratchet_baseline.txt b/scripts/addr_class_ratchet_baseline.txt index b0156607e1..be65a3e608 100644 --- a/scripts/addr_class_ratchet_baseline.txt +++ b/scripts/addr_class_ratchet_baseline.txt @@ -22,13 +22,17 @@ # # Regenerate: python3 scripts/addr_class_inventory.py --write-baseline +handle-floor | crates/perry-ext-events/src/lib.rs | 3 +handle-floor | crates/perry-ext-exponential-backoff/src/lib.rs | 1 +handle-floor | crates/perry-ext-fastify/src/server.rs | 1 +handle-floor | crates/perry-ext-http/src/agent.rs | 3 +handle-floor | crates/perry-ext-http/src/lib.rs | 2 handle-floor | crates/perry-runtime/src/array/alloc.rs | 2 handle-floor | crates/perry-runtime/src/array/concat_reverse.rs | 1 handle-floor | crates/perry-runtime/src/array/flat_clone.rs | 4 handle-floor | crates/perry-runtime/src/array/generic.rs | 4 handle-floor | crates/perry-runtime/src/array/header.rs | 3 handle-floor | crates/perry-runtime/src/array/indexing.rs | 4 -handle-floor | crates/perry-runtime/src/array/iter_methods.rs | 2 handle-floor | crates/perry-runtime/src/array/iter_object.rs | 1 handle-floor | crates/perry-runtime/src/array/iterator.rs | 2 handle-floor | crates/perry-runtime/src/array/push_pop.rs | 1 @@ -105,7 +109,7 @@ handle-floor | crates/perry-runtime/src/object/buffer_dispatch.rs | 2 handle-floor | crates/perry-runtime/src/object/dataview_proto_thunks.rs | 1 handle-floor | crates/perry-runtime/src/object/delete_rest.rs | 1 handle-floor | crates/perry-runtime/src/object/descriptor_state.rs | 1 -handle-floor | crates/perry-runtime/src/object/descriptors.rs | 2 +handle-floor | crates/perry-runtime/src/object/descriptors.rs | 1 handle-floor | crates/perry-runtime/src/object/field_get_set/accessors.rs | 2 handle-floor | crates/perry-runtime/src/object/field_get_set/enumeration.rs | 4 handle-floor | crates/perry-runtime/src/object/field_get_set/field_ops.rs | 3 @@ -126,7 +130,6 @@ handle-floor | crates/perry-runtime/src/object/native_call_method/collection_met handle-floor | crates/perry-runtime/src/object/native_call_method/common_methods.rs | 1 handle-floor | crates/perry-runtime/src/object/native_call_method/handle_methods.rs | 2 handle-floor | crates/perry-runtime/src/object/native_call_method/primitive_methods.rs | 1 -handle-floor | crates/perry-runtime/src/object/native_module.rs | 1 handle-floor | crates/perry-runtime/src/object/native_module/namespace_builders.rs | 1 handle-floor | crates/perry-runtime/src/object/native_module/web_locks.rs | 1 handle-floor | crates/perry-runtime/src/object/object_literal_ops.rs | 1 @@ -142,7 +145,7 @@ handle-floor | crates/perry-runtime/src/object/polymorphic_index.rs | 2 handle-floor | crates/perry-runtime/src/object/property_key.rs | 1 handle-floor | crates/perry-runtime/src/object/prototype_chain.rs | 2 handle-floor | crates/perry-runtime/src/object/prototype_helpers.rs | 1 -handle-floor | crates/perry-runtime/src/object/reflect_support.rs | 3 +handle-floor | crates/perry-runtime/src/object/reflect_support.rs | 2 handle-floor | crates/perry-runtime/src/object/to_string_tag.rs | 10 handle-floor | crates/perry-runtime/src/object/typed_array_define.rs | 1 handle-floor | crates/perry-runtime/src/object/typed_array_proto_thunks.rs | 1 @@ -191,9 +194,6 @@ handle-floor | crates/perry-runtime/src/value/equality.rs | 2 handle-floor | crates/perry-runtime/src/value/nanbox.rs | 1 handle-floor | crates/perry-runtime/src/value/to_string.rs | 3 handle-floor | crates/perry-runtime/src/weakref.rs | 2 -handle-floor | crates/perry-stdlib/src/axios.rs | 1 -handle-floor | crates/perry-stdlib/src/container/mod.rs | 1 -handle-floor | crates/perry-stdlib/src/container/types.rs | 1 handle-floor | crates/perry-stdlib/src/crypto/kdf.rs | 4 handle-floor | crates/perry-stdlib/src/crypto/keys.rs | 1 handle-floor | crates/perry-stdlib/src/crypto/random.rs | 1 @@ -203,7 +203,6 @@ handle-floor | crates/perry-stdlib/src/domain.rs | 1 handle-floor | crates/perry-stdlib/src/events.rs | 1 handle-floor | crates/perry-stdlib/src/exponential_backoff.rs | 1 handle-floor | crates/perry-stdlib/src/fetch/dispatch.rs | 4 -handle-floor | crates/perry-stdlib/src/fetch/mod.rs | 1 handle-floor | crates/perry-stdlib/src/jsonwebtoken.rs | 1 handle-floor | crates/perry-stdlib/src/querystring.rs | 5 handle-floor | crates/perry-stdlib/src/readline/mod.rs | 1 @@ -213,15 +212,12 @@ handle-floor | crates/perry-stdlib/src/streams/byob.rs | 1 handle-floor | crates/perry-stdlib/src/streams/subclass.rs | 1 handle-floor | crates/perry-stdlib/src/streams/transform.rs | 1 handle-floor | crates/perry-stdlib/src/string_decoder.rs | 5 -handle-floor | crates/perry-stdlib/src/tls.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/aes.rs | 4 handle-floor | crates/perry-stdlib/src/webcrypto/hmac.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/jwk.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/supports.rs | 1 handle-floor | crates/perry-stdlib/src/webcrypto/util.rs | 5 -handle-floor | crates/perry-stdlib/src/worker_threads.rs | 1 handle-floor | crates/perry-stdlib/src/zlib.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/array/subclass.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/buffer/access.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/closure/dynamic_props.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/collection_iter.rs | 1 @@ -229,23 +225,20 @@ lone-valid-obj-ptr | crates/perry-runtime/src/error.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/intl.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/intl/ctor_guard.rs | 2 lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/class_meta.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 6 +lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/construct.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/object/class_registry/prototype_objects.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/descriptors.rs | 2 +lone-valid-obj-ptr | crates/perry-runtime/src/object/descriptors.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/accessors.rs | 2 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/enumeration.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/field_ops.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/has_property.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_get_set/ic_miss.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/field_set_by_name/attr_variants.rs | 2 lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/array_error.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/fetch_globals.rs | 1 -lone-valid-obj-ptr | crates/perry-runtime/src/object/global_this/typed_array.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/iterator_prototypes.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method.rs | 3 lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/common_methods.rs | 2 -lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/primitive_methods.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/native_call_method/proto_dispatch.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/native_this_alias.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/object/object_literal_ops.rs | 1 @@ -271,19 +264,3 @@ lone-valid-obj-ptr | crates/perry-runtime/src/util_mime.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/util_style_text.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/value/dyn_index.rs | 1 lone-valid-obj-ptr | crates/perry-runtime/src/value/to_string.rs | 1 - -# #7272: `crates/perry-ext-*` entered this gate's scope. These ten sites are -# not new code — they were invisible because the scan roots stopped at -# perry-runtime/perry-stdlib while the sibling gate -# (gc_store_site_inventory.py) had globbed the ext crates all along. -# -# Five of them are the HTTP server's, which #6826 moved out of -# crates/perry-stdlib/src/http.rs. This file used to carry an entry for that -# path, and once the file vanished the gate reported "baseline says 11, found -# 0 -- lower it to 0": an invitation to ratify a coverage loss as a fix. They -# are baselined here at their real counts instead. -handle-floor | crates/perry-ext-events/src/lib.rs | 3 -handle-floor | crates/perry-ext-exponential-backoff/src/lib.rs | 1 -handle-floor | crates/perry-ext-fastify/src/server.rs | 1 -handle-floor | crates/perry-ext-http/src/agent.rs | 3 -handle-floor | crates/perry-ext-http/src/lib.rs | 2 diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index c62a800629..6de4315b6d 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -582,7 +582,7 @@ def assert_authority_surfaces(sources: dict[str, str]) -> None: ) require_code( generic_body, - r"select\s*\(\s*I1\s*,\s*&is_stamp\s*,\s*I64\s*,\s*&id_token\s*,\s*\"0\"\s*\)", + r"icmp_ne\s*\(\s*I32\s*,\s*&pcid\s*,\s*\"0\"\s*\)", "generic read PIC invalid-id fail-closed token", ) for name in ("lower_put_value_static_write_ic", "lower_put_value_dyn_ic_inline"): diff --git a/scripts/shape_descriptor_census_baseline.json b/scripts/shape_descriptor_census_baseline.json index 02529bfde0..4114a6348f 100644 --- a/scripts/shape_descriptor_census_baseline.json +++ b/scripts/shape_descriptor_census_baseline.json @@ -2,6 +2,8 @@ "codegen_object_header_size_callsite_multiset": { "crates/perry-codegen/src/codegen/artifacts.rs|crate::target_layout::object_header_size_bytes(target_triple),": 1, "crates/perry-codegen/src/expr/element_shape_guard.rs|let header_skip = crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, + "crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs|let meta_offset = (crate::target_layout::object_header_size_bytes(ctx.target_triple)": 1, "crates/perry-codegen/src/expr/member_update.rs|let header_skip = crate::target_layout::object_header_size_bytes(": 1, "crates/perry-codegen/src/expr/property_get.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple)": 3, "crates/perry-codegen/src/expr/property_get/generic_dispatch.rs|crate::target_layout::object_header_size_bytes(ctx.target_triple).to_string();": 1, @@ -45,7 +47,7 @@ "crates/perry-runtime/src/object/object_ops.rs|keys_array|declaration|pub(crate) use keys_array::{": 1 }, "summary": { - "codegen_object_header_size_sites": 34, + "codegen_object_header_size_sites": 36, "raw_member_files": 7, "raw_member_sites": { "keys_array": 24 From 36fccd90ca49ea7ac46ad8335086ad9e23259c8e Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 23 Aug 2026 23:33:11 -0400 Subject: [PATCH 2/6] fix: swap test_runner.rs's raw thread_local! for the hot-path macro scripts/check_thread_locals.py started failing on main because PR #8640 added a raw thread_local! block instead of crate::perry_thread_local! (#7469's hot-path macro; same syntax, same .with() call sites). --- changelog.d/8684-fix-main-red-lint-cargo-test.md | 3 ++- crates/perry-runtime/src/node_submodules/test_runner.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.d/8684-fix-main-red-lint-cargo-test.md b/changelog.d/8684-fix-main-red-lint-cargo-test.md index 01b29b3f6e..44401e5144 100644 --- a/changelog.d/8684-fix-main-red-lint-cargo-test.md +++ b/changelog.d/8684-fix-main-red-lint-cargo-test.md @@ -1,5 +1,6 @@ -Fixed three pre-existing `main` gate failures, none caused by an in-flight PR: +Fixed four pre-existing `main` gate failures, none caused by an in-flight PR: - `scripts/shape_descriptor_census.py` still asserted the old `select(I1, &is_stamp, I64, &id_token, "0")` fail-closed shape in the generic property-read PIC. PR #8665 legitimately replaced it with `icmp_ne(I32, &pcid, "0")` (a documented six-instruction perf win) but never updated this assertion. It also missed the two new, legitimate `object_header_size_bytes(...)` call sites `crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs` gained in PR #8680 — the checked-in baseline is refreshed to include them. - `scripts/addr_class_inventory.py`'s `lone-valid-obj-ptr` rule reimplemented a narrower, ad hoc lookahead window (1 code line) instead of reusing the already-tested `band_predicate_near` helper (6 code lines, comment/blank-aware) used by the `handle-floor` rule. That gap flagged a legitimately paired `is_valid_obj_ptr` + `try_read_gc_header` guard PR #8680 added in `crates/perry-runtime/src/array/subclass.rs` as a false positive. Fixing the rule to call the shared helper also cleared several other long-standing false positives across the tree, so the ratchet baseline is refreshed down to the now-accurate counts. - `commands::compile::build_cache::tests::codegen_env_vars_are_build_cache_inputs` failed because two new codegen env vars were never classified: `PERRY_CONST_ARRAY_DESCRIPTOR` (PR #8583's array-literal const-descriptor gate — changes emitted IR, added to `BUILD_CACHE_ENV_VARS`) and `PERRY_DIALECT_DUMP` (only read on an already-fatal dialect-construction failure, so it can't affect a successful build's bytes — added to `BUILD_CACHE_ENV_EXCLUSIONS`). +- `scripts/check_thread_locals.py` failed because PR #8640 added a raw `thread_local!` block in `crates/perry-runtime/src/node_submodules/test_runner.rs` instead of `crate::perry_thread_local!`. Swapped it to the hot-path macro (same syntax, same `.with()` call sites). diff --git a/crates/perry-runtime/src/node_submodules/test_runner.rs b/crates/perry-runtime/src/node_submodules/test_runner.rs index 4ce596a49e..bd2b6af912 100644 --- a/crates/perry-runtime/src/node_submodules/test_runner.rs +++ b/crates/perry-runtime/src/node_submodules/test_runner.rs @@ -100,7 +100,7 @@ enum HookKind { AfterEach, } -thread_local! { +crate::perry_thread_local! { static TEST_RUNNER: RefCell = RefCell::new(RunnerState::new()); static ACTIVE_CHILDREN: RefCell>> = const { RefCell::new(Vec::new()) }; static ACTIVE_ANCESTORS: RefCell> = const { RefCell::new(Vec::new()) }; From 6bf6be3db8199f893b7fc4bd39c4f73cd8e4838d Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 23 Aug 2026 23:51:00 -0400 Subject: [PATCH 3/6] fix(addr-class): scope lone-valid-obj-ptr's band-predicate match to its own statement band_predicate_near's generic 6-line forward scan is right for handle-floor (#6321's fix shape deliberately spans two separate `if` statements), but it let lone-valid-obj-ptr clear a real dereference finding using a band predicate from a wholly disconnected LATER sibling conditional -- caught by CodeRabbit on PR #8685. Give lone-valid-obj-ptr its own scoped variant that stops the forward scan at the next sibling if/while/for/match, which still accepts the real guard-then-early-return-then-predicate shape (array/subclass.rs) while closing the false negative. --- scripts/addr_class_inventory.py | 74 ++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/scripts/addr_class_inventory.py b/scripts/addr_class_inventory.py index 66a203ac2c..268c92ead8 100644 --- a/scripts/addr_class_inventory.py +++ b/scripts/addr_class_inventory.py @@ -194,6 +194,44 @@ def band_predicate_near(lines: list[str], idx: int) -> bool: return bool(BAND_PREDICATE_RE.search("\n".join(context))) +# A new sibling conditional at the top of a code line — the boundary +# `lone_band_predicate_near` refuses to cross. `handle-floor`'s own #6321 fix +# shape deliberately spans two separate `if` statements (a coarse range +# pre-filter, then the real band-predicate gate a few lines below), so +# `band_predicate_near`'s generic forward scan must keep allowing that. But +# `lone-valid-obj-ptr`'s real safe shape is "guard early-returns, the very +# next statement is the band-predicate call" (see array/subclass.rs) — it +# never needs to reach into an unrelated LATER conditional to find its +# predicate. Stopping there closes the false negative a CodeRabbit review +# caught: an `is_valid_obj_ptr`-only guard that already dereferences inside +# its own block must not be cleared by some other, disconnected `if +# is_handle_band(...)` a few statements later. +SIBLING_CONDITIONAL_RE = re.compile(r"^\s*(?:\}\s*)?(?:else\s+)?(?:if|while|for|match)\b") + + +def lone_band_predicate_near(lines: list[str], idx: int) -> bool: + """`band_predicate_near`, scoped to the current guard's own statement. + + Same backward window; the forward scan stops (without including) at the + first later line that opens a new sibling conditional, since the real + fix shape never needs to reach past that boundary. + """ + + start = max(0, idx - BAND_PREDICATE_LOOKBACK) + context = [strip_comment(line) for line in lines[start : idx + 1]] + taken = 0 + cursor = idx + 1 + while cursor < len(lines) and taken < BAND_PREDICATE_LOOKAHEAD_CODE: + code = strip_comment(lines[cursor]) + if code.strip(): + if SIBLING_CONDITIONAL_RE.search(code): + break + context.append(code) + taken += 1 + cursor += 1 + return bool(BAND_PREDICATE_RE.search("\n".join(context))) + + def scan_text(rel_path: str, text: str) -> list[Finding]: findings: list[Finding] = [] if any(rel_path.startswith(prefix) for prefix in EXCLUDED_PREFIXES): @@ -214,7 +252,7 @@ def scan_text(rel_path: str, text: str) -> list[Finding]: findings.append(Finding(rel_path, line_no, "handle-floor", raw)) if VALID_OBJ_PTR_RE.search(code) and "fn is_valid_obj_ptr" not in code: # A band predicate anywhere in the enclosing guard clears it. - if not band_predicate_near(lines, idx): + if not lone_band_predicate_near(lines, idx): findings.append( Finding(rel_path, line_no, "lone-valid-obj-ptr", raw) ) @@ -410,6 +448,40 @@ def expect(cond: bool, message: str) -> None: ), "lone-valid-obj-ptr must not flag the definition", ) + # The real safe shape (array/subclass.rs): a guard that only early-returns, + # then the very next statement is the band predicate. + early_return_then_predicate = ( + " if obj.is_null() || !is_valid_obj_ptr(obj.cast::()) {\n" + " return None;\n" + " }\n" + " let header = unsafe { try_read_gc_header(obj as usize)? };\n" + ) + expect( + not any( + f.rule == "lone-valid-obj-ptr" + for f in scan_text(runtime, early_return_then_predicate) + ), + "lone-valid-obj-ptr must accept a guard that early-returns " + "then reads the band predicate in the very next statement", + ) + # CodeRabbit (PR #8685): a dereference inside an UNGUARDED is_valid_obj_ptr + # block must still be flagged even when a wholly unrelated, later sibling + # `if` happens to test a band predicate — that predicate does not guard + # the dereference above it. + separated_statement = ( + " if is_valid_obj_ptr(ptr) {\n" + " (*ptr).class_id\n" + " }\n" + " if is_handle_band(other) {}\n" + ) + expect( + any( + f.rule == "lone-valid-obj-ptr" + for f in scan_text(runtime, separated_statement) + ), + "lone-valid-obj-ptr must still flag a dereference cleared only by " + "a disconnected LATER sibling conditional's band predicate", + ) # Band literals in code are caught; comment-only mentions are not. hits = scan_text(runtime, "if addr < 0x100000 {\n") From d411f020e6ce00d71fcbdc87e72ac69c63fe53e7 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 23 Aug 2026 23:56:29 -0400 Subject: [PATCH 4/6] fix(addr-class): track brace depth so lone-valid-obj-ptr accepts nested guards Stopping the forward scan at any later if/while/for/match (6bf6be3db) was too blunt: it also stopped on a band predicate NESTED inside the same guard's block, e.g. `if is_valid_obj_ptr(ptr) { if is_above_handle_band(...) { deref } }` -- caught by a second CodeRabbit pass on PR #8685. Track brace depth relative to the guard's own line and only treat a later conditional as the disconnected-sibling boundary once depth has returned to zero, i.e. the guard's block (and anything nested inside it) has actually closed. --- scripts/addr_class_inventory.py | 37 +++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/scripts/addr_class_inventory.py b/scripts/addr_class_inventory.py index 268c92ead8..dc4df27fe5 100644 --- a/scripts/addr_class_inventory.py +++ b/scripts/addr_class_inventory.py @@ -194,7 +194,7 @@ def band_predicate_near(lines: list[str], idx: int) -> bool: return bool(BAND_PREDICATE_RE.search("\n".join(context))) -# A new sibling conditional at the top of a code line — the boundary +# A new SIBLING conditional at the top of a code line — the boundary # `lone_band_predicate_near` refuses to cross. `handle-floor`'s own #6321 fix # shape deliberately spans two separate `if` statements (a coarse range # pre-filter, then the real band-predicate gate a few lines below), so @@ -206,6 +206,12 @@ def band_predicate_near(lines: list[str], idx: int) -> bool: # caught: an `is_valid_obj_ptr`-only guard that already dereferences inside # its own block must not be cleared by some other, disconnected `if # is_handle_band(...)` a few statements later. +# +# A conditional NESTED inside the guard's own block (brace depth still > 0 +# relative to the guard) is not that boundary — a second CodeRabbit pass +# caught this rule stopping on `if is_valid_obj_ptr(ptr) { if +# is_above_handle_band(...) { deref } }`, where the inner `if` is exactly the +# band predicate protecting the dereference, not a disconnected sibling. SIBLING_CONDITIONAL_RE = re.compile(r"^\s*(?:\}\s*)?(?:else\s+)?(?:if|while|for|match)\b") @@ -213,21 +219,27 @@ def lone_band_predicate_near(lines: list[str], idx: int) -> bool: """`band_predicate_near`, scoped to the current guard's own statement. Same backward window; the forward scan stops (without including) at the - first later line that opens a new sibling conditional, since the real - fix shape never needs to reach past that boundary. + first later line that opens a new SIBLING conditional — one reached only + after the brace depth relative to `lines[idx]` has returned to zero or + below, i.e. the guard's own block (and any block nested inside it) has + already closed. A conditional still nested inside that block never + triggers the boundary, since it may be the very predicate guarding the + dereference. """ start = max(0, idx - BAND_PREDICATE_LOOKBACK) context = [strip_comment(line) for line in lines[start : idx + 1]] + depth = context[-1].count("{") - context[-1].count("}") taken = 0 cursor = idx + 1 while cursor < len(lines) and taken < BAND_PREDICATE_LOOKAHEAD_CODE: code = strip_comment(lines[cursor]) if code.strip(): - if SIBLING_CONDITIONAL_RE.search(code): + if depth <= 0 and SIBLING_CONDITIONAL_RE.search(code): break context.append(code) taken += 1 + depth += code.count("{") - code.count("}") cursor += 1 return bool(BAND_PREDICATE_RE.search("\n".join(context))) @@ -482,6 +494,23 @@ def expect(cond: bool, message: str) -> None: "lone-valid-obj-ptr must still flag a dereference cleared only by " "a disconnected LATER sibling conditional's band predicate", ) + # CodeRabbit (PR #8685, second pass): a band predicate NESTED inside the + # guard's own block — not a disconnected sibling — must still clear the + # finding, since it is exactly what protects the dereference below it. + nested_guard = ( + " if is_valid_obj_ptr(ptr) {\n" + " if is_above_handle_band(ptr as usize) {\n" + " (*ptr).class_id\n" + " }\n" + " }\n" + ) + expect( + not any( + f.rule == "lone-valid-obj-ptr" for f in scan_text(runtime, nested_guard) + ), + "lone-valid-obj-ptr must accept a band predicate NESTED inside the " + "guard's own block", + ) # Band literals in code are caught; comment-only mentions are not. hits = scan_text(runtime, "if addr < 0x100000 {\n") From eaef7289114da0e26ce93ac58309b3ceca6b83ea Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 24 Aug 2026 00:06:09 -0400 Subject: [PATCH 5/6] fix(addr-class): track brace depth in source order, not per-line net count A net per-line count (open-count minus close-count) gets an '} else if is_valid_obj_ptr(ptr) {' guard line wrong: net zero reads as unchanged depth, but the leading close belongs to the ENCLOSING branch and the trailing open starts the guard's OWN block, so depth should end one level deeper. Process each line's braces in source order with a floor at zero instead, so a leading close belonging to a block this scan does not track can't push depth negative and mask the following open. Added the else-if-guard shape CodeRabbit flagged to run_self_tests. --- scripts/addr_class_inventory.py | 45 +++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/scripts/addr_class_inventory.py b/scripts/addr_class_inventory.py index dc4df27fe5..08dc7e3973 100644 --- a/scripts/addr_class_inventory.py +++ b/scripts/addr_class_inventory.py @@ -215,6 +215,26 @@ def band_predicate_near(lines: list[str], idx: int) -> bool: SIBLING_CONDITIONAL_RE = re.compile(r"^\s*(?:\}\s*)?(?:else\s+)?(?:if|while|for|match)\b") +def apply_brace_deltas(depth: int, code: str) -> int: + """Advance `depth` by `code`'s braces, IN SOURCE ORDER, floored at zero. + + A net per-line count (`code.count("{") - code.count("}")`) gets an `} + else if ... {` line wrong: net zero reads as "still at the same depth", + but the leading `}` closes the ENCLOSING block first, so the following + `{` actually opens a brand-new nested one — depth should end at one + level deeper, not unchanged. Processing character by character (with + the floor so a leading close belonging to an outer block we don't track + can't push depth negative) gets this right. + """ + + for char in code: + if char == "{": + depth += 1 + elif char == "}": + depth = max(depth - 1, 0) + return depth + + def lone_band_predicate_near(lines: list[str], idx: int) -> bool: """`band_predicate_near`, scoped to the current guard's own statement. @@ -229,7 +249,7 @@ def lone_band_predicate_near(lines: list[str], idx: int) -> bool: start = max(0, idx - BAND_PREDICATE_LOOKBACK) context = [strip_comment(line) for line in lines[start : idx + 1]] - depth = context[-1].count("{") - context[-1].count("}") + depth = apply_brace_deltas(0, context[-1]) taken = 0 cursor = idx + 1 while cursor < len(lines) and taken < BAND_PREDICATE_LOOKAHEAD_CODE: @@ -239,7 +259,7 @@ def lone_band_predicate_near(lines: list[str], idx: int) -> bool: break context.append(code) taken += 1 - depth += code.count("{") - code.count("}") + depth = apply_brace_deltas(depth, code) cursor += 1 return bool(BAND_PREDICATE_RE.search("\n".join(context))) @@ -511,6 +531,27 @@ def expect(cond: bool, message: str) -> None: "lone-valid-obj-ptr must accept a band predicate NESTED inside the " "guard's own block", ) + # CodeRabbit (PR #8685, third pass): the guard itself following an `else + # if` chain (`} else if is_valid_obj_ptr(ptr) {`) must not miscompute its + # own starting depth as zero — a net per-line brace count treats the + # leading close and the trailing open as cancelling out, when the close + # belongs to the PRIOR branch and the open starts the guard's own block. + else_if_guard = ( + " if some_other_check() {\n" + " do_other_thing();\n" + " } else if is_valid_obj_ptr(ptr) {\n" + " if is_above_handle_band(ptr as usize) {\n" + " (*ptr).class_id\n" + " }\n" + " }\n" + ) + expect( + not any( + f.rule == "lone-valid-obj-ptr" for f in scan_text(runtime, else_if_guard) + ), + "lone-valid-obj-ptr must accept a nested band predicate when the " + "guard itself is an `else if` branch", + ) # Band literals in code are caught; comment-only mentions are not. hits = scan_text(runtime, "if addr < 0x100000 {\n") From 8d9cd4b1df75845e1f2e5837d5013269f9871976 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 24 Aug 2026 00:06:16 -0400 Subject: [PATCH 6/6] fix(addr-class): fix else-if depth miscount; add census sabotage test - addr_class_inventory.py: a per-line net brace count (count("{") - count("}")) gets `} else if COND {` wrong -- the leading close belongs to the prior branch, the trailing open starts the guard's OWN block, so a net-zero line was read as "unchanged depth" when it should end one level deeper. Process braces character-by-character instead (apply_brace_deltas), floored at zero so an outer close we don't track can't go negative. Regression test added for `} else if is_valid_obj_ptr(ptr) { if is_above_handle_band(...) { deref } }`. - shape_descriptor_census.py: added a sabotage self-test for the generic-read-PIC fail-closed-token assertion touched in this PR (icmp_ne(I32, &pcid, "0")), planting a mutated comparand and confirming assert_authority_surfaces rejects it -- proven non-vacuous by manually reverting the production check and confirming the census fails, then restoring it. --- scripts/shape_descriptor_census.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/shape_descriptor_census.py b/scripts/shape_descriptor_census.py index 6de4315b6d..9e0d96b1ca 100644 --- a/scripts/shape_descriptor_census.py +++ b/scripts/shape_descriptor_census.py @@ -815,6 +815,28 @@ def run_sabotage_selftests(sources: dict[str, str], baseline: dict[str, object]) lambda: assert_authority_surfaces(legacy_ir), ) + # #8665: the generic read PIC's invalid-id fail-closed token (pcid != 0) + # must not go quietly missing. Plant a regression that emits an + # always-nonzero comparand instead of the real ShapeId register, and + # prove the census still catches it -- this is what stands between the + # check above and a vacuous pass, per #6942/#6946/#7024's precedent that + # an unexercised assertion is a decision nobody actually made. + dropped_fail_closed = dict(sources) + path = "crates/perry-codegen/src/expr/property_get/generic_dispatch.rs" + sabotaged_body, substitutions = re.subn( + r'icmp_ne\(I32, &pcid, "0"\)', + 'icmp_ne(I32, &pcid, "-1")', + dropped_fail_closed[path], + count=1, + ) + if substitutions != 1: + raise CensusError("generic read PIC fail-closed sabotage fixture missing") + dropped_fail_closed[path] = sabotaged_body + expect_rejected( + "generic read PIC invalid-id fail-closed token silently changed", + lambda: assert_authority_surfaces(dropped_fail_closed), + ) + # #8113: the gep-spelled emitted guards. This arm was VACUOUS before — # it matched only `add(..., "N")` — so plant a keys-offset gep and prove # it is caught now.