diff --git a/.github/workflows/gc-ptr-shape-off-witness.yml b/.github/workflows/gc-ptr-shape-off-witness.yml new file mode 100644 index 0000000000..ec727b5654 --- /dev/null +++ b/.github/workflows/gc-ptr-shape-off-witness.yml @@ -0,0 +1,215 @@ +name: GC Ptr OFF-arm witness + +# #6984: PERRY_PTR_SHAPE_LOCALS is the documented A/B kill switch for repsel +# Phase 3b/5a (`Ptr` proven object locals + the proven-`this` method +# clones it feeds, #6911/#6925). Its whole point is that flipping it OFF +# reverts to the boxed lowering and reproduces byte-identical output — that is +# the entire premise the `rep_ptr_shape_off` matrix arm and every other +# representation's own OFF arm exist to verify (CLAUDE.md's GC knob +# kill-policy: "every GC env knob either has a required CI arm exercising its +# OFF state, or it is deleted after one release of soak"). +# +# That premise was FALSE on `main` for #6976/#6984: with the switch off, an +# interface-typed local with `new` provenance and a scalar-replaceable +# constructor (`const o: Shaped = new Impl(...)`) crashed with `TypeError: +# Cannot read properties of undefined`. No GC was involved (`cycles=0 +# moved=0`) — this is a plain codegen defect in a completely different, +# supposedly-orthogonal optimization (scalar replacement of `new` locals, +# `collectors/escape_news.rs`), which the switch is not even supposed to +# touch. It went unnoticed because the arm that would have caught it — +# `rep_ptr_shape_off`, part of `scripts/gc_repsel_matrix.sh`'s `--arms all` — +# is NOT in `gc-stress`'s PR-gating subset (`PR_ARMS` in that script), so no +# per-PR run ever compiled with the switch off. `--arms all` only runs on +# push-to-main/schedule/workflow_dispatch, so a regression here would sit +# undetected for up to a release cycle, exactly the class of hole #7194 (the +# `gc_gate_wiring_check.py` precedent) was written to close for the moving +# collector. +# +# WHAT THIS DOES +# +# One arm, the whole representation-selection corpus: +# +# scripts/gc_repsel_matrix.sh --arms rep_ptr_shape_off --filter test_gap_repsel_ +# +# `rep_ptr_shape_off` compiles with `PERRY_PTR_SHAPE_LOCALS=0` and runs under +# the evacuating base + forced evacuation (`%E% PERRY_GC_FORCE_EVACUATE=1`), +# so it doubles as the OFF arm's GC-under-evacuation witness — not just this +# one crash's regression test. The matrix's own exit status already covers +# both halves of CLAUDE.md's "four ways a gate can be unable to fail" #4: a +# FAIL cell fails the run, and an all-UNVER table (the arm's `requires=move` +# never actually relocating anything) fails it too +# (`scripts/gc_matrix_liveness_check.py`, invoked by the matrix script +# itself) — so this job needs no separate liveness re-check the way +# `gc-moving-witnesses.yml` does for its non-repsel-prefixed corpus. +# +# THIS JOB IS DESIGNED TO BE ABLE TO FAIL, checked against all four ways +# (CLAUDE.md): +# +# 1. no `continue-on-error`, no `|| true`, no pipe between the matrix and +# the shell's exit status; +# 2. NOT in branch protection's required contexts yet, deliberately: a new +# gate has never been green, and promoting it immediately would block +# every open PR (CLAUDE.md's own corollary). Promote once it has a green +# run on `main` — that is a maintainer action, not something this PR +# does. +# 3. `concurrency` cancels pull-request runs only; `main` runs are keyed on +# the commit SHA so they queue instead of being cancelled by the next +# merge (#7205's fix, replicated from gc-moving-witnesses.yml). +# 4. the matrix's OWN exit status already asserts liveness (see above), so +# a green run here means the OFF arm actually compiled, actually ran, +# and actually matched the pinned Node oracle — not merely that nothing +# threw. +# +# WHAT THIS DELIBERATELY DOES NOT COVER +# +# * The other representations' own OFF arms (`rep_i32_off`, `rep_str_off`, +# `rep_ptr_numarray_off`, `rep_spec_abi_off`, `rep_int_valued_off`, +# `rep_str_static_off`) are still only exercised by `--arms all` on +# push/schedule. This job is scoped to the one knob #6984 was filed +# against; extending it to the others is a separate decision with its own +# cost/coverage tradeoff, not bundled in here. +# * `--arms all`'s other cells continue to run on their existing schedule +# (gc-stress in test.yml); this job does not replace that run, it adds a +# per-PR arm the existing PR-gating subset omits. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + # One group per main COMMIT, cancelling PR runs only — see + # gc-moving-witnesses.yml's comment for the #7205 rationale this mirrors. + group: gc-ptr-shape-off-witness-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + # scripts/gc_repsel_matrix.sh deliberately does NOT set + # PERRY_NO_AUTO_OPTIMIZE (see its own header): the auto-optimizer decides + # whether the linked runtime carries the `diagnostics` feature, which + # changes the GC trace format the harness parses for its liveness evidence. + # Setting it here from outside could silently turn every cell UNVER. + +jobs: + gc-ptr-shape-off-witness: + # ubuntu-latest, matching gc-stress: the matrix already runs there, and + # the cargo cache is shared with that job (same `shared-key`) so this + # build is usually warm. + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Decide whether this change can affect Ptr lowering + id: relevance + env: + GH_TOKEN: ${{ github.token }} + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" != "pull_request" ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + echo "Not a pull request; running the witness." + exit 0 + fi + gh api "repos/$REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' > changed.txt + # Deliberately broad, same rationale as gc-moving-witnesses.yml: + # anything under crates/ can move what a scalar-replaced receiver's + # method-call lowering resolves to, not just the Ptr + # collectors themselves — #6984's defect lived in + # lower_call/scalar_method.rs, three files away from ptr_shape.rs. + if grep -qE '^(crates/|scripts/gc_repsel_matrix\.sh$|test-files/test_gap_repsel_|test-parity/gc_repsel_|Cargo\.(toml|lock)$|\.node-version$|\.github/workflows/gc-ptr-shape-off-witness\.yml$)' changed.txt; then + echo "run=true" >> "$GITHUB_OUTPUT" + echo "Change touches representation-selection-relevant paths; running the witness." + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "No representation-selection-relevant paths changed." + fi + + - name: Install Rust toolchain + if: steps.relevance.outputs.run == 'true' + uses: dtolnay/rust-toolchain@stable + - uses: ./.github/actions/setup-llvm22 + if: steps.relevance.outputs.run == 'true' + + - uses: Swatinem/rust-cache@v2 + if: steps.relevance.outputs.run == 'true' + with: + # Same key as gc-stress, which builds the identical package set, so + # this job usually starts from that job's warm cache. + shared-key: "${{ runner.os }}-perry" + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Install clang + if: steps.relevance.outputs.run == 'true' + run: | + sudo apt-get update + sudo apt-get install -y clang + + - name: Setup Node oracle + if: steps.relevance.outputs.run == 'true' + uses: actions/setup-node@v7 + with: + # Single source of truth: .node-version. The matrix refuses to run + # when the running node disagrees with the pin, so a version drift + # here would abort loudly rather than silently drop a test. + node-version-file: .node-version + + - name: Build perry and the runtime archives + if: steps.relevance.outputs.run == 'true' + env: + # Match cargo-test / gc-stress's linker workaround (lld SIGBUS on + # the shared runner during large links). + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "-C linker-features=-lld" + run: | + set -euo pipefail + # perry-runtime / perry-stdlib are rlib-only; libperry_runtime.a and + # libperry_stdlib.a come from the -static wrapper crates. Building + # without them links a stale archive and makes the whole run + # vacuous (CLAUDE.md, "Verifying a runtime change"). Package set + # matches gc-stress exactly so the cache is shared. + cargo build --release \ + -p perry -p perry-runtime -p perry-stdlib \ + -p perry-runtime-static -p perry-stdlib-static + for artifact in perry libperry_runtime.a libperry_stdlib.a; do + test -s "target/release/$artifact" \ + || { echo "::error::target/release/$artifact was not produced"; exit 1; } + done + + # GATING. No pipe, no `|| true`: this step's exit status IS the gate. + # scripts/gc_repsel_matrix.sh's own exit status already folds in the + # liveness gate (scripts/gc_matrix_liveness_check.py) alongside the + # byte-exactness check, so a green run here means the OFF arm actually + # compiled, actually ran under evacuation, and actually matched the + # pinned Node oracle for every file in the representation-selection + # corpus — not merely that nothing threw (CLAUDE.md's "four ways a + # gate can be unable to fail", #4). + - name: Run the PERRY_PTR_SHAPE_LOCALS=0 witness across the repsel corpus + if: steps.relevance.outputs.run == 'true' + run: | + ./scripts/gc_repsel_matrix.sh \ + --no-build \ + --arms rep_ptr_shape_off \ + --filter test_gap_repsel_ \ + --json gc-ptr-shape-off-witness.json + + - name: Upload the witness report + if: always() && steps.relevance.outputs.run == 'true' + uses: actions/upload-artifact@v7 + with: + name: gc-ptr-shape-off-witness-${{ github.sha }} + path: gc-ptr-shape-off-witness.json + if-no-files-found: ignore + retention-days: 30 diff --git a/CLAUDE.md b/CLAUDE.md index db7f3d8d95..7362dc7f8c 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.1414 +**Current Version:** 0.5.1415 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 67d909e4da..3bda2629e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1414" +version = "0.5.1415" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1414" +version = "0.5.1415" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1414" +version = "0.5.1415" [[package]] name = "perry-ui-tvos" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1414" +version = "0.5.1415" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 4baa4585ef..9631a4fd83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1414" +version = "0.5.1415" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7718-ptr-shape-kill-switch.md b/changelog.d/7718-ptr-shape-kill-switch.md new file mode 100644 index 0000000000..e3aaf4dfc7 --- /dev/null +++ b/changelog.d/7718-ptr-shape-kill-switch.md @@ -0,0 +1,5 @@ +### Fixed +- **repsel: `PERRY_PTR_SHAPE_LOCALS=0` crashed on a scalar-replaced receiver whose declared type is not a class** (#6984, #6976). The kill switch's front-door eligibility gates (`collectors/ptr_shape.rs`, `collectors/proven_this.rs`) were already correctly wired — the bug was one level down, in a completely different, supposedly-orthogonal optimization: scalar replacement of `new` locals (`collectors/escape_news.rs`) and its method-call summarizer (`lower_call/scalar_method.rs`). That summarizer resolved a scalar-replaced receiver's class via `type_analysis::predicates::receiver_class_name`, whose fallback — reached whenever Phase 3b's `Ptr` proof is unavailable, whether from the kill switch or any other denial — returns the local's *declared* type name. For `const o: Shaped = new Impl(...)` (an interface- or alias-typed local with `new` provenance), that name is `"Shaped"`, which is not a registered class. `simple_scalar_method_summary` then failed to find it, the summarizer gave up, and the caller fell through to the ordinary heap-object method-dispatch lowering — which reads the receiver from a local slot that scalar replacement had already turned into a bare, never-initialized alloca (the heap allocation was elided). Reading that slot produced garbage that decoded as `undefined`, and `o.area()` threw `TypeError: Cannot read properties of undefined`. +- Fixed in `lower_call/scalar_method.rs::try_lower_scalar_replaced_method_call` by resolving the receiver's class from `ctx.non_escaping_news` (the exact map that gated scalar replacement for that local in the first place — keyed by the `new` expression's own class name, never the declared annotation) before falling back to `receiver_class_name`. This makes scalar replacement's method-call summarization independent of `PERRY_PTR_SHAPE_LOCALS` for `new`-provenance receivers, matching the house rule that a representation knob may move only the sites of the representation it names. Verified byte-identical LLVM IR for the default (switch-on) build before and after the fix — the change is a pure no-op there — and byte-identical output against the pinned Node oracle for both arms of `PERRY_PTR_SHAPE_LOCALS` on `test_gap_repsel_ptr_shape_locals.ts` and the wider `test_gap_repsel_*` corpus. +- Removed the now-obsolete triage entry for `test_gap_repsel_ptr_shape_locals | rep_ptr_shape_off` in `test-parity/gc_repsel_triage.txt` (per #6976's own instruction to remove it once fixed). +- Added `.github/workflows/gc-ptr-shape-off-witness.yml`: a per-PR CI arm that runs `scripts/gc_repsel_matrix.sh --arms rep_ptr_shape_off --filter test_gap_repsel_` (the whole representation-selection corpus, not just the one file), since `rep_ptr_shape_off` is not part of `gc-stress`'s PR-gating subset and previously only ran on push-to-main/schedule — so a regression on the kill switch's OFF state could sit undetected for up to a release cycle. Not yet in branch protection's required contexts (a new gate has never been green); promoting it once it has a green run on `main` is a maintainer action. diff --git a/crates/perry-codegen/src/lower_call/scalar_method.rs b/crates/perry-codegen/src/lower_call/scalar_method.rs index d3d7011210..e354b5f20a 100644 --- a/crates/perry-codegen/src/lower_call/scalar_method.rs +++ b/crates/perry-codegen/src/lower_call/scalar_method.rs @@ -980,7 +980,43 @@ pub(super) fn try_lower_scalar_replaced_method_call( if !ctx.scalar_replaced.contains_key(receiver_id) { return Ok(None); } - let Some(class_name) = crate::type_analysis::receiver_class_name(ctx, object.as_ref()) else { + // #6984: resolve the class from the `new` expression's own provenance + // (`ctx.non_escaping_news`, the exact map that gated scalar replacement + // for this local in `let_stmt.rs`) BEFORE falling back to the general + // `receiver_class_name` resolver. + // + // `receiver_class_name` is built for guarded/dynamic dispatch, where a + // local's DECLARED type is a safe stand-in whenever no `Ptr` proof + // is available — that dispatch always re-checks the class in the + // registry and falls back cleanly on a miss. Scalar replacement's + // receiver has no such fallback: once a local is scalar-replaced, + // `ctx.locals[receiver_id]` is a bare, never-initialized alloca (the + // heap allocation was elided), so any lowering that reads it as an + // ordinary boxed receiver observes garbage. `receiver_class_name`'s own + // fallback arm returns the DECLARED type name whenever Phase 3b's proof + // is unavailable for ANY reason (not just `PERRY_PTR_SHAPE_LOCALS=0`) — + // and for an interface- or alias-typed local (`const o: Shaped = new + // Impl(...)`) that name is not a registered class at all. Looking it up + // in `ctx.classes` then fails, `simple_scalar_method_summary` returns + // `None`, this function bails, and the caller falls through to the + // ordinary heap-object method-call lowering — which reads the + // uninitialized dummy slot and crashes (`TypeError: Cannot read + // properties of undefined`). + // + // `ctx.non_escaping_news` is exact and structural: it is keyed by the + // `New` expression's own class name, never the declared annotation, so + // it names the correct class regardless of whether Phase 3b ran at all. + // Any id present in `ctx.scalar_replaced` via the `New` branch of + // `let_stmt.rs` is present here with the identical class name; the only + // scalar-replaced receivers NOT covered are non-escaping OBJECT + // LITERALS, which carry no class and keep falling back to + // `receiver_class_name` (unchanged, and already `None` for them there). + let class_name_opt = ctx + .non_escaping_news + .get(receiver_id) + .cloned() + .or_else(|| crate::type_analysis::receiver_class_name(ctx, object.as_ref())); + let Some(class_name) = class_name_opt else { return Ok(None); }; let Some(method) = crate::collectors::simple_scalar_method_summary( diff --git a/docs/src/testing/test-registration.md b/docs/src/testing/test-registration.md index b19dca1480..2e43eef163 100644 --- a/docs/src/testing/test-registration.md +++ b/docs/src/testing/test-registration.md @@ -45,7 +45,7 @@ Run `--list` for the authoritative version with every exclusion and its reason. | Registry | Candidate files | Runner | |----------|-----------------|--------| -| `test-parity/gc_repsel_corpus.txt` | `test-files/test_gap_{gc,repsel,specabi}_*.ts` | `scripts/gc_repsel_matrix.sh` (`gc-stress`, `gc-moving-witnesses`) | +| `test-parity/gc_repsel_corpus.txt` | `test-files/test_gap_{gc,repsel,specabi}_*.ts` | `scripts/gc_repsel_matrix.sh` (`gc-stress`, `gc-moving-witnesses`, `gc-ptr-shape-off-witness`) | | `test-features/feature_matrix.toml` | `test-features/probes/**/*.ts` | `scripts/gen_feature_matrix.py` (`feature-matrix`) | | `benchmarks/compiler_output/workloads.toml` | `benchmarks/compiler_output/fixtures/**/*.ts` | `scripts/compiler_output_regression.py` (`compiler-output-regression`) | | a `mod` declaration in the parent module | `crates/*/**/tests/**/*.rs` below a suite root | `cargo test` | diff --git a/scripts/check_test_registration.py b/scripts/check_test_registration.py index f31da1966c..06b6d79412 100644 --- a/scripts/check_test_registration.py +++ b/scripts/check_test_registration.py @@ -273,7 +273,7 @@ def _stem(rel: str) -> str: "test-files/test_gap_specabi_*.ts", ), registry="test-parity/gc_repsel_corpus.txt", - runner="scripts/gc_repsel_matrix.sh (gc-stress, gc-moving-witnesses)", + runner="scripts/gc_repsel_matrix.sh (gc-stress, gc-moving-witnesses, gc-ptr-shape-off-witness)", key=_stem, registered=lambda t: _read_hash_list(t, "test-parity/gc_repsel_corpus.txt"), entry_to_path=lambda e: "test-files/%s.ts" % e, diff --git a/test-parity/gc_repsel_triage.txt b/test-parity/gc_repsel_triage.txt index 4fdcc3ac8a..3fadf47448 100644 --- a/test-parity/gc_repsel_triage.txt +++ b/test-parity/gc_repsel_triage.txt @@ -14,8 +14,6 @@ # is in the PR arm set, so it is the arm that will catch the next unrooted # temporary. Do not re-triage it without a new issue number and a reason. -test_gap_repsel_ptr_shape_locals | rep_ptr_shape_off | #6976 -- REGRESSION IN THE REPRESENTATION'S OWN OFF-SWITCH, not a defect in this PR. Bisected: passes at 8327ced52, fails at 1a533a3a8 (#6925, repsel Phase 5a proven `this`). With PERRY_PTR_SHAPE_LOCALS=0 the program dies partway with `TypeError: Cannot read properties of undefined (reading 'area')`, losing its last five output lines. It was invisible until now because #6925 also left test_gap_repsel_proven_this_frozen.ts unregistered, which makes this script exit 3 before it runs anything -- the gate was dark, not green. REMOVE THIS ENTRY when #6976 is fixed; an OFF arm is supposed to be the safest cell in the matrix. - # --- #7216's Object.assign witness on the ALLOCATION-POINT arms (#7248) ------ # RETARGETED FROM #7217, AND ITS STATED CAUSE WAS WRONG. #