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/src/emit.js CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { ctx, err, inc, PTR } from './ctx.js'
26
- import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal } from './analyze.js'
26
+ import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal, staticPropertyKey } from './analyze.js'
27
27
  import {
28
28
  typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
29
29
  NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
@@ -34,6 +34,7 @@ import {
34
34
  isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
35
35
  temp, tempI32, tempI64, allocPtr,
36
36
  boxedAddr, readVar, writeVar, isNullish,
37
+ isLiteralStr, resolveValType, isFuncRef,
37
38
  multiCount, loopTop, flat,
38
39
  reconstructArgsWithSpreads,
39
40
  } from './ir.js'
@@ -42,6 +43,46 @@ import {
42
43
  // to decide whether to emit a value-returning or side-effect-only form.
43
44
  let _expect = null
44
45
 
46
+ const FIRST_CLASS_UNARY_MATH = {
47
+ 'math.abs': 'f64.abs',
48
+ 'math.sqrt': 'f64.sqrt',
49
+ 'math.ceil': 'f64.ceil',
50
+ 'math.floor': 'f64.floor',
51
+ 'math.trunc': 'f64.trunc',
52
+ }
53
+
54
+ function builtinFunctionValue(name) {
55
+ const op = FIRST_CLASS_UNARY_MATH[name]
56
+ if (!op) err(`Builtin function '${name}' cannot be used as a first-class value`)
57
+ if (!ctx.closure.table) err(`Builtin function '${name}' used as value requires closure support`)
58
+ const fn = `${T}builtin_${name.replace(/\W/g, '_')}`
59
+ if (!ctx.core.stdlib[fn]) {
60
+ const width = ctx.closure.width ?? MAX_CLOSURE_ARITY
61
+ const params = ['(param $__env f64)', '(param $__argc i32)']
62
+ for (let i = 0; i < width; i++) params.push(`(param $__a${i} f64)`)
63
+ ctx.core.stdlib[fn] = `(func $${fn} ${params.join(' ')} (result f64) (${op} (local.get $__a0)))`
64
+ inc(fn)
65
+ }
66
+ let idx = ctx.closure.table.indexOf(fn)
67
+ if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(fn) }
68
+ const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
69
+ ir.closureFuncIdx = idx
70
+ return ir
71
+ }
72
+
73
+ /** Emit unary negation: constant-fold, or i32 sub from 0 / f64.neg. */
74
+ const emitNeg = (a) => {
75
+ if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
76
+ const v = emit(a)
77
+ return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32'
78
+ ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32')
79
+ : typed(['f64.neg', toNumF64(a, v)], 'f64')
80
+ }
81
+
82
+ /** Try constant-folding binary arith: returns emitNum(result) or null. */
83
+ const foldConst = (va, vb, fn, guard) =>
84
+ isLit(va) && isLit(vb) && (!guard || guard(litVal(vb))) ? emitNum(fn(litVal(va), litVal(vb))) : null
85
+
45
86
  /** Emit typeof comparison: typeof x == typeCode → type-aware check. */
46
87
  export function emitTypeofCmp(a, b, cmpOp) {
47
88
  let typeofExpr, code
@@ -62,10 +103,7 @@ export function emitTypeofCmp(a, b, cmpOp) {
62
103
  if (code === -2) {
63
104
  inc('__ptr_type')
64
105
  const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
65
- const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
66
- const isStr = ['i32.or',
67
- ['i32.eq', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
68
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]]
106
+ const isStr = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]]
69
107
  return typed(eq ? ['i32.and', isPtr, isStr]
70
108
  : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isStr]], 'i32')
71
109
  }
@@ -81,9 +119,7 @@ export function emitTypeofCmp(a, b, cmpOp) {
81
119
  const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
82
120
  const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
83
121
  const notStrFn = ['i32.and',
84
- ['i32.and',
85
- ['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
86
- ['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
122
+ ['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]], ['i32.const', PTR.STRING]],
87
123
  ['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]]
88
124
  const notNullish = ['i32.eqz', isNullish(['local.get', `$${t}`])]
89
125
  const check = ['i32.and', ['i32.and', isPtr, notStrFn], notNullish]
@@ -92,13 +128,24 @@ export function emitTypeofCmp(a, b, cmpOp) {
92
128
  if (code === -6) {
93
129
  inc('__ptr_type')
94
130
  const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
95
- const isFn = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', PTR.CLOSURE]]
131
+ const isFn = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.CLOSURE]]
96
132
  return typed(eq ? ['i32.and', isPtr, isFn] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isFn]], 'i32')
97
133
  }
134
+ if (code === -7) {
135
+ const vt = resolveValType(typeofExpr, valTypeOf, lookupValType)
136
+ if (vt) return typed(['i32.const', (vt === VAL.BIGINT) === eq ? 1 : 0], 'i32')
137
+ const n = ['local.tee', `$${t}`, va]
138
+ const isBigInt = ['i32.and',
139
+ ['f64.eq', n, ['local.get', `$${t}`]],
140
+ ['i32.and',
141
+ ['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]],
142
+ ['f64.lt', ['f64.abs', ['local.get', `$${t}`]], ['f64.const', 2.2250738585072014e-308]]]]
143
+ return typed(eq ? isBigInt : ['i32.eqz', isBigInt], 'i32')
144
+ }
98
145
  if (code >= 0) {
99
146
  inc('__ptr_type')
100
147
  const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
101
- const check = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', code]]
148
+ const check = ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', code]]
102
149
  return typed(eq ? ['i32.and', isPtr, check] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', check]], 'i32')
103
150
  }
104
151
  return null
@@ -112,9 +159,74 @@ const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
112
159
  // heap strings) and BIGINT (content compare).
113
160
  const REF_EQ_KINDS = new Set([
114
161
  VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
115
- VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX,
162
+ VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX, VAL.DATE,
116
163
  ])
117
164
 
165
+ function stringLiteral(node) {
166
+ if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
167
+ if (Array.isArray(node) && node[0] == null && typeof node[1] === 'string') return node[1]
168
+ return null
169
+ }
170
+
171
+ function nonNegIntLiteral(node) {
172
+ const n = intLiteralValue(node)
173
+ return n != null && n >= 0 ? n : null
174
+ }
175
+
176
+ function emitSingleCharIndexCmp(a, b, negate = false) {
177
+ const leftLit = stringLiteral(a)
178
+ const rightLit = stringLiteral(b)
179
+ const aIdx = Array.isArray(a) && a[0] === '[]'
180
+ const bIdx = Array.isArray(b) && b[0] === '[]'
181
+ let indexed, lit
182
+ if (bIdx && leftLit != null) { indexed = b; lit = leftLit }
183
+ else if (aIdx && rightLit != null) { indexed = a; lit = rightLit }
184
+ else return null
185
+
186
+ if (lit.length === 0) return null
187
+ if ([...lit].some(c => c.charCodeAt(0) > 0x7F)) return null
188
+
189
+ const [, obj, key] = indexed
190
+ const idx = nonNegIntLiteral(key)
191
+ if (idx == null) return null
192
+
193
+ const vt = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
194
+ if (vt && vt !== VAL.STRING) return null
195
+
196
+ const finish = expr => negate ? ['i32.eqz', expr] : expr
197
+
198
+ // Known STRING: s[i] always returns 1-char SSO. Multi-char literal → always false.
199
+ if (vt === VAL.STRING && lit.length > 1) return emitNum(negate ? 1 : 0)
200
+
201
+ // Single-char literal: compare byte directly, skipping __str_idx allocation.
202
+ if (lit.length !== 1 || !ctx.core.stdlib['__char_at'] || !ctx.core.stdlib['__str_byteLen']) return null
203
+
204
+ const ptr = temp('sc'), idxIR = ['i32.const', idx]
205
+ inc('__str_byteLen', '__char_at')
206
+ const charEq = ['if', ['result', 'i32'],
207
+ ['i32.gt_u', ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], idxIR],
208
+ ['then', ['i32.eq', ['call', '$__char_at', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxIR], ['i32.const', lit.charCodeAt(0)]]],
209
+ ['else', ['i32.const', 0]]]
210
+
211
+ if (vt === VAL.STRING) {
212
+ return typed(['block', ['result', 'i32'],
213
+ ['local.set', `$${ptr}`, asF64(emit(obj))],
214
+ finish(charEq)], 'i32')
215
+ }
216
+
217
+ inc('__ptr_type', '__typed_idx', '__eq')
218
+ const genericEq = ['call', '$__eq',
219
+ ['i64.reinterpret_f64', ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], idxIR]],
220
+ asI64(emit(['str', lit]))]
221
+ const cmp = ['if', ['result', 'i32'],
222
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]]], ['i32.const', PTR.STRING]],
223
+ ['then', charEq],
224
+ ['else', genericEq]]
225
+ return typed(['block', ['result', 'i32'],
226
+ ['local.set', `$${ptr}`, asF64(emit(obj))],
227
+ finish(cmp)], 'i32')
228
+ }
229
+
118
230
  // === Flow-sensitive type refinement ===
119
231
  // Map typeof code (from resolveTypeof in prepare.js) → VAL kind. Undef/boolean/object have no
120
232
  // single VAL refinement, so they're excluded. String/number/function do.
@@ -219,6 +331,19 @@ function containsNestedLoop(body) {
219
331
  return false
220
332
  }
221
333
 
334
+ function nestedSmallLoopBudget(body) {
335
+ if (!Array.isArray(body)) return 1
336
+ if (body[0] === '=>') return 1
337
+ if (body[0] === 'for') {
338
+ const [, init, cond, step, loopBody] = body
339
+ const n = smallConstForTripCount(init, cond, step)
340
+ return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
341
+ }
342
+ let max = 1
343
+ for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
344
+ return max
345
+ }
346
+
222
347
  function containsDeclOf(body, name) {
223
348
  if (!Array.isArray(body)) return false
224
349
  const op = body[0]
@@ -251,8 +376,17 @@ function cloneWithSubst(node, name, value) {
251
376
  }
252
377
 
253
378
  const MAX_SMALL_FOR_UNROLL = 8
379
+ const MAX_NESTED_FOR_UNROLL = 64
254
380
 
255
- function unrollSmallConstFor(init, cond, step, body) {
381
+ function containsKnownTypedArrayIndex(body) {
382
+ if (!Array.isArray(body)) return false
383
+ if (body[0] === '=>') return false
384
+ if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
385
+ for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
386
+ return false
387
+ }
388
+
389
+ function smallConstForTripCount(init, cond, step) {
256
390
  if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
257
391
  const decl = init[1]
258
392
  if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
@@ -268,8 +402,19 @@ function unrollSmallConstFor(init, cond, step, body) {
268
402
  (step[0] === '++' && step[1] === name) ||
269
403
  (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
270
404
  )
271
- if (!stepOk) return null
272
- if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsNestedLoop(body) || containsDeclOf(body, name)) return null
405
+ return stepOk ? end : null
406
+ }
407
+
408
+ function unrollSmallConstFor(init, cond, step, body) {
409
+ const end = smallConstForTripCount(init, cond, step)
410
+ if (end == null) return null
411
+ const name = init[1][1]
412
+ if (containsNestedLoop(body)) {
413
+ const nestedMode = ctx.transform.optimize?.nestedSmallConstForUnroll
414
+ if (nestedMode !== true && (nestedMode !== 'auto' || !containsKnownTypedArrayIndex(body))) return null
415
+ if (end * nestedSmallLoopBudget(body) > MAX_NESTED_FOR_UNROLL) return null
416
+ }
417
+ if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsDeclOf(body, name)) return null
273
418
  if (isReassigned(body, name)) return null
274
419
 
275
420
  const out = []
@@ -374,7 +519,7 @@ export function materializeMulti(callNode) {
374
519
  ? rawArgs[0].slice(1) : rawArgs
375
520
  const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
376
521
  while (emittedArgs.length < func.sig.params.length)
377
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
522
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
378
523
  const temps = Array.from({ length: n }, () => temp())
379
524
  const out = allocPtr({ type: 1, len: n, tag: 'marr' })
380
525
  const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
@@ -391,13 +536,13 @@ export function emitDecl(...inits) {
391
536
  for (let ii = 0; ii < inits.length; ii++) {
392
537
  const i = inits[ii]
393
538
  if (typeof i === 'string') {
394
- const undef = nullExpr()
539
+ const undef = undefExpr()
395
540
  if (ctx.func.boxed.has(i)) {
396
541
  const cell = ctx.func.boxed.get(i)
397
542
  ctx.func.locals.set(cell, 'i32')
398
- result.push(
399
- ['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
400
- ['f64.store', ['local.get', `$${cell}`], undef])
543
+ if (!ctx.func.preboxed?.has(i))
544
+ result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
545
+ result.push(['f64.store', ['local.get', `$${cell}`], undef])
401
546
  continue
402
547
  }
403
548
  if (isGlobal(i)) {
@@ -436,7 +581,7 @@ export function emitDecl(...inits) {
436
581
  ? rawArgs[0].slice(1) : rawArgs
437
582
  const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
438
583
  while (emittedArgs.length < func.sig.params.length)
439
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
584
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
440
585
  result.push(['call', `$${init[1]}`, ...emittedArgs])
441
586
  for (let k = n - 1; k >= 0; k--)
442
587
  result.push(['local.set', `$${targets[k]}`])
@@ -446,14 +591,14 @@ export function emitDecl(...inits) {
446
591
  }
447
592
  }
448
593
  const isObjLit = Array.isArray(init) && init[0] === '{}'
449
- if (isObjLit) ctx.schema.targetStack.push(name)
594
+ if (isObjLit) ctx.schema.targetStack.push({ name, active: true })
450
595
  const val = emit(init)
451
596
  if (isObjLit) ctx.schema.targetStack.pop()
452
597
  // Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
453
598
  // Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
454
599
  // isReassigned is conservative across nested arrow shadows — we miss the optimization
455
600
  // rather than emit a wrong direct call.
456
- if (val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
601
+ if (Array.isArray(init) && init[0] === '=>' && val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
457
602
  && ctx.func.body && !isReassigned(ctx.func.body, name)) {
458
603
  if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
459
604
  ctx.func.directClosures.set(name, val.closureBodyName)
@@ -461,9 +606,9 @@ export function emitDecl(...inits) {
461
606
  if (ctx.func.boxed.has(name)) {
462
607
  const cell = ctx.func.boxed.get(name)
463
608
  ctx.func.locals.set(cell, 'i32')
464
- result.push(
465
- ['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
466
- ['f64.store', ['local.get', `$${cell}`], asF64(val)])
609
+ if (!ctx.func.preboxed?.has(name))
610
+ result.push(['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]])
611
+ result.push(['f64.store', ['local.get', `$${cell}`], asF64(val)])
467
612
  continue
468
613
  }
469
614
  if (isGlobal(name)) {
@@ -510,21 +655,21 @@ export function emitDecl(...inits) {
510
655
  } else {
511
656
  coerced = localType === 'f64' ? asF64(val) : asI32(val)
512
657
  }
513
- if (!(isLit(coerced) && coerced[1] === 0 && !ctx.func.stack.length))
658
+ if (!(isLit(coerced) && coerced[1] === 0 && !Object.is(coerced[1], -0) && !ctx.func.stack.length))
514
659
  result.push(['local.set', `$${name}`, coerced])
515
660
 
516
661
  const schemaId = ctx.schema.idOf?.(name)
517
662
  if (ctx.func.localProps?.has(name) && schemaId != null) {
518
663
  const schema = ctx.schema.resolve(name)
519
664
  if (schema?.[0] === '__inner__') {
520
- inc('__alloc', '__mkptr')
665
+ inc('__alloc_hdr', '__mkptr')
521
666
  const bt = `${T}bx${ctx.func.uniq++}`
522
667
  ctx.func.locals.set(bt, 'i32')
523
668
  const innerName = `${name}${T}inner`
524
669
  ctx.func.locals.set(innerName, 'f64')
525
670
  result.push(
526
671
  ['local.set', `$${innerName}`, ['local.get', `$${name}`]],
527
- ['local.set', `$${bt}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]],
672
+ ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
528
673
  ['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
529
674
  ...schema.slice(1).map((_, j) =>
530
675
  ['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
@@ -599,11 +744,11 @@ export function buildArrayWithSpreads(items) {
599
744
  const n = multiCount(sec.expr)
600
745
  ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
601
746
  if (sec.baseLocal) {
602
- ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['local.get', `$${sec.local}`]]])
747
+ ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
603
748
  ir.push(['local.set', `$${sec.lenLocal}`, ['i32.load', ['i32.sub', ['local.get', `$${sec.baseLocal}`], ['i32.const', 8]]]])
604
749
  } else {
605
750
  // Cache __len once per spread; reused below for total-len sum and inner copy bound.
606
- ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['local.get', `$${sec.local}`]]])
751
+ ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]]])
607
752
  }
608
753
  }
609
754
  }
@@ -636,12 +781,10 @@ export function buildArrayWithSpreads(items) {
636
781
  ? ['f64.load', ['i32.add', ['local.get', `$${sec.baseLocal}`], ['i32.shl', ['local.get', `$${sidx}`], ['i32.const', 3]]]]
637
782
  : ctx.module.modules['string']
638
783
  ? ['if', ['result', 'f64'],
639
- ['i32.or',
640
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.STRING]],
641
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.SSO]]],
642
- ['then', (inc('__str_idx'), ['call', '$__str_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])],
643
- ['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])]]
644
- : (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])
784
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]]], ['i32.const', PTR.STRING]],
785
+ ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])],
786
+ ['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])]]
787
+ : (inc('__typed_idx'), ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${sec.local}`]], ['local.get', `$${sidx}`]])
645
788
  ir.push(
646
789
  ['local.set', `$${sidx}`, ['i32.const', 0]],
647
790
  ['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
@@ -710,6 +853,34 @@ export function emitBody(node) {
710
853
  const cmpOp = (i32op, f64op, fn) => (a, b) => {
711
854
  const va = emit(a), vb = emit(b)
712
855
  if (isLit(va) && isLit(vb)) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
856
+ // String compare: NaN-boxed string pointers compare as NaN under f64.lt/gt
857
+ // (always false), so without this the spec-correct `"a" < "b"` returns 0.
858
+ // Route both-STRING operands through __str_cmp's three-way result, then apply
859
+ // the same i32 sign op as numeric (lt_s/gt_s/le_s/ge_s vs 0).
860
+ const vta = resolveValType(a, valTypeOf, lookupValType)
861
+ const vtb = resolveValType(b, valTypeOf, lookupValType)
862
+ if (vta === VAL.BIGINT || vtb === VAL.BIGINT) {
863
+ const op = bigintUnsignedBound(a) || bigintUnsignedBound(b) ? i32op.replace('_s', '_u') : i32op
864
+ return typed([`i64.${op}`, asI64(va), asI64(vb)], 'i32')
865
+ }
866
+ if (vta === VAL.STRING && vtb === VAL.STRING) {
867
+ inc('__str_cmp')
868
+ return typed([`i32.${i32op}`, ['call', '$__str_cmp', asI64(va), asI64(vb)], ['i32.const', 0]], 'i32')
869
+ }
870
+ if (vta === VAL.DATE || vtb === VAL.DATE) {
871
+ const dateNum = (node, v, vt) => {
872
+ if (vt !== VAL.DATE) return toNumF64(node, v)
873
+ const ptr = v.ptrKind === VAL.DATE
874
+ ? v
875
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
876
+ return typed(['f64.load', ptr], 'f64')
877
+ }
878
+ return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
879
+ }
880
+ if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
881
+ return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
882
+ if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
883
+ return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
713
884
  const ai = intConstValue(a), bi = intConstValue(b)
714
885
  if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
715
886
  if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
@@ -717,6 +888,16 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
717
888
  ? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
718
889
  }
719
890
 
891
+ function needsRelationalToNumber(expr, vt) {
892
+ if (vt === VAL.STRING) return true
893
+ if (vt != null) return false
894
+ return mayReadBoxedValue(expr)
895
+ }
896
+
897
+ function mayReadBoxedValue(expr) {
898
+ return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
899
+ }
900
+
720
901
  function intConstValue(expr) {
721
902
  if (typeof expr === 'number' && Number.isInteger(expr)) return expr
722
903
  if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
@@ -727,13 +908,45 @@ function intConstValue(expr) {
727
908
  return null
728
909
  }
729
910
 
911
+ function bigintUnsignedBound(expr) {
912
+ const n = bigintConstValue(expr)
913
+ return n != null && n > 0x7fffffffffffffffn && n <= 0xffffffffffffffffn
914
+ }
915
+
916
+ function bigintConstValue(expr) {
917
+ if (typeof expr === 'bigint') return expr
918
+ if (!Array.isArray(expr)) return null
919
+ if (expr[0] == null && typeof expr[1] === 'bigint') return expr[1]
920
+ if (expr[0] === 'u-') {
921
+ const n = bigintConstValue(expr[1])
922
+ return n == null ? null : -n
923
+ }
924
+ return null
925
+ }
926
+
927
+ function arrayIndexKey(key) {
928
+ const n = Number(key)
929
+ const u = n >>> 0
930
+ return String(u) === key && u !== 0xffffffff ? u : null
931
+ }
932
+
730
933
  /** Compound assignment: read → op → write back (via readVar/writeVar). */
731
934
  function compoundAssign(name, val, f64op, i32op) {
732
935
  if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
733
936
  const void_ = _expect === 'void'
734
937
  const va = readVar(name), vb = emit(val)
735
- if (i32op && va.type === 'i32' && vb.type === 'i32')
736
- return writeVar(name, i32op(va, vb), void_)
938
+ // Peel f64.convert_i32_s/u when va is i32 typed-array integer reads wrap their
939
+ // i32.load in convert_i32_* by default, but the i32 arithmetic path can use the
940
+ // raw i32 directly (eliminates per-iter widen + saturating-trunc roundtrip on
941
+ // hot accumulator loops like `let s = 0; for (...) s += i32arr[i]`).
942
+ let vbi = vb
943
+ if (i32op && va.type === 'i32' && vb.type !== 'i32' &&
944
+ Array.isArray(vb) && (vb[0] === 'f64.convert_i32_s' || vb[0] === 'f64.convert_i32_u')) {
945
+ const inner = vb[1]
946
+ vbi = Array.isArray(inner) ? typed(inner, 'i32') : inner
947
+ }
948
+ if (i32op && va.type === 'i32' && vbi.type === 'i32')
949
+ return writeVar(name, i32op(va, vbi), void_)
737
950
  return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
738
951
  }
739
952
 
@@ -880,6 +1093,7 @@ export const emitter = {
880
1093
 
881
1094
  '=': (name, val) => {
882
1095
  if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
1096
+ const void_ = _expect === 'void'
883
1097
  // Array index assignment: arr[i] = x
884
1098
  if (Array.isArray(name) && name[0] === '[]') {
885
1099
  const [, arr, idx] = name
@@ -899,28 +1113,34 @@ export const emitter = {
899
1113
  ['local.set', `$${arrTmp}`, arrExpr],
900
1114
  ['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
901
1115
  ['local.set', `$${valTmp}`, valueExpr],
902
- ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${arrTmp}`], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
1116
+ ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
903
1117
  ]
904
1118
  if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
905
1119
  body.push(['local.get', `$${valTmp}`])
906
1120
  return typed(['block', ['result', 'f64'], ...body], 'f64')
907
1121
  }
908
1122
  const setDyn = () => {
1123
+ if (ctx.transform.strict)
1124
+ err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
909
1125
  inc('__dyn_set')
910
- return typed(['call', '$__dyn_set', asF64(emit(arr)), keyExpr, valueExpr], 'f64')
1126
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
911
1127
  }
912
1128
  const dispatchKey = (numericIR) => {
1129
+ if (ctx.transform.strict)
1130
+ err(`strict mode: dynamic property assignment \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
913
1131
  const keyTmp = temp()
914
1132
  return typed(['block', ['result', 'f64'],
915
1133
  ['local.set', `$${keyTmp}`, keyExpr],
916
- ['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
917
- ['then', ['call', '$__dyn_set', asF64(emit(arr)), ['local.get', `$${keyTmp}`], valueExpr]],
1134
+ ['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
1135
+ ['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
918
1136
  ['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
919
1137
  }
920
1138
  // Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
921
- const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1] : null
1139
+ const litKey = isLiteralStr(idx) ? idx[1]
1140
+ : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
1141
+ : null
922
1142
  if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
923
- const slot = ctx.schema.find(arr, litKey, true)
1143
+ const slot = ctx.schema.find(arr, litKey)
924
1144
  if (slot >= 0) {
925
1145
  const t = temp()
926
1146
  return typed(['block', ['result', 'f64'],
@@ -931,11 +1151,25 @@ export const emitter = {
931
1151
  ['local.get', `$${t}`]], 'f64')
932
1152
  }
933
1153
  }
1154
+ const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
1155
+ if (arrIndex != null && typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
1156
+ const persist = ptr => {
1157
+ if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1158
+ if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1159
+ return ['local.set', `$${arr}`, ptr]
1160
+ }
1161
+ return storeArrayValue(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), persist)
1162
+ }
934
1163
  if (keyType === VAL.STRING) return setDyn()
935
1164
  if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
936
1165
  lookupValType(arr) === 'typed') {
937
- const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val)
1166
+ const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
938
1167
  if (r) return r
1168
+ // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
1169
+ // returns the stored value as f64, used directly as the expr result.
1170
+ inc('__typed_set_idx')
1171
+ return typed(['call', '$__typed_set_idx',
1172
+ asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
939
1173
  }
940
1174
  if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
941
1175
  const inner = ctx.schema.emitInner(arr)
@@ -962,9 +1196,34 @@ export const emitter = {
962
1196
  return storeArrayValue(asF64(va), keyExpr, persist)
963
1197
  }
964
1198
  // arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
965
- const arrVT = (typeof arr === 'string' ? lookupValType(arr) : null) || VAL.OBJECT
1199
+ const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
1200
+ const arrVT = knownArrVT || VAL.OBJECT
966
1201
  if (useRuntimeKeyDispatch) {
967
1202
  inc('__dyn_set', '__is_str_key')
1203
+ // When arr type is unknown (could be TypedArray) and __typed_set_idx is
1204
+ // available, dispatch the numeric branch through __ptr_type so TypedArray
1205
+ // writes go by element type. Without this, ternary-typed arrays (e.g.
1206
+ // `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
1207
+ // f64.store boxed bytes regardless of element width.
1208
+ const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1209
+ if (knownArrVT == null && hasTypedSet) {
1210
+ const objTmp = temp('asu')
1211
+ const idxTmp = tempI32('asi')
1212
+ inc('__ptr_type', '__typed_set_idx')
1213
+ return dispatchKey(keyNode => {
1214
+ const keyI32 = asI32(typed(keyNode, 'f64'))
1215
+ return ['block', ['result', 'f64'],
1216
+ ['local.set', `$${objTmp}`, asF64(va)],
1217
+ ['local.set', `$${idxTmp}`, keyI32],
1218
+ ['local.set', `$${t}`, vv],
1219
+ ['if', ['result', 'f64'],
1220
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1221
+ ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1222
+ ['else', ['block', ['result', 'f64'],
1223
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1224
+ ['local.get', `$${t}`]]]]]
1225
+ })
1226
+ }
968
1227
  return dispatchKey(keyNode => {
969
1228
  const keyI32 = asI32(typed(keyNode, 'f64'))
970
1229
  return ['block', ['result', 'f64'],
@@ -973,6 +1232,62 @@ export const emitter = {
973
1232
  ['local.get', `$${t}`]]
974
1233
  })
975
1234
  }
1235
+ if (typeof arr !== 'string') {
1236
+ const objTmp = temp('asu')
1237
+ const idxTmp = tempI32('asi')
1238
+ const ptrTmp = temp('asp')
1239
+ const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1240
+ inc('__ptr_type', '__arr_set_idx_ptr')
1241
+ if (hasTypedSet) inc('__typed_set_idx')
1242
+ return typed(['block', ['result', 'f64'],
1243
+ ['local.set', `$${objTmp}`, asF64(va)],
1244
+ ['local.set', `$${idxTmp}`, vi],
1245
+ ['local.set', `$${t}`, vv],
1246
+ ['if', ['result', 'f64'],
1247
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1248
+ ['then', ['block', ['result', 'f64'],
1249
+ ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1250
+ ['local.get', `$${t}`]]],
1251
+ ['else', hasTypedSet ? ['if', ['result', 'f64'],
1252
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1253
+ ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1254
+ ['else', ['block', ['result', 'f64'],
1255
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1256
+ ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1257
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1258
+ ['local.get', `$${t}`]]]]], 'f64')
1259
+ }
1260
+ if (typeof arr === 'string' && knownArrVT == null) {
1261
+ const objTmp = temp('asu')
1262
+ const idxTmp = tempI32('asi')
1263
+ const ptrTmp = temp('asp')
1264
+ const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
1265
+ inc('__ptr_type', '__arr_set_idx_ptr')
1266
+ if (hasTypedSet) inc('__typed_set_idx')
1267
+ const persist = ptr => {
1268
+ if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
1269
+ if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
1270
+ return ['local.set', `$${arr}`, ptr]
1271
+ }
1272
+ return typed(['block', ['result', 'f64'],
1273
+ ['local.set', `$${objTmp}`, asF64(va)],
1274
+ ['local.set', `$${idxTmp}`, vi],
1275
+ ['local.set', `$${t}`, vv],
1276
+ ['if', ['result', 'f64'],
1277
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.ARRAY]],
1278
+ ['then', ['block', ['result', 'f64'],
1279
+ ['local.set', `$${ptrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1280
+ persist(['local.get', `$${ptrTmp}`]),
1281
+ ['local.get', `$${t}`]]],
1282
+ ['else', hasTypedSet ? ['if', ['result', 'f64'],
1283
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.TYPED]],
1284
+ ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${t}`]]],
1285
+ ['else', ['block', ['result', 'f64'],
1286
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1287
+ ['local.get', `$${t}`]]]] : ['block', ['result', 'f64'],
1288
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${t}`]],
1289
+ ['local.get', `$${t}`]]]]], 'f64')
1290
+ }
976
1291
  return typed(['block', ['result', 'f64'],
977
1292
  ['local.set', `$${t}`, vv],
978
1293
  ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
@@ -982,10 +1297,8 @@ export const emitter = {
982
1297
  if (Array.isArray(name) && name[0] === '.') {
983
1298
  const [, obj, prop] = name
984
1299
  // Schema-based object → f64.store at fixed offset.
985
- // safe=true: skip structural subtyping when variable's type is unknown,
986
- // otherwise a slot write could clobber an array/string's payload.
987
1300
  if (typeof obj === 'string' && ctx.schema.find) {
988
- const idx = ctx.schema.find(obj, prop, true)
1301
+ const idx = ctx.schema.find(obj, prop)
989
1302
  if (idx >= 0) {
990
1303
  const va = emit(obj), vv = asF64(emit(val)), t = temp()
991
1304
  const shadow = needsDynShadow(obj)
@@ -995,39 +1308,60 @@ export const emitter = {
995
1308
  ['f64.store', ['i32.add', ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]], ['local.get', `$${t}`]],
996
1309
  ]
997
1310
  if (shadow)
998
- stmts.push(['drop', ['call', '$__dyn_set', asF64(va), asF64(emit(['str', prop])), ['local.get', `$${t}`]]])
1311
+ stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
999
1312
  stmts.push(['local.get', `$${t}`])
1000
1313
  return typed(['block', ['result', 'f64'], ...stmts], 'f64')
1001
1314
  }
1002
1315
  }
1003
1316
  if (typeof obj === 'string') {
1004
1317
  const objType = keyValType(obj)
1005
- if (usesDynProps(objType)) {
1318
+ // OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
1319
+ // properties go through __dyn_set, which writes to the per-OBJECT
1320
+ // propsPtr at off-16 — same path as object-literal dyn shadow writes
1321
+ // (module/object.js). __hash_set assumes HASH bucket layout and would
1322
+ // corrupt OBJECT memory.
1323
+ if (usesDynProps(objType) || objType === VAL.OBJECT) {
1324
+ inc('__dyn_set')
1325
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1326
+ }
1327
+ if (ctx.func.names.has(obj) && !ctx.func.locals?.has(obj) && !ctx.func.current?.params?.some(p => p.name === obj)) {
1006
1328
  inc('__dyn_set')
1007
- return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1329
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1330
+ }
1331
+ if (objType == null && ctx.transform.host !== 'wasi') {
1332
+ ctx.features.external = true
1008
1333
  }
1009
- if (objType == null) ctx.features.external = true
1010
1334
  inc('__hash_set')
1011
- const setCall = typed(['call', '$__hash_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1335
+ const setCall = typed(['f64.reinterpret_i64', ['call', '$__hash_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1012
1336
  if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
1013
1337
  ['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
1338
+ // Closure-captured (boxed) locals store the value at the cell address — local.tee
1339
+ // would write to the i32 cell pointer, not the f64 value. Route through writeVar.
1340
+ if (ctx.func.boxed?.has(obj)) return writeVar(obj, setCall, false)
1014
1341
  return typed(['local.tee', `$${obj}`, setCall], 'f64')
1015
1342
  }
1016
- ctx.features.external = true
1343
+ if (ctx.transform.host !== 'wasi') ctx.features.external = true
1017
1344
  inc('__dyn_set')
1018
- return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
1345
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(emit(val))]], 'f64')
1019
1346
  }
1020
1347
  if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
1021
- const void_ = _expect === 'void'
1348
+ if (Array.isArray(val) && val[0] === 'u+' && val[1] === name) {
1349
+ inc('__to_num')
1350
+ return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
1351
+ }
1022
1352
  return writeVar(name, emit(val), void_)
1023
1353
  },
1024
1354
 
1025
1355
  // Compound assignments: read-modify-write with type coercion
1026
1356
  '+=': (name, val) => {
1027
- // String concatenation: desugar to name = name + val (+ handler knows about strings)
1357
+ // String concatenation: desugar to name = name + val (+ handler knows about strings).
1358
+ // Also desugar when either side has unknown type — the `+` operator picks runtime
1359
+ // string/numeric dispatch (`__is_str_key`); compoundAssign would force f64.add and
1360
+ // silently corrupt string concatenations through unknown-typed values.
1028
1361
  const vt = typeof name === 'string' ? keyValType(name) : null
1029
1362
  const vtB = keyValType(val)
1030
1363
  if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
1364
+ if ((vt == null || vtB == null) && ctx.core.stdlib['__str_concat']) return emit(['=', name, ['+', name, val]])
1031
1365
  return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
1032
1366
  },
1033
1367
  ...Object.fromEntries([
@@ -1068,11 +1402,8 @@ export const emitter = {
1068
1402
  const t = temp()
1069
1403
  const va = readVar(name)
1070
1404
  // Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
1071
- const cond = op === '??='
1072
- ? isNullish(['local.tee', `$${t}`, asF64(va)])
1073
- : ['i32.and',
1074
- ['f64.eq', ['local.tee', `$${t}`, asF64(va)], ['local.get', `$${t}`]],
1075
- ['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]]]
1405
+ const lhs = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
1406
+ const cond = op === '??=' ? isNullish(lhs) : truthyIR(lhs)
1076
1407
  // &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
1077
1408
  const [thenExpr, elseExpr] = op === '||='
1078
1409
  ? [['local.get', `$${t}`], asF64(emit(val))]
@@ -1109,12 +1440,25 @@ export const emitter = {
1109
1440
  const vtA = keyValType(a)
1110
1441
  const vtB = keyValType(b)
1111
1442
  if (vtA === VAL.STRING && vtB === VAL.STRING) {
1443
+ // Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
1444
+ // generic concat dispatch when rhs is a string-index. The byte flows
1445
+ // straight from __char_at into memory, and the bump-extend path elides
1446
+ // the alloc+copy when lhs is the heap-top STRING.
1447
+ if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
1448
+ if (keyValType(b[1]) === VAL.STRING) {
1449
+ inc('__str_append_byte', '__char_at')
1450
+ return typed(['call', '$__str_append_byte',
1451
+ asI64(emit(a)),
1452
+ ['call', '$__char_at', asI64(emit(b[1])), asI32(emit(b[2]))],
1453
+ ], 'f64')
1454
+ }
1455
+ }
1112
1456
  inc('__str_concat_raw')
1113
- return typed(['call', '$__str_concat_raw', asF64(emit(a)), asF64(emit(b))], 'f64')
1457
+ return typed(['call', '$__str_concat_raw', asI64(emit(a)), asI64(emit(b))], 'f64')
1114
1458
  }
1115
1459
  if (vtA === VAL.STRING || vtB === VAL.STRING) {
1116
1460
  inc('__str_concat')
1117
- return typed(['call', '$__str_concat', asF64(emit(a)), asF64(emit(b))], 'f64')
1461
+ return typed(['call', '$__str_concat', asI64(emit(a)), asI64(emit(b))], 'f64')
1118
1462
  }
1119
1463
  if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
1120
1464
  return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
@@ -1125,9 +1469,9 @@ export const emitter = {
1125
1469
  if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
1126
1470
  const tA = temp('add'), tB = temp('add')
1127
1471
  inc('__str_concat', '__is_str_key')
1128
- const checkA = vtA == null ? ['call', '$__is_str_key', ['local.tee', `$${tA}`, asF64(emit(a))]] : null
1129
- const checkB = vtB == null ? ['call', '$__is_str_key', ['local.tee', `$${tB}`, asF64(emit(b))]] : null
1130
- const concat = ['call', '$__str_concat', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1472
+ const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
1473
+ const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
1474
+ const concat = ['call', '$__str_concat', ['i64.reinterpret_f64', ['local.get', `$${tA}`]], ['i64.reinterpret_f64', ['local.get', `$${tB}`]]]
1131
1475
  const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
1132
1476
  if (checkA && checkB) {
1133
1477
  return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
@@ -1139,8 +1483,8 @@ export const emitter = {
1139
1483
  ['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
1140
1484
  ], 'f64')
1141
1485
  }
1142
- const va = emit(a), vb = emit(b)
1143
- if (isLit(va) && isLit(vb)) return emitNum(litVal(va) + litVal(vb))
1486
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a + b)
1487
+ if (_f) return _f
1144
1488
  if (isLit(vb) && litVal(vb) === 0) return va
1145
1489
  if (isLit(va) && litVal(va) === 0) return vb
1146
1490
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.add', va, vb], 'i32')
@@ -1152,9 +1496,9 @@ export const emitter = {
1152
1496
  return b === undefined
1153
1497
  ? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1154
1498
  : fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
1155
- if (b === undefined) { const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64') }
1156
- const va = emit(a), vb = emit(b)
1157
- if (isLit(va) && isLit(vb)) return emitNum(litVal(va) - litVal(vb))
1499
+ if (b === undefined) return emitNeg(a)
1500
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a - b)
1501
+ if (_f) return _f
1158
1502
  if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
1159
1503
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.sub', va, vb], 'i32')
1160
1504
  return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
@@ -1162,18 +1506,18 @@ export const emitter = {
1162
1506
  'u+': a => {
1163
1507
  if (valTypeOf(a) === VAL.BIGINT)
1164
1508
  return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
1509
+ const v = emit(a)
1510
+ if (v.type === 'i32') return asF64(v)
1511
+ if (valTypeOf(a) === VAL.NUMBER) return toNumF64(a, v)
1165
1512
  inc('__to_num')
1166
- return typed(['call', '$__to_num', asF64(emit(a))], 'f64')
1167
- },
1168
- 'u-': a => {
1169
- if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
1170
- const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64')
1513
+ return typed(['call', '$__to_num', asI64(v)], 'f64')
1171
1514
  },
1515
+ 'u-': a => emitNeg(a),
1172
1516
  '*': (a, b) => {
1173
1517
  if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1174
1518
  return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
1175
- const va = emit(a), vb = emit(b)
1176
- if (isLit(va) && isLit(vb)) return emitNum(litVal(va) * litVal(vb))
1519
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a * b)
1520
+ if (_f) return _f
1177
1521
  if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1178
1522
  if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
1179
1523
  if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
@@ -1184,23 +1528,24 @@ export const emitter = {
1184
1528
  '/': (a, b) => {
1185
1529
  if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1186
1530
  return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
1187
- const va = emit(a), vb = emit(b)
1188
- if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) / litVal(vb))
1531
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a / b, b => b !== 0)
1532
+ if (_f) return _f
1189
1533
  if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
1190
1534
  return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
1191
1535
  },
1192
1536
  '%': (a, b) => {
1193
1537
  if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
1194
1538
  return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
1195
- const va = emit(a), vb = emit(b)
1196
- if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) % litVal(vb))
1539
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a % b, b => b !== 0)
1540
+ if (_f) return _f
1197
1541
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.rem_s', va, vb], 'i32')
1198
1542
  return f64rem(toNumF64(a, va), toNumF64(b, vb))
1199
1543
  },
1200
-
1201
1544
  // === Comparisons (always i32 result) ===
1202
1545
 
1203
1546
  '==': (a, b) => {
1547
+ const charCmp = emitSingleCharIndexCmp(a, b)
1548
+ if (charCmp) return charCmp
1204
1549
  // JS loose nullish equality: x == null / x == undefined.
1205
1550
  // If the non-literal side has a known non-null VAL type, fold to 0.
1206
1551
  if (isNullishLit(a)) {
@@ -1215,11 +1560,13 @@ export const emitter = {
1215
1560
  const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
1216
1561
  const va = emit(a), vb = emit(b)
1217
1562
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
1218
- // Both sides known-pure NUMBER → f64.eq (skip __eq's pointer-identity/string path).
1219
- // valTypeOf handles literals/arithmetic exprs; lookupValType covers typed locals/params.
1220
- const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1221
- const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
1222
- if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1563
+ // Either side known-pure NUMBER (literal or typed) → f64.eq is correct regardless
1564
+ // of the other side: jz's `==` is strict (prepare.js:868), and every NaN-boxed pointer
1565
+ // reinterprets to a quiet NaN (0x7FF8… prefix) so f64.eq with any normal float is false.
1566
+ // Catches `closureVar === 34` in jzified hot loops where the unknown side has no VAL.
1567
+ const vta = resolveValType(a, valTypeOf, lookupValType)
1568
+ const vtb = resolveValType(b, valTypeOf, lookupValType)
1569
+ if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
1223
1570
  // Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
1224
1571
  // JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
1225
1572
  // STRING needs __eq (heap strings can be equal by content but different pointers).
@@ -1228,9 +1575,11 @@ export const emitter = {
1228
1575
  return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1229
1576
  }
1230
1577
  inc('__eq')
1231
- return typed(['call', '$__eq', asF64(va), asF64(vb)], 'i32')
1578
+ return typed(['call', '$__eq', asI64(va), asI64(vb)], 'i32')
1232
1579
  },
1233
1580
  '!=': (a, b) => {
1581
+ const charCmp = emitSingleCharIndexCmp(a, b, true)
1582
+ if (charCmp) return charCmp
1234
1583
  if (isNullishLit(a)) {
1235
1584
  if (valTypeOf(b)) return emitNum(1)
1236
1585
  return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
@@ -1242,14 +1591,14 @@ export const emitter = {
1242
1591
  const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
1243
1592
  const va = emit(a), vb = emit(b)
1244
1593
  if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
1245
- const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1246
- const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
1247
- if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1594
+ const vta = resolveValType(a, valTypeOf, lookupValType)
1595
+ const vtb = resolveValType(b, valTypeOf, lookupValType)
1596
+ if (vta === VAL.NUMBER || vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
1248
1597
  if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
1249
1598
  return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
1250
1599
  }
1251
1600
  inc('__eq')
1252
- return typed(['i32.eqz', ['call', '$__eq', asF64(va), asF64(vb)]], 'i32')
1601
+ return typed(['i32.eqz', ['call', '$__eq', asI64(va), asI64(vb)]], 'i32')
1253
1602
  },
1254
1603
  '<': cmpOp('lt_s', 'lt', (a, b) => a < b),
1255
1604
  '>': cmpOp('gt_s', 'gt', (a, b) => a > b),
@@ -1264,13 +1613,13 @@ export const emitter = {
1264
1613
  // Unboxed pointer offsets: falsy iff zero offset.
1265
1614
  if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
1266
1615
  // Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
1267
- // Pointers are never 0 / NaN / false / empty-string in the boxed form.
1268
- const vt = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
1269
- if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT) {
1616
+ // Excludes STRING empty string '' is a valid (non-null) pointer but is falsy.
1617
+ const vt = resolveValType(a, valTypeOf, lookupValType)
1618
+ if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT && vt !== VAL.STRING) {
1270
1619
  return isNullish(asF64(v))
1271
1620
  }
1272
1621
  inc('__is_truthy')
1273
- return typed(['i32.eqz', ['call', '$__is_truthy', asF64(v)]], 'i32')
1622
+ return typed(['i32.eqz', ['call', '$__is_truthy', asI64(v)]], 'i32')
1274
1623
  },
1275
1624
 
1276
1625
  '?:': (a, b, c) => {
@@ -1311,7 +1660,21 @@ export const emitter = {
1311
1660
  }
1312
1661
  }
1313
1662
  const fb = asF64(vb), fc = asF64(vc)
1314
- if (isPureIR(fb) && isPureIR(fc))
1663
+ const vtb = resolveValType(b, valTypeOf, lookupValType)
1664
+ const vtc = resolveValType(c, valTypeOf, lookupValType)
1665
+ const isNaNBoxLit = n => Array.isArray(n) && n[0] === 'f64.const' && typeof n[1] === 'string' && n[1].startsWith('nan:')
1666
+ const refPayload = (vtb && vtb === vtc && REF_EQ_KINDS.has(vtb))
1667
+ || vb.closureFuncIdx != null || vc.closureFuncIdx != null
1668
+ || isNaNBoxLit(fb) || isNaNBoxLit(fc)
1669
+ if (refPayload) {
1670
+ const ib = ['i64.reinterpret_f64', fb]
1671
+ const ic = ['i64.reinterpret_f64', fc]
1672
+ const bits = isPureIR(fb) && isPureIR(fc)
1673
+ ? ['select', ib, ic, cond]
1674
+ : ['if', ['result', 'i64'], cond, ['then', ib], ['else', ic]]
1675
+ return typed(['f64.reinterpret_i64', bits], 'f64')
1676
+ }
1677
+ if (!refPayload && isPureIR(fb) && isPureIR(fc))
1315
1678
  return typed(['select', fb, fc, cond], 'f64')
1316
1679
  return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
1317
1680
  },
@@ -1397,7 +1760,11 @@ export const emitter = {
1397
1760
 
1398
1761
  // === Bitwise (i32 for numbers, i64 for BigInt) ===
1399
1762
 
1400
- '~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v), typed(['i32.const', -1], 'i32')], 'i32') },
1763
+ // Per ECMAScript ToInt32, bitwise ops first ToNumber-coerce non-numeric operands.
1764
+ // i32 / lit values are already numeric — the toNumF64 wrap is skipped to keep
1765
+ // the numeric fast path at one wasm instruction. Non-numeric (NaN-boxed string,
1766
+ // unknown type) routes through __to_num so "2026" | 0 === 2026.
1767
+ '~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v.type === 'i32' ? v : toNumF64(a, v)), typed(['i32.const', -1], 'i32')], 'i32') },
1401
1768
  ...Object.fromEntries([
1402
1769
  ['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
1403
1770
  ].map(([op, fn]) => [op, (a, b) => {
@@ -1410,16 +1777,20 @@ export const emitter = {
1410
1777
  if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
1411
1778
  if (op === '>>') return emitNum(la >> lb)
1412
1779
  }
1413
- return typed([`i32.${fn}`, toI32(va), toI32(vb)], 'i32')
1780
+ const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
1781
+ const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
1782
+ return typed([`i32.${fn}`, toI32(ca), toI32(cb)], 'i32')
1414
1783
  }])),
1415
1784
  '>>>': (a, b) => {
1416
- const va = emit(a), vb = emit(b)
1417
- if (isLit(va) && isLit(vb)) return emitNum(litVal(va) >>> litVal(vb))
1785
+ const va = emit(a), vb = emit(b), _f = foldConst(va, vb, (a, b) => a >>> b)
1786
+ if (_f) return _f
1418
1787
  // F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
1419
1788
  // [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
1420
1789
  // signed for negative-high-bit s values, flipping sign and breaking the
1421
1790
  // canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
1422
- const node = typed(['i32.shr_u', toI32(va), toI32(vb)], 'i32')
1791
+ const ca = va.type === 'i32' || isLit(va) ? va : toNumF64(a, va)
1792
+ const cb = vb.type === 'i32' || isLit(vb) ? vb : toNumF64(b, vb)
1793
+ const node = typed(['i32.shr_u', toI32(ca), toI32(cb)], 'i32')
1423
1794
  node.unsigned = true
1424
1795
  return node
1425
1796
  },
@@ -1547,6 +1918,8 @@ export const emitter = {
1547
1918
  let argList = Array.isArray(callArgs)
1548
1919
  ? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
1549
1920
  : callArgs ? [callArgs] : []
1921
+ // Trailing comma in call: parser emits a trailing null sentinel — drop it
1922
+ while (argList.length && argList[argList.length - 1] == null) argList.pop()
1550
1923
 
1551
1924
  // Helper: expand spread arguments into flat list of normal arguments + spread markers
1552
1925
  // Returns { normal: [...], spreads: [(pos, expr), ...] }
@@ -1566,6 +1939,26 @@ export const emitter = {
1566
1939
 
1567
1940
  const parsed = parseArgs(argList)
1568
1941
 
1942
+ // Optional method call: obj?.method(args) — null if obj is nullish, else
1943
+ // obj.method(args). The parser shapes this as ['()', ['?.', obj, method], args],
1944
+ // distinct from the regular method call's ['.', obj, method] callee. Receiver
1945
+ // hoists into a temp so the nullish check and the method dispatch below see
1946
+ // the same evaluation; recursion with a synthetic '.'-callee reuses the type-
1947
+ // aware method-dispatch in this same handler rather than duplicating it.
1948
+ if (Array.isArray(callee) && callee[0] === '?.') {
1949
+ const [, obj, method] = callee
1950
+ const t = `${T}om${ctx.func.uniq++}`
1951
+ ctx.func.locals.set(t, 'f64')
1952
+ const va = asF64(emit(obj))
1953
+ const methodCall = emitter['()'](['.', t, method], callArgs)
1954
+ return typed(['block', ['result', 'f64'],
1955
+ ['local.set', `$${t}`, va],
1956
+ ['if', ['result', 'f64'],
1957
+ ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
1958
+ ['then', asF64(methodCall)],
1959
+ ['else', nullExpr()]]], 'f64')
1960
+ }
1961
+
1569
1962
  // Method call: obj.method(args) → type-aware dispatch
1570
1963
  if (Array.isArray(callee) && callee[0] === '.') {
1571
1964
  const [, obj, method] = callee
@@ -1577,7 +1970,7 @@ export const emitter = {
1577
1970
  const func = ctx.func.map.get(fname)
1578
1971
  const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
1579
1972
  while (emittedArgs.length < func.sig.params.length)
1580
- emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
1973
+ emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
1581
1974
  const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
1582
1975
  if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
1583
1976
  if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
@@ -1585,7 +1978,10 @@ export const emitter = {
1585
1978
  }
1586
1979
  }
1587
1980
 
1588
- const vt = keyValType(obj)
1981
+ let vt = keyValType(obj)
1982
+ if (typeof obj === 'string' && isReassigned(ctx.func.body, obj)
1983
+ && (method === 'slice' || method === 'concat')
1984
+ && vt !== VAL.STRING && vt !== VAL.ARRAY) vt = null
1589
1985
 
1590
1986
  // Helper to call method with arguments (handles spread expansion)
1591
1987
  const callMethod = (objArg, methodEmitter) => {
@@ -1624,23 +2020,23 @@ export const emitter = {
1624
2020
  ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
1625
2021
  ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
1626
2022
  if (srcIsArr) {
1627
- ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${sa}`]]])
2023
+ ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
1628
2024
  ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
1629
2025
  } else {
1630
- ir.push(['local.set', `$${sl}`, ['call', '$__len', ['local.get', `$${sa}`]]])
2026
+ ir.push(['local.set', `$${sl}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${sa}`]]]])
1631
2027
  }
1632
2028
  // Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
1633
2029
  if (objIsArr) {
1634
2030
  ir.push(['local.set', `$${ol}`,
1635
- ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${o}`]], ['i32.const', 8]]]])
2031
+ ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]], ['i32.const', 8]]]])
1636
2032
  } else {
1637
- ir.push(['local.set', `$${ol}`, ['call', '$__len', ['local.get', `$${o}`]]])
2033
+ ir.push(['local.set', `$${ol}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
1638
2034
  }
1639
2035
  // Single grow for the full spread (vs per-element grow check in the generic loop).
1640
- ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['local.get', `$${o}`],
2036
+ ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
1641
2037
  ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
1642
2038
  // base captured AFTER grow (grow may relocate the array).
1643
- ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${o}`]]])
2039
+ ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${o}`]]]])
1644
2040
  // Tight store loop.
1645
2041
  ir.push(['local.set', `$${si}`, ['i32.const', 0]])
1646
2042
  const loopId = ctx.func.uniq++
@@ -1656,7 +2052,7 @@ export const emitter = {
1656
2052
  ['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
1657
2053
  ['br', `$continue${loopId}`]]])
1658
2054
  // Single set_len for the full spread.
1659
- ir.push(['call', '$__set_len', ['local.get', `$${o}`],
2055
+ ir.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${o}`]],
1660
2056
  ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
1661
2057
  // Update source variable: grow may have moved the pointer.
1662
2058
  if (ctx.func.boxed?.has(objArg)) {
@@ -1694,7 +2090,7 @@ export const emitter = {
1694
2090
  inc('__len')
1695
2091
  const n = multiCount(spreadExpr)
1696
2092
  ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
1697
- ir.push(['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${arr}`]]])
2093
+ ir.push(['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arr}`]]]])
1698
2094
  ir.push(['local.set', `$${idx}`,
1699
2095
  reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
1700
2096
  const loopId = ctx.func.uniq++
@@ -1745,7 +2141,7 @@ export const emitter = {
1745
2141
  const n = multiCount(spreadExpr)
1746
2142
  irG.push(
1747
2143
  ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
1748
- ['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
2144
+ ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
1749
2145
  ['local.set', `$${idxL}`, ['i32.const', 0]])
1750
2146
  const loopId = ctx.func.uniq++
1751
2147
  const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
@@ -1784,7 +2180,7 @@ export const emitter = {
1784
2180
  const n = multiCount(spreadExpr)
1785
2181
  irG.push(
1786
2182
  ['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
1787
- ['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
2183
+ ['local.set', `$${lenL}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]],
1788
2184
  ['local.set', `$${idxL}`, ['i32.const', 0]])
1789
2185
  const loopId = ctx.func.uniq++
1790
2186
  const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
@@ -1845,15 +2241,26 @@ export const emitter = {
1845
2241
  const genEmitter = ctx.core.emit[genKey]
1846
2242
  return typed(['block', ['result', 'f64'],
1847
2243
  ['local.set', `$${t}`, asF64(emit(obj))],
1848
- ['local.set', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]],
2244
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
1849
2245
  ['if', ['result', 'f64'],
1850
- ['i32.or',
1851
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
1852
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
2246
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
1853
2247
  ['then', callMethod(t, strEmitter)],
1854
2248
  ['else', callMethod(t, genEmitter)]]], 'f64')
1855
2249
  }
1856
2250
 
2251
+ // Schema property function call: x.prop(args) where prop is a closure in boxed schema
2252
+ if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call) {
2253
+ const idx = ctx.schema.find(obj, method)
2254
+ if (idx >= 0 && !ctx.schema.isBoxed?.(obj)) {
2255
+ const propRead = typed(['f64.load', ['i32.add', ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
2256
+ if (parsed.hasSpread) {
2257
+ const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
2258
+ return ctx.closure.call(propRead, [buildArrayWithSpreads(combined)], true)
2259
+ }
2260
+ return ctx.closure.call(propRead, parsed.normal)
2261
+ }
2262
+ }
2263
+
1857
2264
  // Schema property function call: x.prop(args) where prop is a closure in boxed schema
1858
2265
  if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
1859
2266
  const idx = ctx.schema.find(obj, method)
@@ -1876,7 +2283,7 @@ export const emitter = {
1876
2283
  ctx.func.locals.set(objTmp, 'f64')
1877
2284
  const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1878
2285
  const arrayIR = buildArrayWithSpreads(combined)
1879
- const propRead = typed(['call', '$__dyn_get_expr', ['local.get', `$${objTmp}`], asF64(emit(['str', method]))], 'f64')
2286
+ const propRead = typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], asI64(emit(['str', method]))]], 'f64')
1880
2287
  const propTmp = `${T}mprop${ctx.func.uniq++}`
1881
2288
  ctx.func.locals.set(propTmp, 'f64')
1882
2289
  if (usesDynProps(vt)) {
@@ -1885,7 +2292,18 @@ export const emitter = {
1885
2292
  ['local.set', `$${objTmp}`, asF64(emit(obj))],
1886
2293
  ['local.set', `$${propTmp}`, propRead],
1887
2294
  ['if', ['result', 'f64'],
1888
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
2295
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
2296
+ ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
2297
+ ['else', undefExpr()]]], 'f64')
2298
+ }
2299
+ // WASI: no PTR.EXTERNAL values; closure-only dispatch is correct.
2300
+ if (ctx.transform.host === 'wasi') {
2301
+ inc('__dyn_get_expr', '__ptr_type')
2302
+ return typed(['block', ['result', 'f64'],
2303
+ ['local.set', `$${objTmp}`, asF64(emit(obj))],
2304
+ ['local.set', `$${propTmp}`, propRead],
2305
+ ['if', ['result', 'f64'],
2306
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
1889
2307
  ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
1890
2308
  ['else', undefExpr()]]], 'f64')
1891
2309
  }
@@ -1895,22 +2313,29 @@ export const emitter = {
1895
2313
  ['local.set', `$${objTmp}`, asF64(emit(obj))],
1896
2314
  ['local.set', `$${propTmp}`, propRead],
1897
2315
  ['if', ['result', 'f64'],
1898
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${propTmp}`]], ['i32.const', PTR.CLOSURE]],
2316
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${propTmp}`]]], ['i32.const', PTR.CLOSURE]],
1899
2317
  ['then', ctx.closure.call(typed(['local.get', `$${propTmp}`], 'f64'), [arrayIR], true)],
1900
2318
  ['else', ['if', ['result', 'f64'],
1901
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${objTmp}`]], ['i32.const', PTR.EXTERNAL]],
1902
- ['then', ['call', '$__ext_call', ['local.get', `$${objTmp}`], asF64(emit(['str', method])), arrayIR]],
2319
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]]], ['i32.const', PTR.EXTERNAL]],
2320
+ ['then', ['f64.reinterpret_i64', ['call', '$__ext_call',
2321
+ ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]],
2322
+ ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2323
+ ['i64.reinterpret_f64', arrayIR]]]],
1903
2324
  ['else', undefExpr()]]]]], 'f64')
1904
2325
  }
1905
2326
 
1906
2327
  // Unknown callee - assume external method
1907
2328
  if (ctx.transform.strict)
1908
2329
  err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
2330
+ if (ctx.transform.host === 'wasi') return undefExpr()
1909
2331
  inc('__ext_call')
1910
2332
  ctx.features.external = true
1911
2333
  const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
1912
2334
  const arrayIR = buildArrayWithSpreads(combined)
1913
- return typed(['call', '$__ext_call', asF64(emit(obj)), asF64(emit(['str', method])), arrayIR], 'f64');
2335
+ return typed(['f64.reinterpret_i64', ['call', '$__ext_call',
2336
+ ['i64.reinterpret_f64', asF64(emit(obj))],
2337
+ ['i64.reinterpret_f64', asF64(emit(['str', method]))],
2338
+ ['i64.reinterpret_f64', arrayIR]]], 'f64');
1914
2339
  }
1915
2340
 
1916
2341
  if (ctx.core.emit[callee]) {
@@ -1929,17 +2354,17 @@ export const emitter = {
1929
2354
  }
1930
2355
 
1931
2356
  // Direct call if callee is a known top-level function
1932
- if (typeof callee === 'string' && ctx.func.names.has(callee)) {
2357
+ if (typeof callee === 'string' && ctx.func.names.has(callee) && !ctx.func.locals?.has(callee)) {
1933
2358
  const func = ctx.func.map.get(callee)
1934
2359
 
1935
2360
  // Rest param case: collect all args (including expanded spreads) into array
1936
2361
  if (func?.rest) {
1937
2362
  const fixedParamCount = func.sig.params.length - 1
1938
2363
  const fixedArgs = parsed.normal.slice(0, fixedParamCount)
1939
- // Pad missing fixed args with sentinel for defaults
2364
+ // Pad missing fixed args with `undefined` so default-param init triggers per spec.
1940
2365
  const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
1941
2366
  while (emittedFixed.length < fixedParamCount)
1942
- emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
2367
+ emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
1943
2368
 
1944
2369
  // Reconstruct with spreads, then take rest args
1945
2370
  const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
@@ -1957,10 +2382,16 @@ export const emitter = {
1957
2382
 
1958
2383
  // Regular function call without rest params
1959
2384
  if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
1960
- // Pad missing args with canonical NaN (triggers default param init)
2385
+ // Pad missing args with `undefined` so default-param init triggers per spec
2386
+ // (only undefined, not null, should trigger defaults). Drop extras to match
2387
+ // JS calling convention — emitting them anyway produces an invalid call
2388
+ // when the callee is a fixed-arity import (e.g. `_interp`-registered host
2389
+ // stubs) since wasm validates arg count. Use ?? rather than || so a
2390
+ // legitimate 0-arity callee isn't bypassed.
1961
2391
  const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
1962
- const expected = func?.sig.params.length || args.length
1963
- while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
2392
+ const expected = func?.sig.params.length ?? args.length
2393
+ while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : undefExpr())
2394
+ if (args.length > expected) args.length = expected
1964
2395
  // Multi-value return: materialize as heap array (caller expects single pointer)
1965
2396
  if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
1966
2397
  const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
@@ -2001,13 +2432,80 @@ export const emitter = {
2001
2432
  return ctx.closure.call(emit(callee), parsed.normal)
2002
2433
  }
2003
2434
 
2004
- // Unknown callee — assume direct call
2005
- return typed(['call', `$${callee}`, ...argList.map(a => asF64(emit(a)))], 'f64')
2435
+ // Unknown callee — assume direct call. Match arg count to the declared
2436
+ // signature when the callee is a registered env import (e.g. `_interp`-
2437
+ // wired host stubs). JS calling convention drops extras and pads missing;
2438
+ // wasm requires exact arity, so emitting raw argList against a fixed-arity
2439
+ // import would invalidate the module.
2440
+ let calleeArity = null
2441
+ if (typeof callee === 'string') {
2442
+ const imp = ctx.module.imports?.find(i =>
2443
+ Array.isArray(i) && i[0] === 'import' && i[3]?.[0] === 'func' && i[3]?.[1] === `$${callee}`)
2444
+ if (imp) {
2445
+ let n = 0
2446
+ for (let k = 2; k < imp[3].length; k++) if (Array.isArray(imp[3][k]) && imp[3][k][0] === 'param') n++
2447
+ calleeArity = n
2448
+ }
2449
+ }
2450
+ const emittedArgs = argList.map(a => asF64(emit(a)))
2451
+ if (calleeArity != null) {
2452
+ while (emittedArgs.length < calleeArity) emittedArgs.push(undefExpr())
2453
+ if (emittedArgs.length > calleeArity) emittedArgs.length = calleeArity
2454
+ }
2455
+ return typed(['call', `$${callee}`, ...emittedArgs], 'f64')
2006
2456
  },
2007
2457
  }
2008
2458
 
2009
2459
  // === Emit dispatch ===
2010
2460
 
2461
+ // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined, else `a.b.c`.
2462
+ // Per ECMAScript, an optional access short-circuits the entire continuation, not just
2463
+ // its own access. Without this, `a?.b.c` parses as `(a?.b).c` and `.c` runs on the
2464
+ // nullish result of `a?.b`, returning a wrong value (or trapping in typed lowerings).
2465
+ //
2466
+ // At the outermost `.` / `[]` / `()` whose leftmost descent contains an optional, hoist
2467
+ // the deepest such optional's head into a temp, nullish-guard, and rebuild the chain
2468
+ // with that optional replaced by a regular access. The single guard short-circuits the
2469
+ // whole continuation. Nested optionals further inside the chain are left intact and
2470
+ // handle their own short-circuiting on recursion.
2471
+ function liftOptionalChain(node) {
2472
+ const path = []
2473
+ let cur = node
2474
+ while (Array.isArray(cur) && (cur[0] === '.' || cur[0] === '[]' || cur[0] === '()' ||
2475
+ cur[0] === '?.' || cur[0] === '?.[]' || cur[0] === '?.()')) {
2476
+ path.push(cur)
2477
+ cur = cur[1]
2478
+ }
2479
+ // Find the deepest optional with continuation outside it. optIdx === 0 means the
2480
+ // chain root itself is optional with no continuation — handled by the regular
2481
+ // `?.` / `?.[]` / `?.()` emitters.
2482
+ let optIdx = -1
2483
+ for (let i = path.length - 1; i >= 1; i--) {
2484
+ if (path[i][0] === '?.' || path[i][0] === '?.[]' || path[i][0] === '?.()') {
2485
+ optIdx = i
2486
+ break
2487
+ }
2488
+ }
2489
+ if (optIdx <= 0) return null
2490
+ const opt = path[optIdx]
2491
+ const headExpr = opt[1]
2492
+ const t = temp('oc')
2493
+ let rebuilt
2494
+ if (opt[0] === '?.') rebuilt = ['.', t, opt[2]]
2495
+ else if (opt[0] === '?.[]') rebuilt = ['[]', t, opt[2]]
2496
+ else rebuilt = ['()', t, ...opt.slice(2)]
2497
+ for (let i = optIdx - 1; i >= 0; i--) {
2498
+ const outer = path[i]
2499
+ rebuilt = [outer[0], rebuilt, ...outer.slice(2)]
2500
+ }
2501
+ return typed(['block', ['result', 'f64'],
2502
+ ['local.set', `$${t}`, asF64(emit(headExpr))],
2503
+ ['if', ['result', 'f64'],
2504
+ ['i32.eqz', isNullish(typed(['local.get', `$${t}`], 'f64'))],
2505
+ ['then', asF64(emit(rebuilt))],
2506
+ ['else', undefExpr()]]], 'f64')
2507
+ }
2508
+
2011
2509
  /**
2012
2510
  * Emit single AST node to typed WASM IR.
2013
2511
  * Every returned node has .type = 'i32' | 'f64'.
@@ -2028,14 +2526,10 @@ export function emit(node, expect) {
2028
2526
  const n = node & 0xFFFFFFFFFFFFFFFFn
2029
2527
  return typed(['f64.reinterpret_i64', ['i64.const', '0x' + n.toString(16)]], 'f64')
2030
2528
  }
2031
- if (typeof node === 'number') {
2032
- if (Number.isInteger(node) && node >= -2147483648 && node <= 2147483647)
2033
- return typed(['i32.const', node], 'i32')
2034
- return typed(['f64.const', node], 'f64')
2035
- }
2529
+ if (typeof node === 'number') return emitNum(node)
2036
2530
  if (typeof node === 'string') {
2037
2531
  // Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
2038
- if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node))
2532
+ if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node) || repOf(node)?.intConst != null)
2039
2533
  return readVar(node)
2040
2534
  // Top-level function used as value → wrap as closure pointer for call_indirect
2041
2535
  if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
@@ -2072,17 +2566,28 @@ export function emit(node, expect) {
2072
2566
  }
2073
2567
  let idx = ctx.closure.table.indexOf(trampolineName)
2074
2568
  if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
2075
- return mkPtrIR(PTR.CLOSURE, idx, 0)
2569
+ const ir = mkPtrIR(PTR.CLOSURE, idx, 0)
2570
+ ir.closureFuncIdx = idx
2571
+ return ir
2076
2572
  }
2077
2573
  // Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
2078
- if (node.includes('.') && ctx.core.emit[node]) return ctx.core.emit[node]()
2574
+ if (node.includes('.') && ctx.core.emit[node]) {
2575
+ const handler = ctx.core.emit[node]
2576
+ return handler.length > 0 ? builtinFunctionValue(node) : handler()
2577
+ }
2079
2578
  // Auto-import known host globals (WebAssembly, globalThis, etc.)
2080
2579
  const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
2081
2580
  if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
2581
+ if (ctx.transform.host === 'wasi') err(`host:'wasi': reference to host global \`${node}\` requires an env import. Remove the reference or use host:'js'.`)
2082
2582
  ctx.features.external = true
2083
2583
  ctx.scope.globals.set(node, null)
2084
- ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, ['mut', 'f64']]])
2085
- return typed(['global.get', `$${node}`], 'f64')
2584
+ // Carrier is i64 (not f64) so V8 can't canonicalize the NaN-boxed
2585
+ // external-ref payload across the wasm↔JS global boundary (same hazard
2586
+ // as env.print/setTimeout — see module/console.js header). asF64()
2587
+ // reinterprets to f64 at every read site.
2588
+ ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, 'i64']])
2589
+ ctx.scope.globalTypes.set(node, 'i64')
2590
+ return typed(['global.get', `$${node}`], 'i64')
2086
2591
  }
2087
2592
  const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
2088
2593
  return typed(['local.get', `$${node}`], t)
@@ -2099,6 +2604,14 @@ export function emit(node, expect) {
2099
2604
  return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
2100
2605
  }
2101
2606
 
2607
+ // Optional-chain continuation: `a?.b.c` → if `a` nullish then undefined else `a.b.c`.
2608
+ // Lift before dispatch so the regular `.` / `[]` / `()` handler sees the rebuilt chain
2609
+ // with the optional already replaced by a non-optional access on a guarded temp.
2610
+ if (op === '.' || op === '[]' || op === '()') {
2611
+ const lifted = liftOptionalChain(node)
2612
+ if (lifted) return lifted
2613
+ }
2614
+
2102
2615
  const handler = ctx.core.emit[op]
2103
2616
  if (!handler) err(`Unknown op: ${op}`)
2104
2617
  return handler(...args)