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.
- package/README.md +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1156 -984
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +29 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +26 -3
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +162 -156
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +253 -23
- package/src/compile/index.js +198 -61
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +169 -32
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +960 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1292 -144
- package/src/prepare/index.js +11 -7
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/src/compile/analyze.js
CHANGED
|
@@ -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
|
-
*
|
|
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 =
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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',
|
|
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
|
-
|
|
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 => [
|