jz 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -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
+ }