jz 0.7.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 (53) hide show
  1. package/README.md +37 -33
  2. package/bench/README.md +176 -73
  3. package/bench/bench.svg +58 -71
  4. package/cli.js +12 -5
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +1156 -984
  7. package/index.js +40 -9
  8. package/interop.js +193 -138
  9. package/layout.js +29 -18
  10. package/module/array.js +29 -73
  11. package/module/collection.js +83 -25
  12. package/module/console.js +1 -1
  13. package/module/core.js +161 -15
  14. package/module/json.js +3 -3
  15. package/module/math.js +26 -3
  16. package/module/number.js +247 -13
  17. package/module/object.js +11 -5
  18. package/module/regex.js +8 -7
  19. package/module/string.js +162 -156
  20. package/module/typedarray.js +169 -105
  21. package/package.json +7 -3
  22. package/src/abi/string.js +29 -29
  23. package/src/ast.js +19 -2
  24. package/src/compile/analyze.js +64 -2
  25. package/src/compile/cse-load.js +200 -0
  26. package/src/compile/emit-assign.js +73 -14
  27. package/src/compile/emit.js +253 -23
  28. package/src/compile/index.js +198 -61
  29. package/src/compile/loop-divmod.js +12 -58
  30. package/src/compile/loop-model.js +91 -0
  31. package/src/compile/loop-square.js +102 -0
  32. package/src/compile/narrow.js +169 -32
  33. package/src/compile/peel-stencil.js +18 -64
  34. package/src/compile/plan/common.js +29 -0
  35. package/src/compile/plan/index.js +4 -1
  36. package/src/compile/plan/inline.js +176 -21
  37. package/src/compile/plan/literals.js +93 -19
  38. package/src/ctx.js +51 -12
  39. package/src/helper-counters.js +137 -0
  40. package/src/ir.js +102 -13
  41. package/src/kind-traits.js +7 -3
  42. package/src/kind.js +14 -1
  43. package/src/op-policy.js +5 -2
  44. package/src/ops.js +119 -0
  45. package/src/optimize/index.js +960 -136
  46. package/src/optimize/recurse.js +182 -0
  47. package/src/optimize/vectorize.js +1292 -144
  48. package/src/prepare/index.js +11 -7
  49. package/src/reps.js +4 -1
  50. package/src/type.js +53 -45
  51. package/src/wat/assemble.js +92 -9
  52. package/src/widen.js +21 -0
  53. package/src/wat/optimize.js +0 -3938
@@ -118,6 +118,11 @@ 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)
@@ -168,7 +173,7 @@ export function analyzeBody(body) {
168
173
  // for any slice and can't be WeakMap-keyed. Return empty maps without caching.
169
174
  if (body === null || typeof body !== 'object') return {
170
175
  locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
171
- arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
176
+ arrElemValTypes: new Map(), arrElemTypedCtors: new Map(), typedElems: new Map(), escapes: new Map(),
172
177
  flatObjects: new Map(),
173
178
  }
174
179
  const hit = _bodyFactsCache.get(body)
@@ -184,6 +189,10 @@ export function analyzeBody(body) {
184
189
  // so `chord[j]` is a Number and skips __to_num. Single-level only — enough for the
185
190
  // 2-D table pattern without a general nested-type lattice.
186
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()
187
196
  const typedElems = new Map()
188
197
  const escapes = new Map() // name → bool: local holds allocation, true if it escapes
189
198
 
@@ -224,6 +233,22 @@ export function analyzeBody(body) {
224
233
  return arrElemValTypes.get(name) || null
225
234
  }
226
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
+
227
252
  const exprElemSourceVal = (expr) => {
228
253
  if (typeof expr === 'string') {
229
254
  const repVt = ctx.func.localReps?.get(expr)?.val
@@ -315,6 +340,14 @@ export function analyzeBody(body) {
315
340
  if (exprElemSourceVal(elems[k]) !== common) common = null
316
341
  }
317
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
+ }
318
351
  // Array-of-arrays literal: record the common element-of-element kind so a
319
352
  // later `x = name[i]` binds `x`'s element type one level down.
320
353
  if (common === VAL.ARRAY) {
@@ -341,6 +374,18 @@ export function analyzeBody(body) {
341
374
  const f = ctx.func.map?.get(rhs[1])
342
375
  if (f?.arrayElemValType) observeArrValType(name, f.arrayElemValType)
343
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
+ }
344
389
  if (typeof rhs === 'string') {
345
390
  const v = elemValOf(rhs)
346
391
  if (v) observeArrValType(name, v)
@@ -469,9 +514,20 @@ export function analyzeBody(body) {
469
514
  }
470
515
  observeArrSchema(arr, exprSchemaId(a, localSchemaMap))
471
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))
472
520
  }
473
521
  }
474
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
+
475
531
  // `=` reassignment — locals widen, valTypes/typedElems track,
476
532
  // arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
477
533
  if (op === '=' && typeof node[1] === 'string') {
@@ -485,6 +541,7 @@ export function analyzeBody(body) {
485
541
  trackTyped(name, rhs)
486
542
  if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
487
543
  if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
544
+ if (arrElemTypedCtors.has(name) && !isArrayProducingRhs(rhs)) observeArrTypedCtor(name, null)
488
545
  return
489
546
  }
490
547
 
@@ -573,7 +630,7 @@ export function analyzeBody(body) {
573
630
  // Never-relocated array bindings — reads may skip the realloc-forwarding follow.
574
631
  const neverGrown = doSchemas ? scanNeverGrown(body) : new Set()
575
632
 
576
- const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
633
+ const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, arrElemTypedCtors, typedElems, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
577
634
  _bodyFactsCache.set(body, result)
578
635
  return result
579
636
  }
@@ -705,6 +762,11 @@ export function analyzeValTypes(body) {
705
762
  for (const [name, vt] of facts.arrElemValTypes) {
706
763
  if (vt != null) updateRep(name, { arrayElemValType: vt })
707
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
+ }
708
770
  // Construct-then-fill numeric arrays (`let a = Array(n); a[i] = expr`) carry no
709
771
  // element evidence at their decl, so the walk above leaves them untyped. scanNumericFill
710
772
  // proved every write Numeric and every other use a pure read — record NUMBER so `arr[i]`
@@ -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
+ }
@@ -99,26 +99,54 @@ function dispatchByKeyKind(arr, keyExpr, valueExpr, numericIR) {
99
99
  ['else', numericIR(['local.get', `$${keyTmp}`])]])
100
100
  }
101
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
+
102
129
  /** Build a `__ptr_type`-fork IR for `arr[idx] = val` when receiver is opaque
103
130
  * (non-string expr, or string-named binding of unknown VAL). Forks on
104
131
  * ARRAY → `__arr_set_idx_ptr` (+ optional persist), TYPED → `__typed_set_idx`,
105
- * elseraw f64.store at the OBJECT/HASH payload offset. */
106
- 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) {
107
134
  const objTmp = temp('asu')
108
135
  const idxTmp = tempI32('asi')
109
136
  const ptrTmp = temp('asp')
110
137
  const valTmp = temp()
111
138
  const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
112
139
  inc('__ptr_type', '__arr_set_idx_ptr')
140
+ if (mayBeObject) inc('__dyn_set', '__i32_to_str')
113
141
  if (hasTypedSet) inc('__typed_set_idx')
114
142
  const arrSetCall = ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]
115
143
  const arrayBranch = ['block', ['result', 'f64'],
116
144
  ['local.set', `$${ptrTmp}`, arrSetCall],
117
145
  ...(persist ? [persist(['local.get', `$${ptrTmp}`])] : []),
118
146
  ['local.get', `$${valTmp}`]]
119
- const fallbackStore = ['block', ['result', 'f64'],
120
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
121
- ['local.get', `$${valTmp}`]]
147
+ const fallbackStore = mayBeObject
148
+ ? objHashOrRawStore(objTmp, idxTmp, valTmp, arrVT)
149
+ : rawIndexedStore(objTmp, idxTmp, valTmp, arrVT)
122
150
  const elseBranch = hasTypedSet
123
151
  ? ['if', ['result', 'f64'],
124
152
  ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
@@ -186,8 +214,13 @@ export function emitElementAssign(arr, idx, val) {
186
214
  if (keyType === VAL.STRING) return dynSetCall(arr, keyExpr, valueExpr)
187
215
 
188
216
  // 5. Typed-array receiver → __typed_set_idx (or per-ctor element write).
189
- if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
190
- 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)) {
191
224
  const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
192
225
  if (r) return r
193
226
  // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
@@ -224,6 +257,28 @@ export function emitElementAssign(arr, idx, val) {
224
257
  const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
225
258
  const arrVT = knownArrVT || VAL.OBJECT
226
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
+
227
282
  // 8. Polymorphic + runtime key dispatch — key kind unknown AND receiver shape
228
283
  // possibly TypedArray (or fully opaque). Numeric branch forks on __ptr_type.
229
284
  // Deliberately a 2-fork (TYPED vs else) rather than reusing
@@ -238,6 +293,7 @@ export function emitElementAssign(arr, idx, val) {
238
293
  const idxTmp = tempI32('asi')
239
294
  const valTmp = temp()
240
295
  inc('__ptr_type', '__typed_set_idx')
296
+ if (mayBeObject) inc('__i32_to_str')
241
297
  // When arr type is unknown (could be TypedArray) and __typed_set_idx is
242
298
  // available, dispatch the numeric branch through __ptr_type so TypedArray
243
299
  // writes go by element type. Without this, ternary-typed arrays (e.g.
@@ -250,23 +306,26 @@ export function emitElementAssign(arr, idx, val) {
250
306
  ['if', ['result', 'f64'],
251
307
  ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
252
308
  ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
253
- ['else', ['block', ['result', 'f64'],
254
- ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
255
- ['local.get', `$${valTmp}`]]]]])
309
+ ['else', numStore(objTmp, idxTmp, valTmp)]]])
256
310
  }
311
+ const objTmpB = temp('asu')
312
+ const idxTmpB = tempI32('asi')
257
313
  const valTmp = temp()
314
+ inc('__ptr_type')
315
+ if (mayBeObject) inc('__i32_to_str')
258
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'))],
259
319
  ['local.set', `$${valTmp}`, valueExpr],
260
- ['f64.store', ['i32.add', ptrOffsetIR(asF64(emit(arr)), arrVT), ['i32.shl', asI32(typed(keyNode, 'f64')), ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
261
- ['local.get', `$${valTmp}`]])
320
+ numStore(objTmpB, idxTmpB, valTmp)])
262
321
  }
263
322
 
264
323
  // 9. Opaque receiver (non-string expr) or string-named with unknown VT — pure
265
324
  // __ptr_type dispatch (no key-kind fork: key is provably numeric here).
266
325
  if (typeof arr !== 'string')
267
- return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, null)
326
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, null, mayBeObject)
268
327
  if (knownArrVT == null)
269
- return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, persistBinding(arr))
328
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, persistBinding(arr), mayBeObject)
270
329
 
271
330
  // Default: known-VT receiver that isn't ARRAY/TYPED/OBJECT special — raw f64.store.
272
331
  return withTemp(valueExpr, t => [