jz 0.5.1 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /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 } 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
27
  inferSchemaId, inferValType, inferTypedCtor,
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
  }
@@ -256,7 +264,7 @@ function narrowI32Results(funcs) {
256
264
  while (changed) {
257
265
  changed = false
258
266
  for (const func of funcs) {
259
- if (func.sig.results[0] === 'i32') continue
267
+ if (func.sig.results[0] === 'i32' || func.sig.results[0] === 'v128') continue
260
268
  const body = func.body
261
269
  if (isBlockBody(body) && hasBareReturn(body)) continue
262
270
  const exprs = returnExprs(body)
@@ -265,12 +273,16 @@ function narrowI32Results(funcs) {
265
273
  ctx.func.current = func.sig
266
274
  const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
267
275
  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')
276
+ const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
277
+ const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
269
278
  const anyUnsigned = exprs.some(isUnsignedTail)
270
279
  const allUnsigned = exprs.every(isUnsignedTail)
271
280
  ctx.func.current = savedCurrent
272
- // Narrow only when sign is consistent (all-signed or all-unsigned tails).
273
- if (allI32 && (!anyUnsigned || allUnsigned)) {
281
+ // SIMD: every tail returns a lane vector v128 result.
282
+ if (allV128) {
283
+ func.sig.results = ['v128']
284
+ changed = true
285
+ } else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
274
286
  func.sig.results = ['i32']
275
287
  if (allUnsigned) func.sig.unsignedResult = true
276
288
  changed = true
@@ -401,11 +413,47 @@ function typedAuxOfReturn(expr, localElemMap) {
401
413
  * Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
402
414
  * outer's call to inner contributes a known schema-id.
403
415
  */
416
+ /** A function whose every return is the same parameter that was pointer-ABI
417
+ * narrowed to an unboxed i32 (p.ptrKind set). Returns that param, else null. */
418
+ function passthroughPtrParam(func) {
419
+ const exprs = returnExprs(func.body)
420
+ if (!exprs.length) return null
421
+ const name = exprs[0]
422
+ if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
423
+ return func.sig.params.find(p => p.name === name && p.ptrKind) || null
424
+ }
425
+
404
426
  function narrowPointerResults(funcs, paramReps) {
405
427
  let changed = true
406
428
  while (changed) {
407
429
  changed = false
408
430
  for (const func of funcs) {
431
+ // Pointer pass-through: every return is the same parameter that
432
+ // applyPointerParamAbi narrowed to an unboxed i32 pointer. The result IS that
433
+ // pointer, so its sig must carry the param's ptrKind (+ schemaId for OBJECT).
434
+ // Without this the result is a bare i32 the caller numeric-converts
435
+ // (`f64.convert_i32_s`) instead of reboxing — dropping the schema-id so a
436
+ // later `.prop` read mis-resolves to `undefined`. narrowValResults can't see
437
+ // this (it reads body-locals, not param facts) and narrowI32Results steals it
438
+ // as a numeric i32, so resolve it here from the settled param lattice.
439
+ if (func.sig.ptrKind == null) {
440
+ const pp = passthroughPtrParam(func)
441
+ if (pp) {
442
+ const aux = pp.ptrKind === VAL.OBJECT
443
+ ? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
444
+ : null
445
+ // OBJECT needs a known schema-id to rebox; a polymorphic pass-through
446
+ // (conflicting schemas → null) keeps its current handling.
447
+ if (pp.ptrKind !== VAL.OBJECT || aux != null) {
448
+ func.sig.results = ['i32']
449
+ func.sig.ptrKind = pp.ptrKind
450
+ func.valResult = pp.ptrKind
451
+ if (aux != null) func.sig.ptrAux = aux
452
+ changed = true
453
+ continue
454
+ }
455
+ }
456
+ }
409
457
  if (!func.valResult) continue
410
458
  if (func.sig.results[0] !== 'f64') continue
411
459
  const isBlock = isBlockBody(func.body)
@@ -467,7 +515,7 @@ function createPhaseState() {
467
515
 
468
516
  invalidateBodyFacts() {
469
517
  for (const func of ctx.func.list) {
470
- if (func.body && !func.raw) invalidateValTypesCache(func.body)
518
+ if (func.body && !func.raw) invalidateLocalsCache(func.body)
471
519
  }
472
520
  clearDerived()
473
521
  },
@@ -484,6 +532,67 @@ function createPhaseState() {
484
532
  }
485
533
  }
486
534
 
535
+ const _FIELD_TO_SLICE = {
536
+ arrayElemSchema: 'arrElemSchemas',
537
+ arrayElemValType: 'arrElemValTypes',
538
+ }
539
+
540
+ /** Propagate Array<T> element facts from return paths into caller paramReps (phase G). */
541
+ function narrowReturnArrayElems(field, paramReps, valueUsed) {
542
+ const sliceKey = _FIELD_TO_SLICE[field]
543
+ const targets = ctx.func.list.filter(f =>
544
+ !f.raw && !f.exported && !valueUsed.has(f.name) &&
545
+ f.valResult === VAL.ARRAY && f[field] == null
546
+ )
547
+ let changed = true
548
+ while (changed) {
549
+ changed = false
550
+ for (const f of targets) invalidateLocalsCache(f.body)
551
+ for (const func of targets) {
552
+ if (func[field] != null) continue
553
+ const isBlock = isBlockBody(func.body)
554
+ if (isBlock && !alwaysReturns(func.body)) continue
555
+ const exprs = returnExprs(func.body)
556
+ if (!exprs.length) continue
557
+ const savedLocals = ctx.func.locals
558
+ const facts = analyzeBody(func.body)
559
+ ctx.func.locals = new Map(facts.locals)
560
+ for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
561
+ const localElems = facts[sliceKey]
562
+ ctx.func.locals = savedLocals
563
+ const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
564
+ const resolveExpr = (expr) => {
565
+ if (typeof expr === 'string') {
566
+ if (localElems.has(expr)) {
567
+ const v = localElems.get(expr)
568
+ if (v != null) return v
569
+ }
570
+ if (paramElemMap.has(expr)) return paramElemMap.get(expr)
571
+ return null
572
+ }
573
+ if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
574
+ const f = ctx.func.map?.get(expr[1])
575
+ if (f?.[field] != null) return f[field]
576
+ }
577
+ if (Array.isArray(expr) && expr[0] === '?:') {
578
+ const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
579
+ return a != null && a === b ? a : null
580
+ }
581
+ if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
582
+ const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
583
+ return a != null && a === b ? a : null
584
+ }
585
+ return null
586
+ }
587
+ const v0 = resolveExpr(exprs[0])
588
+ if (v0 == null) continue
589
+ if (!exprs.every(e => resolveExpr(e) === v0)) continue
590
+ func[field] = v0
591
+ changed = true
592
+ }
593
+ }
594
+ }
595
+
487
596
  export default function narrowSignatures(programFacts, ast) {
488
597
  const { callSites, valueUsed, paramReps, hasSchemaLiterals } = programFacts
489
598
 
@@ -518,29 +627,35 @@ export default function narrowSignatures(programFacts, ast) {
518
627
  return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
519
628
  }
520
629
 
630
+ // Per-call-site inference context for a narrowable callee. null for call sites
631
+ // whose callee is exported / value-used / unknown, or whose caller has no ctx.
632
+ const siteState = (cs) => {
633
+ const { callee, argList, callerFunc } = cs
634
+ const func = ctx.func.map.get(callee)
635
+ if (!func || func.exported || valueUsed.has(callee)) return null
636
+ const ctxEntry = callerCtx.get(callerFunc)
637
+ if (!ctxEntry) return null
638
+ const restIdx = func.rest ? func.sig.params.length - 1 : -1
639
+ const paramFacts = new Map()
640
+ return {
641
+ callee, callerFunc, argList, func, restIdx,
642
+ callerLocals: ctxEntry.callerLocals,
643
+ callerValTypes: ctxEntry.callerValTypes,
644
+ callerParamFacts(key) {
645
+ if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
646
+ return paramFacts.get(key)
647
+ },
648
+ }
649
+ }
521
650
  const runCallsiteLattice = (rules) => {
522
651
  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
- }
652
+ const state = siteState(callSites[s])
653
+ if (!state) continue
654
+ const { func, argList } = state
539
655
  for (let k = 0; k < func.sig.params.length; k++) {
540
- const r = ensureParamRep(paramReps, callee, k)
656
+ const r = ensureParamRep(paramReps, state.callee, k)
541
657
  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)
658
+ for (const rule of rules) rule.apply(r, argList[k], k, state)
544
659
  }
545
660
  }
546
661
  }
@@ -568,7 +683,33 @@ export default function narrowSignatures(programFacts, ast) {
568
683
  const pname = state.func.sig.params[k]?.name
569
684
  return pname != null ? state.func.defaults?.[pname] : null
570
685
  }
571
- const mergeRule = (field, infer) => ({
686
+ // Hard consensus val for (funcName, param k): the kind every live call site
687
+ // agrees on, or null if any site is untyped / missing / disagrees. The shared
688
+ // `val` lattice runs SOFT (a value can come from typed sites alone, untyped
689
+ // sites skipped); a consumer that *mutates the signature* off val must instead
690
+ // ask this — it re-folds the sites HARD so it never specializes a param that
691
+ // some call site can't prove. (applyPointerParamAbi is that consumer.)
692
+ const hardParamVal = (funcName, k) => {
693
+ let consensus
694
+ for (let s = 0; s < callSites.length; s++) {
695
+ if (callSites[s].callee !== funcName) continue
696
+ const state = siteState(callSites[s])
697
+ if (!state) continue
698
+ if (k >= state.argList.length) return null // missing → undefined at runtime
699
+ const v = inferValAtSite(state.argList[k], state)
700
+ if (v == null) return null // an untyped site ⇒ not specializable
701
+ if (consensus === undefined) consensus = v
702
+ else if (consensus !== v) return null // disagreement ⇒ TOP
703
+ }
704
+ return consensus ?? null
705
+ }
706
+ // `soft` makes apply treat a null inference as BOTTOM (skip — "this site can't
707
+ // tell yet") instead of TOP (poison): the monotone meet. A soft field never
708
+ // needs clearStickyNull; its consumers either re-validate hard (hardParamVal)
709
+ // or read it after a final hard settling sweep. `missing` poisons regardless —
710
+ // an omitted arg with no default is undefined at runtime, a real reason not to
711
+ // specialize, and must stay sticky.
712
+ const mergeRule = (field, infer, soft = false) => ({
572
713
  missing(r, k, state) {
573
714
  if (r[field] === null) return
574
715
  const def = defaultArg(state, k)
@@ -576,11 +717,19 @@ export default function narrowSignatures(programFacts, ast) {
576
717
  else r[field] = null
577
718
  },
578
719
  apply(r, arg, k, state) {
579
- if (r[field] !== null) mergeParamFact(r, field, infer(arg, k, state))
720
+ if (r[field] === null) return
721
+ const v = infer(arg, k, state)
722
+ if (v == null) { if (!soft) r[field] = null; return }
723
+ mergeParamFact(r, field, v)
580
724
  },
581
725
  })
582
726
  const runFixpoint = () => runCallsiteLattice([
583
- mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state)),
727
+ // val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
728
+ // typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
729
+ // sticky-poison it (the old clearStickyNull undid that). Soft leaves it BOTTOM;
730
+ // the post-enrichment rerun fills it in. applyPointerParamAbi re-validates via
731
+ // hardParamVal; a final hard sweep settles val for emit + late consumers.
732
+ mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state), true),
584
733
  {
585
734
  missing: poison('wasm'),
586
735
  apply(r, arg, _k, state) {
@@ -620,6 +769,16 @@ export default function narrowSignatures(programFacts, ast) {
620
769
  }
621
770
  const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
622
771
  const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
772
+
773
+ // E2 (VAL-kind result inference) FIRST: it's body-driven and call-chain self-
774
+ // fixpointing — independent of the param lattice and the narrowing acts (it reads
775
+ // analyzeBody valTypes + callees' valResult, never paramReps or sig.params). Running
776
+ // it up front means a call arg like `initRows()` resolves to its VAL.ARRAY result on
777
+ // the param fixpoint's FIRST pass, so val/schemaId never get the can't-tell-yet
778
+ // poison that clearStickyNull used to un-stick (root B). Numeric (i32) result
779
+ // narrowing stays below — it benefits from i32 params being narrowed first.
780
+ const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
781
+ narrowValResults(funcsWithNarrowableResult)
623
782
  runFixpoint()
624
783
  runFixpoint()
625
784
 
@@ -645,14 +804,12 @@ export default function narrowSignatures(programFacts, ast) {
645
804
  // (aux carries element-type, handled separately by applyTypedPointerParamAbi).
646
805
  // - exclude params with defaults (nullish sentinel needs the f64 NaN space).
647
806
  // - exclude rest position (array pack/unpack stays f64).
648
- applyPointerParamAbi(paramReps, valueUsed)
807
+ applyPointerParamAbi(paramReps, valueUsed, hardParamVal)
649
808
 
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)
809
+ // E: numeric (i32) result narrowing kept here, after applyI32ParamSpecialization,
810
+ // so a body returning `param + 1` sees param already narrowed to i32. (E2 / VAL
811
+ // result inference ran up front — see above.) funcsWithNarrowableResult hoisted there.
654
812
  narrowI32Results(funcsWithNarrowableResult)
655
- narrowValResults(funcsWithNarrowableResult)
656
813
 
657
814
  // Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
658
815
  // VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
@@ -673,11 +830,9 @@ export default function narrowSignatures(programFacts, ast) {
673
830
  // observation upgrade `undefined` → NUMBER without poisoning earlier
674
831
  // monomorphic observations.
675
832
  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')
833
+ // Re-run with refreshed callerValTypes + the new program-slot observations. (No
834
+ // clearStickyNull needed: valResult was known before the first pass see E2 hoist
835
+ // above so val/schemaId never got the can't-tell-yet poison this used to undo.)
681
836
  runFixpoint()
682
837
  // Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
683
838
  runArrFixpoint()
@@ -720,7 +875,7 @@ export default function narrowSignatures(programFacts, ast) {
720
875
  // i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
721
876
  // per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
722
877
  // that today dominates hot loops dominated by typed-array indexing.
723
- // Call sites coerce via emitArgForParam → ptrOffsetIR(arg, VAL.TYPED).
878
+ // Call sites coerce via coerceArg → ptrOffsetIR(arg, VAL.TYPED).
724
879
  // Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
725
880
  // exported, value-used, raw, defaults, rest position.
726
881
  applyTypedPointerParamAbi(paramReps, valueUsed)
@@ -728,11 +883,11 @@ export default function narrowSignatures(programFacts, ast) {
728
883
  // H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
729
884
  // where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
730
885
  // → 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]`.
886
+ // for caller's params, so inferValType returned null and (under the old hard merge)
887
+ // sticky-poisoned the param's val. The soft val merge leaves it BOTTOM instead, so
888
+ // this rerun now that enrichment has put VAL.TYPED into callerValTypes — simply
889
+ // fills it in (array.js then skips __is_str_key + __str_idx dispatch on `arr[i]`).
734
890
  enrichCallerValTypesFromPointerParams(callerCtx)
735
- clearStickyNull(paramReps, 'val')
736
891
  runFixpoint()
737
892
 
738
893
  // I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
@@ -750,6 +905,12 @@ export default function narrowSignatures(programFacts, ast) {
750
905
  // so the refreshed exprType view propagates.
751
906
  resetParamWasmFacts(paramReps)
752
907
  runFixpoint()
908
+ // Settle val HARD now that every producer (results, typedCtor, enrichment) has run
909
+ // and the soft lattice has converged: re-fold each param's sites and poison any
910
+ // whose val isn't unanimous (a site left BOTTOM = genuinely untyped). After this,
911
+ // r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
912
+ // ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
913
+ runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
753
914
  // Don't steal typed-array params from specializeBimorphicTyped: F phase parks
754
915
  // bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
755
916
  // ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
@@ -764,9 +925,12 @@ export default function narrowSignatures(programFacts, ast) {
764
925
  if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
765
926
  }
766
927
 
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). */
928
+ /** Gate the jsstring carrier on the host. ON by default for the JS host: a
929
+ * js-host build is already JS-locked (it imports `env.*`), so the externref +
930
+ * `wasm:js-string` carrier's JS dependency is free there, and the zero-copy
931
+ * string-read path is a clear win. OFF under WASI: the carrier needs a JS host,
932
+ * and wasi builds must stay portable (wasmtime/Go/Rust). Opt out on JS with
933
+ * `optimize: { jsstring: false }` (e.g. side-by-side benchmarks). */
770
934
  function jsstringEnabled() {
771
935
  if (ctx.transform.host === 'wasi') return false
772
936
  if (ctx.transform.optimize?.jsstring === false) return false
@@ -782,13 +946,15 @@ export function applyJsstringBoundaryCarrierStandalone(programFacts) {
782
946
  }
783
947
 
784
948
  /**
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.
949
+ * Body-local boolean/bigint-result inference. `narrowValResults` is the general
950
+ * (any VAL.*) pass, but it lives inside whole-program narrowing, which is skipped
951
+ * for trivial leaf modules (no call sites). Boolean and bigint are the two kinds
952
+ * whose internal carrier differs from the host-boundary carrier — bool rides a 0/1
953
+ * number internally but crosses as the TRUE_NAN/FALSE_NAN atom; bigint rides an
954
+ * i64-reinterpreted f64 internally but must cross as a real Number so an exported
955
+ * `(a) => a > 2` or `() => 100n` still needs its boundary thunk even on the skip path.
956
+ * This pass only ever *sets* valResult to VAL.BOOL / VAL.BIGINT, so it is safe to run
957
+ * unconditionally — pointer/array/number results are untouched.
792
958
  */
793
959
  export function narrowBoolResults() {
794
960
  for (const func of ctx.func.list) {
@@ -803,6 +969,7 @@ export function narrowBoolResults() {
803
969
  ? (localValTypes?.get(e) || ctx.scope.globalValTypes?.get(e) || null)
804
970
  : valTypeOf(e)
805
971
  if (exprs.every(e => vt(e) === VAL.BOOL)) func.valResult = VAL.BOOL
972
+ else if (exprs.every(e => vt(e) === VAL.BIGINT)) func.valResult = VAL.BIGINT
806
973
  }
807
974
  }
808
975
 
@@ -828,35 +995,37 @@ const JSS_OK_PROPS = new Set(['length', 'charCodeAt'])
828
995
  * `.charCodeAt` whose callee node lives in `safeCC` (provably bounded).
829
996
  * Reassignment / `++` / `--` / closure capture all reject conservatively.
830
997
  *
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.
998
+ * Returns `{ ok, stringDiscriminating, reason? }` — `stringDiscriminating` is
999
+ * true iff we saw at least one string-only use (`.charCodeAt`); `reason` names
1000
+ * the first blocking use when `ok` is false.
835
1001
  */
836
1002
  function paramAllUsesJsstringMappable(body, name, safeCC) {
837
- if (body == null) return { ok: false, stringDiscriminating: false }
1003
+ if (body == null) return { ok: false, stringDiscriminating: false, reason: null }
838
1004
  let ok = true
839
1005
  let stringDiscriminating = false
1006
+ let reason = null
1007
+ const fail = (msg) => { ok = false; reason ||= msg }
1008
+ const refsParam = (node) => {
1009
+ if (node === name) return true
1010
+ if (!Array.isArray(node)) return false
1011
+ for (let i = 1; i < node.length; i++) if (refsParam(node[i])) return true
1012
+ return false
1013
+ }
840
1014
  const walk = (node) => {
841
1015
  if (!ok) return
842
1016
  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
1017
+ if (node === name) fail('bare use of the string param disables the zero-copy externref boundary carrier')
847
1018
  return
848
1019
  }
849
1020
  if (!Array.isArray(node)) return
850
1021
  const op = node[0]
851
1022
  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
1023
  const params = node[1]
855
1024
  const shadowed = Array.isArray(params)
856
1025
  ? params.some(p => (typeof p === 'string' && p === name) ||
857
1026
  (Array.isArray(p) && p[1] === name))
858
1027
  : params === name
859
- if (!shadowed) ok = false
1028
+ if (!shadowed) fail('closure capture of the string param disables the zero-copy externref boundary carrier')
860
1029
  return
861
1030
  }
862
1031
  if ((op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' ||
@@ -864,26 +1033,23 @@ function paramAllUsesJsstringMappable(body, name, safeCC) {
864
1033
  op === '>>=' || op === '<<=' || op === '>>>=' ||
865
1034
  op === '||=' || op === '&&=' || op === '??=' ||
866
1035
  op === '++' || op === '--') && node[1] === name) {
867
- ok = false
1036
+ fail('reassigning the string param disables the zero-copy externref boundary carrier')
1037
+ return
1038
+ }
1039
+ if (op === '+' && node.slice(1).some(arg => refsParam(arg))) {
1040
+ fail('string concatenation on the param disables the zero-copy externref boundary carrier')
868
1041
  return
869
1042
  }
870
1043
  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
1044
  if (node[2] === 'length') return
876
1045
  if (safeCC.has(node)) { stringDiscriminating = true; return }
877
- ok = false
1046
+ fail(`\`.${node[2]}\` on the string param disables the zero-copy externref boundary carrier`)
878
1047
  return
879
1048
  }
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
1049
  for (let i = 1; i < node.length; i++) walk(node[i])
884
1050
  }
885
1051
  walk(body)
886
- return { ok, stringDiscriminating }
1052
+ return { ok, stringDiscriminating, reason }
887
1053
  }
888
1054
 
889
1055
  function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
@@ -919,13 +1085,48 @@ function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
919
1085
  if (!stringDiscriminating && r?.val !== VAL.STRING && !hasStringDefault) continue
920
1086
  p.type = 'externref'
921
1087
  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`).
1088
+ updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
924
1089
  if (hasStringDefault) p.jsstringDefault = defVal[1]
925
1090
  }
926
1091
  }
927
1092
  }
928
1093
 
1094
+ /** Soft warnings when a string param could use the externref carrier but doesn't. */
1095
+ export function adviseJsstringCarrier(paramReps, valueUsed) {
1096
+ if (!ctx.warnings || !jsstringEnabled()) return
1097
+
1098
+ for (const func of ctx.func.list) {
1099
+ if (func.raw || !func.exported || !func.body || func.rest) continue
1100
+ if (valueUsed?.has(func.name)) continue
1101
+
1102
+ const safeCC = new Set()
1103
+ scanBoundedLoops(func.body, safeCC)
1104
+ const reps = paramReps?.get(func.name)
1105
+
1106
+ for (let k = 0; k < func.sig.params.length; k++) {
1107
+ const p = func.sig.params[k]
1108
+ if (p.jsstring) continue
1109
+ if (p.type !== 'f64' || p.ptrKind != null) continue
1110
+
1111
+ const defVal = func.defaults?.[p.name]
1112
+ if (defVal != null && !isLiteralStr(defVal)) continue
1113
+
1114
+ const r = reps?.get(k)
1115
+ if (r && r.val != null && r.val !== VAL.STRING) continue
1116
+
1117
+ const hasStringDefault = defVal != null && isLiteralStr(defVal)
1118
+ const { ok: usesOk, stringDiscriminating, reason } =
1119
+ paramAllUsesJsstringMappable(func.body, p.name, safeCC)
1120
+ const isCandidate = stringDiscriminating || r?.val === VAL.STRING || hasStringDefault
1121
+ if (!isCandidate || usesOk) continue
1122
+
1123
+ warn('jsstring-declined',
1124
+ `export '${func.name}' param '${p.name}': ${reason || 'string param uses disable the zero-copy externref boundary carrier'}`,
1125
+ { fn: func.name }, func.body.loc)
1126
+ }
1127
+ }
1128
+ }
1129
+
929
1130
  /**
930
1131
  * Phase: bimorphic typed-array param specialization.
931
1132
  *
@@ -1142,7 +1343,12 @@ export function refineDynKeys(programFacts) {
1142
1343
  if (!NON_DYN_VTS.has(vt)) real = true
1143
1344
  }
1144
1345
  } else if (op === 'for-in') real = true
1145
- if (op === '=>') return
1346
+ // Recurse into nested arrows too. Closures stay inline (defFunc skips
1347
+ // depth>0), so a dynamic-key access captured in one — e.g. `handlers[op]`
1348
+ // in a dispatch closure — is reachable only through its parent's body.
1349
+ // Matches collectProgramFacts, which also crosses arrows when setting
1350
+ // anyDyn; not crossing here let refineDynKeys reset a flag the initial scan
1351
+ // correctly raised. Monotone-safe: extra visits only ever raise `real`.
1146
1352
  for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
1147
1353
  }
1148
1354