diff --git a/src/gc.zig b/src/gc.zig index 9c0a5fcf..16d6563f 100644 --- a/src/gc.zig +++ b/src/gc.zig @@ -1888,11 +1888,11 @@ test "Function marking and relocation cover every managed field" { var generator_constants = [_]Value{Value.obj(&old_objects[11])}; var async_constants = [_]Value{Value.obj(&old_objects[12])}; var plain_chunk = bytecode.Chunk.init(std.testing.allocator); - plain_chunk.consts = .{ .items = &plain_constants, .capacity = plain_constants.len }; + plain_chunk.consts = .fromOwnedSlice(&plain_constants); var generator_chunk = bytecode.Chunk.init(std.testing.allocator); - generator_chunk.consts = .{ .items = &generator_constants, .capacity = generator_constants.len }; + generator_chunk.consts = .fromOwnedSlice(&generator_constants); var async_chunk = bytecode.Chunk.init(std.testing.allocator); - async_chunk.consts = .{ .items = &async_constants, .capacity = async_constants.len }; + async_chunk.consts = .fromOwnedSlice(&async_constants); var parent_frame_slots = [_]Value{Value.obj(&old_objects[13])}; var parent_frame = vm.Frame{ .slots = &parent_frame_slots, .parent = null }; var captured_frame_slots = [_]Value{Value.obj(&old_objects[14])}; @@ -2092,8 +2092,8 @@ test "Promise relocation rewrites inline and overflow reaction graphs" { .resolve = Value.obj(&old_objects[6]), .reject = Value.obj(&old_objects[7]), }, - .on_fulfill = .{ .items = &fulfill_overflow, .capacity = fulfill_overflow.len }, - .on_reject = .{ .items = &reject_overflow, .capacity = reject_overflow.len }, + .on_fulfill = .fromOwnedSlice(&fulfill_overflow), + .on_reject = .fromOwnedSlice(&reject_overflow), }; const Plan = struct { @@ -2353,7 +2353,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl .stack = .{ .items = &stack, .capacity = stack.len }, .acc = Value.obj(&old_objects[2]), .frame = &child_frame, - .handlers = .{ .items = &handlers, .capacity = handlers.len }, + .handlers = .fromOwnedSlice(&handlers), }, .env = &old_environment, .this_value = Value.obj(&old_objects[5]), @@ -2362,7 +2362,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl .import_meta_slot = &import_meta, .result = &old_objects[9], .async_parent_promise = &old_promise, - .requests = .{ .items = &requests, .capacity = requests.len }, + .requests = .fromOwnedSlice(&requests), }; var helper = value.IterHelper{ .src = Value.obj(&old_objects[14]), @@ -3395,9 +3395,9 @@ test "realm root relocation rewrites Context registries and embedder handles" { context.next_ticks.items = saved_next_ticks; context.next_ticks.head = saved_next_tick_head; } - context.microtasks.items = .{ .items = µtask_items, .capacity = microtask_items.len }; + context.microtasks.items = .fromOwnedSlice(µtask_items); context.microtasks.head = 0; - context.next_ticks.items = .{ .items = &next_tick_items, .capacity = next_tick_items.len }; + context.next_ticks.items = .fromOwnedSlice(&next_tick_items); context.next_ticks.head = 0; var unhandled = [_]*promise.Promise{&old_promises[0]}; @@ -3453,15 +3453,15 @@ test "realm root relocation rewrites Context registries and embedder handles" { context.private_strong_roots = saved_strong_roots; context.private_weak_roots = saved_weak_roots; } - context.unhandled_rejections = .{ .items = &unhandled, .capacity = unhandled.len }; - context.handled_rejections = .{ .items = &handled, .capacity = handled.len }; - context.async_waiters = .{ .items = &async_waiters, .capacity = async_waiters.len }; - context.timers = .{ .items = &timers, .capacity = timers.len }; - context.finalization_cleanup_jobs = .{ .items = &finalization_jobs, .capacity = finalization_jobs.len }; - context.c_api_class_prototypes = .{ .items = &prototypes, .capacity = prototypes.len }; - context.c_api_handles = .{ .items = &handles, .capacity = handles.len }; - context.private_strong_roots = .{ .items = &strong_roots, .capacity = strong_roots.len }; - context.private_weak_roots = .{ .items = &weak_roots, .capacity = weak_roots.len }; + context.unhandled_rejections = .fromOwnedSlice(&unhandled); + context.handled_rejections = .fromOwnedSlice(&handled); + context.async_waiters = .fromOwnedSlice(&async_waiters); + context.timers = .fromOwnedSlice(&timers); + context.finalization_cleanup_jobs = .fromOwnedSlice(&finalization_jobs); + context.c_api_class_prototypes = .fromOwnedSlice(&prototypes); + context.c_api_handles = .fromOwnedSlice(&handles); + context.private_strong_roots = .fromOwnedSlice(&strong_roots); + context.private_weak_roots = .fromOwnedSlice(&weak_roots); const Plan = struct { old_objects: *[18]Object, @@ -4511,7 +4511,7 @@ test "precise heap realm registry traces relocates and retires one sibling exact const saved_cleanup_jobs = sibling.finalization_cleanup_jobs; defer sibling.finalization_cleanup_jobs = saved_cleanup_jobs; var pending_cleanup = [_]*Object{&relocated_sibling_root}; - sibling.finalization_cleanup_jobs = .{ .items = &pending_cleanup, .capacity = pending_cleanup.len }; + sibling.finalization_cleanup_jobs = .fromOwnedSlice(&pending_cleanup); try std.testing.expectError(error.RealmNotQuiescent, state.realms.retireQuiescent(sibling)); sibling.finalization_cleanup_jobs = .empty; 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 const moving_handle = try sibling.protectValue(moving_value); const saved_cleanup_jobs = sibling.finalization_cleanup_jobs; var pending_cleanup = [_]*Object{moving_handle.get().asObj()}; - sibling.finalization_cleanup_jobs = .{ .items = &pending_cleanup, .capacity = pending_cleanup.len }; + sibling.finalization_cleanup_jobs = .fromOwnedSlice(&pending_cleanup); try std.testing.expectEqual(.unsupported, owner.compactGarbage().status); sibling.finalization_cleanup_jobs = saved_cleanup_jobs; const compaction = owner.compactGarbage(); diff --git a/src/jit/optimizer_compiler.zig b/src/jit/optimizer_compiler.zig index 63dd8e5b..6af30743 100644 --- a/src/jit/optimizer_compiler.zig +++ b/src/jit/optimizer_compiler.zig @@ -3576,12 +3576,47 @@ fn emitDirectNamedPropertyWrite( return direct; } +/// The three words the JIT reads out of an `ObjectElementsState.list`, named +/// for the loads that use them. +/// +/// A description of the prefix, not a mirror of the type. +/// +/// This used to be asserted `@sizeOf`-equal to `std.ArrayListUnmanaged(Value)`, +/// which stopped being true when 0.17.0-dev.1963 appended `pointer_stability`. +/// Measured, `@sizeOf(ArrayListUnmanaged(u64))`: +/// +/// toolchain Debug ReleaseSafe ReleaseFast +/// dev.1818 24 24 24 +/// dev.1963 32 32 24 +/// +/// So the equality held everywhere on 1818, and on 1963 it fails in two build +/// modes and passes in the third — a compile error that depends on `-O`. The +/// JIT never read that tail: `@offsetOf(items)` is 0 and `@offsetOf(capacity)` +/// is 16 in all six combinations above. Only the assertion was wrong. +/// +/// The offsets the loads use come from the real type now +/// (`denseListWordOffset`); this struct only bounds them. const DenseListLayout = extern struct { items: [*]Value, len: usize, capacity: usize, }; +const DenseList = std.ArrayListUnmanaged(Value); + +/// Byte offset of one of the words above, inside the real list type. +/// +/// `@offsetOf` on `DenseList`, not on `DenseListLayout`: a std that reorders +/// or extends the struct moves the loads with it instead of silently reading +/// the wrong word. `len` is the second word of the `items` slice — pointer, +/// then length — which is the one layout fact here the language does fix. +fn denseListWordOffset(comptime field: []const u8) usize { + if (comptime std.mem.eql(u8, field, "items")) return @offsetOf(DenseList, "items"); + if (comptime std.mem.eql(u8, field, "len")) return @offsetOf(DenseList, "items") + @sizeOf([*]Value); + if (comptime std.mem.eql(u8, field, "capacity")) return @offsetOf(DenseList, "capacity"); + @compileError("not a word the JIT reads: " ++ field); +} + fn objectStorageByteOffset(comptime field: []const u8) !u15 { return std.math.cast(u15, @offsetOf(ObjectStorageState, field)) orelse error.UnsupportedChunk; } @@ -3589,7 +3624,7 @@ fn objectStorageByteOffset(comptime field: []const u8) !u15 { fn objectElementsByteOffset(comptime field: []const u8) !u15 { return std.math.cast( u15, - @offsetOf(ObjectElementsState, "list") + @offsetOf(DenseListLayout, field), + @offsetOf(ObjectElementsState, "list") + denseListWordOffset(field), ) orelse error.UnsupportedChunk; } @@ -3601,8 +3636,11 @@ fn emitDirectDenseArrayGuards( comptime has_index: bool, ) !void { comptime { - std.debug.assert(@sizeOf(DenseListLayout) == @sizeOf(std.ArrayListUnmanaged(Value))); - std.debug.assert(@alignOf(DenseListLayout) == @alignOf(std.ArrayListUnmanaged(Value))); + // The prefix the JIT reads, not the whole type — see `DenseListLayout`. + std.debug.assert(@sizeOf([]Value) == 2 * @sizeOf(usize)); // pointer, then length + std.debug.assert(@sizeOf(DenseListLayout) <= @sizeOf(DenseList)); + std.debug.assert(@alignOf(DenseListLayout) <= @alignOf(DenseList)); + std.debug.assert(denseListWordOffset("capacity") + @sizeOf(usize) <= @sizeOf(DenseList)); } // Shared-realm mutation requires the element lock. The same global gate diff --git a/src/jsthread.zig b/src/jsthread.zig index baa973ab..f72f9544 100644 --- a/src/jsthread.zig +++ b/src/jsthread.zig @@ -1774,7 +1774,7 @@ test "jsthread native private relocation mirrors every traced payload" { .release_state = .{ .lock = &lock }, }; var pending = [_]*HoldJob{&hold}; - lock.pending = .{ .items = &pending, .capacity = pending.len }; + lock.pending = .fromOwnedSlice(&pending); var waiter_lock = LockRecord{ .gil = undefined, .owner = &old_objects[4] }; var waiter = AsyncCondWaiter{ .lock = &waiter_lock, .outer = &old_objects[5] }; @@ -1782,7 +1782,7 @@ test "jsthread native private relocation mirrors every traced payload" { var condition = CondRecord{ .gil = undefined, .owner = &old_objects[3], - .queue = .{ .items = &queue, .capacity = queue.len }, + .queue = .fromOwnedSlice(&queue), }; var thread_local = TLRecord{ @@ -1905,7 +1905,7 @@ test "realm root relocation rewrites GIL tasks thread records and property waite .release_state = .{ .lock = &lock }, }; var task_items = [_]*anyopaque{@ptrCast(&job)}; - g.tasks = .{ .items = &task_items, .capacity = task_items.len }; + g.tasks = .fromOwnedSlice(&task_items); var queue = promise.MicrotaskQueue{}; 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 .ctx = undefined, .result = Value.obj(&old_objects[3]), .js_obj = &old_objects[4], - .pending_joins = .{ .items = &pending_joins, .capacity = pending_joins.len }, - .settling_joins = .{ .items = &settling_joins, .capacity = settling_joins.len }, + .pending_joins = .fromOwnedSlice(&pending_joins), + .settling_joins = .fromOwnedSlice(&settling_joins), }; var ticket = PropAsyncTicket{ .obj = &old_objects[7], diff --git a/src/value.zig b/src/value.zig index 2b5d654f..3864101b 100644 --- a/src/value.zig +++ b/src/value.zig @@ -4396,7 +4396,10 @@ pub const Object = struct { return err; }; @memcpy(values[0..old_slots.len], old_slots); - state.* = .{ .list = .{ .items = values[0..old_slots.len], .capacity = values.len } }; + // `initBuffer`, not a literal: 0.17.0-dev.1963 added a field to the + // list, and a literal that names the old fields no longer compiles. + state.* = .{ .list = .initBuffer(values) }; + state.list.items.len = old_slots.len; state.list.appendAssumeCapacity(value_); storage.state.slots.store(state, .release); } diff --git a/src/vm.zig b/src/vm.zig index ade58656..6ac61665 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -214,22 +214,30 @@ const OperandStack = struct { self.items[self.items.len - 1] = item; } + /// The stack as the `ArrayListUnmanaged` it is a view of, so the list can + /// grow and free the buffer on the stack's behalf. + /// + /// Built with `initBuffer` rather than a struct literal. A literal names + /// every field, and Zig 0.17.0-dev.1963 added `pointer_stability` to the + /// list — so a literal that was complete on 1818 is a compile error on + /// 1963. `initBuffer` fills whatever fields the current std has, and + /// `items.len` is the only thing this type adds on top. + fn asList(self: *const OperandStack) std.ArrayListUnmanaged(Value) { + var list: std.ArrayListUnmanaged(Value) = .initBuffer(self.items.ptr[0..self.capacity]); + list.items.len = self.items.len; + return list; + } + pub fn ensureTotalCapacity(self: *OperandStack, allocator: std.mem.Allocator, new_capacity: usize) std.mem.Allocator.Error!void { if (new_capacity <= self.capacity) return; - var list: std.ArrayListUnmanaged(Value) = .{ - .items = self.items, - .capacity = self.capacity, - }; + var list = self.asList(); try list.ensureTotalCapacity(allocator, new_capacity); self.items = list.items; self.capacity = list.capacity; } noinline fn grow(self: *OperandStack, allocator: std.mem.Allocator) std.mem.Allocator.Error!void { - var list: std.ArrayListUnmanaged(Value) = .{ - .items = self.items, - .capacity = self.capacity, - }; + var list = self.asList(); try list.ensureTotalCapacity(allocator, self.items.len + 1); self.items = list.items; self.capacity = list.capacity; @@ -252,10 +260,7 @@ const OperandStack = struct { } pub fn deinit(self: *OperandStack, allocator: std.mem.Allocator) void { - var list: std.ArrayListUnmanaged(Value) = .{ - .items = self.items, - .capacity = self.capacity, - }; + var list = self.asList(); list.deinit(allocator); self.* = .empty; } @@ -10044,7 +10049,7 @@ fn runChunk( try vm.notifyDebuggerException(false); return error.Throw; }, - .throw_not_a_reference => return vm.throwNotAReference(@enumFromInt(inst.a)), + .throw_not_a_reference => return vm.throwNotAReference(@fromBackingInt(@intCast(inst.a))), .push_handler, .push_handler_catch, .push_handler_outer => { const outer = inst.op == .push_handler_outer; if (outer and (exec.environment_depth == 0 or vm.env.parent == null)) return error.OutOfMemory;