Skip to content

fix(gc): a non-empty array literal silently voided its #7469 all-pointer declaration (#8102) - #8114

Merged
proggeramlug merged 2 commits into
mainfrom
fix/8102-nonempty-array-literal-all-pointer
Aug 15, 2026
Merged

fix(gc): a non-empty array literal silently voided its #7469 all-pointer declaration (#8102)#8114
proggeramlug merged 2 commits into
mainfrom
fix/8102-nonempty-array-literal-all-pointer

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #8102.

What was wrong

js_array_declare_all_pointer_elements
(crates/perry-runtime/src/array/header.rs) refused every array with
length != 0, on the argument that the all-pointer claim covers 0..length
and is only vacuously true on an empty array.

emit_all_pointer_array_declaration is emitted from the Stmt::Let tail
after an array literal's element stores have already run and installed a
per-slot side mask (expr/array_literal.rs publishes the payload
GC_LAYOUT_POINTER_FREE and notes each pointer slot). So for
const a: C[] = [x, y] the declaration was a silent no-op: the side mask
survived, every later a.push(…) failed the elided-push header test in
expr/array_push.rs (_reserved == SIDE_MASK | ALL_POINTERS with both raw-f64
bits clear), and each push paid the per-store js_gc_note_slot_layout that
#7469 exists to delete.

collectors/all_pointer_arrays.rs already admits such a literal — its module
header says the empty literal "passes vacuously", and
literal_of_object_elements_is_admitted is its test — so the proof was issued
at compile time and thrown away at run time.

The fix

Discharge the claim instead of assuming it. layout_all_pointer_slots_would_hold
(gc/layout.rs) walks the initialized prefix and requires every slot to be
pointer-bearing by layout_pointer_bearing_bits — the same predicate the layout
mask builder and GC_LAYOUT_UNKNOWN's per-slot re-validation use. The
declaration therefore never has to trust the caller's static proof, and a
payload it has not checked can never be declared.

  • Refusal leaves the header byte-unchanged — the raw-f64 bits are cleared
    only once the declaration is known to stick, so a refused call is exactly
    today's behaviour (push routes through js_array_push_f64, which notes every
    slot).
  • length == 0 holds vacuously, so the empty-literal path is bit-identical.
  • Runtime-only: no ABI change, no codegen change, no new symbol.

Measurement

--release compiler and --release libperry_{runtime,stdlib}.a. The same
compiler binary in both arms
— only the .a pair is swapped (the baseline
pair was copied aside before the rebuild, and cmp confirms both archives
moved). PERRY_RUNTIME_DIR pinned, PERRY_NO_AUTO_OPTIMIZE=1,
/usr/bin/time -l instructions retired, medians of 3 interleaved reps, macOS
arm64 (contended host, so no wall clock is quoted). Every arm prints the
identical t: 4000000.

fixture before after Δ
4,000,000 pushes into const a: C[] = [x, y] 20,755,859,948 15,309,077,609 −26.2%
control — same pushes into const a: C[] = [] 15,505,084,925 15,491,525,692 −0.1%
control — [] + 2 pushes + the same loop 15,613,539,814 15,629,659,106 +0.1%

The two controls are the point: the win is confined to the shape that was
broken, and the paths that already worked do not move.

Tests

crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs
(a #[cfg(test)] module under src/, so it runs in cargo-test, not in a
nightly-only integration suite):

  • a_non_empty_all_pointer_literal_is_declared_and_admits_the_elided_store
    the positive case, built the way the literal lowering builds it (element
    stores and their js_gc_note_slot_layout calls first, declaration after),
    asserting the pre-declaration state is genuinely inadmissible before the
    declaration runs.
  • a_non_pointer_element_in_the_literal_still_refuses_the_declaration — the
    permanent sabotage arm. Identical sequence with one numeric element; it
    asserts both that the declaration is refused and that _reserved is
    byte-unchanged. A green positive test therefore means the check
    discriminates, not that nothing was tried.
  • an_empty_array_is_still_declared_vacuously — pins the unchanged path.
  • The pre-existing declaring_a_non_empty_array_is_refused is renamed to
    declaring_an_array_holding_a_non_pointer_element_is_refused. Its fixture
    pushes a number, so the push — not the length — was always what made it a
    refusal; only the name and message were imprecise. No test was deleted.

Validation

  • cargo test -p perry-runtime --lib2334 passed, 0 failed, 4 ignored.
  • Output/exit-code A/B over 61 programs (benchmarks/suite,
    benchmarks/app-patterns/kernels, and the 19-program beat-scriptc sweep
    corpus), same compiler, baseline .a vs fixed .a: 61/61 structurally
    identical
    , 0 compile failures. The 31 deterministic programs match
    byte-for-byte; the 30 suite programs differ only in their printed
    <name>:<elapsed-ms> lines (and ta_untyped_typed_ratio, itself a timing
    ratio).
  • cargo fmt --all -- --check, scripts/check_file_size.sh,
    scripts/addr_class_inventory.py, scripts/gc_runtime_root_holders.py — all
    clean.

Not validated

x86-64; --profile dist; the gap suite (this is a runtime change gated by a
self-verifying predicate, and the 61-program output A/B plus the full
perry-runtime unit suite is what I ran instead — worth a gap run before
merge). Twelve CI contexts are red tree-wide for unrelated reasons (#8092).

https://claude.ai/code/session_01MsfDzkTEnuS2nh7ygsYkoi

Summary by CodeRabbit

  • Bug Fixes

    • Fixed array declarations so non-empty arrays are optimized only when all initialized elements are pointer-bearing.
    • Preserved array state when optimization is rejected for mixed pointer and non-pointer values.
    • Retained support for empty arrays and valid all-pointer arrays.
  • Tests

    • Added coverage for successful all-pointer declarations, mixed-value rejection, state preservation, and empty arrays.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f5545771-8166-4447-87d3-e96bc4504479

📥 Commits

Reviewing files that changed from the base of the PR and between 1ebe4cc and fb95682.

📒 Files selected for processing (4)
  • changelog.d/8114-nonempty-array-literal-all-pointer.md
  • crates/perry-runtime/src/array/header.rs
  • crates/perry-runtime/src/gc/layout.rs
  • crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs

📝 Walkthrough

Walkthrough

Changes

All-pointer array declarations

Layer / File(s) Summary
Validate initialized pointer slots
crates/perry-runtime/src/gc/layout.rs
Adds validation for empty, null, pointer-bearing, and non-pointer slots.
Apply validated all-pointer layout
crates/perry-runtime/src/array/header.rs
Accepts non-empty arrays only when all initialized slots are pointer-bearing. Rejected declarations preserve the existing header state.
Cover accepted and rejected literals
crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs, changelog.d/8114-nonempty-array-literal-all-pointer.md
Adds coverage for pointer-only, mixed, non-pointer, and empty literals. Documents the runtime fix and validation results.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • PerryTS/perry#7138: Both changes modify array GC layout handling, but they address different logic.
  • PerryTS/perry#7501: This change extends the runtime declaration behavior and related regression tests.

Suggested reviewers: jdalton, thehypnoo

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/8102-nonempty-array-literal-all-pointer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit that referenced this pull request Aug 14, 2026
Ralph Küpper added 2 commits August 15, 2026 23:16
…ter declaration (#8102)

`js_array_declare_all_pointer_elements` refused every array with
`length != 0`, on the argument that the all-pointer claim covers `0..length`
and is only vacuously true on an empty array.

`emit_all_pointer_array_declaration` is emitted from the `Stmt::Let` tail —
i.e. *after* an array literal's element stores have run and installed a
per-slot side mask. So for `const a: C[] = [x, y]` the declaration was a
silent no-op, the side mask survived, and every later `a.push(...)` failed the
elided-push header test in `expr/array_push.rs` and paid the per-store
`js_gc_note_slot_layout` that #7469 exists to delete.

`collectors/all_pointer_arrays.rs` already admits such a literal — its module
header says the empty literal "passes vacuously", and
`literal_of_object_elements_is_admitted` is its test — so the proof was being
issued at compile time and discarded at run time.

Discharge the claim instead of assuming it: `layout_all_pointer_slots_would_hold`
walks the initialized prefix and requires every slot to be pointer-bearing by
`layout_pointer_bearing_bits`, the same predicate the mask builder and
`GC_LAYOUT_UNKNOWN`'s per-slot re-validation use. The declaration therefore
never has to trust the caller's static proof, and a payload it has not checked
can never be declared. A refusal leaves the header untouched (in particular the
raw-f64 bits are cleared only once the declaration is known to stick), which is
exactly today's behaviour. `length == 0` holds vacuously, so the empty-literal
path is bit-identical.

Measured, `--release` compiler and `--release` runtime archives, identical
compiler binary in both arms and only the `.a` pair swapped, `/usr/bin/time -l`
instructions retired, medians of 3 interleaved reps, identical program output:

  4,000,000 pushes into `const a: C[] = [x, y]`
    before  20,755,859,948
    after   15,309,077,609        -26.2%

  control: same pushes into `const a: C[] = []`
    before  15,505,084,925
    after   15,491,525,692        -0.1%

  control: `[]` + 2 pushes + the same loop
    before  15,613,539,814
    after   15,629,659,106        +0.1%

Tests: `a_non_empty_all_pointer_literal_is_declared_and_admits_the_elided_store`
is the positive case, and `a_non_pointer_element_in_the_literal_still_refuses_the_declaration`
is its permanent sabotage arm — it runs the identical sequence with one numeric
element and asserts both that the declaration is refused and that the header is
byte-unchanged, so a green positive test means the check discriminates rather
than that nothing was tried. `an_empty_array_is_still_declared_vacuously` pins
the unchanged path. The pre-existing `declaring_a_non_empty_array_is_refused` is
renamed to `declaring_an_array_holding_a_non_pointer_element_is_refused`: its
fixture pushes a number, so the push — not the length — was always what made it
a refusal.

Closes #8102
@proggeramlug
proggeramlug marked this pull request as ready for review August 15, 2026 21:22
@proggeramlug
proggeramlug force-pushed the fix/8102-nonempty-array-literal-all-pointer branch from ccdffc9 to fb95682 Compare August 15, 2026 21:23
@proggeramlug
proggeramlug merged commit c8291ab into main Aug 15, 2026
9 of 54 checks passed
@proggeramlug
proggeramlug deleted the fix/8102-nonempty-array-literal-all-pointer branch August 15, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(gc): a non-empty array literal silently voids its #7469 all-pointer declaration — 33.9% on a push loop

1 participant