jz 0.1.1 → 0.2.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 CHANGED
@@ -2,21 +2,22 @@
2
2
  * Core module — NaN-boxing, bump allocator, property dispatch.
3
3
  *
4
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]
5
+ * NaN-boxing: see LAYOUT in src/ctx.js for the canonical bit layout.
6
6
  *
7
7
  * Auto-included by array/object/string modules.
8
8
  *
9
9
  * @module core
10
10
  */
11
11
 
12
- import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish } from '../src/ir.js'
12
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish } from '../src/ir.js'
13
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'
14
+ import { valTypeOf, lookupValType, VAL, T, repOf, updateRep, shapeOf } from '../src/analyze.js'
15
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
16
16
  import { initSchema } from './schema.js'
17
17
  import { strHashLiteral } from './collection.js'
18
18
 
19
- const NAN_PREFIX = 0x7FF8
19
+ // Pre-shifted NaN prefix as a full i64 mask, for `(i64.const ${NAN_BITS})` use.
20
+ const NAN_BITS = '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
20
21
 
21
22
  const PTR_BY_VAL = {
22
23
  [VAL.ARRAY]: PTR.ARRAY,
@@ -36,91 +37,81 @@ export default (ctx) => {
36
37
  __typed_data: ['__ptr_offset', '__ptr_aux'],
37
38
  __ptr_offset: [],
38
39
  __is_str_key: ['__ptr_type'],
39
- __str_len: ['__ptr_type', '__ptr_offset'],
40
+ __str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
40
41
  __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
- },
42
+ __length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
46
43
  __typeof: ['__ptr_type', '__is_nullish'],
47
44
  __alloc_hdr: ['__alloc'],
48
45
  })
49
46
 
50
- ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v f64) (result i32)
47
+ ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
51
48
  (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}))))`
49
+ (i64.eq (local.get $v) (i64.const ${NULL_NAN}))
50
+ (i64.eq (local.get $v) (i64.const ${UNDEF_NAN}))))`
54
51
 
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)
52
+ ctx.core.stdlib['__eq'] = `(func $__eq (param $a i64) (param $b i64) (result i32)
53
+ (local $fa f64) (local $fb f64) (local $ta i32) (local $tb i32)
57
54
  ;; Fast path: bit equality covers identical pointers AND interned/SSO strings (same content
58
55
  ;; → same bits). Failing universal-NaN test catches NaN===NaN→false. Saves the NaN-check
59
56
  ;; 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)))
57
+ (if (result i32) (i64.eq (local.get $a) (local.get $b))
58
+ (then (i64.ne (local.get $a) (i64.const ${NAN_BITS})))
64
59
  (else
65
60
  ;; Bits differ. Numeric path covers -0/+0 and any normal numeric inequality.
61
+ (local.set $fa (f64.reinterpret_i64 (local.get $a)))
62
+ (local.set $fb (f64.reinterpret_i64 (local.get $b)))
66
63
  (if (result i32)
67
64
  (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)))
65
+ (f64.eq (local.get $fa) (local.get $fa))
66
+ (f64.eq (local.get $fb) (local.get $fb)))
67
+ (then (f64.eq (local.get $fa) (local.get $fb)))
71
68
  (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))))
69
+ ;; At least one operand is a NaN-box. Both STRING (heap or SSO) __str_eq
70
+ ;; handles content compare and SSO fast-fail internally.
71
+ (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
72
+ (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
76
73
  (if (result i32)
77
74
  (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}))))
75
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
76
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
84
77
  (then (call $__str_eq (local.get $a) (local.get $b)))
85
78
  (else (i32.const 0))))))))`
86
79
 
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})))`
80
+ ctx.core.stdlib['__is_null'] = `(func $__is_null (param $v i64) (result i32)
81
+ (i64.eq (local.get $v) (i64.const ${NULL_NAN})))`
89
82
 
90
83
  // Truthy check: handles regular numbers AND NaN-boxed pointers
91
84
  // 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)))
85
+ ctx.core.stdlib['__is_truthy'] = `(func $__is_truthy (param $v i64) (result i32)
86
+ (local $f f64)
87
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
88
+ (if (result i32) (f64.eq (local.get $f) (local.get $f))
89
+ (then (f64.ne (local.get $f) (f64.const 0)))
96
90
  (else
97
- (local.set $bits (i64.reinterpret_f64 (local.get $v)))
98
91
  (i32.and
99
92
  (i32.and
100
- (i64.ne (local.get $bits) (i64.const 0x7FF8000000000000))
101
- (i64.ne (local.get $bits) (i64.const ${NULL_NAN})))
93
+ (i64.ne (local.get $v) (i64.const ${NAN_BITS}))
94
+ (i64.ne (local.get $v) (i64.const ${NULL_NAN})))
102
95
  (i32.and
103
- (i64.ne (local.get $bits) (i64.const ${UNDEF_NAN}))
104
- (i64.ne (local.get $bits) (i64.const 0x7FFA800000000000)))))))`
96
+ (i64.ne (local.get $v) (i64.const ${UNDEF_NAN}))
97
+ (i64.ne (local.get $v) (i64.const 0x7FFA400000000000)))))))`
105
98
 
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))
99
+ ctx.core.stdlib['__is_str_key'] = `(func $__is_str_key (param $v i64) (result i32)
100
+ (local $f f64)
101
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
102
+ (if (result i32) (f64.eq (local.get $f) (local.get $f))
109
103
  (then (i32.const 0))
110
104
  (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}))))))`
105
+ (i32.eq (call $__ptr_type (i64.reinterpret_f64 (local.get $f))) (i32.const ${PTR.STRING})))))`
115
106
 
116
107
 
117
108
  // Default dynamic-property helpers are harmless stubs. The collection module
118
109
  // 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)
110
+ ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
111
+ (i64.const ${UNDEF_NAN}))`
112
+ ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj i64) (param $key i64) (param $fallback i64) (result i64)
122
113
  (local.get $fallback))`
123
- ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj f64) (param $key f64) (param $val f64) (result f64)
114
+ ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj i64) (param $key i64) (param $val i64) (result i64)
124
115
  (local.get $val))`
125
116
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32))`
126
117
 
@@ -130,22 +121,22 @@ export default (ctx) => {
130
121
 
131
122
  ctx.core.stdlib['__mkptr'] = `(func $__mkptr (param $type i32) (param $aux i32) (param $offset i32) (result f64)
132
123
  (f64.reinterpret_i64 (i64.or
133
- (i64.shl (i64.const ${NAN_PREFIX}) (i64.const 48))
124
+ (i64.const ${NAN_BITS})
134
125
  (i64.or
135
- (i64.shl (i64.and (i64.extend_i32_u (local.get $type)) (i64.const 0xF)) (i64.const 47))
126
+ (i64.shl (i64.and (i64.extend_i32_u (local.get $type)) (i64.const ${LAYOUT.TAG_MASK})) (i64.const ${LAYOUT.TAG_SHIFT}))
136
127
  (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)))))))`
128
+ (i64.shl (i64.and (i64.extend_i32_u (local.get $aux)) (i64.const ${LAYOUT.AUX_MASK})) (i64.const ${LAYOUT.AUX_SHIFT}))
129
+ (i64.and (i64.extend_i32_u (local.get $offset)) (i64.const ${LAYOUT.OFFSET_MASK})))))))`
139
130
 
140
- ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr f64) (result i32)
131
+ ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr i64) (result i32)
141
132
  (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))))
133
+ (local.set $bits (local.get $ptr))
134
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
144
135
  ;; Arrays can be reallocated during growth; follow forwarding pointer (cap=-1 sentinel).
145
136
  ;; Bounds are checked inside the loop so non-array ptrs skip them entirely, and well-formed
146
137
  ;; ARRAY ptrs without forwarding still pay only one bounds check before the cap load.
147
138
  (if (i32.eq
148
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
139
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
149
140
  (i32.const ${PTR.ARRAY}))
150
141
  (then
151
142
  (block $done
@@ -157,14 +148,19 @@ export default (ctx) => {
157
148
  (br $follow)))))
158
149
  (local.get $off))`
159
150
 
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))))`
151
+ ctx.core.stdlib['__ptr_aux'] = `(func $__ptr_aux (param $ptr i64) (result i32)
152
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))`
162
153
 
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))))`
154
+ ctx.core.stdlib['__ptr_type'] = `(func $__ptr_type (param $ptr i64) (result i32)
155
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))`
165
156
 
166
157
  // === Bump allocator ===
167
158
 
159
+ // Heap-base watermark: gates header-backed propsPtr fast paths so static-data
160
+ // OBJECT slots (offsets < heap base) don't misread arbitrary memory at off-16.
161
+ // Updated by optimizeModule() when data segment exceeds 1024 bytes.
162
+ ctx.scope.globals.set('__heap_start', '(global $__heap_start (mut i32) (i32.const 1024))')
163
+
168
164
  if (ctx.memory.shared) {
169
165
  // Shared memory: heap offset stored at memory[1020] (i32), just before heap start at 1024
170
166
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
@@ -172,7 +168,7 @@ export default (ctx) => {
172
168
  (local.set $ptr (i32.load (i32.const 1020)))
173
169
  (i32.store (i32.const 1020) (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
174
170
  (local.get $ptr))`
175
- ctx.core.stdlib['__reset'] = `(func $__reset
171
+ ctx.core.stdlib['__clear'] = `(func $__clear
176
172
  (i32.store (i32.const 1020) (i32.const 1024)))`
177
173
  } else {
178
174
  // Own memory: heap offset in a global, auto-grow when needed
@@ -189,7 +185,7 @@ export default (ctx) => {
189
185
  (i32.const -1)) (then (unreachable)))))
190
186
  (global.set $__heap (local.get $next))
191
187
  (local.get $ptr))`
192
- ctx.core.stdlib['__reset'] = `(func $__reset
188
+ ctx.core.stdlib['__clear'] = `(func $__clear
193
189
  (global.set $__heap (i32.const 1024)))`
194
190
  }
195
191
 
@@ -211,7 +207,7 @@ export default (ctx) => {
211
207
  (else (i32.shr_u (local.get $et) (i32.const 1)))))))`
212
208
 
213
209
  // 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)
210
+ ctx.core.stdlib['__typed_data'] = `(func $__typed_data (param $ptr i64) (result i32)
215
211
  (local $off i32)
216
212
  (local.set $off (call $__ptr_offset (local.get $ptr)))
217
213
  (if (result i32) (i32.and (call $__ptr_aux (local.get $ptr)) (i32.const 8))
@@ -220,11 +216,11 @@ export default (ctx) => {
220
216
 
221
217
  // Hot (~85M calls in watr self-host). Type/offset extraction inlined; forwarding
222
218
  // 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)
219
+ ctx.core.stdlib['__len'] = `(func $__len (param $ptr i64) (result i32)
224
220
  (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))))
221
+ (local.set $bits (local.get $ptr))
222
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
223
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
228
224
  ;; ARRAY fast path: follow forwarding inline, then load len at off-8.
229
225
  (if (result i32)
230
226
  (i32.and (i32.eq (local.get $t) (i32.const 1)) (i32.ge_u (local.get $off) (i32.const 8)))
@@ -248,16 +244,16 @@ export default (ctx) => {
248
244
  (then
249
245
  (if (result i32) (i32.eq (local.get $t) (i32.const 3))
250
246
  (then
251
- (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
247
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
252
248
  (if (result i32) (i32.and (local.get $aux) (i32.const 8))
253
249
  (then (i32.shr_u (i32.load (local.get $off))
254
250
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
255
251
  (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 8)))
256
- (call $__typed_shift (local.get $aux))))))
252
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
257
253
  (else (i32.load (i32.sub (local.get $off) (i32.const 8))))))
258
254
  (else (i32.const 0))))))`
259
255
 
260
- ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr f64) (result i32)
256
+ ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr i64) (result i32)
261
257
  (local $t i32) (local $off i32) (local $aux i32)
262
258
  (local.set $t (call $__ptr_type (local.get $ptr)))
263
259
  (local.set $off (call $__ptr_offset (local.get $ptr)))
@@ -279,28 +275,30 @@ export default (ctx) => {
279
275
  (then (i32.shr_u (i32.load (local.get $off))
280
276
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
281
277
  (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 4)))
282
- (call $__typed_shift (local.get $aux))))))
278
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
283
279
  (else (i32.load (i32.sub (local.get $off) (i32.const 4))))))
284
280
  (else (i32.const 0))))`
285
281
 
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)
282
+ // String length (UTF-8 byte count). Heap: [-4:len(i32)][chars...]; SSO: aux & 7.
283
+ ctx.core.stdlib['__str_len'] = `(func $__str_len (param $ptr i64) (result i32)
284
+ (local $off i32) (local $aux i32)
285
+ (if (i32.ne (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.STRING}))
286
+ (then (return (i32.const 0))))
287
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
288
+ (if (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT}))
289
+ (then (return (i32.and (local.get $aux) (i32.const 7)))))
289
290
  (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)))
291
+ (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
294
292
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
295
293
  (else (i32.const 0))))`
296
294
 
297
295
  // Set len in memory (for push/pop). Hot (~42M calls in watr self-host).
298
296
  // 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)
297
+ ctx.core.stdlib['__set_len'] = `(func $__set_len (param $ptr i64) (param $len i32)
300
298
  (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))))
299
+ (local.set $bits (local.get $ptr))
300
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
301
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
304
302
  ;; Only ARRAY (1), TYPED (3), HASH (7), SET (8), MAP (9) carry an 8-byte header.
305
303
  ;; Of those, only ARRAY can be forwarded — follow the chain inline.
306
304
  (if
@@ -322,20 +320,25 @@ export default (ctx) => {
322
320
  (br $follow)))))
323
321
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $len)))))`
324
322
 
325
- // Alloc header(8) + data(cap*stride), store len+cap, return data offset (past header)
323
+ // Alloc header(16) + data(cap*stride). Layout: [propsPtr@-16(f64=0), len@-8, cap@-4],
324
+ // data starts at returned offset. propsPtr at -16 holds a per-object dynamic-property hash
325
+ // (NaN-boxed PTR.HASH) for ARRAY/HASH/MAP/SET; 0 means "no dyn props yet". This lets
326
+ // __dyn_get_t / __dyn_set sidestep the global __dyn_props lookup on the hot path.
327
+ // Read offsets relative to the returned data ptr stay unchanged (-8 len, -4 cap).
326
328
  ctx.core.stdlib['__alloc_hdr'] = `(func $__alloc_hdr (param $len i32) (param $cap i32) (param $stride i32) (result i32)
327
329
  (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)))`
330
+ (local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.mul (local.get $cap) (local.get $stride)))))
331
+ (i64.store (local.get $ptr) (i64.const 0))
332
+ (i32.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $len))
333
+ (i32.store (i32.add (local.get $ptr) (i32.const 12)) (local.get $cap))
334
+ (i32.add (local.get $ptr) (i32.const 16)))`
332
335
 
333
336
  // Allocator + exports are deferred: only included when memory is actually needed.
334
337
  // 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.
338
+ // compile.js emits _alloc/_clear exports + memory section only when __alloc is in includes.
336
339
  ctx.core._allocRawFuncs = [
337
340
  '(func (export "_alloc") (param $bytes i32) (result i32) (call $__alloc (local.get $bytes)))',
338
- '(func (export "_reset") (call $__reset))',
341
+ '(func (export "_clear") (call $__clear))',
339
342
  ]
340
343
 
341
344
  // Not-nullish check: f64 WAT node is neither NULL_NAN nor UNDEF_NAN.
@@ -343,11 +346,12 @@ export default (ctx) => {
343
346
  // apply — otherwise this would always emit a __is_nullish call even for provable cases.
344
347
  const notNullish = v => ['i32.eqz', isNullish(v)]
345
348
 
346
- // Optional-chain wrapper: eval guard, if non-nullish emit access, else NULL_NAN.
349
+ // Optional-chain wrapper: eval guard, if non-nullish emit access, else `undefined`.
350
+ // Per spec, `null?.a` and `undefined?.a` both short-circuit to undefined, not null.
347
351
  const emitNullishGuarded = (guard, access) => typed(['if', ['result', 'f64'],
348
352
  notNullish(guard),
349
353
  ['then', access],
350
- ['else', ['f64.const', `nan:${NULL_NAN}`]]], 'f64')
354
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]], 'f64')
351
355
 
352
356
  // === Shared dispatch helpers ===
353
357
 
@@ -362,11 +366,11 @@ export default (ctx) => {
362
366
  return typed(['f64.convert_i32_s', ['i32.load', ['i32.sub', off, ['i32.const', 8]]]], 'f64')
363
367
  }
364
368
  if (vt === VAL.TYPED)
365
- return typed(['f64.convert_i32_s', ['call', '$__len', va]], 'f64')
369
+ return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', va]]], 'f64')
366
370
  // Known string → byteLen (handles SSO + heap)
367
371
  if (vt === VAL.STRING) {
368
372
  inc('__str_byteLen')
369
- return typed(['f64.convert_i32_s', ['call', '$__str_byteLen', va]], 'f64')
373
+ return typed(['f64.convert_i32_s', ['call', '$__str_byteLen', ['i64.reinterpret_f64', va]]], 'f64')
370
374
  }
371
375
  // Unknown → runtime dispatch via stdlib. Set/Map dispatch arms are pulled
372
376
  // only when user code actually constructs Set/Map (collection.js sets the
@@ -375,7 +379,7 @@ export default (ctx) => {
375
379
  // commonly passed from JS via jz.memory.* without an in-program constructor.
376
380
  inc('__length')
377
381
  ctx.features.typedarray = true
378
- return typed(['call', '$__length', va], 'f64')
382
+ return typed(['call', '$__length', ['i64.reinterpret_f64', va]], 'f64')
379
383
  }
380
384
 
381
385
  // Known-schema fields live in the object payload. Dynamic sidecars are only
@@ -389,8 +393,8 @@ export default (ctx) => {
389
393
 
390
394
  function emitHashGetLocalConst(base, key, prop) {
391
395
  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')
396
+ const receiver = asI64(base?.type ? base : typed(base, 'f64'))
397
+ return typed(['f64.reinterpret_i64', ['call', '$__hash_get_local_h', receiver, key, ['i32.const', strHashLiteral(prop)]]], 'f64')
394
398
  }
395
399
 
396
400
  function emitTypeTag(receiver, vt) {
@@ -402,20 +406,82 @@ export default (ctx) => {
402
406
 
403
407
  function emitDynGetExprTyped(base, key, vt) {
404
408
  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')
409
+ const receiver = asI64(base?.type ? base : typed(base, 'f64'))
410
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
407
411
  }
408
412
 
409
413
  function emitDynGetAnyTyped(base, key, vt) {
410
414
  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')
415
+ const receiver = asI64(base?.type ? base : typed(base, 'f64'))
416
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_any_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
417
+ }
418
+
419
+ // Walk an AST expression that may resolve to an OBJECT literal at compile
420
+ // time. Returns the literal `['{}', ...]` node, or null. Handles direct
421
+ // literals and `.prop` chains over them. Spread props are unsupported —
422
+ // they shift slot positions and would need their own resolution.
423
+ function literalAst(obj) {
424
+ if (Array.isArray(obj) && obj[0] === '{}') {
425
+ // Bail on spreads — they change effective slot ordering.
426
+ const props = obj.slice(1)
427
+ const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ','
428
+ ? props[0].slice(1) : props
429
+ for (const p of flat) if (Array.isArray(p) && p[0] === '...') return null
430
+ return obj
431
+ }
432
+ if (Array.isArray(obj) && obj[0] === '.' && typeof obj[2] === 'string') {
433
+ const inner = literalAst(obj[1])
434
+ if (!inner) return null
435
+ const innerProps = inner.slice(1)
436
+ const innerFlat = innerProps.length === 1 && Array.isArray(innerProps[0]) && innerProps[0][0] === ','
437
+ ? innerProps[0].slice(1) : innerProps
438
+ for (const p of innerFlat) {
439
+ if (Array.isArray(p) && p[0] === ':' && p[1] === obj[2]) return literalAst(p[2])
440
+ }
441
+ }
442
+ return null
443
+ }
444
+
445
+ // Slot index of `prop` within a literal-resolved expression, or -1.
446
+ function literalSlot(obj, prop) {
447
+ const lit = literalAst(obj)
448
+ if (!lit) return -1
449
+ const props = lit.slice(1)
450
+ const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ','
451
+ ? props[0].slice(1) : props
452
+ for (let i = 0; i < flat.length; i++) {
453
+ const p = flat[i]
454
+ if (Array.isArray(p) && p[0] === ':' && p[1] === prop) return i
455
+ }
456
+ return -1
413
457
  }
414
458
 
415
459
  /** Emit .prop access for a WASM f64 node using schema or HASH fallback. */
416
460
  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]))
461
+ // Anonymous-literal fast path: when `obj` resolves at compile time to an
462
+ // object literal `{...}` (either directly, or through a `.prop` chain
463
+ // walked back to one), use the literal's slot index instead of falling
464
+ // through to `__dyn_get_expr`. Fresh OBJECT literals carry no off-16
465
+ // propsPtr so the dispatcher reads NULL_NAN. The varName-bound path
466
+ // (`let o = {a:1}; o.a`) already works via `ctx.schema.idOf(varName)`;
467
+ // this extends the same shape resolution to `({a:1}).a` and chains like
468
+ // `({a:{b:1}}).a.b` where the receiver is anonymous. Spread sources
469
+ // (`{...x}`) shift slot ordering and would need their own resolution.
470
+ const slot = literalSlot(obj, prop)
471
+ if (slot >= 0) return emitSchemaSlotRead(asF64(va), slot)
472
+ let schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
473
+ // Chain receiver (e.g. `o.meta.bias`): when the chain resolves to a known
474
+ // OBJECT shape via JSON-shape propagation, the parent shape's `names`
475
+ // gives the slot directly. Avoids the structural ambiguity of
476
+ // ctx.schema.find(null, prop) when multiple registered schemas share a key.
477
+ if (schemaIdx < 0 && typeof obj !== 'string') {
478
+ const sh = shapeOf(obj)
479
+ if ((sh?.vt === VAL.OBJECT || sh?.vt === VAL.HASH) && sh.names) {
480
+ const i = sh.names.indexOf(prop)
481
+ if (i >= 0) schemaIdx = i
482
+ }
483
+ }
484
+ const key = asI64(emit(['str', prop]))
419
485
  if (schemaIdx >= 0) return emitSchemaSlotRead(asF64(va), schemaIdx)
420
486
  if (typeof obj === 'string') {
421
487
  const vt = lookupValType(obj)
@@ -425,12 +491,21 @@ export default (ctx) => {
425
491
  if (vt === VAL.HASH) {
426
492
  return emitHashGetLocalConst(va, key, prop)
427
493
  }
494
+ // OBJECT off-schema prop: __dyn_get_expr_t reads the per-OBJECT propsPtr
495
+ // at off-16 (set by __dyn_set). __hash_get assumes HASH bucket layout
496
+ // and would mis-read OBJECT memory.
497
+ if (vt === VAL.OBJECT) {
498
+ return emitDynGetExprTyped(va, key, vt)
499
+ }
428
500
  if (vt == null) {
501
+ // In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
502
+ // Skip the external branch and dispatch through the typed HASH/OBJECT path.
503
+ if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt)
429
504
  ctx.features.external = true
430
505
  return emitDynGetAnyTyped(va, key, vt)
431
506
  }
432
507
  inc('__hash_get', '__str_hash', '__str_eq')
433
- return typed(['call', '$__hash_get', asF64(va), key], 'f64')
508
+ return typed(['f64.reinterpret_i64', ['call', '$__hash_get', asI64(va), key]], 'f64')
434
509
  }
435
510
  // Non-string receiver: route through HASH fast path when valTypeOf can
436
511
  // resolve the chain to a known HASH (e.g. `o.meta.bias` where `o.meta` is
@@ -440,13 +515,13 @@ export default (ctx) => {
440
515
  return emitHashGetLocalConst(va, key, prop)
441
516
  }
442
517
  inc('__dyn_get_expr')
443
- return typed(['call', '$__dyn_get_expr', asF64(va), key], 'f64')
518
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', asI64(va), key]], 'f64')
444
519
  }
445
520
 
446
521
  // Runtime .length dispatch — factory elides branches for types that can't exist in
447
522
  // 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.
523
+ // number are always dispatched. The __len disjunction collapses to whichever of
524
+ // ARRAY/TYPED/HASH/SET/MAP are reachable. STRING covers both heap and SSO via __str_len.
450
525
  ctx.core.stdlib['__length'] = () => {
451
526
  const types = [PTR.ARRAY]
452
527
  if (ctx.features.typedarray) types.push(PTR.TYPED)
@@ -464,24 +539,17 @@ export default (ctx) => {
464
539
  (else (f64.const nan:${UNDEF_NAN}))))
465
540
  (else (f64.const nan:${UNDEF_NAN})))`
466
541
  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}))))
542
+ (then (f64.convert_i32_s (call $__str_len (local.get $v))))
471
543
  (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))
544
+ return `(func $__length (param $v i64) (result f64)
545
+ (local $f f64) (local $t i32) (local $off i32)
546
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
547
+ (if (result f64) (f64.eq (local.get $f) (local.get $f))
480
548
  (then (f64.const nan:${UNDEF_NAN}))
481
549
  (else
482
550
  (local.set $t (call $__ptr_type (local.get $v)))
483
551
  (local.set $off (call $__ptr_offset (local.get $v)))
484
- ${afterNumber})))`
552
+ ${stringArm})))`
485
553
  }
486
554
 
487
555
  // === Property dispatch (.length, .prop) ===
@@ -491,7 +559,7 @@ export default (ctx) => {
491
559
  if (typeof obj === 'string' && ctx.schema.isBoxed(obj)) {
492
560
  if (prop === 'length') {
493
561
  const inner = ctx.schema.emitInner(obj)
494
- return typed(['f64.convert_i32_s', ['call', '$__len', inner]], 'f64')
562
+ return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', inner]]], 'f64')
495
563
  }
496
564
  const idx = ctx.schema.find(obj, prop)
497
565
  if (idx >= 0) return emitSchemaSlotRead(asF64(emit(obj)), idx)
@@ -518,6 +586,12 @@ export default (ctx) => {
518
586
  return typed(['f64.convert_i32_s', lenI32], 'f64')
519
587
  }
520
588
  }
589
+ // String literal: fold to its UTF-8 byte length. jz strings are stored as
590
+ // UTF-8 and __str_byteLen returns byte count, so this matches the runtime
591
+ // semantics. Skips the call + NaN-unbox round-trip entirely.
592
+ if (Array.isArray(obj) && (obj[0] === 'str' || obj[0] == null) && typeof obj[1] === 'string') {
593
+ return typed(['f64.const', Buffer.byteLength(obj[1], 'utf8')], 'f64')
594
+ }
521
595
  const vt = typeof obj === 'string' ? repOf(obj)?.val : valTypeOf(obj)
522
596
  return emitLengthAccess(asF64(emit(obj)), vt)
523
597
  }
@@ -544,21 +618,25 @@ export default (ctx) => {
544
618
  if (typeof obj === 'string') {
545
619
  const objType = lookupValType(obj)
546
620
  if (usesDynProps(objType)) {
547
- access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), objType)
621
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
548
622
  } else if (objType === VAL.HASH) {
549
- access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
623
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
550
624
  } else if (objType == null) {
551
- ctx.features.external = true
552
- access = emitDynGetAnyTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), objType)
625
+ // Unknown receiver — in WASI mode use typed dispatch (no PTR.EXTERNAL values).
626
+ // In JS host mode use __dyn_get_any_t but don't force features.external here
627
+ // since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
628
+ access = ctx.transform.host === 'wasi'
629
+ ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
630
+ : emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
553
631
  } else {
554
632
  inc('__hash_get', '__str_hash', '__str_eq')
555
- access = ['call', '$__hash_get', ['local.get', `$${t}`], asF64(emit(['str', prop]))]
633
+ access = ['f64.reinterpret_i64', ['call', '$__hash_get', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI64(emit(['str', prop]))]]
556
634
  }
557
635
  } else {
558
636
  if (valTypeOf(obj) === VAL.HASH) {
559
- access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
637
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
560
638
  } else {
561
- access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), valTypeOf(obj))
639
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj))
562
640
  }
563
641
  }
564
642
  }
@@ -583,6 +661,22 @@ export default (ctx) => {
583
661
 
584
662
  // Optional call: fn?.(...args) → null if fn is null, else call fn
585
663
  ctx.core.emit['?.()'] = (callee, ...args) => {
664
+ // Method-reference callee: `recv.m(...)` or `recv?.m(...)` form. Methods are
665
+ // statically registered emitters and aren't real closure values, so route them
666
+ // as a direct method call. The outer optional short-circuits when the receiver
667
+ // is nullish — the method itself is statically known to exist.
668
+ if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') && typeof callee[2] === 'string') {
669
+ const methodEmit = ctx.core.emit[`.${callee[2]}`]
670
+ if (methodEmit) {
671
+ const recv = callee[1]
672
+ const t = temp()
673
+ const va = asF64(emit(recv))
674
+ const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
675
+ if (vt) updateRep(t, { val: vt })
676
+ const callResult = methodEmit(t, ...args)
677
+ return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
678
+ }
679
+ }
586
680
  const t = temp()
587
681
  const va = asF64(emit(callee))
588
682
  // If nullish → return NULL_NAN, else call via fn.call
@@ -596,6 +690,7 @@ export default (ctx) => {
596
690
  // initialized in __start (see compile.js). Comparison patterns (typeof x === 'string') are optimized
597
691
  // in prepare.js (resolveTypeof) and emitted as direct type checks via emitTypeofCmp, bypassing this path.
598
692
  ctx.core.emit['typeof'] = (a) => {
693
+ if (valTypeOf(a) === VAL.BIGINT) return emit(['str', 'bigint'])
599
694
  if (!ctx.runtime.typeofStrs) {
600
695
  ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
601
696
  for (const s of ctx.runtime.typeofStrs)
@@ -604,20 +699,27 @@ export default (ctx) => {
604
699
  inc('__typeof')
605
700
  // Receiver type unknown; enable branches that wouldn't otherwise be reachable.
606
701
  ctx.features.closure = true
607
- return typed(['call', '$__typeof', asF64(emit(a))], 'f64')
702
+ return typed(['call', '$__typeof', asI64(emit(a))], 'f64')
608
703
  }
609
704
 
610
705
  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}))`
706
+ const stringTest = `(i32.eq (local.get $t) (i32.const ${PTR.STRING}))`
614
707
  const closureArm = ctx.features.closure
615
708
  ? `(if (i32.eq (local.get $t) (i32.const ${PTR.CLOSURE}))
616
709
  (then (return (global.get $__tof_function))))`
617
710
  : ''
618
- return `(func $__typeof (param $v f64) (result f64)
619
- (local $t i32)
620
- (if (f64.eq (local.get $v) (local.get $v))
711
+ return `(func $__typeof (param $v i64) (result f64)
712
+ (local $f f64) (local $t i32)
713
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
714
+ (if (f64.eq (local.get $f) (local.get $f))
715
+ (then (return (global.get $__tof_number))))
716
+ ;; Canonical JS NaN (0x7FF8000000000000) overlaps ATOM tag=0 aux=0 offset=0.
717
+ ;; That bit pattern is the math NaN value, not a tagged pointer — treat as "number".
718
+ ;; Negative-NaN bit patterns (sign bit set) don't match NAN_PREFIX so are uniquely numeric.
719
+ (if (i32.or
720
+ (i64.eq (local.get $v) (i64.const 0x7FF8000000000000))
721
+ (i64.eq (i64.and (local.get $v) (i64.const 0xFFF0000000000000))
722
+ (i64.const 0xFFF0000000000000)))
621
723
  (then (return (global.get $__tof_number))))
622
724
  (if (call $__is_nullish (local.get $v))
623
725
  (then (return (global.get $__tof_undefined))))
@@ -635,9 +737,9 @@ export default (ctx) => {
635
737
 
636
738
  // Low-level pointer helpers callable from jz code
637
739
  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')
740
+ ctx.core.emit['__ptr_type'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_type', asI64(emit(p))]], 'f64')
741
+ ctx.core.emit['__ptr_aux'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asI64(emit(p))]], 'f64')
742
+ ctx.core.emit['__ptr_offset'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asI64(emit(p))]], 'f64')
641
743
 
642
744
  // Error(msg) — passthrough (throw handles any value)
643
745
  ctx.core.emit['Error'] = (msg) => asF64(emit(msg))