-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathlayout.js
More file actions
250 lines (220 loc) · 12.9 KB
/
Copy pathlayout.js
File metadata and controls
250 lines (220 loc) · 12.9 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
/**
* 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,
}
/** 4-bit tagged-pointer type codes. */
export const PTR = {
ATOM: 0,
ARRAY: 1,
BUFFER: 2,
TYPED: 3,
STRING: 4,
OBJECT: 6,
HASH: 7,
SET: 8,
MAP: 9,
CLOSURE: 10,
EXTERNAL: 11,
}
/** Reserved atom aux ids (PTR.ATOM). */
export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
/** 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)
// 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-host, BigInts are raw SIGNED i64 bits
// (kind-erased — see ir.js's SELF-HOST 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 _hx8 = (n) => n.toString(16).toUpperCase().padStart(8, '0')
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
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). */
export function typedElemAux(ctor) {
if (!ctor || !ctor.startsWith('new.')) 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.types.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
const 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 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
export const decodePtrType = hi => (hi >>> (LAYOUT.TAG_SHIFT - 32)) & LAYOUT.TAG_MASK
export const decodePtrAux = hi => hi & LAYOUT.AUX_MASK
/** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
export const nanPrefixHex = () => i64Hex(LAYOUT.NAN_PREFIX_BITS)
/** 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. */
export const nanPrefixMaskHex = () => i64Hex(LAYOUT.NAN_PREFIX_BITS | (1n << 63n))
/** 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][len u32][bytes])
* 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][len u32][bytes] header where the hash cell is a
* LAZY cache — seeded 0 (byte-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. */
export const ssoBitI64Hex = () => i64Hex(BigInt(LAYOUT.SSO_BIT) << AUX_SHIFT)
/** Pre-shifted STRING slice/view aux bit as i64 hex. */
export const sliceBitI64Hex = () => i64Hex(BigInt(LAYOUT.SLICE_BIT) << AUX_SHIFT)
/** Pre-shifted STRING hash-cache aux bit as i64 hex. */
export const hcacheBitI64Hex = () => i64Hex(BigInt(STR_HCACHE_BIT) << AUX_SHIFT)
/** 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…`). */
export 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-host 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-host 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))`