also looking at this
fix(compat): stop describing ArrayList's layout, and read it instead
#876
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.
| @@ -1888,11 +1888,11 @@ test "Function marking and relocation cover every managed field" { | ||
| 1888 | 1888 | var generator_constants = [_]Value{Value.obj(&old_objects[11])}; |
| 1889 | 1889 | var async_constants = [_]Value{Value.obj(&old_objects[12])}; |
| 1890 | 1890 | 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); | |
| 1892 | 1892 | 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); | |
| 1894 | 1894 | 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); | |
| 1896 | 1896 | var parent_frame_slots = [_]Value{Value.obj(&old_objects[13])}; |
| 1897 | 1897 | var parent_frame = vm.Frame{ .slots = &parent_frame_slots, .parent = null }; |
| 1898 | 1898 | var captured_frame_slots = [_]Value{Value.obj(&old_objects[14])}; |
| @@ -2092,8 +2092,8 @@ test "Promise relocation rewrites inline and overflow reaction graphs" { | ||
| 2092 | 2092 | .resolve = Value.obj(&old_objects[6]), |
| 2093 | 2093 | .reject = Value.obj(&old_objects[7]), |
| 2094 | 2094 | }, |
| 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), | |
| 2097 | 2097 | }; |
| 2098 | 2098 | |
| 2099 | 2099 | const Plan = struct { |
| @@ -2353,7 +2353,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl | ||
| 2353 | 2353 | .stack = .{ .items = &stack, .capacity = stack.len }, |
| 2354 | 2354 | .acc = Value.obj(&old_objects[2]), |
| 2355 | 2355 | .frame = &child_frame, |
| 2356 | .handlers = .{ .items = &handlers, .capacity = handlers.len }, | |
| 2356 | .handlers = .fromOwnedSlice(&handlers), | |
| 2357 | 2357 | }, |
| 2358 | 2358 | .env = &old_environment, |
| 2359 | 2359 | .this_value = Value.obj(&old_objects[5]), |
| @@ -2362,7 +2362,7 @@ test "Generator and IteratorHelper marking and relocation cover every managed sl | ||
| 2362 | 2362 | .import_meta_slot = &import_meta, |
| 2363 | 2363 | .result = &old_objects[9], |
| 2364 | 2364 | .async_parent_promise = &old_promise, |
| 2365 | .requests = .{ .items = &requests, .capacity = requests.len }, | |
| 2365 | .requests = .fromOwnedSlice(&requests), | |
| 2366 | 2366 | }; |
| 2367 | 2367 | var helper = value.IterHelper{ |
| 2368 | 2368 | .src = Value.obj(&old_objects[14]), |
| @@ -3395,9 +3395,9 @@ test "realm root relocation rewrites Context registries and embedder handles" { | ||
| 3395 | 3395 | context.next_ticks.items = saved_next_ticks; |
| 3396 | 3396 | context.next_ticks.head = saved_next_tick_head; |
| 3397 | 3397 | } |
| 3398 | context.microtasks.items = .{ .items = µtask_items, .capacity = microtask_items.len }; | |
| 3398 | context.microtasks.items = .fromOwnedSlice(µtask_items); | |
| 3399 | 3399 | 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); | |
| 3401 | 3401 | context.next_ticks.head = 0; |
| 3402 | 3402 | |
| 3403 | 3403 | var unhandled = [_]*promise.Promise{&old_promises[0]}; |
| @@ -3453,15 +3453,15 @@ test "realm root relocation rewrites Context registries and embedder handles" { | ||
| 3453 | 3453 | context.private_strong_roots = saved_strong_roots; |
| 3454 | 3454 | context.private_weak_roots = saved_weak_roots; |
| 3455 | 3455 | } |
| 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); | |
| 3465 | 3465 | |
| 3466 | 3466 | const Plan = struct { |
| 3467 | 3467 | old_objects: *[18]Object, |
| @@ -4511,7 +4511,7 @@ test "precise heap realm registry traces relocates and retires one sibling exact | ||
| 4511 | 4511 | const saved_cleanup_jobs = sibling.finalization_cleanup_jobs; |
| 4512 | 4512 | defer sibling.finalization_cleanup_jobs = saved_cleanup_jobs; |
| 4513 | 4513 | 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); | |
| 4515 | 4515 | try std.testing.expectError(error.RealmNotQuiescent, state.realms.retireQuiescent(sibling)); |
| 4516 | 4516 | sibling.finalization_cleanup_jobs = .empty; |
| 4517 | 4517 | 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 | ||
| 4700 | 4700 | const moving_handle = try sibling.protectValue(moving_value); |
| 4701 | 4701 | const saved_cleanup_jobs = sibling.finalization_cleanup_jobs; |
| 4702 | 4702 | 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); | |
| 4704 | 4704 | try std.testing.expectEqual(.unsupported, owner.compactGarbage().status); |
| 4705 | 4705 | sibling.finalization_cleanup_jobs = saved_cleanup_jobs; |
| 4706 | 4706 | const compaction = owner.compactGarbage(); |
| @@ -3576,12 +3576,47 @@ fn emitDirectNamedPropertyWrite( | ||
| 3576 | 3576 | return direct; |
| 3577 | 3577 | } |
| 3578 | 3578 | |
| 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. | |
| 3579 | 3599 | const DenseListLayout = extern struct { |
| 3580 | 3600 | items: [*]Value, |
| 3581 | 3601 | len: usize, |
| 3582 | 3602 | capacity: usize, |
| 3583 | 3603 | }; |
| 3584 | 3604 | |
| 3605 | const 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. | |
| 3613 | fn 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 | ||
| 3585 | 3620 | fn objectStorageByteOffset(comptime field: []const u8) !u15 { |
| 3586 | 3621 | return std.math.cast(u15, @offsetOf(ObjectStorageState, field)) orelse error.UnsupportedChunk; |
| 3587 | 3622 | } |
| @@ -3589,7 +3624,7 @@ fn objectStorageByteOffset(comptime field: []const u8) !u15 { | ||
| 3589 | 3624 | fn objectElementsByteOffset(comptime field: []const u8) !u15 { |
| 3590 | 3625 | return std.math.cast( |
| 3591 | 3626 | u15, |
| 3592 | @offsetOf(ObjectElementsState, "list") + @offsetOf(DenseListLayout, field), | |
| 3627 | @offsetOf(ObjectElementsState, "list") + denseListWordOffset(field), | |
| 3593 | 3628 | ) orelse error.UnsupportedChunk; |
| 3594 | 3629 | } |
| 3595 | 3630 | |
| @@ -3601,8 +3636,11 @@ fn emitDirectDenseArrayGuards( | ||
| 3601 | 3636 | comptime has_index: bool, |
| 3602 | 3637 | ) !void { |
| 3603 | 3638 | 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)); | |
| 3606 | 3644 | } |
| 3607 | 3645 | |
| 3608 | 3646 | // Shared-realm mutation requires the element lock. The same global gate |
| @@ -1774,7 +1774,7 @@ test "jsthread native private relocation mirrors every traced payload" { | ||
| 1774 | 1774 | .release_state = .{ .lock = &lock }, |
| 1775 | 1775 | }; |
| 1776 | 1776 | var pending = [_]*HoldJob{&hold}; |
| 1777 | lock.pending = .{ .items = &pending, .capacity = pending.len }; | |
| 1777 | lock.pending = .fromOwnedSlice(&pending); | |
| 1778 | 1778 | |
| 1779 | 1779 | var waiter_lock = LockRecord{ .gil = undefined, .owner = &old_objects[4] }; |
| 1780 | 1780 | var waiter = AsyncCondWaiter{ .lock = &waiter_lock, .outer = &old_objects[5] }; |
| @@ -1782,7 +1782,7 @@ test "jsthread native private relocation mirrors every traced payload" { | ||
| 1782 | 1782 | var condition = CondRecord{ |
| 1783 | 1783 | .gil = undefined, |
| 1784 | 1784 | .owner = &old_objects[3], |
| 1785 | .queue = .{ .items = &queue, .capacity = queue.len }, | |
| 1785 | .queue = .fromOwnedSlice(&queue), | |
| 1786 | 1786 | }; |
| 1787 | 1787 | |
| 1788 | 1788 | var thread_local = TLRecord{ |
| @@ -1905,7 +1905,7 @@ test "realm root relocation rewrites GIL tasks thread records and property waite | ||
| 1905 | 1905 | .release_state = .{ .lock = &lock }, |
| 1906 | 1906 | }; |
| 1907 | 1907 | var task_items = [_]*anyopaque{@ptrCast(&job)}; |
| 1908 | g.tasks = .{ .items = &task_items, .capacity = task_items.len }; | |
| 1908 | g.tasks = .fromOwnedSlice(&task_items); | |
| 1909 | 1909 | |
| 1910 | 1910 | var queue = promise.MicrotaskQueue{}; |
| 1911 | 1911 | 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 | ||
| 1916 | 1916 | .ctx = undefined, |
| 1917 | 1917 | .result = Value.obj(&old_objects[3]), |
| 1918 | 1918 | .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), | |
| 1921 | 1921 | }; |
| 1922 | 1922 | var ticket = PropAsyncTicket{ |
| 1923 | 1923 | .obj = &old_objects[7], |
| @@ -4396,7 +4396,10 @@ pub const Object = struct { | ||
| 4396 | 4396 | return err; |
| 4397 | 4397 | }; |
| 4398 | 4398 | @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; | |
| 4400 | 4403 | state.list.appendAssumeCapacity(value_); |
| 4401 | 4404 | storage.state.slots.store(state, .release); |
| 4402 | 4405 | } |
| @@ -214,22 +214,30 @@ const OperandStack = struct { | ||
| 214 | 214 | self.items[self.items.len - 1] = item; |
| 215 | 215 | } |
| 216 | 216 | |
| 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 | ||
| 217 | 231 | pub fn ensureTotalCapacity(self: *OperandStack, allocator: std.mem.Allocator, new_capacity: usize) std.mem.Allocator.Error!void { |
| 218 | 232 | 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(); | |
| 223 | 234 | try list.ensureTotalCapacity(allocator, new_capacity); |
| 224 | 235 | self.items = list.items; |
| 225 | 236 | self.capacity = list.capacity; |
| 226 | 237 | } |
| 227 | 238 | |
| 228 | 239 | 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(); | |
| 233 | 241 | try list.ensureTotalCapacity(allocator, self.items.len + 1); |
| 234 | 242 | self.items = list.items; |
| 235 | 243 | self.capacity = list.capacity; |
| @@ -252,10 +260,7 @@ const OperandStack = struct { | ||
| 252 | 260 | } |
| 253 | 261 | |
| 254 | 262 | 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(); | |
| 259 | 264 | list.deinit(allocator); |
| 260 | 265 | self.* = .empty; |
| 261 | 266 | } |
| @@ -10044,7 +10049,7 @@ fn runChunk( | ||
| 10044 | 10049 | try vm.notifyDebuggerException(false); |
| 10045 | 10050 | return error.Throw; |
| 10046 | 10051 | }, |
| 10047 | .throw_not_a_reference => return vm.throwNotAReference(@enumFromInt(inst.a)), | |
| 10052 | .throw_not_a_reference => return vm.throwNotAReference(@fromBackingInt(@intCast(inst.a))), | |
| 10048 | 10053 | .push_handler, .push_handler_catch, .push_handler_outer => { |
| 10049 | 10054 | const outer = inst.op == .push_handler_outer; |
| 10050 | 10055 | if (outer and (exec.environment_depth == 0 or vm.env.parent == null)) return error.OutOfMemory; |