jz 0.5.0 → 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 +281 -142
  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 +461 -185
  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 +591 -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} +600 -205
  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 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /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'],
@@ -127,6 +129,16 @@ const MOD_DEPS = {
127
129
  regex: ['core', 'string', 'array'],
128
130
  }
129
131
 
132
+ export function includeModule(name) {
133
+ const modName = MOD_ALIAS[name] || name
134
+ const init = mods[modName]
135
+ if (!init) return err(`Module not found: ${name}`)
136
+ if (ctx.module.modules[modName]) return
137
+ ctx.module.modules[modName] = true
138
+ for (const dep of MOD_DEPS[modName] || []) includeModule(dep)
139
+ init(ctx)
140
+ }
141
+
130
142
  export const hasModule = name => Boolean(mods[MOD_ALIAS[name] || name])
131
143
 
132
144
  export const includeMods = (...names) => names.forEach(includeModule)
@@ -186,13 +198,3 @@ export const includeForRuntimeCtor = name => {
186
198
  else if (kind === 'array') includeMods('core', 'array')
187
199
  return kind
188
200
  }
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
- }
package/src/bridge.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Stdlib module bridge — `module/*` imports from here, not `src/compile/emit.js`.
3
+ *
4
+ * Emit impls bind on `ctx.bridge` at reset(). Registration: `wat(name, body)`
5
+ * for WAT stdlib, `reg(name, deps, fn)` for emit, or `reg(name, { deps, wat, emit })`
6
+ * to co-register both. `method`/`call` remain sugar for simple `$stdlib` calls.
7
+ *
8
+ * @module bridge
9
+ */
10
+
11
+ import { ctx, emitter } from './ctx.js'
12
+ import { typed, asF64, asI32, asI64 } from './ir.js'
13
+
14
+ export { emitter } from './ctx.js'
15
+
16
+ export const emit = (...a) => ctx.bridge.emit(...a)
17
+ export const flat = (...a) => ctx.bridge.flat(...a)
18
+ export const body = (...a) => ctx.bridge.body(...a)
19
+ export const bool = (...a) => ctx.bridge.bool(...a)
20
+ /** Index expr → i32 IR. */
21
+ export const idx = (...a) => ctx.bridge.idx(...a)
22
+ export const spread = (...a) => ctx.bridge.spread(...a)
23
+
24
+ /** Attach a pre-built handler (e.g. from method/emitter) to ctx.core.emit. */
25
+ export const bind = (name, handler) => {
26
+ ctx.core.emit[name] = handler
27
+ return handler
28
+ }
29
+
30
+ /** Register a host import once, idempotent on (module, name). Stdlib modules
31
+ * call this from each use site without re-adding the env/wasi import. */
32
+ export const hostImport = (mod, name, fn) => {
33
+ if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
34
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
35
+ }
36
+
37
+ /** WAT stdlib→stdlib deps for `resolveIncludes()`. */
38
+ export const deps = (map) => Object.assign(ctx.core.stdlibDeps, map)
39
+
40
+ /** WAT stdlib body (+ optional deps edge for resolveIncludes). */
41
+ export const wat = (name, body, depNames = []) => {
42
+ ctx.core.stdlib[name] = body
43
+ if (depNames.length) deps({ [name]: depNames })
44
+ }
45
+
46
+ /** Emit handler; optionally co-register WAT when `depsOrOpts.wat` is set.
47
+ * reg(name, deps, fn) — emit only
48
+ * reg(name, { deps, wat, emit }) — WAT key inferred from first `__…` dep
49
+ * reg(name, { watKey, deps, wat, emit }) — explicit WAT key when deps differ */
50
+ export const reg = (name, depsOrOpts, maybeFn) => {
51
+ if (typeof depsOrOpts === 'object' && depsOrOpts !== null && !Array.isArray(depsOrOpts)) {
52
+ const o = depsOrOpts
53
+ const depsList = o.deps ?? []
54
+ if (o.wat) {
55
+ const watKey = o.watKey ?? depsList.find(d => d.startsWith('__')) ?? name
56
+ wat(watKey, o.wat, o.watDeps ?? [])
57
+ }
58
+ if (o.emit) {
59
+ const h = emitter(depsList, o.emit)
60
+ ctx.core.emit[name] = h
61
+ return h
62
+ }
63
+ return
64
+ }
65
+ const h = emitter(depsOrOpts, maybeFn)
66
+ ctx.core.emit[name] = h
67
+ return h
68
+ }
69
+
70
+ /** Tag a hand-wrapped handler with `.deps` (pow/** dual lowering). */
71
+ export const tag = (handler, deps) => {
72
+ handler.deps = deps
73
+ return handler
74
+ }
75
+
76
+ /** `fast(firstArg)` → `core`, else `wrap`. Keeps wrap `.deps`. */
77
+ export const dual = (wrap, core, fast) => {
78
+ const h = (a, ...rest) => (fast(a) ? core(a, ...rest) : wrap(a, ...rest))
79
+ h.deps = wrap.deps
80
+ h.argc = wrap.argc ?? wrap.length
81
+ return h
82
+ }
83
+
84
+ const cast = { I: asI64, F: asF64, i: asI32 }
85
+
86
+ const coerce = (sig, nodes) =>
87
+ sig.split('').map((c, i) => cast[c](emit(nodes[i])))
88
+
89
+ const wrap = (fmt, call) => {
90
+ if (fmt === 'i64') return typed(['f64.reinterpret_i64', call], 'f64')
91
+ if (fmt === 'i32') return typed(['f64.convert_i32_s', call], 'f64')
92
+ return typed(call, 'f64')
93
+ }
94
+
95
+ /** `(…args) → call($stdlib, coerced…)`. fmt: f64 · i64 · i32 */
96
+ export const call = (stdlib, sig, fmt = 'f64') => {
97
+ const h = emitter([stdlib], (...nodes) =>
98
+ wrap(fmt, ['call', `$${stdlib}`, ...coerce(sig, nodes)]))
99
+ h.argc = sig.length
100
+ return h
101
+ }
102
+
103
+ /** method `(recv, …args) → call($stdlib, …)`. sig: I · F · i per arg. */
104
+ export const method = (stdlib, sig, ret = 'f64') => {
105
+ const h = emitter([stdlib], (...nodes) => {
106
+ const c = ['call', `$${stdlib}`, ...coerce(sig, nodes)]
107
+ return typed(ret === 'i32' ? ['f64.convert_i32_s', c] : c, 'f64')
108
+ })
109
+ h.argc = sig.length
110
+ return h
111
+ }