jz 0.7.0 → 0.8.1
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 +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -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 +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- 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 +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- 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 +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- 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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Partial unroll (×2) + scalar replacement of a unit-stride ARRAY RECURRENCE.
|
|
2
|
+
//
|
|
3
|
+
// A DP/scan loop that reads `arr[j-1]` and writes `arr[j]` (unit stride) carries the
|
|
4
|
+
// just-written value to the next iteration THROUGH MEMORY: it stores `arr[j]`, then the next
|
|
5
|
+
// iteration loads `arr[j-1]` — the very cell it just wrote. V8/TurboFan forwards that store→load
|
|
6
|
+
// and unrolls the loop internally; Cranelift/wasmtime and the baseline tiers do neither, so the
|
|
7
|
+
// loop pays a store→load round trip plus full per-iteration overhead on every cell. clang/gcc
|
|
8
|
+
// fix exactly this with this transform — measured 2.15× on wasmtime for the Levenshtein DP
|
|
9
|
+
// (V8-neutral, bit-exact).
|
|
10
|
+
//
|
|
11
|
+
// Recognized (post-prepare AST): a unit-stride `for (let j = LO; j </<= HI; j++)` whose body, for
|
|
12
|
+
// ONE array `arr`, has a single store `arr[j] = <var>` and ≥1 read `arr[j-1]`, accesses `arr` at
|
|
13
|
+
// no other index, never aliases `arr` elsewhere, and contains no call / nested loop / break /
|
|
14
|
+
// continue / return / closure. The `arr[j-1]` read becomes a scalar `left` seeded from `arr[LO-1]`
|
|
15
|
+
// and refreshed after each store; the body is then unrolled ×2 (with a 1-cell tail) so the carry
|
|
16
|
+
// between the paired cells lives in a register and the loop overhead is halved. A `LO <= HI` guard
|
|
17
|
+
// keeps the seed load in step with the original (which reads `arr[LO-1]` only when it iterates),
|
|
18
|
+
// and falls back to the untouched loop on the empty range — sound for any trip count.
|
|
19
|
+
|
|
20
|
+
import { findMutations } from './analyze-scans.js'
|
|
21
|
+
import { litVal, litN, unitIncVar, normalizeLoop, freshLoopId } from './loop-model.js'
|
|
22
|
+
import { rewriteBlocks, closureMutatedVars } from './loop-model.js'
|
|
23
|
+
|
|
24
|
+
const isArr = (n) => Array.isArray(n) // wrap (not alias): the self-host kernel rejects a builtin used as a first-class value
|
|
25
|
+
const clone = (n) => isArr(n) ? n.map(clone) : n
|
|
26
|
+
const isIvMinus1 = (n, iv) => isArr(n) && n[0] === '-' && n[1] === iv && litN(n[2], 1) // (iv - 1)
|
|
27
|
+
|
|
28
|
+
// Ops whose presence makes duplicating the body in place unsound (control that escapes the cell,
|
|
29
|
+
// or a call that could alias/mutate `arr` or reorder side effects).
|
|
30
|
+
const REJECT = new Set(['for', 'while', 'do', 'for-in', 'for-of', 'break', 'continue', 'return',
|
|
31
|
+
'throw', 'switch', 'try', 'catch', 'finally', '=>', 'label'])
|
|
32
|
+
const hasUnsafe = (n) => {
|
|
33
|
+
if (!isArr(n)) return false
|
|
34
|
+
if (REJECT.has(n[0])) return true
|
|
35
|
+
if (n[0] === '()' && typeof n[1] === 'string') return true // function call `f(args)`
|
|
36
|
+
return n.some(hasUnsafe)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Substitute every value-reference of `iv` with (iv + 1); leave the op slot and property keys.
|
|
40
|
+
const subPlus1 = (n, iv) => {
|
|
41
|
+
if (n === iv) return ['+', iv, 1]
|
|
42
|
+
if (!isArr(n)) return n
|
|
43
|
+
if (n[0] === '.' && n.length === 3) return ['.', subPlus1(n[1], iv), n[2]]
|
|
44
|
+
return [n[0], ...n.slice(1).map(c => subPlus1(c, iv))]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rename every let/const-DECLARED var in `stmts` with a suffix, throughout — so the 2nd unrolled
|
|
48
|
+
// cell's locals don't collide with the 1st. Loop-carried outer vars (assigned, not declared here)
|
|
49
|
+
// are untouched, so the recurrence still threads through them.
|
|
50
|
+
function renameDecls(stmts, suf) {
|
|
51
|
+
const declared = new Set()
|
|
52
|
+
const collect = (n) => {
|
|
53
|
+
if (!isArr(n)) return
|
|
54
|
+
if (n[0] === 'let' || n[0] === 'const')
|
|
55
|
+
for (let k = 1; k < n.length; k++) if (isArr(n[k]) && n[k][0] === '=' && typeof n[k][1] === 'string') declared.add(n[k][1])
|
|
56
|
+
n.forEach(collect)
|
|
57
|
+
}
|
|
58
|
+
stmts.forEach(collect)
|
|
59
|
+
if (!declared.size) return stmts
|
|
60
|
+
const ren = (n) => {
|
|
61
|
+
if (typeof n === 'string') return declared.has(n) ? n + suf : n
|
|
62
|
+
if (!isArr(n)) return n
|
|
63
|
+
if (n[0] === '.' && n.length === 3) return ['.', ren(n[1]), n[2]]
|
|
64
|
+
return [n[0], ...n.slice(1).map(ren)]
|
|
65
|
+
}
|
|
66
|
+
return stmts.map(ren)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Replace `arr[iv-1]` reads with `left`; keep the store; emit `left = storeVal` after each store.
|
|
70
|
+
function scalarReplace(stmts, arr, iv, left, storeVal) {
|
|
71
|
+
const repl = (n) => {
|
|
72
|
+
if (!isArr(n)) return n
|
|
73
|
+
if (n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) return left
|
|
74
|
+
if (n[0] === '.' && n.length === 3) return ['.', repl(n[1]), n[2]]
|
|
75
|
+
return [n[0], ...n.slice(1).map(repl)]
|
|
76
|
+
}
|
|
77
|
+
const out = []
|
|
78
|
+
for (const s of stmts) {
|
|
79
|
+
out.push(repl(s))
|
|
80
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
81
|
+
out.push(['=', left, storeVal])
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function tryUnroll(stmt, cm) {
|
|
87
|
+
const L = normalizeLoop(stmt)
|
|
88
|
+
if (!L || L.kind !== 'for') return null
|
|
89
|
+
const body = L.body
|
|
90
|
+
if (!isArr(body) || body[0] !== ';') return null
|
|
91
|
+
const iv = unitIncVar(L.step)
|
|
92
|
+
if (!iv) return null
|
|
93
|
+
|
|
94
|
+
// init `let iv = LO`, LO a literal ≥ 1 (so arr[LO-1] is a valid in-bounds index)
|
|
95
|
+
if (!(isArr(L.init) && L.init[0] === 'let' && isArr(L.init[1]) && L.init[1][0] === '=' && L.init[1][1] === iv)) return null
|
|
96
|
+
const LO = L.init[1][2], loVal = litVal(LO)
|
|
97
|
+
if (loVal == null || loVal < 1) return null
|
|
98
|
+
|
|
99
|
+
// cond `iv <= HI` / `iv < HI`, HI loop-invariant
|
|
100
|
+
if (!(isArr(L.cond) && (L.cond[0] === '<=' || L.cond[0] === '<') && L.cond[1] === iv)) return null
|
|
101
|
+
const cmpOp = L.cond[0], HI = L.cond[2]
|
|
102
|
+
if (!(typeof HI === 'string' || litVal(HI) != null)) return null
|
|
103
|
+
|
|
104
|
+
if (hasUnsafe(body)) return null
|
|
105
|
+
const stmts = body.slice(1)
|
|
106
|
+
|
|
107
|
+
// exactly one store `arr[iv] = <var>` — the recurrence array + carried value
|
|
108
|
+
let arr = null, storeVal = null, nStore = 0
|
|
109
|
+
for (const s of stmts)
|
|
110
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][2] === iv) {
|
|
111
|
+
if (typeof s[2] !== 'string') return null
|
|
112
|
+
arr = s[1][1]; storeVal = s[2]; nStore++
|
|
113
|
+
}
|
|
114
|
+
if (nStore !== 1 || typeof arr !== 'string') return null
|
|
115
|
+
if (storeVal === iv || storeVal === arr) return null
|
|
116
|
+
|
|
117
|
+
// every `arr[...]` is `arr[iv]` or `arr[iv-1]`, the only write is the store, ≥1 recurrence read,
|
|
118
|
+
// and `arr` never appears bare (passed/aliased)
|
|
119
|
+
let hasRec = false, bad = false
|
|
120
|
+
const scan = (n) => {
|
|
121
|
+
if (!isArr(n)) return
|
|
122
|
+
if (n[0] === '[]' && n[1] === arr) {
|
|
123
|
+
if (n[2] === iv) {} else if (isIvMinus1(n[2], iv)) hasRec = true; else bad = true
|
|
124
|
+
}
|
|
125
|
+
if (n[0] === '=' && isArr(n[1]) && n[1][0] === '[]' && n[1][1] === arr && n[1][2] !== iv) bad = true
|
|
126
|
+
if (!(n[0] === '[]' || n[0] === '.')) for (let k = 1; k < n.length; k++) if (n[k] === arr) bad = true
|
|
127
|
+
n.forEach(scan)
|
|
128
|
+
}
|
|
129
|
+
scan(body)
|
|
130
|
+
if (bad || !hasRec) return null
|
|
131
|
+
|
|
132
|
+
// The carry `left = storeVal` is emitted right after the store, so a recurrence read AFTER the
|
|
133
|
+
// store would see this cell's value, not arr[iv-1]. Require every arr[iv-1] read to precede it.
|
|
134
|
+
const storeIdx = stmts.findIndex(s => isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
135
|
+
const readsRec = (n) => isArr(n) && ((n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) || n.some(readsRec))
|
|
136
|
+
for (let k = storeIdx + 1; k < stmts.length; k++) if (readsRec(stmts[k])) return null
|
|
137
|
+
|
|
138
|
+
// iv assigned only by the step; iv/arr/HI loop-invariant (not mutated, incl. via a closure call)
|
|
139
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
140
|
+
if (ivMut.has(iv)) return null
|
|
141
|
+
if (cm.has(iv) || cm.has(arr)) return null
|
|
142
|
+
if (typeof HI === 'string') { const hiMut = new Set(); findMutations(body, new Set([HI]), hiMut); if (hiMut.has(HI) || cm.has(HI)) return null }
|
|
143
|
+
|
|
144
|
+
// --- transform ---
|
|
145
|
+
const id = freshLoopId()
|
|
146
|
+
const left = `__rec${id}`
|
|
147
|
+
const bodyS = scalarReplace(stmts, arr, iv, left, storeVal)
|
|
148
|
+
const cellJ = () => bodyS.map(clone)
|
|
149
|
+
const cellJ1 = renameDecls(bodyS.map(s => subPlus1(clone(s), iv)), `$r${id}`)
|
|
150
|
+
|
|
151
|
+
const seed = ['let', ['=', left, ['[]', arr, loVal - 1]]] // left = arr[LO-1]
|
|
152
|
+
const letIv = ['let', ['=', iv, clone(LO)]] // let iv = LO
|
|
153
|
+
const twoFit = cmpOp === '<=' ? ['<', iv, clone(HI)] : ['<', iv, ['-', clone(HI), 1]]
|
|
154
|
+
const main = ['while', twoFit,
|
|
155
|
+
[';', ['{}', [';', ...cellJ()]], ['{}', [';', ...cellJ1]], ['=', iv, ['+', iv, 2]]]]
|
|
156
|
+
const tail = ['if', [cmpOp, iv, clone(HI)],
|
|
157
|
+
['{}', [';', ...cellJ(), ['=', iv, ['+', iv, 1]]]]]
|
|
158
|
+
const block = ['{}', [';', letIv, seed, main, tail]]
|
|
159
|
+
// Run the unrolled form only on a non-empty range (so the seed's arr[LO-1] load matches the
|
|
160
|
+
// original, which reads it only when it iterates); otherwise the untouched loop.
|
|
161
|
+
return [['if', [cmpOp, clone(LO), clone(HI)], block, stmt]]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function unrollRecurrence(body) {
|
|
165
|
+
const cm = closureMutatedVars(body)
|
|
166
|
+
return rewriteBlocks(body, stmt => tryUnroll(stmt, cm))
|
|
167
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Bounded-square narrowing: carry `i*i` as i32 inside a loop guarded by `i*i < CONST`.
|
|
2
|
+
//
|
|
3
|
+
// A PRODUCT `i*i` is f64 in unhinted JS — the integer-overflow contract: a product can
|
|
4
|
+
// exceed 2³¹, where JS keeps a Number, so jz can't blindly use i32.mul (it would wrap).
|
|
5
|
+
// In a Sieve-of-Eratosthenes `for(i=2; i*i<LIMIT; i++) for(j=i*i; j<LIMIT; j+=i) …`, that
|
|
6
|
+
// makes the outer bound, the inner counter `j`'s init, and the whole index chain f64 —
|
|
7
|
+
// each typed-array access then pays an f64→i32 convert.
|
|
8
|
+
//
|
|
9
|
+
// But when the loop GUARD is `i*i < CONST` with CONST a compile-time constant ≤ 2³⁰, the
|
|
10
|
+
// counter is i ≤ ⌈√CONST⌉ ≤ 2¹⁵ throughout the body (the loop is still running, and i is
|
|
11
|
+
// only incremented by +1), so EVERY `i*i` there is < 2³⁰ < 2³¹ and `Math.imul(i,i) == i*i`
|
|
12
|
+
// exactly. The exit overshoot (the first i with i*i ≥ CONST, evaluated in the condition)
|
|
13
|
+
// is ≤ CONST + 2√CONST+1 < 2³¹ for CONST ≤ 2³⁰, so the condition narrows soundly too. The
|
|
14
|
+
// hard cap is 2³⁰ — well under the 2³¹ overflow point, with margin for the overshoot.
|
|
15
|
+
//
|
|
16
|
+
// So we rewrite those `i*i` to `Math.imul(i,i)` (jz's i32 multiply): semantically identical
|
|
17
|
+
// in the proven range, and it lets the EXISTING i32 machinery carry the index chain as i32
|
|
18
|
+
// — the inner `j` (init now i32, step `j+i` i32, bound `j<CONST` i32) cascades on its own.
|
|
19
|
+
//
|
|
20
|
+
// Sound iff: the loop condition is `(i*i) </≤ CONST` (CONST ≤ 2³⁰), and the IV `i` is
|
|
21
|
+
// incremented by +1 and NOT otherwise mutated (so within a body iteration i ∈ {entry, +1},
|
|
22
|
+
// both squares < 2³¹). Mirrors strength-reduce-divmod's structure (post-prepare `while`).
|
|
23
|
+
|
|
24
|
+
import { findMutations } from './analyze-scans.js'
|
|
25
|
+
import { includeMods } from '../autoload.js'
|
|
26
|
+
import { ctx } from '../ctx.js'
|
|
27
|
+
import { litVal, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks } from './loop-model.js'
|
|
28
|
+
|
|
29
|
+
const SQUARE_BOUND_MAX = 2 ** 30
|
|
30
|
+
// The constant numeric value of a bound: a literal, OR a module const folded to an int
|
|
31
|
+
// (`const LIMIT = 1<<20` → ctx.scope.constInts.get('LIMIT') = 1048576 — the bench form).
|
|
32
|
+
const boundVal = (n) => {
|
|
33
|
+
const lit = litVal(n)
|
|
34
|
+
if (lit != null) return lit
|
|
35
|
+
if (typeof n === 'string') { const v = ctx.scope.constInts?.get(n); return typeof v === 'number' ? v : null }
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
// `i * i` — the IV squared.
|
|
39
|
+
const isSquare = (n, iv) => Array.isArray(n) && n[0] === '*' && n[1] === iv && n[2] === iv && typeof iv === 'string'
|
|
40
|
+
// Math.imul(i, i) in CANONICAL post-prepare form: prepare resolves `Math.imul` → the string
|
|
41
|
+
// ref `'math.imul'`, so the call is `['()', 'math.imul', [',', i, i]]`. math.imul emits a
|
|
42
|
+
// primitive `i32.mul` (no stdlib helper / module include needed).
|
|
43
|
+
const imulOf = (iv) => ['()', 'math.imul', [',', iv, iv]]
|
|
44
|
+
|
|
45
|
+
// The IV of a `(i*i) </≤ CONST` (or mirrored `CONST >/≥ (i*i)`) guard, CONST ≤ 2³⁰, else null.
|
|
46
|
+
function boundedSquareIV(cond) {
|
|
47
|
+
if (!Array.isArray(cond)) return null
|
|
48
|
+
let prod, bound
|
|
49
|
+
if (cond[0] === '<' || cond[0] === '<=') { prod = cond[1]; bound = cond[2] }
|
|
50
|
+
else if (cond[0] === '>' || cond[0] === '>=') { prod = cond[2]; bound = cond[1] }
|
|
51
|
+
else return null
|
|
52
|
+
if (!isSquare(prod, prod && prod[1])) return null
|
|
53
|
+
const b = boundVal(bound)
|
|
54
|
+
if (b == null || b < 0 || b > SQUARE_BOUND_MAX) return null
|
|
55
|
+
return prod[1]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Narrow a `for`/`while` whose guard is `(i*i) </≤ CONST` (CONST ≤ 2³⁰) and whose IV `i` is
|
|
59
|
+
// incremented by +1 and not otherwise mutated — then within any body iteration i ∈ {entry,
|
|
60
|
+
// entry+1}, entry ≤ ⌈√CONST⌉ ≤ 2¹⁵, so every `i*i` (and the exit overshoot) is < 2³¹ and
|
|
61
|
+
// Math.imul(i,i) == i*i. Rewrites those products; the dependent counter chain cascades to i32.
|
|
62
|
+
// `cm` is the function's closure-mutated-vars set (an IV in it has an unprovable entry value).
|
|
63
|
+
function tryNarrow(stmt, cm) {
|
|
64
|
+
const L = normalizeLoop(stmt)
|
|
65
|
+
if (!L) return null
|
|
66
|
+
const { kind, cond, step, body } = L, isFor = kind === 'for'
|
|
67
|
+
|
|
68
|
+
const iv = boundedSquareIV(cond)
|
|
69
|
+
if (!iv) return null
|
|
70
|
+
if (cm.has(iv)) return null // mutable via a closure call — entry value unprovable
|
|
71
|
+
|
|
72
|
+
if (isFor) {
|
|
73
|
+
// The `for` update is the IV's sole, +1 mutation; the body must not reassign i.
|
|
74
|
+
if (unitIncVar(step) !== iv) return null
|
|
75
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
76
|
+
if (ivMut.has(iv)) return null
|
|
77
|
+
} else {
|
|
78
|
+
// `while`: the increment lives in the body (exactly one +1; nothing else mutates i).
|
|
79
|
+
if (!Array.isArray(body) || body[0] !== ';') return null
|
|
80
|
+
let ivIdx = -1
|
|
81
|
+
for (let k = 1; k < body.length; k++) if (unitIncVar(body[k]) === iv) { if (ivIdx >= 0) return null; ivIdx = k }
|
|
82
|
+
if (ivIdx < 0) return null
|
|
83
|
+
const ivMut = new Set()
|
|
84
|
+
findMutations([';', ...body.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
85
|
+
if (ivMut.has(iv)) return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// We're injecting `math.imul` after prepare's auto-import step, so ensure the math module
|
|
89
|
+
// (which registers the `math.imul` → i32.mul primitive emitter) is included.
|
|
90
|
+
includeMods('math')
|
|
91
|
+
// Rewrite every `i*i` in the condition + body to Math.imul(i,i) (NOT init/update — they're
|
|
92
|
+
// `i=2` / `i++`). The inner counter whose init this feeds cascades to i32 on its own.
|
|
93
|
+
const rw = (n) => !Array.isArray(n) ? n : isSquare(n, iv) ? imulOf(iv) : n.map(rw)
|
|
94
|
+
return [isFor
|
|
95
|
+
? ['for', L.init, rw(cond), step, rw(body)]
|
|
96
|
+
: ['while', rw(cond), rw(body)]]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function narrowBoundedSquare(body) {
|
|
100
|
+
const cm = closureMutatedVars(body)
|
|
101
|
+
return rewriteBlocks(body, stmt => tryNarrow(stmt, cm))
|
|
102
|
+
}
|
package/src/compile/narrow.js
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
} from './infer.js'
|
|
29
29
|
|
|
30
30
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
31
|
+
// Integer-preserving ops: an expr over integers stays integer (ToInt32-consistent) through these.
|
|
32
|
+
// Excludes /, %, ** (fractional). Used to recognize a recursive arg whose i32-ness follows from
|
|
33
|
+
// its inputs' i32-ness (`f(n - 1)`), so it carries no independent type evidence.
|
|
34
|
+
const RECUR_INT_OPS = new Set(['+', '-', '*', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>', '~'])
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
function filterLiveCallSites(callSites, valueUsed) {
|
|
@@ -58,12 +62,17 @@ function filterLiveCallSites(callSites, valueUsed) {
|
|
|
58
62
|
|
|
59
63
|
function buildCallerCtx() {
|
|
60
64
|
const callerCtx = new Map()
|
|
61
|
-
|
|
65
|
+
const globalTE = ctx.scope.globalTypedElem || new Map()
|
|
66
|
+
callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes, callerTypedElems: globalTE })
|
|
62
67
|
for (const func of ctx.func.list) {
|
|
63
68
|
if (!func.body || func.raw) continue
|
|
64
69
|
const facts = analyzeBody(func.body)
|
|
65
70
|
for (const p of func.sig.params) if (!facts.locals.has(p.name)) facts.locals.set(p.name, p.type)
|
|
66
|
-
|
|
71
|
+
// Shadow-aware local+global typed-array map: a `const buf = new Int32Array(…)`
|
|
72
|
+
// local makes `buf[i]` arg reads type i32 at this caller's sites, so a callee
|
|
73
|
+
// param fed only such elements narrows (else it stays f64 and `1 << p` drags in
|
|
74
|
+
// __to_num → the whole string↔number stdlib). Mirrors callerTypedElemsFor.
|
|
75
|
+
callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes, callerTypedElems: callerTypedElemsFor(func, globalTE) })
|
|
67
76
|
}
|
|
68
77
|
return callerCtx
|
|
69
78
|
}
|
|
@@ -165,9 +174,10 @@ function refreshCallerValTypes(callerCtx) {
|
|
|
165
174
|
// it is the same single typed-array ctor (scope.js invalidates on any conflict),
|
|
166
175
|
// so it can't denote a different kind at the call site.
|
|
167
176
|
function callerTypedElemsFor(func, globalTE) {
|
|
168
|
-
const
|
|
177
|
+
const facts = analyzeBody(func.body)
|
|
178
|
+
const local = facts.typedElems
|
|
169
179
|
if (!globalTE.size) return local
|
|
170
|
-
const shadowed = new Set(
|
|
180
|
+
const shadowed = new Set(facts.locals.keys())
|
|
171
181
|
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
172
182
|
const merged = new Map()
|
|
173
183
|
for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
|
|
@@ -223,6 +233,7 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
function refreshCallerLocals(callerCtx) {
|
|
236
|
+
const prevTE = ctx.types.typedElem
|
|
226
237
|
for (const func of ctx.func.list) {
|
|
227
238
|
if (!func.body || func.raw) continue
|
|
228
239
|
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
@@ -232,13 +243,24 @@ function refreshCallerLocals(callerCtx) {
|
|
|
232
243
|
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
233
244
|
// emit time, so this transient localReps doesn't leak past narrowing.
|
|
234
245
|
ctx.func.localReps = new Map()
|
|
235
|
-
|
|
246
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params (element ctor from
|
|
247
|
+
// ptrAux), exactly as analyzeFuncForEmit does at emit time. Without it, a local bound to
|
|
248
|
+
// an integer typed-array PARAM element — `aa = perm[perm[X]+Y]` (noise), perm an Int32
|
|
249
|
+
// pointer param — types f64 here, so a callee fed it (`grad(aa,…)`, used only as `aa&3`)
|
|
250
|
+
// never narrows its param to i32. Mirrors emit so narrow-time callerLocals agree with it.
|
|
251
|
+
const te = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : new Map()
|
|
252
|
+
for (const p of func.sig.params) {
|
|
253
|
+
if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
|
|
254
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) { const c = ctorFromElemAux(p.ptrAux); if (c != null) te.set(p.name, c) }
|
|
255
|
+
}
|
|
256
|
+
ctx.types.typedElem = te
|
|
236
257
|
invalidateLocalsCache(func.body)
|
|
237
258
|
const fresh = analyzeBody(func.body).locals
|
|
238
259
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
239
260
|
callerCtx.get(func).callerLocals = fresh
|
|
240
261
|
}
|
|
241
262
|
ctx.func.localReps = null
|
|
263
|
+
ctx.types.typedElem = prevTE
|
|
242
264
|
}
|
|
243
265
|
|
|
244
266
|
function resetParamWasmFacts(paramReps) {
|
|
@@ -280,6 +302,35 @@ function narrowI32Results(funcs) {
|
|
|
280
302
|
e[0] === '>>>' ||
|
|
281
303
|
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
282
304
|
)
|
|
305
|
+
const callsSelf = (n, name) => Array.isArray(n) && ((n[0] === '()' && n[1] === name) || n.some(c => callsSelf(c, name)))
|
|
306
|
+
// Classify a func's return tails as all-v128 / all-i32 (+ sign) under the CURRENT sig.results.
|
|
307
|
+
const evalTails = (func, body, exprs) => {
|
|
308
|
+
const savedCurrent = ctx.func.current
|
|
309
|
+
ctx.func.current = func.sig
|
|
310
|
+
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
311
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
312
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params so a return tail
|
|
313
|
+
// reading a typed-array element — `return vals[h]`, vals an Int32Array param (dict's
|
|
314
|
+
// `lookup`) — types as i32, not NaN-boxed f64. Without it the call site keeps the full
|
|
315
|
+
// __typed_idx/ToNumber unbox dispatch (491520× per dict kernel run). Mirrors
|
|
316
|
+
// refreshCallerLocals + analyzeFuncForEmit. Only meaningful once Phase G has tagged params
|
|
317
|
+
// ptrKind=TYPED (the I2 re-run below); harmless before (no typed params → overlay untouched).
|
|
318
|
+
const savedTE = ctx.types.typedElem
|
|
319
|
+
let te = null
|
|
320
|
+
for (const p of func.sig.params) {
|
|
321
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
322
|
+
const c = ctorFromElemAux(p.ptrAux)
|
|
323
|
+
if (c != null) { if (!te) te = savedTE ? new Map(savedTE) : new Map(); te.set(p.name, c) }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (te) ctx.types.typedElem = te
|
|
327
|
+
const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
|
|
328
|
+
const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
|
|
329
|
+
if (te) ctx.types.typedElem = savedTE
|
|
330
|
+
const r = { allV128, allI32, anyUnsigned: exprs.some(isUnsignedTail), allUnsigned: exprs.every(isUnsignedTail) }
|
|
331
|
+
ctx.func.current = savedCurrent
|
|
332
|
+
return r
|
|
333
|
+
}
|
|
283
334
|
let changed = true
|
|
284
335
|
while (changed) {
|
|
285
336
|
changed = false
|
|
@@ -289,22 +340,40 @@ function narrowI32Results(funcs) {
|
|
|
289
340
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
290
341
|
const exprs = returnExprs(body)
|
|
291
342
|
if (!exprs.length) continue
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
343
|
+
let r = evalTails(func, body, exprs)
|
|
344
|
+
// Recursive result cycle: a self-call in a return tail — or feeding a returned local
|
|
345
|
+
// (nqueens' `cnt = cnt + solve(…); return cnt`) — reads solve's own not-yet-narrowed
|
|
346
|
+
// f64 result, so `cnt` widens to f64 and the i32 narrowing never fires. Break the cycle
|
|
347
|
+
// optimistically: tentatively assume the i32 result, re-analyze, and keep it ONLY if every
|
|
348
|
+
// tail is then i32 (else revert). Sound — committed only when self-consistent.
|
|
349
|
+
if (!r.allI32 && !r.allV128 && callsSelf(body, func.name)) {
|
|
350
|
+
const saved = func.sig.results
|
|
351
|
+
func.sig.results = ['i32']
|
|
352
|
+
invalidateLocalsCache(body)
|
|
353
|
+
const opt = evalTails(func, body, exprs)
|
|
354
|
+
if (opt.allI32 && (!opt.anyUnsigned || opt.allUnsigned)) {
|
|
355
|
+
if (opt.allUnsigned) func.sig.unsignedResult = true
|
|
356
|
+
changed = true
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
func.sig.results = saved
|
|
360
|
+
invalidateLocalsCache(body)
|
|
361
|
+
r = evalTails(func, body, exprs)
|
|
362
|
+
}
|
|
301
363
|
// SIMD: every tail returns a lane vector → v128 result.
|
|
302
|
-
if (allV128) {
|
|
364
|
+
if (r.allV128) {
|
|
303
365
|
func.sig.results = ['v128']
|
|
304
366
|
changed = true
|
|
305
|
-
} else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
|
|
367
|
+
} else if (r.allI32 && (!r.anyUnsigned || r.allUnsigned)) { // sign-consistent i32 tails
|
|
306
368
|
func.sig.results = ['i32']
|
|
307
|
-
if (allUnsigned) func.sig.unsignedResult = true
|
|
369
|
+
if (r.allUnsigned) func.sig.unsignedResult = true
|
|
370
|
+
// A committed i32 result is a genuine NUMBER, so stamp valResult for the call-site
|
|
371
|
+
// VAL dispatch — E2 (narrowValResults) ran ABOVE the param lattice and so couldn't
|
|
372
|
+
// type a `return typedArrayParam[idx]` tail (hashjoin's `probe` → `vals[h]`), leaving
|
|
373
|
+
// valResult unset → the hot `sum + probe()` stayed the polymorphic string-or-number
|
|
374
|
+
// `+`. Only-if-unset: an UNBOXED-pointer i32 result already carries its ARRAY/OBJECT/
|
|
375
|
+
// TYPED valResult (the unboxing ABI needs it), so this never overwrites a pointer kind.
|
|
376
|
+
if (func.valResult == null) func.valResult = VAL.NUMBER
|
|
308
377
|
changed = true
|
|
309
378
|
}
|
|
310
379
|
}
|
|
@@ -661,6 +730,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
661
730
|
callee, callerFunc, argList, func, restIdx,
|
|
662
731
|
callerLocals: ctxEntry.callerLocals,
|
|
663
732
|
callerValTypes: ctxEntry.callerValTypes,
|
|
733
|
+
callerTypedElems: ctxEntry.callerTypedElems,
|
|
664
734
|
callerParamFacts(key) {
|
|
665
735
|
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
666
736
|
return paramFacts.get(key)
|
|
@@ -672,10 +742,20 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
672
742
|
const state = siteState(callSites[s])
|
|
673
743
|
if (!state) continue
|
|
674
744
|
const { func, argList } = state
|
|
745
|
+
const recursive = state.callee === state.callerFunc?.name
|
|
675
746
|
for (let k = 0; k < func.sig.params.length; k++) {
|
|
676
747
|
const r = ensureParamRep(paramReps, state.callee, k)
|
|
677
748
|
if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
|
|
678
|
-
|
|
749
|
+
const arg = argList[k]
|
|
750
|
+
// Recursive identity arg — `f(…, p, …)` calling itself with its own param p threaded
|
|
751
|
+
// through at the same position — is a fixpoint identity: it carries whatever type p
|
|
752
|
+
// settles to, so it constrains nothing. Skip it, else exprType(p) reads p's not-yet-
|
|
753
|
+
// narrowed f64 and the meet poisons the type the non-recursive call sites would prove
|
|
754
|
+
// (nqueens' `solve(all, …)` — `all` stuck f64 while cols/d1/d2, passed as i32 bitwise
|
|
755
|
+
// exprs, narrowed fine).
|
|
756
|
+
const pname = func.sig.params[k].name
|
|
757
|
+
if (recursive && (arg === pname || (Array.isArray(arg) && arg[0] === 'local.get' && arg[1] === pname))) continue
|
|
758
|
+
for (const rule of rules) rule.apply(r, arg, k, state)
|
|
679
759
|
}
|
|
680
760
|
}
|
|
681
761
|
}
|
|
@@ -743,6 +823,49 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
743
823
|
mergeParamFact(r, field, v)
|
|
744
824
|
},
|
|
745
825
|
})
|
|
826
|
+
// WASM type of a call arg. exprType resolves most shapes, but an INTEGER typed-array
|
|
827
|
+
// element read `intArr[idx]` (and arithmetic over it, `intArr[idx]+1`) types f64 here:
|
|
828
|
+
// exprType's `[]` rule reads the typedElem OVERLAY, which doesn't see a typedCtor-narrowed
|
|
829
|
+
// PARAM array at fixpoint time — yet the element is a 32-bit machine integer. Install the
|
|
830
|
+
// caller's resolved param-typedCtors (+ module globals) as that overlay for the duration
|
|
831
|
+
// of the type query, so a param fed only such integer elements (dict's key `k` ← src[i],
|
|
832
|
+
// threaded through Math.imul / === keys[h] / keys[h]=k) narrows to i32 instead of paying
|
|
833
|
+
// convert + f64-compare + trunc round-trips through its probe loop.
|
|
834
|
+
// A value built ONLY from the callee's own params + already-i32 locals + integer constants via
|
|
835
|
+
// integer-preserving ops. Its i32-ness follows from its inputs' — for a recursive self-call it
|
|
836
|
+
// carries no INDEPENDENT evidence about whether the params are i32. Used for the optimism below.
|
|
837
|
+
const isRecurIntExpr = (n, pnames, callerLocals) => {
|
|
838
|
+
if (typeof n === 'string') return pnames.has(n) || callerLocals?.get?.(n) === 'i32'
|
|
839
|
+
if (typeof n === 'number') return Number.isInteger(n)
|
|
840
|
+
if (!Array.isArray(n)) return false
|
|
841
|
+
if (n[0] == null) return typeof n[1] === 'number' && Number.isInteger(n[1]) // boxed int literal
|
|
842
|
+
if (n[0] === 'local.get') return pnames.has(n[1]) || callerLocals?.get?.(n[1]) === 'i32'
|
|
843
|
+
if (RECUR_INT_OPS.has(n[0])) return n.slice(1).every(c => isRecurIntExpr(c, pnames, callerLocals))
|
|
844
|
+
return false
|
|
845
|
+
}
|
|
846
|
+
const argWasmType = (arg, state) => {
|
|
847
|
+
// Recursive self-call: an arg built only from the callee's own params + already-i32 locals +
|
|
848
|
+
// int constants (`f(n - 1)`, `f(n - 1 - i)`) is i32 IFF those params are i32 — a fixpoint
|
|
849
|
+
// identity carrying no INDEPENDENT type evidence. Optimistically type it i32 so the NON-
|
|
850
|
+
// recursive call sites decide: all i32 ⇒ the param narrows; any f64 ⇒ the meet still poisons
|
|
851
|
+
// it. Lets a plain decreasing recursion narrow with no `|0` source crutch. (The bare-identity
|
|
852
|
+
// arg `f(n)` is already skipped wholesale in runCallsiteLattice.)
|
|
853
|
+
if (state.callee === state.callerFunc?.name &&
|
|
854
|
+
isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
|
|
855
|
+
if (!state._teOverlay) {
|
|
856
|
+
// Caller's typed arrays (locals + non-shadowed globals, precomputed shadow-aware
|
|
857
|
+
// in buildCallerCtx) so a LOCAL `const buf = new Int32Array(…)` makes `buf[i]`
|
|
858
|
+
// type i32 here — not just module globals / typedCtor-narrowed params.
|
|
859
|
+
const m = new Map(state.callerTypedElems || ctx.scope.globalTypedElem || [])
|
|
860
|
+
const pf = state.callerParamFacts('typedCtor')
|
|
861
|
+
if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
|
|
862
|
+
state._teOverlay = m
|
|
863
|
+
}
|
|
864
|
+
const prev = ctx.func.localTypedElemsOverlay
|
|
865
|
+
ctx.func.localTypedElemsOverlay = state._teOverlay
|
|
866
|
+
try { return exprType(arg, state.callerLocals) }
|
|
867
|
+
finally { ctx.func.localTypedElemsOverlay = prev }
|
|
868
|
+
}
|
|
746
869
|
const runFixpoint = () => runCallsiteLattice([
|
|
747
870
|
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
748
871
|
// typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
|
|
@@ -754,7 +877,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
754
877
|
missing: poison('wasm'),
|
|
755
878
|
apply(r, arg, _k, state) {
|
|
756
879
|
if (r.wasm === null) return
|
|
757
|
-
const wt =
|
|
880
|
+
const wt = argWasmType(arg, state)
|
|
758
881
|
if (r.wasm === undefined) r.wasm = wt
|
|
759
882
|
else if (r.wasm !== wt) r.wasm = null
|
|
760
883
|
},
|
|
@@ -919,6 +1042,16 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
919
1042
|
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
920
1043
|
// only after E phase narrowed mix's result).
|
|
921
1044
|
phase.refreshLocals()
|
|
1045
|
+
// I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
|
|
1046
|
+
// typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
|
|
1047
|
+
// typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
|
|
1048
|
+
// vals an Int32Array param — had its return tail type as NaN-boxed f64 (vals not yet a
|
|
1049
|
+
// typed pointer), leaving sig.results f64 and the call site running the full
|
|
1050
|
+
// __typed_idx/ToNumber unbox on every probe step (491520× per dict kernel run). Now that
|
|
1051
|
+
// evalTails seeds the typed-param overlay and params carry ptrAux, the fixpoint catches
|
|
1052
|
+
// `vals[h]` as i32, narrows the result, and the dispatch vanishes; the runFixpoint below
|
|
1053
|
+
// then propagates the i32 result into `let v = lookup(...)` at the call sites.
|
|
1054
|
+
narrowI32Results(funcsWithNarrowableResult)
|
|
922
1055
|
// Reset wasm field unconditionally — first pass populated it from stale callerLocals
|
|
923
1056
|
// (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
|
|
924
1057
|
// yet). clearStickyNull only resets null; here we need to reset f64-observed too
|
|
@@ -1334,25 +1467,37 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1334
1467
|
|
|
1335
1468
|
// Build one clone per distinct combo.
|
|
1336
1469
|
const cloneByKey = new Map()
|
|
1337
|
-
for (const [
|
|
1338
|
-
|
|
1470
|
+
for (const [dkey, cmb] of distinct) {
|
|
1471
|
+
// NB: this loop variable must NOT reuse the name `combo` (declared twice above, at the
|
|
1472
|
+
// site loop and the distinct-building loop). The self-host miscompiles a for-of loop
|
|
1473
|
+
// variable whose name collides with an earlier block-scoped declaration — it aliases the
|
|
1474
|
+
// prior binding instead of rebinding per iteration, so `combo` would stay stuck at the
|
|
1475
|
+
// last site's ctor and every clone would get the same (wrong) element type → silent
|
|
1476
|
+
// garbage. A unique name gets a clean per-iteration binding.
|
|
1477
|
+
const suffix = cmb.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
|
|
1339
1478
|
let cloneName = `${func.name}$${suffix}`
|
|
1340
1479
|
let n = 0
|
|
1341
1480
|
while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
|
|
1342
1481
|
|
|
1482
|
+
// Build cloneSig with clean, fully-formed object literals — never by spreading a
|
|
1483
|
+
// live object and then overriding/extending its keys. A MULTI-prop spread of a
|
|
1484
|
+
// member-access source (`{ ...func.sig, params, results }`) takes the static
|
|
1485
|
+
// allKnown OBJECT-merge path, which trusts func.sig's COMPILE-TIME schema; sig
|
|
1486
|
+
// objects are polymorphic (some carry result/ptrKind/unsignedResult), so that
|
|
1487
|
+
// schema can be a subset of the runtime shape and the slot-copy then faults a
|
|
1488
|
+
// later `sig.params` read out of bounds in the self-host. (The single-unknown
|
|
1489
|
+
// `{ ...x }` clone is fixed at the root — __obj_clone — but the allKnown merge
|
|
1490
|
+
// path is a separate hazard.) Constructing each param with its pointer ABI baked
|
|
1491
|
+
// in sidesteps it; output is unchanged on the host.
|
|
1343
1492
|
const cloneSig = {
|
|
1344
|
-
|
|
1345
|
-
|
|
1493
|
+
params: func.sig.params.map((p, idx) => {
|
|
1494
|
+
const bi = bimorphic.indexOf(idx)
|
|
1495
|
+
return bi < 0
|
|
1496
|
+
? { ...p }
|
|
1497
|
+
: { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: typedElemAux(cmb[bi]) }
|
|
1498
|
+
}),
|
|
1346
1499
|
results: [...func.sig.results],
|
|
1347
1500
|
}
|
|
1348
|
-
for (let i = 0; i < bimorphic.length; i++) {
|
|
1349
|
-
const k = bimorphic[i]
|
|
1350
|
-
const aux = typedElemAux(combo[i])
|
|
1351
|
-
const p = cloneSig.params[k]
|
|
1352
|
-
p.type = 'i32'
|
|
1353
|
-
p.ptrKind = VAL.TYPED
|
|
1354
|
-
p.ptrAux = aux
|
|
1355
|
-
}
|
|
1356
1501
|
const clone = { ...func, name: cloneName, sig: cloneSig }
|
|
1357
1502
|
ctx.func.list.push(clone)
|
|
1358
1503
|
ctx.func.map.set(cloneName, clone)
|
|
@@ -1360,19 +1505,20 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1360
1505
|
|
|
1361
1506
|
// Mirror per-param reps under the clone's name with mono ctors at bimorphic
|
|
1362
1507
|
// positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
|
|
1363
|
-
// `arr[i]` lowers to direct typed load.
|
|
1508
|
+
// `arr[i]` lowers to direct typed load. Each `{ ...r }` is a true clone, so
|
|
1509
|
+
// pinning typedCtor on it leaves the source rep untouched (__obj_clone).
|
|
1364
1510
|
const cloneReps = new Map()
|
|
1365
1511
|
for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
1366
1512
|
for (let i = 0; i < bimorphic.length; i++) {
|
|
1367
1513
|
const k = bimorphic[i]
|
|
1368
1514
|
const r = cloneReps.get(k) || {}
|
|
1369
|
-
r.typedCtor =
|
|
1515
|
+
r.typedCtor = cmb[i]
|
|
1370
1516
|
r.val = VAL.TYPED
|
|
1371
1517
|
cloneReps.set(k, r)
|
|
1372
1518
|
}
|
|
1373
1519
|
paramReps.set(cloneName, cloneReps)
|
|
1374
1520
|
|
|
1375
|
-
cloneByKey.set(
|
|
1521
|
+
cloneByKey.set(dkey, clone)
|
|
1376
1522
|
}
|
|
1377
1523
|
|
|
1378
1524
|
// Rewrite each site's call AST to point at the matching clone.
|