jz 0.0.0 → 0.1.1

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