ReviewOS

also looking at this

zig-utils/zig-js

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

#876
Merged glennmichael123 wants to merge fix/zig-1963-arraylist-compat into main
5 files +88 -42

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

src/gc.zigmodified+20-20
Changes to src/gc.zig
@@ -1888,11 +1888,11 @@ test "Function marking and relocation cover every managed field" {
18881888 var generator_constants = [_]Value{Value.obj(&old_objects[11])};
18891889 var async_constants = [_]Value{Value.obj(&old_objects[12])};
18901890 var plain_chunk = bytecode.Chunk.init(std.testing.allocator);
1891 plain_chunk.consts = .{ .items = &plain_constants, .capacity = plain_constants.len };
1891 plain_chunk.consts = .fromOwnedSlice(&plain_constants);
18921892 var generator_chunk = bytecode.Chunk.init(std.testing.allocator);
1893 generator_chunk.consts = .{ .items = &generator_constants, .capacity = generator_constants.len };
1893 generator_chunk.consts = .fromOwnedSlice(&generator_constants);
18941894 var async_chunk = bytecode.Chunk.init(std.testing.allocator);
1895 async_chunk.consts = .{ .items = &async_constants, .capacity = async_constants.len };
1895 async_chunk.consts = .fromOwnedSlice(&async_constants);
18961896 var parent_frame_slots = [_]Value{Value.obj(&old_objects[13])};
18971897 var parent_frame = vm.Frame{ .slots = &parent_frame_slots, .parent = null };
18981898 var captured_frame_slots = [_]Value{Value.obj(&old_objects[14])};
@@ -2092,8 +2092,8 @@ test "Promise relocation rewrites inline and overflow reaction graphs" {
20922092 .resolve = Value.obj(&old_objects[6]),
20932093 .reject = Value.obj(&old_objects[7]),
20942094 },
2095 .on_fulfill = .{ .items = &fulfill_overflow, .capacity = fulfill_overflow.len },
2096 .on_reject = .{ .items = &reject_overflow, .capacity = reject_overflow.len },
2095 .on_fulfill = .fromOwnedSlice(&fulfill_overflow),
2096 .on_reject = .fromOwnedSlice(&reject_overflow),
20972097 };
20982098
20992099 const Plan = struct {
@@ -2353,7 +2353,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl
23532353 .stack = .{ .items = &stack, .capacity = stack.len },
23542354 .acc = Value.obj(&old_objects[2]),
23552355 .frame = &child_frame,
2356 .handlers = .{ .items = &handlers, .capacity = handlers.len },
2356 .handlers = .fromOwnedSlice(&handlers),
23572357 },
23582358 .env = &old_environment,
23592359 .this_value = Value.obj(&old_objects[5]),
@@ -2362,7 +2362,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl
23622362 .import_meta_slot = &import_meta,
23632363 .result = &old_objects[9],
23642364 .async_parent_promise = &old_promise,
2365 .requests = .{ .items = &requests, .capacity = requests.len },
2365 .requests = .fromOwnedSlice(&requests),
23662366 };
23672367 var helper = value.IterHelper{
23682368 .src = Value.obj(&old_objects[14]),
@@ -3395,9 +3395,9 @@ test "realm root relocation rewrites Context registries and embedder handles" {
33953395 context.next_ticks.items = saved_next_ticks;
33963396 context.next_ticks.head = saved_next_tick_head;
33973397 }
3398 context.microtasks.items = .{ .items = &microtask_items, .capacity = microtask_items.len };
3398 context.microtasks.items = .fromOwnedSlice(&microtask_items);
33993399 context.microtasks.head = 0;
3400 context.next_ticks.items = .{ .items = &next_tick_items, .capacity = next_tick_items.len };
3400 context.next_ticks.items = .fromOwnedSlice(&next_tick_items);
34013401 context.next_ticks.head = 0;
34023402
34033403 var unhandled = [_]*promise.Promise{&old_promises[0]};
@@ -3453,15 +3453,15 @@ test "realm root relocation rewrites Context registries and embedder handles" {
34533453 context.private_strong_roots = saved_strong_roots;
34543454 context.private_weak_roots = saved_weak_roots;
34553455 }
3456 context.unhandled_rejections = .{ .items = &unhandled, .capacity = unhandled.len };
3457 context.handled_rejections = .{ .items = &handled, .capacity = handled.len };
3458 context.async_waiters = .{ .items = &async_waiters, .capacity = async_waiters.len };
3459 context.timers = .{ .items = &timers, .capacity = timers.len };
3460 context.finalization_cleanup_jobs = .{ .items = &finalization_jobs, .capacity = finalization_jobs.len };
3461 context.c_api_class_prototypes = .{ .items = &prototypes, .capacity = prototypes.len };
3462 context.c_api_handles = .{ .items = &handles, .capacity = handles.len };
3463 context.private_strong_roots = .{ .items = &strong_roots, .capacity = strong_roots.len };
3464 context.private_weak_roots = .{ .items = &weak_roots, .capacity = weak_roots.len };
3456 context.unhandled_rejections = .fromOwnedSlice(&unhandled);
3457 context.handled_rejections = .fromOwnedSlice(&handled);
3458 context.async_waiters = .fromOwnedSlice(&async_waiters);
3459 context.timers = .fromOwnedSlice(&timers);
3460 context.finalization_cleanup_jobs = .fromOwnedSlice(&finalization_jobs);
3461 context.c_api_class_prototypes = .fromOwnedSlice(&prototypes);
3462 context.c_api_handles = .fromOwnedSlice(&handles);
3463 context.private_strong_roots = .fromOwnedSlice(&strong_roots);
3464 context.private_weak_roots = .fromOwnedSlice(&weak_roots);
34653465
34663466 const Plan = struct {
34673467 old_objects: *[18]Object,
@@ -4511,7 +4511,7 @@ test "precise heap realm registry traces relocates and retires one sibling exact
45114511 const saved_cleanup_jobs = sibling.finalization_cleanup_jobs;
45124512 defer sibling.finalization_cleanup_jobs = saved_cleanup_jobs;
45134513 var pending_cleanup = [_]*Object{&relocated_sibling_root};
4514 sibling.finalization_cleanup_jobs = .{ .items = &pending_cleanup, .capacity = pending_cleanup.len };
4514 sibling.finalization_cleanup_jobs = .fromOwnedSlice(&pending_cleanup);
45154515 try std.testing.expectError(error.RealmNotQuiescent, state.realms.retireQuiescent(sibling));
45164516 sibling.finalization_cleanup_jobs = .empty;
45174517 try std.testing.expectError(error.OwnerCannotRetire, state.realms.retireQuiescent(owner));
@@ -4700,7 +4700,7 @@ test "precise sibling realm shares one heap and retires only after cross-realm h
47004700 const moving_handle = try sibling.protectValue(moving_value);
47014701 const saved_cleanup_jobs = sibling.finalization_cleanup_jobs;
47024702 var pending_cleanup = [_]*Object{moving_handle.get().asObj()};
4703 sibling.finalization_cleanup_jobs = .{ .items = &pending_cleanup, .capacity = pending_cleanup.len };
4703 sibling.finalization_cleanup_jobs = .fromOwnedSlice(&pending_cleanup);
47044704 try std.testing.expectEqual(.unsupported, owner.compactGarbage().status);
47054705 sibling.finalization_cleanup_jobs = saved_cleanup_jobs;
47064706 const compaction = owner.compactGarbage();
src/jit/optimizer_compiler.zigmodified+41-3
Changes to src/jit/optimizer_compiler.zig
@@ -3576,12 +3576,47 @@ fn emitDirectNamedPropertyWrite(
35763576 return direct;
35773577}
35783578
3579/// The three words the JIT reads out of an `ObjectElementsState.list`, named
3580/// for the loads that use them.
3581///
3582/// A description of the prefix, not a mirror of the type.
3583///
3584/// This used to be asserted `@sizeOf`-equal to `std.ArrayListUnmanaged(Value)`,
3585/// which stopped being true when 0.17.0-dev.1963 appended `pointer_stability`.
3586/// Measured, `@sizeOf(ArrayListUnmanaged(u64))`:
3587///
3588/// toolchain Debug ReleaseSafe ReleaseFast
3589/// dev.1818 24 24 24
3590/// dev.1963 32 32 24
3591///
3592/// So the equality held everywhere on 1818, and on 1963 it fails in two build
3593/// modes and passes in the third — a compile error that depends on `-O`. The
3594/// JIT never read that tail: `@offsetOf(items)` is 0 and `@offsetOf(capacity)`
3595/// is 16 in all six combinations above. Only the assertion was wrong.
3596///
3597/// The offsets the loads use come from the real type now
3598/// (`denseListWordOffset`); this struct only bounds them.
35793599const DenseListLayout = extern struct {
35803600 items: [*]Value,
35813601 len: usize,
35823602 capacity: usize,
35833603};
35843604
3605const DenseList = std.ArrayListUnmanaged(Value);
3606
3607/// Byte offset of one of the words above, inside the real list type.
3608///
3609/// `@offsetOf` on `DenseList`, not on `DenseListLayout`: a std that reorders
3610/// or extends the struct moves the loads with it instead of silently reading
3611/// the wrong word. `len` is the second word of the `items` slice — pointer,
3612/// then length — which is the one layout fact here the language does fix.
3613fn denseListWordOffset(comptime field: []const u8) usize {
3614 if (comptime std.mem.eql(u8, field, "items")) return @offsetOf(DenseList, "items");
3615 if (comptime std.mem.eql(u8, field, "len")) return @offsetOf(DenseList, "items") + @sizeOf([*]Value);
3616 if (comptime std.mem.eql(u8, field, "capacity")) return @offsetOf(DenseList, "capacity");
3617 @compileError("not a word the JIT reads: " ++ field);
3618}
3619
35853620fn objectStorageByteOffset(comptime field: []const u8) !u15 {
35863621 return std.math.cast(u15, @offsetOf(ObjectStorageState, field)) orelse error.UnsupportedChunk;
35873622}
@@ -3589,7 +3624,7 @@ fn objectStorageByteOffset(comptime field: []const u8) !u15 {
35893624fn objectElementsByteOffset(comptime field: []const u8) !u15 {
35903625 return std.math.cast(
35913626 u15,
3592 @offsetOf(ObjectElementsState, "list") + @offsetOf(DenseListLayout, field),
3627 @offsetOf(ObjectElementsState, "list") + denseListWordOffset(field),
35933628 ) orelse error.UnsupportedChunk;
35943629}
35953630
@@ -3601,8 +3636,11 @@ fn emitDirectDenseArrayGuards(
36013636 comptime has_index: bool,
36023637) !void {
36033638 comptime {
3604 std.debug.assert(@sizeOf(DenseListLayout) == @sizeOf(std.ArrayListUnmanaged(Value)));
3605 std.debug.assert(@alignOf(DenseListLayout) == @alignOf(std.ArrayListUnmanaged(Value)));
3639 // The prefix the JIT reads, not the whole type — see `DenseListLayout`.
3640 std.debug.assert(@sizeOf([]Value) == 2 * @sizeOf(usize)); // pointer, then length
3641 std.debug.assert(@sizeOf(DenseListLayout) <= @sizeOf(DenseList));
3642 std.debug.assert(@alignOf(DenseListLayout) <= @alignOf(DenseList));
3643 std.debug.assert(denseListWordOffset("capacity") + @sizeOf(usize) <= @sizeOf(DenseList));
36063644 }
36073645
36083646 // Shared-realm mutation requires the element lock. The same global gate
src/jsthread.zigmodified+5-5
Changes to src/jsthread.zig
@@ -1774,7 +1774,7 @@ test "jsthread native private relocation mirrors every traced payload" {
17741774 .release_state = .{ .lock = &lock },
17751775 };
17761776 var pending = [_]*HoldJob{&hold};
1777 lock.pending = .{ .items = &pending, .capacity = pending.len };
1777 lock.pending = .fromOwnedSlice(&pending);
17781778
17791779 var waiter_lock = LockRecord{ .gil = undefined, .owner = &old_objects[4] };
17801780 var waiter = AsyncCondWaiter{ .lock = &waiter_lock, .outer = &old_objects[5] };
@@ -1782,7 +1782,7 @@ test "jsthread native private relocation mirrors every traced payload" {
17821782 var condition = CondRecord{
17831783 .gil = undefined,
17841784 .owner = &old_objects[3],
1785 .queue = .{ .items = &queue, .capacity = queue.len },
1785 .queue = .fromOwnedSlice(&queue),
17861786 };
17871787
17881788 var thread_local = TLRecord{
@@ -1905,7 +1905,7 @@ test "realm root relocation rewrites GIL tasks thread records and property waite
19051905 .release_state = .{ .lock = &lock },
19061906 };
19071907 var task_items = [_]*anyopaque{@ptrCast(&job)};
1908 g.tasks = .{ .items = &task_items, .capacity = task_items.len };
1908 g.tasks = .fromOwnedSlice(&task_items);
19091909
19101910 var queue = promise.MicrotaskQueue{};
19111911 var pending_joins = [_]PendingJoin{.{ .promise = &old_objects[5], .microtasks = &queue }};
@@ -1916,8 +1916,8 @@ test "realm root relocation rewrites GIL tasks thread records and property waite
19161916 .ctx = undefined,
19171917 .result = Value.obj(&old_objects[3]),
19181918 .js_obj = &old_objects[4],
1919 .pending_joins = .{ .items = &pending_joins, .capacity = pending_joins.len },
1920 .settling_joins = .{ .items = &settling_joins, .capacity = settling_joins.len },
1919 .pending_joins = .fromOwnedSlice(&pending_joins),
1920 .settling_joins = .fromOwnedSlice(&settling_joins),
19211921 };
19221922 var ticket = PropAsyncTicket{
19231923 .obj = &old_objects[7],
src/value.zigmodified+4-1
Changes to src/value.zig
@@ -4396,7 +4396,10 @@ pub const Object = struct {
43964396 return err;
43974397 };
43984398 @memcpy(values[0..old_slots.len], old_slots);
4399 state.* = .{ .list = .{ .items = values[0..old_slots.len], .capacity = values.len } };
4399 // `initBuffer`, not a literal: 0.17.0-dev.1963 added a field to the
4400 // list, and a literal that names the old fields no longer compiles.
4401 state.* = .{ .list = .initBuffer(values) };
4402 state.list.items.len = old_slots.len;
44004403 state.list.appendAssumeCapacity(value_);
44014404 storage.state.slots.store(state, .release);
44024405 }
src/vm.zigmodified+18-13
Changes to src/vm.zig
@@ -214,22 +214,30 @@ const OperandStack = struct {
214214 self.items[self.items.len - 1] = item;
215215 }
216216
217 /// The stack as the `ArrayListUnmanaged` it is a view of, so the list can
218 /// grow and free the buffer on the stack's behalf.
219 ///
220 /// Built with `initBuffer` rather than a struct literal. A literal names
221 /// every field, and Zig 0.17.0-dev.1963 added `pointer_stability` to the
222 /// list — so a literal that was complete on 1818 is a compile error on
223 /// 1963. `initBuffer` fills whatever fields the current std has, and
224 /// `items.len` is the only thing this type adds on top.
225 fn asList(self: *const OperandStack) std.ArrayListUnmanaged(Value) {
226 var list: std.ArrayListUnmanaged(Value) = .initBuffer(self.items.ptr[0..self.capacity]);
227 list.items.len = self.items.len;
228 return list;
229 }
230
217231 pub fn ensureTotalCapacity(self: *OperandStack, allocator: std.mem.Allocator, new_capacity: usize) std.mem.Allocator.Error!void {
218232 if (new_capacity <= self.capacity) return;
219 var list: std.ArrayListUnmanaged(Value) = .{
220 .items = self.items,
221 .capacity = self.capacity,
222 };
233 var list = self.asList();
223234 try list.ensureTotalCapacity(allocator, new_capacity);
224235 self.items = list.items;
225236 self.capacity = list.capacity;
226237 }
227238
228239 noinline fn grow(self: *OperandStack, allocator: std.mem.Allocator) std.mem.Allocator.Error!void {
229 var list: std.ArrayListUnmanaged(Value) = .{
230 .items = self.items,
231 .capacity = self.capacity,
232 };
240 var list = self.asList();
233241 try list.ensureTotalCapacity(allocator, self.items.len + 1);
234242 self.items = list.items;
235243 self.capacity = list.capacity;
@@ -252,10 +260,7 @@ const OperandStack = struct {
252260 }
253261
254262 pub fn deinit(self: *OperandStack, allocator: std.mem.Allocator) void {
255 var list: std.ArrayListUnmanaged(Value) = .{
256 .items = self.items,
257 .capacity = self.capacity,
258 };
263 var list = self.asList();
259264 list.deinit(allocator);
260265 self.* = .empty;
261266 }
@@ -10044,7 +10049,7 @@ fn runChunk(
1004410049 try vm.notifyDebuggerException(false);
1004510050 return error.Throw;
1004610051 },
10047 .throw_not_a_reference => return vm.throwNotAReference(@enumFromInt(inst.a)),
10052 .throw_not_a_reference => return vm.throwNotAReference(@fromBackingInt(@intCast(inst.a))),
1004810053 .push_handler, .push_handler_catch, .push_handler_outer => {
1004910054 const outer = inst.op == .push_handler_outer;
1005010055 if (outer and (exec.environment_depth == 0 or vm.env.parent == null)) return error.OutOfMemory;