-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalue.zig
More file actions
3177 lines (2918 loc) · 143 KB
/
Copy pathvalue.zig
File metadata and controls
3177 lines (2918 loc) · 143 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const std = @import("std");
const Shape = @import("shape.zig").Shape;
const SharedBufferStorage = @import("shared_buffer.zig").SharedBufferStorage;
const gc_runtime = @import("gc_runtime.zig");
const object_profile = @import("object_profile.zig");
const strcell = @import("strcell.zig");
const StringCell = strcell.StringCell;
/// GC insertion barrier for a `Value` stored into a live cell. It records an
/// old-to-young edge for minor collection and shades the child during an active
/// incremental/full mark. Only `.object` carries a cell.
inline fn gcBarrier(owner: *Object, v: Value) void {
if (v.isObject()) gc_runtime.barrierFrom(@ptrCast(owner), @ptrCast(v.asObj()));
}
/// The C-ABI shape of a host (Zig/C) function exposed to JS via
/// `JSObjectMakeFunctionWithCallback`. Kept here so both the interpreter and
/// the C-API layer agree on the type. All ref args are word-sized opaque
/// pointers (JSValueRef / JSObjectRef / JSContextRef in JSC terms).
pub const HostCallback = *const fn (
ctx: ?*anyopaque,
function: ?*anyopaque,
this_object: ?*anyopaque,
argument_count: usize,
arguments: [*c]const ?*anyopaque,
exception: [*c]?*anyopaque,
) callconv(.c) ?*anyopaque;
/// Error set a native builtin may return (mirrors the interpreter's EvalError;
/// error values are global by name, so they coerce). `OptShortCircuit` is an
/// internal control-flow signal natives never actually produce, included only
/// so the sets unify.
pub const HostError = error{ OutOfMemory, Throw, OptShortCircuit };
/// A Zig-native function exposed to JS. Unlike `HostCallback` (the C-ABI JSC
/// shape used across the FFI boundary), this is the in-process hook the
/// interpreter calls directly: `ctx` is the `*Interpreter` (type-erased to
/// avoid an import cycle; cast it back), `this` is the receiver, and the native
/// may allocate via the interpreter's arena and raise JS exceptions. Used for
/// engine builtins and the conformance harness's `assert`.
pub const NativeFn = *const fn (ctx: *anyopaque, this: Value, args: []const Value) HostError!Value;
/// The element type of a typed array (no BigInt variants — those need a BigInt
/// value type). Each carries its byte width and how to read/write a value.
pub const TAKind = enum {
i8,
u8,
u8c, // Uint8ClampedArray
i16,
u16,
i32,
u32,
f16,
f32,
f64,
i64, // BigInt64Array (elements are BigInt)
u64, // BigUint64Array (elements are BigInt)
pub fn byteSize(self: TAKind) usize {
return switch (self) {
.i8, .u8, .u8c => 1,
.i16, .u16, .f16 => 2,
.i32, .u32, .f32 => 4,
.f64, .i64, .u64 => 8,
};
}
/// The constructor/`Symbol.toStringTag` name (`"Int8Array"`, …).
pub fn ctorName(self: TAKind) []const u8 {
return switch (self) {
.i8 => "Int8Array",
.u8 => "Uint8Array",
.u8c => "Uint8ClampedArray",
.i16 => "Int16Array",
.u16 => "Uint16Array",
.i32 => "Int32Array",
.u32 => "Uint32Array",
.f16 => "Float16Array",
.f32 => "Float32Array",
.f64 => "Float64Array",
.i64 => "BigInt64Array",
.u64 => "BigUint64Array",
};
}
/// Whether the element type is BigInt (BigInt64Array / BigUint64Array): such
/// arrays read/write BigInt values rather than Numbers.
pub fn isBigInt(self: TAKind) bool {
return self == .i64 or self == .u64;
}
pub fn fromName(name: []const u8) ?TAKind {
inline for (.{ .i8, .u8, .u8c, .i16, .u16, .i32, .u32, .f16, .f32, .f64, .i64, .u64 }) |k| {
if (std.mem.eql(u8, name, (@as(TAKind, k)).ctorName())) return k;
}
return null;
}
};
/// An `ArrayBuffer`'s backing bytes. `detached` is set by `$262.detachArrayBuffer`
/// / transfer; a detached buffer's views read undefined / throw on length checks.
pub const ArrayBufferData = struct {
lock: std.atomic.Mutex = .unlocked,
/// Seqlock counter for `local_data` swaps in `resize` (see `bytes`). Even =
/// stable, odd = swap in progress.
resize_seq: std.atomic.Value(u32) = .init(0),
/// Backing bytes of a NON-shared buffer. Arena contexts keep these in the
/// realm arena; GC contexts allocate them from the context backing
/// allocator so object finalization can release them on collection. Empty
/// and unused when `shared` is set — a SharedArrayBuffer's bytes live in
/// process-wide refcounted storage so they can outlive this realm and be
/// seen by other agents. Always read the live bytes through `bytes()`,
/// never this field directly.
local_data: []u8,
/// Non-null iff `is_shared`: the cross-agent backing storage. The wrapper
/// holds one reference, tracked in the owning realm's `RetainList`.
shared: ?*SharedBufferStorage = null,
/// Metadata and `local_data` are owned by the GC finalizer rather than the
/// arena. Shared buffers set this for the metadata only; their bytes are
/// owned by `SharedBufferStorage`.
gc_owned: bool = false,
detached_flag: std.atomic.Value(bool) = .init(false),
/// For a resizable ArrayBuffer (or growable SharedArrayBuffer), the maximum
/// byte length; null means fixed-length (not resizable/growable).
max_byte_length: ?usize = null,
/// A SharedArrayBuffer (never detaches; `grow` only increases length).
is_shared: bool = false,
/// An immutable ArrayBuffer (from `transferToImmutable`/`sliceToImmutable`):
/// fixed-length, never detaches, and rejects every write.
immutable: bool = false,
/// The buffer's live bytes: the shared storage's published slice for a
/// SharedArrayBuffer (always current, even after another realm grows it),
/// the arena bytes otherwise.
/// A resize swaps `local_data` (ptr+len) under `lockBuffer`; an unlocked
/// reader here could otherwise read a torn ptr/len (→ OOB). Under parallel
/// mode, read it through a seqlock against `resize_seq` (bumped odd→even
/// around the swap in `arrayBufferResize`). Default engine: the gate is off,
/// so this stays a single field load. Callers that already hold `lockBuffer`
/// (the Atomics paths) see a stable `resize_seq` and return on the first try —
/// no re-entrancy, no deadlock.
pub inline fn bytes(self: *const ArrayBufferData) []u8 {
if (self.shared) |s| return s.slice();
if (!Object.element_locks_enabled.load(.monotonic)) return self.local_data;
// Seqlock read: ptr+len accessed with relaxed atomics (a plain mov each,
// so even a retried read is a non-racing atomic access — TSan-clean), and
// the seqcount makes the pair consistent across a concurrent resize swap.
const ld = &@constCast(self).local_data;
const sc = &@constCast(self).resize_seq;
while (true) {
const s1 = sc.load(.acquire);
if ((s1 & 1) != 0) { // writer mid-swap
std.atomic.spinLoopHint();
continue;
}
const p = @atomicLoad([*]u8, &ld.ptr, .monotonic);
const l = @atomicLoad(usize, &ld.len, .monotonic);
// Re-read the seqcount with a no-op acq_rel RMW (Zig has no standalone
// fence): its release ordering keeps the ptr/len loads from being
// reordered past it, so a torn pair can't slip through the check.
if (sc.fetchAdd(0, .acq_rel) == s1) return p[0..l];
std.atomic.spinLoopHint();
}
}
/// Swap `local_data` to `fresh` as a seqlock writer: odd during the swap,
/// even after; ptr/len stored with relaxed atomics to pair with `bytes()`.
/// Caller holds `lockBuffer` (writers serialized).
pub fn swapLocalData(self: *ArrayBufferData, fresh: []u8) void {
_ = self.resize_seq.fetchAdd(1, .acq_rel); // → odd
@atomicStore([*]u8, &self.local_data.ptr, fresh.ptr, .monotonic);
@atomicStore(usize, &self.local_data.len, fresh.len, .monotonic);
_ = self.resize_seq.fetchAdd(1, .release); // → even
}
/// The detach flag, accessed atomically: a peer thread can `transfer`/detach
/// a (non-shared) buffer while another reads it (no-GIL). A single bool is one
/// byte, so `.monotonic` is a plain load/store — it just marks the access
/// synchronized for ThreadSanitizer.
pub inline fn isDetached(self: *const ArrayBufferData) bool {
return @constCast(self).detached_flag.load(.monotonic);
}
pub inline fn setDetached(self: *ArrayBufferData, v: bool) void {
self.detached_flag.store(v, .monotonic);
}
pub fn lockBuffer(self: *const ArrayBufferData) void {
var spins: usize = 0;
const mutex = &@constCast(self).lock;
while (!mutex.tryLock()) : (spins += 1) {
if ((spins & 0xff) == 0) {
std.Thread.yield() catch {};
} else {
std.atomic.spinLoopHint();
}
}
}
pub fn unlockBuffer(self: *const ArrayBufferData) void {
@constCast(self).lock.unlock();
}
/// Whether a bulk-byte op (a `copyWithin` memmove, etc.) must hold
/// `lockBuffer` across pointer resolution through `bytes()` and the copy.
///
/// A non-shared buffer's `resize` swaps `local_data` and then FREES the old
/// backing (interpreter `arrayBufferResizeFn`), so under no-GIL a peer
/// resizing mid-copy can pull the base out from under an in-flight memmove —
/// a use-after-free / torn copy. Locking serializes against the swap+free
/// exactly as `taRead`/`taWrite` do. Shared buffers never take it (storage is
/// page-reserved to the max and never freed on grow; the per-wrapper mutex
/// gives no cross-agent exclusion regardless), and the gate stays off in the
/// default single-threaded engine, where there is no concurrent resizer.
pub inline fn needsElementLock(self: *const ArrayBufferData) bool {
return !self.is_shared and Object.element_locks_enabled.load(.monotonic);
}
};
/// Read typed-array element `i` (within bounds, buffer attached) as a Number.
pub fn taRead(ta: *const TypedArrayData, i: usize) Value {
const buf = ta.buffer.array_buffer.?;
buf.lockBuffer();
defer buf.unlockBuffer();
const bytes = buf.bytes();
const off = ta.byte_offset + i * ta.kind.byteSize();
// A resizable buffer may have shrunk below the view's cached length; reading
// out of bounds returns 0 rather than a panic.
if (off + ta.kind.byteSize() > bytes.len) return Value.num(0);
const n: f64 = switch (ta.kind) {
.i8 => @floatFromInt(@as(i8, @bitCast(bytes[off]))),
.u8, .u8c => @floatFromInt(bytes[off]),
.i16 => @floatFromInt(std.mem.readInt(i16, bytes[off..][0..2], .little)),
.u16 => @floatFromInt(std.mem.readInt(u16, bytes[off..][0..2], .little)),
.i32 => @floatFromInt(std.mem.readInt(i32, bytes[off..][0..4], .little)),
.u32 => @floatFromInt(std.mem.readInt(u32, bytes[off..][0..4], .little)),
.f16 => @floatCast(@as(f16, @bitCast(std.mem.readInt(u16, bytes[off..][0..2], .little)))),
.f32 => @floatCast(@as(f32, @bitCast(std.mem.readInt(u32, bytes[off..][0..4], .little)))),
.f64 => @bitCast(std.mem.readInt(u64, bytes[off..][0..8], .little)),
// A BigInt element read as a Number is lossy, but keeps the Number-typed
// method paths crash-free; the interpreter's index get uses `taReadBig`.
.i64 => @floatFromInt(std.mem.readInt(i64, bytes[off..][0..8], .little)),
.u64 => @floatFromInt(std.mem.readInt(u64, bytes[off..][0..8], .little)),
};
return Value.num(n);
}
/// Read a BigInt typed-array element `i` as an `i128` (the raw 64-bit value,
/// sign-extended for BigInt64Array).
pub fn taReadBig(ta: *const TypedArrayData, i: usize) i128 {
const buf = ta.buffer.array_buffer.?;
buf.lockBuffer();
defer buf.unlockBuffer();
const bytes = buf.bytes();
const off = ta.byte_offset + i * ta.kind.byteSize();
if (off + 8 > bytes.len) return 0;
return switch (ta.kind) {
.i64 => std.mem.readInt(i64, bytes[off..][0..8], .little),
.u64 => @as(i128, std.mem.readInt(u64, bytes[off..][0..8], .little)),
else => 0,
};
}
/// Write a BigInt typed-array element `i` from an `i128` (the low 64 bits).
pub fn taWriteBig(ta: *const TypedArrayData, i: usize, val: i128) void {
const buf = ta.buffer.array_buffer.?;
buf.lockBuffer();
defer buf.unlockBuffer();
const bytes = buf.bytes();
const off = ta.byte_offset + i * ta.kind.byteSize();
if (off + 8 > bytes.len) return;
const low: u64 = @truncate(@as(u128, @bitCast(val)));
std.mem.writeInt(u64, bytes[off..][0..8], low, .little);
}
/// ToInt of `num` truncated to a wrapping integer width (NaN/±Inf → 0).
fn taToInt(comptime T: type, num: f64) T {
if (std.math.isNan(num) or std.math.isInf(num)) return 0;
const bits = @bitSizeOf(T);
const two_pow: f64 = std.math.pow(f64, 2, @floatFromInt(bits));
var m = @mod(@trunc(num), two_pow); // wrap into [0, 2^bits)
if (m < 0) m += two_pow;
// For a signed target, map the upper half [2^(bits-1), 2^bits) to negatives.
if (@typeInfo(T).int.signedness == .signed and m >= two_pow / 2) m -= two_pow;
return @intFromFloat(m);
}
/// Write Number `num` into typed-array element `i`, coercing to the element type
/// (integer wrap, Uint8Clamped rounding/clamping, float narrowing).
pub fn taWrite(ta: *const TypedArrayData, i: usize, num: f64) void {
const buf = ta.buffer.array_buffer.?;
buf.lockBuffer();
defer buf.unlockBuffer();
const bytes = buf.bytes();
const off = ta.byte_offset + i * ta.kind.byteSize();
if (off + ta.kind.byteSize() > bytes.len) return; // shrunk resizable buffer
switch (ta.kind) {
.i8 => bytes[off] = @bitCast(taToInt(i8, num)),
.u8 => bytes[off] = taToInt(u8, num),
.u8c => {
// ToUint8Clamp: NaN→0, round-half-to-even, clamp [0,255].
if (std.math.isNan(num) or num <= 0) {
bytes[off] = 0;
} else if (num >= 255) {
bytes[off] = 255;
} else {
const f = @floor(num);
const rounded: f64 = if (num - f == 0.5)
(if (@mod(f, 2) == 0) f else f + 1)
else
@round(num);
bytes[off] = @intFromFloat(rounded);
}
},
.i16 => std.mem.writeInt(i16, bytes[off..][0..2], taToInt(i16, num), .little),
.u16 => std.mem.writeInt(u16, bytes[off..][0..2], taToInt(u16, num), .little),
.i32 => std.mem.writeInt(i32, bytes[off..][0..4], taToInt(i32, num), .little),
.u32 => std.mem.writeInt(u32, bytes[off..][0..4], taToInt(u32, num), .little),
.f16 => std.mem.writeInt(u16, bytes[off..][0..2], @bitCast(@as(f16, @floatCast(num))), .little),
.f32 => std.mem.writeInt(u32, bytes[off..][0..4], @bitCast(@as(f32, @floatCast(num))), .little),
.f64 => std.mem.writeInt(u64, bytes[off..][0..8], @bitCast(num), .little),
// A Number written to a BigInt array is only reached via the lossy
// Number-typed method paths; the index set uses `taWriteBig`.
.i64 => std.mem.writeInt(i64, bytes[off..][0..8], taToInt(i64, num), .little),
.u64 => std.mem.writeInt(u64, bytes[off..][0..8], taToInt(u64, num), .little),
}
}
// ---- Atomic element access (the `Atomics.*` fast paths) -------------------
//
// Each helper performs ONE SeqCst hardware atomic on the element's bytes, so
// racing agents over a SharedArrayBuffer can never tear an element or lose an
// update. Raw bits travel as zero-extended u64 (preserving full 64-bit
// precision for BigInt views, which the f64 route cannot). Alignment is
// guaranteed: element offsets are spec-forced multiples of the element size,
// and buffer allocations are at least 8-byte aligned (shared slabs are
// page-aligned; `makeArrayBuffer` aligns its arena bytes). The byte order
// matches the non-atomic paths' explicit little-endian on every supported
// target. Out-of-bounds (a shrunk resizable buffer) mirrors `taRead`/`taWrite`:
// loads read 0, stores no-op.
/// The element's byte address, or null when the view is out of bounds.
fn taElemPtr(ta: *const TypedArrayData, i: usize) ?[*]u8 {
const b = ta.buffer.array_buffer.?.bytes();
const off = ta.byte_offset + i * ta.kind.byteSize();
if (off + ta.kind.byteSize() > b.len) return null;
return b.ptr + off;
}
/// Whether an atomic op on `buf` must hold `lockBuffer` around the WHOLE op
/// (element-pointer resolution through `bytes()` plus the hardware atomic).
///
/// A non-shared ArrayBuffer's `resize` swaps `local_data` and then FREES the old
/// backing (interpreter `arrayBufferResizeFn`), so a peer thread resizing under
/// no-GIL can otherwise pull the base out from under an atomic that already
/// resolved its element pointer — a use-after-free that reads a stale/foreign
/// value. Serializing against the swap+free (exactly as `taRead`/`taWrite` do)
/// closes the window: an atomic holding the lock blocks the resize's swap, and
/// once the resize has swapped every later atomic resolves the fresh base.
///
/// Shared buffers never take this lock: their storage is page-reserved to the
/// maximum and never freed on grow, and each agent holds a distinct wrapper (so
/// this per-wrapper mutex gives no cross-agent exclusion regardless) — the
/// hardware atomic alone orders concurrent agents. The gate also stays off in
/// the default single-threaded engine, where there is no concurrent resizer.
inline fn atomicNeedsLock(buf: *const ArrayBufferData) bool {
return buf.needsElementLock();
}
/// The element address as a typed pointer (alignment guaranteed, see above).
fn elemAs(comptime T: type, p: [*]u8) *T {
return @ptrCast(@alignCast(p));
}
/// Sign-aware conversion of raw element bits to a Number (integer kinds only;
/// BigInt/float kinds never take this path).
pub fn taRawToF64(kind: TAKind, raw: u64) f64 {
return switch (kind) {
.i8 => @floatFromInt(@as(i8, @bitCast(@as(u8, @truncate(raw))))),
.u8, .u8c => @floatFromInt(@as(u8, @truncate(raw))),
.i16 => @floatFromInt(@as(i16, @bitCast(@as(u16, @truncate(raw))))),
.u16 => @floatFromInt(@as(u16, @truncate(raw))),
.i32 => @floatFromInt(@as(i32, @bitCast(@as(u32, @truncate(raw))))),
.u32 => @floatFromInt(@as(u32, @truncate(raw))),
else => 0,
};
}
/// Wrap an integer Number into the element type, as zero-extended raw bits
/// (the atomic-path counterpart of `taToInt` + write).
pub fn taNumToRaw(kind: TAKind, num: f64) u64 {
return switch (kind) {
.i8 => @as(u8, @bitCast(taToInt(i8, num))),
.u8, .u8c => taToInt(u8, num),
.i16 => @as(u16, @bitCast(taToInt(i16, num))),
.u16 => taToInt(u16, num),
.i32 => @as(u32, @bitCast(taToInt(i32, num))),
.u32 => taToInt(u32, num),
.i64 => @as(u64, @bitCast(taToInt(i64, num))),
.u64 => taToInt(u64, num),
else => 0,
};
}
pub fn taAtomicLoadRaw(ta: *const TypedArrayData, i: usize) u64 {
const buf = ta.buffer.array_buffer.?;
const locked = atomicNeedsLock(buf);
if (locked) buf.lockBuffer();
defer {
if (locked) buf.unlockBuffer();
}
const p = taElemPtr(ta, i) orelse return 0;
return switch (ta.kind.byteSize()) {
1 => @atomicLoad(u8, elemAs(u8, p), .seq_cst),
2 => @atomicLoad(u16, elemAs(u16, p), .seq_cst),
4 => @atomicLoad(u32, elemAs(u32, p), .seq_cst),
else => @atomicLoad(u64, elemAs(u64, p), .seq_cst),
};
}
pub fn taAtomicStoreRaw(ta: *const TypedArrayData, i: usize, raw: u64) void {
const buf = ta.buffer.array_buffer.?;
const locked = atomicNeedsLock(buf);
if (locked) buf.lockBuffer();
defer {
if (locked) buf.unlockBuffer();
}
const p = taElemPtr(ta, i) orelse return;
switch (ta.kind.byteSize()) {
1 => @atomicStore(u8, elemAs(u8, p), @truncate(raw), .seq_cst),
2 => @atomicStore(u16, elemAs(u16, p), @truncate(raw), .seq_cst),
4 => @atomicStore(u32, elemAs(u32, p), @truncate(raw), .seq_cst),
else => @atomicStore(u64, elemAs(u64, p), raw, .seq_cst),
}
}
/// One atomic read-modify-write; returns the previous raw bits. Integer ops
/// wrap modulo the element width, matching the spec's modular arithmetic.
pub fn taAtomicRmwRaw(comptime op: std.builtin.AtomicRmwOp, ta: *const TypedArrayData, i: usize, raw: u64) u64 {
const buf = ta.buffer.array_buffer.?;
const locked = atomicNeedsLock(buf);
if (locked) buf.lockBuffer();
defer {
if (locked) buf.unlockBuffer();
}
const p = taElemPtr(ta, i) orelse return 0;
return switch (ta.kind.byteSize()) {
1 => @atomicRmw(u8, elemAs(u8, p), op, @truncate(raw), .seq_cst),
2 => @atomicRmw(u16, elemAs(u16, p), op, @truncate(raw), .seq_cst),
4 => @atomicRmw(u32, elemAs(u32, p), op, @truncate(raw), .seq_cst),
else => @atomicRmw(u64, elemAs(u64, p), op, raw, .seq_cst),
};
}
/// One atomic compare-exchange; returns the previous raw bits (== `expected`
/// when the swap happened, per `Atomics.compareExchange` semantics).
pub fn taAtomicCasRaw(ta: *const TypedArrayData, i: usize, expected: u64, replacement: u64) u64 {
const buf = ta.buffer.array_buffer.?;
const locked = atomicNeedsLock(buf);
if (locked) buf.lockBuffer();
defer {
if (locked) buf.unlockBuffer();
}
const p = taElemPtr(ta, i) orelse return 0;
switch (ta.kind.byteSize()) {
1 => {
const e: u8 = @truncate(expected);
return @cmpxchgStrong(u8, elemAs(u8, p), e, @truncate(replacement), .seq_cst, .seq_cst) orelse e;
},
2 => {
const e: u16 = @truncate(expected);
return @cmpxchgStrong(u16, elemAs(u16, p), e, @truncate(replacement), .seq_cst, .seq_cst) orelse e;
},
4 => {
const e: u32 = @truncate(expected);
return @cmpxchgStrong(u32, elemAs(u32, p), e, @truncate(replacement), .seq_cst, .seq_cst) orelse e;
},
else => return @cmpxchgStrong(u64, elemAs(u64, p), expected, replacement, .seq_cst, .seq_cst) orelse expected,
}
}
/// A typed-array view: `length` elements of `kind`, starting at `byte_offset`
/// into `buffer`'s bytes.
pub const TypedArrayData = struct {
buffer: *Object,
byte_offset: usize,
length: usize,
kind: TAKind,
/// A length-tracking view (created without an explicit length on a resizable
/// ArrayBuffer): its length follows the buffer's current size rather than the
/// cached `length`.
track_length: bool = false,
/// The view's current element length, or null if it is out of bounds (the
/// backing resizable buffer shrank below it) or detached. A length-tracking
/// view recomputes from the live buffer size; a fixed view keeps `length`
/// unless its range no longer fits.
pub fn currentLength(self: *const TypedArrayData) ?usize {
const buf = self.buffer.array_buffer orelse return null;
if (buf.isDetached()) return null;
const esz = self.kind.byteSize();
if (self.byte_offset > buf.bytes().len) return null;
if (self.track_length) return (buf.bytes().len - self.byte_offset) / esz;
if (self.byte_offset + self.length * esz > buf.bytes().len) return null;
return self.length;
}
};
/// A `DataView`: a typed read/write window of `byte_length` bytes starting at
/// `byte_offset` into `buffer`'s bytes, with per-access endianness.
pub const DataViewData = struct {
buffer: *Object,
byte_offset: usize,
byte_length: usize,
/// A length-tracking DataView (no explicit byteLength on a resizable buffer).
track_length: bool = false,
/// The view's current byte length, or null if it is out of bounds (the
/// resizable buffer shrank below it) or detached.
pub fn currentByteLength(self: *const DataViewData) ?usize {
const buf = self.buffer.array_buffer orelse return null;
if (buf.isDetached()) return null;
if (self.byte_offset > buf.bytes().len) return null;
if (self.track_length) return buf.bytes().len - self.byte_offset;
if (self.byte_offset + self.byte_length > buf.bytes().len) return null;
return self.byte_length;
}
};
/// Internal slots for the `Temporal.*` types. One flat record covers every
/// kind (the `kind` tag selects which fields are meaningful), keeping the
/// `Object` footprint to a single pointer.
pub const TemporalData = struct {
pub const Kind = enum { instant, plain_date, plain_time, plain_date_time, plain_year_month, plain_month_day, duration, zoned_date_time };
kind: Kind,
// ISO date components (PlainDate/DateTime/YearMonth/MonthDay/ZonedDateTime).
year: i32 = 0,
month: u8 = 1,
day: u8 = 1,
// ISO time components (PlainTime/DateTime/ZonedDateTime).
hour: u8 = 0,
minute: u8 = 0,
second: u8 = 0,
millisecond: u16 = 0,
microsecond: u16 = 0,
nanosecond: u16 = 0,
// Instant / ZonedDateTime: nanoseconds since the Unix epoch.
epoch_ns: i128 = 0,
// ZonedDateTime time zone: its identifier and (for fixed-offset zones) the
// UTC offset in nanoseconds. IANA-named zones without DST data use offset 0.
tz_name: []const u8 = "UTC",
tz_offset_ns: i64 = 0,
// Duration components (signed, may be fractional only for the smallest set).
dur: [10]f64 = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // years,months,weeks,days,hours,minutes,seconds,ms,us,ns
// The calendar identifier for a date-bearing value. "iso8601" is the default
// and the only one whose date arithmetic is the engine's native (proleptic
// Gregorian) math; other ids (e.g. "gregory") share that math but differ in
// era/eraYear reflection and the toString `[u-ca=…]` annotation.
calendar: []const u8 = "iso8601",
};
/// State for a lazy Iterator Helper (the object returned by `map`/`filter`/…).
pub const IterHelper = struct {
pub const Kind = enum(u8) { map, filter, take, drop, flat_map, wrap, concat, zip, zip_keyed };
lock: std.atomic.Mutex = .unlocked,
src: Value, // the underlying iterator (its `.next()` is pulled)
next_method: Value = Value.undef(), // captured once (GetIteratorDirect); called per step
kind: Kind,
func: Value = Value.undef(), // mapper/filterer/flatMapper; or zip_keyed's key array
counter: f64 = 0, // index argument to the callback
limit: f64 = 0, // take/drop count; or zip mode (0 shortest, 1 longest, 2 strict)
inner: ?Value = null, // flat_map's current inner iterator; or zip's per-source done-flag array
inner_next: Value = Value.undef(), // flat_map inner iterator's captured `next`
padding: Value = Value.undef(), // zip(longest)'s per-source padding values
done: bool = false,
started: bool = false, // drop: the initial skip has run
is_async: bool = false, // AsyncIterator helper: `next` returns a promise
running: bool = false, // GeneratorValidate: a re-entrant next() is a TypeError
pub fn lockState(self: *IterHelper) void {
var spins: usize = 0;
while (!self.lock.tryLock()) : (spins += 1) {
if ((spins & 0xff) == 0) std.Thread.yield() catch {} else std.atomic.spinLoopHint();
}
gc_runtime.enterTraceSensitiveLock();
}
pub fn unlockState(self: *IterHelper) void {
gc_runtime.leaveTraceSensitiveLock();
self.lock.unlock();
}
};
pub const WeakCollectionEntry = struct {
key: ?*anyopaque = null,
value: Value = Value.undef(),
};
pub const FinalizationRecord = struct {
target: ?*anyopaque = null,
held: Value = Value.undef(),
token: ?*anyopaque = null,
ready: bool = false,
};
pub const ObjectBackingFlags = packed struct {
slots: bool = false,
elements: bool = false,
accessors: bool = false,
key_order: bool = false,
attrs: bool = false,
holes: bool = false,
weak_entries: bool = false,
finalization_records: bool = false,
typed_array: bool = false,
data_view: bool = false,
temporal: bool = false,
arg_map_names: bool = false,
arg_map_severed: bool = false,
};
pub const ObjectPrivateDataTag = enum(u8) {
none,
host,
jsthread_thread,
jsthread_lock,
jsthread_condition,
jsthread_thread_local,
jsthread_unlock_token,
jsthread_release_state,
};
pub const PreparedInlineLiteralShape = struct {
final_shape: *Shape,
slot_count: u8,
};
/// A JavaScript object. v1 keeps this deliberately small: a string-keyed
/// property map, an optional dense array part, and three flavors of callable:
/// a JS-defined function (`js_func`, type-erased `*Function` to avoid an
/// import cycle with the interpreter), a Zig-native builtin (`native`), and a
/// C-ABI host callback (`callback`). In arena mode, backing stores still share
/// the owning Context's arena; in GC mode, migrated backing stores record their
/// allocator so object finalization can reclaim them before Context teardown.
pub const Object = struct {
pub const inline_slot_capacity: usize = 4;
/// Non-null when this object's lazily-allocated backing stores have moved
/// out of the arena for GC-mode reclamation. Individual flags below record
/// which stores actually use this allocator, so mixed legacy/GC state is
/// finalized accurately while migration is incremental.
backing_allocator: ?std.mem.Allocator = null,
backing_flags: ObjectBackingFlags = .{},
/// `backingFor`/`(de)activateBacking` touch `backing_flags`/`backing_allocator`
/// while the caller holds *whichever* structure lock matches the field
/// (elements_lock for elements, property_lock for slots/attrs/accessors), so
/// those don't mutually exclude on the shared packed `backing_flags` byte —
/// under `parallel_js` an elements-grow on one thread races an attrs-grow on
/// another. This dedicated lock serializes backing activation across them.
/// Gated on `element_locks_enabled` so the default engine pays nothing.
backing_lock: std.atomic.Mutex = .unlocked,
/// Coarse synchronization for ordinary named-property metadata: shape
/// publication, slots, accessors, attributes, and key order. The Layer-B GIL
/// still serializes JS execution today; this lock is the Layer-C object-side
/// convergence point for paths that already flow through Object helpers.
property_lock: std.atomic.Mutex = .unlocked,
/// Coarse synchronization for the dense/indexed element store. Arrays,
/// Map/Set data, iterator cursor cells, and many small engine tuples use
/// `elements`; Layer-C work must move each direct access behind this lock or
/// a narrower equivalent before the GIL can go away.
elements_lock: std.atomic.Mutex = .unlocked,
/// Named properties live behind a shared `Shape` (null = no own properties)
/// plus a flat per-object `slots` array indexed by the shape. See shape.zig.
shape: ?*Shape = null,
slots: std.ArrayListUnmanaged(Value) = .empty,
/// Most ordinary objects never grow beyond a handful of named properties.
/// Keep those values in the GC cell itself so object literals avoid a
/// second allocator round trip; `slots.items` points here until growth.
inline_slots: [inline_slot_capacity]Value = undefined,
/// Prototype link ([[Prototype]]): property lookup walks this chain. An
/// instance's proto is its constructor's `.prototype`; a class's `.prototype`
/// protos to its superclass's `.prototype`.
proto: ?*Object = null,
/// Callable built-ins may leave `proto` null and inherit %Function.prototype%
/// implicitly. Once [[SetPrototypeOf]] explicitly sets null, that null must be
/// observable through [[GetPrototypeOf]] instead of falling back again.
proto_explicit_null: bool = false,
/// Accessor (get/set) properties, lazily allocated. Checked before the data
/// slot at each level of the prototype walk. The POINTER is accessed atomically
/// (`.load`/`.store(.monotonic)`, a plain mov — zero cost): under `parallel_js`
/// a peer's `setAccessor` publishes the map (null -> non-null) under
/// `lockProperties` while other threads read the many unlocked `accessors ==
/// null` fast-path guards, which on a plain field ThreadSanitizer flags as a
/// race. The map CONTENTS are only ever grown under `lockProperties`, so the
/// pointer is the only unsynchronized access.
accessors: std.atomic.Value(?*std.StringHashMapUnmanaged(Accessor)) = .init(null),
/// The set of private names this object was *branded* with at construction
/// (its class's private fields/methods/accessors). PrivateGet/PrivateSet check
/// brand membership here rather than via prototype inheritance, so an object
/// missing the brand (a non-instance, a derived `this` before `super()`
/// returns, an instance of a different evaluation of the class) is rejected.
/// Lazily allocated. Keyed by the (evaluation-unique) private storage name.
private_brands: ?*std.StringHashMapUnmanaged(void) = null,
/// All own named keys (data AND accessor) in creation order — allocated
/// lazily only when an accessor is first added, since data slots otherwise
/// keep insertion order via the shape chain. `ownKeys` uses this to interleave
/// data and accessor keys by creation order (the shape can't, as accessors
/// live in a side map). May hold stale/duplicate entries (deleted or re-added
/// keys) — readers re-check membership and keep each key's LAST occurrence.
/// The POINTER is accessed atomically (`.load`/`.store(.monotonic)`, a plain
/// mov): under `parallel_js` a peer publishes it null->non-null under
/// `lockProperties` (`ensureKeyOrderUnlocked`, first accessor) while other
/// threads read the unlocked `key_order == null` fast-path guards — a data race
/// on a plain field. The list CONTENTS are only mutated under `lockProperties`,
/// so the pointer is the only unsynchronized access (mirrors `accessors`).
key_order: std.atomic.Value(?*std.ArrayListUnmanaged([]const u8)) = .init(null),
elements: std.ArrayListUnmanaged(Value) = .empty,
/// Per-property attribute overrides, lazily allocated. Absent name = the
/// all-true default (a plain-assignment property). See `PropAttr`.
attrs: ?*std.StringHashMapUnmanaged(PropAttr) = null,
/// When false (set by `Object.preventExtensions`/`seal`/`freeze`), new own
/// properties can't be added. Accessed atomically via `isExtensible`/
/// `setExtensible`: under `parallel_js` a peer can seal/freeze a shared object
/// while another thread reads extensibility to add a property, which on a
/// plain bool is a data race. `.monotonic` is a plain byte load/store that
/// just marks the access synchronized for ThreadSanitizer (the freeze-vs-add
/// outcome is racy-by-spec regardless of who wins).
extensible_flag: std.atomic.Value(bool) = .init(true),
/// A Symbol (a tagged object so identity `===` and storage reuse the object
/// machinery; `typeof` reports "symbol"). `sym_key` is its unique property-key
/// encoding (used when a symbol is an object property key).
is_symbol: bool = false,
sym_key: []const u8 = "",
/// A BigInt primitive (`typeof` reports "bigint"; treated as a primitive in
/// equality/arithmetic). Small values use the `i128` fast path; oversized
/// literals/decimal strings keep a canonical decimal identity in
/// `bigint_text` until full arbitrary-precision arithmetic lands.
is_bigint: bool = false,
bigint: i128 = 0,
bigint_text: ?[]const u8 = null,
/// An `[[IsHTMLDDA]]` exotic object (e.g. `document.all`): `typeof` reports
/// "undefined", ToBoolean is false, and it is loosely-equal to null/undefined.
is_htmldda: bool = false,
/// A `JSON.rawJSON(...)` result (carries `[[IsRawJSON]]`): a frozen null-proto
/// object with an own "rawJSON" string property emitted verbatim by stringify.
is_raw_json: bool = false,
/// A Symbol's `[[Description]]`: `null` = no description (reads as
/// `undefined`), else the string. Held in this dedicated slot rather than an
/// own `description` property so it stays invisible to reflection
/// (`Object.getOwnPropertyDescriptor(sym, "description")` is undefined) —
/// `Symbol.prototype.description` is a prototype accessor instead.
sym_desc: ?[]const u8 = null,
/// A `Date` instance — its [[DateValue]] (ms since the Unix epoch, or NaN
/// for an invalid date) is the internal-slot field `date_ms`, invisible to
/// reflection/enumeration; methods are dispatched in `dateMethod`.
is_date: bool = false,
date_ms: f64 = 0,
is_array: bool = false,
// (atomic accessors for `date_ms` are defined as methods below)
/// Test-shell `$vm.ensureArrayStorage(array)` marker. zig-js uses one
/// generic array element backing rather than JSC's multiple butterfly
/// regimes; this bit records the requested ArrayStorage witness mode so
/// `$vm.indexingMode` can report the effective stress precondition without
/// changing ordinary ECMAScript array semantics.
forced_array_storage: bool = false,
/// Conservative guard for fast indexed writes: once an object has ever had an
/// array-index data/accessor property, prototype-chain writes must use the
/// ordinary path so inherited setters/non-writable data stay observable.
/// Atomic: read on the indexed-write fast path while a peer can set it when a
/// proto-chain object gains an array-index property (no-GIL). One byte → a
/// plain load/store. Mirrors `indexed_own_seen`.
has_indexed_property: std.atomic.Value(bool) = .init(false),
/// Conservative cross-thread guard for prototype-chain indexed writes. Unlike
/// `has_indexed_property`, this also records dense element creation and is
/// used only when another object is consulting this object as a prototype.
indexed_own_seen: std.atomic.Value(bool) = .init(false),
/// For arrays, a *logical* length floor used when it exceeds the physically
/// stored `elements` — so `new Array(4294967295)` / `arr.length = big` track a
/// length without materializing (and OOM-ing on) that many holes. The array's
/// observable length is `max(elements.items.len, array_len)`.
array_len: usize = 0,
callback: ?HostCallback = null,
/// Owning Context for C-API host callbacks. Kept separate from
/// `private_data`, which belongs to `JSObjectMake(..., data)` embedders.
callback_context: ?*anyopaque = null,
native: ?NativeFn = null,
/// For a `native` function, whether it implements [[Construct]] — i.e. is
/// `new`-able. Most built-ins are *not* constructors (methods, `Math.*`,
/// `parseInt`, `Symbol`, …); only the handful that the spec defines as
/// constructors (`Array`, `Object`, `Map`, `RegExp`, …) set this, so
/// `new Object.keys()` / `new Symbol()` throw a TypeError as required.
native_ctor: bool = false,
/// `*Interpreter.Function`, type-erased to break the value↔interpreter
/// import cycle. The interpreter casts it back when calling.
js_func: ?*anyopaque = null,
/// `*vm.Generator`, type-erased (same cycle break as `js_func`). Non-null
/// marks a generator *object* — the iterator returned by calling a
/// `function*`; its `.next()`/`.return()`/`.throw()` drive the suspendable VM.
gen: ?*anyopaque = null,
/// `*Interpreter.BoundFn`, type-erased. Non-null marks a bound function
/// (`fn.bind(this, ...args)`): calling it invokes the target with the bound
/// `this` and the bound args prepended.
bound: ?*anyopaque = null,
/// Opaque `data` pointer carried for `JSObjectMake(ctx, class, data)` and
/// surfaced to host callbacks via private-data accessors later.
private_data: ?*anyopaque = null,
/// Internal owner tag for engine-managed `private_data` roots. Untagged
/// host data stays opaque; tracers must not inspect it speculatively.
private_data_tag: ObjectPrivateDataTag = .none,
/// Promise resolving functions carry one shared, spec-observable
/// [[AlreadyResolved]] record per resolve/reject pair. The resolve function
/// object owns that tiny record directly; the reject function points at the
/// resolve object through `private_data`.
promise_resolving_already: bool = false,
/// `Thread.restrict(obj)`: the only OS thread allowed to touch this
/// object through the enforced internal-method funnels (0 =
/// unrestricted). Foreign access throws `ConcurrentAccessError`. Atomic: the
/// claim is a CAS (two `Thread.restrict` calls on the same object race), and
/// the enforcement check reads it from any thread. Thread ids are never 0.
restricted_to: std.atomic.Value(u64) = .init(0),
/// True for `Error`-family instances; drives `toString` and `instanceof`.
is_error: bool = false,
/// True for `RegExp` instances (carries `source`/`flags` properties; matching
/// is backed by zig-regex).
is_regex: bool = false,
/// True for function `arguments` objects; they are array-like internally but
/// carry the Arguments brand for Object.prototype.toString.
is_arguments: bool = false,
/// A mapped (sloppy-mode, simple-parameter) arguments object's
/// `[[ParameterMap]]`: the call's environment record (type-erased
/// `*Environment`) and the parameter name each index maps to (`""` = not
/// initially mapped). A mapped index reads/writes the parameter binding;
/// defining it as an accessor or non-writable, or deleting it, atomically
/// severs the mapping in `arg_map_severed`. The names slice is immutable once
/// the arguments object is published so no-GIL readers cannot tear a slice
/// pair while another thread severs an index.
arg_map_env: ?*anyopaque = null,
arg_map_names: [][]const u8 = &.{},
arg_map_severed: []std.atomic.Value(bool) = &.{},
/// `Map`/`Set` instances. A Map keeps `[key,value]` pair-arrays in
/// `elements`; a Set keeps values directly. `size` is a maintained property.
is_map: bool = false,
is_set: bool = false,
/// Internal tombstone for SetData slots deleted during observable iteration.
/// User code can never obtain one; Set/iterator helpers skip these slots.
is_set_deleted: bool = false,
/// A WeakMap/WeakSet reuses the `is_map`/`is_set` storage but carries this
/// flag so the brand checks can tell a Map from a WeakMap (and Set/WeakSet).
is_weak: bool = false,
/// WeakMap/WeakSet entries. Keys are weak GC edges; for WeakMap, `value`
/// becomes live iff `key` is live during the ephemeron fixed-point pass.
weak_entries: std.ArrayListUnmanaged(WeakCollectionEntry) = .empty,
/// Pointer identity -> `weak_entries` index. This keeps WeakMap/WeakSet
/// operations O(1) while the entry list remains the GC-visible ephemeron
/// storage. Stale/missing entries are tolerated and repaired by mutators.
weak_index: std.AutoHashMapUnmanaged(usize, usize) = .empty,
/// For error instances, the error class name (e.g. "TypeError"); for a
/// builtin error *constructor* object, see `error_ctor`.
error_name: []const u8 = "",
/// Non-null marks this object as a builtin error constructor; the value is
/// the class name it produces ("Error", "TypeError", ...). Callable both
/// plainly (`TypeError("x")`) and via `new`.
error_ctor: ?[]const u8 = null,
/// For objects created by `new F()`, the constructor function's object —
/// used by `instanceof` to walk the (flat, v1) construction link.
ctor_ref: ?*Object = null,
/// `*Interpreter.Promise`, type-erased (cycle break like `js_func`/`gen`).
/// Non-null marks a Promise object — its `then`/`catch`/`finally` are
/// dispatched specially and it carries pending reactions + settled state.
promise: ?*anyopaque = null,
/// A primitive-wrapper object's boxed [[NumberData]]/[[StringData]]/
/// [[BooleanData]] — set by `new Number(x)` / `new String(x)` / `new
/// Boolean(x)`. Non-null marks the object as a wrapper: `typeof` is still
/// "object", but `valueOf`/ToPrimitive unwrap it and `Object.prototype.
/// toString` reports `[object Number|String|Boolean]`.
prim: ?Value = null,
/// `Proxy` exotic object: the wrapped target and the handler object. Both
/// non-null marks a proxy — property operations route through the handler's
/// traps (falling back to the target). A revoked proxy has both set to a
/// sentinel `revoked` flag.
proxy_target: ?*Object = null,
proxy_handler: ?*Object = null,
proxy_revoked: bool = false,
/// Proxies keep their [[Call]] exotic behavior even after revocation. Once
/// revoked, the target slot is gone, so cache the callable bit at creation.
proxy_callable: bool = false,
/// Module Namespace exotic object: points to an `interpreter.ModuleNs`
/// (its sorted export names + live bindings). When set, this object is a
/// `[[Module]]` namespace and the engine intercepts its essential internal
/// methods (live [[Get]], [[HasProperty]], sorted [[OwnPropertyKeys]],
/// frozen/non-extensible, throwing [[Set]]/[[Delete]]/[[DefineOwnProperty]]).
module_ns: ?*anyopaque = null,
/// For arrays: the set of dense-index *holes* (gaps that read as absent — a
/// deleted element, an elision in `[1,,3]`, or a gap created by a sparse
/// assignment). The `elements` slot for a hole still exists (holds undefined),
/// but `HasProperty`/iteration treat the index as not present. Lazily allocated.
holes: ?*std.AutoHashMapUnmanaged(usize, void) = null,
/// `ArrayBuffer` backing store (non-null marks an ArrayBuffer object).
array_buffer: ?*ArrayBufferData = null,
/// Typed-array view (non-null marks a `Int8Array`/…/`Float64Array`): an
/// integer-indexed view over `buffer`'s bytes. Index get/set read/write the
/// underlying bytes coerced to/from the element type.
typed_array: ?*TypedArrayData = null,
/// `DataView` view (non-null marks a DataView): a typed read/write window
/// over `buffer`'s bytes with per-access endianness.
data_view: ?*DataViewData = null,
/// A RegExp's [[OriginalSource]] / [[OriginalFlags]] internal slots. Held off
/// the property map so `source`/`flags`/`global`/… resolve through the
/// RegExp.prototype accessor getters (not instance own data properties).
regex_source: []const u8 = "",
regex_flags: []const u8 = "",
/// Cached `*regex.Regex`, type-erased to keep value.zig independent from the
/// regex package. Invalidated when `RegExp.prototype.compile` changes slots.
regex_compiled: ?*anyopaque = null,
/// Marks a `WeakRef` instance. The target is a weak GC edge, so collection
/// may clear it while the WeakRef object itself remains branded.
is_weak_ref: bool = false,
weak_ref_target: ?*Object = null,
/// Marks a `FinalizationRegistry`. Dead targets make records ready for
/// automatic host cleanup delivery at quiescent collection points.
is_finalization_registry: bool = false,
finalization_callback: Value = Value.undef(),
finalization_records: std.ArrayListUnmanaged(FinalizationRecord) = .empty,
/// Lazy Iterator-Helper state (`map`/`filter`/`take`/`drop`/`flatMap`/wrap),
/// non-null on a helper iterator returned by those methods.
iter_helper: ?*IterHelper = null,
/// Marks a `ShadowRealm` instance (its child realm's Environment is in
/// `private_data`).
is_shadow_realm: bool = false,
/// `Temporal.*` internal slots (PlainDate/Time/DateTime/Duration/Instant/…),
/// non-null on a Temporal object.
temporal: ?*TemporalData = null,
fn lockBacking(self: *Object) bool {
if (!element_locks_enabled.load(.acquire)) return false;
var spins: usize = 0;
while (!self.backing_lock.tryLock()) : (spins += 1) {
if ((spins & 0xff) == 0) std.Thread.yield() catch {} else std.atomic.spinLoopHint();
}
object_profile.recordBackingLockAcquire(spins);
return true;
}
fn unlockBacking(self: *Object, held: bool) void {
if (held) self.backing_lock.unlock();
}
// Assumes `backing_lock` held (called only from `backingFor`).
fn activateBacking(self: *Object, comptime field: []const u8) ?std.mem.Allocator {
const state = gc_runtime.activeObjectBacking() orelse return null;
if (self.backing_allocator == null) self.backing_allocator = state.allocator;
if (!@field(self.backing_flags, field)) {
@field(self.backing_flags, field) = true;
// Atomic: parallel mutators (post-GIL) bump this shared accounting
// counter concurrently. Identical result single-threaded.
if (state.stores_live) |live| _ = @atomicRmw(usize, live, .Add, 1, .monotonic);
}
return self.backing_allocator.?;
}
fn deactivateBacking(self: *Object, comptime field: []const u8) void {
const backing_locked = self.lockBacking();
defer self.unlockBacking(backing_locked);
if (!@field(self.backing_flags, field)) return;
@field(self.backing_flags, field) = false;
if (gc_runtime.activeObjectBacking()) |state| {
if (state.stores_live) |live| {
_ = @atomicRmw(usize, live, .Sub, 1, .monotonic);
}
}
}
fn backingFor(self: *Object, fallback: std.mem.Allocator, comptime field: []const u8) std.mem.Allocator {
const backing_locked = self.lockBacking();
defer self.unlockBacking(backing_locked);
if (self.backing_allocator) |a| {
if (!@field(self.backing_flags, field)) {
if (self.activateBacking(field)) |active| return active;
}
return a;
}
return self.activateBacking(field) orelse fallback;
}
/// Whether new own properties may be added (see `extensible_flag`).
pub inline fn isExtensible(self: *const Object) bool {
return @constCast(self).extensible_flag.load(.monotonic);
}
/// Set by `Object.preventExtensions`/`seal`/`freeze`.
pub inline fn setExtensible(self: *Object, v: bool) void {
self.extensible_flag.store(v, .monotonic);
}
pub fn slotsAllocator(self: *Object, fallback: std.mem.Allocator) std.mem.Allocator {
return self.backingFor(fallback, "slots");
}
pub fn initInlineSlots(self: *Object) void {
self.slots = .{ .items = self.inline_slots[0..0], .capacity = self.inline_slots.len };
}