fix(runtime,codegen): counted loops over an Array subclass read element 0 as the object's meta word - #8976
Conversation
…ops as their own kind The loop guard resolved an elements-backed receiver to its store and published it as kind 1. Kind 1 means "the receiver IS the ArrayHeader", and the generated loop computes `receiver + header + i*8` itself — on the ordinary (non-capture) path `fast_raw` is the receiver's own address — so element 0 read the object's `meta` word as a double (`1.8e-311`) while later elements happened to land on real data. `issue_8773_closure_capture_packed_loops`'s dense case caught it once the store became the default representation (PerryTS#8974). Elements-backed receivers are now admitted as **kind 3**: every proof is the store's (no descriptors, prototype latch clear, `bound <= length <= capacity`, and the whole-array raw-f64 bit for a numeric mode), the live address stays the RECEIVER so the capture-safe caller keeps reloading the binding it owns, and the payload address is published in descriptor word 3. Codegen's two plain-payload sites take their base from that word (`plain_payload_base`), so both the capture path and the ordinary path read the payload. Revalidation re-resolves the store from the receiver, refreshes word 3 after an evacuation, and side-exits when an append re-allocates it — exactly as a grown plain Array does. Mode 2 (the fused ECS entity-id clone) declines, as it does for plain Arrays. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
|
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 (1)
📝 WalkthroughWalkthroughElements-backed ChangesElements-backed counted loops
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The change fixes counted loops over Array subclasses so they read actual element values instead of object metadata, while preserving safe fallback behavior when storage changes. No actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description is detailed and covers the bug, cause, fix, and verification results. It does not use the repository template headings or include an explicit checklist, related-issue section, or screenshots section, but the substantive information is mostly complete. Full details: Docstring CoverageExplanation Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 3 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches🧪 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 |
|
Merged. I reproduced this end to end rather than trusting the test, because a silent wrong-value bug deserves to be seen:
That The fix is the right shape. Giving elements-backed receivers their own kind rather than overloading kind 1 is what makes the two paths agree: keeping the live address as the receiver preserves the capture-safe reload, while publishing the payload in descriptor word 3 gives both the capture path and the ordinary path the same base. Refreshing word 3 after an evacuation (contents unchanged, address moved) and side-exiting when an append re-allocates are the two cases that would otherwise reintroduce a stale base. Worth recording how this got in, since I merged it. I landed #8974 an hour ago on the strength of all three knob states at 2779/0 plus its whole-corpus differential — 1386 files, 9 differences, none attributable. Both were true and neither caught this. The shape needs a nested counted loop over a subclass instance reached through a closure capture, and it corrupts only element 0, so a corpus diff over fixtures that mostly do not nest this way stays clean. #8966 hid it behind the gate; #8974 made it the default. A green corpus differential is evidence about the fixtures in it, not about the representation. Validation — runtime 2779/0 and codegen 1341/0 ( One fix pushed: the fragment used the |
The bug (live on main since #8974)
1.27e-311is a bare heap pointer read as a double — the object'smetaword.Cause
The loop guard resolved an elements-backed receiver (#8966) to its elements store and published it as kind 1. Kind 1 means "the receiver IS the
ArrayHeader": the generated loop computesreceiver + header + i*8itself, and on the ordinary (non-capture) pathfast_rawis the receiver's own address — the object, not the payload. So element 0 readobject + 8(themetapointer) while later elements happened to land on real data. It was invisible while the store was gated off; #8974 made it the default representation, andissue_8773_closure_capture_packed_loops's dense case catches it with the gate on.Fix
Elements-backed receivers get their own kind (3):
bound <= length <= capacity, and the whole-array raw-f64 bit when a numeric mode is requested;plain_payload_base) — this is what makes both the capture path and the ordinary path read the payload rather than the object;Verification
the_counted_loop_guard_admits_an_elements_backed_receiver_as_kind_three: kind 3, live address == receiver, word 3 == store, bound == length, revalidation stable, then a re-allocating append → side exit → fresh admission tracks the new store.issue_8773_closure_capture_packed_loops4/4,issue_8690_loop_versioned_arraylike3/3,issue_8655_array_subclass_indexing2/2,issue_8772_short_packed_spread5/5.cargo test -p perry-runtime --lib2779/0;RUSTFLAGS=-D warnings cargo check --workspace --all-targetsclean. (temp_root_operand_temporaries::string_literal_concat_operand_is_re_derived_below_the_allocating_siblingfails on plainorigin/maintoo — unrelated.)https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
Summary by CodeRabbit
Arraysubclasses so elements-backed arrays read the correct values, including element 0.