Repository navigation
Expand file tree
/
Copy pathlayout.js
More file actions
411 lines (363 loc) · 22.6 KB
/
Copy pathlayout.js
File metadata and controls
411 lines (363 loc) · 22.6 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
/**
* Shared runtime layout: heap, NaN-box carrier, pointer tags.
*
* Compiler-free — safe for `jz/interop` and tests without pulling the compiler.
*
* @module layout
*/
/** Bump-allocator cells in linear memory / globals. */
export const HEAP = { PTR_ADDR: 1020, START: 1024 }
/** NaN-box bit layout (i64 carrier). */
export const LAYOUT = {
TAG_SHIFT: 47,
TAG_MASK: 0xF,
AUX_SHIFT: 32,
AUX_MASK: 0x7FFF,
OFFSET_MASK: 0xFFFFFFFF,
NAN_PREFIX: 0x7FF8,
NAN_PREFIX_BITS: 0x7FF8000000000000n,
SSO_BIT: 0x4000,
SLICE_BIT: 0x2000,
SLICE_LEN_MASK: 0x1FFF,
MAX_SSO: 6, // ASCII chars an SSO string holds inline (6 × 7-bit lanes)
}
/** 4-bit tagged-pointer type codes. */
export const PTR = {
ATOM: 0,
ARRAY: 1,
BUFFER: 2,
TYPED: 3,
STRING: 4,
BIGINT: 5,
OBJECT: 6,
HASH: 7,
SET: 8,
MAP: 9,
CLOSURE: 10,
EXTERNAL: 11,
// Never a value: the named properties of an array whose names the program
// fixes, the slots of a record layout its aux names (module/collection/record.js).
RECORD: 12,
}
// Host field contracts use pointer-tag bits plus three scalar families.
// Detail is a typed-element aux or nested schema id; -1 means unrestricted.
export const FIELD = { NUMBER: 1 << 12, BOOL: 1 << 13, NULLISH: 1 << 14, ANY: (1 << 15) - 1 }
/** Reserved atom aux ids (PTR.ATOM). Aux 3 is private error-code transport
* (err-codes.js), materialized before a source catch observes it. */
export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
export const SYMBOL_MIN = 16 // first user Symbol atom id
/** Private absent-slot marker; no JavaScript value encodes this atom. */
export const TOMB_NAN = '0x7FF87FFFFFFFFFFF'
/** The aux of an ARRAY pointer whose elements are raw Numbers (src/compile/numeric-arrays.js):
* every element is an f64 of any bits, no element a box or a hole, so a walk
* reads none of them as a pointer. Every pointer to one array carries it. */
export const ARRAY_RAW = 1
/** Tags whose heap block can relocate on growth (ARRAY/HASH/SET/MAP) — leaving a
* forwarding header that `__ptr_offset` must follow. `(1 << tag) & FORWARDING_MASK`
* tests membership in one shl+and, replacing a 4-way tag-equality OR. */
export const FORWARDING_MASK = (1 << PTR.ARRAY) | (1 << PTR.HASH) | (1 << PTR.SET) | (1 << PTR.MAP)
// A hidden property has no enumeration rank. Keep the shared entry width intact.
export const HIDDEN_PROPERTY_SEQ = 0xffffffff
/** A resizable ArrayBuffer (`new ArrayBuffer(n, { maxByteLength })`) reserves its
* maximum: its header holds the current byte length at -8, the maximum at -4
* (the block's capacity) and this in the properties word at -16. A BUFFER keeps
* its properties in the global table, so the word is free; no properties word
* holds 2 (it is 0, a box, or either with its low bit set). A typed array or
* DataView over such a buffer is always a view, never the buffer's own header. */
export const RESIZABLE_MARK = 2
/** A view descriptor's word at 12 where the program makes resizable buffers:
* 1 for a view whose length tracks its buffer's (made without a length), else 0. */
export const VIEW_TRACKS = 1
// BigInt views of the NaN-box fields — the carrier is 64-bit, JS bit-ops are 32-bit.
const TAG_SHIFT = BigInt(LAYOUT.TAG_SHIFT), TAG_MASK = BigInt(LAYOUT.TAG_MASK)
const AUX_SHIFT = BigInt(LAYOUT.AUX_SHIFT), AUX_MASK = BigInt(LAYOUT.AUX_MASK)
const OFFSET_MASK = BigInt(LAYOUT.OFFSET_MASK)
/** Format an i64 BigInt as a zero-padded `0x…` hex literal for WAT/IR templates. */
// Formatted via 32-bit halves with an explicit logical shift, NOT
// bits.toString(16): under self-compile, BigInts are raw SIGNED i64 bits
// (kind-erased — see ir.js's SELF-COMPILE CONTRACT), so toString(16) of a
// bit-63-set value renders a signed "-8000…" fragment and the emitted
// `(i64.const 0x00-8000…)` kills the kernel's watr parse ("Bad int") — the
// nanPrefixMaskHex regression that silently broke every durable-log helper
// the kernel compiled. `>> 32n` sign-extends on raw bits; the & masks the
// extension off, so both host and kernel produce the same unsigned halves.
const HEX_DIGITS = '0123456789ABCDEF'
const _hx8 = n => {
let hex = ''
for (let shift = 28; shift >= 0; shift -= 4) hex += HEX_DIGITS[(n >>> shift) & 15]
return hex
}
export const i64Hex = bits => '0x' + _hx8(Number((bits >> 32n) & 0xFFFFFFFFn)) + _hx8(Number(bits & 0xFFFFFFFFn))
/** Pack (type, aux, offset) into the i64 NaN-box carrier — the single source of
* truth for pointer bit layout. Compiler IR (`packPtrBits`/`mkPtrIR`), the
* `$__mkptr` inline specializer, and the interop high-word encoder all derive
* from this. `offset` defaults to 0 to yield the box prefix (offset OR'd later). */
export const ptrBits = (type, aux = 0, offset = 0) =>
LAYOUT.NAN_PREFIX_BITS
| ((BigInt(type) & TAG_MASK) << TAG_SHIFT)
| ((BigInt(aux) & AUX_MASK) << AUX_SHIFT)
| (BigInt(offset >>> 0) & OFFSET_MASK)
// =============================================================================
// PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
// field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
// layout it encodes, shared by the compiler (type/analyze/narrow/infer) and the
// `module/typedarray` stdlib.
// =============================================================================
/** Base element-type codes for PTR.TYPED aux (0–7). Flag-sharing kinds ride a
* base code (which fixes stride/shift) plus a discriminator bit: BigInt ctors
* share 7 + BIGINT_FLAG, Float16Array shares 3 (u16 storage) + F16_FLAG,
* Uint8ClampedArray shares 1 (u8 storage) + CLAMPED_FLAG. */
export const TYPED_ELEM_CODE = {
Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
BigInt64Array: 7, BigUint64Array: 7,
Float16Array: 3, Uint8ClampedArray: 1,
}
export const TYPED_ELEM_VIEW_FLAG = 8
export const TYPED_ELEM_BIGINT_FLAG = 16
export const TYPED_ELEM_F16_FLAG = 32
export const TYPED_ELEM_CLAMPED_FLAG = 64
// DataView shares the view descriptor, but has no indexed elements.
export const DATA_VIEW_FLAG = 128
export const DATA_VIEW_AUX = DATA_VIEW_FLAG | TYPED_ELEM_VIEW_FLAG
// A static fact only, never a pointer's aux: an array of one element kind that
// may be owned or a view (`cond ? a.subarray(4) : new Float64Array(8)`). Its
// accesses read the view bit off the pointer (constructor form `new.X.anyview`).
export const TYPED_ELEM_ANY_VIEW_FLAG = 256
/** What the dynamic-property cache's offset holds while the cache is empty
* (module/collection.js `__dyn_get_cache_off`). No key is 1: a memory offset
* is a multiple of 8, a closure's key is negative (-1 - its table index, so
* -1 is the first function's). */
export const DYN_CACHE_EMPTY = 1
export const TYPED_ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']
/** Encode element-type name (+ optional view/bigint flags) to PTR.TYPED aux bits. */
export function encodeTypedElemAux(name, isView = false) {
const et = TYPED_ELEM_CODE[name]
if (et == null) return null
return et | (isView ? TYPED_ELEM_VIEW_FLAG : 0) |
(name === 'BigInt64Array' || name === 'BigUint64Array' ? TYPED_ELEM_BIGINT_FLAG : 0) |
(name === 'Float16Array' ? TYPED_ELEM_F16_FLAG : 0) |
(name === 'Uint8ClampedArray' ? TYPED_ELEM_CLAMPED_FLAG : 0)
}
/** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
* aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView)
* and for an array that may be owned or a view, which no one pointer aux describes. */
export function typedElemAux(ctor) {
if (!ctor || !ctor.startsWith('new.') || ctor.endsWith('.anyview')) return null
const isView = ctor.endsWith('.view')
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
return encodeTypedElemAux(name, isView)
}
/** Reverse of typedElemAux: pick a canonical ctor string for a 4-bit elem aux. Used
* to round-trip TYPED-narrowed call results through ctx.func.typedElem so the
* unboxed local's rep picks up the same aux. aux=7 is shared with BigInt typed
* arrays — Float64Array is canonical (read-side compares aux only). */
export function ctorFromElemAux(aux) {
if (aux == null) return null
if (aux & DATA_VIEW_FLAG) return null
const anyView = (aux & TYPED_ELEM_ANY_VIEW_FLAG) !== 0, isView = (aux & 8) !== 0
const name = (aux & TYPED_ELEM_F16_FLAG) !== 0 ? 'Float16Array'
: (aux & TYPED_ELEM_CLAMPED_FLAG) !== 0 ? 'Uint8ClampedArray'
: (aux & 16) !== 0 ? 'BigInt64Array' : TYPED_ELEM_NAMES[aux & 7]
if (!name) return null
return anyView ? `new.${name}.anyview` : isView ? `new.${name}.view` : `new.${name}`
}
/** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop) — the
* top 32 bits of `ptrBits(type, aux)` (offset lives in the low word). */
export const encodePtrHi = (type, aux) =>
((LAYOUT.NAN_PREFIX << 16) | ((type & LAYOUT.TAG_MASK) << (LAYOUT.TAG_SHIFT - 32)) | (aux & LAYOUT.AUX_MASK)) >>> 0
/** `ptr` (i64) is a box of tag `type`: the prefix and the tag, its top 17 bits. */
export const ptrIsWat = (type, ptr = '(local.get $ptr)') =>
`(i32.eq (i32.wrap_i64 (i64.shr_u ${ptr} (i64.const ${LAYOUT.TAG_SHIFT}))) (i32.const ${encodePtrHi(type, 0) >>> LAYOUT.TAG_SHIFT - 32}))`
/** The element heads (module/core.js `__len` and the reads, module/array.js the stores): the
* common case of a hot runtime helper in a few instructions, small enough for the engine to
* inline into the loop that indexes, the rest in the helper's tail.
* A numeric typed array, owned or a view, has an aux of its element code and at most the view
* flag (no DataView, BigInt, Float16 or clamped flag): its pointer's top 29 bits are this word
* with their low bit, the view flag, free. */
const NUM_TYPED_HI = encodePtrHi(PTR.TYPED, 0) >>> 3
/** `$ptr` is a numeric typed array: `body` runs with `$aux` its high word, `$et` the element
* code, `$s` the log2 width and `$off` its offset, where numTypedBytesWat reads its byte length
* (a view's descriptor holds it, an owned array's header). */
export const numTypedWat = (body) => `
(if (i32.and
(i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const 35))) (i32.const -2)) (i32.const ${NUM_TYPED_HI}))
(i32.ge_u (local.tee $off (i32.wrap_i64 (local.get $ptr))) (i32.const 8)))
(then
(local.set $s (i32.sub
(i32.shr_u (local.tee $et (i32.and (local.tee $aux (i32.wrap_i64 (i64.shr_u (local.get $ptr) (i64.const 32)))) (i32.const 7))) (i32.const 1))
(i32.eq (local.get $et) (i32.const 6))))
${body}))`
export const numTypedBytesWat = `(i32.load (select (local.get $off) (i32.sub (local.get $off) (i32.const 8)) (i32.and (local.get $aux) (i32.const 8))))`
/** numTypedBytesWat where the program makes resizable buffers: a view's byte length
* is its buffer's to bound (`$__view_bytes`, module/typedarray.js). */
export const numTypedBytesRzWat = `(if (result i32) (i32.and (local.get $aux) (i32.const 8))
(then (call $__view_bytes (local.get $off))) (else (i32.load (i32.sub (local.get $off) (i32.const 8)))))`
/** A numeric typed array's element `$i` in bounds: `body` runs with `$off` its address
* (numTypedWat's locals besides), and returns or falls through to the tail. */
export const numTypedElemWat = (body, resizable = false) => numTypedWat(`
(if (i32.lt_u (local.get $i) (i32.shr_u ${resizable ? numTypedBytesRzWat : numTypedBytesWat} (local.get $s)))
(then
(if (i32.and (local.get $aux) (i32.const 8))
(then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
(local.set $off (i32.add (local.get $off) (i32.shl (local.get $i) (local.get $s))))
${body}))`)
export const decodePtrType = hi => (hi >>> (LAYOUT.TAG_SHIFT - 32)) & LAYOUT.TAG_MASK
export const decodePtrAux = hi => hi & LAYOUT.AUX_MASK
/** Top-32-bits i64 mask for a NaN-boxed OBJECT+schema guard: `(bits &
* OBJECT_SCHEMA_HI_MASK) == objectSchemaGuardHex(sid)` proves "is an OBJECT"
* AND "is exactly schema `sid`" in one i64 compare (low word — the instance's
* heap offset — is irrelevant and stays unmasked). Shared by every schema-
* devirtualization guard (module/core.js's property dispatch, src/ir.js's
* Error-schema toStrI64 arm) so the encoding lives in one place. */
export const OBJECT_SCHEMA_HI_MASK = '0xFFFFFFFF00000000'
export const objectSchemaGuardHex = sid => i64Hex(BigInt(encodePtrHi(PTR.OBJECT, sid)) << 32n)
/** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
const _nanPrefixHex = i64Hex(LAYOUT.NAN_PREFIX_BITS)
export const nanPrefixHex = () => _nanPrefixHex
/** AND-mask isolating the boxed-carrier prefix (sign + exponent + quiet bit):
* `(v & nanPrefixMask) == nanPrefix` ⇔ v is a NaN-boxed carrier (any tag/aux/
* payload). The mask is the prefix with the SIGN bit forced on — boxes are
* emitted sign-clear, so a set sign bit must fail the carrier test. */
const _nanPrefixMaskHex = i64Hex(LAYOUT.NAN_PREFIX_BITS | (1n << 63n))
export const nanPrefixMaskHex = () => _nanPrefixMaskHex
/** Atom sentinel as i64 hex (compiler WAT templates). */
export const atomNanHex = atomId => i64Hex(LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << AUX_SHIFT))
/** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
* CANONICAL interned string — the static-pool copy (or an intern-table hit
* resolving to it). Two canonicals are bit-equal iff content-equal, so
* __str_eq answers unequal canonicals without touching bytes, and interned
* statics carry a cached FNV hash at offset-8 ([hash u32][unit length u32][UTF-16LE units])
* that __str_hash loads instead of re-hashing. Inert elsewhere: slice-length
* bits are only read under SLICE_BIT, SSO length under SSO_BIT, and plain-
* heap consumers read the len header at -4 regardless of aux. */
export const STR_INTERN_BIT = 0x1
/** STRING aux bit 1 on a PLAIN-HEAP string (SSO and SLICE clear): the string was
* allocated with an [hash u32][unit length u32][UTF-16LE units] header where the hash cell is a
* LAZY cache — seeded 0 (unit-FNV clamps to ≥2, so 0 is unambiguous "uncomputed"),
* filled by __str_hash on first hash. Sound because heap strings never relocate
* and die with their arena; the two in-place mutators (concat/append bump-extend
* of a heap-top accumulator) zero the cell when they change the bytes. Unlike
* STR_INTERN_BIT it carries NO canonicality claim — two hcache strings with equal
* content are ordinary bit-unequal pointers. Inert for slices (their aux[12:0]
* is a length, only read under SLICE_BIT) and every len/byte reader (-4 header
* unchanged). Producers opt in one by one; an unmarked string just re-hashes. */
export const STR_HCACHE_BIT = 0x2
/** Pre-shifted STRING SSO aux bit as i64 hex. */
const _ssoBitI64Hex = i64Hex(BigInt(LAYOUT.SSO_BIT) << AUX_SHIFT)
export const ssoBitI64Hex = () => _ssoBitI64Hex
/** Pre-shifted STRING slice/view aux bit as i64 hex. */
const _sliceBitI64Hex = i64Hex(BigInt(LAYOUT.SLICE_BIT) << AUX_SHIFT)
export const sliceBitI64Hex = () => _sliceBitI64Hex
/** Pre-shifted STRING hash-cache aux bit as i64 hex. */
const _hcacheBitI64Hex = i64Hex(BigInt(STR_HCACHE_BIT) << AUX_SHIFT)
export const hcacheBitI64Hex = () => _hcacheBitI64Hex
/** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
export const ptrNanHex = (ptrType, aux = 0) => i64Hex(ptrBits(ptrType, aux))
/** Compile-time i64 prefix for mkPtrIR (before offset OR). */
export const ptrBoxPrefixBigInt = (ptrType, aux = 0) => ptrBits(ptrType, aux)
/** Host-side atom sentinel high-u32 values (interop f64 decode). */
export const ATOM_HI = {
[ATOM.NULL]: encodePtrHi(PTR.ATOM, ATOM.NULL),
[ATOM.UNDEF]: encodePtrHi(PTR.ATOM, ATOM.UNDEF),
[ATOM.FALSE]: encodePtrHi(PTR.ATOM, ATOM.FALSE),
[ATOM.TRUE]: encodePtrHi(PTR.ATOM, ATOM.TRUE),
}
/** OOB / canonical quiet-NaN f64 literal for WAT and IR (`nan:0x7FF8…`). */
const oobNanLiteral = () => `nan:${nanPrefixHex()}`
export const oobNanIR = () => ['f64.const', oobNanLiteral()]
/** Heap forwarding-pointer follow (WAT fragment).
* ARRAY/HASH/SET/MAP relocate on growth, leaving the old cell as a forwarding
* header: cap=-1 sentinel at off-4, relocated offset at off-8. `off` is the
* i32 local (token incl. `$`) holding the current offset — mutated in place.
* `lowGuard` adds the `off < 8` bailout; omit it when the caller already
* proved off≥8.
*
* Shape: a loop-free in-bounds + sentinel CHECK with a cold call into
* $__ptr_offset_fwd (the actual chase loop, module/core.js) only when the
* first hop is a real forward. Keeping every inline copy loop-free is what
* lets the engine inline the hot heap helpers (__ptr_offset, __len,
* __arr_idx_known, __typed_idx…) — a body containing a loop is excluded from
* V8's wasm inliner, and these helpers sit on ~25% of self-compile compile time.
* Callers must list '__ptr_offset_fwd' in their deps()/wat() dependency set. */
// Upper bound is `off <= memory.size() * 65536` (total bytes currently backed) — read from
// $__heap_end64 (module/core.js, kept in sync by __memgrow on every grow), NOT recomputed
// as `i64.shl(i64.extend_i32_u(memory.size), 16)` inline: this check is on the hottest
// pointer-dereference path (~25% of self-compile compile time, per the doc comment above), so
// a per-call recompute would cost 3 extra instructions at every inlined site instead of one
// global read. The i32 form `memory.size() << 16` (what a naive version — and $__heap_end
// itself — uses) overflows to exactly 0 at the wasm32 ceiling (memory.size()==65536 pages,
// i.e. the full 4 GiB: 65536*65536 == 2^32, unrepresentable in i32). Reading $__heap_end
// here instead of $__heap_end64 would make the bound "off <= 0", failing for every real off
// and silently disabling the forward-chase for the rest of execution: any pointer to an
// already-relocated ARRAY/SET/MAP/HASH stops following its forwarding header and reads the
// abandoned old block (cap=-1 sentinel misread as a real capacity) instead. That wraparound
// is benign for $__heap_end's own consumer (__alloc's fast-path check just slow-paths one
// extra time and __memgrow re-derives everything fresh in i64) but NOT here, where it gates
// whether forwarding runs at all — hence the separate i64 global instead of reusing $__heap_end.
export const followForwardingWat = (off = '$off', { lowGuard = true } = {}) =>
`(if (i32.and
${lowGuard ? `(i32.ge_u (local.get ${off}) (i32.const 8))` : '(i32.const 1)'}
(i64.le_u (i64.extend_i32_u (local.get ${off})) (global.get $__heap_end64)))
(then (if (i32.eq (i32.load (i32.sub (local.get ${off}) (i32.const 4))) (i32.const -1))
(then (local.set ${off} (call $__ptr_offset_fwd (local.get ${off})))))))`
/** The cold forwarding-chase loop behind followForwardingWat — the only body
* allowed to loop. Re-checks the sentinel each hop (first re-check is
* redundant with the caller's guard; the cold path doesn't care). */
export const ptrOffsetFwdWat = () =>
`(func $__ptr_offset_fwd (param $off i32) (result i32)
(block $done (loop $follow
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
(br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
(br $follow)))
(local.get $off))`
/** An OBJECT's deleted-slot mask: the header `len` word (off-8), which an
* OBJECT does not use otherwise (`__alloc_hdr(0, cap)`; `__len` answers 0 for
* one without reading it). `delete o[k]` keeps undefined in the schema slot,
* so every static read of the slot is JS's `o.a` after the delete, and sets
* bit i for slot i; bit 31 is sticky for the slots from 31 on, which read
* deleted while they hold undefined. A write to the slot clears its bit.
* A static-segment object (off < __heap_start) has no header and, being
* immutable, no deletes: its mask is 0. Read by the presence probe
* (`__dyn_get_t_hm`), enumeration (Object.keys, for-in) and `__obj_clone`;
* written by `__dyn_del` and `__dyn_set`. Both dialects of the runtime read
* it through the builders below. */
const DELETED_STICKY_BIT = 31
/** WAT: the mask of the OBJECT whose payload is at the local `off`. */
export const deletedMaskWat = (off = '$off') =>
`(if (result i32) (i32.ge_u (local.get ${off}) (global.get $__heap_start))
(then (i32.load (i32.sub (local.get ${off}) (i32.const 8))))
(else (i32.const 0)))`
/** WAT: 1 when slot `idx` of an object with mask `mask` is deleted; `val` is
* the slot's i64 (the sticky bit's slots read deleted while undefined). */
export const deletedSlotWat = (mask, idx, val) =>
`(if (result i32) (i32.lt_u (local.get ${idx}) (i32.const ${DELETED_STICKY_BIT}))
(then (i32.and (i32.shr_u (local.get ${mask}) (local.get ${idx})) (i32.const 1)))
(else (i32.and (i32.shr_u (local.get ${mask}) (i32.const ${DELETED_STICKY_BIT}))
(i64.eq (local.get ${val}) (i64.const ${atomNanHex(ATOM.UNDEF)})))))`
/** WAT: mark slot `idx` of the heap OBJECT at `off` deleted (`on`), or present. */
export const markDeletedSlotWat = (off, idx, on) => {
const word = `(i32.sub (local.get ${off}) (i32.const 8))`
const bit = on
? `(i32.shl (i32.const 1) (select (local.get ${idx}) (i32.const ${DELETED_STICKY_BIT}) (i32.lt_u (local.get ${idx}) (i32.const ${DELETED_STICKY_BIT}))))`
: `(i32.shl (i32.const 1) (local.get ${idx}))`
const guard = on
? `(i32.ge_u (local.get ${off}) (global.get $__heap_start))`
: `(i32.and (i32.ge_u (local.get ${off}) (global.get $__heap_start)) (i32.lt_u (local.get ${idx}) (i32.const ${DELETED_STICKY_BIT})))`
return `(if ${guard}
(then (i32.store ${word} (${on ? 'i32.or' : 'i32.and'} (i32.load ${word}) ${on ? bit : `(i32.xor ${bit} (i32.const -1))`}))))`
}
/** IR: the mask of the OBJECT whose payload is in the i32 local `base`. */
export const deletedMaskIR = (base) =>
['if', ['result', 'i32'], ['i32.ge_u', ['local.get', `$${base}`], ['global.get', '$__heap_start']],
['then', ['i32.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 8]]]],
['else', ['i32.const', 0]]]
/** IR: deletedSlotWat over i32 locals `mask`, `idx` and an i64 slot expression. */
export const deletedSlotIR = (mask, idx, val) =>
['if', ['result', 'i32'], ['i32.lt_u', ['local.get', `$${idx}`], ['i32.const', DELETED_STICKY_BIT]],
['then', ['i32.and', ['i32.shr_u', ['local.get', `$${mask}`], ['local.get', `$${idx}`]], ['i32.const', 1]]],
['else', ['i32.and', ['i32.shr_u', ['local.get', `$${mask}`], ['i32.const', DELETED_STICKY_BIT]],
['i64.eq', val, ['i64.const', atomNanHex(ATOM.UNDEF)]]]]]