jz 0.6.0 → 0.8.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 (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -7,7 +7,7 @@
7
7
  * function `sig` records change.
8
8
  */
9
9
 
10
- import { ctx, warn } from '../ctx.js'
10
+ import { ctx, warn, err } from '../ctx.js'
11
11
  import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
12
12
  import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
13
13
  import {
@@ -24,10 +24,14 @@ import {
24
24
  } from '../param-reps.js'
25
25
  import {
26
26
  inferArrElemSchema, inferArrElemValType,
27
- inferSchemaId, inferValType, inferTypedCtor,
27
+ inferSchemaId, inferValType, inferTypedCtor, inferParams,
28
28
  } from './infer.js'
29
29
 
30
30
  const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
31
+ // Integer-preserving ops: an expr over integers stays integer (ToInt32-consistent) through these.
32
+ // Excludes /, %, ** (fractional). Used to recognize a recursive arg whose i32-ness follows from
33
+ // its inputs' i32-ness (`f(n - 1)`), so it carries no independent type evidence.
34
+ const RECUR_INT_OPS = new Set(['+', '-', '*', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>', '~'])
31
35
 
32
36
 
33
37
  function filterLiveCallSites(callSites, valueUsed) {
@@ -156,12 +160,33 @@ function refreshCallerValTypes(callerCtx) {
156
160
  }
157
161
  }
158
162
 
163
+ // Per-caller typed-elem context: the caller's body-local typed arrays, layered
164
+ // over the module's typed-array globals so a call like `f(globalArr)` resolves
165
+ // `globalArr`'s ctor (inferTypedCtor reads only this map for a bare-name arg).
166
+ // A global is visible UNLESS the caller shadows the name with a param or local
167
+ // of its own — only then could the name denote a non-typed value. Globals are
168
+ // sound to consult: globalTypedElem holds a name only when EVERY assignment to
169
+ // it is the same single typed-array ctor (scope.js invalidates on any conflict),
170
+ // so it can't denote a different kind at the call site.
171
+ function callerTypedElemsFor(func, globalTE) {
172
+ const facts = analyzeBody(func.body)
173
+ const local = facts.typedElems
174
+ if (!globalTE.size) return local
175
+ const shadowed = new Set(facts.locals.keys())
176
+ for (const p of func.sig?.params || []) shadowed.add(p.name)
177
+ const merged = new Map()
178
+ for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
179
+ for (const [k, v] of local) merged.set(k, v) // local typed binding shadows the global
180
+ return merged
181
+ }
182
+
159
183
  function buildCallerTypedCtx() {
160
184
  const callerTypedCtx = new Map()
161
- callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
185
+ const globalTE = ctx.scope.globalTypedElem || new Map()
186
+ callerTypedCtx.set(null, globalTE)
162
187
  for (const func of ctx.func.list) {
163
188
  if (!func.body || func.raw) continue
164
- callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
189
+ callerTypedCtx.set(func, callerTypedElemsFor(func, globalTE))
165
190
  }
166
191
  return callerTypedCtx
167
192
  }
@@ -203,6 +228,7 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
203
228
  }
204
229
 
205
230
  function refreshCallerLocals(callerCtx) {
231
+ const prevTE = ctx.types.typedElem
206
232
  for (const func of ctx.func.list) {
207
233
  if (!func.body || func.raw) continue
208
234
  // Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
@@ -212,13 +238,24 @@ function refreshCallerLocals(callerCtx) {
212
238
  // (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
213
239
  // emit time, so this transient localReps doesn't leak past narrowing.
214
240
  ctx.func.localReps = new Map()
215
- for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
241
+ // Seed the typedElem overlay with this func's TYPED-pointer params (element ctor from
242
+ // ptrAux), exactly as analyzeFuncForEmit does at emit time. Without it, a local bound to
243
+ // an integer typed-array PARAM element — `aa = perm[perm[X]+Y]` (noise), perm an Int32
244
+ // pointer param — types f64 here, so a callee fed it (`grad(aa,…)`, used only as `aa&3`)
245
+ // never narrows its param to i32. Mirrors emit so narrow-time callerLocals agree with it.
246
+ const te = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : new Map()
247
+ for (const p of func.sig.params) {
248
+ if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
249
+ if (p.ptrKind === VAL.TYPED && p.ptrAux != null) { const c = ctorFromElemAux(p.ptrAux); if (c != null) te.set(p.name, c) }
250
+ }
251
+ ctx.types.typedElem = te
216
252
  invalidateLocalsCache(func.body)
217
253
  const fresh = analyzeBody(func.body).locals
218
254
  for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
219
255
  callerCtx.get(func).callerLocals = fresh
220
256
  }
221
257
  ctx.func.localReps = null
258
+ ctx.types.typedElem = prevTE
222
259
  }
223
260
 
224
261
  function resetParamWasmFacts(paramReps) {
@@ -260,6 +297,35 @@ function narrowI32Results(funcs) {
260
297
  e[0] === '>>>' ||
261
298
  (e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
262
299
  )
300
+ const callsSelf = (n, name) => Array.isArray(n) && ((n[0] === '()' && n[1] === name) || n.some(c => callsSelf(c, name)))
301
+ // Classify a func's return tails as all-v128 / all-i32 (+ sign) under the CURRENT sig.results.
302
+ const evalTails = (func, body, exprs) => {
303
+ const savedCurrent = ctx.func.current
304
+ ctx.func.current = func.sig
305
+ const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
306
+ for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
307
+ // Seed the typedElem overlay with this func's TYPED-pointer params so a return tail
308
+ // reading a typed-array element — `return vals[h]`, vals an Int32Array param (dict's
309
+ // `lookup`) — types as i32, not NaN-boxed f64. Without it the call site keeps the full
310
+ // __typed_idx/ToNumber unbox dispatch (491520× per dict kernel run). Mirrors
311
+ // refreshCallerLocals + analyzeFuncForEmit. Only meaningful once Phase G has tagged params
312
+ // ptrKind=TYPED (the I2 re-run below); harmless before (no typed params → overlay untouched).
313
+ const savedTE = ctx.types.typedElem
314
+ let te = null
315
+ for (const p of func.sig.params) {
316
+ if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
317
+ const c = ctorFromElemAux(p.ptrAux)
318
+ if (c != null) { if (!te) te = savedTE ? new Map(savedTE) : new Map(); te.set(p.name, c) }
319
+ }
320
+ }
321
+ if (te) ctx.types.typedElem = te
322
+ const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
323
+ const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
324
+ if (te) ctx.types.typedElem = savedTE
325
+ const r = { allV128, allI32, anyUnsigned: exprs.some(isUnsignedTail), allUnsigned: exprs.every(isUnsignedTail) }
326
+ ctx.func.current = savedCurrent
327
+ return r
328
+ }
263
329
  let changed = true
264
330
  while (changed) {
265
331
  changed = false
@@ -269,22 +335,40 @@ function narrowI32Results(funcs) {
269
335
  if (isBlockBody(body) && hasBareReturn(body)) continue
270
336
  const exprs = returnExprs(body)
271
337
  if (!exprs.length) continue
272
- const savedCurrent = ctx.func.current
273
- ctx.func.current = func.sig
274
- const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
275
- for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
276
- const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
277
- const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
278
- const anyUnsigned = exprs.some(isUnsignedTail)
279
- const allUnsigned = exprs.every(isUnsignedTail)
280
- ctx.func.current = savedCurrent
338
+ let r = evalTails(func, body, exprs)
339
+ // Recursive result cycle: a self-call in a return tail — or feeding a returned local
340
+ // (nqueens' `cnt = cnt + solve(); return cnt`) reads solve's own not-yet-narrowed
341
+ // f64 result, so `cnt` widens to f64 and the i32 narrowing never fires. Break the cycle
342
+ // optimistically: tentatively assume the i32 result, re-analyze, and keep it ONLY if every
343
+ // tail is then i32 (else revert). Sound committed only when self-consistent.
344
+ if (!r.allI32 && !r.allV128 && callsSelf(body, func.name)) {
345
+ const saved = func.sig.results
346
+ func.sig.results = ['i32']
347
+ invalidateLocalsCache(body)
348
+ const opt = evalTails(func, body, exprs)
349
+ if (opt.allI32 && (!opt.anyUnsigned || opt.allUnsigned)) {
350
+ if (opt.allUnsigned) func.sig.unsignedResult = true
351
+ changed = true
352
+ continue
353
+ }
354
+ func.sig.results = saved
355
+ invalidateLocalsCache(body)
356
+ r = evalTails(func, body, exprs)
357
+ }
281
358
  // SIMD: every tail returns a lane vector → v128 result.
282
- if (allV128) {
359
+ if (r.allV128) {
283
360
  func.sig.results = ['v128']
284
361
  changed = true
285
- } else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
362
+ } else if (r.allI32 && (!r.anyUnsigned || r.allUnsigned)) { // sign-consistent i32 tails
286
363
  func.sig.results = ['i32']
287
- if (allUnsigned) func.sig.unsignedResult = true
364
+ if (r.allUnsigned) func.sig.unsignedResult = true
365
+ // A committed i32 result is a genuine NUMBER, so stamp valResult for the call-site
366
+ // VAL dispatch — E2 (narrowValResults) ran ABOVE the param lattice and so couldn't
367
+ // type a `return typedArrayParam[idx]` tail (hashjoin's `probe` → `vals[h]`), leaving
368
+ // valResult unset → the hot `sum + probe()` stayed the polymorphic string-or-number
369
+ // `+`. Only-if-unset: an UNBOXED-pointer i32 result already carries its ARRAY/OBJECT/
370
+ // TYPED valResult (the unboxing ABI needs it), so this never overwrites a pointer kind.
371
+ if (func.valResult == null) func.valResult = VAL.NUMBER
288
372
  changed = true
289
373
  }
290
374
  }
@@ -652,10 +736,20 @@ export default function narrowSignatures(programFacts, ast) {
652
736
  const state = siteState(callSites[s])
653
737
  if (!state) continue
654
738
  const { func, argList } = state
739
+ const recursive = state.callee === state.callerFunc?.name
655
740
  for (let k = 0; k < func.sig.params.length; k++) {
656
741
  const r = ensureParamRep(paramReps, state.callee, k)
657
742
  if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
658
- for (const rule of rules) rule.apply(r, argList[k], k, state)
743
+ const arg = argList[k]
744
+ // Recursive identity arg — `f(…, p, …)` calling itself with its own param p threaded
745
+ // through at the same position — is a fixpoint identity: it carries whatever type p
746
+ // settles to, so it constrains nothing. Skip it, else exprType(p) reads p's not-yet-
747
+ // narrowed f64 and the meet poisons the type the non-recursive call sites would prove
748
+ // (nqueens' `solve(all, …)` — `all` stuck f64 while cols/d1/d2, passed as i32 bitwise
749
+ // exprs, narrowed fine).
750
+ const pname = func.sig.params[k].name
751
+ if (recursive && (arg === pname || (Array.isArray(arg) && arg[0] === 'local.get' && arg[1] === pname))) continue
752
+ for (const rule of rules) rule.apply(r, arg, k, state)
659
753
  }
660
754
  }
661
755
  }
@@ -723,6 +817,46 @@ export default function narrowSignatures(programFacts, ast) {
723
817
  mergeParamFact(r, field, v)
724
818
  },
725
819
  })
820
+ // WASM type of a call arg. exprType resolves most shapes, but an INTEGER typed-array
821
+ // element read `intArr[idx]` (and arithmetic over it, `intArr[idx]+1`) types f64 here:
822
+ // exprType's `[]` rule reads the typedElem OVERLAY, which doesn't see a typedCtor-narrowed
823
+ // PARAM array at fixpoint time — yet the element is a 32-bit machine integer. Install the
824
+ // caller's resolved param-typedCtors (+ module globals) as that overlay for the duration
825
+ // of the type query, so a param fed only such integer elements (dict's key `k` ← src[i],
826
+ // threaded through Math.imul / === keys[h] / keys[h]=k) narrows to i32 instead of paying
827
+ // convert + f64-compare + trunc round-trips through its probe loop.
828
+ // A value built ONLY from the callee's own params + already-i32 locals + integer constants via
829
+ // integer-preserving ops. Its i32-ness follows from its inputs' — for a recursive self-call it
830
+ // carries no INDEPENDENT evidence about whether the params are i32. Used for the optimism below.
831
+ const isRecurIntExpr = (n, pnames, callerLocals) => {
832
+ if (typeof n === 'string') return pnames.has(n) || callerLocals?.get?.(n) === 'i32'
833
+ if (typeof n === 'number') return Number.isInteger(n)
834
+ if (!Array.isArray(n)) return false
835
+ if (n[0] == null) return typeof n[1] === 'number' && Number.isInteger(n[1]) // boxed int literal
836
+ if (n[0] === 'local.get') return pnames.has(n[1]) || callerLocals?.get?.(n[1]) === 'i32'
837
+ if (RECUR_INT_OPS.has(n[0])) return n.slice(1).every(c => isRecurIntExpr(c, pnames, callerLocals))
838
+ return false
839
+ }
840
+ const argWasmType = (arg, state) => {
841
+ // Recursive self-call: an arg built only from the callee's own params + already-i32 locals +
842
+ // int constants (`f(n - 1)`, `f(n - 1 - i)`) is i32 IFF those params are i32 — a fixpoint
843
+ // identity carrying no INDEPENDENT type evidence. Optimistically type it i32 so the NON-
844
+ // recursive call sites decide: all i32 ⇒ the param narrows; any f64 ⇒ the meet still poisons
845
+ // it. Lets a plain decreasing recursion narrow with no `|0` source crutch. (The bare-identity
846
+ // arg `f(n)` is already skipped wholesale in runCallsiteLattice.)
847
+ if (state.callee === state.callerFunc?.name &&
848
+ isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
849
+ if (!state._teOverlay) {
850
+ const m = new Map(ctx.scope.globalTypedElem || [])
851
+ const pf = state.callerParamFacts('typedCtor')
852
+ if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
853
+ state._teOverlay = m
854
+ }
855
+ const prev = ctx.func.localTypedElemsOverlay
856
+ ctx.func.localTypedElemsOverlay = state._teOverlay
857
+ try { return exprType(arg, state.callerLocals) }
858
+ finally { ctx.func.localTypedElemsOverlay = prev }
859
+ }
726
860
  const runFixpoint = () => runCallsiteLattice([
727
861
  // val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
728
862
  // typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
@@ -734,7 +868,7 @@ export default function narrowSignatures(programFacts, ast) {
734
868
  missing: poison('wasm'),
735
869
  apply(r, arg, _k, state) {
736
870
  if (r.wasm === null) return
737
- const wt = exprType(arg, state.callerLocals)
871
+ const wt = argWasmType(arg, state)
738
872
  if (r.wasm === undefined) r.wasm = wt
739
873
  else if (r.wasm !== wt) r.wasm = null
740
874
  },
@@ -899,6 +1033,16 @@ export default function narrowSignatures(programFacts, ast) {
899
1033
  // (callback bench: mix is FNV — params and result all i32-shaped, but inferred
900
1034
  // only after E phase narrowed mix's result).
901
1035
  phase.refreshLocals()
1036
+ // I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
1037
+ // typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
1038
+ // typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
1039
+ // vals an Int32Array param — had its return tail type as NaN-boxed f64 (vals not yet a
1040
+ // typed pointer), leaving sig.results f64 and the call site running the full
1041
+ // __typed_idx/ToNumber unbox on every probe step (491520× per dict kernel run). Now that
1042
+ // evalTails seeds the typed-param overlay and params carry ptrAux, the fixpoint catches
1043
+ // `vals[h]` as i32, narrows the result, and the dispatch vanishes; the runFixpoint below
1044
+ // then propagates the i32 result into `let v = lookup(...)` at the call sites.
1045
+ narrowI32Results(funcsWithNarrowableResult)
902
1046
  // Reset wasm field unconditionally — first pass populated it from stale callerLocals
903
1047
  // (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
904
1048
  // yet). clearStickyNull only resets null; here we need to reset f64-observed too
@@ -1127,6 +1271,87 @@ export function adviseJsstringCarrier(paramReps, valueUsed) {
1127
1271
  }
1128
1272
  }
1129
1273
 
1274
+ // Two value-kinds CONFLICT when passing one where the other is expected would
1275
+ // rely on a JS boundary coercion jz does not implement (so the result diverges).
1276
+ // NUMBER↔STRING is the canonical pair: `"5" * 2` is 10 in JS but NaN in jz, and a
1277
+ // STRING passed to a numeric param reads its NaN-boxed bits as an f64. ARRAY/OBJECT
1278
+ // vs NUMBER/STRING likewise. We treat the four primitive-ish kinds as mutually
1279
+ // exclusive; BOOL is omitted (it nanboxes to 0/1 and numeric code tolerates it).
1280
+ const STRICT_CONFLICT = {
1281
+ [VAL.NUMBER]: new Set([VAL.STRING, VAL.ARRAY, VAL.OBJECT]),
1282
+ [VAL.STRING]: new Set([VAL.NUMBER, VAL.ARRAY, VAL.OBJECT]),
1283
+ [VAL.ARRAY]: new Set([VAL.NUMBER, VAL.STRING]),
1284
+ [VAL.OBJECT]: new Set([VAL.NUMBER, VAL.STRING]),
1285
+ }
1286
+ const kindName = (v) => ({ [VAL.NUMBER]: 'number', [VAL.STRING]: 'string', [VAL.ARRAY]: 'array', [VAL.OBJECT]: 'object' }[v] || 'value')
1287
+
1288
+ /**
1289
+ * Strict-mode boundary type check (standalone; runs in both the full-narrow and
1290
+ * skip-narrow plan paths).
1291
+ *
1292
+ * jz infers a param's type from how it's used (`x * 2` → number, `s.charCodeAt`
1293
+ * → string) or from an explicit default (`x = 0` → number). In permissive mode a
1294
+ * caller may pass any type and jz silently computes a divergent result (`"5"*2`
1295
+ * is 10 in JS, NaN here). Strict mode is the canonical subset where that misuse
1296
+ * is a compile error instead — consistent with strict already rejecting `==`,
1297
+ * dynamic dispatch, and `void`.
1298
+ *
1299
+ * Fires ONLY when BOTH sides are statically certain and conflict:
1300
+ * - the callee param has a known kind (its default-value type, or a settled
1301
+ * `val` rep from body-usage / call-site inference), AND
1302
+ * - the argument expression has a known, conflicting kind (a literal or a
1303
+ * locally-typed binding).
1304
+ * An untyped param or untyped arg is never flagged — no false positives on
1305
+ * genuinely polymorphic code.
1306
+ */
1307
+ export function strictBoundaryTypeCheck(programFacts) {
1308
+ if (!ctx.transform.strict) return
1309
+ const { callSites, paramReps } = programFacts
1310
+
1311
+ // Per-callee body-evidence cache: methodEvidence (.charCodeAt→STRING, .push→
1312
+ // ARRAY) keyed by param name. Computed lazily, once per callee.
1313
+ const bodyEvidence = new Map()
1314
+ const evidenceOf = (func) => {
1315
+ if (!bodyEvidence.has(func.name)) {
1316
+ const names = func.sig.params.map(p => p.name).filter(Boolean)
1317
+ bodyEvidence.set(func.name, func.body ? inferParams(func.body, names) : new Map())
1318
+ }
1319
+ return bodyEvidence.get(func.name)
1320
+ }
1321
+
1322
+ // Expected kind of callee param k, from any statically-certain source:
1323
+ // 1. explicit default value type — the source's own declaration (x = 0 → number)
1324
+ // 2. settled call-site/usage rep `val` (paramReps; present after full narrow)
1325
+ // 3. type-exclusive body evidence (methodEvidence; works in skip-narrow path too)
1326
+ // First certain source wins; null when the param is genuinely polymorphic.
1327
+ const paramKind = (func, k) => {
1328
+ const p = func.sig.params[k]
1329
+ if (!p) return null
1330
+ const def = func.defaults?.[p.name]
1331
+ if (def != null) { const dv = valTypeOf(def); if (dv) return dv }
1332
+ const repVal = paramReps?.get(func.name)?.get(k)?.val
1333
+ if (repVal != null) return repVal
1334
+ return evidenceOf(func).get(p.name)?.val ?? null
1335
+ }
1336
+
1337
+ for (const cs of callSites) {
1338
+ const func = ctx.func.map.get(cs.callee)
1339
+ if (!func || func.raw || !func.sig) continue
1340
+ if (func.rest) continue // rest packs args into an array
1341
+ for (let k = 0; k < cs.argList.length && k < func.sig.params.length; k++) {
1342
+ const want = paramKind(func, k)
1343
+ if (want == null) continue
1344
+ const conflicts = STRICT_CONFLICT[want]
1345
+ if (!conflicts) continue
1346
+ const got = valTypeOf(cs.argList[k])
1347
+ if (got != null && conflicts.has(got)) {
1348
+ const pname = func.sig.params[k].name
1349
+ err(`strict mode: ${kindName(want)} parameter '${pname}' of '${cs.callee}' received a ${kindName(got)} argument — jz does not coerce ${kindName(got)}→${kindName(want)} at the call boundary (the result would diverge from JS). Pass a ${kindName(want)}, or { strict: false }.`)
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+
1130
1355
  /**
1131
1356
  * Phase: bimorphic typed-array param specialization.
1132
1357
  *
@@ -1161,13 +1386,9 @@ export function specializeBimorphicTyped(programFacts) {
1161
1386
  if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
1162
1387
  }
1163
1388
 
1164
- // Per-caller typedElem map (literal `new TypedArray(N)` bindings inside body).
1165
- const callerTypedCtx = new Map()
1166
- callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
1167
- for (const func of ctx.func.list) {
1168
- if (!func.body || func.raw) continue
1169
- callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
1170
- }
1389
+ // Per-caller typedElem map: body-local `new TypedArray(N)` bindings layered
1390
+ // over the module's typed globals (shared with buildCallerTypedCtx).
1391
+ const callerTypedCtx = buildCallerTypedCtx()
1171
1392
  // Per-caller typed-param map: caller's own params that F/G already narrowed
1172
1393
  // (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
1173
1394
  const callerTypedParamsCtx = new Map()
@@ -1237,25 +1458,37 @@ export function specializeBimorphicTyped(programFacts) {
1237
1458
 
1238
1459
  // Build one clone per distinct combo.
1239
1460
  const cloneByKey = new Map()
1240
- for (const [key, combo] of distinct) {
1241
- const suffix = combo.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
1461
+ for (const [dkey, cmb] of distinct) {
1462
+ // NB: this loop variable must NOT reuse the name `combo` (declared twice above, at the
1463
+ // site loop and the distinct-building loop). The self-host miscompiles a for-of loop
1464
+ // variable whose name collides with an earlier block-scoped declaration — it aliases the
1465
+ // prior binding instead of rebinding per iteration, so `combo` would stay stuck at the
1466
+ // last site's ctor and every clone would get the same (wrong) element type → silent
1467
+ // garbage. A unique name gets a clean per-iteration binding.
1468
+ const suffix = cmb.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
1242
1469
  let cloneName = `${func.name}$${suffix}`
1243
1470
  let n = 0
1244
1471
  while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
1245
1472
 
1473
+ // Build cloneSig with clean, fully-formed object literals — never by spreading a
1474
+ // live object and then overriding/extending its keys. A MULTI-prop spread of a
1475
+ // member-access source (`{ ...func.sig, params, results }`) takes the static
1476
+ // allKnown OBJECT-merge path, which trusts func.sig's COMPILE-TIME schema; sig
1477
+ // objects are polymorphic (some carry result/ptrKind/unsignedResult), so that
1478
+ // schema can be a subset of the runtime shape and the slot-copy then faults a
1479
+ // later `sig.params` read out of bounds in the self-host. (The single-unknown
1480
+ // `{ ...x }` clone is fixed at the root — __obj_clone — but the allKnown merge
1481
+ // path is a separate hazard.) Constructing each param with its pointer ABI baked
1482
+ // in sidesteps it; output is unchanged on the host.
1246
1483
  const cloneSig = {
1247
- ...func.sig,
1248
- params: func.sig.params.map(p => ({ ...p })),
1484
+ params: func.sig.params.map((p, idx) => {
1485
+ const bi = bimorphic.indexOf(idx)
1486
+ return bi < 0
1487
+ ? { ...p }
1488
+ : { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: typedElemAux(cmb[bi]) }
1489
+ }),
1249
1490
  results: [...func.sig.results],
1250
1491
  }
1251
- for (let i = 0; i < bimorphic.length; i++) {
1252
- const k = bimorphic[i]
1253
- const aux = typedElemAux(combo[i])
1254
- const p = cloneSig.params[k]
1255
- p.type = 'i32'
1256
- p.ptrKind = VAL.TYPED
1257
- p.ptrAux = aux
1258
- }
1259
1492
  const clone = { ...func, name: cloneName, sig: cloneSig }
1260
1493
  ctx.func.list.push(clone)
1261
1494
  ctx.func.map.set(cloneName, clone)
@@ -1263,19 +1496,20 @@ export function specializeBimorphicTyped(programFacts) {
1263
1496
 
1264
1497
  // Mirror per-param reps under the clone's name with mono ctors at bimorphic
1265
1498
  // positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
1266
- // `arr[i]` lowers to direct typed load.
1499
+ // `arr[i]` lowers to direct typed load. Each `{ ...r }` is a true clone, so
1500
+ // pinning typedCtor on it leaves the source rep untouched (__obj_clone).
1267
1501
  const cloneReps = new Map()
1268
1502
  for (const [k, r] of reps) cloneReps.set(k, { ...r })
1269
1503
  for (let i = 0; i < bimorphic.length; i++) {
1270
1504
  const k = bimorphic[i]
1271
1505
  const r = cloneReps.get(k) || {}
1272
- r.typedCtor = combo[i]
1506
+ r.typedCtor = cmb[i]
1273
1507
  r.val = VAL.TYPED
1274
1508
  cloneReps.set(k, r)
1275
1509
  }
1276
1510
  paramReps.set(cloneName, cloneReps)
1277
1511
 
1278
- cloneByKey.set(key, clone)
1512
+ cloneByKey.set(dkey, clone)
1279
1513
  }
1280
1514
 
1281
1515
  // Rewrite each site's call AST to point at the matching clone.