jz 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/analyze.js CHANGED
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { ctx, err } from './ctx.js'
27
+ import { isLiteralStr, isFuncRef } from './ir.js'
27
28
 
28
29
  export const T = '\uE000'
29
30
 
@@ -89,7 +90,7 @@ export const VAL = {
89
90
  NUMBER: 'number', ARRAY: 'array', STRING: 'string',
90
91
  OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
91
92
  CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
92
- BIGINT: 'bigint', BUFFER: 'buffer',
93
+ BIGINT: 'bigint', BUFFER: 'buffer', DATE: 'date',
93
94
  }
94
95
 
95
96
  /**
@@ -233,6 +234,7 @@ export function valTypeOf(expr) {
233
234
  // Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint
234
235
  if (args.length === 0) return null // undefined literal
235
236
  if (args[0] == null) return null // null literal
237
+ if (typeof args[0] === 'symbol') return null // prepared null sentinel
236
238
  return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
237
239
  }
238
240
 
@@ -241,16 +243,22 @@ export function valTypeOf(expr) {
241
243
  if (op === '=>') return VAL.CLOSURE
242
244
  if (op === '//') return VAL.REGEX
243
245
  if (op === '{}' && args[0]?.[0] === ':') return VAL.OBJECT
246
+ if (op === '?:') {
247
+ const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
248
+ return ta && ta === tb ? ta : null
249
+ }
244
250
  // `[]` op covers both array literals (1 arg) and index access (2 args).
245
251
  // Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
246
252
  // Index access: `arr[i]` → ['[]', arr, i].
247
253
  if (op === '[]') {
248
254
  if (args.length < 2) return VAL.ARRAY
249
- // Indexed read on a known typed-array receiver yields a number (BigInt64/BigUint64Array
250
- // would yield BigInt, but they're rare and we don't track per-elem type here — the
251
- // .typed:[] emit path already handles their f64-cast correctly; this only affects
252
- // arithmetic-time __to_num elision, where assuming Number is safe-by-construction).
253
- if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED) return VAL.NUMBER
255
+ // Indexed read on a known typed-array receiver yields Number except for
256
+ // BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
257
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
258
+ return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
259
+ // Indexed read on a STRING returns a 1-char string (SSO at runtime).
260
+ if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.STRING) return VAL.STRING
261
+ if (Array.isArray(args[0]) && valTypeOf(args[0]) === VAL.STRING) return VAL.STRING
254
262
  // Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
255
263
  // Set by analyzeValTypes from body observations + emitFunc preseed for params.
256
264
  if (typeof args[0] === 'string') {
@@ -267,12 +275,12 @@ export function valTypeOf(expr) {
267
275
  const slotVT = ctx.schema.slotVT(args[0], args[1])
268
276
  if (slotVT) return slotVT
269
277
  }
270
- // VAL.HASH `.prop` propagation: when the receiver chain roots at a binding
278
+ // OBJECT `.prop` propagation: when the receiver chain roots at a binding
271
279
  // sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
272
280
  // child's val-type. Generic for any compile-time-known JSON literal.
273
281
  if (op === '.' && typeof args[1] === 'string') {
274
282
  const sh = shapeOf(args[0])
275
- if (sh?.vt === VAL.HASH) {
283
+ if (sh?.vt === VAL.OBJECT || sh?.vt === VAL.HASH) {
276
284
  const child = sh.props[args[1]]
277
285
  if (child) return child.vt
278
286
  }
@@ -301,6 +309,7 @@ export function valTypeOf(expr) {
301
309
  if (typeof callee === 'string') {
302
310
  if (callee === 'new.Set') return VAL.SET
303
311
  if (callee === 'new.Map') return VAL.MAP
312
+ if (callee === 'new.Date') return VAL.DATE
304
313
  if (callee === 'new.ArrayBuffer') return VAL.BUFFER
305
314
  if (callee === 'new.DataView') return VAL.BUFFER
306
315
  if (callee.startsWith('new.')) return VAL.TYPED
@@ -310,7 +319,10 @@ export function valTypeOf(expr) {
310
319
  const src = jsonConstString(args[1])
311
320
  if (src != null) {
312
321
  const c = src.trimStart()[0]
313
- if (c === '{') return VAL.HASH
322
+ // Objects emit as fixed-shape OBJECT (slot-based) see
323
+ // module/json.js:emitJsonConstValue. The downstream `.prop` reads
324
+ // hit emitSchemaSlotRead via ctx.schema.find, bypassing hash probes.
325
+ if (c === '{') return VAL.OBJECT
314
326
  if (c === '[') return VAL.ARRAY
315
327
  if (c === '"') return VAL.STRING
316
328
  if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
@@ -340,16 +352,21 @@ export function valTypeOf(expr) {
340
352
  return null
341
353
  }
342
354
  if (method === 'push') return VAL.ARRAY
355
+ if ((method === 'shift' || method === 'pop') && typeof obj === 'string') {
356
+ const elemVt = ctx.func.repByLocal?.get(obj)?.arrayElemValType
357
+ if (elemVt) return elemVt
358
+ }
343
359
  if (method === 'add' || method === 'delete') return VAL.SET
344
360
  if (method === 'set') return VAL.MAP
345
361
  // String-returning methods
346
362
  if (['toUpperCase', 'toLowerCase', 'trim', 'trimStart', 'trimEnd',
347
- 'repeat', 'padStart', 'padEnd', 'replace', 'charAt', 'substring'].includes(method)) return VAL.STRING
363
+ 'repeat', 'padStart', 'padEnd', 'replace', 'replaceAll', 'charAt', 'substring'].includes(method)) return VAL.STRING
364
+ if (method === 'split') return VAL.ARRAY
348
365
  // slice/concat preserve caller type (string.slice → string, array.slice → array)
349
366
  if (method === 'slice' || method === 'concat') {
350
367
  const objType = valTypeOf(obj)
351
- if (objType) return objType
352
- return VAL.ARRAY // default to array when unknown
368
+ if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
369
+ return null
353
370
  }
354
371
  }
355
372
  }
@@ -359,7 +376,22 @@ export function valTypeOf(expr) {
359
376
  function jsonConstString(expr) {
360
377
  if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
361
378
  if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
362
- if (typeof expr === 'string') return ctx.scope.constStrs?.get(expr) ?? null
379
+ if (typeof expr === 'string') {
380
+ // Prefer shapeStrs (broader: includes effectively-const `let` string literals
381
+ // at module scope) over constStrs (const-only). Module/json's static-fold
382
+ // path uses its own constStrs-only resolver to avoid folding `let`-bound
383
+ // initializers — preserving the user-controlled distinction. Shape
384
+ // inference is sound either way: an effectively-const literal's value is
385
+ // invariant, so the parsed shape it produces is too.
386
+ return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
387
+ }
388
+ return null
389
+ }
390
+
391
+ function jsonShapeStrings(expr) {
392
+ const single = jsonConstString(expr)
393
+ if (single != null) return [single]
394
+ if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
363
395
  return null
364
396
  }
365
397
 
@@ -384,18 +416,19 @@ function shapeOfJsonValue(v) {
384
416
  }
385
417
  if (typeof v === 'object') {
386
418
  const props = Object.create(null)
387
- for (const k of Object.keys(v)) {
419
+ const names = Object.keys(v)
420
+ for (const k of names) {
388
421
  const s = shapeOfJsonValue(v[k])
389
422
  if (s) props[k] = s
390
423
  }
391
- return { vt: VAL.HASH, props }
424
+ return { vt: VAL.OBJECT, props, names }
392
425
  }
393
426
  return null
394
427
  }
395
428
 
396
429
  function shapeUnifies(a, b) {
397
430
  if (!a || !b || a.vt !== b.vt) return false
398
- if (a.vt === VAL.HASH) {
431
+ if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
399
432
  const ak = Object.keys(a.props), bk = Object.keys(b.props)
400
433
  if (ak.length !== bk.length) return false
401
434
  for (const k of ak) {
@@ -409,6 +442,16 @@ function shapeUnifies(a, b) {
409
442
  return true
410
443
  }
411
444
 
445
+ function shapeLayoutUnifies(a, b) {
446
+ if (!shapeUnifies(a, b)) return false
447
+ if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
448
+ if (a.names?.length !== b.names?.length) return false
449
+ for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
450
+ }
451
+ if (a.vt === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
452
+ return true
453
+ }
454
+
412
455
  const _jsonShapeCache = new WeakMap()
413
456
  function parseJsonShape(src) {
414
457
  if (typeof src !== 'string') return null
@@ -420,6 +463,18 @@ function parseJsonShape(src) {
420
463
  return sh
421
464
  }
422
465
 
466
+ function parseUnifiedJsonShape(srcs) {
467
+ if (!srcs?.length) return null
468
+ let out = null
469
+ for (const src of srcs) {
470
+ const sh = parseJsonShape(src)
471
+ if (!sh) return null
472
+ if (!out) out = sh
473
+ else if (!shapeLayoutUnifies(out, sh)) return null
474
+ }
475
+ return out
476
+ }
477
+
423
478
  /** Resolve the json shape for an expression by walking name → rep.jsonShape and
424
479
  * `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
425
480
  export function shapeOf(expr) {
@@ -427,12 +482,12 @@ export function shapeOf(expr) {
427
482
  if (!Array.isArray(expr)) return null
428
483
  const [op, ...args] = expr
429
484
  if (op === '()' && args[0] === 'JSON.parse') {
430
- const src = jsonConstString(args[1])
431
- if (src != null) return parseJsonShape(src)
485
+ const srcs = jsonShapeStrings(args[1])
486
+ if (srcs) return parseUnifiedJsonShape(srcs)
432
487
  }
433
488
  if (op === '.' && typeof args[1] === 'string') {
434
489
  const parent = shapeOf(args[0])
435
- if (parent?.vt === VAL.HASH) return parent.props[args[1]] || null
490
+ if (parent?.vt === VAL.OBJECT || parent?.vt === VAL.HASH) return parent.props[args[1]] || null
436
491
  }
437
492
  if (op === '[]' && args.length === 2) {
438
493
  const parent = shapeOf(args[0])
@@ -442,6 +497,84 @@ export function shapeOf(expr) {
442
497
  }
443
498
 
444
499
 
500
+ /** Static property-key evaluation for computed member names: folds a node into
501
+ * its constant value (numeric, string, boolean, null) and stringifies it. Returns
502
+ * null if any sub-expression isn't statically known. Handles literals, named
503
+ * string constants, String()/Number() casts, ternaries, short-circuit ops, and
504
+ * unary/binary arithmetic and bit ops. */
505
+ const NO_VALUE = Symbol('no-static-property-key')
506
+
507
+ export function staticPropertyKey(node) {
508
+ const value = staticValue(node)
509
+ return value === NO_VALUE ? null : String(value)
510
+ }
511
+
512
+ function staticValue(node) {
513
+ if (node === undefined) return undefined
514
+ if (node === null || typeof node === 'number' || typeof node === 'boolean') return node
515
+ if (typeof node === 'string') return ctx.scope.constStrs?.get(node) ?? NO_VALUE
516
+ if (!Array.isArray(node)) return NO_VALUE
517
+
518
+ const [op, ...args] = node
519
+ if (op == null) return args.length ? args[0] : undefined
520
+ if (op === 'str') return args[0]
521
+ if (op === '[]' && args.length === 1) return staticValue(args[0])
522
+ if (op === '()' && args[0] === 'String' && args.length === 2) {
523
+ const value = staticValue(args[1])
524
+ return value === NO_VALUE ? NO_VALUE : String(value)
525
+ }
526
+ if (op === '()' && args[0] === 'Number' && args.length === 2) {
527
+ const value = staticValue(args[1])
528
+ return value === NO_VALUE ? NO_VALUE : Number(value)
529
+ }
530
+ if (op === '?:' || op === '?') {
531
+ const cond = staticValue(args[0])
532
+ return cond === NO_VALUE ? NO_VALUE : staticValue(cond ? args[1] : args[2])
533
+ }
534
+ if (op === '&&' || op === '||') {
535
+ const left = staticValue(args[0])
536
+ if (left === NO_VALUE) return NO_VALUE
537
+ return op === '&&' ? (left ? staticValue(args[1]) : left) : (left ? left : staticValue(args[1]))
538
+ }
539
+ if (op === '??') {
540
+ const left = staticValue(args[0])
541
+ return left === NO_VALUE ? NO_VALUE : left == null ? staticValue(args[1]) : left
542
+ }
543
+
544
+ if (args.length === 1) {
545
+ const value = staticValue(args[0])
546
+ if (value === NO_VALUE) return NO_VALUE
547
+ if (op === 'u+') return +value
548
+ if (op === 'u-') return -value
549
+ if (op === '!') return !value
550
+ if (op === '~') return ~value
551
+ return NO_VALUE
552
+ }
553
+
554
+ if (args.length === 2) {
555
+ const left = staticValue(args[0])
556
+ const right = staticValue(args[1])
557
+ if (left === NO_VALUE || right === NO_VALUE) return NO_VALUE
558
+ switch (op) {
559
+ case '+': return typeof left === 'string' || typeof right === 'string' ? String(left) + String(right) : Number(left) + Number(right)
560
+ case '-': return Number(left) - Number(right)
561
+ case '*': return Number(left) * Number(right)
562
+ case '/': return Number(left) / Number(right)
563
+ case '%': return Number(left) % Number(right)
564
+ case '**': return Number(left) ** Number(right)
565
+ case '&': return Number(left) & Number(right)
566
+ case '|': return Number(left) | Number(right)
567
+ case '^': return Number(left) ^ Number(right)
568
+ case '<<': return Number(left) << Number(right)
569
+ case '>>': return Number(left) >> Number(right)
570
+ case '>>>': return Number(left) >>> Number(right)
571
+ default: return NO_VALUE
572
+ }
573
+ }
574
+
575
+ return NO_VALUE
576
+ }
577
+
445
578
  /** Decode a `['{}', ...]` AST's children into `{names, values}`, or null if any
446
579
  * property is non-static-key (computed key, spread, shorthand). Matches the
447
580
  * emitter's flatten rule for comma-grouped props. Used by collectProgramFacts,
@@ -457,6 +590,15 @@ export function staticObjectProps(args) {
457
590
  return names.length ? { names, values } : null
458
591
  }
459
592
 
593
+ function staticArrayElems(expr) {
594
+ if (!Array.isArray(expr)) return null
595
+ if (expr[0] === '[') return expr.slice(1)
596
+ if (expr[0] !== '[]' || expr.length >= 3) return null
597
+ const arg = expr[1]
598
+ if (arg == null) return []
599
+ return Array.isArray(arg) && arg[0] === ',' ? arg.slice(1) : [arg]
600
+ }
601
+
460
602
  /** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
461
603
  function objLiteralSchemaId(expr) {
462
604
  if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
@@ -508,7 +650,7 @@ export function typedElemCtor(rhs) {
508
650
  const _ELEM_AUX = {
509
651
  Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
510
652
  Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
511
- BigInt64Array: 7, BigUint64Array: 7,
653
+ BigInt64Array: 23, BigUint64Array: 23,
512
654
  }
513
655
  /** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
514
656
  * aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
@@ -529,11 +671,41 @@ const _ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
529
671
  export function ctorFromElemAux(aux) {
530
672
  if (aux == null) return null
531
673
  const isView = (aux & 8) !== 0
532
- const name = _ELEM_NAMES[aux & 7]
674
+ const name = (aux & 16) !== 0 ? 'BigInt64Array' : _ELEM_NAMES[aux & 7]
533
675
  if (!name) return null
534
676
  return isView ? `new.${name}.view` : `new.${name}`
535
677
  }
536
678
 
679
+ /** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
680
+ * different typed-array ctors — caller should drop any cached entry rather
681
+ * than leave a stale ctor (which would lock the wrong store width). */
682
+ export const MIXED_CTORS = Symbol('MIXED_CTORS')
683
+
684
+ const typedCtorElemValType = (ctor) => {
685
+ if (!ctor) return null
686
+ const isView = ctor.endsWith('.view')
687
+ const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
688
+ return name === 'BigInt64Array' || name === 'BigUint64Array' ? VAL.BIGINT : VAL.NUMBER
689
+ }
690
+
691
+ /** A `?:`/`&&`/`||` expression — value depends on a condition, so its ctor
692
+ * must be derived by walking branches (handled by `ternaryCtorOfRhs`). */
693
+ const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e[0] === '||')
694
+
695
+ /** Walk a `?:`/`&&`/`||` expression and return:
696
+ * - a single ctor string when every branch resolves to the same ctor,
697
+ * - MIXED_CTORS when branches resolve to different ctors,
698
+ * - null when no branch resolves (caller's behavior unchanged). */
699
+ export function ternaryCtorOfRhs(rhs) {
700
+ if (!Array.isArray(rhs)) return null
701
+ const op = rhs[0]
702
+ const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
703
+ if (!lo) return null
704
+ const a = ternaryCtorOfRhs(rhs[lo]) ?? typedElemCtor(rhs[lo])
705
+ const b = ternaryCtorOfRhs(rhs[lo + 1]) ?? typedElemCtor(rhs[lo + 1])
706
+ return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
707
+ }
708
+
537
709
  // === Cross-call argument inference helpers (used by narrowSignatures fixpoint) ===
538
710
  // Each `inferArg*(expr, ...callerCtx)` resolves an argument expression to a single
539
711
  // fact (val/schemaId/elem*/typedCtor) using caller-local observations and program
@@ -668,7 +840,7 @@ export function analyzeBody(body) {
668
840
  // for any slice and can't be WeakMap-keyed. Return empty maps without caching.
669
841
  if (body === null || typeof body !== 'object') return {
670
842
  locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
671
- arrElemValTypes: new Map(), typedElems: new Map(),
843
+ arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
672
844
  }
673
845
  const hit = _bodyFactsCache.get(body)
674
846
  if (hit) return hit
@@ -678,6 +850,8 @@ export function analyzeBody(body) {
678
850
  const arrElemSchemas = new Map()
679
851
  const arrElemValTypes = new Map()
680
852
  const typedElems = new Map()
853
+ const escapes = new Map() // name → bool: local holds allocation, true if it escapes
854
+ const valPoison = new Set()
681
855
 
682
856
  const doSchemas = !!ctx.schema?.register
683
857
  // Per-walk local schema map for chained `arr.push(name)` resolution.
@@ -725,16 +899,48 @@ export function analyzeBody(body) {
725
899
  return valTypeOf(expr)
726
900
  }
727
901
 
902
+ const typedPoison = new Set()
903
+ const trackVal = (name, vt) => {
904
+ if (valPoison.has(name)) return
905
+ const prev = valTypes.get(name)
906
+ if (!vt) {
907
+ if (prev) valPoison.add(name)
908
+ valTypes.delete(name)
909
+ return
910
+ }
911
+ if (prev && prev !== vt) {
912
+ valPoison.add(name)
913
+ valTypes.delete(name)
914
+ return
915
+ }
916
+ valTypes.set(name, vt)
917
+ }
918
+ const invalidateTyped = (name) => {
919
+ typedPoison.add(name)
920
+ typedElems.delete(name)
921
+ }
728
922
  const trackTyped = (name, rhs) => {
923
+ if (typedPoison.has(name)) return
924
+ const setOrInvalidate = (c) => {
925
+ if (c === MIXED_CTORS) return invalidateTyped(name)
926
+ const prev = typedElems.get(name)
927
+ if (prev && prev !== c) invalidateTyped(name)
928
+ else typedElems.set(name, c)
929
+ }
729
930
  const ctor = typedElemCtor(rhs)
730
- if (ctor) { typedElems.set(name, ctor); return }
931
+ if (ctor) return setOrInvalidate(ctor)
731
932
  if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
732
933
  const f = ctx.func.map?.get(rhs[1])
733
934
  if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
734
935
  const c = ctorFromElemAux(f.sig.ptrAux)
735
- if (c) typedElems.set(name, c)
936
+ if (c) setOrInvalidate(c)
736
937
  }
938
+ return
737
939
  }
940
+ // Heterogeneous ternary: ctors that don't unify must invalidate so a sibling
941
+ // decl (jz hoists `let` to function scope) can't lock in the wrong width.
942
+ const tc = ternaryCtorOfRhs(rhs)
943
+ if (tc) setOrInvalidate(tc)
738
944
  }
739
945
 
740
946
  // === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
@@ -745,8 +951,7 @@ export function analyzeBody(body) {
745
951
  else if (locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
746
952
 
747
953
  // val type (valTypes slice)
748
- const vt = valTypeOf(rhs)
749
- if (vt) valTypes.set(name, vt); else valTypes.delete(name)
954
+ trackVal(name, valTypeOf(rhs))
750
955
 
751
956
  // typed-array element ctor (typedElems slice)
752
957
  trackTyped(name, rhs)
@@ -755,14 +960,17 @@ export function analyzeBody(body) {
755
960
  if (doSchemas) {
756
961
  const sid = exprSchemaId(rhs, localSchemaMap)
757
962
  if (sid != null) localSchemaMap.set(name, sid)
758
- if (Array.isArray(rhs) && rhs[0] === '[]') {
759
- const elems = rhs.slice(1).filter(e => e != null)
760
- if (elems.length) {
761
- let common = exprSchemaId(elems[0], localSchemaMap)
762
- for (let k = 1; k < elems.length && common != null; k++) {
763
- if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
963
+ {
964
+ const rawElems = staticArrayElems(rhs)
965
+ if (rawElems) {
966
+ const elems = rawElems.filter(e => e != null)
967
+ if (elems.length && elems.length === rawElems.length) {
968
+ let common = exprSchemaId(elems[0], localSchemaMap)
969
+ for (let k = 1; k < elems.length && common != null; k++) {
970
+ if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
971
+ }
972
+ if (common != null) observeArrSchema(name, common)
764
973
  }
765
- if (common != null) observeArrSchema(name, common)
766
974
  }
767
975
  }
768
976
  if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
@@ -780,14 +988,17 @@ export function analyzeBody(body) {
780
988
  }
781
989
 
782
990
  // arr-elem val type (arrElemValTypes slice) — array-literal init + call return + alias + .map/.filter/.slice/.concat chain
783
- if (Array.isArray(rhs) && rhs[0] === '[]') {
784
- const elems = rhs.slice(1).filter(e => e != null)
785
- if (elems.length) {
786
- let common = exprElemSourceVal(elems[0])
787
- for (let k = 1; k < elems.length && common != null; k++) {
788
- if (exprElemSourceVal(elems[k]) !== common) common = null
991
+ {
992
+ const rawElems = staticArrayElems(rhs)
993
+ if (rawElems) {
994
+ const elems = rawElems.filter(e => e != null)
995
+ if (elems.length && elems.length === rawElems.length) {
996
+ let common = exprElemSourceVal(elems[0])
997
+ for (let k = 1; k < elems.length && common != null; k++) {
998
+ if (exprElemSourceVal(elems[k]) !== common) common = null
999
+ }
1000
+ if (common != null) observeArrValType(name, common)
789
1001
  }
790
- if (common != null) observeArrValType(name, common)
791
1002
  }
792
1003
  }
793
1004
  if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
@@ -805,6 +1016,8 @@ export function analyzeBody(body) {
805
1016
  if (method === 'filter' || method === 'slice' || method === 'concat') {
806
1017
  const v = elemValOf(recvName)
807
1018
  if (v) observeArrValType(name, v)
1019
+ } else if (method === 'split' && valTypeOf(recvName) === VAL.STRING) {
1020
+ observeArrValType(name, VAL.STRING)
808
1021
  } else if (method === 'map') {
809
1022
  const arrowFn = rhs[2]
810
1023
  const recvVt = elemValOf(recvName)
@@ -830,14 +1043,47 @@ export function analyzeBody(body) {
830
1043
  }
831
1044
  }
832
1045
  }
1046
+ if (Array.isArray(rhs) && rhs[0] === '()' &&
1047
+ Array.isArray(rhs[1]) && rhs[1][0] === '.' && rhs[1][2] === 'split' &&
1048
+ valTypeOf(rhs[1][1]) === VAL.STRING) {
1049
+ observeArrValType(name, VAL.STRING)
1050
+ }
833
1051
  }
834
1052
 
835
1053
  // arrElem invalidation rule — fires on `=` reassign of tracked name to non-array
836
1054
  const isArrayProducingRhs = (rhs) =>
837
- Array.isArray(rhs) && (rhs[0] === '[]' ||
1055
+ Array.isArray(rhs) && (staticArrayElems(rhs) != null ||
838
1056
  (rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
839
1057
  (rhs[1][2] === 'slice' || rhs[1][2] === 'concat')))
840
1058
 
1059
+ const markEscape = (name) => { if (escapes.has(name)) escapes.set(name, true) }
1060
+
1061
+ const isStaticIndex = (key) =>
1062
+ typeof key === 'number' || typeof key === 'string' ||
1063
+ (Array.isArray(key) && ((key[0] == null && Number.isInteger(key[1])) || key[0] === 'str')) ||
1064
+ staticPropertyKey(key) != null
1065
+
1066
+ const markEscapeValue = (expr) => {
1067
+ if (typeof expr === 'string') { markEscape(expr); return }
1068
+ if (!Array.isArray(expr)) return
1069
+ const op = expr[0]
1070
+ if (op === 'str') return
1071
+ if (op === ':') { markEscapeValue(expr[2]); return }
1072
+ if ((op === '.' || op === '?.') && typeof expr[1] === 'string' && escapes.has(expr[1])) return
1073
+ if (op === '[]' && typeof expr[1] === 'string' && escapes.has(expr[1])) {
1074
+ if (!isStaticIndex(expr[2])) markEscape(expr[1])
1075
+ markEscapeValue(expr[2])
1076
+ return
1077
+ }
1078
+ for (let i = 1; i < expr.length; i++) markEscapeValue(expr[i])
1079
+ }
1080
+
1081
+ const markEscapeArgs = (args) => {
1082
+ if (args == null) return
1083
+ const list = Array.isArray(args) && args[0] === ',' ? args.slice(1) : [args]
1084
+ for (const a of list) markEscapeValue(Array.isArray(a) && a[0] === '...' ? a[1] : a)
1085
+ }
1086
+
841
1087
  // === Single walk ===
842
1088
  function walk(node) {
843
1089
  if (!Array.isArray(node)) return
@@ -858,6 +1104,10 @@ export function analyzeBody(body) {
858
1104
  }
859
1105
  const name = a[1], rhs = a[2]
860
1106
  processDecl(name, rhs)
1107
+ if (Array.isArray(rhs) && (rhs[0] === '[' || rhs[0] === '{}')) {
1108
+ escapes.set(name, false)
1109
+ }
1110
+ markEscapeValue(rhs)
861
1111
  // Walk rhs only — never enter the `=` node so the reassignment-invalidation
862
1112
  // rule won't misfire on the binding's own initializer.
863
1113
  walk(rhs)
@@ -865,6 +1115,14 @@ export function analyzeBody(body) {
865
1115
  return
866
1116
  }
867
1117
 
1118
+ if (op === 'return' && node[1] != null) {
1119
+ markEscapeValue(node[1])
1120
+ }
1121
+
1122
+ if (op === '()' && node.length > 2) {
1123
+ markEscapeArgs(node[2])
1124
+ }
1125
+
868
1126
  // arr.push(...) — observe both schemas and val types in one pass
869
1127
  if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && node[1][2] === 'push' && typeof node[1][1] === 'string') {
870
1128
  const arr = node[1][1]
@@ -884,13 +1142,16 @@ export function analyzeBody(body) {
884
1142
  // arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
885
1143
  if (op === '=' && typeof node[1] === 'string') {
886
1144
  const name = node[1], rhs = node[2]
1145
+ walk(rhs)
1146
+ markEscape(name)
1147
+ markEscapeValue(rhs)
887
1148
  const wt = exprType(rhs, locals)
888
1149
  if (locals.has(name) && locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
889
- const vt = valTypeOf(rhs)
890
- if (vt) valTypes.set(name, vt); else valTypes.delete(name)
1150
+ trackVal(name, valTypeOf(rhs))
891
1151
  trackTyped(name, rhs)
892
1152
  if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
893
1153
  if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
1154
+ return
894
1155
  }
895
1156
 
896
1157
  // compound-assign widening (locals slice)
@@ -903,6 +1164,28 @@ export function analyzeBody(body) {
903
1164
  if (locals.has(node[1])) locals.set(node[1], 'f64')
904
1165
  }
905
1166
 
1167
+ if (op === 'for' || op === 'for-in' || op === 'for-of') {
1168
+ if (node[1] != null) markEscapeValue(node[1])
1169
+ }
1170
+
1171
+ if (op === '[' || op === '{}') {
1172
+ for (let i = 1; i < node.length; i++) {
1173
+ const c = node[i]
1174
+ if (Array.isArray(c) && c[0] === ',') {
1175
+ for (let j = 1; j < c.length; j++) {
1176
+ if (Array.isArray(c[j]) && c[j][0] === '...') markEscapeValue(c[j][1])
1177
+ }
1178
+ } else if (Array.isArray(c) && c[0] === '...') {
1179
+ markEscapeValue(c[1])
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ if (op === '[]' && typeof node[1] === 'string' && escapes.has(node[1])) {
1185
+ const key = node[2]
1186
+ if (!isStaticIndex(key)) markEscape(node[1])
1187
+ }
1188
+
906
1189
  for (let i = 1; i < node.length; i++) walk(node[i])
907
1190
  }
908
1191
 
@@ -910,8 +1193,13 @@ export function analyzeBody(body) {
910
1193
  // resolve chains (`const a = new TypedArr(); const b = a[0]` → b: NUMBER)
911
1194
  // and shorthand-bound `{a}` props see a's type. Restored after walk completes.
912
1195
  const prevOverlay = ctx.func.localValTypesOverlay
1196
+ const prevTypedOverlay = ctx.func.localTypedElemsOverlay
913
1197
  ctx.func.localValTypesOverlay = valTypes
914
- try { walk(body) } finally { ctx.func.localValTypesOverlay = prevOverlay }
1198
+ ctx.func.localTypedElemsOverlay = typedElems
1199
+ try { walk(body) } finally {
1200
+ ctx.func.localValTypesOverlay = prevOverlay
1201
+ ctx.func.localTypedElemsOverlay = prevTypedOverlay
1202
+ }
915
1203
 
916
1204
  // Second pass: widen i32 locals compared against f64.
917
1205
  const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
@@ -928,7 +1216,36 @@ export function analyzeBody(body) {
928
1216
  }
929
1217
  widenPass(body)
930
1218
 
931
- const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems }
1219
+ // Re-resolve let-decl RHS types now that widenPass has widened. A `let x2 =
1220
+ // zx*zx` declared at i32 because zx was i32 at scan time must widen if zx
1221
+ // was later re-typed to f64. Without this, integer-init locals shadowed
1222
+ // by f64-arithmetic RHSs end up with `i32.trunc_sat_f64_s` truncating the
1223
+ // fractional value (e.g. mandelbrot escape: `x2 = zx*zx` losing 3.515 → 3).
1224
+ // Monotonic: only widens i32 → f64. Bound by locals count so no infinite loop.
1225
+ let widened = true
1226
+ while (widened) {
1227
+ widened = false
1228
+ const recheck = (node) => {
1229
+ if (!Array.isArray(node)) return
1230
+ const op = node[0]
1231
+ if (op === '=>') return
1232
+ if (op === 'let' || op === 'const') {
1233
+ for (let i = 1; i < node.length; i++) {
1234
+ const a = node[i]
1235
+ if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
1236
+ const name = a[1], rhs = a[2]
1237
+ if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64') {
1238
+ locals.set(name, 'f64'); widened = true
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ for (let i = 1; i < node.length; i++) recheck(node[i])
1244
+ }
1245
+ recheck(body)
1246
+ }
1247
+
1248
+ const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes }
932
1249
  _bodyFactsCache.set(body, result)
933
1250
  return result
934
1251
  }
@@ -941,6 +1258,69 @@ export function invalidateLocalsCache(body) {
941
1258
  if (body && typeof body === 'object') _bodyFactsCache.delete(body)
942
1259
  }
943
1260
 
1261
+ // String-only methods: presence of `name.method(...)` is definite evidence that
1262
+ // `name` is VAL.STRING. Excludes ambiguous methods (length, slice, indexOf, [],
1263
+ // concat) that strings share with arrays/typed-arrays.
1264
+ const STRING_ONLY_METHODS = new Set([
1265
+ 'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
1266
+ 'toUpperCase', 'toLowerCase', 'normalize', 'localeCompare',
1267
+ 'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
1268
+ 'matchAll', 'match', 'replace', 'replaceAll', 'split',
1269
+ ])
1270
+ // Array-only methods: presence rules out STRING.
1271
+ const ARRAY_ONLY_METHODS = new Set([
1272
+ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
1273
+ 'flat', 'flatMap', 'copyWithin',
1274
+ ])
1275
+
1276
+ /** Infer VAL.STRING for `names` from method-call evidence in `body`.
1277
+ * Descends into nested closures (captured names retain shape) but respects
1278
+ * shadowing: a param of a nested `=>` removes that name from the inference
1279
+ * scope inside that closure. Returns Map<name, VAL.STRING> for names that
1280
+ * have at least one string-only method call and no array-only conflicts. */
1281
+ export function inferStringParams(body, names) {
1282
+ if (!names || names.length === 0) return new Map()
1283
+ const evidence = new Map() // name → 'string' | 'conflict'
1284
+ function walk(node, scope) {
1285
+ if (!Array.isArray(node) || scope.size === 0) return
1286
+ const op = node[0]
1287
+ if (op === '=>') {
1288
+ const shadowed = collectParamNames([node[1]])
1289
+ let inner = scope
1290
+ for (const s of shadowed) {
1291
+ if (inner.has(s)) {
1292
+ if (inner === scope) inner = new Set(scope)
1293
+ inner.delete(s)
1294
+ }
1295
+ }
1296
+ walk(node[2], inner)
1297
+ return
1298
+ }
1299
+ // `name.method` — observe positive/negative evidence
1300
+ if (op === '.' && typeof node[1] === 'string' && scope.has(node[1])) {
1301
+ const m = node[2]
1302
+ if (typeof m === 'string') {
1303
+ if (STRING_ONLY_METHODS.has(m)) {
1304
+ if (evidence.get(node[1]) !== 'conflict') evidence.set(node[1], 'string')
1305
+ } else if (ARRAY_ONLY_METHODS.has(m)) {
1306
+ evidence.set(node[1], 'conflict')
1307
+ }
1308
+ }
1309
+ }
1310
+ // Reassignment to an unambiguously non-string RHS poisons the inference
1311
+ if (op === '=' && typeof node[1] === 'string' && scope.has(node[1])) {
1312
+ const rhs = node[2]
1313
+ const vt = valTypeOf(rhs)
1314
+ if (vt && vt !== VAL.STRING) evidence.set(node[1], 'conflict')
1315
+ }
1316
+ for (let i = 1; i < node.length; i++) walk(node[i], scope)
1317
+ }
1318
+ walk(body, new Set(names))
1319
+ const out = new Map()
1320
+ for (const [n, ev] of evidence) if (ev === 'string') out.set(n, VAL.STRING)
1321
+ return out
1322
+ }
1323
+
944
1324
  /** Drop the cached analyzeBody entry. Used after E2-phase valResult narrowing
945
1325
  * so the next walk re-evaluates `valTypeOf(call)` with up-to-date `f.valResult`
946
1326
  * — required for the D-pass paramReps val/arrayElemSchema re-fixpoint to see
@@ -955,7 +1335,22 @@ export function invalidateValTypesCache(body) {
955
1335
  * and schema resolution.
956
1336
  */
957
1337
  export function analyzeValTypes(body) {
958
- const setVal = (name, vt) => updateRep(name, { val: vt || undefined })
1338
+ const valPoison = new Set()
1339
+ const setVal = (name, vt) => {
1340
+ if (valPoison.has(name)) return
1341
+ const prev = ctx.func.repByLocal?.get(name)?.val
1342
+ if (!vt) {
1343
+ if (prev) valPoison.add(name)
1344
+ updateRep(name, { val: undefined })
1345
+ return
1346
+ }
1347
+ if (prev && prev !== vt) {
1348
+ valPoison.add(name)
1349
+ updateRep(name, { val: undefined })
1350
+ return
1351
+ }
1352
+ updateRep(name, { val: vt })
1353
+ }
959
1354
  const getVal = name => ctx.func.repByLocal?.get(name)?.val
960
1355
  // Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
961
1356
  // on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
@@ -967,6 +1362,13 @@ export function analyzeValTypes(body) {
967
1362
  for (const [name, vt] of facts.arrElemValTypes) {
968
1363
  if (vt != null) updateRep(name, { arrayElemValType: vt })
969
1364
  }
1365
+ // Propagate body-observed array-elem schemas to repByLocal so analyzePtrUnboxable's
1366
+ // `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
1367
+ // to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
1368
+ // pay an i64.reinterpret/i32.wrap on every slot access (no aliasing → CSE can't fold).
1369
+ for (const [name, sid] of arrElems) {
1370
+ if (sid != null) updateRep(name, { arrayElemSchema: sid })
1371
+ }
970
1372
  // Resolve a name's array-elem-schema, preferring rep.arrayElemSchema (set from
971
1373
  // paramReps[k].arrayElemSchema at emit start) over local body observations.
972
1374
  const arrElemSchemaOf = (name) => {
@@ -979,20 +1381,42 @@ export function analyzeValTypes(body) {
979
1381
  function trackRegex(name, rhs) {
980
1382
  if (ctx.runtime.regex && Array.isArray(rhs) && rhs[0] === '//') ctx.runtime.regex.vars.set(name, rhs)
981
1383
  }
1384
+ // Names whose decls disagree on element ctor (e.g. two `let arr = ...` decls
1385
+ // in different scopes — jz hoists `let` to function scope so they share a name).
1386
+ // Once invalidated, no later setter can re-establish a definite ctor.
1387
+ const typedPoison = new Set()
1388
+ const invalidate = (name) => {
1389
+ typedPoison.add(name)
1390
+ ctx.types.typedElem?.delete(name)
1391
+ }
982
1392
  function trackTyped(name, rhs) {
983
1393
  if (!ctx.types.typedElem) ctx.types.typedElem = new Map() // first use in this function scope
1394
+ if (typedPoison.has(name)) return
1395
+ const setOrInvalidate = (c) => {
1396
+ if (c === MIXED_CTORS) return invalidate(name)
1397
+ const prev = ctx.types.typedElem.get(name)
1398
+ if (prev && prev !== c) invalidate(name)
1399
+ else ctx.types.typedElem.set(name, c)
1400
+ }
984
1401
  const ctor = typedElemCtor(rhs)
985
- if (ctor) { ctx.types.typedElem.set(name, ctor); return }
1402
+ if (ctor) return setOrInvalidate(ctor)
986
1403
  // TYPED-narrowed call result carries elem aux on f.sig.ptrAux — reverse-map it
987
1404
  // back to a canonical ctor string so analyzePtrUnboxable's typedElemAux lookup
988
1405
  // (compile.js) restores the same aux on the unboxed local's rep.
989
1406
  if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
990
1407
  const f = ctx.func.map?.get(rhs[1])
991
1408
  if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
992
- const ctor = ctorFromElemAux(f.sig.ptrAux)
993
- if (ctor) ctx.types.typedElem.set(name, ctor)
1409
+ const c = ctorFromElemAux(f.sig.ptrAux)
1410
+ if (c) setOrInvalidate(c)
994
1411
  }
1412
+ return
995
1413
  }
1414
+ // Heterogeneous ternary (e.g. `n === 16 ? new Uint8Array(16) : new Uint16Array(8)`):
1415
+ // typedElemCtor returns null for `?:`. When branches don't unify to the same ctor,
1416
+ // poison the name so a later sibling-scope decl (jz hoists `let` to function scope)
1417
+ // can't lock in the wrong store width.
1418
+ const tc = ternaryCtorOfRhs(rhs)
1419
+ if (tc) setOrInvalidate(tc)
996
1420
  }
997
1421
  function walk(node) {
998
1422
  if (!Array.isArray(node)) return
@@ -1018,7 +1442,10 @@ export function analyzeValTypes(body) {
1018
1442
  const vt = valTypeOf(a[2])
1019
1443
  setVal(a[1], vt)
1020
1444
  if (vt === VAL.REGEX) trackRegex(a[1], a[2])
1021
- if (vt === VAL.TYPED || vt === VAL.BUFFER) trackTyped(a[1], a[2])
1445
+ // VAL gate covers definite-typed RHS; `?:`/`&&`/`||` slip through valTypeOf
1446
+ // returning null but may still need ctor unification (or poisoning when
1447
+ // branches disagree, since jz hoists `let` to function scope).
1448
+ if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(a[2])) trackTyped(a[1], a[2])
1022
1449
  propagateTyped(a[1], a[2])
1023
1450
  // JSON-shape propagation. When the RHS resolves to a known JSON shape
1024
1451
  // (root: `JSON.parse(literal)`; nested: `o.meta`, `items[j]` from a known
@@ -1030,6 +1457,17 @@ export function analyzeValTypes(body) {
1030
1457
  updateRep(a[1], { jsonShape: sh })
1031
1458
  if (sh.vt === VAL.ARRAY && sh.elem?.vt) {
1032
1459
  updateRep(a[1], { arrayElemValType: sh.elem.vt })
1460
+ // Array of fixed-shape OBJECTs: register elem schema so `it = items[j]`
1461
+ // → `it.prop` lowers to slot read via the existing arr-elem-schema path.
1462
+ if (sh.elem.vt === VAL.OBJECT && sh.elem.names && ctx.schema.register) {
1463
+ const elemSid = ctx.schema.register(sh.elem.names)
1464
+ updateRep(a[1], { arrayElemSchema: elemSid })
1465
+ }
1466
+ }
1467
+ if (sh.vt === VAL.OBJECT && sh.names && ctx.schema.register) {
1468
+ const sid = ctx.schema.register(sh.names)
1469
+ updateRep(a[1], { schemaId: sid })
1470
+ ctx.schema.vars.set(a[1], sid)
1033
1471
  }
1034
1472
  }
1035
1473
  // Propagate schemaId from a narrowed call result so subsequent valTypeOf
@@ -1055,11 +1493,13 @@ export function analyzeValTypes(body) {
1055
1493
  }
1056
1494
  }
1057
1495
  if (op === '=' && typeof args[0] === 'string') {
1496
+ walk(args[1])
1058
1497
  const vt = valTypeOf(args[1])
1059
1498
  setVal(args[0], vt)
1060
1499
  if (vt === VAL.REGEX) trackRegex(args[0], args[1])
1061
- if (vt === VAL.TYPED || vt === VAL.BUFFER) trackTyped(args[0], args[1])
1500
+ if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(args[1])) trackTyped(args[0], args[1])
1062
1501
  propagateTyped(args[0], args[1])
1502
+ return
1063
1503
  }
1064
1504
  // Track property assignments for auto-boxing: x.prop = val
1065
1505
  if (op === '=' && Array.isArray(args[0]) && args[0][0] === '.' && typeof args[0][1] === 'string') {
@@ -1163,15 +1603,19 @@ export function analyzeIntCertain(body) {
1163
1603
  for (const name of defs.keys()) intCertain.set(name, true)
1164
1604
 
1165
1605
  const isIntExpr = (expr) => {
1166
- if (typeof expr === 'number') return Number.isInteger(expr)
1606
+ // -0 is integer-valued mathematically but i32 has no signed zero — must stay f64.
1607
+ if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
1167
1608
  if (typeof expr === 'boolean') return true
1168
1609
  if (typeof expr === 'string') return intCertain.get(expr) === true
1169
1610
  if (!Array.isArray(expr)) return false
1611
+ // Statically evaluable to -0 (e.g. -1 * 0, 0 / -1) — i32 would lose the sign.
1612
+ const sv = staticValue(expr)
1613
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
1170
1614
  const [op, ...args] = expr
1171
1615
  if (op == null) {
1172
1616
  // `[, value]` / `[null, value]` literal form
1173
1617
  const v = args[0]
1174
- if (typeof v === 'number') return Number.isInteger(v)
1618
+ if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
1175
1619
  if (typeof v === 'boolean') return true
1176
1620
  return false
1177
1621
  }
@@ -1228,11 +1672,19 @@ export function analyzeIntCertain(body) {
1228
1672
  export function exprType(expr, locals) {
1229
1673
  if (expr == null) return 'f64'
1230
1674
  if (typeof expr === 'number')
1231
- return Number.isInteger(expr) && expr >= -2147483648 && expr <= 2147483647 ? 'i32' : 'f64'
1675
+ return Number.isInteger(expr) && expr >= -2147483648 && expr <= 2147483647 && !Object.is(expr, -0) ? 'i32' : 'f64'
1232
1676
  if (typeof expr === 'string') {
1233
1677
  if (locals?.has?.(expr)) return locals.get(expr)
1234
1678
  const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
1235
1679
  if (paramType) return paramType
1680
+ // Module-level numeric consts (top-level `const N = 128`) are emitted as
1681
+ // wasm globals with a known wasm type. Without this lookup, references to
1682
+ // them inside functions fall back to f64, widening counters bounded by the
1683
+ // const (`for (let r = 0; r < N_ROUNDS; r++)`) to f64 via the comparison
1684
+ // pass. Only propagate primitive numeric kinds — i64 globals are reserved
1685
+ // for the NaN-box carrier ABI and shouldn't influence local typing.
1686
+ const gt = ctx.scope?.globalTypes?.get?.(expr)
1687
+ if (gt === 'i32' || gt === 'f64') return gt
1236
1688
  return 'f64'
1237
1689
  }
1238
1690
  if (!Array.isArray(expr)) return 'f64'
@@ -1240,8 +1692,26 @@ export function exprType(expr, locals) {
1240
1692
  const [op, ...args] = expr
1241
1693
  if (op == null) return exprType(args[0], locals) // literal [, value]
1242
1694
 
1695
+ // Statically evaluable to -0 (e.g. -1 * 0) — i32 would lose the sign.
1696
+ const sv = staticValue(expr)
1697
+ if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 'f64'
1698
+
1243
1699
  // Always f64
1244
- if (op === '/' || op === '**' || op === '[' || op === '[]' || op === '{}' || op === 'str') return 'f64'
1700
+ if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
1701
+ // arr[i] — typed integer arrays return i32. Only Int8/Uint8/Int16/Uint16/Int32
1702
+ // (every value fits in signed i32). Skip Uint32: 0..2^32-1 overflows signed.
1703
+ // During analyzeBody the in-progress typedElems is in localTypedElemsOverlay;
1704
+ // post-analyze passes read from ctx.types.typedElem.
1705
+ if (op === '[]') {
1706
+ if (typeof args[0] === 'string') {
1707
+ const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
1708
+ if (ctor) {
1709
+ const aux = typedElemAux(ctor)
1710
+ if (aux != null && (aux & 7) <= 4) return 'i32'
1711
+ }
1712
+ }
1713
+ return 'f64'
1714
+ }
1245
1715
  // `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
1246
1716
  // both return i32). Letting it stay i32 lets analyzeLocals keep the counter
1247
1717
  // local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
@@ -1339,7 +1809,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
1339
1809
  const disqualified = new Set()
1340
1810
  const valOf = name => ctx.func.repByLocal?.get(name)?.val
1341
1811
 
1342
- const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE])
1812
+ const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.DATE])
1343
1813
 
1344
1814
  // RHS must produce a fresh, non-null pointer of the declared VAL kind.
1345
1815
  // OBJECT ← `{…}`
@@ -1376,6 +1846,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
1376
1846
  if (callee.startsWith('new.')) {
1377
1847
  if (kind === VAL.SET) return callee === 'new.Set'
1378
1848
  if (kind === VAL.MAP) return callee === 'new.Map'
1849
+ if (kind === VAL.DATE) return callee === 'new.Date'
1379
1850
  if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer' || callee === 'new.DataView'
1380
1851
  if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
1381
1852
  }
@@ -1503,6 +1974,38 @@ export function collectParamNames(raw, out = new Set()) {
1503
1974
  return out
1504
1975
  }
1505
1976
 
1977
+ /** Observe shared AST facts on a single node (dyn, schema, arity).
1978
+ * Mutates `f`: { anyDyn, dynVars:Set, hasSchemaLiterals, maxDef, maxCall, hasRest, hasSpread }.
1979
+ * Used by both prepare.js walk and analyze.js walkFacts. */
1980
+ export function observeNodeFacts(node, f) {
1981
+ if (!Array.isArray(node)) return
1982
+ const [op, ...args] = node
1983
+ if (op === '[]') {
1984
+ const [obj, idx] = args
1985
+ if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
1986
+ } else if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
1987
+ const [, obj, idx] = args[0]
1988
+ if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
1989
+ } else if (op === 'for-in') {
1990
+ f.anyDyn = true
1991
+ if (typeof args[1] === 'string') f.dynVars.add(args[1])
1992
+ } else if (op === '{}') {
1993
+ f.hasSchemaLiterals = true
1994
+ } else if (op === '=>') {
1995
+ let fixedN = 0
1996
+ for (const r of extractParams(args[0])) {
1997
+ if (classifyParam(r).kind === 'rest') f.hasRest = true
1998
+ else fixedN++
1999
+ }
2000
+ if (fixedN > f.maxDef) f.maxDef = fixedN
2001
+ } else if (op === '()') {
2002
+ const a = args[1]
2003
+ const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
2004
+ if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) f.hasSpread = true
2005
+ if (callArgs.length > f.maxCall) f.maxCall = callArgs.length
2006
+ }
2007
+ }
2008
+
1506
2009
  /** Find free variables in AST: referenced in node, not in `bound`, present in `scope`. */
1507
2010
  export function findFreeVars(node, bound, free, scope) {
1508
2011
  if (node == null) return
@@ -1567,7 +2070,6 @@ export function findMutations(node, names, mutated) {
1567
2070
  export function analyzeDynKeys(...roots) {
1568
2071
  const dynVars = new Set()
1569
2072
  let anyDyn = false
1570
- const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
1571
2073
 
1572
2074
  function walk(node) {
1573
2075
  if (!Array.isArray(node)) return
@@ -1579,6 +2081,13 @@ export function analyzeDynKeys(...roots) {
1579
2081
  if (typeof obj === 'string') dynVars.add(obj)
1580
2082
  }
1581
2083
  }
2084
+ if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
2085
+ const [, obj, idx] = args[0]
2086
+ if (!isLiteralStr(idx)) {
2087
+ anyDyn = true
2088
+ if (typeof obj === 'string') dynVars.add(obj)
2089
+ }
2090
+ }
1582
2091
  // Runtime for-in (compile-time unroll didn't fire) → walks via shadow
1583
2092
  if (op === 'for-in') {
1584
2093
  anyDyn = true
@@ -1609,34 +2118,55 @@ export function analyzeBoxedCaptures(body) {
1609
2118
  const [op, ...args] = node
1610
2119
  if (op === '=>') return
1611
2120
  if (op === 'let' || op === 'const')
1612
- for (const a of args)
1613
- if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') outerScope.add(a[1])
2121
+ collectParamNames(args, outerScope)
1614
2122
  for (const a of args) collectDecls(a)
1615
2123
  })(body)
1616
2124
  if (ctx.func.current?.params) for (const p of ctx.func.current.params) outerScope.add(p.name)
1617
2125
  if (ctx.func.locals) for (const k of ctx.func.locals.keys()) outerScope.add(k)
1618
2126
 
1619
- ;(function walk(node, assignTarget) {
2127
+ const markArrowCaptures = (node, assignTarget, seen) => {
2128
+ const pnode = node[1]
2129
+ let p = pnode
2130
+ if (Array.isArray(p) && p[0] === '()') p = p[1]
2131
+ const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
2132
+ const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
2133
+ const captures = []
2134
+ findFreeVars(node[2], paramSet, captures, outerScope)
2135
+ if (captures.length === 0) return
2136
+ const captureSet = new Set(captures)
2137
+ const boxed = new Set()
2138
+ findMutations(body, captureSet, boxed)
2139
+ for (const v of captures) if (!seen.has(v)) boxed.add(v)
2140
+ if (assignTarget && captureSet.has(assignTarget)) boxed.add(assignTarget)
2141
+ for (const v of boxed) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
2142
+ }
2143
+
2144
+ ;(function walk(node, assignTarget, seen = new Set(ctx.func.current?.params?.map(p => p.name) || [])) {
1620
2145
  if (!Array.isArray(node)) return
1621
2146
  const [op, ...args] = node
1622
2147
  if (op === '=>') {
1623
- let p = args[0]
1624
- if (Array.isArray(p) && p[0] === '()') p = p[1]
1625
- const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
1626
- const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
1627
- const captures = []
1628
- findFreeVars(args[1], paramSet, captures, outerScope)
1629
- if (captures.length === 0) return
1630
- const captureSet = new Set(captures)
1631
- const mutated = new Set()
1632
- findMutations(body, captureSet, mutated)
1633
- if (assignTarget && captureSet.has(assignTarget)) mutated.add(assignTarget)
1634
- for (const v of mutated) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
2148
+ markArrowCaptures(node, assignTarget, seen)
1635
2149
  return
1636
2150
  }
2151
+
2152
+ if (op === ';' || op === '{}') {
2153
+ const blockSeen = new Set(seen)
2154
+ for (const a of args) walk(a, null, blockSeen)
2155
+ return
2156
+ }
2157
+
2158
+ if (op === 'let' || op === 'const') {
2159
+ for (const decl of args) {
2160
+ if (Array.isArray(decl) && decl[0] === '=') walk(decl[2], typeof decl[1] === 'string' ? decl[1] : null, seen)
2161
+ else walk(decl, null, seen)
2162
+ collectParamNames([decl], seen)
2163
+ }
2164
+ return
2165
+ }
2166
+
1637
2167
  if (op === '=' && typeof args[0] === 'string' && Array.isArray(args[1]) && args[1][0] === '=>')
1638
- return walk(args[1], args[0])
1639
- for (const a of args) walk(a)
2168
+ return walk(args[1], args[0], seen)
2169
+ for (const a of args) walk(a, null, seen)
1640
2170
  })(body)
1641
2171
  }
1642
2172
 
@@ -1750,56 +2280,24 @@ export function narrowReturnArrayElems(field, paramReps, valueUsed) {
1750
2280
  export function collectProgramFacts(ast) {
1751
2281
  const paramReps = new Map()
1752
2282
  const valueUsed = new Set()
1753
- const dynVars = new Set()
1754
- let anyDyn = false
1755
2283
  const propMap = new Map()
1756
2284
  const callSites = []
1757
2285
  const doSchema = ast && ctx.schema.register
1758
2286
  const doArity = !!ctx.closure.make
1759
- let hasSchemaLiterals = false
1760
- let maxDef = 0, maxCall = 0, hasRest = false, hasSpread = false
1761
- const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
2287
+ const f = { dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false, maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false }
1762
2288
  // Slot-type observation lives in the dedicated `observeProgramSlots` pass below;
1763
2289
  // walkFacts only registers schemas (which is local to the AST node).
1764
2290
  const walkFacts = (node, full, inArrow, callerFunc) => {
1765
2291
  if (!Array.isArray(node)) return
1766
2292
  const [op, ...args] = node
1767
- // dyn-key detection. Strict check deferred to emit time (e.g. `buf[i]` on a
1768
- // Float64Array uses typed-array load, not __dyn_get — only the actual
1769
- // dynamic-dispatch fallback should error in strict mode).
1770
- if (op === '[]') {
1771
- const [obj, idx] = args
1772
- if (!isLiteralStr(idx)) { anyDyn = true; if (typeof obj === 'string') dynVars.add(obj) }
1773
- } else if (op === 'for-in') {
1774
- if (ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
1775
- anyDyn = true
1776
- if (typeof args[1] === 'string') dynVars.add(args[1])
1777
- }
1778
- // Object literal: register schema. Slot val-type observation is deferred to a
1779
- // second pass (observeSlotsIn below) so that shorthand `{x}` (expanded by prepare
1780
- // to `[':', x, x]`) resolves x's val type via per-function locals, not just globals.
2293
+ // shared dyn/schema/arity facts (duplicated pattern synced with prepare.js)
2294
+ observeNodeFacts(node, f)
2295
+ // strict for-in check
2296
+ if (op === 'for-in' && ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
2297
+ // schema registration (analyze-specific)
1781
2298
  if (op === '{}' && doSchema) {
1782
2299
  const parsed = staticObjectProps(args)
1783
- if (parsed) {
1784
- ctx.schema.register(parsed.names)
1785
- hasSchemaLiterals = true
1786
- }
1787
- }
1788
- // closure ABI arity
1789
- if (doArity) {
1790
- if (op === '=>') {
1791
- let fixedN = 0
1792
- for (const r of extractParams(args[0])) {
1793
- if (classifyParam(r).kind === 'rest') hasRest = true
1794
- else fixedN++
1795
- }
1796
- if (fixedN > maxDef) maxDef = fixedN
1797
- } else if (op === '()') {
1798
- const a = args[1]
1799
- const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
1800
- if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) hasSpread = true
1801
- if (callArgs.length > maxCall) maxCall = callArgs.length
1802
- }
2300
+ if (parsed) ctx.schema.register(parsed.names)
1803
2301
  }
1804
2302
  // Crossing into a closure body: from now on, no call-site collection (matches the
1805
2303
  // pre-fusion scanCalls bailing at '=>'). Still walks children for arity/dyn.
@@ -1817,26 +2315,22 @@ export function collectProgramFacts(ast) {
1817
2315
  }
1818
2316
  }
1819
2317
  // first-class function-value + static-call-site scan
1820
- if (op === '()' && typeof args[0] === 'string' && ctx.func.names.has(args[0])) {
2318
+ if (op === '()' && isFuncRef(args[0], ctx.func.names)) {
1821
2319
  if (!inArrow) {
1822
- // Record call site for the type/schema fixpoint. Filtering by
1823
- // exported/raw/valueUsed happens later (valueUsed isn't fully populated yet).
1824
- // `node` is the call AST node itself; specializeBimorphicTyped mutates
1825
- // node[1] (the callee name) to point at a per-ctor clone.
1826
2320
  const a = args[1]
1827
2321
  const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
1828
2322
  callSites.push({ callee: args[0], argList, callerFunc, node })
1829
2323
  }
1830
2324
  for (let i = 1; i < args.length; i++) {
1831
2325
  const a = args[i]
1832
- if (typeof a === 'string' && ctx.func.names.has(a)) valueUsed.add(a)
2326
+ if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
1833
2327
  else walkFacts(a, true, inArrow, callerFunc)
1834
2328
  }
1835
2329
  return
1836
2330
  }
1837
- if ((op === '.' || op === '?.') && typeof args[0] === 'string' && ctx.func.names.has(args[0])) return
2331
+ if ((op === '.' || op === '?.') && isFuncRef(args[0], ctx.func.names)) return
1838
2332
  for (const a of args) {
1839
- if (typeof a === 'string' && ctx.func.names.has(a)) valueUsed.add(a)
2333
+ if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
1840
2334
  else walkFacts(a, true, inArrow, callerFunc)
1841
2335
  }
1842
2336
  } else {
@@ -1848,16 +2342,16 @@ export function collectProgramFacts(ast) {
1848
2342
  const initFacts = ctx.module.initFacts
1849
2343
  if (initFacts) {
1850
2344
  if (initFacts.anyDyn) {
1851
- anyDyn = true
1852
- for (const v of initFacts.dynVars) dynVars.add(v)
2345
+ f.anyDyn = true
2346
+ for (const v of initFacts.dynVars) f.dynVars.add(v)
1853
2347
  }
1854
2348
  if (doArity) {
1855
- if (initFacts.maxDef > maxDef) maxDef = initFacts.maxDef
1856
- if (initFacts.maxCall > maxCall) maxCall = initFacts.maxCall
1857
- if (initFacts.hasRest) hasRest = true
1858
- if (initFacts.hasSpread) hasSpread = true
2349
+ if (initFacts.maxDef > f.maxDef) f.maxDef = initFacts.maxDef
2350
+ if (initFacts.maxCall > f.maxCall) f.maxCall = initFacts.maxCall
2351
+ if (initFacts.hasRest) f.hasRest = true
2352
+ if (initFacts.hasSpread) f.hasSpread = true
1859
2353
  }
1860
- if (doSchema && initFacts.hasSchemaLiterals) hasSchemaLiterals = true
2354
+ if (doSchema && initFacts.hasSchemaLiterals) f.hasSchemaLiterals = true
1861
2355
  }
1862
2356
 
1863
2357
  // Slot-type observation pass: walk every `{}` literal with the right scope's
@@ -1866,12 +2360,12 @@ export function collectProgramFacts(ast) {
1866
2360
  // through valTypeOf → lookupValType. Skips into closures — they're observed via
1867
2361
  // their own func.list entry. The overlay is the per-function analyzeBody.valTypes
1868
2362
  // map (already populated with the same overlay-aware walk).
1869
- if (doSchema && hasSchemaLiterals) observeProgramSlots(ast)
2363
+ if (doSchema && f.hasSchemaLiterals) observeProgramSlots(ast)
1870
2364
 
1871
2365
  return {
1872
- dynVars, anyDyn, propMap, valueUsed, callSites,
1873
- maxDef, maxCall, hasRest, hasSpread,
1874
- paramReps, hasSchemaLiterals,
2366
+ dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
2367
+ maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
2368
+ paramReps, hasSchemaLiterals: f.hasSchemaLiterals,
1875
2369
  }
1876
2370
  }
1877
2371