Skip to content

fix(compat): stop describing ArrayList's layout, and read it instead - #876

Merged
chrisbbreuer merged 1 commit into
mainfrom
fix/zig-1963-arraylist-compat
Sep 3, 2026
Merged

fix(compat): stop describing ArrayList's layout, and read it instead#876
chrisbbreuer merged 1 commit into
mainfrom
fix/zig-1963-arraylist-compat

Conversation

@glennmichael123

@glennmichael123 glennmichael123 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What this changes

Zig 0.17.0-dev.1963 appends pointer_stability: debug.SafetyLock to ArrayList, and two idioms here assumed the old shape. 27 struct literals of the form .{ .items = &buf, .capacity = buf.len } name every field, so one more field stops them compiling — all are test fixtures wrapping a fixed array, and .fromOwnedSlice(&buf) says the same thing without enumerating fields. The other is a layout assertion in the JIT, which is the part worth reading.

emitDirectDenseArrayGuards mirrors the list as DenseListLayout and asserted it @sizeOf-equal to std.ArrayListUnmanaged(Value). Measured, @sizeOf(ArrayListUnmanaged(u64)) on aarch64-macos:

toolchain Debug ReleaseSafe ReleaseFast
dev.1818 24 24 24
dev.1963 32 32 24

SafetyLock is zero-sized when runtime safety is off, so on 1963 that assertion fails in Debug and ReleaseSafe and passes in ReleaseFast — a compile error that depends on -O.

The mirror was never wrong about the part the JIT uses. @offsetOf(items) is 0 and @offsetOf(capacity) is 16 in all six combinations above, and the emitted loads only ever touch those two words plus the slice length. It was the equality that was wrong: it asserted a fact about a tail nothing reads. So the offsets now come from the real type via denseListWordOffset, and DenseListLayout is documented as a description of the prefix rather than a mirror.

Impact

Flips 0 test262 cases. No behaviour change on the current toolchain — same offsets, same emitted loads; @offsetOf resolves to the identical constants the literal did. What changes is that a future std reordering these fields moves the loads with it, instead of silently reading the wrong word — which the old assertion would not have caught either, since it only compared sizes.

Unblocks a downstream consumer (craft) that had to move off 0.17.0-dev.1509 because ziglang.org prunes dev builds: every 1509 tarball now 404s, so the pin could only be satisfied from cache.

Verification

  • zig build test on 0.17.0-dev.1963+e00c6c439: 2285 passed, 1 skipped, 0 failed, 0 leaked, exit 0.
  • zig build on 0.17.0-dev.1818+7051f8e73 (this repo's ZIG_VERSION): clean, with siblings at upstream main.
  • Size/offset table above measured directly on both toolchains across all three optimize modes, not inferred.

The 1963 figure is from a completed run whose log I later clobbered, and two attempts to regenerate it were killed by SIGTERM before finishing (error: process terminated with signal TERM) rather than failing — so treat it as reported, not attached. CI on this PR is the authoritative measurement and covers the 1818 leg the repo actually pins.

Zig 0.17.0-dev.1963 appends `pointer_stability: debug.SafetyLock` to
`ArrayList`. Two idioms here assumed the old shape.

**27 struct literals.** `.{ .items = &buf, .capacity = buf.len }` names every
field, so a std with one more field stops compiling. All of them are test
fixtures wrapping a fixed array; `.fromOwnedSlice(&buf)` says the same thing
and does not enumerate fields. It exists on both toolchains.

**One layout assertion, which is the interesting half.** The JIT mirrors the
list as `DenseListLayout` and asserted it `@sizeOf`-equal to the real type.
Measured, `@sizeOf(ArrayListUnmanaged(u64))` on aarch64-macos:

    toolchain   Debug  ReleaseSafe  ReleaseFast
    dev.1818       24           24           24
    dev.1963       32           32           24

`SafetyLock` is zero-sized when runtime safety is off, so on 1963 the
assertion fails in Debug and ReleaseSafe and passes in ReleaseFast — a
compile error that depends on `-O`.

The mirror was never wrong about the part the JIT uses. `@offsetOf(items)` is
0 and `@offsetOf(capacity)` is 16 in all six combinations above, and the
emitted loads only ever touch those two words plus the slice length. It was
the equality that was wrong: it asserted a fact about the tail nothing reads.

So the offsets now come from the real type via `denseListWordOffset`, and
`DenseListLayout` is documented as a description of the prefix rather than a
mirror. A future std that reorders these fields moves the loads with it
instead of silently reading the wrong word — which the old assertion would
not have caught either, since it only compared sizes.

No behaviour change on the current toolchain: same offsets, same loads.
@chrisbbreuer
chrisbbreuer merged commit f9c2653 into main Sep 3, 2026
54 checks passed
@chrisbbreuer
chrisbbreuer deleted the fix/zig-1963-arraylist-compat branch September 3, 2026 16:04
glennmichael123 added a commit to craft-native/craft that referenced this pull request Sep 3, 2026
…1963 (#106)

main has been red on `zig-core` since #104 moved craft to
0.17.0-dev.1963. Every failure was in zig-js at the old pin — that
toolchain appends `pointer_stability` to ArrayList, and zig-js had 27 struct
literals and one JIT layout assertion that named the old shape.

zig-utils/zig-js#876 fixes that upstream, verified there on 1963 (2285
passed, 0 failed) and on the 1818 its own CI pins (53/53 checks). This is the
one-line pin to it.

Locally, craft's full `zig build` and `zig build test` against this zig-js
content on 1963: 153/153 steps, 2519 passed, 0 failed.

Co-authored-by: glennmichael123 <gtorregosa@gmail.com>
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.

2 participants