fix(gc): a non-empty array literal silently voided its #7469 all-pointer declaration (#8102) - #8114
Merged
Merged
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughChangesAll-pointer array declarations
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
proggeramlug
pushed a commit
that referenced
this pull request
Aug 14, 2026
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
marked this pull request as ready for review
August 15, 2026 21:22
proggeramlug
force-pushed
the
fix/8102-nonempty-array-literal-all-pointer
branch
from
August 15, 2026 21:23
ccdffc9 to
fb95682
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8102.
What was wrong
js_array_declare_all_pointer_elements(
crates/perry-runtime/src/array/header.rs) refused every array withlength != 0, on the argument that the all-pointer claim covers0..lengthand is only vacuously true on an empty array.
emit_all_pointer_array_declarationis emitted from theStmt::Lettail —after an array literal's element stores have already run and installed a
per-slot side mask (
expr/array_literal.rspublishes the payloadGC_LAYOUT_POINTER_FREEand notes each pointer slot). So forconst a: C[] = [x, y]the declaration was a silent no-op: the side masksurvived, every later
a.push(…)failed the elided-push header test inexpr/array_push.rs(_reserved == SIDE_MASK | ALL_POINTERSwith both raw-f64bits clear), and each push paid the per-store
js_gc_note_slot_layoutthat#7469 exists to delete.
collectors/all_pointer_arrays.rsalready admits such a literal — its moduleheader says the empty literal "passes vacuously", and
literal_of_object_elements_is_admittedis its test — so the proof was issuedat 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 bepointer-bearing by
layout_pointer_bearing_bits— the same predicate the layoutmask builder and
GC_LAYOUT_UNKNOWN's per-slot re-validation use. Thedeclaration therefore never has to trust the caller's static proof, and a
payload it has not checked can never be declared.
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 everyslot).
length == 0holds vacuously, so the empty-literal path is bit-identical.Measurement
--releasecompiler and--releaselibperry_{runtime,stdlib}.a. The samecompiler binary in both arms — only the
.apair is swapped (the baselinepair was copied aside before the rebuild, and
cmpconfirms both archivesmoved).
PERRY_RUNTIME_DIRpinned,PERRY_NO_AUTO_OPTIMIZE=1,/usr/bin/time -linstructions retired, medians of 3 interleaved reps, macOSarm64 (contended host, so no wall clock is quoted). Every arm prints the
identical
t: 4000000.const a: C[] = [x, y]const a: C[] = [][]+ 2 pushes + the same loopThe 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 undersrc/, so it runs incargo-test, not in anightly-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_layoutcalls 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— thepermanent sabotage arm. Identical sequence with one numeric element; it
asserts both that the declaration is refused and that
_reservedisbyte-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.declaring_a_non_empty_array_is_refusedis renamed todeclaring_an_array_holding_a_non_pointer_element_is_refused. Its fixturepushes 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 --lib— 2334 passed, 0 failed, 4 ignored.benchmarks/suite,benchmarks/app-patterns/kernels, and the 19-program beat-scriptc sweepcorpus), same compiler, baseline
.avs fixed.a: 61/61 structurallyidentical, 0 compile failures. The 31 deterministic programs match
byte-for-byte; the 30 suite programs differ only in their printed
<name>:<elapsed-ms>lines (andta_untyped_typed_ratio, itself a timingratio).
cargo fmt --all -- --check,scripts/check_file_size.sh,scripts/addr_class_inventory.py,scripts/gc_runtime_root_holders.py— allclean.
Not validated
x86-64;
--profile dist; the gap suite (this is a runtime change gated by aself-verifying predicate, and the 61-program output A/B plus the full
perry-runtimeunit suite is what I ran instead — worth a gap run beforemerge). Twelve CI contexts are red tree-wide for unrelated reasons (#8092).
https://claude.ai/code/session_01MsfDzkTEnuS2nh7ygsYkoi
Summary by CodeRabbit
Bug Fixes
Tests