jz 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulator-fusion recursion unrolling — for a self-recursive function whose
|
|
3
|
+
* single recursive call feeds an accumulator (`acc = acc + self(…)` — the
|
|
4
|
+
* backtracking / tree-search / divide-and-conquer sum), inline one level of the
|
|
5
|
+
* call and FUSE its accumulator into the caller's: the inlined loop adds straight
|
|
6
|
+
* into `acc`, and the base case becomes a cheap `acc += const`. No call frame, no
|
|
7
|
+
* result block, no second accumulator.
|
|
8
|
+
*
|
|
9
|
+
* Plain inlining alone wins nothing here — V8 already inlines hot wasm calls, and
|
|
10
|
+
* a `(block (result T) … br)` costs what the call did. The measured speedup comes
|
|
11
|
+
* from the fusion: `acc = acc + (Σ inner)` ⇒ `for each inner: acc += inner`
|
|
12
|
+
* (associativity), which removes the per-level accumulator and its final add.
|
|
13
|
+
*
|
|
14
|
+
* WAT-IR layer, speed-tier only (-O3). Strictly gated: exactly one non-tail
|
|
15
|
+
* self-call, consumed by `acc = acc ± self(…)` where `acc` is a local; a small
|
|
16
|
+
* body; only soundly-cloneable constructs. Off at -O2/-Os.
|
|
17
|
+
*
|
|
18
|
+
* @module optimize/recurse
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const isArr = (x) => Array.isArray(x) // wrapped: jz self-host rejects a builtin used as a value
|
|
22
|
+
|
|
23
|
+
const UNROLL_DEPTH = 2
|
|
24
|
+
const MAX_BODY_NODES = 110
|
|
25
|
+
const ADD_OPS = new Set(['i32.add', 'i64.add', 'f64.add', 'f32.add'])
|
|
26
|
+
// Replay per-frame zero-init for the callee's NON-accumulator locals (the acc is
|
|
27
|
+
// shared with the caller and must NOT be reset). dropDeadZeroInit removes the
|
|
28
|
+
// redundant ones (locals written before read) in a later pass. A function (not an
|
|
29
|
+
// object) keeps it on jz's self-host subset and returns a fresh node each call.
|
|
30
|
+
const ZERO = (t) => t === 'i32' ? ['i32.const', 0] : t === 'i64' ? ['i64.const', 0]
|
|
31
|
+
: t === 'f64' ? ['f64.const', 0] : t === 'f32' ? ['f32.const', 0] : null
|
|
32
|
+
|
|
33
|
+
const deepClone = (n) => {
|
|
34
|
+
if (!isArr(n)) return n
|
|
35
|
+
const c = n.map(deepClone)
|
|
36
|
+
if (n.type !== undefined) c.type = n.type
|
|
37
|
+
return c
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nodeCount = (n) => {
|
|
41
|
+
if (!isArr(n)) return 1
|
|
42
|
+
let c = 1
|
|
43
|
+
for (let i = 1; i < n.length; i++) c += nodeCount(n[i])
|
|
44
|
+
return c
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Clone a body subtree with fusion rewrites:
|
|
48
|
+
// • locals/labels renamed (except `acc`, which stays shared with the caller)
|
|
49
|
+
// • `return acc` → `br $skip` (acc already holds the sum)
|
|
50
|
+
// • `return V` → `acc = acc + V; br $skip` (base case folds into acc)
|
|
51
|
+
function cloneFuse(node, c) {
|
|
52
|
+
if (!isArr(node)) return node
|
|
53
|
+
const op = node[0]
|
|
54
|
+
let out
|
|
55
|
+
if (op === 'return') {
|
|
56
|
+
if (node.length === 1) { out = ['br', c.skip] }
|
|
57
|
+
else if (isArr(node[1]) && node[1][0] === 'local.get' && node[1][1] === c.acc) { out = ['br', c.skip] }
|
|
58
|
+
else {
|
|
59
|
+
const v = cloneFuse(node[1], c)
|
|
60
|
+
out = ['block', ['local.set', c.acc, [c.add, ['local.get', c.acc], v]], ['br', c.skip]]
|
|
61
|
+
}
|
|
62
|
+
} else if (op === 'local.get') {
|
|
63
|
+
out = ['local.get', node[1] === c.acc ? c.acc : (c.names.get(node[1]) || node[1])]
|
|
64
|
+
} else if (op === 'local.set' || op === 'local.tee') {
|
|
65
|
+
out = [op, node[1] === c.acc ? c.acc : (c.names.get(node[1]) || node[1]), cloneFuse(node[2], c)]
|
|
66
|
+
} else if ((op === 'block' || op === 'loop') && typeof node[1] === 'string' && node[1][0] === '$') {
|
|
67
|
+
out = [op, c.labels.get(node[1]) || node[1]]
|
|
68
|
+
for (let i = 2; i < node.length; i++) out.push(cloneFuse(node[i], c))
|
|
69
|
+
} else if (op === 'br' || op === 'br_if') {
|
|
70
|
+
out = [op, c.labels.get(node[1]) || node[1]]
|
|
71
|
+
for (let i = 2; i < node.length; i++) out.push(cloneFuse(node[i], c))
|
|
72
|
+
} else {
|
|
73
|
+
out = [op]
|
|
74
|
+
for (let i = 1; i < node.length; i++) out.push(cloneFuse(node[i], c))
|
|
75
|
+
}
|
|
76
|
+
if (node.type !== undefined) out.type = node.type
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find `(local.set A (ADD (local.get A) (call $self …)))` (either operand order).
|
|
81
|
+
// That statement IS the recursive call's only consumer — the fusion site.
|
|
82
|
+
function findAccConsume(root, self) {
|
|
83
|
+
if (!isArr(root)) return null
|
|
84
|
+
for (let i = 1; i < root.length; i++) {
|
|
85
|
+
const c = root[i]
|
|
86
|
+
if (isArr(c) && c[0] === 'local.set' && typeof c[1] === 'string' && isArr(c[2]) && ADD_OPS.has(c[2][0])) {
|
|
87
|
+
const A = c[1], add = c[2]
|
|
88
|
+
const isAcc = (e) => isArr(e) && e[0] === 'local.get' && e[1] === A
|
|
89
|
+
const isCall = (e) => isArr(e) && e[0] === 'call' && e[1] === self
|
|
90
|
+
if (isAcc(add[1]) && isCall(add[2])) return { parent: root, idx: i, acc: A, add: add[0], call: add[2] }
|
|
91
|
+
if (isAcc(add[2]) && isCall(add[1])) return { parent: root, idx: i, acc: A, add: add[0], call: add[1] }
|
|
92
|
+
}
|
|
93
|
+
const r = findAccConsume(c, self)
|
|
94
|
+
if (r) return r
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const freshen = (orig, level, used) => {
|
|
100
|
+
let name = `${orig}_ru${level}`
|
|
101
|
+
while (used.has(name)) name += 'x'
|
|
102
|
+
used.add(name)
|
|
103
|
+
return name
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Fuse-unroll a self-recursive accumulator function's recursive call. */
|
|
107
|
+
export function recursionUnroll(fn) {
|
|
108
|
+
if (!isArr(fn) || fn[0] !== 'func') return
|
|
109
|
+
|
|
110
|
+
const self = fn[1]
|
|
111
|
+
const params = [], locals = [], names = new Set()
|
|
112
|
+
let resultType = null, bodyStart = 2
|
|
113
|
+
for (let i = 2; i < fn.length; i++) {
|
|
114
|
+
const d = fn[i]
|
|
115
|
+
if (!isArr(d)) { bodyStart = i; break }
|
|
116
|
+
if (d[0] === 'param') { params.push([d[1], d[2]]); names.add(d[1]); bodyStart = i + 1 }
|
|
117
|
+
else if (d[0] === 'local') { locals.push([d[1], d[2]]); names.add(d[1]); bodyStart = i + 1 }
|
|
118
|
+
else if (d[0] === 'result') { resultType = d[1]; bodyStart = i + 1 }
|
|
119
|
+
else if (d[0] === 'export' || d[0] === 'import' || d[0] === 'type') { bodyStart = i + 1 }
|
|
120
|
+
else { bodyStart = i; break }
|
|
121
|
+
}
|
|
122
|
+
if (resultType == null) return
|
|
123
|
+
|
|
124
|
+
// Gate: exactly one non-tail self-call, only soundly-cloneable constructs.
|
|
125
|
+
let selfCalls = 0, bail = false
|
|
126
|
+
const labelNames = new Set()
|
|
127
|
+
const scan = (n) => {
|
|
128
|
+
if (!isArr(n)) return
|
|
129
|
+
const op = n[0]
|
|
130
|
+
if (op === 'call' && n[1] === self) selfCalls++
|
|
131
|
+
else if (op === 'return_call' || op === 'return_call_indirect' || op === 'return_call_ref') bail = true
|
|
132
|
+
else if (op === 'br_table') bail = true
|
|
133
|
+
else if ((op === 'br' || op === 'br_if') && typeof n[1] !== 'string') bail = true
|
|
134
|
+
else if ((op === 'block' || op === 'loop') && typeof n[1] === 'string' && n[1][0] === '$') labelNames.add(n[1])
|
|
135
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
136
|
+
}
|
|
137
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
138
|
+
if (bail || selfCalls !== 1) return
|
|
139
|
+
|
|
140
|
+
// The recursive call must be consumed by `acc = acc ± self(…)`, acc a local.
|
|
141
|
+
const consume = findAccConsume(fn, self)
|
|
142
|
+
if (!consume) return
|
|
143
|
+
const accName = consume.acc
|
|
144
|
+
if (!locals.some(([n]) => n === accName)) return // acc must be a local, not a param
|
|
145
|
+
if (!ZERO(resultType)) return // need a typed add for the base-case fold
|
|
146
|
+
// Non-accumulator locals must be re-zeroable (they get fresh per-level copies).
|
|
147
|
+
for (const [ln, lt] of locals) if (ln !== accName && !ZERO(lt)) return
|
|
148
|
+
|
|
149
|
+
const template = []
|
|
150
|
+
for (let i = bodyStart; i < fn.length; i++) template.push(deepClone(fn[i]))
|
|
151
|
+
let bodyNodes = 0
|
|
152
|
+
for (const s of template) bodyNodes += nodeCount(s)
|
|
153
|
+
if (bodyNodes > MAX_BODY_NODES) return
|
|
154
|
+
|
|
155
|
+
let loc = consume
|
|
156
|
+
const newDecls = []
|
|
157
|
+
for (let level = 1; level <= UNROLL_DEPTH; level++) {
|
|
158
|
+
const args = loc.call.slice(2)
|
|
159
|
+
const nameMap = new Map(), labelMap = new Map()
|
|
160
|
+
const skip = freshen('$__ru_skip', level, names)
|
|
161
|
+
for (const [pn, pt] of params) { const f = freshen(pn, level, names); nameMap.set(pn, f); newDecls.push(['local', f, pt]) }
|
|
162
|
+
for (const [ln, lt] of locals) {
|
|
163
|
+
if (ln === accName) continue // accumulator is shared with the caller
|
|
164
|
+
const f = freshen(ln, level, names); nameMap.set(ln, f); newDecls.push(['local', f, lt])
|
|
165
|
+
}
|
|
166
|
+
for (const lb of labelNames) labelMap.set(lb, freshen(lb, level, names))
|
|
167
|
+
|
|
168
|
+
const c = { acc: accName, add: consume.add, skip, names: nameMap, labels: labelMap }
|
|
169
|
+
const cloned = template.map(s => cloneFuse(s, c))
|
|
170
|
+
const binds = params.map(([pn], k) => ['local.set', nameMap.get(pn), args[k]])
|
|
171
|
+
const zeros = locals.filter(([ln]) => ln !== accName).map(([ln, lt]) => ['local.set', nameMap.get(ln), ZERO(lt)])
|
|
172
|
+
// Void block: the inlined body accumulates straight into `accName`; `$skip`
|
|
173
|
+
// is the early-out for the base case. Replaces the whole `acc = acc + call`.
|
|
174
|
+
const block = ['block', skip, ...binds, ...zeros, ...cloned]
|
|
175
|
+
|
|
176
|
+
loc.parent[loc.idx] = block
|
|
177
|
+
const next = findAccConsume(block, self) // inner fusion site, for the next level
|
|
178
|
+
if (!next) break
|
|
179
|
+
loc = next
|
|
180
|
+
}
|
|
181
|
+
fn.splice(bodyStart, 0, ...newDecls)
|
|
182
|
+
}
|