jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/ast.js ADDED
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Shared Jessie/subscript AST shape helpers and walks.
3
+ *
4
+ * Cycle-free: no ctx/analyze/ir imports. Shared with abi/* and ir.js.
5
+ *
6
+ * @module ast
7
+ */
8
+
9
+ /** Template placeholder in prepared AST (prepare.js). */
10
+ export const T = '\uE000'
11
+
12
+ // === Atom sentinels (shared by prepare + emit \u2014 keeps the stage boundary
13
+ // import-clean: emit must not reach into prepare for a constant) ===
14
+
15
+ // `null` and `undefined` are distinct NaN-box atoms (aux 1 vs 2), so they get
16
+ // distinct sentinels \u2014 collapsing both to one made `cond ? undefined : x`
17
+ // surface as `null` (the value flows through emit's symbol case, which can
18
+ // only carry one atom).
19
+ export const JZ_NULL = Symbol('null')
20
+ export const JZ_UNDEF = Symbol('undefined')
21
+
22
+ /** `typeof` comparison codes, keyed by the JS typeof string — negative so they
23
+ * can't collide with positive user-supplied PTR kinds in the same compare slot
24
+ * (prepare folds `typeof x == "number"` to `[op, ['typeof', x], [, TYPEOF.number]]`;
25
+ * emit's emitTypeofCmp and flow-types' refinements dispatch on the codes).
26
+ * Null-proto: prepare indexes it with arbitrary user strings — a plain literal
27
+ * would leak `constructor`/`toString` through the lookup. */
28
+ export const TYPEOF = Object.freeze(Object.assign(Object.create(null), {
29
+ number: -1, string: -2, undefined: -3, boolean: -4, object: -5, 'function': -6, bigint: -7,
30
+ }))
31
+
32
+ // === Numeric range (shared by analyze + ir) ===
33
+
34
+ export const I32_MIN = -2147483648
35
+ export const I32_MAX = 2147483647
36
+ export const isI32 = (v) => Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX && !Object.is(v, -0)
37
+
38
+ // === Statement / block-body classification ===
39
+
40
+ /** Statement operators — distinguish block bodies from object literals. */
41
+ export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'for-in', 'while', 'break', 'continue', 'switch',
42
+ '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
43
+ 'throw', 'try', 'catch', 'finally', '++', '--', '()'])
44
+
45
+ /** jzify superset: pre-lowered JS shapes before prepare strips them. */
46
+ export const JZ_BLOCK_OPS = new Set([...STMT_OPS, 'var', 'for-of', 'do', 'function', 'class', 'import', 'export', 'label', 'case', 'default'])
47
+
48
+ /** Valid labeled-statement bodies in jzify. */
49
+ export const LABEL_BODY_OPS = new Set([';', 'if', 'for', 'for-in', 'for-of', 'while', 'do', 'switch', 'try', 'throw'])
50
+
51
+ /** Statement-only ops: heads that can never be a concise arrow *value* body.
52
+ * A concise-body arrow with one of these can only have come from method/function
53
+ * shorthand the parser unwrapped (`m(){ if … }` → `['=>', p, ['if', …]]`), so it
54
+ * must be re-blocked. Excludes `function`/`class` (those ARE expression bodies,
55
+ * e.g. `() => function(){}`), assignment/update/call, and switch-internal
56
+ * `case`/`default`. */
57
+ export const STMT_ONLY_OPS = new Set([';', 'if', 'for', 'for-in', 'for-of', 'while', 'do', 'switch',
58
+ 'return', 'break', 'continue', 'throw', 'try', 'let', 'const', 'var', 'label'])
59
+
60
+ /** Distinguish a function block body `{ … }` from an expression object literal `({a:1})`. */
61
+ export const isBlockBody = (body) =>
62
+ Array.isArray(body) && body[0] === '{}' && (body.length === 1 || STMT_OPS.has(body[1]?.[0]))
63
+
64
+ // === AST node classifiers ===
65
+
66
+ export const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
67
+ export const isFuncRef = (node, funcNames) => typeof node === 'string' && funcNames.has(node)
68
+
69
+ // === Assignment / reassignment ===
70
+
71
+ /** Assignment operators — shared across analyze, plan, emit, abi. */
72
+ export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
73
+
74
+ /** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`. */
75
+ export function isReassigned(body, name) {
76
+ if (!Array.isArray(body)) return false
77
+ const op = body[0]
78
+ if (ASSIGN_OPS.has(op) && body[1] === name) return true
79
+ if ((op === '++' || op === '--') && body[1] === name) return true
80
+ if (op === 'let' || op === 'const') {
81
+ for (let i = 1; i < body.length; i++) {
82
+ const d = body[i]
83
+ if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
84
+ }
85
+ return false
86
+ }
87
+ for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
88
+ return false
89
+ }
90
+
91
+ // A deeply-constant array literal (every element a compile-time literal), safe to
92
+ // allocate once and share. At emit time an array literal is `['[', e0, e1, …]` (flat
93
+ // elements); an INDEX access is `['[]', base, idx]` (op `[]`) — NOT a literal.
94
+ export function isConstLiteral(node) {
95
+ if (!Array.isArray(node)) return false
96
+ const op = node[0]
97
+ if (op == null) return true // [null, n] / [,bool] / [null,null] primitive
98
+ if (op === '[') { for (let i = 1; i < node.length; i++) if (!isConstLiteral(node[i])) return false; return true }
99
+ return false
100
+ }
101
+
102
+ // Is `node` a pure reference-projection of a tainted name? (`t`, `t[i]`, `t.p`, chained) —
103
+ // a value that aliases into the shared literal, so it must be tracked too.
104
+ const isProjection = (node, tainted) => {
105
+ if (typeof node === 'string') return tainted.has(node)
106
+ return Array.isArray(node) && (node[0] === '[]' || node[0] === '.') && node.length === 3 && isProjection(node[1], tainted)
107
+ }
108
+ const baseIsTainted = (m, tainted) => isProjection(m, tainted) // m is a [] / . access node
109
+
110
+ // Could a single shared allocation of `name` be observed to differ from per-iteration
111
+ // allocation within `body`? It can't iff `name` and every alias derived from it by pure
112
+ // reference-projection is only ever READ (as a `[]`/`.` base) — never written through,
113
+ // method-called, or escaped (bare value, call arg, return, comparison). Then hoisting the
114
+ // allocation out of a loop is sound. Conservative: any use it can't classify as a read
115
+ // bails out (returns true = "not hoistable").
116
+ export function constLiteralHoistable(body, name) {
117
+ const decls = []
118
+ ;(function collect(n) {
119
+ if (!Array.isArray(n)) return
120
+ if (n[0] === 'const' || n[0] === 'let')
121
+ for (let i = 1; i < n.length; i++) { const d = n[i]; if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') decls.push(d) }
122
+ for (let i = 1; i < n.length; i++) collect(n[i])
123
+ })(body)
124
+ const tainted = new Set([name])
125
+ for (let changed = true; changed;) {
126
+ changed = false
127
+ for (const d of decls) if (!tainted.has(d[1]) && isProjection(d[2], tainted)) { tainted.add(d[1]); changed = true }
128
+ }
129
+ // unsafe use of any tainted name?
130
+ return !(function bad(node) {
131
+ if (typeof node === 'string') return tainted.has(node) // bare tainted value = escape
132
+ if (!Array.isArray(node)) return false
133
+ const op = node[0]
134
+ // decl: the bound names are bindings, not uses — only the initializer values matter
135
+ if (op === 'const' || op === 'let') {
136
+ for (let i = 1; i < node.length; i++) { const d = node[i]; if (Array.isArray(d) && d[0] === '=') { if (bad(d[2])) return true } else if (bad(d)) return true }
137
+ return false
138
+ }
139
+ // read of a tainted base (`t[i]` / `t.p`): base is fine, only the index expr can be unsafe
140
+ if ((op === '[]' || op === '.') && node.length === 3 && typeof node[1] === 'string' && tainted.has(node[1]))
141
+ return op === '[]' ? bad(node[2]) : false
142
+ if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && tainted.has(node[1])) return true // reassign the binding
143
+ if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && (node[1][0] === '[]' || node[1][0] === '.') && baseIsTainted(node[1], tainted)) return true // write through
144
+ if ((op === '++' || op === '--') && typeof node[1] === 'string' && tainted.has(node[1])) return true
145
+ if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && baseIsTainted(node[1], tainted)) return true // method call
146
+ for (let i = 1; i < node.length; i++) if (bad(node[i])) return true
147
+ return false
148
+ })(body)
149
+ }
150
+
151
+ // Sound over-approximation: could `name`'s array length change anywhere in `body`?
152
+ // True if it is reassigned, has a length-mutating method called on it (push/pop/shift/
153
+ // unshift/splice), is assigned through (`name.x = …` / `name[i] = …`, the latter may grow),
154
+ // or is handed to a call as an argument (a callee might push to it). Lets a plain array's
155
+ // `arr.length` loop bound be hoisted when this is false (see immutableLenBound).
156
+ export function mutatesArrayLength(body, name) {
157
+ if (!Array.isArray(body)) return false
158
+ const op = body[0]
159
+ if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && body[1] === name) return true
160
+ // write through `name` (`name.x = …`, `name[i] = …` — index write may extend length)
161
+ if (ASSIGN_OPS.has(op) && Array.isArray(body[1]) && (body[1][0] === '.' || body[1][0] === '[]') && body[1][1] === name) return true
162
+ if (op === '()') {
163
+ // method call on `name` (`name.push(…)` etc.) — any method, to stay sound
164
+ if (Array.isArray(body[1]) && body[1][0] === '.' && body[1][1] === name) return true
165
+ // `name` passed as a call argument — the callee could mutate it
166
+ for (let i = 2; i < body.length; i++) if (body[i] === name) return true
167
+ }
168
+ for (let i = 1; i < body.length; i++) if (mutatesArrayLength(body[i], name)) return true
169
+ return false
170
+ }
171
+
172
+ /** Normalize a call's raw arg slot: null → [], comma-group → elems, else singleton. */
173
+ export function commaList(raw) {
174
+ if (raw == null) return []
175
+ return Array.isArray(raw) && raw[0] === ',' ? raw.slice(1) : [raw]
176
+ }
177
+
178
+ /** Args of a `['()', callee, raw]` node, or null when `node` is not a call. */
179
+ export function callArgs(node) {
180
+ if (!Array.isArray(node) || node[0] !== '()') return null
181
+ return commaList(node[2])
182
+ }
183
+
184
+ /** Write normalized args back onto a call node. */
185
+ export function setCallArgs(node, args) {
186
+ node[2] = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
187
+ }
188
+
189
+ /** Unwrap handler/rest `args` when the sole element is a comma-group. */
190
+ export function spreadArgs(args) {
191
+ if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') return args[0].slice(1)
192
+ return args
193
+ }
194
+
195
+ export const isSeq = node => Array.isArray(node) && node[0] === ';'
196
+
197
+ /** Statement list inside a block `{…}`; null when `body` is not a block. */
198
+ export function blockStmts(body) {
199
+ if (!Array.isArray(body) || body[0] !== '{}') return null
200
+ const inner = body[1]
201
+ if (!Array.isArray(inner)) return inner == null ? [] : [inner]
202
+ return inner[0] === ';' ? inner.slice(1) : [inner]
203
+ }
204
+
205
+ /** Flatten a block/seq/single-stmt body into a statement array. */
206
+ export function stmtList(body) {
207
+ if (!Array.isArray(body)) return body == null ? [] : [body]
208
+ if (body[0] === '{}') return stmtList(body[1])
209
+ if (body[0] === ';') return body.slice(1)
210
+ return [body]
211
+ }
212
+
213
+ /** Handler/rest args with comma unwrap and null drop (jzify/prepare). */
214
+ export function handlerArgs(args) {
215
+ return spreadArgs(args).filter(a => a != null)
216
+ }
217
+
218
+ /** Early-exit walk; skips into `=>` bodies by default. */
219
+ export function some(node, pred, { skipArrow = true } = {}) {
220
+ if (!Array.isArray(node)) return false
221
+ if (pred(node)) return true
222
+ if (skipArrow && node[0] === '=>') return false
223
+ for (let i = 1; i < node.length; i++) if (some(node[i], pred, { skipArrow })) return true
224
+ return false
225
+ }
226
+
227
+ /** Options for {@link refsName} / {@link refsAny}. */
228
+ // skipArrow (default true): stop at `=>` boundaries — matches `some()`.
229
+ // skipStr: don't descend into `str` literal nodes.
230
+ // skipBindingPositions: on `.`/`?.` recurse only the receiver; on `:` only the value.
231
+
232
+ /** Expression-position name refs: descends into `=>`, skips literal keys and `str`. */
233
+ export const REFS_IN_EXPR = { skipArrow: false, skipStr: true, skipBindingPositions: true }
234
+
235
+ /** True if bare identifier `name` appears anywhere in `node`. */
236
+ export function refsName(node, name, opts = {}) {
237
+ const skipArrow = opts.skipArrow !== false
238
+ if (typeof node === 'string') return node === name
239
+ if (!Array.isArray(node)) return false
240
+ const op = node[0]
241
+ if (skipArrow && op === '=>') return false
242
+ if (opts.skipStr && op === 'str') return false
243
+ if (opts.skipBindingPositions) {
244
+ if (op === '.' || op === '?.') return refsName(node[1], name, opts)
245
+ if (op === ':') return refsName(node[2], name, opts)
246
+ }
247
+ for (let i = 1; i < node.length; i++) if (refsName(node[i], name, opts)) return true
248
+ return false
249
+ }
250
+
251
+ /** True if any name in `names` (Set) appears in `node`. Same options as refsName. */
252
+ export function refsAny(node, names, opts = {}) {
253
+ if (!names?.size) return false
254
+ if (typeof node === 'string') return names.has(node)
255
+ if (!Array.isArray(node)) return false
256
+ const op = node[0]
257
+ if (opts.skipArrow !== false && op === '=>') return false
258
+ if (opts.skipStr && op === 'str') return false
259
+ if (opts.skipBindingPositions) {
260
+ if (op === '.' || op === '?.') return refsAny(node[1], names, opts)
261
+ if (op === ':') return refsAny(node[2], names, opts)
262
+ }
263
+ for (let i = 1; i < node.length; i++) if (refsAny(node[i], names, opts)) return true
264
+ return false
265
+ }
266
+
267
+ const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
268
+
269
+ /** Does `body` contain return/throw/break/continue (not inside nested `=>`)? */
270
+ export function hasControlTransfer(body) {
271
+ if (!Array.isArray(body)) return false
272
+ if (CONTROL_TRANSFER.has(body[0])) return true
273
+ if (body[0] === '=>') return false
274
+ for (let i = 1; i < body.length; i++) if (hasControlTransfer(body[i])) return true
275
+ return false
276
+ }
277
+
278
+ /** Does `body` contain a `continue` that targets THIS loop? */
279
+ export function hasOwnContinue(body) {
280
+ if (!Array.isArray(body)) return false
281
+ const op = body[0]
282
+ if (op === 'continue') return true
283
+ if (op === 'for' || op === 'while' || op === 'do') return false
284
+ for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
285
+ return false
286
+ }
287
+
288
+ /** Does `body` contain `continue <label>` targeting the given label? Descends through nested
289
+ * loops (a labeled continue crosses loop boundaries) but not into closures. */
290
+ export function hasLabeledContinueTo(body, label) {
291
+ if (!Array.isArray(body)) return false
292
+ const op = body[0]
293
+ if (op === 'continue' && body[1] === label) return true
294
+ if (op === '=>') return false
295
+ for (let i = 1; i < body.length; i++) if (hasLabeledContinueTo(body[i], label)) return true
296
+ return false
297
+ }
298
+
299
+ export function hasOwnBreakOrContinue(body) {
300
+ if (!Array.isArray(body)) return false
301
+ const op = body[0]
302
+ if (op === 'break' || op === 'continue') return true
303
+ if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
304
+ for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
305
+ return false
306
+ }
307
+
308
+ // === Arrow param normalization ===
309
+
310
+
311
+ export function extractParams(rawParams) {
312
+ let p = rawParams
313
+ if (Array.isArray(p) && p[0] === '()') p = p[1]
314
+ return p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
315
+ }
316
+
317
+ export function classifyParam(r) {
318
+ if (Array.isArray(r) && r[0] === '...') return { kind: 'rest', name: r[1] }
319
+ if (Array.isArray(r) && r[0] === '=') {
320
+ if (typeof r[1] === 'string') return { kind: 'default', name: r[1], defValue: r[2] }
321
+ return { kind: 'destruct-default', pattern: r[1], defValue: r[2] }
322
+ }
323
+ if (Array.isArray(r) && (r[0] === '[]' || r[0] === '{}')) return { kind: 'destruct', pattern: r }
324
+ return { kind: 'plain', name: r }
325
+ }
326
+
327
+ export function collectParamNames(raw, out = new Set()) {
328
+ for (const r of raw) {
329
+ if (typeof r === 'string') out.add(r)
330
+ else if (Array.isArray(r)) {
331
+ if (r[0] === '=' && typeof r[1] === 'string') out.add(r[1])
332
+ else if (r[0] === '...' && typeof r[1] === 'string') out.add(r[1])
333
+ else if (r[0] === '=' && Array.isArray(r[1])) collectParamNames([r[1]], out)
334
+ else if (r[0] === '[]' || r[0] === '{}' || r[0] === ',') collectParamNames(r.slice(1), out)
335
+ }
336
+ }
337
+ return out
338
+ }
339
+
340
+ // === Return-path queries (narrowing) ===
341
+
342
+ const collectReturnExprs = (node, out) => {
343
+ if (!Array.isArray(node)) return
344
+ const [op, ...args] = node
345
+ if (op === '=>') return
346
+ if (op === 'return') { if (args[0] != null) out.push(args[0]); return }
347
+ for (const a of args) collectReturnExprs(a, out)
348
+ }
349
+
350
+ export const alwaysReturns = (n) => {
351
+ if (!Array.isArray(n)) return false
352
+ const op = n[0]
353
+ if (op === '=>') return false
354
+ if (op === 'return' || op === 'throw') return true
355
+ if (op === '{}' || op === ';') return alwaysReturns(n[n.length - 1])
356
+ if (op === 'if') return n.length >= 4 && alwaysReturns(n[2]) && alwaysReturns(n[3])
357
+ return false
358
+ }
359
+
360
+ export const hasBareReturn = (n) => {
361
+ if (!Array.isArray(n)) return false
362
+ if (n[0] === '=>') return false
363
+ if (n[0] === 'return' && n[1] == null) return true
364
+ return n.some(hasBareReturn)
365
+ }
366
+
367
+ export const returnExprs = (body) => {
368
+ if (isBlockBody(body)) {
369
+ const out = []
370
+ collectReturnExprs(body, out)
371
+ return out
372
+ }
373
+ return [body]
374
+ }
375
+
376
+ // === Clone / compare / module body / bare refs ===
377
+
378
+ /** Deep-clone an AST node (arrays only; primitives pass through). */
379
+ export function cloneNode(node) {
380
+ if (node == null || typeof node !== 'object') return node
381
+ if (!Array.isArray(node)) return node
382
+ return node.map(cloneNode)
383
+ }
384
+
385
+ /** Structural equality via JSON. AST nodes are JSON-serializable except i64.const
386
+ * BigInt payloads (NaN-box prefixes — dcbb433 routes pointer offsets through boxed
387
+ * forms); the replacer stringifies those as `<n>n` (cf. formatErrorNode in ctx.js). */
388
+ export const bigintSafeKey = (_k, v) => typeof v === 'bigint' ? `${v}n` : v
389
+ export function nodeEqual(a, b) {
390
+ return JSON.stringify(a, bigintSafeKey) === JSON.stringify(b, bigintSafeKey)
391
+ }
392
+
393
+ /** Property entries of an object-literal AST node (`['{}', …]`). */
394
+ export function descriptorProps(node) {
395
+ if (!Array.isArray(node) || node[0] !== '{}') return null
396
+ const body = node[1]
397
+ if (body == null) return []
398
+ if (Array.isArray(body) && body[0] === ',') return body.slice(1)
399
+ return [body]
400
+ }
401
+
402
+ /** Comma-unwrapped entries from an object-literal constructor arg list. */
403
+ export function objectLiteralEntries(args) {
404
+ const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
405
+ return raw.filter(p => p != null)
406
+ }
407
+
408
+ /** String literal node → string, or null. */
409
+ export function literalString(node) {
410
+ return Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
411
+ }
412
+
413
+ /** Zero numeric literal node. */
414
+ export function isZeroLiteral(node) {
415
+ return Array.isArray(node) && node[0] == null && node[1] === 0
416
+ }
417
+
418
+ /** Top-level module statements; null when `ast` is not module-shaped. */
419
+ export function moduleStmts(ast) {
420
+ if (!Array.isArray(ast)) return null
421
+ return ast[0] === ';' ? ast.slice(1).filter(Boolean) : [ast]
422
+ }
423
+
424
+ /** Unwrap esbuild module binding to `[name, init]`, or null. */
425
+ export function bindingOf(stmt) {
426
+ if (!Array.isArray(stmt)) return null
427
+ if (stmt[0] === '=' && typeof stmt[1] === 'string') return [stmt[1], stmt[2]]
428
+ if ((stmt[0] === 'let' || stmt[0] === 'const' || stmt[0] === 'var') && stmt.length === 2 &&
429
+ Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string')
430
+ return [stmt[1][1], stmt[1][2]]
431
+ return null
432
+ }
433
+
434
+ /** Identifier refs in value positions (skip decl names, member keys, object keys). */
435
+ export function collectBareRefs(node, out) {
436
+ if (typeof node === 'string') return void out.add(node)
437
+ if (!Array.isArray(node)) return
438
+ if (node[0] === 'let' || node[0] === 'const' || node[0] === 'var') {
439
+ for (let i = 1; i < node.length; i++)
440
+ if (Array.isArray(node[i]) && node[i][0] === '=') collectBareRefs(node[i][2], out)
441
+ } else if ((node[0] === '.' || node[0] === '?.') && typeof node[2] === 'string') {
442
+ collectBareRefs(node[1], out)
443
+ } else if (node[0] === ':') {
444
+ collectBareRefs(node[2], out)
445
+ } else {
446
+ for (let i = 1; i < node.length; i++) collectBareRefs(node[i], out)
447
+ }
448
+ }
449
+
450
+ /** Deep walk: `pred` on every node including bare identifiers; descends into arrows. */
451
+ export function someDeep(node, pred) {
452
+ if (pred(node)) return true
453
+ if (!Array.isArray(node)) return false
454
+ for (let i = 1; i < node.length; i++) if (someDeep(node[i], pred)) return true
455
+ return false
456
+ }
457
+
458
+ /** Alias for {@link extractParams}. */
459
+ export const paramList = extractParams
460
+
package/src/autoload.js CHANGED
@@ -5,6 +5,10 @@ import * as mods from '../module/index.js'
5
5
 
6
6
  const dict = obj => Object.assign(Object.create(null), obj)
7
7
 
8
+ export const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', Date: 'date', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string',
9
+ // SIMD intrinsic namespaces (f32x4/i32x4/f64x2/v128) all live in the `simd` module.
10
+ f32x4: 'simd', i32x4: 'simd', f64x2: 'simd', v128: 'simd' }
11
+
8
12
  export const PROP_MODULES = Object.assign(Object.create(null), {
9
13
  push: ['core', 'array'], pop: ['core', 'array'], shift: ['core', 'array'], unshift: ['core', 'array'],
10
14
  splice: ['core', 'array'], reverse: ['core', 'array'], sort: ['core', 'array'], fill: ['core', 'array'],
@@ -12,7 +16,7 @@ export const PROP_MODULES = Object.assign(Object.create(null), {
12
16
  forEach: ['core', 'array'], find: ['core', 'array'], findIndex: ['core', 'array'],
13
17
  findLast: ['core', 'array'], findLastIndex: ['core', 'array'],
14
18
  every: ['core', 'array'], some: ['core', 'array'], flat: ['core', 'array'], flatMap: ['core', 'array'],
15
- join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'array'],
19
+ join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'string', 'array'],
16
20
  charAt: ['core', 'string'], charCodeAt: ['core', 'string'], codePointAt: ['core', 'string'],
17
21
  toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], toLocaleLowerCase: ['core', 'string'], trim: ['core', 'string'],
18
22
  trimStart: ['core', 'string'], trimEnd: ['core', 'string'],
@@ -46,6 +50,8 @@ export const OP_MODULES = {
46
50
  '**': ['math'],
47
51
  }
48
52
 
53
+ export const TYPED_CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
54
+
49
55
  export const CALL_MODULES = dict({
50
56
  ArrayBuffer: ['core', 'typedarray'],
51
57
  DataView: ['core', 'typedarray'],
@@ -75,6 +81,7 @@ export const CALL_MODULES = dict({
75
81
  'Object.assign': ['core', 'object'],
76
82
  'Object.create': ['core', 'object'],
77
83
  'Object.defineProperty': ['core', 'object'],
84
+ '__object_toString': ['core', 'object', 'string'],
78
85
  'Date.UTC': ['core', 'date'],
79
86
  'Date.parse': ['core', 'date'],
80
87
  'Date.now': ['core', 'console'],
@@ -84,14 +91,7 @@ export const CALL_MODULES = dict({
84
91
  'String.fromCodePoint': ['core', 'string'],
85
92
  'BigInt.asIntN': ['number'],
86
93
  'BigInt.asUintN': ['number'],
87
- 'Float64Array.from': ['core', 'typedarray', 'array'],
88
- 'Float32Array.from': ['core', 'typedarray', 'array'],
89
- 'Int32Array.from': ['core', 'typedarray', 'array'],
90
- 'Uint32Array.from': ['core', 'typedarray', 'array'],
91
- 'Int16Array.from': ['core', 'typedarray', 'array'],
92
- 'Uint16Array.from': ['core', 'typedarray', 'array'],
93
- 'Int8Array.from': ['core', 'typedarray', 'array'],
94
- 'Uint8Array.from': ['core', 'typedarray', 'array'],
94
+ ...Object.fromEntries(TYPED_CTORS.filter(n => n.endsWith('Array')).map(n => [`${n}.from`, ['core', 'typedarray', 'array']])),
95
95
  'ArrayBuffer.isView': ['core', 'typedarray'],
96
96
  // instanceof Map / Set / TypedArray predicates (synthesized by jzify).
97
97
  '__is_map': ['core', 'collection'],
@@ -107,13 +107,15 @@ export const GENERIC_METHOD_MODULES = dict({
107
107
  hasOwnProperty: ['core', 'object', 'string', 'collection'],
108
108
  })
109
109
 
110
- export const CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','Set','Map','Date']
111
- export const TYPED_CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
112
- export const COLLECTION_CTORS = ['Set', 'Map']
110
+ export const CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','Set','Map','WeakSet','WeakMap','Date']
111
+ // WeakSet/WeakMap fold to Set/Map (the `new` handler rewrites the ctor name).
112
+ // jz has no GC, so weakness is unobservable; this also accepts primitive keys
113
+ // (real WeakMap throws TypeError) and exposes `.size`/iteration — a deliberate
114
+ // semantic deviation documented in README. Compilers lean on them as identity
115
+ // caches / cycle-detection sets and never observe the missing weak semantics.
116
+ export const COLLECTION_CTORS = ['Set', 'Map', 'WeakSet', 'WeakMap']
113
117
  export const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'])
114
118
 
115
- export const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', Date: 'date', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string' }
116
-
117
119
  const MOD_DEPS = {
118
120
  number: ['core', 'string'],
119
121
  string: ['core', 'number'],
@@ -125,6 +127,19 @@ const MOD_DEPS = {
125
127
  date: ['core', 'number', 'string'],
126
128
  console: ['core', 'string', 'number'],
127
129
  regex: ['core', 'string', 'array'],
130
+ // f64x2.sin/cos lower to math's $math.sin2/$math.cos2 WAT helpers; pull math in so
131
+ // they're registered. Unused math helpers are pruned by reachability — no output cost.
132
+ simd: ['math'],
133
+ }
134
+
135
+ export function includeModule(name) {
136
+ const modName = MOD_ALIAS[name] || name
137
+ const init = mods[modName]
138
+ if (!init) return err(`Module not found: ${name}`)
139
+ if (ctx.module.modules[modName]) return
140
+ ctx.module.modules[modName] = true
141
+ for (const dep of MOD_DEPS[modName] || []) includeModule(dep)
142
+ init(ctx)
128
143
  }
129
144
 
130
145
  export const hasModule = name => Boolean(mods[MOD_ALIAS[name] || name])
@@ -186,13 +201,3 @@ export const includeForRuntimeCtor = name => {
186
201
  else if (kind === 'array') includeMods('core', 'array')
187
202
  return kind
188
203
  }
189
-
190
- export function includeModule(name) {
191
- const modName = MOD_ALIAS[name] || name
192
- const init = mods[modName]
193
- if (!init) return err(`Module not found: ${name}`)
194
- if (ctx.module.modules[modName]) return
195
- ctx.module.modules[modName] = true
196
- for (const dep of MOD_DEPS[modName] || []) includeModule(dep)
197
- init(ctx)
198
- }