jz 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/core.js ADDED
@@ -0,0 +1,642 @@
1
+ /**
2
+ * Core module — NaN-boxing, bump allocator, property dispatch.
3
+ *
4
+ * Foundation for all heap types. Every module depends on this.
5
+ * NaN-boxing: quiet NaN (0x7FF8) + 51-bit payload [type:4][aux:15][offset:32]
6
+ *
7
+ * Auto-included by array/object/string modules.
8
+ *
9
+ * @module core
10
+ */
11
+
12
+ import { emit, typed, asF64, asI32, valTypeOf, lookupValType, VAL, T, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, repOf, updateRep } from '../src/compile.js'
13
+ import { err, inc, PTR } from '../src/ctx.js'
14
+ import { initSchema } from './schema.js'
15
+ import { strHashLiteral } from './collection.js'
16
+
17
+ const NAN_PREFIX = 0x7FF8
18
+
19
+ const PTR_BY_VAL = {
20
+ [VAL.ARRAY]: PTR.ARRAY,
21
+ [VAL.OBJECT]: PTR.OBJECT,
22
+ [VAL.TYPED]: PTR.TYPED,
23
+ [VAL.SET]: PTR.SET,
24
+ [VAL.MAP]: PTR.MAP,
25
+ [VAL.CLOSURE]: PTR.CLOSURE,
26
+ }
27
+
28
+ export default (ctx) => {
29
+ Object.assign(ctx.core.stdlibDeps, {
30
+ __eq: ['__str_eq', '__ptr_type'],
31
+ __typeof: ['__ptr_type', '__is_nullish'],
32
+ __len: ['__typed_shift'],
33
+ __cap: ['__typed_shift', '__ptr_type', '__ptr_offset', '__ptr_aux'],
34
+ __typed_data: ['__ptr_offset', '__ptr_aux'],
35
+ __ptr_offset: [],
36
+ __is_str_key: ['__ptr_type'],
37
+ __str_len: ['__ptr_type', '__ptr_offset'],
38
+ __set_len: [],
39
+ __length: () => {
40
+ const d = ['__ptr_type', '__ptr_offset', '__str_len', '__len']
41
+ if (ctx.features.sso) d.push('__ptr_aux')
42
+ return d
43
+ },
44
+ __typeof: ['__ptr_type', '__is_nullish'],
45
+ __alloc_hdr: ['__alloc'],
46
+ })
47
+
48
+ ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v f64) (result i32)
49
+ (i32.or
50
+ (i64.eq (i64.reinterpret_f64 (local.get $v)) (i64.const ${NULL_NAN}))
51
+ (i64.eq (i64.reinterpret_f64 (local.get $v)) (i64.const ${UNDEF_NAN}))))`
52
+
53
+ ctx.core.stdlib['__eq'] = `(func $__eq (param $a f64) (param $b f64) (result i32)
54
+ (local $ra i64) (local $rb i64) (local $ta i32) (local $tb i32)
55
+ ;; Fast path: bit equality covers identical pointers AND interned/SSO strings (same content
56
+ ;; → same bits). Failing universal-NaN test catches NaN===NaN→false. Saves the NaN-check
57
+ ;; pair (4 f64.eq) on the hottest case in watr (op === 'literal-string').
58
+ (local.set $ra (i64.reinterpret_f64 (local.get $a)))
59
+ (local.set $rb (i64.reinterpret_f64 (local.get $b)))
60
+ (if (result i32) (i64.eq (local.get $ra) (local.get $rb))
61
+ (then (i64.ne (local.get $ra) (i64.const 0x7FF8000000000000)))
62
+ (else
63
+ ;; Bits differ. Numeric path covers -0/+0 and any normal numeric inequality.
64
+ (if (result i32)
65
+ (i32.and
66
+ (f64.eq (local.get $a) (local.get $a))
67
+ (f64.eq (local.get $b) (local.get $b)))
68
+ (then (f64.eq (local.get $a) (local.get $b)))
69
+ (else
70
+ ;; ≥1 is a NaN-box. Heap-allocated STRING with same content can have different
71
+ ;; offsets — fall through to byte-wise __str_eq.
72
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ra) (i64.const 47)) (i64.const 0xF))))
73
+ (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $rb) (i64.const 47)) (i64.const 0xF))))
74
+ (if (result i32)
75
+ (i32.and
76
+ (i32.or
77
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
78
+ (i32.eq (local.get $ta) (i32.const ${PTR.SSO})))
79
+ (i32.or
80
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING}))
81
+ (i32.eq (local.get $tb) (i32.const ${PTR.SSO}))))
82
+ (then (call $__str_eq (local.get $a) (local.get $b)))
83
+ (else (i32.const 0))))))))`
84
+
85
+ ctx.core.stdlib['__is_null'] = `(func $__is_null (param $v f64) (result i32)
86
+ (i64.eq (i64.reinterpret_f64 (local.get $v)) (i64.const ${NULL_NAN})))`
87
+
88
+ // Truthy check: handles regular numbers AND NaN-boxed pointers
89
+ // Falsy: 0, -0, NaN, null, undefined, "" (empty SSO)
90
+ ctx.core.stdlib['__is_truthy'] = `(func $__is_truthy (param $v f64) (result i32)
91
+ (local $bits i64)
92
+ (if (result i32) (f64.eq (local.get $v) (local.get $v))
93
+ (then (f64.ne (local.get $v) (f64.const 0)))
94
+ (else
95
+ (local.set $bits (i64.reinterpret_f64 (local.get $v)))
96
+ (i32.and
97
+ (i32.and
98
+ (i64.ne (local.get $bits) (i64.const 0x7FF8000000000000))
99
+ (i64.ne (local.get $bits) (i64.const ${NULL_NAN})))
100
+ (i32.and
101
+ (i64.ne (local.get $bits) (i64.const ${UNDEF_NAN}))
102
+ (i64.ne (local.get $bits) (i64.const 0x7FFA800000000000)))))))`
103
+
104
+ ctx.core.stdlib['__is_str_key'] = `(func $__is_str_key (param $v f64) (result i32)
105
+ (local $t i32)
106
+ (if (result i32) (f64.eq (local.get $v) (local.get $v))
107
+ (then (i32.const 0))
108
+ (else
109
+ (local.set $t (call $__ptr_type (local.get $v)))
110
+ (i32.or
111
+ (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
112
+ (i32.eq (local.get $t) (i32.const ${PTR.SSO}))))))`
113
+
114
+
115
+ // Default dynamic-property helpers are harmless stubs. The collection module
116
+ // overrides them with the real sidecar-property implementation.
117
+ ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj f64) (param $key f64) (result f64)
118
+ (f64.const nan:${UNDEF_NAN}))`
119
+ ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj f64) (param $key f64) (param $fallback f64) (result f64)
120
+ (local.get $fallback))`
121
+ ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj f64) (param $key f64) (param $val f64) (result f64)
122
+ (local.get $val))`
123
+ ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32))`
124
+
125
+ // Memory section auto-enabled: compile.js checks ctx.module.modules.ptr
126
+
127
+ // === NaN-boxing: encode/decode ===
128
+
129
+ ctx.core.stdlib['__mkptr'] = `(func $__mkptr (param $type i32) (param $aux i32) (param $offset i32) (result f64)
130
+ (f64.reinterpret_i64 (i64.or
131
+ (i64.shl (i64.const ${NAN_PREFIX}) (i64.const 48))
132
+ (i64.or
133
+ (i64.shl (i64.and (i64.extend_i32_u (local.get $type)) (i64.const 0xF)) (i64.const 47))
134
+ (i64.or
135
+ (i64.shl (i64.and (i64.extend_i32_u (local.get $aux)) (i64.const 0x7FFF)) (i64.const 32))
136
+ (i64.and (i64.extend_i32_u (local.get $offset)) (i64.const 0xFFFFFFFF)))))))`
137
+
138
+ ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr f64) (result i32)
139
+ (local $bits i64) (local $off i32)
140
+ (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
141
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
142
+ ;; Arrays can be reallocated during growth; follow forwarding pointer (cap=-1 sentinel).
143
+ ;; Bounds are checked inside the loop so non-array ptrs skip them entirely, and well-formed
144
+ ;; ARRAY ptrs without forwarding still pay only one bounds check before the cap load.
145
+ (if (i32.eq
146
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
147
+ (i32.const ${PTR.ARRAY}))
148
+ (then
149
+ (block $done
150
+ (loop $follow
151
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
152
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
153
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
154
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
155
+ (br $follow)))))
156
+ (local.get $off))`
157
+
158
+ ctx.core.stdlib['__ptr_aux'] = `(func $__ptr_aux (param $ptr f64) (result i32)
159
+ (i32.wrap_i64 (i64.and (i64.shr_u (i64.reinterpret_f64 (local.get $ptr)) (i64.const 32)) (i64.const 0x7FFF))))`
160
+
161
+ ctx.core.stdlib['__ptr_type'] = `(func $__ptr_type (param $ptr f64) (result i32)
162
+ (i32.wrap_i64 (i64.and (i64.shr_u (i64.reinterpret_f64 (local.get $ptr)) (i64.const 47)) (i64.const 0xF))))`
163
+
164
+ // === Bump allocator ===
165
+
166
+ if (ctx.memory.shared) {
167
+ // Shared memory: heap offset stored at memory[1020] (i32), just before heap start at 1024
168
+ ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
169
+ (local $ptr i32)
170
+ (local.set $ptr (i32.load (i32.const 1020)))
171
+ (i32.store (i32.const 1020) (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
172
+ (local.get $ptr))`
173
+ ctx.core.stdlib['__reset'] = `(func $__reset
174
+ (i32.store (i32.const 1020) (i32.const 1024)))`
175
+ } else {
176
+ // Own memory: heap offset in a global, auto-grow when needed
177
+ ctx.scope.globals.set('__heap', '(global $__heap (mut i32) (i32.const 1024))')
178
+ ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
179
+ (local $ptr i32) (local $next i32)
180
+ (local.set $ptr (global.get $__heap))
181
+ ;; Align next allocation to 8 bytes
182
+ (local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
183
+ ;; Grow memory if needed (each page = 65536 bytes)
184
+ (if (i32.gt_u (local.get $next) (i32.mul (memory.size) (i32.const 65536)))
185
+ (then (if (i32.eq (memory.grow
186
+ (i32.shr_u (i32.add (i32.sub (local.get $next) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
187
+ (i32.const -1)) (then (unreachable)))))
188
+ (global.set $__heap (local.get $next))
189
+ (local.get $ptr))`
190
+ ctx.core.stdlib['__reset'] = `(func $__reset
191
+ (global.set $__heap (i32.const 1024)))`
192
+ }
193
+
194
+ // === Memory-based length/cap helpers (C-style headers) ===
195
+
196
+ // Array/TypedArray/Buffer: [-8:len(i32)][-4:cap(i32)][data...]
197
+ // For ARRAY/HASH/SET/MAP: len is element count.
198
+ // For BUFFER: len is byte count. For owned TYPED: header stores byte count; len
199
+ // is derived as byteLen >> log2(stride) so reinterpret views share their parent
200
+ // BUFFER's header (zero-copy aliasing).
201
+ // For TYPED subviews (aux bit 3 set): offset points to a 16-byte descriptor
202
+ // [0:byteLen(i32)][4:dataOff(i32)][8:parentOff(i32)][12:pad]
203
+ // elemType = aux & 7, isView = aux & 8.
204
+ ctx.core.stdlib['__typed_shift'] = `(func $__typed_shift (param $et i32) (result i32)
205
+ (if (result i32) (i32.eq (local.get $et) (i32.const 7))
206
+ (then (i32.const 3))
207
+ (else (if (result i32) (i32.ge_u (local.get $et) (i32.const 4))
208
+ (then (i32.const 2))
209
+ (else (i32.shr_u (local.get $et) (i32.const 1)))))))`
210
+
211
+ // Real data address for any TYPED ptr: owned → offset, view → [offset+4].
212
+ ctx.core.stdlib['__typed_data'] = `(func $__typed_data (param $ptr f64) (result i32)
213
+ (local $off i32)
214
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
215
+ (if (result i32) (i32.and (call $__ptr_aux (local.get $ptr)) (i32.const 8))
216
+ (then (i32.load (i32.add (local.get $off) (i32.const 4))))
217
+ (else (local.get $off))))`
218
+
219
+ // Hot (~85M calls in watr self-host). Type/offset extraction inlined; forwarding
220
+ // loop only entered for ARRAY. ARRAY fast path dominates (nodes?.length, out.length …).
221
+ ctx.core.stdlib['__len'] = `(func $__len (param $ptr f64) (result i32)
222
+ (local $bits i64) (local $t i32) (local $off i32) (local $aux i32)
223
+ (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
224
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
225
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
226
+ ;; ARRAY fast path: follow forwarding inline, then load len at off-8.
227
+ (if (result i32)
228
+ (i32.and (i32.eq (local.get $t) (i32.const 1)) (i32.ge_u (local.get $off) (i32.const 8)))
229
+ (then
230
+ (block $done
231
+ (loop $follow
232
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
233
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
234
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
235
+ (br $follow)))
236
+ (i32.load (i32.sub (local.get $off) (i32.const 8))))
237
+ (else
238
+ (if (result i32)
239
+ (i32.and
240
+ (i32.ge_u (local.get $off) (i32.const 8))
241
+ (i32.or
242
+ (i32.eq (local.get $t) (i32.const 3))
243
+ (i32.or (i32.eq (local.get $t) (i32.const ${PTR.BUFFER}))
244
+ (i32.or (i32.eq (local.get $t) (i32.const 7))
245
+ (i32.or (i32.eq (local.get $t) (i32.const 8)) (i32.eq (local.get $t) (i32.const 9)))))))
246
+ (then
247
+ (if (result i32) (i32.eq (local.get $t) (i32.const 3))
248
+ (then
249
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
250
+ (if (result i32) (i32.and (local.get $aux) (i32.const 8))
251
+ (then (i32.shr_u (i32.load (local.get $off))
252
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
253
+ (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 8)))
254
+ (call $__typed_shift (local.get $aux))))))
255
+ (else (i32.load (i32.sub (local.get $off) (i32.const 8))))))
256
+ (else (i32.const 0))))))`
257
+
258
+ ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr f64) (result i32)
259
+ (local $t i32) (local $off i32) (local $aux i32)
260
+ (local.set $t (call $__ptr_type (local.get $ptr)))
261
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
262
+ (if (result i32)
263
+ (i32.and
264
+ (i32.ge_u (local.get $off) (i32.const 4))
265
+ (i32.or
266
+ (i32.or
267
+ (i32.or (i32.eq (local.get $t) (i32.const 1)) (i32.eq (local.get $t) (i32.const 3)))
268
+ (i32.eq (local.get $t) (i32.const ${PTR.BUFFER})))
269
+ (i32.or (i32.eq (local.get $t) (i32.const 7))
270
+ (i32.or (i32.eq (local.get $t) (i32.const 8)) (i32.eq (local.get $t) (i32.const 9))))))
271
+ (then
272
+ (if (result i32) (i32.eq (local.get $t) (i32.const 3))
273
+ (then
274
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
275
+ (if (result i32) (i32.and (local.get $aux) (i32.const 8))
276
+ ;; views are non-growable: cap = len (byteLen at [off])
277
+ (then (i32.shr_u (i32.load (local.get $off))
278
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
279
+ (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 4)))
280
+ (call $__typed_shift (local.get $aux))))))
281
+ (else (i32.load (i32.sub (local.get $off) (i32.const 4))))))
282
+ (else (i32.const 0))))`
283
+
284
+ // String (heap): [-4:len(i32)][chars...]
285
+ ctx.core.stdlib['__str_len'] = `(func $__str_len (param $ptr f64) (result i32)
286
+ (local $off i32)
287
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
288
+ (if (result i32)
289
+ (i32.and
290
+ (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.STRING}))
291
+ (i32.ge_u (local.get $off) (i32.const 4)))
292
+ (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
293
+ (else (i32.const 0))))`
294
+
295
+ // Set len in memory (for push/pop). Hot (~42M calls in watr self-host).
296
+ // Type/offset extraction inlined; forwarding loop only entered for ARRAY.
297
+ ctx.core.stdlib['__set_len'] = `(func $__set_len (param $ptr f64) (param $len i32)
298
+ (local $bits i64) (local $t i32) (local $off i32)
299
+ (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
300
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
301
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
302
+ ;; Only ARRAY (1), TYPED (3), HASH (7), SET (8), MAP (9) carry an 8-byte header.
303
+ ;; Of those, only ARRAY can be forwarded — follow the chain inline.
304
+ (if
305
+ (i32.and
306
+ (i32.ge_u (local.get $off) (i32.const 8))
307
+ (i32.or
308
+ (i32.or (i32.eq (local.get $t) (i32.const 1)) (i32.eq (local.get $t) (i32.const 3)))
309
+ (i32.or (i32.eq (local.get $t) (i32.const 7))
310
+ (i32.or (i32.eq (local.get $t) (i32.const 8)) (i32.eq (local.get $t) (i32.const 9))))))
311
+ (then
312
+ (if (i32.eq (local.get $t) (i32.const 1))
313
+ (then
314
+ (block $done
315
+ (loop $follow
316
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
317
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
318
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
319
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
320
+ (br $follow)))))
321
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $len)))))`
322
+
323
+ // Alloc header(8) + data(cap*stride), store len+cap, return data offset (past header)
324
+ ctx.core.stdlib['__alloc_hdr'] = `(func $__alloc_hdr (param $len i32) (param $cap i32) (param $stride i32) (result i32)
325
+ (local $ptr i32)
326
+ (local.set $ptr (call $__alloc (i32.add (i32.const 8) (i32.mul (local.get $cap) (local.get $stride)))))
327
+ (i32.store (local.get $ptr) (local.get $len))
328
+ (i32.store (i32.add (local.get $ptr) (i32.const 4)) (local.get $cap))
329
+ (i32.add (local.get $ptr) (i32.const 8)))`
330
+
331
+ // Allocator + exports are deferred: only included when memory is actually needed.
332
+ // Any module using allocPtr/inc('__alloc') pulls these in via ctx.core.stdlibDeps.
333
+ // compile.js emits _alloc/_reset exports + memory section only when __alloc is in includes.
334
+ ctx.core._allocRawFuncs = [
335
+ '(func (export "_alloc") (param $bytes i32) (result i32) (call $__alloc (local.get $bytes)))',
336
+ '(func (export "_reset") (call $__reset))',
337
+ ]
338
+
339
+ // Not-nullish check: f64 WAT node is neither NULL_NAN nor UNDEF_NAN.
340
+ // Routes through isNullish() so peepholes (ptrKind, NaN-boxed literal, local.get inline)
341
+ // apply — otherwise this would always emit a __is_nullish call even for provable cases.
342
+ const notNullish = v => ['i32.eqz', isNullish(v)]
343
+
344
+ // Optional-chain wrapper: eval guard, if non-nullish emit access, else NULL_NAN.
345
+ const emitNullishGuarded = (guard, access) => typed(['if', ['result', 'f64'],
346
+ notNullish(guard),
347
+ ['then', access],
348
+ ['else', ['f64.const', `nan:${NULL_NAN}`]]], 'f64')
349
+
350
+ // === Shared dispatch helpers ===
351
+
352
+ /** Emit .length access for a WASM f64 node. Monomorphize by vt, or runtime dispatch.
353
+ * ARRAY/SET/MAP share a single layout: length is i32 at offset-8. We inline that load
354
+ * directly instead of calling __len which re-dispatches on type. ptrOffsetIR handles
355
+ * ARRAY forwarding (non-ARRAY skips the forwarding loop). TYPED has a variable-width
356
+ * layout depending on the aux typed-element shift, so it still routes through __len. */
357
+ function emitLengthAccess(va, vt) {
358
+ if (vt === VAL.ARRAY || vt === VAL.SET || vt === VAL.MAP) {
359
+ const off = ptrOffsetIR(va, vt)
360
+ return typed(['f64.convert_i32_s', ['i32.load', ['i32.sub', off, ['i32.const', 8]]]], 'f64')
361
+ }
362
+ if (vt === VAL.TYPED)
363
+ return typed(['f64.convert_i32_s', ['call', '$__len', va]], 'f64')
364
+ // Known string → byteLen (handles SSO + heap)
365
+ if (vt === VAL.STRING) {
366
+ inc('__str_byteLen')
367
+ return typed(['f64.convert_i32_s', ['call', '$__str_byteLen', va]], 'f64')
368
+ }
369
+ // Unknown → runtime dispatch via stdlib. Set/Map dispatch arms are pulled
370
+ // only when user code actually constructs Set/Map (collection.js sets the
371
+ // feature flags at the construction site); otherwise dispatch falls through
372
+ // to ARRAY/STRING/TYPED. typedarray stays on because typed arrays are
373
+ // commonly passed from JS via jz.memory.* without an in-program constructor.
374
+ inc('__length')
375
+ ctx.features.typedarray = true
376
+ return typed(['call', '$__length', va], 'f64')
377
+ }
378
+
379
+ // Known-schema fields live in the object payload. Dynamic sidecars are only
380
+ // for ad-hoc props on pointer-backed values, so schema reads should bypass it.
381
+ // Slot val-types reach the emit-time consumer via valTypeOf → ctx.schema.slotVT
382
+ // (read on the AST `.prop` node), not via tagging this IR node.
383
+ function emitSchemaSlotRead(baseExpr, idx) {
384
+ const base = baseExpr?.type === 'f64' ? baseExpr : typed(baseExpr, 'f64')
385
+ return typed(['f64.load', ['i32.add', ptrOffsetIR(base, VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
386
+ }
387
+
388
+ function emitHashGetLocalConst(base, key, prop) {
389
+ inc('__hash_get_local_h')
390
+ const receiver = base?.type ? asF64(base) : typed(base, 'f64')
391
+ return typed(['call', '$__hash_get_local_h', receiver, key, ['i32.const', strHashLiteral(prop)]], 'f64')
392
+ }
393
+
394
+ function emitTypeTag(receiver, vt) {
395
+ const p = PTR_BY_VAL[vt]
396
+ if (p != null) return ['i32.const', p]
397
+ inc('__ptr_type')
398
+ return ['call', '$__ptr_type', receiver]
399
+ }
400
+
401
+ function emitDynGetExprTyped(base, key, vt) {
402
+ inc('__dyn_get_expr_t')
403
+ const receiver = base?.type ? asF64(base) : typed(base, 'f64')
404
+ return typed(['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)], 'f64')
405
+ }
406
+
407
+ function emitDynGetAnyTyped(base, key, vt) {
408
+ inc('__dyn_get_any_t')
409
+ const receiver = base?.type ? asF64(base) : typed(base, 'f64')
410
+ return typed(['call', '$__dyn_get_any_t', receiver, key, emitTypeTag(receiver, vt)], 'f64')
411
+ }
412
+
413
+ /** Emit .prop access for a WASM f64 node using schema or HASH fallback. */
414
+ function emitPropAccess(va, obj, prop) {
415
+ const schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
416
+ const key = asF64(emit(['str', prop]))
417
+ if (schemaIdx >= 0) return emitSchemaSlotRead(asF64(va), schemaIdx)
418
+ if (typeof obj === 'string') {
419
+ const vt = lookupValType(obj)
420
+ if (usesDynProps(vt)) {
421
+ return emitDynGetExprTyped(va, key, vt)
422
+ }
423
+ if (vt === VAL.HASH) {
424
+ return emitHashGetLocalConst(va, key, prop)
425
+ }
426
+ if (vt == null) {
427
+ ctx.features.external = true
428
+ return emitDynGetAnyTyped(va, key, vt)
429
+ }
430
+ inc('__hash_get', '__str_hash', '__str_eq')
431
+ return typed(['call', '$__hash_get', asF64(va), key], 'f64')
432
+ }
433
+ // Non-string receiver: route through HASH fast path when valTypeOf can
434
+ // resolve the chain to a known HASH (e.g. `o.meta.bias` where `o.meta` is
435
+ // a HASH per the parsed JSON shape). Falls back to dynamic dispatch
436
+ // otherwise.
437
+ if (valTypeOf(obj) === VAL.HASH) {
438
+ return emitHashGetLocalConst(va, key, prop)
439
+ }
440
+ inc('__dyn_get_expr')
441
+ return typed(['call', '$__dyn_get_expr', asF64(va), key], 'f64')
442
+ }
443
+
444
+ // Runtime .length dispatch — factory elides branches for types that can't exist in
445
+ // this program (features.* + hash-stdlib presence). ARRAY is always live; STRING and
446
+ // number are always dispatched. SSO branch elided when features.sso is off. The __len
447
+ // disjunction collapses to whichever of ARRAY/TYPED/HASH/SET/MAP are reachable.
448
+ ctx.core.stdlib['__length'] = () => {
449
+ const types = [PTR.ARRAY]
450
+ if (ctx.features.typedarray) types.push(PTR.TYPED)
451
+ if (ctx.core.includes.has('__hash_new') || ctx.core.includes.has('__dyn_set') || ctx.core.includes.has('__hash_set'))
452
+ types.push(PTR.HASH)
453
+ if (ctx.features.set) types.push(PTR.SET)
454
+ if (ctx.features.map) types.push(PTR.MAP)
455
+ const eqT = (n) => `(i32.eq (local.get $t) (i32.const ${n}))`
456
+ let disj = eqT(types[0])
457
+ for (let i = 1; i < types.length; i++) disj = `(i32.or ${disj} ${eqT(types[i])})`
458
+ const lenArm = `(if (result f64) ${disj}
459
+ (then
460
+ (if (result f64) (i32.ge_u (local.get $off) (i32.const 8))
461
+ (then (f64.convert_i32_s (call $__len (local.get $v))))
462
+ (else (f64.const nan:${UNDEF_NAN}))))
463
+ (else (f64.const nan:${UNDEF_NAN})))`
464
+ const stringArm = `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
465
+ (then
466
+ (if (result f64) (i32.ge_u (local.get $off) (i32.const 4))
467
+ (then (f64.convert_i32_s (call $__str_len (local.get $v))))
468
+ (else (f64.const nan:${UNDEF_NAN}))))
469
+ (else ${lenArm}))`
470
+ const afterNumber = ctx.features.sso
471
+ ? `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.SSO}))
472
+ (then (f64.convert_i32_s (call $__ptr_aux (local.get $v))))
473
+ (else ${stringArm}))`
474
+ : stringArm
475
+ return `(func $__length (param $v f64) (result f64)
476
+ (local $t i32) (local $off i32)
477
+ (if (result f64) (f64.eq (local.get $v) (local.get $v))
478
+ (then (f64.const nan:${UNDEF_NAN}))
479
+ (else
480
+ (local.set $t (call $__ptr_type (local.get $v)))
481
+ (local.set $off (call $__ptr_offset (local.get $v)))
482
+ ${afterNumber})))`
483
+ }
484
+
485
+ // === Property dispatch (.length, .prop) ===
486
+
487
+ ctx.core.emit['.'] = (obj, prop) => {
488
+ // Boxed object: delegate .length and .prop to inner value or schema
489
+ if (typeof obj === 'string' && ctx.schema.isBoxed(obj)) {
490
+ if (prop === 'length') {
491
+ const inner = ctx.schema.emitInner(obj)
492
+ return typed(['f64.convert_i32_s', ['call', '$__len', inner]], 'f64')
493
+ }
494
+ const idx = ctx.schema.find(obj, prop)
495
+ if (idx >= 0) return emitSchemaSlotRead(asF64(emit(obj)), idx)
496
+ }
497
+
498
+ if (prop === 'length') {
499
+ // Fast path: typed-narrowed local (ptrKind=TYPED with known ptrAux) — bypass
500
+ // the f64 NaN-rebox + __len ptr-type/aux re-extraction round-trip.
501
+ // Owned typed (aux & 8 == 0): byteLen at off-8, shifted by element shift.
502
+ // View typed (aux & 8): byteLen stored at off+0 (descriptor head), shifted.
503
+ if (typeof obj === 'string') {
504
+ const r = repOf(obj)
505
+ if (r?.ptrKind === VAL.TYPED && r.ptrAux != null) {
506
+ const aux = r.ptrAux, isView = (aux & 8) !== 0
507
+ const et = aux & 7
508
+ const shift = et === 7 ? 3 : et >= 4 ? 2 : et >> 1
509
+ const off = ['local.get', `$${obj}`]
510
+ const byteLen = isView
511
+ ? ['i32.load', off]
512
+ : ['i32.load', ['i32.sub', off, ['i32.const', 8]]]
513
+ const lenI32 = shift === 0
514
+ ? typed(byteLen, 'i32')
515
+ : typed(['i32.shr_u', byteLen, ['i32.const', shift]], 'i32')
516
+ return typed(['f64.convert_i32_s', lenI32], 'f64')
517
+ }
518
+ }
519
+ const vt = typeof obj === 'string' ? repOf(obj)?.val : valTypeOf(obj)
520
+ return emitLengthAccess(asF64(emit(obj)), vt)
521
+ }
522
+
523
+ // Module-registered property emitter (.size, etc.)
524
+ const propKey = `.${prop}`
525
+ if (ctx.core.emit[propKey]) return ctx.core.emit[propKey](obj)
526
+
527
+ return emitPropAccess(emit(obj), obj, prop)
528
+ }
529
+
530
+ // Optional chaining: obj?.prop → null if obj is null, else obj.prop
531
+ ctx.core.emit['?.'] = (obj, prop) => {
532
+ const t = temp()
533
+ const va = asF64(emit(obj))
534
+ const vt = typeof obj === 'string' ? repOf(obj)?.val : valTypeOf(obj)
535
+ let access
536
+ if (prop === 'length') {
537
+ access = emitLengthAccess(['local.get', `$${t}`], vt)
538
+ } else {
539
+ const propIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : -1
540
+ if (propIdx >= 0) access = emitSchemaSlotRead(['local.get', `$${t}`], propIdx)
541
+ else {
542
+ if (typeof obj === 'string') {
543
+ const objType = lookupValType(obj)
544
+ if (usesDynProps(objType)) {
545
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), objType)
546
+ } else if (objType === VAL.HASH) {
547
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
548
+ } else if (objType == null) {
549
+ ctx.features.external = true
550
+ access = emitDynGetAnyTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), objType)
551
+ } else {
552
+ inc('__hash_get', '__str_hash', '__str_eq')
553
+ access = ['call', '$__hash_get', ['local.get', `$${t}`], asF64(emit(['str', prop]))]
554
+ }
555
+ } else {
556
+ if (valTypeOf(obj) === VAL.HASH) {
557
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
558
+ } else {
559
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), valTypeOf(obj))
560
+ }
561
+ }
562
+ }
563
+ }
564
+ return emitNullishGuarded(['local.tee', `$${t}`, va], access)
565
+ }
566
+
567
+ // Optional index: arr?.[i] → null if arr is null, else arr[i]
568
+ // Cache base in temp, propagate valType so []'s type dispatch works
569
+ ctx.core.emit['?.[]'] = (arr, idx) => {
570
+ const t = temp()
571
+ const va = asF64(emit(arr))
572
+ // Propagate source type to temp so [] dispatch (string, typed, etc.) works
573
+ const srcType = typeof arr === 'string' ? repOf(arr)?.val : null
574
+ if (srcType) updateRep(t, { val: srcType })
575
+ if (typeof arr === 'string' && ctx.types.typedElem?.has(arr)) {
576
+ if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
577
+ ctx.types.typedElem.set(t, ctx.types.typedElem.get(arr))
578
+ }
579
+ return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(ctx.core.emit['[]'](t, idx)))
580
+ }
581
+
582
+ // Optional call: fn?.(...args) → null if fn is null, else call fn
583
+ ctx.core.emit['?.()'] = (callee, ...args) => {
584
+ const t = temp()
585
+ const va = asF64(emit(callee))
586
+ // If nullish → return NULL_NAN, else call via fn.call
587
+ if (!ctx.closure.call) err('Optional call requires fn module')
588
+ const callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
589
+ return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
590
+ }
591
+
592
+ // typeof: returns JS-style string. Reachable results are number/undefined/string/function/symbol/object
593
+ // (booleans are f64 and hit the number branch; no bigints). Strings are preallocated into globals and
594
+ // initialized in __start (see compile.js). Comparison patterns (typeof x === 'string') are optimized
595
+ // in prepare.js (resolveTypeof) and emitted as direct type checks via emitTypeofCmp, bypassing this path.
596
+ ctx.core.emit['typeof'] = (a) => {
597
+ if (!ctx.runtime.typeofStrs) {
598
+ ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
599
+ for (const s of ctx.runtime.typeofStrs)
600
+ ctx.scope.globals.set(`__tof_${s}`, `(global $__tof_${s} (mut f64) (f64.const 0))`)
601
+ }
602
+ inc('__typeof')
603
+ // Receiver type unknown; enable branches that wouldn't otherwise be reachable.
604
+ ctx.features.closure = true
605
+ return typed(['call', '$__typeof', asF64(emit(a))], 'f64')
606
+ }
607
+
608
+ ctx.core.stdlib['__typeof'] = () => {
609
+ const stringTest = ctx.features.sso
610
+ ? `(i32.or (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.eq (local.get $t) (i32.const ${PTR.SSO})))`
611
+ : `(i32.eq (local.get $t) (i32.const ${PTR.STRING}))`
612
+ const closureArm = ctx.features.closure
613
+ ? `(if (i32.eq (local.get $t) (i32.const ${PTR.CLOSURE}))
614
+ (then (return (global.get $__tof_function))))`
615
+ : ''
616
+ return `(func $__typeof (param $v f64) (result f64)
617
+ (local $t i32)
618
+ (if (f64.eq (local.get $v) (local.get $v))
619
+ (then (return (global.get $__tof_number))))
620
+ (if (call $__is_nullish (local.get $v))
621
+ (then (return (global.get $__tof_undefined))))
622
+ (local.set $t (call $__ptr_type (local.get $v)))
623
+ (if ${stringTest}
624
+ (then (return (global.get $__tof_string))))
625
+ ${closureArm}
626
+ (if (i32.eqz (local.get $t))
627
+ (then (return (global.get $__tof_symbol))))
628
+ (global.get $__tof_object))`
629
+ }
630
+
631
+ // === Schema helpers (centralized in module/schema.js) ===
632
+ initSchema(ctx)
633
+
634
+ // Low-level pointer helpers callable from jz code
635
+ ctx.core.emit['__mkptr'] = (t, a, o) => typed(['call', '$__mkptr', asI32(emit(t)), asI32(emit(a)), asI32(emit(o))], 'f64')
636
+ ctx.core.emit['__ptr_type'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_type', asF64(emit(p))]], 'f64')
637
+ ctx.core.emit['__ptr_aux'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asF64(emit(p))]], 'f64')
638
+ ctx.core.emit['__ptr_offset'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asF64(emit(p))]], 'f64')
639
+
640
+ // Error(msg) — passthrough (throw handles any value)
641
+ ctx.core.emit['Error'] = (msg) => asF64(emit(msg))
642
+ }