jz 0.1.0 → 0.2.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 CHANGED
@@ -2,19 +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 { 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'
12
+ import { typed, asF64, asI32, asI64, 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, shapeOf } from '../src/analyze.js'
15
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
14
16
  import { initSchema } from './schema.js'
15
17
  import { strHashLiteral } from './collection.js'
16
18
 
17
- 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')
18
21
 
19
22
  const PTR_BY_VAL = {
20
23
  [VAL.ARRAY]: PTR.ARRAY,
@@ -34,91 +37,81 @@ export default (ctx) => {
34
37
  __typed_data: ['__ptr_offset', '__ptr_aux'],
35
38
  __ptr_offset: [],
36
39
  __is_str_key: ['__ptr_type'],
37
- __str_len: ['__ptr_type', '__ptr_offset'],
40
+ __str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
38
41
  __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
- },
42
+ __length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
44
43
  __typeof: ['__ptr_type', '__is_nullish'],
45
44
  __alloc_hdr: ['__alloc'],
46
45
  })
47
46
 
48
- 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)
49
48
  (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}))))`
49
+ (i64.eq (local.get $v) (i64.const ${NULL_NAN}))
50
+ (i64.eq (local.get $v) (i64.const ${UNDEF_NAN}))))`
52
51
 
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)
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)
55
54
  ;; Fast path: bit equality covers identical pointers AND interned/SSO strings (same content
56
55
  ;; → same bits). Failing universal-NaN test catches NaN===NaN→false. Saves the NaN-check
57
56
  ;; 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)))
57
+ (if (result i32) (i64.eq (local.get $a) (local.get $b))
58
+ (then (i64.ne (local.get $a) (i64.const ${NAN_BITS})))
62
59
  (else
63
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)))
64
63
  (if (result i32)
65
64
  (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)))
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)))
69
68
  (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))))
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}))))
74
73
  (if (result i32)
75
74
  (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}))))
75
+ (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
76
+ (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
82
77
  (then (call $__str_eq (local.get $a) (local.get $b)))
83
78
  (else (i32.const 0))))))))`
84
79
 
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})))`
80
+ ctx.core.stdlib['__is_null'] = `(func $__is_null (param $v i64) (result i32)
81
+ (i64.eq (local.get $v) (i64.const ${NULL_NAN})))`
87
82
 
88
83
  // Truthy check: handles regular numbers AND NaN-boxed pointers
89
84
  // 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)))
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)))
94
90
  (else
95
- (local.set $bits (i64.reinterpret_f64 (local.get $v)))
96
91
  (i32.and
97
92
  (i32.and
98
- (i64.ne (local.get $bits) (i64.const 0x7FF8000000000000))
99
- (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})))
100
95
  (i32.and
101
- (i64.ne (local.get $bits) (i64.const ${UNDEF_NAN}))
102
- (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)))))))`
103
98
 
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))
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))
107
103
  (then (i32.const 0))
108
104
  (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}))))))`
105
+ (i32.eq (call $__ptr_type (i64.reinterpret_f64 (local.get $f))) (i32.const ${PTR.STRING})))))`
113
106
 
114
107
 
115
108
  // Default dynamic-property helpers are harmless stubs. The collection module
116
109
  // 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)
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)
120
113
  (local.get $fallback))`
121
- 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)
122
115
  (local.get $val))`
123
116
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32))`
124
117
 
@@ -128,22 +121,22 @@ export default (ctx) => {
128
121
 
129
122
  ctx.core.stdlib['__mkptr'] = `(func $__mkptr (param $type i32) (param $aux i32) (param $offset i32) (result f64)
130
123
  (f64.reinterpret_i64 (i64.or
131
- (i64.shl (i64.const ${NAN_PREFIX}) (i64.const 48))
124
+ (i64.const ${NAN_BITS})
132
125
  (i64.or
133
- (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}))
134
127
  (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)))))))`
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})))))))`
137
130
 
138
- 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)
139
132
  (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))))
133
+ (local.set $bits (local.get $ptr))
134
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
142
135
  ;; Arrays can be reallocated during growth; follow forwarding pointer (cap=-1 sentinel).
143
136
  ;; Bounds are checked inside the loop so non-array ptrs skip them entirely, and well-formed
144
137
  ;; ARRAY ptrs without forwarding still pay only one bounds check before the cap load.
145
138
  (if (i32.eq
146
- (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})))
147
140
  (i32.const ${PTR.ARRAY}))
148
141
  (then
149
142
  (block $done
@@ -155,14 +148,19 @@ export default (ctx) => {
155
148
  (br $follow)))))
156
149
  (local.get $off))`
157
150
 
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))))`
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}))))`
160
153
 
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))))`
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}))))`
163
156
 
164
157
  // === Bump allocator ===
165
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
+
166
164
  if (ctx.memory.shared) {
167
165
  // Shared memory: heap offset stored at memory[1020] (i32), just before heap start at 1024
168
166
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
@@ -170,7 +168,7 @@ export default (ctx) => {
170
168
  (local.set $ptr (i32.load (i32.const 1020)))
171
169
  (i32.store (i32.const 1020) (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
172
170
  (local.get $ptr))`
173
- ctx.core.stdlib['__reset'] = `(func $__reset
171
+ ctx.core.stdlib['__clear'] = `(func $__clear
174
172
  (i32.store (i32.const 1020) (i32.const 1024)))`
175
173
  } else {
176
174
  // Own memory: heap offset in a global, auto-grow when needed
@@ -187,7 +185,7 @@ export default (ctx) => {
187
185
  (i32.const -1)) (then (unreachable)))))
188
186
  (global.set $__heap (local.get $next))
189
187
  (local.get $ptr))`
190
- ctx.core.stdlib['__reset'] = `(func $__reset
188
+ ctx.core.stdlib['__clear'] = `(func $__clear
191
189
  (global.set $__heap (i32.const 1024)))`
192
190
  }
193
191
 
@@ -209,7 +207,7 @@ export default (ctx) => {
209
207
  (else (i32.shr_u (local.get $et) (i32.const 1)))))))`
210
208
 
211
209
  // 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)
210
+ ctx.core.stdlib['__typed_data'] = `(func $__typed_data (param $ptr i64) (result i32)
213
211
  (local $off i32)
214
212
  (local.set $off (call $__ptr_offset (local.get $ptr)))
215
213
  (if (result i32) (i32.and (call $__ptr_aux (local.get $ptr)) (i32.const 8))
@@ -218,11 +216,11 @@ export default (ctx) => {
218
216
 
219
217
  // Hot (~85M calls in watr self-host). Type/offset extraction inlined; forwarding
220
218
  // 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)
219
+ ctx.core.stdlib['__len'] = `(func $__len (param $ptr i64) (result i32)
222
220
  (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))))
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}))))
226
224
  ;; ARRAY fast path: follow forwarding inline, then load len at off-8.
227
225
  (if (result i32)
228
226
  (i32.and (i32.eq (local.get $t) (i32.const 1)) (i32.ge_u (local.get $off) (i32.const 8)))
@@ -246,16 +244,16 @@ export default (ctx) => {
246
244
  (then
247
245
  (if (result i32) (i32.eq (local.get $t) (i32.const 3))
248
246
  (then
249
- (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}))))
250
248
  (if (result i32) (i32.and (local.get $aux) (i32.const 8))
251
249
  (then (i32.shr_u (i32.load (local.get $off))
252
250
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
253
251
  (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 8)))
254
- (call $__typed_shift (local.get $aux))))))
252
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
255
253
  (else (i32.load (i32.sub (local.get $off) (i32.const 8))))))
256
254
  (else (i32.const 0))))))`
257
255
 
258
- ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr f64) (result i32)
256
+ ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr i64) (result i32)
259
257
  (local $t i32) (local $off i32) (local $aux i32)
260
258
  (local.set $t (call $__ptr_type (local.get $ptr)))
261
259
  (local.set $off (call $__ptr_offset (local.get $ptr)))
@@ -277,28 +275,30 @@ export default (ctx) => {
277
275
  (then (i32.shr_u (i32.load (local.get $off))
278
276
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
279
277
  (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 4)))
280
- (call $__typed_shift (local.get $aux))))))
278
+ (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
281
279
  (else (i32.load (i32.sub (local.get $off) (i32.const 4))))))
282
280
  (else (i32.const 0))))`
283
281
 
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)
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)))))
287
290
  (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)))
291
+ (if (result i32) (i32.ge_u (local.get $off) (i32.const 4))
292
292
  (then (i32.load (i32.sub (local.get $off) (i32.const 4))))
293
293
  (else (i32.const 0))))`
294
294
 
295
295
  // Set len in memory (for push/pop). Hot (~42M calls in watr self-host).
296
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)
297
+ ctx.core.stdlib['__set_len'] = `(func $__set_len (param $ptr i64) (param $len i32)
298
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))))
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}))))
302
302
  ;; Only ARRAY (1), TYPED (3), HASH (7), SET (8), MAP (9) carry an 8-byte header.
303
303
  ;; Of those, only ARRAY can be forwarded — follow the chain inline.
304
304
  (if
@@ -320,20 +320,25 @@ export default (ctx) => {
320
320
  (br $follow)))))
321
321
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $len)))))`
322
322
 
323
- // 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).
324
328
  ctx.core.stdlib['__alloc_hdr'] = `(func $__alloc_hdr (param $len i32) (param $cap i32) (param $stride i32) (result i32)
325
329
  (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
+ (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)))`
330
335
 
331
336
  // Allocator + exports are deferred: only included when memory is actually needed.
332
337
  // 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.
338
+ // compile.js emits _alloc/_clear exports + memory section only when __alloc is in includes.
334
339
  ctx.core._allocRawFuncs = [
335
340
  '(func (export "_alloc") (param $bytes i32) (result i32) (call $__alloc (local.get $bytes)))',
336
- '(func (export "_reset") (call $__reset))',
341
+ '(func (export "_clear") (call $__clear))',
337
342
  ]
338
343
 
339
344
  // Not-nullish check: f64 WAT node is neither NULL_NAN nor UNDEF_NAN.
@@ -341,11 +346,12 @@ export default (ctx) => {
341
346
  // apply — otherwise this would always emit a __is_nullish call even for provable cases.
342
347
  const notNullish = v => ['i32.eqz', isNullish(v)]
343
348
 
344
- // 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.
345
351
  const emitNullishGuarded = (guard, access) => typed(['if', ['result', 'f64'],
346
352
  notNullish(guard),
347
353
  ['then', access],
348
- ['else', ['f64.const', `nan:${NULL_NAN}`]]], 'f64')
354
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]], 'f64')
349
355
 
350
356
  // === Shared dispatch helpers ===
351
357
 
@@ -360,11 +366,11 @@ export default (ctx) => {
360
366
  return typed(['f64.convert_i32_s', ['i32.load', ['i32.sub', off, ['i32.const', 8]]]], 'f64')
361
367
  }
362
368
  if (vt === VAL.TYPED)
363
- return typed(['f64.convert_i32_s', ['call', '$__len', va]], 'f64')
369
+ return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', va]]], 'f64')
364
370
  // Known string → byteLen (handles SSO + heap)
365
371
  if (vt === VAL.STRING) {
366
372
  inc('__str_byteLen')
367
- 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')
368
374
  }
369
375
  // Unknown → runtime dispatch via stdlib. Set/Map dispatch arms are pulled
370
376
  // only when user code actually constructs Set/Map (collection.js sets the
@@ -373,7 +379,7 @@ export default (ctx) => {
373
379
  // commonly passed from JS via jz.memory.* without an in-program constructor.
374
380
  inc('__length')
375
381
  ctx.features.typedarray = true
376
- return typed(['call', '$__length', va], 'f64')
382
+ return typed(['call', '$__length', ['i64.reinterpret_f64', va]], 'f64')
377
383
  }
378
384
 
379
385
  // Known-schema fields live in the object payload. Dynamic sidecars are only
@@ -387,8 +393,8 @@ export default (ctx) => {
387
393
 
388
394
  function emitHashGetLocalConst(base, key, prop) {
389
395
  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')
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')
392
398
  }
393
399
 
394
400
  function emitTypeTag(receiver, vt) {
@@ -400,20 +406,82 @@ export default (ctx) => {
400
406
 
401
407
  function emitDynGetExprTyped(base, key, vt) {
402
408
  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')
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')
405
411
  }
406
412
 
407
413
  function emitDynGetAnyTyped(base, key, vt) {
408
414
  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')
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
411
457
  }
412
458
 
413
459
  /** Emit .prop access for a WASM f64 node using schema or HASH fallback. */
414
460
  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]))
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]))
417
485
  if (schemaIdx >= 0) return emitSchemaSlotRead(asF64(va), schemaIdx)
418
486
  if (typeof obj === 'string') {
419
487
  const vt = lookupValType(obj)
@@ -423,12 +491,21 @@ export default (ctx) => {
423
491
  if (vt === VAL.HASH) {
424
492
  return emitHashGetLocalConst(va, key, prop)
425
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
+ }
426
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)
427
504
  ctx.features.external = true
428
505
  return emitDynGetAnyTyped(va, key, vt)
429
506
  }
430
507
  inc('__hash_get', '__str_hash', '__str_eq')
431
- return typed(['call', '$__hash_get', asF64(va), key], 'f64')
508
+ return typed(['f64.reinterpret_i64', ['call', '$__hash_get', asI64(va), key]], 'f64')
432
509
  }
433
510
  // Non-string receiver: route through HASH fast path when valTypeOf can
434
511
  // resolve the chain to a known HASH (e.g. `o.meta.bias` where `o.meta` is
@@ -438,13 +515,13 @@ export default (ctx) => {
438
515
  return emitHashGetLocalConst(va, key, prop)
439
516
  }
440
517
  inc('__dyn_get_expr')
441
- return typed(['call', '$__dyn_get_expr', asF64(va), key], 'f64')
518
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', asI64(va), key]], 'f64')
442
519
  }
443
520
 
444
521
  // Runtime .length dispatch — factory elides branches for types that can't exist in
445
522
  // 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.
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.
448
525
  ctx.core.stdlib['__length'] = () => {
449
526
  const types = [PTR.ARRAY]
450
527
  if (ctx.features.typedarray) types.push(PTR.TYPED)
@@ -462,24 +539,17 @@ export default (ctx) => {
462
539
  (else (f64.const nan:${UNDEF_NAN}))))
463
540
  (else (f64.const nan:${UNDEF_NAN})))`
464
541
  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}))))
542
+ (then (f64.convert_i32_s (call $__str_len (local.get $v))))
469
543
  (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))
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))
478
548
  (then (f64.const nan:${UNDEF_NAN}))
479
549
  (else
480
550
  (local.set $t (call $__ptr_type (local.get $v)))
481
551
  (local.set $off (call $__ptr_offset (local.get $v)))
482
- ${afterNumber})))`
552
+ ${stringArm})))`
483
553
  }
484
554
 
485
555
  // === Property dispatch (.length, .prop) ===
@@ -489,7 +559,7 @@ export default (ctx) => {
489
559
  if (typeof obj === 'string' && ctx.schema.isBoxed(obj)) {
490
560
  if (prop === 'length') {
491
561
  const inner = ctx.schema.emitInner(obj)
492
- return typed(['f64.convert_i32_s', ['call', '$__len', inner]], 'f64')
562
+ return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', inner]]], 'f64')
493
563
  }
494
564
  const idx = ctx.schema.find(obj, prop)
495
565
  if (idx >= 0) return emitSchemaSlotRead(asF64(emit(obj)), idx)
@@ -516,6 +586,12 @@ export default (ctx) => {
516
586
  return typed(['f64.convert_i32_s', lenI32], 'f64')
517
587
  }
518
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
+ }
519
595
  const vt = typeof obj === 'string' ? repOf(obj)?.val : valTypeOf(obj)
520
596
  return emitLengthAccess(asF64(emit(obj)), vt)
521
597
  }
@@ -542,21 +618,25 @@ export default (ctx) => {
542
618
  if (typeof obj === 'string') {
543
619
  const objType = lookupValType(obj)
544
620
  if (usesDynProps(objType)) {
545
- access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), objType)
621
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
546
622
  } else if (objType === VAL.HASH) {
547
- access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
623
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
548
624
  } else if (objType == null) {
549
- ctx.features.external = true
550
- 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)
551
631
  } else {
552
632
  inc('__hash_get', '__str_hash', '__str_eq')
553
- 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]))]]
554
634
  }
555
635
  } else {
556
636
  if (valTypeOf(obj) === VAL.HASH) {
557
- access = emitHashGetLocalConst(['local.get', `$${t}`], asF64(emit(['str', prop])), prop)
637
+ access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
558
638
  } else {
559
- access = emitDynGetExprTyped(['local.get', `$${t}`], asF64(emit(['str', prop])), valTypeOf(obj))
639
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj))
560
640
  }
561
641
  }
562
642
  }
@@ -581,6 +661,22 @@ export default (ctx) => {
581
661
 
582
662
  // Optional call: fn?.(...args) → null if fn is null, else call fn
583
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
+ }
584
680
  const t = temp()
585
681
  const va = asF64(emit(callee))
586
682
  // If nullish → return NULL_NAN, else call via fn.call
@@ -594,6 +690,7 @@ export default (ctx) => {
594
690
  // initialized in __start (see compile.js). Comparison patterns (typeof x === 'string') are optimized
595
691
  // in prepare.js (resolveTypeof) and emitted as direct type checks via emitTypeofCmp, bypassing this path.
596
692
  ctx.core.emit['typeof'] = (a) => {
693
+ if (valTypeOf(a) === VAL.BIGINT) return emit(['str', 'bigint'])
597
694
  if (!ctx.runtime.typeofStrs) {
598
695
  ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
599
696
  for (const s of ctx.runtime.typeofStrs)
@@ -602,20 +699,27 @@ export default (ctx) => {
602
699
  inc('__typeof')
603
700
  // Receiver type unknown; enable branches that wouldn't otherwise be reachable.
604
701
  ctx.features.closure = true
605
- return typed(['call', '$__typeof', asF64(emit(a))], 'f64')
702
+ return typed(['call', '$__typeof', asI64(emit(a))], 'f64')
606
703
  }
607
704
 
608
705
  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}))`
706
+ const stringTest = `(i32.eq (local.get $t) (i32.const ${PTR.STRING}))`
612
707
  const closureArm = ctx.features.closure
613
708
  ? `(if (i32.eq (local.get $t) (i32.const ${PTR.CLOSURE}))
614
709
  (then (return (global.get $__tof_function))))`
615
710
  : ''
616
- return `(func $__typeof (param $v f64) (result f64)
617
- (local $t i32)
618
- (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)))
619
723
  (then (return (global.get $__tof_number))))
620
724
  (if (call $__is_nullish (local.get $v))
621
725
  (then (return (global.get $__tof_undefined))))
@@ -633,9 +737,9 @@ export default (ctx) => {
633
737
 
634
738
  // Low-level pointer helpers callable from jz code
635
739
  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')
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')
639
743
 
640
744
  // Error(msg) — passthrough (throw handles any value)
641
745
  ctx.core.emit['Error'] = (msg) => asF64(emit(msg))