jz 0.5.1 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,316 @@
1
+ import { ctx, warn } from '../../ctx.js'
2
+ import { refsName, REFS_IN_EXPR } from '../../ast.js'
3
+ import { intLiteralValue } from '../../static.js'
4
+ import { VAL } from '../../reps.js'
5
+ import { adviseJsstringCarrier } from '../narrow.js'
6
+
7
+ /** Compile-time advisories — heap growth, Map iteration order, SIMD hints. */
8
+ const HEAP_LOOP_OPS = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'do-while'])
9
+ const HEAP_VALS = new Set([
10
+ VAL.ARRAY, VAL.STRING, VAL.OBJECT, VAL.HASH, VAL.SET, VAL.MAP,
11
+ VAL.CLOSURE, VAL.TYPED, VAL.REGEX, VAL.BUFFER,
12
+ ])
13
+
14
+ function returnsHeap(func) {
15
+ if (func.sig.ptrKind != null) return true
16
+ return func.valResult != null && HEAP_VALS.has(func.valResult)
17
+ }
18
+
19
+ function isHeapAlloc(node) {
20
+ if (!Array.isArray(node)) return false
21
+ const op = node[0]
22
+ if (op === '{}') {
23
+ // `['{}', [';', …]]` is a block body, not an object literal.
24
+ if (node.length === 2 && Array.isArray(node[1]) && node[1][0] === ';') return false
25
+ return node.length > 1
26
+ }
27
+ if (op === '[]') return node.length === 2
28
+ if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.') {
29
+ const method = node[1][2]
30
+ if (method === 'push' || method === 'concat') return true
31
+ }
32
+ return false
33
+ }
34
+
35
+ function containsHeapAlloc(node) {
36
+ if (!Array.isArray(node)) return false
37
+ if (isHeapAlloc(node)) return true
38
+ for (let i = 1; i < node.length; i++)
39
+ if (containsHeapAlloc(node[i])) return true
40
+ return false
41
+ }
42
+
43
+ function heapLoopBody(node) {
44
+ if (!Array.isArray(node) || !HEAP_LOOP_OPS.has(node[0])) return null
45
+ return node[node.length - 1]
46
+ }
47
+
48
+ function heapLoopAllocSites(body) {
49
+ const sites = []
50
+ const walk = (node) => {
51
+ if (!Array.isArray(node)) return
52
+ if (HEAP_LOOP_OPS.has(node[0])) {
53
+ const lb = heapLoopBody(node)
54
+ if (lb && containsHeapAlloc(lb))
55
+ sites.push({ loc: node.loc ?? lb.loc })
56
+ }
57
+ for (let i = 1; i < node.length; i++) walk(node[i])
58
+ }
59
+ walk(body)
60
+ return sites
61
+ }
62
+
63
+ function bodyHeapAllocates(body) {
64
+ return body != null && containsHeapAlloc(body)
65
+ }
66
+
67
+ /** Mirrors `applyArenaRewind` eligibility in src/assemble.js (AST-level). */
68
+ function isArenaRewindable(func) {
69
+ if (func.raw) return false
70
+ if (func.sig.params.length !== 0) return false
71
+ if (func.sig.results.length !== 1) return false
72
+ if (func.sig.ptrKind != null) return false
73
+ if (returnsHeap(func)) return false
74
+ if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER && func.valResult != null)
75
+ return false
76
+ if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
77
+ return bodyHeapAllocates(func.body)
78
+ }
79
+
80
+ function exportedFuncNames() {
81
+ const names = new Set()
82
+ for (const [key, val] of Object.entries(ctx.func.exports)) {
83
+ const name = val === true ? key : (typeof val === 'string' ? val : null)
84
+ if (name) names.add(name)
85
+ }
86
+ return names
87
+ }
88
+
89
+ /** Bump-allocator growth advisories — no-op without an `opts.warnings` sink. */
90
+ function adviseHeapGrowth() {
91
+ if (!ctx.warnings) return
92
+ if (ctx.transform.alloc === false) return
93
+
94
+ const exported = exportedFuncNames()
95
+
96
+ for (const func of ctx.func.list) {
97
+ if (func.raw || !func.body) continue
98
+
99
+ const fn = func.name
100
+ const isExport = exported.has(fn)
101
+
102
+ if (isExport && returnsHeap(func)) {
103
+ warn('heap-return',
104
+ `export '${fn}' returns a heap value — repeated calls grow linear memory; call memory.reset() between batches from the host`,
105
+ { fn }, func.body.loc)
106
+ continue
107
+ }
108
+
109
+ const loopSites = heapLoopAllocSites(func.body)
110
+ for (const site of loopSites) {
111
+ warn('heap-loop',
112
+ `${isExport ? `export '${fn}'` : `'${fn}'`} allocates heap values inside a loop — peak memory grows with trip count; call memory.reset() between batches from the host`,
113
+ { fn }, site.loc)
114
+ }
115
+
116
+ if (isExport && !returnsHeap(func) && bodyHeapAllocates(func.body)
117
+ && !isArenaRewindable(func) && loopSites.length === 0) {
118
+ const code = func.sig.params.length > 0 ? 'arena-rewind-skipped' : 'heap-per-call'
119
+ const detail = func.sig.params.length > 0
120
+ ? `export '${fn}' allocates heap values but cannot rewind per call (parameters or returned pointers prevent arena rewind)`
121
+ : `export '${fn}' allocates heap values — jz does not reclaim between calls`
122
+ warn(code,
123
+ `${detail}; call memory.reset() between batches from the host`,
124
+ { fn }, func.body.loc)
125
+ }
126
+ }
127
+ }
128
+
129
+ const SET_MAP_ITER_OPS = new Set(['for-in', 'for-of'])
130
+ const SET_MAP_METHODS = new Set(['keys', 'values', 'entries', 'forEach'])
131
+ const SET_MAP_SLOT_ORDER = 'uses slot order, not insertion order — results may differ from JavaScript'
132
+
133
+ function newSetMapKind(node) {
134
+ if (!Array.isArray(node)) return null
135
+ if (node[0] === 'new') {
136
+ const ctor = node[1]
137
+ const name = typeof ctor === 'string' ? ctor
138
+ : Array.isArray(ctor) && ctor[0] === '()' && typeof ctor[1] === 'string' ? ctor[1]
139
+ : null
140
+ if (name === 'Set') return 'set'
141
+ if (name === 'Map') return 'map'
142
+ }
143
+ if (node[0] === '()' && typeof node[1] === 'string') {
144
+ if (node[1] === 'new.Set') return 'set'
145
+ if (node[1] === 'new.Map') return 'map'
146
+ }
147
+ return null
148
+ }
149
+
150
+ function collectSetMapBindings(body) {
151
+ const bindings = new Map()
152
+ const walk = (node) => {
153
+ if (!Array.isArray(node)) return
154
+ const op = node[0]
155
+ if (op === 'let' || op === 'const') {
156
+ for (let i = 1; i < node.length; i++) {
157
+ const d = node[i]
158
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
159
+ const kind = newSetMapKind(d[2])
160
+ if (kind) bindings.set(d[1], kind)
161
+ }
162
+ }
163
+ for (let i = 1; i < node.length; i++) walk(node[i])
164
+ }
165
+ walk(body)
166
+ return bindings
167
+ }
168
+
169
+ function exprSetMapKind(expr, bindings) {
170
+ const direct = newSetMapKind(expr)
171
+ if (direct) return direct
172
+ return typeof expr === 'string' ? bindings.get(expr) || null : null
173
+ }
174
+
175
+ function isJsonStringifyCall(node) {
176
+ if (!Array.isArray(node) || node[0] !== '()') return false
177
+ const callee = node[1]
178
+ if (callee === 'JSON.stringify') return true
179
+ return Array.isArray(callee) && callee[0] === '.' && callee[1] === 'JSON' && callee[2] === 'stringify'
180
+ }
181
+
182
+ function adviseSetMapIterationOrder() {
183
+ if (!ctx.warnings) return
184
+
185
+ for (const func of ctx.func.list) {
186
+ if (func.raw || !func.body) continue
187
+ const fn = func.name
188
+ const bindings = collectSetMapBindings(func.body)
189
+
190
+ const warnOrder = (msg, loc) => warn('set-map-order', msg, { fn }, loc)
191
+ const label = (kind) => kind === 'set' ? 'Set' : 'Map'
192
+
193
+ const walk = (node) => {
194
+ if (!Array.isArray(node)) return
195
+ const op = node[0]
196
+
197
+ if (SET_MAP_ITER_OPS.has(op)) {
198
+ const kind = exprSetMapKind(node[2], bindings)
199
+ if (kind) warnOrder(`${label(kind)} iteration ${SET_MAP_SLOT_ORDER}`, node.loc ?? node[2]?.loc)
200
+ }
201
+
202
+ if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.') {
203
+ const [, recv, method] = node[1]
204
+ const kind = SET_MAP_METHODS.has(method) ? exprSetMapKind(recv, bindings) : null
205
+ if (kind) warnOrder(`${label(kind)}.${method}() ${SET_MAP_SLOT_ORDER}`, node.loc ?? recv?.loc)
206
+ }
207
+
208
+ if (isJsonStringifyCall(node)) {
209
+ const kind = exprSetMapKind(node[2], bindings)
210
+ if (kind) warnOrder(`JSON.stringify on a ${kind} serializes entries in slot order, not insertion order — output may differ from JavaScript`, node.loc)
211
+ }
212
+
213
+ if (op === '...') {
214
+ const kind = exprSetMapKind(node[1], bindings)
215
+ if (kind) warnOrder(`spread over a ${kind} follows slot order, not insertion order — element order may differ from JavaScript`, node.loc)
216
+ }
217
+
218
+ for (let i = 1; i < node.length; i++) walk(node[i])
219
+ }
220
+ walk(func.body)
221
+ }
222
+ }
223
+
224
+ function forInductionVar(step) {
225
+ if (!Array.isArray(step)) return null
226
+ if (step[0] === '++' || step[0] === '--') return typeof step[1] === 'string' ? step[1] : null
227
+ if (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++')
228
+ return typeof step[1][1] === 'string' ? step[1][1] : null
229
+ if ((step[0] === '+=' || step[0] === '-=') && step[2]?.[0] == null && step[2]?.[1] === 1)
230
+ return typeof step[1] === 'string' ? step[1] : null
231
+ return null
232
+ }
233
+
234
+ function indexStrideOnVar(indexExpr, iv) {
235
+ if (!Array.isArray(indexExpr)) return 1
236
+ const op = indexExpr[0]
237
+ if (indexExpr === iv) return 1
238
+ if (op === '[]' && indexExpr[2] === iv) return 1
239
+ if (op === '*' && ((indexExpr[1] === iv && intLiteralValue(indexExpr[2]) > 1)
240
+ || (indexExpr[2] === iv && intLiteralValue(indexExpr[1]) > 1)))
241
+ return intLiteralValue(indexExpr[1] === iv ? indexExpr[2] : indexExpr[1])
242
+ if (op === '+') {
243
+ for (let i = 1; i < indexExpr.length; i++) {
244
+ const s = indexStrideOnVar(indexExpr[i], iv)
245
+ if (s > 1) return s
246
+ }
247
+ }
248
+ return 1
249
+ }
250
+
251
+ const SIMD_REDUCE_OPS = new Set(['+=', '|=', '&=', '^=', '-=', '*=', '/=', '%='])
252
+
253
+ function simdLoopIssues(body, iv) {
254
+ let indexed = false, carried = false, maxStride = 1
255
+ const walk = (node) => {
256
+ if (!Array.isArray(node)) return
257
+ const op = node[0]
258
+ if (op === '=>') return
259
+ if (op === '[]' && node.length === 3) {
260
+ const idx = node[2]
261
+ if (idx === iv || (Array.isArray(idx) && refsName(idx, iv, REFS_IN_EXPR))) indexed = true
262
+ const stride = indexStrideOnVar(idx, iv)
263
+ if (stride > maxStride) maxStride = stride
264
+ }
265
+ if (SIMD_REDUCE_OPS.has(op) && typeof node[1] === 'string' && node[1] !== iv) carried = true
266
+ if (op === '=' && typeof node[1] === 'string' && node[1] !== iv) {
267
+ const rhs = node[2]
268
+ if (rhs === node[1] || (Array.isArray(rhs) && refsName(rhs, node[1], REFS_IN_EXPR))) carried = true
269
+ }
270
+ for (let i = 1; i < node.length; i++) walk(node[i])
271
+ }
272
+ walk(body)
273
+ return { indexed, carried, maxStride }
274
+ }
275
+
276
+ function adviseSimdLoops() {
277
+ if (!ctx.warnings) return
278
+ if (ctx.transform.optimize?.vectorizeLaneLocal === false) return
279
+
280
+ for (const func of ctx.func.list) {
281
+ if (func.raw || !func.body) continue
282
+ const fn = func.name
283
+
284
+ const walk = (node) => {
285
+ if (!Array.isArray(node)) return
286
+ if (node[0] === 'for' && node.length >= 5) {
287
+ const [, , , step, body] = node
288
+ const iv = forInductionVar(step)
289
+ if (iv) {
290
+ const { indexed, carried, maxStride } = simdLoopIssues(body, iv)
291
+ if (indexed && carried) {
292
+ warn('simd-loop-carried',
293
+ `'${fn}' loop carries a scalar updated each iteration — SIMD vectorization skipped; split the reduction or use a separate accumulator`,
294
+ { fn }, node.loc)
295
+ }
296
+ if (indexed && maxStride > 1) {
297
+ warn('simd-aos-stride',
298
+ `'${fn}' indexed access stride ${maxStride} on loop counter — split into one typed array per field for SIMD (array-of-structures blocks vectorization)`,
299
+ { fn }, node.loc)
300
+ }
301
+ }
302
+ }
303
+ for (let i = 1; i < node.length; i++) walk(node[i])
304
+ }
305
+ walk(func.body)
306
+ }
307
+ }
308
+
309
+ /** Compile-time advisories at end of plan — extensible home for soft warnings. */
310
+ export function adviseProgram(programFacts) {
311
+ adviseHeapGrowth()
312
+ adviseSetMapIterationOrder()
313
+ if (programFacts) adviseJsstringCarrier(programFacts.paramReps, programFacts.valueUsed)
314
+ adviseSimdLoops()
315
+ }
316
+
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Shared AST utilities for plan/ subsystems.
3
+ *
4
+ * Helpers used by multiple subfiles (scalarize/loops/inline). Single-use helpers
5
+ * stay with their consumer.
6
+ *
7
+ * @module compile/plan/common
8
+ */
9
+
10
+ import { ctx } from '../../ctx.js'
11
+ import { ASSIGN_OPS, some, callArgs } from '../../ast.js'
12
+ import { constIntExpr } from '../../static.js'
13
+ import { typedElemCtor } from '../../type.js'
14
+ import { PASS_NAMES } from '../../optimize/index.js'
15
+
16
+ /** True iff the optimizer is active (any non-falsy pass under `ctx.transform.optimize`). */
17
+ export const optimizing = () => { const c = ctx.transform.optimize; return !!c && PASS_NAMES.some(n => c[n]) }
18
+
19
+ /** Ops whose body opens a new loop scope. (`for-in`/`for-of` excluded — they
20
+ * bind a fresh per-iter local on each entry, so jz lowers them differently.) */
21
+ export const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
22
+
23
+ /** Inline-substitution argument check — pure, side-effect-free, captures nothing. */
24
+ export const isSimpleArg = node => {
25
+ if (typeof node === 'string' || typeof node === 'number') return true
26
+ if (!Array.isArray(node)) return false
27
+ if (node[0] == null) return typeof node[1] === 'number'
28
+ if (node[0] === 'str') return typeof node[1] === 'string'
29
+ if (node[0] === 'u-' || (node[0] === '-' && node.length === 2)) return isSimpleArg(node[1])
30
+ if (['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(node[0]))
31
+ return isSimpleArg(node[1]) && isSimpleArg(node[2])
32
+ return false
33
+ }
34
+
35
+ /** Maximum loop nesting under `node`. Closures (`=>`) opaque — their loops don't count. */
36
+ export const loopDepth = (node, depth) => {
37
+ if (!Array.isArray(node)) return depth
38
+ if (node[0] === '=>') return depth
39
+ const here = LOOP_OPS.has(node[0]) ? depth + 1 : depth
40
+ let max = here
41
+ for (let i = 1; i < node.length; i++) {
42
+ const d = loopDepth(node[i], here)
43
+ if (d > max) max = d
44
+ }
45
+ return max
46
+ }
47
+
48
+ /** Node-count weight — drives inline cost heuristics. */
49
+ export const nodeSize = (node) => {
50
+ if (!Array.isArray(node)) return 1
51
+ let n = 1
52
+ for (let i = 1; i < node.length; i++) n += nodeSize(node[i])
53
+ return n
54
+ }
55
+
56
+ /** Collect every binder name in `node` into `out` (lexical-scope set; doesn't
57
+ * descend into nested arrow bodies — those open a new scope). */
58
+ export const collectBindings = (node, out) => {
59
+ if (!Array.isArray(node)) return
60
+ const op = node[0]
61
+ if (op === '=>') return
62
+ if (op === 'let' || op === 'const') {
63
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
64
+ }
65
+ for (let i = 1; i < node.length; i++) collectBindings(node[i], out)
66
+ }
67
+
68
+ const collectBindingTarget = (node, out) => {
69
+ if (typeof node === 'string') { out.add(node); return }
70
+ if (!Array.isArray(node)) return
71
+ if (node[0] === '=') collectBindingTarget(node[1], out)
72
+ else if (node[0] === '...' && typeof node[1] === 'string') out.add(node[1])
73
+ else if (node[0] === ',' || node[0] === '[]' || node[0] === '{}')
74
+ for (let i = 1; i < node.length; i++) collectBindingTarget(node[i], out)
75
+ }
76
+
77
+ /** True iff `node` writes to any name in `names` (incl. `++`/`--` and compound assigns). */
78
+ export const mutatesAny = (node, names) => some(node, n => {
79
+ const op = n[0]
80
+ if ((op === '++' || op === '--') && typeof n[1] === 'string') return names.has(n[1])
81
+ return ASSIGN_OPS.has(op) && typeof n[1] === 'string' && names.has(n[1])
82
+ })
83
+
84
+ /** Deep-clone array-tree AST. Plain values pass through by identity. */
85
+ export const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
86
+
87
+ /** AST index of a `for` loop's body, or null if `stmt` isn't a `for`.
88
+ * Handles both `['for', cond, body]` (3-arg while-like) and the full
89
+ * `['for', init, cond, step, body]` C-style form. */
90
+ export const forLoopBodyIndex = (stmt) =>
91
+ Array.isArray(stmt) && stmt[0] === 'for' ? (stmt.length === 3 ? 2 : 4) : null
92
+
93
+ /** Reconstruct a `for` node with its body replaced. Preserves arity. */
94
+ export const withForLoopBody = (stmt, body) =>
95
+ stmt.length === 3 ? ['for', stmt[1], body] : ['for', stmt[1], stmt[2], stmt[3], body]
96
+
97
+ // === Fixed-size typed-array recognition ===
98
+ // Shared by scalarize (replaces the literal with N locals) and inline (filters
99
+ // inlinings that would trample a typed-array param). Tables/thresholds live here
100
+ // so both subsystems agree on what "scalarizable" means.
101
+
102
+ /** Fixed-size typed-array ctors eligible for scalar replacement, mapped to the
103
+ * element store-coercion kind ('' = none, i.e. Float64Array's f64-identity).
104
+ * Excluded: Float32Array (Math.fround pulls module), Uint32Array (range > i32),
105
+ * Uint8ClampedArray (round-half-even clamp). Coerced (truthy) types are only
106
+ * scalarized when fully local. */
107
+ export const SCALAR_TYPED_COERCE = {
108
+ 'new.Float64Array': '',
109
+ 'new.Int32Array': 'i32',
110
+ 'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
111
+ 'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
112
+ }
113
+
114
+ // Default 64 covers the 8×8 block kernels (DCT/JPEG-shaped). Measured at 64
115
+ // elements: scalarized form is ~2.2× SMALLER (stores fold away; local refs
116
+ // out-LEB the memory ops they replace) and 2.5× faster than the memory form —
117
+ // there is no LEB128 cliff in practice.
118
+ export const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 64
119
+
120
+ /** Recognize `new TypedArrayCtor(N)` with `N` a static small integer.
121
+ * Returns `{len, coerce}` or null. */
122
+ export const fixedScalarTypedArray = (expr) => {
123
+ const ctor = typedElemCtor(expr)
124
+ if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
125
+ const args = callArgs(expr)
126
+ if (!args || args.length !== 1) return null
127
+ const len = constIntExpr(args[0])
128
+ return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
129
+ ? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
130
+ }
131
+
132
+ /** Map of name → {len, coerce} for every fixed-size typed-array binding declared
133
+ * via `let`/`const` directly in `body` (no descent into nested arrows). */
134
+ export const fixedTypedArraysInBody = (body) => {
135
+ const out = new Map()
136
+ const walk = node => {
137
+ if (!Array.isArray(node) || node[0] === '=>') return
138
+ if (node[0] === 'let' || node[0] === 'const') {
139
+ for (let i = 1; i < node.length; i++) {
140
+ const d = node[i]
141
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
142
+ const fixed = fixedScalarTypedArray(d[2])
143
+ if (fixed != null) out.set(d[1], fixed)
144
+ }
145
+ }
146
+ for (let i = 1; i < node.length; i++) walk(node[i])
147
+ }
148
+ walk(body)
149
+ return out
150
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Pre-emit compile planning: bridges prepare (AST shape) and emit (wasm bytes).
3
+ *
4
+ * # Stage contract
5
+ * IN: populated `ctx` from prepare.js (functions, schemas, scopes, modules)
6
+ * plus the prepared AST.
7
+ * OUT: returns a `programFacts` object; mutates `ctx` so each function has
8
+ * narrowed signatures, finalized global reps, and per-call decisions.
9
+ *
10
+ * # Pipeline (top-level `plan(ast)`)
11
+ * 1. unboxConstTypedGlobals — finalize global storage. (Global value facts
12
+ * themselves are seeded by prepare via `infer.recordGlobalRep`.)
13
+ * 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
14
+ * loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
15
+ * 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
16
+ * 4. Whole-program narrowing (skipped on simple programs):
17
+ * - narrowSignatures — pick a specialization per function from call sites
18
+ * - specializeBimorphicTyped — split typed-elem hot paths into two variants
19
+ * when callers diverge between two ctors
20
+ * - refineDynKeys — tighten dynamic property-key sets
21
+ *
22
+ * No bytes are emitted here; emit.js consumes the planned ctx + programFacts.
23
+ *
24
+ * @module plan
25
+ */
26
+
27
+ import { ctx } from '../../ctx.js'
28
+ import { collectProgramFacts, refreshProgramFacts } from '../program-facts.js'
29
+ import narrowSignatures, {
30
+ specializeBimorphicTyped, refineDynKeys,
31
+ applyJsstringBoundaryCarrierStandalone, narrowBoolResults,
32
+ } from '../narrow.js'
33
+
34
+ import { optimizing } from './common.js'
35
+ import { adviseProgram } from './advise.js'
36
+ import {
37
+ inferModuleLetTypes, unboxConstTypedGlobals, inferModuleIntGlobals,
38
+ flattenFuncNamespaces, devirtGlobalCalls,
39
+ materializeAutoBoxSchemas, resolveClosureWidth, canSkipWholeProgramNarrowing,
40
+ } from './scope.js'
41
+ import { inlineHotInternalCalls, inlineLocalLambdas, specializeFixedRestCalls } from './inline.js'
42
+ import { bindNestedRowLengths, unrollRowLenPadLoops, splitCharScanLoops } from './loops.js'
43
+ import {
44
+ scalarizeFunctionTypedArrays, scalarizeFunctionArrayLiterals,
45
+ promoteIntArrayLiterals, scalarizeFunctionObjectLiterals,
46
+ } from './literals.js'
47
+
48
+ export default function plan(ast, profiler) {
49
+ // Per-pass timing under `plan:` — the plan stage is the compile pipeline's
50
+ // multi-pass hot spot (each mutating pass triggers a whole-program fact
51
+ // refresh), so the profile must show WHICH pass and refresh dominate.
52
+ const t = profiler?.time ? (name, fn) => profiler.time(`plan:${name}`, fn) : (_, fn) => fn()
53
+ // AST-mutating pass: run timed; on change, re-sweep program facts (timed
54
+ // separately — the refreshes are usually the cost, not the passes).
55
+ let programFacts
56
+ const sweep = (name, pass) => {
57
+ if (t(name, pass)) programFacts = t('refreshFacts', () => refreshProgramFacts(ast, programFacts))
58
+ }
59
+
60
+ t('inferModuleLetTypes', () => inferModuleLetTypes(ast))
61
+ t('unboxConstTypedGlobals', unboxConstTypedGlobals)
62
+ t('inferModuleIntGlobals', () => inferModuleIntGlobals(ast))
63
+
64
+ programFacts = t('collectFacts', () => collectProgramFacts(ast))
65
+ // Function-namespace SROA — dissolve reassigned `f.prop` slots into module
66
+ // globals before inlining/narrowing, so all downstream passes see plain
67
+ // globals instead of the dynamic property machinery.
68
+ sweep('flattenFuncNamespaces', () => flattenFuncNamespaces(ast))
69
+ // Devirtualize calls through init-constant function globals (closure
70
+ // devirtualization) — must follow the SROA above, which creates the globals.
71
+ t('devirtGlobalCalls', () => devirtGlobalCalls(ast))
72
+ sweep('bindNestedRowLengths', bindNestedRowLengths)
73
+ sweep('unrollRowLenPadLoops', unrollRowLenPadLoops)
74
+ // The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
75
+ // is a pure speed optimization — the un-inlined calls emit correctly. Scalar
76
+ // replacement (`scalarize*`) and array promotion gate on `optimizing()`: off only
77
+ // under a fully-disabled optimizer, on for every enabled preset (incl. the
78
+ // `optimize:{sourceInline:false}` heap-elision-test form, which is level-2 based).
79
+ sweep('inlineHotInternalCalls', () => inlineHotInternalCalls(programFacts, ast))
80
+ sweep('bindNestedRowLengths', bindNestedRowLengths)
81
+ sweep('unrollRowLenPadLoops', unrollRowLenPadLoops)
82
+ sweep('inlineLocalLambdas', inlineLocalLambdas)
83
+ sweep('specializeFixedRestCalls', () => specializeFixedRestCalls(programFacts))
84
+ if (optimizing()) {
85
+ sweep('splitCharScan', splitCharScanLoops)
86
+ sweep('scalarizeArrayLiterals', scalarizeFunctionArrayLiterals)
87
+ sweep('scalarizeObjectLiterals', scalarizeFunctionObjectLiterals)
88
+ // Promotion runs AFTER literal scalarization (those that fully reduce to scalars
89
+ // are gone) and BEFORE typed-array scalarization (so a freshly-promoted array's
90
+ // fixed-length-typed-of-known-size variant could still participate in loop
91
+ // unrolling — currently it can't, since promotion produces the `[...]`-arg
92
+ // form rather than `new Int32Array(N)`, but the ordering keeps the door open).
93
+ sweep('promoteIntArrayLiterals', promoteIntArrayLiterals)
94
+ sweep('scalarizeTypedArrays', () => scalarizeFunctionTypedArrays(programFacts))
95
+ }
96
+ ctx.types.dynKeyVars = programFacts.dynVars
97
+ ctx.types.anyDynKey = programFacts.anyDyn
98
+
99
+ t('materializeAutoBoxSchemas', () => materializeAutoBoxSchemas(programFacts))
100
+ t('resolveClosureWidth', () => resolveClosureWidth(programFacts))
101
+ if (canSkipWholeProgramNarrowing(programFacts)) {
102
+ // Phase J (jsstring boundary opt-in) is body-local and call-site-independent;
103
+ // run it even when the rest of narrowing is skipped so simple `export let
104
+ // f = (s) => s.length` still flips to externref. Likewise the boolean-result
105
+ // fact, so `export let f = (a) => a > 2` boxes its boundary atom.
106
+ applyJsstringBoundaryCarrierStandalone(programFacts)
107
+ narrowBoolResults()
108
+ adviseProgram(programFacts)
109
+ return programFacts
110
+ }
111
+
112
+ t('narrowSignatures', () => narrowSignatures(programFacts, ast))
113
+ t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
114
+ t('refineDynKeys', () => refineDynKeys(programFacts))
115
+
116
+ adviseProgram(programFacts)
117
+ return programFacts
118
+ }