jz 0.8.0 → 0.9.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 (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
@@ -8,16 +8,17 @@
8
8
  */
9
9
 
10
10
  import { ctx, warn, err } from '../ctx.js'
11
- import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
11
+ import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs, callArgs, ASSIGN_OPS, extractParams, classifyParam } from '../ast.js'
12
12
  import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
13
13
  import {
14
- analyzeBody, findMutations, invalidateLocalsCache,
14
+ analyzeBody, findMutations, invalidateLocalsCache, mayBeNullish,
15
15
  } from './analyze.js'
16
16
  import { staticObjectProps } from '../static.js'
17
17
  import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
18
18
  import { typedElemAux, ctorFromElemAux } from '../../layout.js'
19
19
  import { observeProgramSlots } from './program-facts.js'
20
20
  import { valTypeOf } from '../kind.js'
21
+ import { typedCtorElemValType } from '../kind-traits.js'
21
22
  import { VAL, updateRep } from '../reps.js'
22
23
  import {
23
24
  paramFactsOf, ensureParamRep, mergeParamFact,
@@ -62,12 +63,17 @@ function filterLiveCallSites(callSites, valueUsed) {
62
63
 
63
64
  function buildCallerCtx() {
64
65
  const callerCtx = new Map()
65
- callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes })
66
+ const globalTE = ctx.scope.globalTypedElem || new Map()
67
+ callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes, callerTypedElems: globalTE })
66
68
  for (const func of ctx.func.list) {
67
69
  if (!func.body || func.raw) continue
68
70
  const facts = analyzeBody(func.body)
69
71
  for (const p of func.sig.params) if (!facts.locals.has(p.name)) facts.locals.set(p.name, p.type)
70
- callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes })
72
+ // Shadow-aware local+global typed-array map: a `const buf = new Int32Array(…)`
73
+ // local makes `buf[i]` arg reads type i32 at this caller's sites, so a callee
74
+ // param fed only such elements narrows (else it stays f64 and `1 << p` drags in
75
+ // __to_num → the whole string↔number stdlib). Mirrors callerTypedElemsFor.
76
+ callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes, callerTypedElems: callerTypedElemsFor(func, globalTE) })
71
77
  }
72
78
  return callerCtx
73
79
  }
@@ -88,13 +94,23 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
88
94
  const reps = paramReps.get(func.name)
89
95
  if (!reps) continue
90
96
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
97
+ // A narrowed param type is a CALLER-side contract; the body keeps it only if it
98
+ // never writes the param (a body write emits through the generic f64 assign path
99
+ // — `a += 1` after an i32 narrow is a validation-failing local.set type clash).
100
+ // Same exclusion validateIntConstParams applies, for the same reason.
101
+ let mutated = null
91
102
  for (const [k, r] of reps) {
92
103
  if (k === restIdx || k >= func.sig.params.length) continue
93
104
  const p = func.sig.params[k]
94
105
  if (func.defaults?.[p.name] != null) continue
106
+ if (r.wasm !== 'v128' && (r.wasm !== 'i32' || p.type === 'i32')) continue
107
+ if (mutated === null) {
108
+ mutated = new Set()
109
+ if (func.body) findMutations(func.body, new Set(func.sig.params.map(p => p.name)), mutated)
110
+ }
111
+ if (mutated.has(p.name)) continue
95
112
  // SIMD: a param passed a v128 (lane vector) at every call site is a v128 param.
96
113
  if (r.wasm === 'v128') { p.type = 'v128'; continue }
97
- if (r.wasm !== 'i32' || p.type === 'i32') continue
98
114
  if (skipTyped && r.val === VAL.TYPED) continue
99
115
  p.type = 'i32'
100
116
  }
@@ -129,7 +145,14 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
129
145
  const reps = paramReps.get(func.name)
130
146
  if (!reps) continue
131
147
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
132
- for (const [k] of reps) {
148
+ // A pointer-narrowed param is a CALLER-side contract (unboxed i32 offset,
149
+ // reads rebox via ptrKind/ptrAux). The body keeps it only if it never
150
+ // WRITES the param: a reassignment (`v = v['@@iterator']()`) stores a
151
+ // boxed f64 into the i32 local — mixed views, wasm validation failure
152
+ // (the recorded reassigned-param kind bug). Same rule the wasm-type
153
+ // narrowing applies, for the same reason.
154
+ let mutated = null
155
+ for (const [k, r] of reps) {
133
156
  // Re-fold call sites HARD (the shared val lattice is soft, so r.val may be a
134
157
  // partial consensus from typed sites alone) — only specialize when every site
135
158
  // proves the same pointer kind.
@@ -140,6 +163,31 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
140
163
  const p = func.sig.params[k]
141
164
  if (p.type === 'i32') continue
142
165
  if (func.defaults?.[p.name] != null) continue
166
+ if (mutated === null) {
167
+ mutated = new Set()
168
+ if (func.body) findMutations(func.body, new Set(func.sig.params.map(q => q.name)), mutated)
169
+ }
170
+ if (mutated.has(p.name)) continue
171
+ // OBJECT is the one PTR_ABI_KINDS member whose unboxed i32 offset is
172
+ // ambiguous without a schema id: SET/MAP/BUFFER have a fixed runtime layout
173
+ // (aux always 0, per narrowPointerResults), but an OBJECT's payload slots are
174
+ // laid out per-schema, and the offset alone can't tell a reader which schema
175
+ // to rebox against. r.schemaId is the SAME hard (never-reset) call-site fact
176
+ // narrowPointerResults' return-value arm trusts (param-reps.js: "schemaId ...
177
+ // stay HARD"); demanding it here too, and skipping the narrow when it's absent
178
+ // or conflicting, closes the gap this function used to leave: it set
179
+ // p.ptrKind = VAL.OBJECT with no p.ptrAux, so every later reboxer of this
180
+ // param — asF64's boxPtrIR defaults an omitted aux to 0 — stamped the offset
181
+ // with WHATEVER schema happens to be id 0 program-wide (a live, unrelated
182
+ // object's field layout, not this parameter's own). A fresh instance built
183
+ // from that mistagged parameter (watr's own `normalize(opts)`, opts narrowed
184
+ // this way with no callers.length>1 disagreement to save it) then reads as
185
+ // if it belonged to schema 0 — the wild "phantom keys" class of miscompile.
186
+ if (hv === VAL.OBJECT) {
187
+ const aux = r.schemaId
188
+ if (aux == null) continue
189
+ p.ptrAux = aux
190
+ }
143
191
  p.type = 'i32'
144
192
  p.ptrKind = hv
145
193
  }
@@ -197,6 +245,7 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
197
245
  const reps = paramReps.get(func.name)
198
246
  if (!reps) continue
199
247
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
248
+ let mutated = null // body-write guard — same contract as applyPointerParamAbi
200
249
  for (const [k, r] of reps) {
201
250
  const ctor = r.typedCtor
202
251
  if (ctor == null) continue
@@ -205,6 +254,11 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
205
254
  const p = func.sig.params[k]
206
255
  if (p.type === 'i32') continue
207
256
  if (func.defaults?.[p.name] != null) continue
257
+ if (mutated === null) {
258
+ mutated = new Set()
259
+ if (func.body) findMutations(func.body, new Set(func.sig.params.map(q => q.name)), mutated)
260
+ }
261
+ if (mutated.has(p.name)) continue
208
262
  const aux = typedElemAux(ctor)
209
263
  if (aux == null) continue
210
264
  p.type = 'i32'
@@ -497,14 +551,51 @@ function typedAuxOfReturn(expr, localElemMap) {
497
551
  * Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
498
552
  * outer's call to inner contributes a known schema-id.
499
553
  */
554
+ /** True iff return-expr `e` is provably just `paramName` unchanged: either the bare
555
+ * name itself, or a recursive call to `func.name` that forwards `paramName` at its
556
+ * OWN parameter index (`return f(x, out, y)` inside `f`) — by induction on recursion
557
+ * depth, that call's result is whatever `f` would return given that same value, which
558
+ * bottoms out at the direct-return arms below. Strict AST-identity match only (no
559
+ * attempt to prove two *different* expressions are equal at runtime). */
560
+ function passesParamThrough(e, paramName, paramIdx, funcName) {
561
+ if (e === paramName) return true
562
+ if (!Array.isArray(e) || e[0] !== '()' || e[1] !== funcName) return false
563
+ return callArgs(e)[paramIdx] === paramName
564
+ }
565
+
500
566
  /** A function whose every return is the same parameter that was pointer-ABI
501
- * narrowed to an unboxed i32 (p.ptrKind set). Returns that param, else null. */
567
+ * narrowed to an unboxed i32 (p.ptrKind set) directly, or via a same-function
568
+ * recursive call that forwards it unchanged (see passesParamThrough). Without the
569
+ * recursive case, a function like flow-types.js's extractRefinements — whose `!`
570
+ * branch delegates via `return extractRefinements(cond[1], out, !sense)` instead of
571
+ * a bare `return out` — fails the naive all-return-exprs-are-the-bare-name check on
572
+ * that ONE path, so the whole function loses ptrKind tracking (see
573
+ * narrowPointerResults below): the caller then numeric-converts the returned offset
574
+ * bits into a bogus float instead of reboxing them into a NaN-boxed pointer — a
575
+ * silent value corruption, not a compile error, that only a receiver expecting the
576
+ * real pointer (e.g. Map.prototype.size's raw __len dispatch) turns into a wild
577
+ * offset read.
578
+ *
579
+ * Every path must also yield a real value, not fall through / bare-`return` into
580
+ * `undefined` (an f64 atom) — a match here forces this function's signature to
581
+ * unconditional i32, so a value-less path would be a genuine wasm type error
582
+ * (validated on encode, not just a mistracked type). Same guarantee
583
+ * narrowPointerResults' func.valResult-driven arm takes via alwaysReturns, and
584
+ * narrowValResults skips via hasBareReturn, for the identical reason: a recursive
585
+ * walker whose tail happens to read `return helper(...)` (a value only its OWN
586
+ * recursive call consumes) commonly has OTHER arms that are bare `return;`
587
+ * early-exits — real shape, not hypothetical (plan/literals.js's
588
+ * _disqualifyPromotion: single value-bearing return via self-recursion, but
589
+ * multiple bare `return`s alongside it that returnExprs below never sees).
590
+ *
591
+ * Returns that param, else null. */
502
592
  function passthroughPtrParam(func) {
503
- const exprs = returnExprs(func.body)
593
+ const body = func.body
594
+ if (isBlockBody(body) && (!alwaysReturns(body) || hasBareReturn(body))) return null
595
+ const exprs = returnExprs(body)
504
596
  if (!exprs.length) return null
505
- const name = exprs[0]
506
- if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
507
- return func.sig.params.find(p => p.name === name && p.ptrKind) || null
597
+ return func.sig.params.find((p, idx) =>
598
+ p.ptrKind && exprs.every(e => passesParamThrough(e, p.name, idx, func.name))) || null
508
599
  }
509
600
 
510
601
  function narrowPointerResults(funcs, paramReps) {
@@ -523,9 +614,12 @@ function narrowPointerResults(funcs, paramReps) {
523
614
  if (func.sig.ptrKind == null) {
524
615
  const pp = passthroughPtrParam(func)
525
616
  if (pp) {
617
+ // OBJECT reboxes by schema-id; TYPED by the param's ELEM-TYPE bits
618
+ // (pp.ptrAux) — without them the caller reboxes with aux 0 (Int8
619
+ // dispatch) and every read of the returned array mis-strides to 0.
526
620
  const aux = pp.ptrKind === VAL.OBJECT
527
621
  ? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
528
- : null
622
+ : pp.ptrAux ?? null
529
623
  // OBJECT needs a known schema-id to rebox; a polymorphic pass-through
530
624
  // (conflicting schemas → null) keeps its current handling.
531
625
  if (pp.ptrKind !== VAL.OBJECT || aux != null) {
@@ -725,6 +819,7 @@ export default function narrowSignatures(programFacts, ast) {
725
819
  callee, callerFunc, argList, func, restIdx,
726
820
  callerLocals: ctxEntry.callerLocals,
727
821
  callerValTypes: ctxEntry.callerValTypes,
822
+ callerTypedElems: ctxEntry.callerTypedElems,
728
823
  callerParamFacts(key) {
729
824
  if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
730
825
  return paramFacts.get(key)
@@ -763,7 +858,17 @@ export default function narrowSignatures(programFacts, ast) {
763
858
  const inferValAtSite = (arg, state) => {
764
859
  const v = inferValType(arg, state.callerValTypes)
765
860
  if (v != null) return v
766
- if (typeof arg !== 'string') return null
861
+ if (typeof arg !== 'string') {
862
+ // Typed-array element read `recv[i]` where the receiver is a TYPED param/local of the CALLER:
863
+ // valTypeOf can't see this (it queries ctx.func, not the caller), so `f(src[i])` with a
864
+ // Float64Array PARAM `src` never propagated Number to f's param. Mirror VT['[]'] exactly,
865
+ // but resolve the receiver through the caller's own context — sound: only fires when the
866
+ // receiver is provably VAL.TYPED, and the ctor decides Number vs BigInt (BigInt64Array).
867
+ if (Array.isArray(arg) && arg[0] === '[]' && arg.length === 3 && typeof arg[1] === 'string' &&
868
+ (state.callerValTypes?.get(arg[1]) || ctx.scope.globalValTypes?.get(arg[1])) === VAL.TYPED)
869
+ return typedCtorElemValType(state.callerTypedElems?.get(arg[1])) || VAL.NUMBER
870
+ return null
871
+ }
767
872
  const fromParam = state.callerParamFacts('val')?.get(arg)
768
873
  if (fromParam != null) return fromParam
769
874
  const def = state.callerFunc?.defaults?.[arg]
@@ -847,15 +952,32 @@ export default function narrowSignatures(programFacts, ast) {
847
952
  if (state.callee === state.callerFunc?.name &&
848
953
  isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
849
954
  if (!state._teOverlay) {
850
- const m = new Map(ctx.scope.globalTypedElem || [])
955
+ // Caller's typed arrays (locals + non-shadowed globals, precomputed shadow-aware
956
+ // in buildCallerCtx) so a LOCAL `const buf = new Int32Array(…)` makes `buf[i]`
957
+ // type i32 here — not just module globals / typedCtor-narrowed params.
958
+ const m = new Map(state.callerTypedElems || ctx.scope.globalTypedElem || [])
851
959
  const pf = state.callerParamFacts('typedCtor')
852
960
  if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
853
961
  state._teOverlay = m
854
962
  }
855
963
  const prev = ctx.func.localTypedElemsOverlay
856
964
  ctx.func.localTypedElemsOverlay = state._teOverlay
857
- try { return exprType(arg, state.callerLocals) }
965
+ let wt
966
+ try { wt = exprType(arg, state.callerLocals) }
858
967
  finally { ctx.func.localTypedElemsOverlay = prev }
968
+ // An i32-typed BARE NAME that carries a POINTER kind in the caller (a local
969
+ // or param already narrowed to an unboxed i32 offset) is NOT integer
970
+ // evidence: narrowing the callee's param to plain i32 on it makes every
971
+ // callee read widen the raw offset NUMERICALLY (f64.convert_i32_s) — the
972
+ // pointer arrives as a small number and every prop probe silently misses
973
+ // (this ate `Promise.any(obj)`'s GetIterator through __p_any → __p_list).
974
+ // Report the boxed f64 lane instead; only applyPointerParamAbi — which
975
+ // stamps ptrKind/ptrAux so reads REBOX — may unbox pointer params.
976
+ if (wt === 'i32' && typeof arg === 'string') {
977
+ const vk = state.callerValTypes?.get?.(arg)
978
+ if (vk != null && vk !== VAL.NUMBER && vk !== VAL.BOOL) return 'f64'
979
+ }
980
+ return wt
859
981
  }
860
982
  const runFixpoint = () => runCallsiteLattice([
861
983
  // val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
@@ -890,8 +1012,8 @@ export default function narrowSignatures(programFacts, ast) {
890
1012
  // Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
891
1013
  // as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
892
1014
  // poisons params whose call sites still can't be proven (genuinely-untyped args).
893
- const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
894
- const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
1015
+ const runArrElemFixpoint = (field, inferFn, elemsCtxMap, sidsCtxMap) => {
1016
+ const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field), sidsCtxMap?.get(state.callerFunc))
895
1017
  let changed
896
1018
  const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
897
1019
  const soft = {
@@ -913,6 +1035,23 @@ export default function narrowSignatures(programFacts, ast) {
913
1035
  // narrowing stays below — it benefits from i32 params being narrowed first.
914
1036
  const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
915
1037
  narrowValResults(funcsWithNarrowableResult)
1038
+ // Own-default typed annotation: `(arr = new Int32Array(0)) => …` self-declares
1039
+ // the param's element ctor — the ONLY evidence a host-called export can carry
1040
+ // (no call sites to lattice over; Workers v1 SPMD kernels are exactly this
1041
+ // shape). A WEAK seed: set only at BOTTOM, so call-site facts merge/poison
1042
+ // over it as usual. Runtime safety for Atomics receivers is the tag+elem
1043
+ // guard in __atomics_addr (module/atomics.js) — a wrong host arg throws.
1044
+ for (const func of ctx.func.list) {
1045
+ if (!func.sig?.params || !func.defaults) continue
1046
+ for (let k = 0; k < func.sig.params.length; k++) {
1047
+ const d = func.defaults[func.sig.params[k].name]
1048
+ if (Array.isArray(d) && d[0] === '()' && typeof d[1] === 'string' &&
1049
+ d[1].startsWith('new.') && d[1].endsWith('Array')) {
1050
+ const r = ensureParamRep(paramReps, func.name, k)
1051
+ if (r.typedCtor === undefined) r.typedCtor = d[1]
1052
+ }
1053
+ }
1054
+ }
916
1055
  runFixpoint()
917
1056
  runFixpoint()
918
1057
 
@@ -996,11 +1135,65 @@ export default function narrowSignatures(programFacts, ast) {
996
1135
  // (before E3 ran) are stale (mkInput's ptrKind was unset then).
997
1136
  phase.invalidateBodyFacts()
998
1137
  const callerTypedCtx = phase.callerTyped()
1138
+ // Per-caller receiver schemas for field-provenance args (`transform(plan.tw…)`):
1139
+ // a small decl scan per body — collision-free and live-rep-independent (the
1140
+ // rep/schema.vars chains aren't trustworthy mid-lattice). inferSchemaId covers
1141
+ // literals and calls (valResult/ptrAux — E-complete by this phase); chained
1142
+ // decls resolve through the accumulating map. Module-const sids (`const P =
1143
+ // mk(n)` at top level) seed every caller's map; a local decl SHADOWS the seed
1144
+ // (unresolvable → masks it), and any reassignment drops the name (the second
1145
+ // write could carry a different schema).
1146
+ const moduleSids = new Map()
1147
+ {
1148
+ const visitDecl = (node) => {
1149
+ if (!Array.isArray(node)) return
1150
+ if (node[0] === 'let' || node[0] === 'const') {
1151
+ for (const d of node.slice(1)) {
1152
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
1153
+ if (!ctx.scope.consts?.has(d[1])) continue
1154
+ const sid = inferSchemaId(d[2], moduleSids)
1155
+ if (sid != null && !moduleSids.has(d[1])) moduleSids.set(d[1], sid)
1156
+ }
1157
+ return
1158
+ }
1159
+ if (node[0] === ';' || node[0] === 'export') for (let i = 1; i < node.length; i++) visitDecl(node[i])
1160
+ }
1161
+ if (Array.isArray(ast)) {
1162
+ if (ast[0] === ';') for (let i = 1; i < ast.length; i++) visitDecl(ast[i])
1163
+ else visitDecl(ast)
1164
+ }
1165
+ }
1166
+ const callerSidsCtx = new Map()
1167
+ for (const func of ctx.func.list) {
1168
+ if (!func.body || func.raw) continue
1169
+ const sids = new Map(moduleSids), poisoned = new Set()
1170
+ const scan = (n) => {
1171
+ if (!Array.isArray(n)) return
1172
+ if (n[0] === '=>') return
1173
+ if (n[0] === 'let' || n[0] === 'const') {
1174
+ for (const d of n.slice(1)) {
1175
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
1176
+ const sid = inferSchemaId(d[2], sids)
1177
+ sids.delete(d[1]) // a local decl shadows any module seed
1178
+ if (sid != null && !poisoned.has(d[1])) sids.set(d[1], sid)
1179
+ scan(d[2])
1180
+ } else scan(d)
1181
+ }
1182
+ return
1183
+ }
1184
+ if ((ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') && typeof n[1] === 'string') { poisoned.add(n[1]); sids.delete(n[1]) }
1185
+ for (let i = 1; i < n.length; i++) scan(n[i])
1186
+ }
1187
+ // params shadow module seeds too — an arg-bound `P` is not the module const
1188
+ for (const p of func.sig?.params || []) sids.delete(p.name)
1189
+ scan(func.body)
1190
+ callerSidsCtx.set(func, sids)
1191
+ }
999
1192
  // Two-pass fixpoint: lets a caller's params, once typed, propagate further to
1000
1193
  // its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
1001
1194
  // for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
1002
1195
  // (same shape — field/inferFn/elemsCtxMap parameterization).
1003
- const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx)
1196
+ const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx, callerSidsCtx)
1004
1197
  runTypedFixpoint()
1005
1198
  runTypedFixpoint()
1006
1199
 
@@ -1033,6 +1226,15 @@ export default function narrowSignatures(programFacts, ast) {
1033
1226
  // (callback bench: mix is FNV — params and result all i32-shaped, but inferred
1034
1227
  // only after E phase narrowed mix's result).
1035
1228
  phase.refreshLocals()
1229
+ // I1: Re-run POINTER results now that Phase G has tagged typed params — a
1230
+ // pass-through like `norm = (w) => { …w[i]…; return w }` gains w.ptrKind only
1231
+ // in G, AFTER the first narrowPointerResults sweep, so its sig.ptrKind stayed
1232
+ // null and the I2 numeric-results pass below then STOLE the return as plain
1233
+ // i32 — the caller f64.convert_i32_s'd the returned offset into a bogus float
1234
+ // (the cross-module memo miscompile: `g._w = norm(w)` stored a number, the
1235
+ // slot read dispatched on it as a pointer → undefined). The rerun stamps
1236
+ // sig.ptrKind first; I2's f64-results guard then skips these functions.
1237
+ narrowPointerResults(funcsWithNarrowableResult, paramReps)
1036
1238
  // I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
1037
1239
  // typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
1038
1240
  // typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
@@ -1055,6 +1257,63 @@ export default function narrowSignatures(programFacts, ast) {
1055
1257
  // r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
1056
1258
  // ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
1057
1259
  runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
1260
+ // BIGINT params: re-derive nullability the val claim just erased. VT['?:']
1261
+ // carries BIGINT through a nullish-literal arm (the kind is untaggable at
1262
+ // runtime — kind.js), so a site arg like `c ? BigInt(x) : null` PROVES
1263
+ // BIGINT while still passing null on the other path; the settle above
1264
+ // stamps bare val=BIGINT and strictSentinel would then FOLD the callee's
1265
+ // `x == null` guard (watr's `_i64Arith(r)`: fold-miss null crosses for
1266
+ // real — the guard must return null, not format atom bits as hex). Any
1267
+ // site arg not structurally non-nullish marks the param nullable, which
1268
+ // only suppresses that fold (emitFunc's caller-side nullability block).
1269
+ // Scoped to BIGINT vals: tagged kinds keep their runtime forks and their
1270
+ // folds. Name args resolve through the CALLER body's own writes — at plan
1271
+ // time no caller ctx.func is installed, so rep lookups would misread;
1272
+ // an unwritten name (param/global/closure) fails closed.
1273
+ const bodyNameNullable = (callerFunc) => {
1274
+ const seen = new Set()
1275
+ const nameNullable = (name) => {
1276
+ if (seen.has(name)) return false // cyclic alias: no new evidence
1277
+ seen.add(name)
1278
+ let found = false, nullish = false
1279
+ const walk = (node) => {
1280
+ if (nullish || !Array.isArray(node)) return
1281
+ const op = node[0]
1282
+ if ((op === 'let' || op === 'const') && node.length >= 2) {
1283
+ for (let i = 1; i < node.length; i++) {
1284
+ const d = node[i]
1285
+ if (Array.isArray(d) && d[0] === '=' && d[1] === name) {
1286
+ found = true
1287
+ if (mayBeNullish(d[2], nameNullable)) nullish = true
1288
+ }
1289
+ }
1290
+ } else if (op === '=' && node[1] === name) {
1291
+ found = true
1292
+ if (mayBeNullish(node[2], nameNullable)) nullish = true
1293
+ } else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
1294
+ ASSIGN_OPS.has(op) && node[1] === name) {
1295
+ found = true
1296
+ nullish = true // ??=/||= etc. — fail closed
1297
+ }
1298
+ for (let i = 1; i < node.length; i++) walk(node[i])
1299
+ }
1300
+ walk(callerFunc?.body)
1301
+ return found ? nullish : true // unwritten name — fail closed
1302
+ }
1303
+ return nameNullable
1304
+ }
1305
+ for (const [fname, reps] of paramReps) {
1306
+ for (const [k, r] of reps) {
1307
+ if (r.val !== VAL.BIGINT || r.nullable) continue
1308
+ for (const cs of callSites) {
1309
+ if (cs.callee !== fname || k >= cs.argList.length) continue
1310
+ if (mayBeNullish(cs.argList[k], bodyNameNullable(cs.callerFunc))) {
1311
+ r.nullable = true
1312
+ break
1313
+ }
1314
+ }
1315
+ }
1316
+ }
1058
1317
  // Don't steal typed-array params from specializeBimorphicTyped: F phase parks
1059
1318
  // bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
1060
1319
  // ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
@@ -1067,6 +1326,22 @@ export default function narrowSignatures(programFacts, ast) {
1067
1326
  // slot from f64 (nanbox SSO carrier) to externref so the JS host passes the
1068
1327
  // native string directly. Zero copy, zero transcoding. See applyJsstringBoundaryCarrier.
1069
1328
  if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
1329
+
1330
+ // Stamp the settled per-param val kind onto sig.params (mirror of emitFunc's
1331
+ // updateRep(pname, { val: r.val }) merge — same source, same condition). The
1332
+ // call-site emitter needs it to pick the arg carrier: a BOOL arg into an
1333
+ // UNTYPED f64 param boxes to its TRUE/FALSE atom (boolean identity crosses
1334
+ // the boundary), while a val-known param keeps the raw 0/1 ABI its body
1335
+ // assumes. Read by coerceArg (emit.js).
1336
+ for (const func of ctx.func.list) {
1337
+ if (!func.sig || func.raw) continue
1338
+ const reps = paramReps.get(func.name)
1339
+ if (!reps) continue
1340
+ for (const [k, r] of reps) {
1341
+ const p = func.sig.params[k]
1342
+ if (p && r.val != null && p.val == null) p.val = r.val
1343
+ }
1344
+ }
1070
1345
  }
1071
1346
 
1072
1347
  /** Gate the jsstring carrier on the host. ON by default for the JS host: a
@@ -1520,6 +1795,268 @@ export function specializeBimorphicTyped(programFacts) {
1520
1795
  }
1521
1796
  }
1522
1797
 
1798
+ /**
1799
+ * Speculative typed-param specialization — the GUARDED sibling of
1800
+ * specializeBimorphicTyped, for params whose args can never be statically
1801
+ * PROVEN typed: plan objects through Map caches, nullable module-let memos,
1802
+ * returned-object fields — the fftplan/provenance shape-class, where the
1803
+ * receiver's schema is unknowable but every value that ever reaches the call
1804
+ * is, in practice, the same typed array.
1805
+ *
1806
+ * When every static call site's arg at a position carries the SAME ctor as
1807
+ * evidence — a proven inferTypedCtor, or the program-wide write-gated slot
1808
+ * census for a bare field read (ctx.schema.slotTypedCtorByProp, the
1809
+ * guardedSlotOf contract) — clone the callee with those params typed
1810
+ * (identical machinery to the bimorphic clones) and record it in
1811
+ * ctx.types.specFns. Emit rewrites every static direct call into
1812
+ *
1813
+ * tags-all-match? call $f$spec(raw offsets…) : call $f(boxes…)
1814
+ *
1815
+ * (emitSpeculativeCall) — one masked NaN-box compare per speculated arg per
1816
+ * CALL, amortized over the callee's loops. A nullish / other-kind / view
1817
+ * value simply falls to the original call unchanged, so speculation can only
1818
+ * ever be as fast, never wrong; the original function stays for indirect and
1819
+ * boundary callers. Gate on a loop in the body: the win is per-ELEMENT
1820
+ * access, so guarding loop-free leaf calls only spends the compare.
1821
+ *
1822
+ * Evidence is a recursive WEAK lattice (soundness never depends on it — the
1823
+ * runtime guard does; evidence only decides where speculating is worth it):
1824
+ * - proven inferTypedCtor at the site (strong)
1825
+ * - `x.prop` → program-wide write-gated slot census (slotTypedCtorByProp)
1826
+ * - a name → its single `=` binding's init, chased recursively
1827
+ * - a name that is an ENCLOSING ARROW's param → meet over the arrow's own
1828
+ * call sites at that position (the `edge(wre, wim)` harness shape: the
1829
+ * kernel call sits inside a local arrow whose args carry the evidence)
1830
+ * - `f(...)` → f's return census: every return a `new K`, a local bound to
1831
+ * one, a censused field read, or another censused call; nullish returns
1832
+ * are SKIPPED (they fail the guard at runtime, by design — this is what
1833
+ * lets Map-cache/memo getters like getPlan(n) census through)
1834
+ */
1835
+ export function speculateTypedParams(programFacts, ast) {
1836
+ const { callSites, paramReps } = programFacts
1837
+ if (!ctx.schema.slotTypedCtorByProp) return
1838
+
1839
+ const sitesByCallee = new Map()
1840
+ for (const cs of callSites) {
1841
+ const list = sitesByCallee.get(cs.callee)
1842
+ if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
1843
+ }
1844
+ const callerTypedCtx = buildCallerTypedCtx()
1845
+ const callerTypedParamsCtx = new Map()
1846
+ for (const func of ctx.func.list) {
1847
+ const m = paramFactsOf(paramReps, func, 'typedCtor') || null
1848
+ let acc = m
1849
+ if (func.sig?.params) for (const p of func.sig.params) {
1850
+ if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
1851
+ acc ||= new Map()
1852
+ if (!acc.has(p.name)) acc.set(p.name, ctorFromElemAux(p.ptrAux))
1853
+ }
1854
+ }
1855
+ if (acc) callerTypedParamsCtx.set(func, acc)
1856
+ }
1857
+
1858
+ const hasLoop = (n) => Array.isArray(n)
1859
+ && (n[0] === 'for' || n[0] === 'while' || n[0] === 'do' || n.some((c, i) => i > 0 && hasLoop(c)))
1860
+
1861
+ // ---- weak evidence engine (see doc above) ----
1862
+ const DBG2 = typeof process !== 'undefined' && !!process.env?.JZ_DBG_SPEC
1863
+ const isNullish = (r) => r == null || r === 'null' || r === 'undefined'
1864
+ || (Array.isArray(r) && (r[0] === 'null' || r[0] === 'undefined'))
1865
+ const retMemo = new Map()
1866
+ const MAX_DEPTH = 6
1867
+ const bodyOf = (callerFunc) => callerFunc ? callerFunc.body : ast
1868
+
1869
+ // Return census of a named function: the single ctor every non-nullish
1870
+ // return resolves to, or null.
1871
+ function retCensus(fname, depth) {
1872
+ if (retMemo.has(fname)) return retMemo.get(fname)
1873
+ retMemo.set(fname, null) // cycle guard
1874
+ const func = ctx.func.map.get(fname)
1875
+ if (!func?.body || func.raw || depth > MAX_DEPTH) return null
1876
+ const te = analyzeBody(func.body).typedElems
1877
+ let ctor = null
1878
+ for (const r of returnExprs(func.body)) {
1879
+ if (isNullish(r)) continue
1880
+ const c = typedElemCtor(r)
1881
+ || (typeof r === 'string' ? te?.get(r) : null)
1882
+ || (Array.isArray(r) && r[0] === '.' && typeof r[2] === 'string' ? ctx.schema.slotTypedCtorByProp(r[2]) : null)
1883
+ || (Array.isArray(r) && r[0] === '()' && typeof r[1] === 'string' ? retCensus(r[1], depth + 1) : null)
1884
+ if (!c || (ctor && c !== ctor)) return null
1885
+ ctor = c
1886
+ }
1887
+ retMemo.set(fname, ctor)
1888
+ return ctor
1889
+ }
1890
+
1891
+ // Chain of arrow nodes enclosing `target` inside `root` (outermost first).
1892
+ function arrowPathTo(root, target) {
1893
+ const path = []
1894
+ const walk = (n) => {
1895
+ if (!Array.isArray(n)) return false
1896
+ if (n === target) return true
1897
+ const isArrow = n[0] === '=>'
1898
+ if (isArrow) path.push(n)
1899
+ for (let i = 1; i < n.length; i++) if (walk(n[i])) return true
1900
+ if (isArrow) path.pop()
1901
+ return false
1902
+ }
1903
+ return walk(root) ? path : null
1904
+ }
1905
+
1906
+ function evidenceOfArg(arg, callerFunc, siteNode, depth, seen) {
1907
+ const r = evidenceOfArgInner(arg, callerFunc, siteNode, depth, seen)
1908
+ if (DBG2) console.error('[evid=]', JSON.stringify(arg)?.slice(0, 50), '→', r)
1909
+ return r
1910
+ }
1911
+ function evidenceOfArgInner(arg, callerFunc, siteNode, depth, seen) {
1912
+ if (arg == null || depth > MAX_DEPTH) return null
1913
+ const proven = inferTypedCtor(arg, callerTypedCtx.get(callerFunc), callerTypedParamsCtx.get(callerFunc))
1914
+ if (proven) return proven
1915
+ if (Array.isArray(arg)) {
1916
+ if (arg[0] === '.' && typeof arg[2] === 'string') return ctx.schema.slotTypedCtorByProp(arg[2])
1917
+ if (arg[0] === '()' && typeof arg[1] === 'string' && ctx.func.map.has(arg[1])) return retCensus(arg[1], depth)
1918
+ return typedElemCtor(arg)
1919
+ }
1920
+ if (typeof arg !== 'string') return null
1921
+ // Cycle guard is PATH-local: the key backtracks on exit (a name legitimately
1922
+ // recurs across sibling meet branches — e.g. duplicated call sites after
1923
+ // lambda inlining — and must resolve fresh in each).
1924
+ const key = (callerFunc?.name || '') + '|' + arg
1925
+ if (seen.has(key)) return null
1926
+ seen.add(key)
1927
+ try {
1928
+ return resolveName()
1929
+ } finally { seen.delete(key) }
1930
+
1931
+ function resolveName() {
1932
+ const body = bodyOf(callerFunc)
1933
+ if (!body) return null
1934
+ // Enclosing-arrow param? Meet the arrow's own call sites at that position.
1935
+ const arrows = siteNode ? arrowPathTo(body, siteNode) : null
1936
+ if (arrows) for (let a = arrows.length - 1; a >= 0; a--) {
1937
+ const names = extractParams(arrows[a][1]).map(p => classifyParam(p)).filter(c => c.kind === 'plain').map(c => c.name)
1938
+ const j = names.indexOf(arg)
1939
+ if (j < 0) continue
1940
+ // The binding name of this arrow (`const edge = (…) => …`), then its calls.
1941
+ let bindName = null
1942
+ const findBind = (n) => {
1943
+ if (!Array.isArray(n)) return
1944
+ if (n[0] === '=' && typeof n[1] === 'string' && n[2] === arrows[a]) bindName = n[1]
1945
+ for (let i = 1; i < n.length && !bindName; i++) findBind(n[i])
1946
+ }
1947
+ findBind(body)
1948
+ if (!bindName) return null
1949
+ const calls = []
1950
+ const findCalls = (n) => {
1951
+ if (!Array.isArray(n)) return
1952
+ if (n[0] === '()' && n[1] === bindName) calls.push(n)
1953
+ for (let i = 1; i < n.length; i++) findCalls(n[i])
1954
+ }
1955
+ findCalls(body)
1956
+ if (!calls.length) return null
1957
+ let ctor = null
1958
+ for (const cn of calls) {
1959
+ const list = cn[2] == null ? [] : (Array.isArray(cn[2]) && cn[2][0] === ',') ? cn[2].slice(1) : [cn[2]]
1960
+ const c = evidenceOfArg(list[j], callerFunc, cn, depth + 1, seen)
1961
+ if (!c || (ctor && c !== ctor)) return null
1962
+ ctor = c
1963
+ }
1964
+ return ctor
1965
+ }
1966
+ // Single-`=` local binding: chase its init. More than one write → too
1967
+ // murky to be worth the guard.
1968
+ let decl = null, writes = 0
1969
+ const findDecl = (n) => {
1970
+ if (!Array.isArray(n)) return
1971
+ if (typeof n[1] === 'string' && n[1] === arg) {
1972
+ if (n[0] === '=') { decl = n; writes++ }
1973
+ else if (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') writes++
1974
+ }
1975
+ for (let i = 1; i < n.length; i++) findDecl(n[i])
1976
+ }
1977
+ findDecl(body)
1978
+ if (!decl || writes !== 1) return null
1979
+ return evidenceOfArg(decl[2], callerFunc, decl, depth + 1, seen)
1980
+ }
1981
+ }
1982
+
1983
+ const DBG = typeof process !== 'undefined' && !!process.env?.JZ_DBG_SPEC
1984
+ const originals = ctx.func.list.slice()
1985
+ for (const func of originals) {
1986
+ if (func.raw || func.rest || !func.body) continue
1987
+ if (func.sig?.results?.length !== 1) continue
1988
+ const sites = sitesByCallee.get(func.name)
1989
+ if (!sites?.length) { if (DBG) console.error('[spec]', func.name, 'no sites'); continue }
1990
+ if (!hasLoop(func.body)) continue
1991
+
1992
+ // Candidate positions: still a dyn f64 box after F-phase/bimorphic (no
1993
+ // pointer ABI, not numeric-narrowed), no default (undefined must reach it).
1994
+ const reps = paramReps.get(func.name)
1995
+ const candidates = []
1996
+ for (let k = 0; k < func.sig.params.length; k++) {
1997
+ const p = func.sig.params[k]
1998
+ if (p.type !== 'f64' || p.ptrKind != null) continue
1999
+ if (func.defaults?.[p.name] != null) continue
2000
+ const r = reps?.get(k)
2001
+ if (r && (r.val != null && r.val !== VAL.TYPED)) continue
2002
+ candidates.push(k)
2003
+ }
2004
+ if (!candidates.length) continue
2005
+
2006
+ // Evidence meet across sites per position. A site WITHOUT evidence is
2007
+ // NEUTRAL — its calls just miss the guard at runtime and take the
2008
+ // original (a plain-array or debug caller must not veto the hot sites'
2009
+ // win). Only CONFLICTING evidence (two different ctors) kills the
2010
+ // position — a guard can only test one kind.
2011
+ const specs = []
2012
+ for (const k of candidates) {
2013
+ let ctor = null, dead = false
2014
+ for (const site of sites) {
2015
+ const arg = site.argList[k]
2016
+ if (arg == null) continue
2017
+ const proven = inferTypedCtor(arg, callerTypedCtx.get(site.callerFunc), callerTypedParamsCtx.get(site.callerFunc))
2018
+ const c = proven ?? evidenceOfArg(arg, site.callerFunc, site.node, 0, new Set())
2019
+ if (DBG) console.error('[spec]', func.name, 'k=' + k, JSON.stringify(arg)?.slice(0, 60), 'proven=' + proven, 'c=' + c)
2020
+ if (c == null) continue
2021
+ if (ctor && c !== ctor) { dead = true; break }
2022
+ ctor = c
2023
+ }
2024
+ if (dead || !ctor) continue
2025
+ const aux = typedElemAux(ctor)
2026
+ if (aux == null) continue
2027
+ specs.push({ k, ctor, aux })
2028
+ }
2029
+ if (!specs.length) continue
2030
+
2031
+ let cloneName = `${func.name}$spec`
2032
+ while (ctx.func.names.has(cloneName)) cloneName += '$'
2033
+ const specAt = new Map(specs.map(s => [s.k, s]))
2034
+ const cloneSig = {
2035
+ params: func.sig.params.map((p, idx) => {
2036
+ const s = specAt.get(idx)
2037
+ return s ? { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: s.aux } : { ...p }
2038
+ }),
2039
+ results: [...func.sig.results],
2040
+ }
2041
+ const clone = { ...func, name: cloneName, sig: cloneSig, exported: false }
2042
+ ctx.func.list.push(clone)
2043
+ ctx.func.map.set(cloneName, clone)
2044
+ ctx.func.names.add(cloneName)
2045
+
2046
+ const cloneReps = new Map()
2047
+ if (reps) for (const [k, r] of reps) cloneReps.set(k, { ...r })
2048
+ for (const s of specs) {
2049
+ const r = cloneReps.get(s.k) || {}
2050
+ r.typedCtor = s.ctor
2051
+ r.val = VAL.TYPED
2052
+ cloneReps.set(s.k, r)
2053
+ }
2054
+ paramReps.set(cloneName, cloneReps)
2055
+
2056
+ ;(ctx.types.specFns ||= new Map()).set(func.name, { clone: cloneName, guards: specs.map(s => ({ k: s.k, aux: s.aux })) })
2057
+ }
2058
+ }
2059
+
1523
2060
  /**
1524
2061
  * Phase: refine ctx.types.anyDynKey using post-narrowSignatures type info.
1525
2062
  */