jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -7,26 +7,29 @@
7
7
  * function `sig` records change.
8
8
  */
9
9
 
10
- import { ctx } from './ctx.js'
11
- import { isLiteralStr, I32_MIN, I32_MAX } from './ir.js'
10
+ import { ctx, warn, err } from '../ctx.js'
11
+ import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
12
+ import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
12
13
  import {
13
- VAL,
14
- analyzeBody,
15
- paramFactsOf,
16
- exprType, findMutations, hasBareReturn,
17
- invalidateLocalsCache, invalidateValTypesCache, isBlockBody, alwaysReturns,
18
- narrowReturnArrayElems, observeProgramSlots, returnExprs, staticObjectProps,
19
- scanBoundedLoops,
20
- typedElemAux, typedElemCtor, ctorFromElemAux, valTypeOf,
14
+ analyzeBody, findMutations, invalidateLocalsCache,
21
15
  } from './analyze.js'
16
+ import { staticObjectProps } from '../static.js'
17
+ import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
18
+ import { typedElemAux, ctorFromElemAux } from '../../layout.js'
19
+ import { observeProgramSlots } from './program-facts.js'
20
+ import { valTypeOf } from '../kind.js'
21
+ import { VAL, updateRep } from '../reps.js'
22
+ import {
23
+ paramFactsOf, ensureParamRep, mergeParamFact,
24
+ } from '../param-reps.js'
22
25
  import {
23
- clearStickyNull, ensureParamRep, mergeParamFact,
24
26
  inferArrElemSchema, inferArrElemValType,
25
- inferSchemaId, inferValType, inferTypedCtor,
27
+ inferSchemaId, inferValType, inferTypedCtor, inferParams,
26
28
  } from './infer.js'
27
29
 
28
30
  const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
29
31
 
32
+
30
33
  function filterLiveCallSites(callSites, valueUsed) {
31
34
  if (!callSites.length) return
32
35
 
@@ -82,11 +85,12 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
82
85
  if (!reps) continue
83
86
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
84
87
  for (const [k, r] of reps) {
85
- if (r.wasm !== 'i32' || k === restIdx) continue
86
- if (k >= func.sig.params.length) continue
88
+ if (k === restIdx || k >= func.sig.params.length) continue
87
89
  const p = func.sig.params[k]
88
- if (p.type === 'i32') continue
89
90
  if (func.defaults?.[p.name] != null) continue
91
+ // SIMD: a param passed a v128 (lane vector) at every call site is a v128 param.
92
+ if (r.wasm === 'v128') { p.type = 'v128'; continue }
93
+ if (r.wasm !== 'i32' || p.type === 'i32') continue
90
94
  if (skipTyped && r.val === VAL.TYPED) continue
91
95
  p.type = 'i32'
92
96
  }
@@ -115,21 +119,25 @@ function validateIntConstParams(paramReps, valueUsed) {
115
119
  }
116
120
  }
117
121
 
118
- function applyPointerParamAbi(paramReps, valueUsed) {
122
+ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
119
123
  for (const func of ctx.func.list) {
120
124
  if (func.exported || func.raw || valueUsed.has(func.name)) continue
121
125
  const reps = paramReps.get(func.name)
122
126
  if (!reps) continue
123
127
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
124
- for (const [k, r] of reps) {
125
- if (!PTR_ABI_KINDS.has(r.val)) continue
128
+ for (const [k] of reps) {
129
+ // Re-fold call sites HARD (the shared val lattice is soft, so r.val may be a
130
+ // partial consensus from typed sites alone) — only specialize when every site
131
+ // proves the same pointer kind.
132
+ const hv = hardParamVal(func.name, k)
133
+ if (!PTR_ABI_KINDS.has(hv)) continue
126
134
  if (k === restIdx) continue
127
135
  if (k >= func.sig.params.length) continue
128
136
  const p = func.sig.params[k]
129
137
  if (p.type === 'i32') continue
130
138
  if (func.defaults?.[p.name] != null) continue
131
139
  p.type = 'i32'
132
- p.ptrKind = r.val
140
+ p.ptrKind = hv
133
141
  }
134
142
  }
135
143
  }
@@ -148,12 +156,32 @@ function refreshCallerValTypes(callerCtx) {
148
156
  }
149
157
  }
150
158
 
159
+ // Per-caller typed-elem context: the caller's body-local typed arrays, layered
160
+ // over the module's typed-array globals so a call like `f(globalArr)` resolves
161
+ // `globalArr`'s ctor (inferTypedCtor reads only this map for a bare-name arg).
162
+ // A global is visible UNLESS the caller shadows the name with a param or local
163
+ // of its own — only then could the name denote a non-typed value. Globals are
164
+ // sound to consult: globalTypedElem holds a name only when EVERY assignment to
165
+ // it is the same single typed-array ctor (scope.js invalidates on any conflict),
166
+ // so it can't denote a different kind at the call site.
167
+ function callerTypedElemsFor(func, globalTE) {
168
+ const local = analyzeBody(func.body).typedElems
169
+ if (!globalTE.size) return local
170
+ const shadowed = new Set(analyzeBody(func.body).locals.keys())
171
+ for (const p of func.sig?.params || []) shadowed.add(p.name)
172
+ const merged = new Map()
173
+ for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
174
+ for (const [k, v] of local) merged.set(k, v) // local typed binding shadows the global
175
+ return merged
176
+ }
177
+
151
178
  function buildCallerTypedCtx() {
152
179
  const callerTypedCtx = new Map()
153
- callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
180
+ const globalTE = ctx.scope.globalTypedElem || new Map()
181
+ callerTypedCtx.set(null, globalTE)
154
182
  for (const func of ctx.func.list) {
155
183
  if (!func.body || func.raw) continue
156
- callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
184
+ callerTypedCtx.set(func, callerTypedElemsFor(func, globalTE))
157
185
  }
158
186
  return callerTypedCtx
159
187
  }
@@ -256,7 +284,7 @@ function narrowI32Results(funcs) {
256
284
  while (changed) {
257
285
  changed = false
258
286
  for (const func of funcs) {
259
- if (func.sig.results[0] === 'i32') continue
287
+ if (func.sig.results[0] === 'i32' || func.sig.results[0] === 'v128') continue
260
288
  const body = func.body
261
289
  if (isBlockBody(body) && hasBareReturn(body)) continue
262
290
  const exprs = returnExprs(body)
@@ -265,12 +293,16 @@ function narrowI32Results(funcs) {
265
293
  ctx.func.current = func.sig
266
294
  const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
267
295
  for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
268
- const allI32 = exprs.every(e => exprType(e, locals) === 'i32')
296
+ const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
297
+ const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
269
298
  const anyUnsigned = exprs.some(isUnsignedTail)
270
299
  const allUnsigned = exprs.every(isUnsignedTail)
271
300
  ctx.func.current = savedCurrent
272
- // Narrow only when sign is consistent (all-signed or all-unsigned tails).
273
- if (allI32 && (!anyUnsigned || allUnsigned)) {
301
+ // SIMD: every tail returns a lane vector v128 result.
302
+ if (allV128) {
303
+ func.sig.results = ['v128']
304
+ changed = true
305
+ } else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
274
306
  func.sig.results = ['i32']
275
307
  if (allUnsigned) func.sig.unsignedResult = true
276
308
  changed = true
@@ -401,11 +433,47 @@ function typedAuxOfReturn(expr, localElemMap) {
401
433
  * Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
402
434
  * outer's call to inner contributes a known schema-id.
403
435
  */
436
+ /** A function whose every return is the same parameter that was pointer-ABI
437
+ * narrowed to an unboxed i32 (p.ptrKind set). Returns that param, else null. */
438
+ function passthroughPtrParam(func) {
439
+ const exprs = returnExprs(func.body)
440
+ if (!exprs.length) return null
441
+ const name = exprs[0]
442
+ if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
443
+ return func.sig.params.find(p => p.name === name && p.ptrKind) || null
444
+ }
445
+
404
446
  function narrowPointerResults(funcs, paramReps) {
405
447
  let changed = true
406
448
  while (changed) {
407
449
  changed = false
408
450
  for (const func of funcs) {
451
+ // Pointer pass-through: every return is the same parameter that
452
+ // applyPointerParamAbi narrowed to an unboxed i32 pointer. The result IS that
453
+ // pointer, so its sig must carry the param's ptrKind (+ schemaId for OBJECT).
454
+ // Without this the result is a bare i32 the caller numeric-converts
455
+ // (`f64.convert_i32_s`) instead of reboxing — dropping the schema-id so a
456
+ // later `.prop` read mis-resolves to `undefined`. narrowValResults can't see
457
+ // this (it reads body-locals, not param facts) and narrowI32Results steals it
458
+ // as a numeric i32, so resolve it here from the settled param lattice.
459
+ if (func.sig.ptrKind == null) {
460
+ const pp = passthroughPtrParam(func)
461
+ if (pp) {
462
+ const aux = pp.ptrKind === VAL.OBJECT
463
+ ? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
464
+ : null
465
+ // OBJECT needs a known schema-id to rebox; a polymorphic pass-through
466
+ // (conflicting schemas → null) keeps its current handling.
467
+ if (pp.ptrKind !== VAL.OBJECT || aux != null) {
468
+ func.sig.results = ['i32']
469
+ func.sig.ptrKind = pp.ptrKind
470
+ func.valResult = pp.ptrKind
471
+ if (aux != null) func.sig.ptrAux = aux
472
+ changed = true
473
+ continue
474
+ }
475
+ }
476
+ }
409
477
  if (!func.valResult) continue
410
478
  if (func.sig.results[0] !== 'f64') continue
411
479
  const isBlock = isBlockBody(func.body)
@@ -467,7 +535,7 @@ function createPhaseState() {
467
535
 
468
536
  invalidateBodyFacts() {
469
537
  for (const func of ctx.func.list) {
470
- if (func.body && !func.raw) invalidateValTypesCache(func.body)
538
+ if (func.body && !func.raw) invalidateLocalsCache(func.body)
471
539
  }
472
540
  clearDerived()
473
541
  },
@@ -484,6 +552,67 @@ function createPhaseState() {
484
552
  }
485
553
  }
486
554
 
555
+ const _FIELD_TO_SLICE = {
556
+ arrayElemSchema: 'arrElemSchemas',
557
+ arrayElemValType: 'arrElemValTypes',
558
+ }
559
+
560
+ /** Propagate Array<T> element facts from return paths into caller paramReps (phase G). */
561
+ function narrowReturnArrayElems(field, paramReps, valueUsed) {
562
+ const sliceKey = _FIELD_TO_SLICE[field]
563
+ const targets = ctx.func.list.filter(f =>
564
+ !f.raw && !f.exported && !valueUsed.has(f.name) &&
565
+ f.valResult === VAL.ARRAY && f[field] == null
566
+ )
567
+ let changed = true
568
+ while (changed) {
569
+ changed = false
570
+ for (const f of targets) invalidateLocalsCache(f.body)
571
+ for (const func of targets) {
572
+ if (func[field] != null) continue
573
+ const isBlock = isBlockBody(func.body)
574
+ if (isBlock && !alwaysReturns(func.body)) continue
575
+ const exprs = returnExprs(func.body)
576
+ if (!exprs.length) continue
577
+ const savedLocals = ctx.func.locals
578
+ const facts = analyzeBody(func.body)
579
+ ctx.func.locals = new Map(facts.locals)
580
+ for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
581
+ const localElems = facts[sliceKey]
582
+ ctx.func.locals = savedLocals
583
+ const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
584
+ const resolveExpr = (expr) => {
585
+ if (typeof expr === 'string') {
586
+ if (localElems.has(expr)) {
587
+ const v = localElems.get(expr)
588
+ if (v != null) return v
589
+ }
590
+ if (paramElemMap.has(expr)) return paramElemMap.get(expr)
591
+ return null
592
+ }
593
+ if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
594
+ const f = ctx.func.map?.get(expr[1])
595
+ if (f?.[field] != null) return f[field]
596
+ }
597
+ if (Array.isArray(expr) && expr[0] === '?:') {
598
+ const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
599
+ return a != null && a === b ? a : null
600
+ }
601
+ if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
602
+ const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
603
+ return a != null && a === b ? a : null
604
+ }
605
+ return null
606
+ }
607
+ const v0 = resolveExpr(exprs[0])
608
+ if (v0 == null) continue
609
+ if (!exprs.every(e => resolveExpr(e) === v0)) continue
610
+ func[field] = v0
611
+ changed = true
612
+ }
613
+ }
614
+ }
615
+
487
616
  export default function narrowSignatures(programFacts, ast) {
488
617
  const { callSites, valueUsed, paramReps, hasSchemaLiterals } = programFacts
489
618
 
@@ -518,29 +647,35 @@ export default function narrowSignatures(programFacts, ast) {
518
647
  return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
519
648
  }
520
649
 
650
+ // Per-call-site inference context for a narrowable callee. null for call sites
651
+ // whose callee is exported / value-used / unknown, or whose caller has no ctx.
652
+ const siteState = (cs) => {
653
+ const { callee, argList, callerFunc } = cs
654
+ const func = ctx.func.map.get(callee)
655
+ if (!func || func.exported || valueUsed.has(callee)) return null
656
+ const ctxEntry = callerCtx.get(callerFunc)
657
+ if (!ctxEntry) return null
658
+ const restIdx = func.rest ? func.sig.params.length - 1 : -1
659
+ const paramFacts = new Map()
660
+ return {
661
+ callee, callerFunc, argList, func, restIdx,
662
+ callerLocals: ctxEntry.callerLocals,
663
+ callerValTypes: ctxEntry.callerValTypes,
664
+ callerParamFacts(key) {
665
+ if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
666
+ return paramFacts.get(key)
667
+ },
668
+ }
669
+ }
521
670
  const runCallsiteLattice = (rules) => {
522
671
  for (let s = 0; s < callSites.length; s++) {
523
- const { callee, argList, callerFunc } = callSites[s]
524
- const func = ctx.func.map.get(callee)
525
- if (!func || func.exported || valueUsed.has(callee)) continue
526
- const ctxEntry = callerCtx.get(callerFunc)
527
- if (!ctxEntry) continue
528
- const restIdx = func.rest ? func.sig.params.length - 1 : -1
529
- const paramFacts = new Map()
530
- const state = {
531
- callee, callerFunc, argList, func, restIdx,
532
- callerLocals: ctxEntry.callerLocals,
533
- callerValTypes: ctxEntry.callerValTypes,
534
- callerParamFacts(key) {
535
- if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
536
- return paramFacts.get(key)
537
- },
538
- }
672
+ const state = siteState(callSites[s])
673
+ if (!state) continue
674
+ const { func, argList } = state
539
675
  for (let k = 0; k < func.sig.params.length; k++) {
540
- const r = ensureParamRep(paramReps, callee, k)
676
+ const r = ensureParamRep(paramReps, state.callee, k)
541
677
  if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
542
- const arg = argList[k]
543
- for (const rule of rules) rule.apply(r, arg, k, state)
678
+ for (const rule of rules) rule.apply(r, argList[k], k, state)
544
679
  }
545
680
  }
546
681
  }
@@ -568,7 +703,33 @@ export default function narrowSignatures(programFacts, ast) {
568
703
  const pname = state.func.sig.params[k]?.name
569
704
  return pname != null ? state.func.defaults?.[pname] : null
570
705
  }
571
- const mergeRule = (field, infer) => ({
706
+ // Hard consensus val for (funcName, param k): the kind every live call site
707
+ // agrees on, or null if any site is untyped / missing / disagrees. The shared
708
+ // `val` lattice runs SOFT (a value can come from typed sites alone, untyped
709
+ // sites skipped); a consumer that *mutates the signature* off val must instead
710
+ // ask this — it re-folds the sites HARD so it never specializes a param that
711
+ // some call site can't prove. (applyPointerParamAbi is that consumer.)
712
+ const hardParamVal = (funcName, k) => {
713
+ let consensus
714
+ for (let s = 0; s < callSites.length; s++) {
715
+ if (callSites[s].callee !== funcName) continue
716
+ const state = siteState(callSites[s])
717
+ if (!state) continue
718
+ if (k >= state.argList.length) return null // missing → undefined at runtime
719
+ const v = inferValAtSite(state.argList[k], state)
720
+ if (v == null) return null // an untyped site ⇒ not specializable
721
+ if (consensus === undefined) consensus = v
722
+ else if (consensus !== v) return null // disagreement ⇒ TOP
723
+ }
724
+ return consensus ?? null
725
+ }
726
+ // `soft` makes apply treat a null inference as BOTTOM (skip — "this site can't
727
+ // tell yet") instead of TOP (poison): the monotone meet. A soft field never
728
+ // needs clearStickyNull; its consumers either re-validate hard (hardParamVal)
729
+ // or read it after a final hard settling sweep. `missing` poisons regardless —
730
+ // an omitted arg with no default is undefined at runtime, a real reason not to
731
+ // specialize, and must stay sticky.
732
+ const mergeRule = (field, infer, soft = false) => ({
572
733
  missing(r, k, state) {
573
734
  if (r[field] === null) return
574
735
  const def = defaultArg(state, k)
@@ -576,11 +737,19 @@ export default function narrowSignatures(programFacts, ast) {
576
737
  else r[field] = null
577
738
  },
578
739
  apply(r, arg, k, state) {
579
- if (r[field] !== null) mergeParamFact(r, field, infer(arg, k, state))
740
+ if (r[field] === null) return
741
+ const v = infer(arg, k, state)
742
+ if (v == null) { if (!soft) r[field] = null; return }
743
+ mergeParamFact(r, field, v)
580
744
  },
581
745
  })
582
746
  const runFixpoint = () => runCallsiteLattice([
583
- mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state)),
747
+ // val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
748
+ // typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
749
+ // sticky-poison it (the old clearStickyNull undid that). Soft leaves it BOTTOM;
750
+ // the post-enrichment rerun fills it in. applyPointerParamAbi re-validates via
751
+ // hardParamVal; a final hard sweep settles val for emit + late consumers.
752
+ mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state), true),
584
753
  {
585
754
  missing: poison('wasm'),
586
755
  apply(r, arg, _k, state) {
@@ -620,6 +789,16 @@ export default function narrowSignatures(programFacts, ast) {
620
789
  }
621
790
  const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
622
791
  const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
792
+
793
+ // E2 (VAL-kind result inference) FIRST: it's body-driven and call-chain self-
794
+ // fixpointing — independent of the param lattice and the narrowing acts (it reads
795
+ // analyzeBody valTypes + callees' valResult, never paramReps or sig.params). Running
796
+ // it up front means a call arg like `initRows()` resolves to its VAL.ARRAY result on
797
+ // the param fixpoint's FIRST pass, so val/schemaId never get the can't-tell-yet
798
+ // poison that clearStickyNull used to un-stick (root B). Numeric (i32) result
799
+ // narrowing stays below — it benefits from i32 params being narrowed first.
800
+ const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
801
+ narrowValResults(funcsWithNarrowableResult)
623
802
  runFixpoint()
624
803
  runFixpoint()
625
804
 
@@ -645,14 +824,12 @@ export default function narrowSignatures(programFacts, ast) {
645
824
  // (aux carries element-type, handled separately by applyTypedPointerParamAbi).
646
825
  // - exclude params with defaults (nullish sentinel needs the f64 NaN space).
647
826
  // - exclude rest position (array pack/unpack stays f64).
648
- applyPointerParamAbi(paramReps, valueUsed)
827
+ applyPointerParamAbi(paramReps, valueUsed, hardParamVal)
649
828
 
650
- // E + E2: body-driven result-type inference. Pool of narrowable funcs is shared
651
- // across the i32/VAL/pointer passes same predicate (non-raw, non-value-used,
652
- // single-result).
653
- const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
829
+ // E: numeric (i32) result narrowing kept here, after applyI32ParamSpecialization,
830
+ // so a body returning `param + 1` sees param already narrowed to i32. (E2 / VAL
831
+ // result inference ran up front — see above.) funcsWithNarrowableResult hoisted there.
654
832
  narrowI32Results(funcsWithNarrowableResult)
655
- narrowValResults(funcsWithNarrowableResult)
656
833
 
657
834
  // Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
658
835
  // VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
@@ -673,11 +850,9 @@ export default function narrowSignatures(programFacts, ast) {
673
850
  // observation upgrade `undefined` → NUMBER without poisoning earlier
674
851
  // monomorphic observations.
675
852
  if (hasSchemaLiterals) observeProgramSlots(ast)
676
- // Clear sticky-null on val/schemaId first 2 passes ran with valResult unset, so
677
- // call args resolving via `f.valResult` returned null and got stuck. Re-running
678
- // with refreshed callerValTypes lets these flow.
679
- clearStickyNull(paramReps, 'val')
680
- clearStickyNull(paramReps, 'schemaId')
853
+ // Re-run with refreshed callerValTypes + the new program-slot observations. (No
854
+ // clearStickyNull needed: valResult was known before the first pass see E2 hoist
855
+ // above so val/schemaId never got the can't-tell-yet poison this used to undo.)
681
856
  runFixpoint()
682
857
  // Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
683
858
  runArrFixpoint()
@@ -720,7 +895,7 @@ export default function narrowSignatures(programFacts, ast) {
720
895
  // i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
721
896
  // per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
722
897
  // that today dominates hot loops dominated by typed-array indexing.
723
- // Call sites coerce via emitArgForParam → ptrOffsetIR(arg, VAL.TYPED).
898
+ // Call sites coerce via coerceArg → ptrOffsetIR(arg, VAL.TYPED).
724
899
  // Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
725
900
  // exported, value-used, raw, defaults, rest position.
726
901
  applyTypedPointerParamAbi(paramReps, valueUsed)
@@ -728,11 +903,11 @@ export default function narrowSignatures(programFacts, ast) {
728
903
  // H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
729
904
  // where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
730
905
  // → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
731
- // for caller's params, so inferValType returns null and paramReps[callee][k].val is
732
- // sticky null. With ptrKind enriching callerValTypes, sum's arr gets val=TYPED in
733
- // its rep, letting array.js skip __is_str_key + __str_idx dispatch on `arr[i]`.
906
+ // for caller's params, so inferValType returned null and (under the old hard merge)
907
+ // sticky-poisoned the param's val. The soft val merge leaves it BOTTOM instead, so
908
+ // this rerun now that enrichment has put VAL.TYPED into callerValTypes — simply
909
+ // fills it in (array.js then skips __is_str_key + __str_idx dispatch on `arr[i]`).
734
910
  enrichCallerValTypesFromPointerParams(callerCtx)
735
- clearStickyNull(paramReps, 'val')
736
911
  runFixpoint()
737
912
 
738
913
  // I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
@@ -750,6 +925,12 @@ export default function narrowSignatures(programFacts, ast) {
750
925
  // so the refreshed exprType view propagates.
751
926
  resetParamWasmFacts(paramReps)
752
927
  runFixpoint()
928
+ // Settle val HARD now that every producer (results, typedCtor, enrichment) has run
929
+ // and the soft lattice has converged: re-fold each param's sites and poison any
930
+ // whose val isn't unanimous (a site left BOTTOM = genuinely untyped). After this,
931
+ // r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
932
+ // ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
933
+ runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
753
934
  // Don't steal typed-array params from specializeBimorphicTyped: F phase parks
754
935
  // bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
755
936
  // ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
@@ -764,9 +945,12 @@ export default function narrowSignatures(programFacts, ast) {
764
945
  if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
765
946
  }
766
947
 
767
- /** Gate the jsstring opt-in: enabled by default for JS hosts; disabled under
768
- * WASI (env imports unavailable) or when explicitly opted out via the
769
- * `optimize: { jsstring: false }` knob (used by side-by-side benchmarks). */
948
+ /** Gate the jsstring carrier on the host. ON by default for the JS host: a
949
+ * js-host build is already JS-locked (it imports `env.*`), so the externref +
950
+ * `wasm:js-string` carrier's JS dependency is free there, and the zero-copy
951
+ * string-read path is a clear win. OFF under WASI: the carrier needs a JS host,
952
+ * and wasi builds must stay portable (wasmtime/Go/Rust). Opt out on JS with
953
+ * `optimize: { jsstring: false }` (e.g. side-by-side benchmarks). */
770
954
  function jsstringEnabled() {
771
955
  if (ctx.transform.host === 'wasi') return false
772
956
  if (ctx.transform.optimize?.jsstring === false) return false
@@ -782,13 +966,15 @@ export function applyJsstringBoundaryCarrierStandalone(programFacts) {
782
966
  }
783
967
 
784
968
  /**
785
- * Body-local boolean-result inference. `narrowValResults` is the general (any
786
- * VAL.*) pass, but it lives inside whole-program narrowing, which is skipped for
787
- * trivial leaf modules (no call sites). A boolean result is the one kind whose
788
- * internal carrier (the 0/1 number) differs from its host-boundary carrier (the
789
- * TRUE_NAN/FALSE_NAN atom), so an exported `(a) => a > 2` still needs its boundary
790
- * box even on the skip path. This pass only ever *sets* valResult = VAL.BOOL, so
791
- * it is safe to run unconditionally pointer/array/number results are untouched.
969
+ * Body-local boolean/bigint-result inference. `narrowValResults` is the general
970
+ * (any VAL.*) pass, but it lives inside whole-program narrowing, which is skipped
971
+ * for trivial leaf modules (no call sites). Boolean and bigint are the two kinds
972
+ * whose internal carrier differs from the host-boundary carrier — bool rides a 0/1
973
+ * number internally but crosses as the TRUE_NAN/FALSE_NAN atom; bigint rides an
974
+ * i64-reinterpreted f64 internally but must cross as a real Number so an exported
975
+ * `(a) => a > 2` or `() => 100n` still needs its boundary thunk even on the skip path.
976
+ * This pass only ever *sets* valResult to VAL.BOOL / VAL.BIGINT, so it is safe to run
977
+ * unconditionally — pointer/array/number results are untouched.
792
978
  */
793
979
  export function narrowBoolResults() {
794
980
  for (const func of ctx.func.list) {
@@ -803,6 +989,7 @@ export function narrowBoolResults() {
803
989
  ? (localValTypes?.get(e) || ctx.scope.globalValTypes?.get(e) || null)
804
990
  : valTypeOf(e)
805
991
  if (exprs.every(e => vt(e) === VAL.BOOL)) func.valResult = VAL.BOOL
992
+ else if (exprs.every(e => vt(e) === VAL.BIGINT)) func.valResult = VAL.BIGINT
806
993
  }
807
994
  }
808
995
 
@@ -828,35 +1015,37 @@ const JSS_OK_PROPS = new Set(['length', 'charCodeAt'])
828
1015
  * `.charCodeAt` whose callee node lives in `safeCC` (provably bounded).
829
1016
  * Reassignment / `++` / `--` / closure capture all reject conservatively.
830
1017
  *
831
- * Returns `{ ok, stringDiscriminating }` — `stringDiscriminating` is true iff
832
- * we saw at least one string-only use (`.charCodeAt`); the caller uses this
833
- * to gate on whether the body actually proves the param is a string, since
834
- * `.length` alone is polymorphic across string/array/typed-array.
1018
+ * Returns `{ ok, stringDiscriminating, reason? }` — `stringDiscriminating` is
1019
+ * true iff we saw at least one string-only use (`.charCodeAt`); `reason` names
1020
+ * the first blocking use when `ok` is false.
835
1021
  */
836
1022
  function paramAllUsesJsstringMappable(body, name, safeCC) {
837
- if (body == null) return { ok: false, stringDiscriminating: false }
1023
+ if (body == null) return { ok: false, stringDiscriminating: false, reason: null }
838
1024
  let ok = true
839
1025
  let stringDiscriminating = false
1026
+ let reason = null
1027
+ const fail = (msg) => { ok = false; reason ||= msg }
1028
+ const refsParam = (node) => {
1029
+ if (node === name) return true
1030
+ if (!Array.isArray(node)) return false
1031
+ for (let i = 1; i < node.length; i++) if (refsParam(node[i])) return true
1032
+ return false
1033
+ }
840
1034
  const walk = (node) => {
841
1035
  if (!ok) return
842
1036
  if (typeof node === 'string') {
843
- // A bare reference to the param outside an eligible `[., name, prop]`
844
- // callee slot is a fallback trigger — the param escaped to a non-
845
- // mappable use.
846
- if (node === name) ok = false
1037
+ if (node === name) fail('bare use of the string param disables the zero-copy externref boundary carrier')
847
1038
  return
848
1039
  }
849
1040
  if (!Array.isArray(node)) return
850
1041
  const op = node[0]
851
1042
  if (op === '=>') {
852
- // Nested arrow may shadow `name` with its own param; if it doesn't, any
853
- // capture of the outer `name` is an escape we can't track — reject.
854
1043
  const params = node[1]
855
1044
  const shadowed = Array.isArray(params)
856
1045
  ? params.some(p => (typeof p === 'string' && p === name) ||
857
1046
  (Array.isArray(p) && p[1] === name))
858
1047
  : params === name
859
- if (!shadowed) ok = false
1048
+ if (!shadowed) fail('closure capture of the string param disables the zero-copy externref boundary carrier')
860
1049
  return
861
1050
  }
862
1051
  if ((op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' ||
@@ -864,26 +1053,23 @@ function paramAllUsesJsstringMappable(body, name, safeCC) {
864
1053
  op === '>>=' || op === '<<=' || op === '>>>=' ||
865
1054
  op === '||=' || op === '&&=' || op === '??=' ||
866
1055
  op === '++' || op === '--') && node[1] === name) {
867
- ok = false
1056
+ fail('reassigning the string param disables the zero-copy externref boundary carrier')
1057
+ return
1058
+ }
1059
+ if (op === '+' && node.slice(1).some(arg => refsParam(arg))) {
1060
+ fail('string concatenation on the param disables the zero-copy externref boundary carrier')
868
1061
  return
869
1062
  }
870
1063
  if (op === '.' && node[1] === name && JSS_OK_PROPS.has(node[2])) {
871
- // .length is always safe but polymorphic across string/array/typed.
872
- // .charCodeAt is only safe when the loop-bounds analysis proves the
873
- // index in-bounds (this dotted callee node is exactly the one collected
874
- // by scanBoundedLoops) — but it IS string-discriminating.
875
1064
  if (node[2] === 'length') return
876
1065
  if (safeCC.has(node)) { stringDiscriminating = true; return }
877
- ok = false
1066
+ fail(`\`.${node[2]}\` on the string param disables the zero-copy externref boundary carrier`)
878
1067
  return
879
1068
  }
880
- // For `['()', callee, ...args]` with callee = ['.', name, 'charCodeAt'],
881
- // `name` is in the callee receiver slot (validated by the branch above).
882
- // The `.length` case is just a member read — no `()` wrapping.
883
1069
  for (let i = 1; i < node.length; i++) walk(node[i])
884
1070
  }
885
1071
  walk(body)
886
- return { ok, stringDiscriminating }
1072
+ return { ok, stringDiscriminating, reason }
887
1073
  }
888
1074
 
889
1075
  function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
@@ -919,13 +1105,129 @@ function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
919
1105
  if (!stringDiscriminating && r?.val !== VAL.STRING && !hasStringDefault) continue
920
1106
  p.type = 'externref'
921
1107
  p.jsstring = true
922
- // Record the literal default so compile.js can omit wasm-side substitution
923
- // (the JS-side interop wrapper applies the default on `undefined`).
1108
+ updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
924
1109
  if (hasStringDefault) p.jsstringDefault = defVal[1]
925
1110
  }
926
1111
  }
927
1112
  }
928
1113
 
1114
+ /** Soft warnings when a string param could use the externref carrier but doesn't. */
1115
+ export function adviseJsstringCarrier(paramReps, valueUsed) {
1116
+ if (!ctx.warnings || !jsstringEnabled()) return
1117
+
1118
+ for (const func of ctx.func.list) {
1119
+ if (func.raw || !func.exported || !func.body || func.rest) continue
1120
+ if (valueUsed?.has(func.name)) continue
1121
+
1122
+ const safeCC = new Set()
1123
+ scanBoundedLoops(func.body, safeCC)
1124
+ const reps = paramReps?.get(func.name)
1125
+
1126
+ for (let k = 0; k < func.sig.params.length; k++) {
1127
+ const p = func.sig.params[k]
1128
+ if (p.jsstring) continue
1129
+ if (p.type !== 'f64' || p.ptrKind != null) continue
1130
+
1131
+ const defVal = func.defaults?.[p.name]
1132
+ if (defVal != null && !isLiteralStr(defVal)) continue
1133
+
1134
+ const r = reps?.get(k)
1135
+ if (r && r.val != null && r.val !== VAL.STRING) continue
1136
+
1137
+ const hasStringDefault = defVal != null && isLiteralStr(defVal)
1138
+ const { ok: usesOk, stringDiscriminating, reason } =
1139
+ paramAllUsesJsstringMappable(func.body, p.name, safeCC)
1140
+ const isCandidate = stringDiscriminating || r?.val === VAL.STRING || hasStringDefault
1141
+ if (!isCandidate || usesOk) continue
1142
+
1143
+ warn('jsstring-declined',
1144
+ `export '${func.name}' param '${p.name}': ${reason || 'string param uses disable the zero-copy externref boundary carrier'}`,
1145
+ { fn: func.name }, func.body.loc)
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ // Two value-kinds CONFLICT when passing one where the other is expected would
1151
+ // rely on a JS boundary coercion jz does not implement (so the result diverges).
1152
+ // NUMBER↔STRING is the canonical pair: `"5" * 2` is 10 in JS but NaN in jz, and a
1153
+ // STRING passed to a numeric param reads its NaN-boxed bits as an f64. ARRAY/OBJECT
1154
+ // vs NUMBER/STRING likewise. We treat the four primitive-ish kinds as mutually
1155
+ // exclusive; BOOL is omitted (it nanboxes to 0/1 and numeric code tolerates it).
1156
+ const STRICT_CONFLICT = {
1157
+ [VAL.NUMBER]: new Set([VAL.STRING, VAL.ARRAY, VAL.OBJECT]),
1158
+ [VAL.STRING]: new Set([VAL.NUMBER, VAL.ARRAY, VAL.OBJECT]),
1159
+ [VAL.ARRAY]: new Set([VAL.NUMBER, VAL.STRING]),
1160
+ [VAL.OBJECT]: new Set([VAL.NUMBER, VAL.STRING]),
1161
+ }
1162
+ const kindName = (v) => ({ [VAL.NUMBER]: 'number', [VAL.STRING]: 'string', [VAL.ARRAY]: 'array', [VAL.OBJECT]: 'object' }[v] || 'value')
1163
+
1164
+ /**
1165
+ * Strict-mode boundary type check (standalone; runs in both the full-narrow and
1166
+ * skip-narrow plan paths).
1167
+ *
1168
+ * jz infers a param's type from how it's used (`x * 2` → number, `s.charCodeAt`
1169
+ * → string) or from an explicit default (`x = 0` → number). In permissive mode a
1170
+ * caller may pass any type and jz silently computes a divergent result (`"5"*2`
1171
+ * is 10 in JS, NaN here). Strict mode is the canonical subset where that misuse
1172
+ * is a compile error instead — consistent with strict already rejecting `==`,
1173
+ * dynamic dispatch, and `void`.
1174
+ *
1175
+ * Fires ONLY when BOTH sides are statically certain and conflict:
1176
+ * - the callee param has a known kind (its default-value type, or a settled
1177
+ * `val` rep from body-usage / call-site inference), AND
1178
+ * - the argument expression has a known, conflicting kind (a literal or a
1179
+ * locally-typed binding).
1180
+ * An untyped param or untyped arg is never flagged — no false positives on
1181
+ * genuinely polymorphic code.
1182
+ */
1183
+ export function strictBoundaryTypeCheck(programFacts) {
1184
+ if (!ctx.transform.strict) return
1185
+ const { callSites, paramReps } = programFacts
1186
+
1187
+ // Per-callee body-evidence cache: methodEvidence (.charCodeAt→STRING, .push→
1188
+ // ARRAY) keyed by param name. Computed lazily, once per callee.
1189
+ const bodyEvidence = new Map()
1190
+ const evidenceOf = (func) => {
1191
+ if (!bodyEvidence.has(func.name)) {
1192
+ const names = func.sig.params.map(p => p.name).filter(Boolean)
1193
+ bodyEvidence.set(func.name, func.body ? inferParams(func.body, names) : new Map())
1194
+ }
1195
+ return bodyEvidence.get(func.name)
1196
+ }
1197
+
1198
+ // Expected kind of callee param k, from any statically-certain source:
1199
+ // 1. explicit default value type — the source's own declaration (x = 0 → number)
1200
+ // 2. settled call-site/usage rep `val` (paramReps; present after full narrow)
1201
+ // 3. type-exclusive body evidence (methodEvidence; works in skip-narrow path too)
1202
+ // First certain source wins; null when the param is genuinely polymorphic.
1203
+ const paramKind = (func, k) => {
1204
+ const p = func.sig.params[k]
1205
+ if (!p) return null
1206
+ const def = func.defaults?.[p.name]
1207
+ if (def != null) { const dv = valTypeOf(def); if (dv) return dv }
1208
+ const repVal = paramReps?.get(func.name)?.get(k)?.val
1209
+ if (repVal != null) return repVal
1210
+ return evidenceOf(func).get(p.name)?.val ?? null
1211
+ }
1212
+
1213
+ for (const cs of callSites) {
1214
+ const func = ctx.func.map.get(cs.callee)
1215
+ if (!func || func.raw || !func.sig) continue
1216
+ if (func.rest) continue // rest packs args into an array
1217
+ for (let k = 0; k < cs.argList.length && k < func.sig.params.length; k++) {
1218
+ const want = paramKind(func, k)
1219
+ if (want == null) continue
1220
+ const conflicts = STRICT_CONFLICT[want]
1221
+ if (!conflicts) continue
1222
+ const got = valTypeOf(cs.argList[k])
1223
+ if (got != null && conflicts.has(got)) {
1224
+ const pname = func.sig.params[k].name
1225
+ err(`strict mode: ${kindName(want)} parameter '${pname}' of '${cs.callee}' received a ${kindName(got)} argument — jz does not coerce ${kindName(got)}→${kindName(want)} at the call boundary (the result would diverge from JS). Pass a ${kindName(want)}, or { strict: false }.`)
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+
929
1231
  /**
930
1232
  * Phase: bimorphic typed-array param specialization.
931
1233
  *
@@ -960,13 +1262,9 @@ export function specializeBimorphicTyped(programFacts) {
960
1262
  if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
961
1263
  }
962
1264
 
963
- // Per-caller typedElem map (literal `new TypedArray(N)` bindings inside body).
964
- const callerTypedCtx = new Map()
965
- callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
966
- for (const func of ctx.func.list) {
967
- if (!func.body || func.raw) continue
968
- callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
969
- }
1265
+ // Per-caller typedElem map: body-local `new TypedArray(N)` bindings layered
1266
+ // over the module's typed globals (shared with buildCallerTypedCtx).
1267
+ const callerTypedCtx = buildCallerTypedCtx()
970
1268
  // Per-caller typed-param map: caller's own params that F/G already narrowed
971
1269
  // (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
972
1270
  const callerTypedParamsCtx = new Map()
@@ -1142,7 +1440,12 @@ export function refineDynKeys(programFacts) {
1142
1440
  if (!NON_DYN_VTS.has(vt)) real = true
1143
1441
  }
1144
1442
  } else if (op === 'for-in') real = true
1145
- if (op === '=>') return
1443
+ // Recurse into nested arrows too. Closures stay inline (defFunc skips
1444
+ // depth>0), so a dynamic-key access captured in one — e.g. `handlers[op]`
1445
+ // in a dispatch closure — is reachable only through its parent's body.
1446
+ // Matches collectProgramFacts, which also crosses arrows when setting
1447
+ // anyDyn; not crossing here let refineDynKeys reset a flag the initial scan
1448
+ // correctly raised. Monotone-safe: extra visits only ever raise `real`.
1146
1449
  for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
1147
1450
  }
1148
1451