jz 0.5.1 → 0.7.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.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/core.js CHANGED
@@ -9,32 +9,38 @@
9
9
  * @module core
10
10
  */
11
11
 
12
- import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr } from '../src/ir.js'
13
- import { emit, buildArrayWithSpreads } from '../src/emit.js'
12
+ import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, FALSE_NAN, TRUE_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr, sidecarOverride, undefExpr } from '../src/ir.js'
13
+ import { emit, spread, deps } from '../src/bridge.js'
14
14
  import { reconstructArgsWithSpreads } from '../src/ir.js'
15
- import { valTypeOf, lookupValType, lookupNotString, VAL, T, repOf, updateRep, shapeOf, inlineArraySid } from '../src/analyze.js'
16
- import { ctx, err, inc, PTR, LAYOUT } from '../src/ctx.js'
15
+ import { valTypeOf, shapeOf } from '../src/kind.js'
16
+ import { T } from '../src/ast.js'
17
+ import { inlineArraySid } from '../src/static.js'
18
+ import { VAL, lookupValType, lookupNotString, repOf, updateRep } from '../src/reps.js'
19
+ import { ctx, err, inc, PTR, LAYOUT, HEAP, FORWARDING_MASK, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
20
+ import { ptrOffsetFwdWat, STR_INTERN_BIT } from '../layout.js'
21
+ import { nanPrefixHex } from '../layout.js'
17
22
  import { initSchema } from './schema.js'
18
23
  import { strHashLiteral } from './collection.js'
19
24
 
20
- // Pre-shifted NaN prefix as a full i64 mask, for `(i64.const ${NAN_BITS})` use.
21
- const NAN_BITS = '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
25
+ const NAN_BITS = nanPrefixHex()
22
26
 
23
27
  export default (ctx) => {
24
- Object.assign(ctx.core.stdlibDeps, {
28
+ deps({
25
29
  __eq: ['__str_eq', '__ptr_type'],
26
30
  __typeof: ['__ptr_type', '__is_nullish'],
27
- __len: ['__typed_shift'],
31
+ __len: ['__typed_shift', '__ptr_offset', '__ptr_offset_fwd'],
28
32
  __cap: ['__typed_shift', '__ptr_type', '__ptr_offset', '__ptr_aux'],
29
33
  __typed_data: ['__ptr_offset', '__ptr_aux'],
30
- __ptr_offset: [],
34
+ __ptr_offset: ['__ptr_offset_fwd'],
35
+ __ptr_offset_fwd: [],
31
36
  __is_str_key: ['__ptr_type'],
32
37
  __str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
33
- __set_len: [],
38
+ __set_len: ['__ptr_offset_fwd'],
34
39
  __length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
35
- __typeof: ['__ptr_type', '__is_nullish'],
40
+ __alloc: ['__memgrow'],
36
41
  __alloc_hdr: ['__alloc'],
37
42
  __alloc_hdr_n: ['__alloc'],
43
+ __coll_order: ['__alloc'],
38
44
  })
39
45
 
40
46
  ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
@@ -62,15 +68,26 @@ export default (ctx) => {
62
68
  (f64.eq (local.get $fb) (local.get $fb)))
63
69
  (then (f64.eq (local.get $fa) (local.get $fb)))
64
70
  (else
65
- ;; At least one operand is a NaN-box. Both STRING (heap or SSO) __str_eq
66
- ;; handles content compare and SSO fast-fail internally.
71
+ ;; At least one operand is a NaN-box (the && above failed). For both to
72
+ ;; be strings BOTH must be NaN-boxed: tag bits are only meaningful on a
73
+ ;; NaN-box, so a normal number whose exponent bits happen to alias the
74
+ ;; STRING tag (e.g. ASCII content read as f64) must NOT route to __str_eq
75
+ ;; — that would deref garbage. number-vs-string is simply false.
67
76
  (local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
68
77
  (local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
69
78
  (if (result i32)
70
79
  (i32.and
71
- (i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
72
- (i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
73
- (then (call $__str_eq (local.get $a) (local.get $b)))
80
+ (i32.and (f64.ne (local.get $fa) (local.get $fa)) (i32.eq (local.get $ta) (i32.const ${PTR.STRING})))
81
+ (i32.and (f64.ne (local.get $fb) (local.get $fb)) (i32.eq (local.get $tb) (i32.const ${PTR.STRING}))))
82
+ (then
83
+ ;; both canonical interned (bit-ne already known) ⇒ unequal —
84
+ ;; skip the __str_eq call entirely (see STR_INTERN_BIT, layout.js)
85
+ (if (result i32)
86
+ (i32.and
87
+ (i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))
88
+ (i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT})))
89
+ (then (i32.const 0))
90
+ (else (call $__str_eq (local.get $a) (local.get $b)))))
74
91
  (else (i32.const 0))))))))`
75
92
 
76
93
  ctx.core.stdlib['__is_null'] = `(func $__is_null (param $v i64) (result i32)
@@ -86,11 +103,13 @@ export default (ctx) => {
86
103
  (else
87
104
  (i32.and
88
105
  (i32.and
89
- (i64.ne (local.get $v) (i64.const ${NAN_BITS}))
90
- (i64.ne (local.get $v) (i64.const ${NULL_NAN})))
91
- (i32.and
92
- (i64.ne (local.get $v) (i64.const ${UNDEF_NAN}))
93
- (i64.ne (local.get $v) (i64.const 0x7FFA400000000000)))))))`
106
+ (i32.and
107
+ (i64.ne (local.get $v) (i64.const ${NAN_BITS}))
108
+ (i64.ne (local.get $v) (i64.const ${NULL_NAN})))
109
+ (i32.and
110
+ (i64.ne (local.get $v) (i64.const ${UNDEF_NAN}))
111
+ (i64.ne (local.get $v) (i64.const 0x7FFA400000000000))))
112
+ (i64.ne (local.get $v) (i64.const ${FALSE_NAN}))))))`
94
113
 
95
114
  ctx.core.stdlib['__is_str_key'] = `(func $__is_str_key (param $v i64) (result i32)
96
115
  (local $f f64)
@@ -124,29 +143,54 @@ export default (ctx) => {
124
143
  (i64.shl (i64.and (i64.extend_i32_u (local.get $aux)) (i64.const ${LAYOUT.AUX_MASK})) (i64.const ${LAYOUT.AUX_SHIFT}))
125
144
  (i64.and (i64.extend_i32_u (local.get $offset)) (i64.const ${LAYOUT.OFFSET_MASK})))))))`
126
145
 
146
+ ctx.core.stdlib['__ptr_offset_fwd'] = ptrOffsetFwdWat()
147
+
127
148
  ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr i64) (result i32)
128
- (local $bits i64) (local $off i32)
149
+ (local $bits i64) (local $off i32) (local $t i32)
129
150
  (local.set $bits (local.get $ptr))
130
151
  (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
131
- ;; Arrays can be reallocated during growth; follow forwarding pointer (cap=-1 sentinel).
132
- ;; Bounds are checked inside the loop so non-array ptrs skip them entirely, and well-formed
133
- ;; ARRAY ptrs without forwarding still pay only one bounds check before the cap load.
134
- (if (i32.eq
135
- (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
136
- (i32.const ${PTR.ARRAY}))
152
+ ;; ARRAY/SET/MAP/HASH can be reallocated on growth; follow the forwarding pointer
153
+ ;; (cap=-1 sentinel at -4, new offset at -8). Other types never forward, so they skip
154
+ ;; the loop; a well-formed ptr without forwarding pays one bounds + cap check per hop.
155
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
156
+ (if (i32.and (i32.shl (i32.const 1) (local.get $t)) (i32.const ${FORWARDING_MASK}))
137
157
  (then
138
- (block $done
139
- (loop $follow
140
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
141
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
142
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
143
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
144
- (br $follow)))))
158
+ ${followForwardingWat('$off', { lowGuard: true })}))
145
159
  (local.get $off))`
146
160
 
147
161
  ctx.core.stdlib['__ptr_aux'] = `(func $__ptr_aux (param $ptr i64) (result i32)
148
162
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))`
149
163
 
164
+ // Exact JS `%` (fmod) for the f64 path. wasm has no f64 remainder, and the
165
+ // textbook `a - b*trunc(a/b)` is both INEXACT (rounding in trunc/mul/sub for
166
+ // large a/b) and WRONG on the IEEE edges. This does the spec exactly:
167
+ // NaN if a or b is NaN, a is ±Inf, or b is 0; a if b is ±Inf or |a|<|b|;
168
+ // otherwise binary long division — scale |b| up to ≤|a|, then subtract-and-
169
+ // halve back down to |b|. Every step (×2, ×0.5, aligned subtraction) is
170
+ // exact in f64, so the remainder is bit-identical to JS. Sign follows the
171
+ // dividend (copysign), matching `(-5)%3 === -2`, `5%(-3) === 2`, `-0%3 === -0`.
172
+ ctx.core.stdlib['__rem'] = `(func $__rem (param $a f64) (param $b f64) (result f64)
173
+ (local $x f64) (local $y f64)
174
+ (if (f64.ne (local.get $a) (local.get $a)) (then (return (local.get $a))))
175
+ (if (f64.ne (local.get $b) (local.get $b)) (then (return (local.get $b))))
176
+ (local.set $x (f64.abs (local.get $a)))
177
+ (local.set $y (f64.abs (local.get $b)))
178
+ (if (i32.or (f64.eq (local.get $x) (f64.const inf)) (f64.eq (local.get $y) (f64.const 0)))
179
+ (then (return (f64.div (f64.const 0) (f64.const 0)))))
180
+ (if (i32.or (f64.eq (local.get $y) (f64.const inf)) (f64.lt (local.get $x) (local.get $y)))
181
+ (then (return (local.get $a))))
182
+ (block $up (loop $ul
183
+ (br_if $up (f64.gt (f64.mul (local.get $y) (f64.const 2)) (local.get $x)))
184
+ (local.set $y (f64.mul (local.get $y) (f64.const 2)))
185
+ (br $ul)))
186
+ (block $dn (loop $dl
187
+ (br_if $dn (f64.lt (local.get $y) (f64.abs (local.get $b))))
188
+ (if (f64.ge (local.get $x) (local.get $y)) (then (local.set $x (f64.sub (local.get $x) (local.get $y)))))
189
+ (local.set $y (f64.mul (local.get $y) (f64.const 0.5)))
190
+ (br $dl)))
191
+ (f64.copysign (local.get $x) (local.get $a)))`
192
+
193
+
150
194
  ctx.core.stdlib['__ptr_type'] = `(func $__ptr_type (param $ptr i64) (result i32)
151
195
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))`
152
196
 
@@ -167,66 +211,115 @@ export default (ctx) => {
167
211
 
168
212
  // Heap-base watermark: gates header-backed propsPtr fast paths so static-data
169
213
  // OBJECT slots (offsets < heap base) don't misread arbitrary memory at off-16.
170
- // Updated by optimizeModule() when data segment exceeds 1024 bytes.
171
- ctx.scope.globals.set('__heap_start', '(global $__heap_start (mut i32) (i32.const 1024))')
214
+ // Updated by optimizeModule() when data segment exceeds HEAP.START bytes.
215
+ declGlobal('__heap_start', 'i32', HEAP.START)
172
216
 
173
- // Shared memory keeps the heap pointer in linear memory (memory[1020]):
217
+ // Shared memory keeps the heap pointer in linear memory (memory[HEAP.PTR_ADDR]):
174
218
  // wasm globals are per-instance, so threads sharing one memory must share one
175
219
  // pointer cell. Non-shared memory (incl. alloc:false) uses the `$__heap`
176
220
  // global — exported so the JS-side adapter (memory.String etc) bumps the same
177
221
  // pointer. Storing it in memory would collide with the static data section
178
- // whenever the data exceeds 1020 bytes.
222
+ // whenever the data exceeds HEAP.PTR_ADDR bytes.
223
+ // Geometric memory growth shared by `__alloc` and the in-place string
224
+ // bump-extend paths (string.js). Ensures linear memory covers byte offset
225
+ // `$next`, growing when short. Growing one page at a time turns a long-running
226
+ // embedding (watr called thousands of times) into O(n²) — each memory.grow may
227
+ // relocate and copy the whole heap — so we request at least the current size
228
+ // (≥2× total) in one shot; only on hitting the declared maximum do we fall back
229
+ // to the bare minimum. `$need` is the TOTAL pages required to cover $next; the
230
+ // byte size of memory ((memory.size)<<16) is computed in i64 because it
231
+ // overflows i32 at the wasm32 max of 65536 pages (4 GiB) — without that,
232
+ // capacity reads as 0 and every allocation spuriously tries to grow past the
233
+ // ceiling, trapping near 4 GiB.
234
+ ctx.core.stdlib['__memgrow'] = `(func $__memgrow (param $next i32)
235
+ (local $cur i32) (local $need i32)
236
+ (local.set $need (i32.wrap_i64 (i64.shr_u (i64.add (i64.extend_i32_u (local.get $next)) (i64.const 65535)) (i64.const 16))))
237
+ (if (i32.gt_u (local.get $need) (memory.size))
238
+ (then
239
+ (if (i64.gt_u (i64.extend_i32_u (local.get $need)) (i64.const 65536)) (then (unreachable)))
240
+ (local.set $cur (i32.sub (local.get $need) (memory.size))) ;; minimum delta
241
+ (if (i32.lt_u (local.get $cur) (memory.size)) (then (local.set $cur (memory.size)))) ;; geometric
242
+ (if (i32.gt_u (i32.add (local.get $cur) (memory.size)) (i32.const 65536))
243
+ (then (local.set $cur (i32.sub (i32.const 65536) (memory.size))))) ;; cap at wasm32 max
244
+ (if (i32.eq (memory.grow (local.get $cur)) (i32.const -1))
245
+ (then (if (i32.eq (memory.grow (i32.sub (local.get $need) (memory.size))) (i32.const -1))
246
+ (then (unreachable))))))))`
247
+
179
248
  if (ctx.memory.shared) {
180
- // Heap offset stored at memory[1020] (i32), just before heap start at 1024.
181
- // We still want to grow memory when running out of pages — without growth a
182
- // host-run binary traps on the first allocation that exceeds the initial
183
- // 64 KB. Use the same geometric-growth strategy as the owned-memory variant.
249
+ // Heap offset stored at memory[HEAP.PTR_ADDR] (i32), just before heap start at
250
+ // HEAP.START. Threads sharing one memory must share one pointer cell.
184
251
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
185
- (local $ptr i32) (local $next i32) (local $cur i32) (local $need i32)
186
- (local.set $ptr (i32.load (i32.const 1020)))
252
+ (local $ptr i32) (local $next i32)
253
+ (local.set $ptr (i32.load (i32.const ${HEAP.PTR_ADDR})))
187
254
  (local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
188
- (local.set $cur (i32.shl (memory.size) (i32.const 16)))
189
- (if (i32.gt_u (local.get $next) (local.get $cur))
190
- (then
191
- (local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
192
- (if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
193
- (if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
194
- (then (if (i32.eq (memory.grow
195
- (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
196
- (i32.const -1)) (then (unreachable)))))))
197
- (i32.store (i32.const 1020) (local.get $next))
255
+ (call $__memgrow (local.get $next))
256
+ (i32.store (i32.const ${HEAP.PTR_ADDR}) (local.get $next))
198
257
  (local.get $ptr))`
199
258
  ctx.core.stdlib['__clear'] = `(func $__clear
200
- (i32.store (i32.const 1020) (i32.const 1024)))`
259
+ (i32.store (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
201
260
  } else {
202
- // Own memory: heap offset in a global, auto-grow when needed. Exported so
203
- // the JS-side adapter (alloc:false, no `_alloc` export) shares the pointer.
204
- ctx.scope.globals.set('__heap', '(global $__heap (export "__heap") (mut i32) (i32.const 1024))')
205
- // Bump allocator with geometric growth. Growing one page at a time turns a
206
- // long-running embedding (e.g. watr called thousands of times) into O(n²) —
207
- // each memory.grow may relocate and copy the whole heap. So when we must
208
- // grow, request at least the current size (≥2× total) in one shot; only on
209
- // hitting the declared maximum do we fall back to the bare minimum.
261
+ // Own memory: heap offset in a global, exported so the JS-side adapter
262
+ // (alloc:false, no `_alloc` export) shares the pointer.
263
+ declGlobal('__heap', 'i32', HEAP.START, { export: '__heap' })
210
264
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
211
- (local $ptr i32) (local $next i32) (local $cur i32) (local $need i32)
265
+ (local $ptr i32) (local $next i32)
212
266
  (local.set $ptr (global.get $__heap))
213
- ;; Align next allocation to 8 bytes
214
267
  (local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
215
- (local.set $cur (i32.shl (memory.size) (i32.const 16)))
216
- (if (i32.gt_u (local.get $next) (local.get $cur))
217
- (then
218
- (local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
219
- (if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
220
- (if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
221
- (then (if (i32.eq (memory.grow
222
- (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
223
- (i32.const -1)) (then (unreachable)))))))
268
+ (call $__memgrow (local.get $next))
224
269
  (global.set $__heap (local.get $next))
225
270
  (local.get $ptr))`
226
271
  ctx.core.stdlib['__clear'] = `(func $__clear
227
- (global.set $__heap (i32.const 1024)))`
272
+ (global.set $__heap (i32.const ${HEAP.START})))`
228
273
  }
229
274
 
275
+ // Build an insertion-ordered list of live slot offsets for a Set/Map/HASH
276
+ // backing table at $off (cap slots of $stride bytes). Returns a fresh i32 array
277
+ // (live-count entries) of slot offsets sorted by packed sequence (the insertion
278
+ // counter rides in each entry's hash-word high 32 bits — see collection.js's
279
+ // seqStore). Every order-sensitive iteration (keys/values/entries, for-in,
280
+ // spread, JSON, Map copy) walks this instead of raw slot order, so jz matches
281
+ // the JS spec's insertion order. Lives in core (not collection) because object
282
+ // and json iterate HASH tables without pulling the collection module. Insertion
283
+ // sort: enumerated collections are small, and it stays branch-light when sorted.
284
+ ctx.core.stdlib['__coll_order'] = `(func $__coll_order (param $off i32) (param $cap i32) (param $stride i32) (result i32)
285
+ (local $i i32) (local $n i32) (local $slot i32) (local $buf i32)
286
+ (local $j i32) (local $k i32) (local $cur i32) (local $sq i32)
287
+ ;; A null/empty backing pointer (off below the heap base) has no live slots —
288
+ ;; ordering it yields the empty list. Guard before the $off-8 length read so a
289
+ ;; degenerate receiver returns an empty buffer instead of faulting on load(-8).
290
+ (if (i32.lt_u (local.get $off) (i32.const ${HEAP.START})) (then (return (call $__alloc (i32.const 0)))))
291
+ (local.set $buf (call $__alloc (i32.shl (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 2))))
292
+ ;; gather live slot offsets (occupied ⇔ hash word ≠ 0)
293
+ (block $gd (loop $gl
294
+ (br_if $gd (i32.ge_s (local.get $i) (local.get $cap)))
295
+ (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (local.get $stride))))
296
+ (if (i64.ne (i64.load (local.get $slot)) (i64.const 0))
297
+ (then
298
+ (i32.store (i32.add (local.get $buf) (i32.shl (local.get $n) (i32.const 2))) (local.get $slot))
299
+ (local.set $n (i32.add (local.get $n) (i32.const 1)))))
300
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
301
+ (br $gl)))
302
+ ;; insertion-sort buf[0..n) ascending by sequence = hash-word high 32 bits
303
+ (local.set $j (i32.const 1))
304
+ (block $sd (loop $sl
305
+ (br_if $sd (i32.ge_s (local.get $j) (local.get $n)))
306
+ (local.set $cur (i32.load (i32.add (local.get $buf) (i32.shl (local.get $j) (i32.const 2)))))
307
+ (local.set $sq (i32.wrap_i64 (i64.shr_u (i64.load (local.get $cur)) (i64.const 32))))
308
+ (local.set $k (i32.sub (local.get $j) (i32.const 1)))
309
+ (block $id (loop $il
310
+ (br_if $id (i32.lt_s (local.get $k) (i32.const 0)))
311
+ (br_if $id (i32.le_u
312
+ (i32.wrap_i64 (i64.shr_u (i64.load (i32.load (i32.add (local.get $buf) (i32.shl (local.get $k) (i32.const 2))))) (i64.const 32)))
313
+ (local.get $sq)))
314
+ (i32.store (i32.add (local.get $buf) (i32.shl (i32.add (local.get $k) (i32.const 1)) (i32.const 2)))
315
+ (i32.load (i32.add (local.get $buf) (i32.shl (local.get $k) (i32.const 2)))))
316
+ (local.set $k (i32.sub (local.get $k) (i32.const 1)))
317
+ (br $il)))
318
+ (i32.store (i32.add (local.get $buf) (i32.shl (i32.add (local.get $k) (i32.const 1)) (i32.const 2))) (local.get $cur))
319
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
320
+ (br $sl)))
321
+ (local.get $buf))`
322
+
230
323
  // === Memory-based length/cap helpers (C-style headers) ===
231
324
 
232
325
  // Array/TypedArray/Buffer: [-8:len(i32)][-4:cap(i32)][data...]
@@ -263,12 +356,7 @@ export default (ctx) => {
263
356
  (if (result i32)
264
357
  (i32.and (i32.eq (local.get $t) (i32.const 1)) (i32.ge_u (local.get $off) (i32.const 8)))
265
358
  (then
266
- (block $done
267
- (loop $follow
268
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
269
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
270
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
271
- (br $follow)))
359
+ ${followForwardingWat('$off', { lowGuard: false })}
272
360
  (i32.load (i32.sub (local.get $off) (i32.const 8))))
273
361
  (else
274
362
  (if (result i32)
@@ -288,7 +376,9 @@ export default (ctx) => {
288
376
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
289
377
  (else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 8)))
290
378
  (call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
291
- (else (i32.load (i32.sub (local.get $off) (i32.const 8))))))
379
+ ;; HASH/SET/MAP/BUFFER: re-resolve offset so grown SET/MAP follow the
380
+ ;; forwarding chain (HASH/BUFFER never forward → same inline offset).
381
+ (else (i32.load (i32.sub (call $__ptr_offset (local.get $ptr)) (i32.const 8))))))
292
382
  (else (i32.const 0))))))`
293
383
 
294
384
  ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr i64) (result i32)
@@ -349,13 +439,7 @@ export default (ctx) => {
349
439
  (then
350
440
  (if (i32.eq (local.get $t) (i32.const 1))
351
441
  (then
352
- (block $done
353
- (loop $follow
354
- (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
355
- (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
356
- (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
357
- (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
358
- (br $follow)))))
442
+ ${followForwardingWat('$off', { lowGuard: true })}))
359
443
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $len)))))`
360
444
 
361
445
  // Alloc header(16) + data(cap*stride). Layout: [propsPtr@-16(f64=0), len@-8, cap@-4],
@@ -377,12 +461,18 @@ export default (ctx) => {
377
461
 
378
462
  // Generic header allocator for non-8 strides: Set (16), Map probe (24), TypedArray raw (1).
379
463
  // Same 16-byte header layout as __alloc_hdr; per-entry stride is passed dynamically.
464
+ // Header (16B) + cap*stride slots. Collections (Set/Map/HASH) key "empty slot"
465
+ // off a zero hash word, so the slot region MUST start zeroed. The bump allocator
466
+ // reuses memory after a heap reset (__clear) without re-zeroing, so we cannot
467
+ // lean on fresh-page zeroing here — clear the slots explicitly. Also covers the
468
+ // grow path, which rehashes into a freshly-allocated table expecting empties.
380
469
  ctx.core.stdlib['__alloc_hdr_n'] = `(func $__alloc_hdr_n (param $len i32) (param $cap i32) (param $stride i32) (result i32)
381
470
  (local $ptr i32)
382
471
  (local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.mul (local.get $cap) (local.get $stride)))))
383
472
  (i64.store (local.get $ptr) (i64.const 0))
384
473
  (i32.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $len))
385
474
  (i32.store (i32.add (local.get $ptr) (i32.const 12)) (local.get $cap))
475
+ (memory.fill (i32.add (local.get $ptr) (i32.const 16)) (i32.const 0) (i32.mul (local.get $cap) (local.get $stride)))
386
476
  (i32.add (local.get $ptr) (i32.const 16)))`
387
477
 
388
478
  // Allocator + exports are deferred: only included when memory is actually needed.
@@ -460,7 +550,14 @@ export default (ctx) => {
460
550
  // Slot val-types reach the emit-time consumer via valTypeOf → ctx.schema.slotVT
461
551
  // (read on the AST `.prop` node), not via tagging this IR node.
462
552
  function emitSchemaSlotRead(baseExpr, idx) {
463
- const base = baseExpr?.type === 'f64' ? baseExpr : typed(baseExpr, 'f64')
553
+ // An unboxed proven-non-ARRAY pointer (a structInline element cell, a narrowed local)
554
+ // reaches ptrOffsetIR raw so it returns the offset directly — no `__ptr_offset` call.
555
+ // Pre-boxing via asF64 strips ptrKind and forces every field read onto the call path
556
+ // (the dcbb433 perf cliff on object/struct kernels — `p.x,p.y,p.z` per loop iteration).
557
+ // A NaN-box or untyped value still routes through f64 for the reinterpret/forwarding path.
558
+ const base = (baseExpr?.ptrKind != null && baseExpr.ptrKind !== VAL.ARRAY)
559
+ ? baseExpr
560
+ : (baseExpr?.type === 'f64' ? baseExpr : asF64(baseExpr))
464
561
  return typed(ctx.abi.object.ops.load(ptrOffsetIR(base, VAL.OBJECT), idx), 'f64')
465
562
  }
466
563
 
@@ -542,7 +639,7 @@ export default (ctx) => {
542
639
  }
543
640
 
544
641
  /** Emit .prop access for a WASM f64 node using schema or HASH fallback. */
545
- function emitPropAccess(va, obj, prop) {
642
+ function emitPropAccess(va, obj, prop, fromOptional = false) {
546
643
  // Anonymous-literal fast path: when `obj` resolves at compile time to an
547
644
  // object literal `{...}` (either directly, or through a `.prop` chain
548
645
  // walked back to one), use the literal's slot index instead of falling
@@ -553,21 +650,21 @@ export default (ctx) => {
553
650
  // `({a:{b:1}}).a.b` where the receiver is anonymous. Spread sources
554
651
  // (`{...x}`) shift slot ordering and would need their own resolution.
555
652
  const slot = literalSlot(obj, prop)
556
- if (slot >= 0) return emitSchemaSlotRead(asF64(va), slot)
653
+ if (slot >= 0) return emitSchemaSlotRead(va, slot)
557
654
  // Receiver IR is an unboxed OBJECT pointer carrying its own schema (a
558
655
  // structInline element cell, a narrowed local): resolve the field's fixed
559
656
  // slot directly from `ptrAux` — more precise than the structural
560
- // `ctx.schema.find(null, …)` and never falls to the dyn dispatcher.
657
+ // `ctx.schema.slotOf(null, …)` and never falls to the dyn dispatcher.
561
658
  if (va?.ptrKind === VAL.OBJECT && va.ptrAux != null && typeof prop === 'string') {
562
659
  const sch = ctx.schema.list[va.ptrAux]
563
660
  const si = sch ? sch.indexOf(prop) : -1
564
- if (si >= 0) return emitSchemaSlotRead(asF64(va), si)
661
+ if (si >= 0) return emitSchemaSlotRead(va, si)
565
662
  }
566
- let schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
663
+ let schemaIdx = typeof obj === 'string' ? ctx.schema.slotOf(obj, prop) : ctx.schema.slotOf(null, prop)
567
664
  // Chain receiver (e.g. `o.meta.bias`): when the chain resolves to a known
568
665
  // OBJECT shape via JSON-shape propagation, the parent shape's `names`
569
666
  // gives the slot directly. Avoids the structural ambiguity of
570
- // ctx.schema.find(null, prop) when multiple registered schemas share a key.
667
+ // ctx.schema.slotOf(null, prop) when multiple registered schemas share a key.
571
668
  if (schemaIdx < 0 && typeof obj !== 'string') {
572
669
  const sh = shapeOf(obj)
573
670
  if ((sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) && sh.names) {
@@ -576,7 +673,7 @@ export default (ctx) => {
576
673
  }
577
674
  }
578
675
  const key = asI64(emit(['str', prop]))
579
- if (schemaIdx >= 0) return emitSchemaSlotRead(asF64(va), schemaIdx)
676
+ if (schemaIdx >= 0) return emitSchemaSlotRead(va, schemaIdx)
580
677
  if (typeof obj === 'string') {
581
678
  const vt = lookupValType(obj)
582
679
  if (usesDynProps(vt)) {
@@ -595,9 +692,16 @@ export default (ctx) => {
595
692
  // In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
596
693
  // Skip the external branch and dispatch through the typed HASH/OBJECT path.
597
694
  if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt, prop)
598
- ctx.features.external = true
695
+ // `fromOptional` (a `?.prop` read) short-circuits on nullish, so its
696
+ // PTR.EXTERNAL arm is dead unless host externals are already in play —
697
+ // don't force the __ext_prop import just for an optional read.
698
+ if (!fromOptional) ctx.features.external = true
599
699
  return emitDynGetAnyTyped(va, key, vt, prop)
600
700
  }
701
+ // Primitive receiver (number/boolean/bigint): no dynamic props — `(5).foo` is
702
+ // undefined. Without this the value falls to the __hash_get fallback, which
703
+ // reinterprets the primitive's bits as a HASH pointer and reads heap → OOB.
704
+ if (vt === VAL.NUMBER || vt === VAL.BOOL || vt === VAL.BIGINT) return undefExpr()
601
705
  inc('__hash_get', '__str_hash', '__str_eq')
602
706
  return typed(['f64.reinterpret_i64', ['call', '$__hash_get', asI64(va), key]], 'f64')
603
707
  }
@@ -662,8 +766,8 @@ export default (ctx) => {
662
766
  const inner = ctx.schema.emitInner(obj)
663
767
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', inner]]], 'f64')
664
768
  }
665
- const idx = ctx.schema.find(obj, prop)
666
- if (idx >= 0) return emitSchemaSlotRead(asF64(emit(obj)), idx)
769
+ const idx = ctx.schema.slotOf(obj, prop)
770
+ if (idx >= 0) return emitSchemaSlotRead(emit(obj), idx)
667
771
  }
668
772
 
669
773
  if (prop === 'length') {
@@ -691,7 +795,7 @@ export default (ctx) => {
691
795
  // UTF-8 and __str_byteLen returns byte count, so this matches the runtime
692
796
  // semantics. Skips the call + NaN-unbox round-trip entirely.
693
797
  if (Array.isArray(obj) && (obj[0] === 'str' || obj[0] == null) && typeof obj[1] === 'string') {
694
- return typed(['f64.const', Buffer.byteLength(obj[1], 'utf8')], 'f64')
798
+ return typed(['f64.const', new TextEncoder().encode(obj[1]).length], 'f64')
695
799
  }
696
800
  // structInline Array<S>: the header `len` counts physical f64 cells (K
697
801
  // per element), so the JS array length is `physicalLen / K`.
@@ -712,14 +816,15 @@ export default (ctx) => {
712
816
  }
713
817
 
714
818
  // Type-specific property emitter (`.regex:source`, …) — the property-read
715
- // mirror of the `.vt:method` method-dispatch table. Property emitters take
716
- // a single arg (the receiver); arity 2 entries are method emitters and
717
- // must not be routed here (reading `re.test` as a value is not a call).
819
+ // mirror of the `.vt:method` method-dispatch table. Only entries tagged as
820
+ // getters (via `getter()`) fire here: reading `re.source` yields a value,
821
+ // but reading `m.keys`/`re.test` is not a call and must not invoke the
822
+ // method (which would materialize a view / run the probe).
718
823
  const ptRep = typeof obj === 'string' ? repOf(obj) : null
719
824
  const ptVt = ptRep ? ptRep.val : valTypeOf(obj)
720
825
  if (ptVt) {
721
826
  const tpEmitter = ctx.core.emit[`.${ptVt}:${prop}`]
722
- if (tpEmitter && tpEmitter.length <= 1) return tpEmitter(obj)
827
+ if (tpEmitter?.getter) return tpEmitter(obj)
723
828
  }
724
829
 
725
830
  // valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
@@ -734,24 +839,19 @@ export default (ctx) => {
734
839
  if ((prop === 'valueOf' || prop === 'toString') && ctx.closure.call &&
735
840
  (ptVt === VAL.ARRAY || ptVt === VAL.TYPED || ptVt === VAL.OBJECT)) {
736
841
  const builtin = ctx.core.emit[`.${ptVt}:${prop}`] || ctx.core.emit[`.${prop}`]
737
- if (builtin && builtin.length <= 1) {
738
- const o = temp('vo'), p = temp('vp')
739
- inc('__dyn_get_expr', '__ptr_type')
740
- return typed(['block', ['result', 'f64'],
741
- ['local.set', `$${o}`, asF64(emit(obj))],
742
- ['local.set', `$${p}`, ['f64.reinterpret_i64',
743
- ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], asI64(emit(['str', prop]))]]],
744
- ['if', ['result', 'f64'],
745
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${p}`]]], ['i32.const', PTR.CLOSURE]],
746
- ['then', ['local.get', `$${p}`]],
747
- ['else', asF64(builtin(o))]]], 'f64')
842
+ if (builtin && emitArity(builtin) <= 1) {
843
+ return sidecarOverride(emit(obj), asI64(emit(['str', prop])),
844
+ (p) => ['local.get', `$${p}`], // READ: yield the override closure value
845
+ (o) => asF64(builtin(o))) // else the arity-≤1 builtin's value
748
846
  }
749
847
  }
750
848
 
751
- // Module-registered property emitter (.size, etc.)
849
+ // Module-registered property getter (.size, .byteLength, …). Methods sharing
850
+ // the bare-`.prop` table (`.values`, `.pop`, date getters) are untagged and
851
+ // fall through to a real property read — `m.values` reads the "values" field.
752
852
  const propKey = `.${prop}`
753
853
  const propEmitter = ctx.core.emit[propKey]
754
- if (propEmitter && propEmitter.length <= 1) return propEmitter(obj)
854
+ if (propEmitter?.getter) return propEmitter(obj)
755
855
 
756
856
  return emitPropAccess(emit(obj), obj, prop)
757
857
  }
@@ -783,7 +883,13 @@ export default (ctx) => {
783
883
  return optionalGuard(t, va, useFn(t))
784
884
  }
785
885
 
786
- // Optional chaining: obj?.prop → null if obj is null, else obj.prop
886
+ // Optional chaining: obj?.prop → undefined if obj is nullish, else obj.prop.
887
+ // Delegate the property read to emitPropAccess — the SAME resolution the plain
888
+ // `.` emitter uses (passing the hoisted temp's value for the load, but the
889
+ // original `obj` name for schema/valType lookup). The previous hand-rolled copy
890
+ // diverged: it lacked emitPropAccess's `VAL.OBJECT off-schema → __dyn_get_expr`
891
+ // branch and fell to `__hash_get`, which mis-reads fixed-shape OBJECT memory
892
+ // (a self-host miscompile — `o?.x` returned undefined under the kernel).
787
893
  ctx.core.emit['?.'] = (obj, prop) => evalOnce(obj, (t) => {
788
894
  const rep = typeof obj === 'string' ? repOf(obj) : null
789
895
  const vt = rep ? rep.val : valTypeOf(obj)
@@ -791,27 +897,19 @@ export default (ctx) => {
791
897
  const notString = vt == null && typeof obj === 'string' && lookupNotString(obj)
792
898
  return emitLengthAccess(['local.get', `$${t}`], vt, notString)
793
899
  }
794
- const propIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : -1
795
- if (propIdx >= 0) return emitSchemaSlotRead(['local.get', `$${t}`], propIdx)
796
- if (typeof obj === 'string') {
797
- const objType = lookupValType(obj)
798
- if (usesDynProps(objType))
799
- return emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
800
- if (objType === VAL.HASH)
801
- return emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
802
- if (objType == null)
803
- // Unknown receiver — in WASI mode use typed dispatch (no PTR.EXTERNAL values).
804
- // In JS host mode use __dyn_get_any_t but don't force features.external here
805
- // since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
806
- return ctx.transform.host === 'wasi'
807
- ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
808
- : emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
809
- inc('__hash_get', '__str_hash', '__str_eq')
810
- return ['f64.reinterpret_i64', ['call', '$__hash_get', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI64(emit(['str', prop]))]]
900
+ // Type-specific + module-registered property getters (`.size`, `.byteLength`,
901
+ // `.regex:source`, ) the SAME getter dispatch the plain `.` emitter runs
902
+ // (only entries tagged via `getter()` fire; untagged `.values`/`.pop` stay a
903
+ // field read). Read the already-hoisted, null-guarded temp `t` rather than
904
+ // re-emitting `obj`. Without this `s?.size` fell straight to emitPropAccess (a
905
+ // plain field read) and returned undefined — a Set/Map size getter never ran.
906
+ if (vt) {
907
+ const tg = ctx.core.emit[`.${vt}:${prop}`]
908
+ if (tg?.getter) return tg(t)
811
909
  }
812
- if (valTypeOf(obj) === VAL.HASH)
813
- return emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
814
- return emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj), prop)
910
+ const g = ctx.core.emit[`.${prop}`]
911
+ if (g?.getter) return g(t)
912
+ return emitPropAccess(typed(['local.get', `$${t}`], 'f64'), obj, prop, true)
815
913
  })
816
914
 
817
915
  // Optional index: arr?.[i] → null if arr is null, else arr[i]
@@ -836,14 +934,21 @@ export default (ctx) => {
836
934
  // as a direct method call. The outer optional short-circuits when the receiver
837
935
  // is nullish — the method itself is statically known to exist.
838
936
  if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') && typeof callee[2] === 'string') {
839
- const methodEmit = ctx.core.emit[`.${callee[2]}`]
840
- if (methodEmit) {
937
+ const method = callee[2]
938
+ if (ctx.core.emit[`.${method}`]) {
841
939
  const recv = callee[1]
842
940
  return evalOnce(recv, (t) => {
843
- // Emit-time rep seed on fresh `?.()` recv-temp so methodEmit's dispatch fast-paths fire.
941
+ // Emit-time rep seed on fresh `?.()` recv-temp so the dispatch fast-paths fire.
844
942
  const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
845
943
  if (vt) updateRep(t, { val: vt })
846
- return asF64(methodEmit(t, ...args))
944
+ // Re-enter the full `()` method dispatch (runtime string/array dispatch,
945
+ // charCodeAt, schema, …) rather than the bare generic `.${method}` emitter
946
+ // — that emitter is the *array* `includes`/`indexOf`/… and would mis-run on
947
+ // a string receiver. Mirrors `?.[]`'s re-entry into `[]`. The method is
948
+ // statically known to exist, so the inner optional is moot; `t` is already
949
+ // nullish-guarded by evalOnce. Args re-bundle into the `()` arg slot.
950
+ const callArgs = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
951
+ return asF64(ctx.core.emit['()'](['.', t, method], callArgs))
847
952
  })
848
953
  }
849
954
  }
@@ -862,7 +967,7 @@ export default (ctx) => {
862
967
  else normal.push(a)
863
968
  }
864
969
  const combined = reconstructArgsWithSpreads(normal, spreads)
865
- const arrayIR = buildArrayWithSpreads(combined)
970
+ const arrayIR = spread(combined)
866
971
  callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), [arrayIR], true)
867
972
  } else {
868
973
  callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
@@ -891,9 +996,9 @@ export default (ctx) => {
891
996
  // boolean; isBoolExpr additionally catches `Boolean(x)` and parenthesized forms.
892
997
  if (valTypeOf(a) === VAL.BOOL || isBoolExpr(a)) return emit(['str', 'boolean'])
893
998
  if (!ctx.runtime.typeofStrs) {
894
- ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
999
+ ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object', 'boolean']
895
1000
  for (const s of ctx.runtime.typeofStrs)
896
- ctx.scope.globals.set(`__tof_${s}`, `(global $__tof_${s} (mut f64) (f64.const 0))`)
1001
+ declGlobal(`__tof_${s}`, 'f64')
897
1002
  }
898
1003
  inc('__typeof')
899
1004
  // Receiver type unknown; enable branches that wouldn't otherwise be reachable.
@@ -916,7 +1021,7 @@ export default (ctx) => {
916
1021
  ;; That bit pattern is the math NaN value, not a tagged pointer — treat as "number".
917
1022
  ;; Negative-NaN bit patterns (sign bit set) don't match NAN_PREFIX so are uniquely numeric.
918
1023
  (if (i32.or
919
- (i64.eq (local.get $v) (i64.const 0x7FF8000000000000))
1024
+ (i64.eq (local.get $v) (i64.const ${NAN_BITS}))
920
1025
  (i64.eq (i64.and (local.get $v) (i64.const 0xFFF0000000000000))
921
1026
  (i64.const 0xFFF0000000000000)))
922
1027
  (then (return (global.get $__tof_number))))
@@ -925,6 +1030,9 @@ export default (ctx) => {
925
1030
  ;; typeof null === "object" — the historical JS quirk, distinct from undefined.
926
1031
  (if (i64.eq (local.get $v) (i64.const ${NULL_NAN}))
927
1032
  (then (return (global.get $__tof_object))))
1033
+ ;; Boolean atoms (FALSE_NAN / TRUE_NAN) — carry at the JS boundary.
1034
+ (if (i64.eq (i64.and (local.get $v) (i64.const 0xFFFFFFFEFFFFFFFF)) (i64.const ${FALSE_NAN}))
1035
+ (then (return (global.get $__tof_boolean))))
928
1036
  (local.set $t (call $__ptr_type (local.get $v)))
929
1037
  (if ${stringTest}
930
1038
  (then (return (global.get $__tof_string))))
@@ -937,11 +1045,14 @@ export default (ctx) => {
937
1045
  // === Schema helpers (centralized in module/schema.js) ===
938
1046
  initSchema(ctx)
939
1047
 
940
- // Low-level pointer helpers callable from jz code
941
- ctx.core.emit['__mkptr'] = (t, a, o) => typed(['call', '$__mkptr', asI32(emit(t)), asI32(emit(a)), asI32(emit(o))], 'f64')
942
- ctx.core.emit['__ptr_type'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_type', asI64(emit(p))]], 'f64')
943
- ctx.core.emit['__ptr_aux'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asI64(emit(p))]], 'f64')
944
- ctx.core.emit['__ptr_offset'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asI64(emit(p))]], 'f64')
1048
+ // Low-level pointer helpers callable from jz code. Each handler inc()'s its
1049
+ // stdlib func so the call resolves at every optimize level. At opt≥1 fusedRewrite
1050
+ // inlines `call $__ptr_*` to bit-ops (the func is then dead-code-eliminated), but
1051
+ // the inc() must fire first so pullStdlib has the body when watr assembles at opt0.
1052
+ ctx.core.emit['__mkptr'] = (t, a, o) => (inc('__mkptr'), typed(['call', '$__mkptr', asI32(emit(t)), asI32(emit(a)), asI32(emit(o))], 'f64'))
1053
+ ctx.core.emit['__ptr_type'] = (p) => (inc('__ptr_type'), typed(['f64.convert_i32_s', ['call', '$__ptr_type', asI64(emit(p))]], 'f64'))
1054
+ ctx.core.emit['__ptr_aux'] = (p) => (inc('__ptr_aux'), typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asI64(emit(p))]], 'f64'))
1055
+ ctx.core.emit['__ptr_offset'] = (p) => (inc('__ptr_offset'), typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asI64(emit(p))]], 'f64'))
945
1056
 
946
1057
  // Error(msg) — passthrough (throw handles any value). Subclasses share the
947
1058
  // same shape: jz doesn't model typed-error dispatch, so SyntaxError/TypeError/