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
@@ -76,7 +76,7 @@ import { TYPED_ELEM_CODE, TYPED_ELEM_VIEW_FLAG, TYPED_ELEM_BIGINT_FLAG, encodeTy
76
76
  import {
77
77
  findFreeVars, findMutations, boxedCaptures,
78
78
  collectI32SafeIndexVars, collectF64StridedIndexVars, narrowUint32, scanBindingUses,
79
- scanFlatObjects, scanSliceViews, USE,
79
+ scanFlatObjects, scanSliceViews, scanNeverGrown, scanNumericFill, USE,
80
80
  } from './analyze-scans.js'
81
81
 
82
82
  export { findFreeVars, findMutations, boxedCaptures } from './analyze-scans.js'
@@ -118,12 +118,26 @@ const makeTypedTracker = (get, set, del) => {
118
118
  if (poison.has(name)) return
119
119
  const setOrInvalidate = (c) => {
120
120
  if (c === MIXED_CTORS) return invalidate(name)
121
+ // Module-level alias fact: a `.view` ctor (subarray / buffer-backed) is the ONLY
122
+ // way two typed-array bindings can overlap. Recording that the program creates
123
+ // ANY view lets memory-reordering passes (SLP) stay sound by bailing when set —
124
+ // with no view, distinct typed bases own disjoint allocations.
125
+ if (typeof c === 'string' && c.endsWith('.view')) ctx.features.typedView = true
121
126
  const prev = get(name)
122
127
  if (prev && prev !== c) invalidate(name)
123
128
  else set(name, c)
124
129
  }
125
130
  const ctor = typedElemCtor(rhs)
126
131
  if (ctor) return setOrInvalidate(ctor)
132
+ // `recv.subarray(...)` is a zero-copy VIEW aliasing the receiver's buffer — its elem
133
+ // ctor is the receiver's type with the `.view` flag, so the binding unboxes to a typed
134
+ // pointer and element writes take the descriptor-indirected path (not desc-as-data).
135
+ if (Array.isArray(rhs) && rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.'
136
+ && rhs[1][2] === 'subarray' && typeof rhs[1][1] === 'string') {
137
+ const recvCtor = resolveName(rhs[1][1])
138
+ if (recvCtor) return setOrInvalidate((recvCtor.endsWith('.view') ? recvCtor.slice(0, -5) : recvCtor) + '.view')
139
+ return
140
+ }
127
141
  // TYPED-narrowed call result carries its elem aux on f.sig.ptrAux — reverse-map
128
142
  // to a canonical ctor so the unboxed local's rep restores the same aux.
129
143
  if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
@@ -159,7 +173,7 @@ export function analyzeBody(body) {
159
173
  // for any slice and can't be WeakMap-keyed. Return empty maps without caching.
160
174
  if (body === null || typeof body !== 'object') return {
161
175
  locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
162
- arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
176
+ arrElemValTypes: new Map(), arrElemTypedCtors: new Map(), typedElems: new Map(), escapes: new Map(),
163
177
  flatObjects: new Map(),
164
178
  }
165
179
  const hit = _bodyFactsCache.get(body)
@@ -175,6 +189,10 @@ export function analyzeBody(body) {
175
189
  // so `chord[j]` is a Number and skips __to_num. Single-level only — enough for the
176
190
  // 2-D table pattern without a general nested-type lattice.
177
191
  const arrElemElemValTypes = new Map()
192
+ // `name`'s elements are all typed arrays of one ctor ('new.Float32Array'), e.g.
193
+ // `Array.from(nCh, () => new Float32Array(n))` (codec channelData). Lets `arr[i]`
194
+ // resolve as that typed array so `arr[i][j]` / `let o = arr[i]; o[j]` inline.
195
+ const arrElemTypedCtors = new Map()
178
196
  const typedElems = new Map()
179
197
  const escapes = new Map() // name → bool: local holds allocation, true if it escapes
180
198
 
@@ -215,6 +233,22 @@ export function analyzeBody(body) {
215
233
  return arrElemValTypes.get(name) || null
216
234
  }
217
235
 
236
+ // Disagreement → null poison, like observeArrValType. Records the common
237
+ // TypedArray ctor of an array's elements.
238
+ const observeArrTypedCtor = (arr, ctor) => {
239
+ if (typeof arr !== 'string') return
240
+ if (arrElemTypedCtors.get(arr) === null) return
241
+ if (!ctor) { arrElemTypedCtors.set(arr, null); return }
242
+ if (!arrElemTypedCtors.has(arr)) arrElemTypedCtors.set(arr, ctor)
243
+ else if (arrElemTypedCtors.get(arr) !== ctor) arrElemTypedCtors.set(arr, null)
244
+ }
245
+ // The ctor a `new XxxArray(...)` element expr produces ('new.Float32Array'), if any.
246
+ const elemTypedCtorOf = (expr) => {
247
+ const c = typedElemCtor(expr)
248
+ // typed-array views/buffers only — exclude ArrayBuffer/DataView (no element index).
249
+ return c && !c.includes('ArrayBuffer') && !c.includes('DataView') ? c : null
250
+ }
251
+
218
252
  const exprElemSourceVal = (expr) => {
219
253
  if (typeof expr === 'string') {
220
254
  const repVt = ctx.func.localReps?.get(expr)?.val
@@ -306,6 +340,14 @@ export function analyzeBody(body) {
306
340
  if (exprElemSourceVal(elems[k]) !== common) common = null
307
341
  }
308
342
  if (common != null) observeArrValType(name, common)
343
+ // Array-of-typed-arrays literal (`[new Float32Array(n), …]`): record the
344
+ // common element ctor so `name[i]` is a known typed array.
345
+ if (common === VAL.TYPED) {
346
+ let ctor = elemTypedCtorOf(elems[0])
347
+ for (let k = 1; k < elems.length && ctor != null; k++)
348
+ if (elemTypedCtorOf(elems[k]) !== ctor) ctor = null
349
+ observeArrTypedCtor(name, ctor)
350
+ }
309
351
  // Array-of-arrays literal: record the common element-of-element kind so a
310
352
  // later `x = name[i]` binds `x`'s element type one level down.
311
353
  if (common === VAL.ARRAY) {
@@ -319,8 +361,12 @@ export function analyzeBody(body) {
319
361
  }
320
362
  // `x = arr[i]` where `arr` is a known array-of-arrays → `x`'s elements take
321
363
  // `arr`'s nested element kind (the missing index-step in observeArrValType).
364
+ // `arr` may be a function-local (arrElemElemValTypes) or a module-level const
365
+ // table (global rep, recorded by recordGlobalRep) — the latter dynWrite-guarded.
322
366
  if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 && typeof rhs[1] === 'string') {
323
367
  const nested = arrElemElemValTypes.get(rhs[1])
368
+ ?? (!ctx.func.localReps?.has(rhs[1]) && !ctx.types?.dynWriteVars?.has(rhs[1])
369
+ ? ctx.scope.globalReps?.get(rhs[1])?.arrayElemElemValType : null)
324
370
  if (nested) observeArrValType(name, nested)
325
371
  }
326
372
  }
@@ -328,6 +374,18 @@ export function analyzeBody(body) {
328
374
  const f = ctx.func.map?.get(rhs[1])
329
375
  if (f?.arrayElemValType) observeArrValType(name, f.arrayElemValType)
330
376
  }
377
+ // `Array.from(arg, () => new XxxArray(...))` — codec channelData and per-row
378
+ // typed-array tables. The map-callback's returned ctor is every element's type.
379
+ // Post-prepare AST: `['()', 'Array.from', [',', arg, callback]]` (args in a comma node).
380
+ if (Array.isArray(rhs) && rhs[0] === '()' && rhs[1] === 'Array.from' && Array.isArray(rhs[2])) {
381
+ const args = rhs[2][0] === ',' ? rhs[2].slice(1) : [rhs[2]]
382
+ const fn = args[1]
383
+ const body = Array.isArray(fn) && fn[0] === '=>' ? fn[2] : null
384
+ const ret = Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === 'return'
385
+ ? body[1][1] : body
386
+ const ctor = ret && elemTypedCtorOf(ret)
387
+ if (ctor) { observeArrValType(name, VAL.TYPED); observeArrTypedCtor(name, ctor) }
388
+ }
331
389
  if (typeof rhs === 'string') {
332
390
  const v = elemValOf(rhs)
333
391
  if (v) observeArrValType(name, v)
@@ -456,9 +514,20 @@ export function analyzeBody(body) {
456
514
  }
457
515
  observeArrSchema(arr, exprSchemaId(a, localSchemaMap))
458
516
  observeArrValType(arr, exprElemSourceVal(a))
517
+ // `ch.push(new Float32Array(m))` — track the element ctor so `ch[c][i]`
518
+ // inlines, same as the Array.from / array-literal forms.
519
+ if (exprElemSourceVal(a) === VAL.TYPED) observeArrTypedCtor(arr, elemTypedCtorOf(a))
459
520
  }
460
521
  }
461
522
 
523
+ // `ch[c] = new Float32Array(m)` — index-fill construction of a typed-array-of-
524
+ // arrays (`let ch = new Array(n); for(c) ch[c] = new T(m)`). Mirror push.
525
+ if (op === '=' && Array.isArray(node[1]) && node[1][0] === '[]' && node[1].length === 3
526
+ && typeof node[1][1] === 'string' && valTypeOf(node[2]) === VAL.TYPED) {
527
+ observeArrValType(node[1][1], VAL.TYPED)
528
+ observeArrTypedCtor(node[1][1], elemTypedCtorOf(node[2]))
529
+ }
530
+
462
531
  // `=` reassignment — locals widen, valTypes/typedElems track,
463
532
  // arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
464
533
  if (op === '=' && typeof node[1] === 'string') {
@@ -472,6 +541,7 @@ export function analyzeBody(body) {
472
541
  trackTyped(name, rhs)
473
542
  if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
474
543
  if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
544
+ if (arrElemTypedCtors.has(name) && !isArrayProducingRhs(rhs)) observeArrTypedCtor(name, null)
475
545
  return
476
546
  }
477
547
 
@@ -517,7 +587,7 @@ export function analyzeBody(body) {
517
587
  const prevTypedOverlay = ctx.func.localTypedElemsOverlay
518
588
  ctx.func.localValTypesOverlay = valTypes
519
589
  ctx.func.localTypedElemsOverlay = typedElems
520
- let unsignedLocals
590
+ let unsignedLocals, numericFill
521
591
  try {
522
592
  walk(body)
523
593
  widenLocalTypes(body, locals)
@@ -526,6 +596,18 @@ export function analyzeBody(body) {
526
596
  // reconsidered with final types — and stays f64, since a relational compare
527
597
  // is a non-transparent read that disqualifies narrowing anyway.
528
598
  unsignedLocals = narrowUint32(body, locals)
599
+ // Numeric-fill arrays — fresh `Array(n)`/`[]` whose every element write stores a
600
+ // Number, so `a[i]` reads can skip __to_num (the win `[1,2,3]` already gets, for the
601
+ // construct-then-fill kernel shape). Runs HERE, inside the val-type overlay, so a
602
+ // write of a bare numeric local (`a[i] = out`) resolves via the just-built `valTypes`.
603
+ // A bare read of the array's OWN elements (`a[i] = a[j]`, heapsort) is Numeric by
604
+ // induction; any genuinely non-numeric write still fails the test and disqualifies.
605
+ const numericFillRhs = (rhs, selfName) => {
606
+ if (Array.isArray(rhs) && rhs[0] === '[]' && rhs[1] === selfName) return true
607
+ if (typeof rhs === 'string') return valTypes.get(rhs) === VAL.NUMBER || exprElemSourceVal(rhs) === VAL.NUMBER
608
+ return valTypeOf(rhs) === VAL.NUMBER
609
+ }
610
+ numericFill = scanNumericFill(body, numericFillRhs)
529
611
  } finally {
530
612
  ctx.func.localValTypesOverlay = prevOverlay
531
613
  ctx.func.localTypedElemsOverlay = prevTypedOverlay
@@ -545,7 +627,10 @@ export function analyzeBody(body) {
545
627
  // Consumed by emitDecl, which lowers the initializer to a SLICE_BIT view.
546
628
  const sliceViews = doSchemas ? scanSliceViews(body) : new Set()
547
629
 
548
- const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes, flatObjects, sliceViews, unsignedLocals }
630
+ // Never-relocated array bindings reads may skip the realloc-forwarding follow.
631
+ const neverGrown = doSchemas ? scanNeverGrown(body) : new Set()
632
+
633
+ const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, arrElemTypedCtors, typedElems, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
549
634
  _bodyFactsCache.set(body, result)
550
635
  return result
551
636
  }
@@ -677,6 +762,18 @@ export function analyzeValTypes(body) {
677
762
  for (const [name, vt] of facts.arrElemValTypes) {
678
763
  if (vt != null) updateRep(name, { arrayElemValType: vt })
679
764
  }
765
+ // Array-of-typed-arrays element ctor → rep, so `arr[i]` resolves as a typed array
766
+ // and `arr[i][j]` / `let o = arr[i]; o[j]` inline (codec channelData scatter).
767
+ for (const [name, ctor] of facts.arrElemTypedCtors) {
768
+ if (ctor != null) updateRep(name, { arrayElemTypedCtor: ctor })
769
+ }
770
+ // Construct-then-fill numeric arrays (`let a = Array(n); a[i] = expr`) carry no
771
+ // element evidence at their decl, so the walk above leaves them untyped. scanNumericFill
772
+ // proved every write Numeric and every other use a pure read — record NUMBER so `arr[i]`
773
+ // reads skip __to_num, unless an observation already poisoned the slot to a conflict.
774
+ for (const name of facts.numericFill || []) {
775
+ if (facts.arrElemValTypes.get(name) !== null) updateRep(name, { arrayElemValType: VAL.NUMBER })
776
+ }
680
777
  // Propagate body-observed array-elem schemas to localReps so unboxablePtrs's
681
778
  // `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
682
779
  // to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Sound CSE of repeated pure typed-array element loads within a straight-line region.
3
+ *
4
+ * `re[b] = re[a] - tr; …; re[a] = re[a] + tr` — the fft butterfly loads `re[a]` twice.
5
+ * Cache the first load in a temp and reuse it (eliminating the redundant load) when every
6
+ * intervening store writes a provably-DIFFERENT element, so it cannot clobber the cached value.
7
+ *
8
+ * Soundness (no array-distinctness / non-aliasing assumption):
9
+ * A cached `arr[idx]` survives a store `any[idx2]` iff `idx2 ≠ idx` is PROVABLE. If the indices
10
+ * differ, the store hits a different element even if `any` aliases `arr`. If `idx2` might equal
11
+ * `idx` (incl. a different array at the same index — could alias), invalidate. Array-name-
12
+ * agnostic; never assumes non-aliasing. Reassigning `arr` / any var in `idx`, an impure call, or
13
+ * a control-flow edge also flushes. So `re[a]` (intervening stores all at index `b = a+half ≠ a`)
14
+ * is CSE'd, while `im[a]` (the `re[a]` store is at the same index `a`) correctly is NOT.
15
+ *
16
+ * provablyDiffer: distinct int constants, or `idx2 = idx ± P` with P provably > 0 — the canonical
17
+ * P is a loop bound: inside `for (j = C≥0; j < B; …)` the body runs only when `0 ≤ j < B`, so
18
+ * `B ≥ 1`, hence `a + B ≠ a`. A name index resolves through its single `let X = a + b` def.
19
+ *
20
+ * Runs post-analyze (purity known) and pre-emit, mutating the body. Purely conservative.
21
+ */
22
+
23
+ import { ASSIGN_OPS } from '../ast.js'
24
+
25
+ const isArr = (x) => Array.isArray(x) // arrow, not a bare builtin alias — jz can't self-host a builtin as a first-class value
26
+ const isName = (x) => typeof x === 'string'
27
+
28
+ const stableIdx = (e) => {
29
+ if (isName(e) || typeof e === 'number') return true
30
+ if (!isArr(e)) return false
31
+ const op = e[0]
32
+ if (op == null) return typeof e[1] === 'number'
33
+ if ((op === '+' || op === '-' || op === '*') && e.length === 3) return stableIdx(e[1]) && stableIdx(e[2])
34
+ return false
35
+ }
36
+
37
+ const idxKey = (e) => {
38
+ if (isName(e)) return e
39
+ if (typeof e === 'number') return `#${e}`
40
+ if (isArr(e) && e[0] == null) return `#${e[1]}`
41
+ if (isArr(e)) return `(${e[0]} ${idxKey(e[1])} ${e[2] !== undefined ? idxKey(e[2]) : ''})`
42
+ return '?'
43
+ }
44
+
45
+ const idxVars = (e, out) => {
46
+ if (isName(e)) out.add(e)
47
+ else if (isArr(e)) for (let i = 1; i < e.length; i++) idxVars(e[i], out)
48
+ }
49
+
50
+ const CONTROL = new Set(['for', 'while', 'do', 'if', 'loop', 'block', 'switch', 'try', '=>',
51
+ 'br', 'br_if', 'br_table', 'return', 'continue', 'break', 'throw', 'unreachable'])
52
+ const ASSIGN = new Set([...ASSIGN_OPS, '++', '--'])
53
+
54
+ function buildFacts(body) {
55
+ const def = new Map(), declCount = new Map(), positive = new Set()
56
+ const scan = (n) => {
57
+ if (!isArr(n)) return
58
+ const op = n[0]
59
+ if (op === 'let' || op === 'const') {
60
+ for (let i = 1; i < n.length; i++) {
61
+ const d = n[i]
62
+ if (isName(d)) declCount.set(d, (declCount.get(d) || 0) + 1)
63
+ else if (isArr(d) && d[0] === '=' && isName(d[1])) {
64
+ declCount.set(d[1], (declCount.get(d[1]) || 0) + 1)
65
+ def.set(d[1], d[2])
66
+ }
67
+ }
68
+ }
69
+ if (op === 'for') {
70
+ // parse shape ['for', [';', init, cond, step], body]; post-prepare ['for', init, cond, step, body].
71
+ const hdr2 = isArr(n[1]) && n[1][0] === ';'
72
+ const init = hdr2 ? n[1][1] : n[1]
73
+ const cond = hdr2 ? n[1][2] : n[2]
74
+ const lo = isArr(init) && (init[0] === 'let' || init[0] === 'const') && isArr(init[1]) && init[1][0] === '=' ? init[1][2]
75
+ : (isArr(init) && init[0] === '=' ? init[2] : null)
76
+ const loZero = (typeof lo === 'number' && lo >= 0) || (isArr(lo) && lo[0] == null && lo[1] >= 0)
77
+ // `for (j = 0; j < BOUND; …)` ⇒ inside the body BOUND ≥ 1 (j ≥ 0 ∧ j < BOUND). Mark BOUND positive.
78
+ if (loZero && isArr(cond) && (cond[0] === '<' || cond[0] === '<=') && isName(cond[2])) positive.add(cond[2])
79
+ }
80
+ for (let i = 1; i < n.length; i++) scan(n[i])
81
+ }
82
+ scan(body)
83
+ for (const [n, c] of declCount) if (c > 1) def.delete(n) // reassigned ⇒ not a stable def
84
+ return { def, positive }
85
+ }
86
+
87
+ const cval = (e) => typeof e === 'number' ? e : (isArr(e) && e[0] == null ? e[1] : null)
88
+
89
+ const isPositive = (e, F) => {
90
+ const c = cval(e); if (c != null) return c > 0
91
+ if (isName(e)) return F.positive.has(e)
92
+ if (isArr(e) && (e[0] === '+' || e[0] === '*') && e.length === 3) return isPositive(e[1], F) && isPositive(e[2], F)
93
+ return false
94
+ }
95
+
96
+ const asBasePlus = (e, F) => {
97
+ if (isName(e) && F.def.has(e)) e = F.def.get(e)
98
+ if (isArr(e) && e[0] === '+' && e.length === 3) return { base: e[1], off: e[2] }
99
+ return null
100
+ }
101
+
102
+ function provablyDiffer(idx, idx2, F) {
103
+ const ka = idxKey(idx), kb = idxKey(idx2)
104
+ if (ka === kb) return false
105
+ const va = cval(idx), vb = cval(idx2)
106
+ if (va != null && vb != null) return va !== vb
107
+ const bp2 = asBasePlus(idx2, F); if (bp2 && idxKey(bp2.base) === ka && isPositive(bp2.off, F)) return true
108
+ const bp1 = asBasePlus(idx, F); if (bp1 && idxKey(bp1.base) === kb && isPositive(bp1.off, F)) return true
109
+ return false
110
+ }
111
+
112
+ /**
113
+ * @param body function-body AST (mutated in place)
114
+ * @param isTypedArray (name) => boolean — receiver is a pure typed-array load
115
+ * @param freshName () => string — unique temp local name
116
+ * @returns number of loads eliminated
117
+ */
118
+ export function cseLoads(body, isTypedArray, freshName) {
119
+ if (!isArr(body)) return 0
120
+ const F = buildFacts(body)
121
+ let eliminated = 0
122
+
123
+ // Process the statement list of a `[';', …]` node (children [1..]).
124
+ const runSeq = (seq) => {
125
+ // key → { arr, idxNode, idxVars, temp, firstParent, firstIdx, firstStmt }
126
+ const avail = new Map()
127
+ const inserts = [] // { at: stmtIdx, binding }
128
+
129
+ const flush = () => avail.clear()
130
+ const invalidateVar = (name) => { for (const [k, e] of avail) if (e.arr === name || e.idxVars.has(name)) avail.delete(k) }
131
+
132
+ const reads = (node, parent, pi, si) => {
133
+ if (!isArr(node) || node[0] === 'str') return
134
+ // Element/member assignment targets must stay targets. Plain `=` does not
135
+ // read the slot; compound/update ops do, but rewriting the LHS node itself
136
+ // turns the eventual store into a temp assignment. Only inspect receiver /
137
+ // index subexpressions here, then let the RHS participate in load CSE.
138
+ if (ASSIGN.has(node[0]) && isArr(node[1]) && (node[1][0] === '[]' || node[1][0] === '.' || node[1][0] === '?.')) {
139
+ const lhs = node[1]
140
+ reads(lhs[0] === '[]' ? lhs[2] : lhs[1], lhs, lhs[0] === '[]' ? 2 : 1, si) // index / receiver
141
+ for (let i = 2; i < node.length; i++) reads(node[i], node, i, si) // rhs value
142
+ return
143
+ }
144
+ if (node[0] === '[]' && isName(node[1]) && isTypedArray(node[1]) && stableIdx(node[2])) {
145
+ const arr = node[1], key = `${arr}|${idxKey(node[2])}`
146
+ const e = avail.get(key)
147
+ if (e) {
148
+ if (e.temp === null) {
149
+ e.temp = freshName()
150
+ inserts.push({ at: e.firstStmt, binding: ['let', ['=', e.temp, ['[]', arr, e.idxNode]]] })
151
+ e.firstParent[e.firstIdx] = e.temp // rewrite the 1st occurrence to read the temp
152
+ }
153
+ parent[pi] = e.temp // rewrite this (2nd+) occurrence
154
+ eliminated++
155
+ return
156
+ }
157
+ const vars = new Set(); idxVars(node[2], vars)
158
+ avail.set(key, { arr, idxNode: node[2], idxVars: vars, temp: null, firstParent: parent, firstIdx: pi, firstStmt: si })
159
+ return // don't descend into a stable index
160
+ }
161
+ if (node[0] === '()' || node[0] === 'call') { flush(); for (let i = 1; i < node.length; i++) reads(node[i], node, i, si); return }
162
+ for (let i = 1; i < node.length; i++) reads(node[i], node, i, si)
163
+ }
164
+
165
+ const writes = (node) => {
166
+ if (!isArr(node)) return
167
+ const op = node[0]
168
+ if (ASSIGN.has(op)) {
169
+ const lhs = node[1]
170
+ if (isName(lhs)) invalidateVar(lhs)
171
+ else if (isArr(lhs) && lhs[0] === '[]') {
172
+ const idx2 = lhs[2]
173
+ for (const [k, e] of [...avail]) if (!provablyDiffer(e.idxNode, idx2, F)) avail.delete(k)
174
+ }
175
+ for (let i = 2; i < node.length; i++) writes(node[i])
176
+ return
177
+ }
178
+ for (let i = 1; i < node.length; i++) writes(node[i])
179
+ }
180
+
181
+ for (let si = 1; si < seq.length; si++) {
182
+ const s = seq[si]
183
+ if (!isArr(s)) continue
184
+ if (CONTROL.has(s[0])) { flush(); continue } // nesting handled by the outer `descend`
185
+ reads(s, seq, si, si)
186
+ writes(s)
187
+ }
188
+ inserts.sort((a, b) => b.at - a.at)
189
+ for (const ins of inserts) seq.splice(ins.at, 0, ins.binding)
190
+ }
191
+
192
+ // Walk to every `[';', …]` sequence; run CSE on each, then recurse into its (now-rewritten) stmts.
193
+ const descend = (node) => {
194
+ if (!isArr(node)) return
195
+ if (node[0] === ';') runSeq(node)
196
+ for (let i = 1; i < node.length; i++) descend(node[i])
197
+ }
198
+ descend(body)
199
+ return eliminated
200
+ }
@@ -10,9 +10,9 @@
10
10
  * @module compile/emit-assign
11
11
  */
12
12
 
13
- import { ctx, err, inc, PTR } from '../ctx.js'
13
+ import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
14
14
  import { T } from '../ast.js'
15
- import { staticPropertyKey } from '../static.js'
15
+ import { staticPropertyKey, staticIndexKey } from '../static.js'
16
16
  import { valTypeOf, shapeOf } from '../kind.js'
17
17
  import { VAL, lookupValType, repOf } from '../reps.js'
18
18
  import {
@@ -74,8 +74,9 @@ function storeArrayPayload(arrExpr, idxNode, valueExpr, persist) {
74
74
  /** Strict-mode guard for dynamic property writes — emitted in branches that
75
75
  * fall through to `__dyn_set` or its key-kind dispatch. */
76
76
  function ensureDynSetAllowed(arr) {
77
- if (!ctx.transform.strict) return
78
77
  const arrLabel = typeof arr === 'string' ? arr : '<expr>'
78
+ warnDeopt('deopt-dyn-write', `dynamic property write \`${arrLabel}[…] = …\` couldn't resolve a static type — it falls back to a runtime hash store (~2× slower than a typed/slot write, far worse in a hot loop). Use a literal key, a numeric typed-array index, or a Map for genuinely dynamic keys.`)
79
+ if (!ctx.transform.strict) return
79
80
  err(`strict mode: dynamic property assignment \`${arrLabel}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
80
81
  }
81
82
 
@@ -98,26 +99,54 @@ function dispatchByKeyKind(arr, keyExpr, valueExpr, numericIR) {
98
99
  ['else', numericIR(['local.get', `$${keyTmp}`])]])
99
100
  }
100
101
 
102
+ /** Raw indexed f64.store at `ptrOffset(o)+idx*8` — the lean fallback for a receiver
103
+ * proven to be ARRAY/TYPED at runtime (the ARRAY/TYPED forks are taken first).
104
+ * Operands are pre-set locals: `$obj` f64, `$idx` i32, `$val` f64. */
105
+ const rawIndexedStore = (obj, idx, val, arrVT) => ['block', ['result', 'f64'],
106
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${obj}`], arrVT), ['i32.shl', ['local.get', `$${idx}`], ['i32.const', 3]]], ['local.get', `$${val}`]],
107
+ ['local.get', `$${val}`]]
108
+
109
+ /** Numeric element store for a receiver that may be OBJECT/HASH at runtime: those
110
+ * keep dynamic indexed props in the propsPtr HASH sidecar at off-16 (paired with
111
+ * __dyn_get); a raw store at ptrOffset(o)+i*8 lands in the schema-slot region —
112
+ * silent corruption at small i, an OOB trap at large i (the self-host `blur`
113
+ * crash). The propsPtr hash is STRING-keyed (object keys are strings: `o[3]` ≡
114
+ * `o["3"]`), so the index is rendered to its string form — the same string the
115
+ * __dyn_get read produces, content-compared by the hash. ARRAY/TYPED fall to the
116
+ * raw store. Caller must `inc('__dyn_set','__i32_to_str')`. Gated by `mayBeObject`
117
+ * so pure typed-array programs (an f64 param indexed in a hot loop) keep the lean
118
+ * raw store and never drag in __dyn_set / __i32_to_str. */
119
+ const objHashOrRawStore = (obj, idx, val, arrVT) => ['if', ['result', 'f64'],
120
+ ['i32.or', ptrTypeEq(['local.get', `$${obj}`], PTR.OBJECT), ptrTypeEq(['local.get', `$${obj}`], PTR.HASH)],
121
+ ['then', ['block', ['result', 'f64'],
122
+ ['drop', ['call', '$__dyn_set',
123
+ ['i64.reinterpret_f64', ['local.get', `$${obj}`]],
124
+ ['i64.reinterpret_f64', ['call', '$__i32_to_str', ['local.get', `$${idx}`]]],
125
+ ['i64.reinterpret_f64', ['local.get', `$${val}`]]]],
126
+ ['local.get', `$${val}`]]],
127
+ ['else', rawIndexedStore(obj, idx, val, arrVT)]]
128
+
101
129
  /** Build a `__ptr_type`-fork IR for `arr[idx] = val` when receiver is opaque
102
130
  * (non-string expr, or string-named binding of unknown VAL). Forks on
103
131
  * ARRAY → `__arr_set_idx_ptr` (+ optional persist), TYPED → `__typed_set_idx`,
104
- * elseraw f64.store at the OBJECT/HASH payload offset. */
105
- function emitPolymorphicElementStore(arrExpr, idxI32, valueExpr, arrVT, persist) {
132
+ * OBJECT/HASH`__dyn_set` (only when `mayBeObject`), else raw f64.store. */
133
+ function emitPolymorphicElementStore(arrExpr, idxI32, valueExpr, arrVT, persist, mayBeObject) {
106
134
  const objTmp = temp('asu')
107
135
  const idxTmp = tempI32('asi')
108
136
  const ptrTmp = temp('asp')
109
137
  const valTmp = temp()
110
138
  const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
111
139
  inc('__ptr_type', '__arr_set_idx_ptr')
140
+ if (mayBeObject) inc('__dyn_set', '__i32_to_str')
112
141
  if (hasTypedSet) inc('__typed_set_idx')
113
142
  const arrSetCall = ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]
114
143
  const arrayBranch = ['block', ['result', 'f64'],
115
144
  ['local.set', `$${ptrTmp}`, arrSetCall],
116
145
  ...(persist ? [persist(['local.get', `$${ptrTmp}`])] : []),
117
146
  ['local.get', `$${valTmp}`]]
118
- const fallbackStore = ['block', ['result', 'f64'],
119
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
120
- ['local.get', `$${valTmp}`]]
147
+ const fallbackStore = mayBeObject
148
+ ? objHashOrRawStore(objTmp, idxTmp, valTmp, arrVT)
149
+ : rawIndexedStore(objTmp, idxTmp, valTmp, arrVT)
121
150
  const elseBranch = hasTypedSet
122
151
  ? ['if', ['result', 'f64'],
123
152
  ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
@@ -159,10 +188,12 @@ export function emitElementAssign(arr, idx, val) {
159
188
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
160
189
  : null
161
190
 
162
- // 1. SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
163
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
191
+ // 1. SRoA flat object/array: `o['k'] = x` / `a[2] = x` → `local.set $o#i` (no heap
192
+ // store). A bare integer index resolves its slot key here (not via `litKey`).
193
+ if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
164
194
  const fo = ctx.func.flatObjects.get(arr)
165
- const fi = fo.names.indexOf(litKey)
195
+ const flatKey = litKey != null ? litKey : staticIndexKey(idx)
196
+ const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
166
197
  if (fi >= 0) return withTemp(valueExpr, t => [
167
198
  ['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
168
199
  ['local.get', `$${t}`]])
@@ -183,8 +214,13 @@ export function emitElementAssign(arr, idx, val) {
183
214
  if (keyType === VAL.STRING) return dynSetCall(arr, keyExpr, valueExpr)
184
215
 
185
216
  // 5. Typed-array receiver → __typed_set_idx (or per-ctor element write).
186
- if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
187
- lookupValType(arr) === 'typed') {
217
+ // Also fires for a nested `arr[c]` receiver whose array's elements are typed
218
+ // arrays of a known ctor (codec `ch[c][i] = …` channelData scatter) — the
219
+ // `.typed:[]=` emitter resolves the element ctor and inlines the store.
220
+ const nestedElemTypedCtor = Array.isArray(arr) && arr[0] === '[]' && arr.length === 3 &&
221
+ typeof arr[1] === 'string' ? repOf(arr[1])?.arrayElemTypedCtor : null
222
+ if (ctx.core.emit['.typed:[]='] &&
223
+ ((typeof arr === 'string' && lookupValType(arr) === 'typed') || nestedElemTypedCtor)) {
188
224
  const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
189
225
  if (r) return r
190
226
  // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
@@ -221,6 +257,28 @@ export function emitElementAssign(arr, idx, val) {
221
257
  const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
222
258
  const arrVT = knownArrVT || VAL.OBJECT
223
259
 
260
+ // 7b. Known-OBJECT receiver with a non-static key. The schema-slot (step 2) and
261
+ // string-key (step 4) fast paths already returned, so the key here is a numeric
262
+ // or runtime-unknown expression. A raw indexed f64.store (steps 8–9 / the default
263
+ // below) would compute `ptrOffset(o) + i*8` into an allocation sized for schema
264
+ // slots only — silent slot corruption at small i, an out-of-bounds memory trap at
265
+ // large i. Route to __dyn_set (the per-OBJECT propsPtr hash sidecar), mirroring
266
+ // emitPropertyAssign's OBJECT dot-write path; __dyn_get reads it back. This closes
267
+ // the `o.prop=v` vs `o[expr]=v` asymmetry that faulted the self-host.
268
+ if (knownArrVT === VAL.OBJECT) return dynSetCall(arr, keyExpr, valueExpr)
269
+
270
+ // A receiver "may be an OBJECT/HASH at runtime" unless the analyzer has proven it
271
+ // is an indexable array/typed candidate (`rep.notString`, set by infer.js for
272
+ // bindings used as `x[i]` array/typed receivers — e.g. a Float64Array param `buf`).
273
+ // Those keep the lean raw-store path and never drag in __dyn_set / __i32_to_str
274
+ // (which carry their own rehash/itoa loops — a real size + hot-loop-ratchet cost).
275
+ // Everything else (an object-literal local `let o = {}` — the Root A′ case — or a
276
+ // fully-opaque expression) gets the OBJECT-safe fork that routes to the propsPtr
277
+ // sidecar instead of an out-of-bounds raw f64.store.
278
+ const mayBeObject = typeof arr !== 'string' || repOf(arr)?.notString !== true
279
+ // OBJECT-safe numeric store when the receiver may be an object, else the lean raw store.
280
+ const numStore = (o, i, v) => mayBeObject ? objHashOrRawStore(o, i, v, arrVT) : rawIndexedStore(o, i, v, arrVT)
281
+
224
282
  // 8. Polymorphic + runtime key dispatch — key kind unknown AND receiver shape
225
283
  // possibly TypedArray (or fully opaque). Numeric branch forks on __ptr_type.
226
284
  // Deliberately a 2-fork (TYPED vs else) rather than reusing
@@ -235,6 +293,7 @@ export function emitElementAssign(arr, idx, val) {
235
293
  const idxTmp = tempI32('asi')
236
294
  const valTmp = temp()
237
295
  inc('__ptr_type', '__typed_set_idx')
296
+ if (mayBeObject) inc('__i32_to_str')
238
297
  // When arr type is unknown (could be TypedArray) and __typed_set_idx is
239
298
  // available, dispatch the numeric branch through __ptr_type so TypedArray
240
299
  // writes go by element type. Without this, ternary-typed arrays (e.g.
@@ -247,23 +306,26 @@ export function emitElementAssign(arr, idx, val) {
247
306
  ['if', ['result', 'f64'],
248
307
  ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
249
308
  ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
250
- ['else', ['block', ['result', 'f64'],
251
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
252
- ['local.get', `$${valTmp}`]]]]])
309
+ ['else', numStore(objTmp, idxTmp, valTmp)]]])
253
310
  }
311
+ const objTmpB = temp('asu')
312
+ const idxTmpB = tempI32('asi')
254
313
  const valTmp = temp()
314
+ inc('__ptr_type')
315
+ if (mayBeObject) inc('__i32_to_str')
255
316
  return dispatchByKeyKind(arr, keyExpr, valueExpr, keyNode => ['block', ['result', 'f64'],
317
+ ['local.set', `$${objTmpB}`, asF64(emit(arr))],
318
+ ['local.set', `$${idxTmpB}`, asI32(typed(keyNode, 'f64'))],
256
319
  ['local.set', `$${valTmp}`, valueExpr],
257
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(emit(arr)), arrVT), ['i32.shl', asI32(typed(keyNode, 'f64')), ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
258
- ['local.get', `$${valTmp}`]])
320
+ numStore(objTmpB, idxTmpB, valTmp)])
259
321
  }
260
322
 
261
323
  // 9. Opaque receiver (non-string expr) or string-named with unknown VT — pure
262
324
  // __ptr_type dispatch (no key-kind fork: key is provably numeric here).
263
325
  if (typeof arr !== 'string')
264
- return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, null)
326
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, null, mayBeObject)
265
327
  if (knownArrVT == null)
266
- return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, persistBinding(arr))
328
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, persistBinding(arr), mayBeObject)
267
329
 
268
330
  // Default: known-VT receiver that isn't ARRAY/TYPED/OBJECT special — raw f64.store.
269
331
  return withTemp(valueExpr, t => [