jz 0.8.1 → 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 (72) 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 -6271
  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 +299 -44
  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 +892 -32
  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 +301 -84
  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 +71 -5
  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 +979 -54
  45. package/src/compile/index.js +271 -29
  46. package/src/compile/infer.js +34 -3
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/narrow.js +543 -15
  49. package/src/compile/peel-stencil.js +5 -0
  50. package/src/compile/plan/index.js +45 -3
  51. package/src/compile/plan/inline.js +8 -1
  52. package/src/compile/plan/literals.js +39 -0
  53. package/src/compile/plan/scope.js +430 -23
  54. package/src/compile/program-facts.js +732 -38
  55. package/src/ctx.js +64 -9
  56. package/src/helper-counters.js +7 -1
  57. package/src/ir.js +113 -5
  58. package/src/kind-traits.js +66 -3
  59. package/src/kind.js +84 -12
  60. package/src/op-policy.js +7 -4
  61. package/src/optimize/index.js +1060 -750
  62. package/src/optimize/recurse.js +2 -2
  63. package/src/optimize/vectorize.js +972 -67
  64. package/src/prepare/index.js +792 -63
  65. package/src/prepare/math-kernel.js +331 -0
  66. package/src/prepare/pre-eval.js +714 -0
  67. package/src/snapshot.js +194 -0
  68. package/src/type.js +1170 -56
  69. package/src/wat/assemble.js +403 -65
  70. package/src/wat/codegen.js +74 -14
  71. package/transform.js +113 -4
  72. 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,
@@ -93,13 +94,23 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
93
94
  const reps = paramReps.get(func.name)
94
95
  if (!reps) continue
95
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
96
102
  for (const [k, r] of reps) {
97
103
  if (k === restIdx || k >= func.sig.params.length) continue
98
104
  const p = func.sig.params[k]
99
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
100
112
  // SIMD: a param passed a v128 (lane vector) at every call site is a v128 param.
101
113
  if (r.wasm === 'v128') { p.type = 'v128'; continue }
102
- if (r.wasm !== 'i32' || p.type === 'i32') continue
103
114
  if (skipTyped && r.val === VAL.TYPED) continue
104
115
  p.type = 'i32'
105
116
  }
@@ -134,7 +145,14 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
134
145
  const reps = paramReps.get(func.name)
135
146
  if (!reps) continue
136
147
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
137
- 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) {
138
156
  // Re-fold call sites HARD (the shared val lattice is soft, so r.val may be a
139
157
  // partial consensus from typed sites alone) — only specialize when every site
140
158
  // proves the same pointer kind.
@@ -145,6 +163,31 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
145
163
  const p = func.sig.params[k]
146
164
  if (p.type === 'i32') continue
147
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
+ }
148
191
  p.type = 'i32'
149
192
  p.ptrKind = hv
150
193
  }
@@ -202,6 +245,7 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
202
245
  const reps = paramReps.get(func.name)
203
246
  if (!reps) continue
204
247
  const restIdx = func.rest ? func.sig.params.length - 1 : -1
248
+ let mutated = null // body-write guard — same contract as applyPointerParamAbi
205
249
  for (const [k, r] of reps) {
206
250
  const ctor = r.typedCtor
207
251
  if (ctor == null) continue
@@ -210,6 +254,11 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
210
254
  const p = func.sig.params[k]
211
255
  if (p.type === 'i32') continue
212
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
213
262
  const aux = typedElemAux(ctor)
214
263
  if (aux == null) continue
215
264
  p.type = 'i32'
@@ -502,14 +551,51 @@ function typedAuxOfReturn(expr, localElemMap) {
502
551
  * Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
503
552
  * outer's call to inner contributes a known schema-id.
504
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
+
505
566
  /** A function whose every return is the same parameter that was pointer-ABI
506
- * 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. */
507
592
  function passthroughPtrParam(func) {
508
- 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)
509
596
  if (!exprs.length) return null
510
- const name = exprs[0]
511
- if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
512
- 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
513
599
  }
514
600
 
515
601
  function narrowPointerResults(funcs, paramReps) {
@@ -528,9 +614,12 @@ function narrowPointerResults(funcs, paramReps) {
528
614
  if (func.sig.ptrKind == null) {
529
615
  const pp = passthroughPtrParam(func)
530
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.
531
620
  const aux = pp.ptrKind === VAL.OBJECT
532
621
  ? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
533
- : null
622
+ : pp.ptrAux ?? null
534
623
  // OBJECT needs a known schema-id to rebox; a polymorphic pass-through
535
624
  // (conflicting schemas → null) keeps its current handling.
536
625
  if (pp.ptrKind !== VAL.OBJECT || aux != null) {
@@ -769,7 +858,17 @@ export default function narrowSignatures(programFacts, ast) {
769
858
  const inferValAtSite = (arg, state) => {
770
859
  const v = inferValType(arg, state.callerValTypes)
771
860
  if (v != null) return v
772
- 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
+ }
773
872
  const fromParam = state.callerParamFacts('val')?.get(arg)
774
873
  if (fromParam != null) return fromParam
775
874
  const def = state.callerFunc?.defaults?.[arg]
@@ -863,8 +962,22 @@ export default function narrowSignatures(programFacts, ast) {
863
962
  }
864
963
  const prev = ctx.func.localTypedElemsOverlay
865
964
  ctx.func.localTypedElemsOverlay = state._teOverlay
866
- try { return exprType(arg, state.callerLocals) }
965
+ let wt
966
+ try { wt = exprType(arg, state.callerLocals) }
867
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
868
981
  }
869
982
  const runFixpoint = () => runCallsiteLattice([
870
983
  // val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
@@ -899,8 +1012,8 @@ export default function narrowSignatures(programFacts, ast) {
899
1012
  // Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
900
1013
  // as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
901
1014
  // poisons params whose call sites still can't be proven (genuinely-untyped args).
902
- const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
903
- 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))
904
1017
  let changed
905
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 }
906
1019
  const soft = {
@@ -922,6 +1035,23 @@ export default function narrowSignatures(programFacts, ast) {
922
1035
  // narrowing stays below — it benefits from i32 params being narrowed first.
923
1036
  const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
924
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
+ }
925
1055
  runFixpoint()
926
1056
  runFixpoint()
927
1057
 
@@ -1005,11 +1135,65 @@ export default function narrowSignatures(programFacts, ast) {
1005
1135
  // (before E3 ran) are stale (mkInput's ptrKind was unset then).
1006
1136
  phase.invalidateBodyFacts()
1007
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
+ }
1008
1192
  // Two-pass fixpoint: lets a caller's params, once typed, propagate further to
1009
1193
  // its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
1010
1194
  // for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
1011
1195
  // (same shape — field/inferFn/elemsCtxMap parameterization).
1012
- const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx)
1196
+ const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx, callerSidsCtx)
1013
1197
  runTypedFixpoint()
1014
1198
  runTypedFixpoint()
1015
1199
 
@@ -1042,6 +1226,15 @@ export default function narrowSignatures(programFacts, ast) {
1042
1226
  // (callback bench: mix is FNV — params and result all i32-shaped, but inferred
1043
1227
  // only after E phase narrowed mix's result).
1044
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)
1045
1238
  // I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
1046
1239
  // typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
1047
1240
  // typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
@@ -1064,6 +1257,63 @@ export default function narrowSignatures(programFacts, ast) {
1064
1257
  // r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
1065
1258
  // ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
1066
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
+ }
1067
1317
  // Don't steal typed-array params from specializeBimorphicTyped: F phase parks
1068
1318
  // bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
1069
1319
  // ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
@@ -1076,6 +1326,22 @@ export default function narrowSignatures(programFacts, ast) {
1076
1326
  // slot from f64 (nanbox SSO carrier) to externref so the JS host passes the
1077
1327
  // native string directly. Zero copy, zero transcoding. See applyJsstringBoundaryCarrier.
1078
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
+ }
1079
1345
  }
1080
1346
 
1081
1347
  /** Gate the jsstring carrier on the host. ON by default for the JS host: a
@@ -1529,6 +1795,268 @@ export function specializeBimorphicTyped(programFacts) {
1529
1795
  }
1530
1796
  }
1531
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
+
1532
2060
  /**
1533
2061
  * Phase: refine ctx.types.anyDynKey using post-narrowSignatures type info.
1534
2062
  */