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,106 @@
1
+ /**
2
+ * switch → if/else + fall-through lowering.
3
+ * @module jzify/switch
4
+ */
5
+
6
+ /** Flatten a switch clause body to a single node, dropping ASI position markers.
7
+ * Unlike a plain statement list this keeps any `break` intact — transformSwitch
8
+ * needs the breaks to gate fall-through; it rewrites them to a sticky flag. */
9
+ export function normalizeCaseBody(body) {
10
+ if (!Array.isArray(body) || body[0] !== ';') return body
11
+ const stmts = body.slice(1).filter(s => s != null && typeof s !== 'number')
12
+ return stmts.length === 0 ? null : stmts.length === 1 ? stmts[0] : [';', ...stmts]
13
+ }
14
+
15
+ const SWITCH_BREAK_BOUNDARIES = new Set(['for', 'for-in', 'for-of', 'while', 'do', 'switch', '=>', 'function', 'class'])
16
+
17
+ function hasOwnSwitchBreak(node) {
18
+ if (!Array.isArray(node)) return false
19
+ if (node[0] === 'break') return true
20
+ if (SWITCH_BREAK_BOUNDARIES.has(node[0])) return false
21
+ for (let i = 1; i < node.length; i++) if (hasOwnSwitchBreak(node[i])) return true
22
+ return false
23
+ }
24
+
25
+ function rewriteSwitchBreaks(node, flag) {
26
+ if (!Array.isArray(node)) return node
27
+ const op = node[0]
28
+ if (op === 'break') return ['=', flag, [null, true]]
29
+ if (SWITCH_BREAK_BOUNDARIES.has(op)) return node
30
+
31
+ if (op === ';') {
32
+ const out = []
33
+ const stmts = node.slice(1)
34
+ for (let i = 0; i < stmts.length; i++) {
35
+ const stmt = stmts[i]
36
+ out.push(rewriteSwitchBreaks(stmt, flag))
37
+ if (hasOwnSwitchBreak(stmt) && i < stmts.length - 1) {
38
+ const tail = rewriteSwitchBreaks([';', ...stmts.slice(i + 1)], flag)
39
+ out.push(['if', ['!', flag], tail])
40
+ break
41
+ }
42
+ }
43
+ return out.length === 0 ? null : out.length === 1 ? out[0] : [';', ...out]
44
+ }
45
+
46
+ return node.map((part, i) => i === 0 ? part : rewriteSwitchBreaks(part, flag))
47
+ }
48
+
49
+ /** Transform a switch into structured control flow with faithful fall-through.
50
+ *
51
+ * A pure if/else-if chain (the former lowering) can't express fall-through,
52
+ * stacked labels, or a `default` clause that isn't last \u2014 it ran only the first
53
+ * matching body. The correct model is two-phase, evaluated once with no goto:
54
+ *
55
+ * 1. ENTRY \u2014 compare the discriminant against each `case` label in source
56
+ * order; the first `===` match fixes the entry index. No case matches \u2192
57
+ * entry = the `default` clause's source index (or past-end if none).
58
+ * 2. RUN \u2014 walk clauses in source order; `entry <= i` runs clause i, so every
59
+ * clause from the entry onward executes (fall-through). A `break` flips the
60
+ * sticky `brk` flag (via rewriteSwitchBreaks) and gates the rest.
61
+ *
62
+ * The discriminant is bound to a temp only when re-reading it isn't free/safe; a
63
+ * bare identifier is compared directly \u2014 a synthetic temp would shed its STRING
64
+ * val-type and mis-fold string `case`s to `false` under strict-=== folding. */
65
+ export function createSwitchLowering(transform, names) {
66
+ return function transformSwitch(discriminant, cases) {
67
+ const disc = transform(discriminant)
68
+ const simple = typeof disc === 'string' || (Array.isArray(disc) && disc[0] == null)
69
+ const tmp = simple ? disc : names.switchDisc()
70
+ const start = names.switchStart()
71
+ const needsBreakFlag = cases.some(c => hasOwnSwitchBreak(c[0] === 'case' ? c[2] : c[1]))
72
+ const brk = needsBreakFlag ? names.switchBreak() : null
73
+
74
+ const n = cases.length
75
+ let defaultIdx = -1
76
+ const bodies = cases.map((c, i) => {
77
+ if (c[0] === 'default') { defaultIdx = i; return transform(c[1]) }
78
+ return transform(c[2])
79
+ })
80
+
81
+ const stmts = []
82
+ if (!simple) stmts.push(['let', ['=', tmp, disc]])
83
+
84
+ // Phase 1 \u2014 entry index. Init to default's position (or n = "no clause runs"),
85
+ // then let the first matching label override it via an if/else-if chain.
86
+ stmts.push(['let', ['=', start, [null, defaultIdx >= 0 ? defaultIdx : n]]])
87
+ let chain = null
88
+ for (let i = n - 1; i >= 0; i--) {
89
+ if (cases[i][0] !== 'case') continue
90
+ const hit = ['=', start, [null, i]]
91
+ const cond = ['===', tmp, transform(cases[i][1])]
92
+ chain = chain != null ? ['if', cond, hit, chain] : ['if', cond, hit]
93
+ }
94
+ if (chain) stmts.push(chain)
95
+ if (brk) stmts.push(['let', ['=', brk, [null, false]]])
96
+
97
+ // Phase 2 \u2014 run clauses from the entry index, falling through until a break.
98
+ for (let i = 0; i < n; i++) {
99
+ if (bodies[i] == null) continue
100
+ const body = brk ? rewriteSwitchBreaks(bodies[i], brk) : bodies[i]
101
+ const reached = ['<=', start, [null, i]]
102
+ stmts.push(['if', brk ? ['&&', ['!', brk], reached] : reached, body])
103
+ }
104
+ return [';', ...stmts]
105
+ }
106
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * AST transform handlers — function/class/control-flow lowering after hoisting.
3
+ * @module jzify/transform
4
+ */
5
+
6
+ import { warn } from '../src/ctx.js'
7
+ import { JZ_BLOCK_OPS, LABEL_BODY_OPS, STMT_ONLY_OPS, paramList } from '../src/ast.js'
8
+ import { isDestructurePat } from './hoist-vars.js'
9
+
10
+ const ERROR_INSTANCEOF = new Set(['Error', 'TypeError', 'SyntaxError', 'RangeError', 'ReferenceError', 'URIError', 'EvalError'])
11
+
12
+ const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32Array',
13
+ 'Int16Array','Uint16Array','Int8Array','Uint8Array',
14
+ 'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
15
+
16
+ const isProto = n => Array.isArray(n) && n[0] === '.' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'prototype'
17
+
18
+ function staticInstanceofFold(val, ctor) {
19
+ if (typeof ctor !== 'string' || !Array.isArray(val)) return null
20
+ if (val[0] === '()' && val.length === 2) return staticInstanceofFold(val[1], ctor)
21
+ if (val[0] === '[]' && val.length <= 2) return ctor === 'Array' || ctor === 'Object'
22
+ if (val[0] === '{}') return ctor === 'Object'
23
+ if (val[0] === '//') return ctor === 'RegExp' || ctor === 'Object'
24
+ if (val[0] === 'new') {
25
+ const inner = val[1]
26
+ const cname = typeof inner === 'string' ? inner
27
+ : (Array.isArray(inner) && inner[0] === '()' && typeof inner[1] === 'string') ? inner[1]
28
+ : null
29
+ if (cname) return cname === ctor || (cname !== 'Object' && ctor === 'Object')
30
+ }
31
+ if (val[0] == null && val.length === 2) {
32
+ const v = val[1]
33
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v == null) return false
34
+ }
35
+ return null
36
+ }
37
+
38
+ function dedupeRedecls(stmts) {
39
+ const declName = d => typeof d === 'string' ? d
40
+ : Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' ? d[1] : null
41
+ const seen = new Set(), out = []
42
+ for (const s of stmts) {
43
+ if (!Array.isArray(s) || (s[0] !== 'let' && s[0] !== 'const' && s[0] !== 'var')) { out.push(s); continue }
44
+ const keep = [s[0]], reassign = []
45
+ for (let i = 1; i < s.length; i++) {
46
+ const d = s[i], n = declName(d)
47
+ if (n == null) { keep.push(d); continue }
48
+ if (seen.has(n)) { if (Array.isArray(d) && d[0] === '=') reassign.push(['=', d[1], d[2]]) }
49
+ else { seen.add(n); keep.push(d) }
50
+ }
51
+ if (keep.length > 1) out.push(keep)
52
+ for (const r of reassign) out.push(r)
53
+ }
54
+ return out
55
+ }
56
+
57
+ function functionBodyBlock(body) {
58
+ if (Array.isArray(body) && body[0] === '{}') return body
59
+ if (Array.isArray(body) && body[0] === ';') return ['{}', body]
60
+ return ['{}', [';', body]]
61
+ }
62
+
63
+ const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
64
+
65
+ /**
66
+ * @param {object} opts
67
+ * @param {ReturnType<import('./names.js').createNames>} opts.names
68
+ * @param {Function} opts.lowerArguments
69
+ * @param {Function} opts.transformPattern
70
+ * @param {Function} opts.normalizeCaseBody
71
+ * @param {Function} opts.transformSwitch
72
+ * @param {() => Function} opts.lowerClass
73
+ * @param {() => Function} opts.lowerObjectLiteralThis
74
+ * @param {() => Function} opts.lowerArrayConstructor
75
+ */
76
+ export function createTransform(opts) {
77
+ const { names, lowerArguments, transformPattern, normalizeCaseBody, transformSwitch } = opts
78
+ const lowerClass = (...a) => opts.lowerClass()(...a)
79
+ const lowerObjectLiteralThis = (...a) => opts.lowerObjectLiteralThis()(...a)
80
+ const lowerArrayConstructor = (...a) => opts.lowerArrayConstructor()(...a)
81
+
82
+ const methodOverrideHasOwn = (a, b) => {
83
+ const proto = isProto(a) ? a : isProto(b) ? b : null
84
+ if (!proto) return null
85
+ const other = proto === a ? b : a
86
+ if (!Array.isArray(other) || other[0] !== '.' || other[2] !== proto[2]) return null
87
+ return ['()', ['.', transform(other[1]), 'hasOwnProperty'], [null, proto[2]]]
88
+ }
89
+
90
+ function wrapArrowBody(body) {
91
+ const t = transformScope(body)
92
+ if (!Array.isArray(t)) return ['{}', [';', t]]
93
+ if (t[0] === ';') return ['{}', t]
94
+ if (t[0] !== '{}') return ['{}', [';', t]]
95
+ if (t.length === 2 && !(Array.isArray(t[1]) && t[1][0] === ';')) return ['{}', [';', t[1]]]
96
+ return t
97
+ }
98
+
99
+ function hoistFnDecl(name, params, body) {
100
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
101
+ const decl = ['const', ['=', name, ['=>', p2, wrapArrowBody(b2)]]]
102
+ decl._hoisted = true
103
+ return decl
104
+ }
105
+
106
+ function transformScope(node) {
107
+ if (!Array.isArray(node)) return transform(node)
108
+
109
+ const [op, ...args] = node
110
+
111
+ if (op === 'function' && args[0]) return hoistFnDecl(...args)
112
+ if (op === 'class' && args[0]) return ['let', ['=', args[0], lowerClass(...args)]]
113
+
114
+ if (op === ';') {
115
+ const hoisted = [], rest = []
116
+ for (let i = 0; i < args.length; i++) {
117
+ const stmt = args[i]
118
+ if (Array.isArray(stmt) && stmt[0] === 'function' && stmt[1]) {
119
+ hoisted.push(hoistFnDecl(stmt[1], stmt[2], stmt[3]))
120
+ continue
121
+ }
122
+ if (Array.isArray(stmt) && stmt[0] === 'class' && stmt[1]) {
123
+ rest.push(['let', ['=', stmt[1], lowerClass(stmt[1], stmt[2], stmt[3])]])
124
+ continue
125
+ }
126
+ const t = transform(stmt)
127
+ if (t == null) continue
128
+ if (Array.isArray(t) && t[0] === 'const' && t._hoisted) {
129
+ hoisted.push(t)
130
+ } else if (Array.isArray(t) && t[0] === ';') {
131
+ for (const s of t.slice(1)) {
132
+ if (s != null) {
133
+ if (Array.isArray(s) && s[0] === 'const' && s._hoisted) hoisted.push(s)
134
+ else rest.push(s)
135
+ }
136
+ }
137
+ } else {
138
+ rest.push(t)
139
+ }
140
+ }
141
+ // ES hoists every import binding above any function body. jzify mirrors
142
+ // that by floating imports ahead of hoisted function decls. A combo import
143
+ // `import d, { n } from 'm'` parses as `[',', ['import',…], ['from',…]]`, so
144
+ // match the comma-wrapped form too — otherwise its bindings land after the
145
+ // hoisted functions that reference them ("X is not in scope").
146
+ const isImportStmt = s => Array.isArray(s) &&
147
+ (s[0] === 'import' || (s[0] === ',' && Array.isArray(s[1]) && s[1][0] === 'import'))
148
+ const imports = rest.filter(isImportStmt)
149
+ const nonImports = rest.filter(s => !isImportStmt(s))
150
+ const all = dedupeRedecls([...imports, ...hoisted, ...nonImports])
151
+ return all.length === 0 ? null : all.length === 1 ? all[0] : [';', ...all]
152
+ }
153
+
154
+ return transform(node)
155
+ }
156
+
157
+ const handlers = {
158
+ '()'(callee, ...rest) {
159
+ if (callee === 'Array') {
160
+ const lit = lowerArrayConstructor(rest[0])
161
+ if (lit) return lit
162
+ }
163
+ if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
164
+ const [, name, params, body] = callee[1]
165
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
166
+ // `(function name(){…})(args)` — the named binding must be lowered as a
167
+ // self-contained EXPRESSION (it can sit in concise-arrow-body / argument
168
+ // position), so wrap the `let name = arrow; name(args)` in a block IIFE
169
+ // rather than emitting a bare `;`-sequence. A statement-sequence in
170
+ // expression position never reaches the `'=>'` handler's block-wrap (that
171
+ // runs before this transform), and emit miscompiles a `let`-closure decl in
172
+ // a concise `;`-body. Mirrors the bare-`function name` lowering below.
173
+ return ['()', ['=>', null, ['{}', [';',
174
+ ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]],
175
+ ['return', ['()', name, ...rest.map(transform)]],
176
+ ]]], null]
177
+ }
178
+ },
179
+
180
+ 'function'(name, params, body) {
181
+ const [p2, b2] = lowerArguments(params, functionBodyBlock(body))
182
+ const arrow = ['=>', p2, wrapArrowBody(b2)]
183
+ if (name) {
184
+ return ['()', ['()', ['=>', null, ['{}', [';',
185
+ ['let', name],
186
+ ['=', name, arrow],
187
+ ['return', name]
188
+ ]]]], null]
189
+ }
190
+ return arrow
191
+ },
192
+
193
+ '=>'(params, body) {
194
+ let b = body
195
+ if (Array.isArray(b) && b[0] === '{}' && b.length === 2) {
196
+ const inner = b[1]
197
+ if (inner != null && !(Array.isArray(inner) && inner[0] === ';')) {
198
+ b = ['{}', [';', inner]]
199
+ }
200
+ } else if (Array.isArray(b) && STMT_ONLY_OPS.has(b[0])) {
201
+ // Subscript's expression grammar lets statement-only ops (`if`, `for`,
202
+ // `return`, `;`, …) appear in concise-body position — method shorthand
203
+ // `m(){ stmt }` parses to `['=>', p, stmt]` with no `{}` wrap (the body
204
+ // braces are structural, not a group operator). A concise body must
205
+ // yield a value, so these void ops would otherwise be coerced into the
206
+ // f64 return slot ("not enough arguments on the stack for f64.convert_i32_s").
207
+ // Re-wrap as a block — statement bodies belong in block form.
208
+ b = b[0] === ';' ? ['{}', b] : ['{}', [';', b]]
209
+ }
210
+ const [p2, b2] = lowerArguments(params, b)
211
+ return ['=>', p2, transform(b2)]
212
+ },
213
+
214
+ 'class'(name, heritage, body) { return lowerClass(name, heritage, body) },
215
+
216
+ 'var'(...args) {
217
+ return ['let', ...args.map(transform)]
218
+ },
219
+
220
+ ':'(label, body) {
221
+ if (typeof label === 'string' && Array.isArray(body) && LABEL_BODY_OPS.has(body[0]))
222
+ return ['label', label, transform(body)]
223
+ },
224
+
225
+ '='(lhs, rhs) {
226
+ if (isDestructurePat(lhs)) return ['=', transformPattern(lhs), transform(rhs)]
227
+ },
228
+
229
+ 'switch'(disc, ...cases) {
230
+ const clean = cases.map(c => {
231
+ if (c[0] === 'case') return ['case', c[1], normalizeCaseBody(c[2])]
232
+ if (c[0] === 'default') return ['default', normalizeCaseBody(c[1])]
233
+ return c
234
+ })
235
+ return transformSwitch(disc, clean)
236
+ },
237
+
238
+ '=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; return isProto(a) || isProto(b) ? 1 : ['==', transform(a), transform(b)] },
239
+ '!='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; return isProto(a) || isProto(b) ? 0 : ['!=', transform(a), transform(b)] },
240
+ '==='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return ['!', own]; if (isProto(a) || isProto(b)) return 1 },
241
+ '!=='(a, b) { const own = methodOverrideHasOwn(a, b); if (own) return own; if (isProto(a) || isProto(b)) return 0 },
242
+
243
+ 'new'(ctor, ...cargs) {
244
+ if (Array.isArray(ctor) && ctor[0] === '()' && ctor[1] === 'Array') {
245
+ const lit = lowerArrayConstructor(ctor[2])
246
+ if (lit) return lit
247
+ }
248
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
249
+ // Preserve `new` for native constructors the compiler resolves under its own
250
+ // `new` handler (prepare/index.js): typed arrays/Array/RegExp need the `new`
251
+ // form, and `new URL(rel, import.meta.url)` lowers to a static href string there.
252
+ // User classes (lowered to factory arrows by jzify) become plain calls below.
253
+ if (typeof name === 'string' && (TYPED_ARRAYS.has(name) || name === 'Array' || name === 'RegExp' || name === 'URL')) return ['new', transform(ctor), ...cargs.map(transform)]
254
+ if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
255
+ return ['()', transform(ctor), ...(cargs.length ? cargs.map(transform) : [null])]
256
+ },
257
+
258
+ 'instanceof'(val, ctor) {
259
+ const t = transform(val)
260
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
261
+ const fold = staticInstanceofFold(val, name)
262
+ if (fold != null) return [null, fold]
263
+ if (typeof name === 'string' && ERROR_INSTANCEOF.has(name)) {
264
+ warn('untagged-instanceof',
265
+ `\`instanceof ${name}\` does not discriminate thrown values in jz — errors are untagged; inspect the message or value instead`,
266
+ {}, Array.isArray(val) ? val.loc : null)
267
+ }
268
+ if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
269
+ if (name === 'Map') return ['()', '__is_map', t]
270
+ if (name === 'Set') return ['()', '__is_set', t]
271
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name) && name !== 'ArrayBuffer' && name !== 'DataView')
272
+ return ['()', '__is_typed', t]
273
+ return ['===', ['typeof', t], [null, 'object']]
274
+ },
275
+
276
+ 'do'(body, cond) {
277
+ const flag = names.doFlag()
278
+ return [';',
279
+ ['let', ['=', flag, [null, true]]],
280
+ ['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
281
+ },
282
+
283
+ // The classic for-head `[';', init, cond, step]` is a fixed 3-slot structure,
284
+ // NOT a statement sequence. Transform each slot individually and keep null slots
285
+ // in place. Without this handler, `for` falls to the generic recurse, the head
286
+ // hits the `;` handler (transformScope), and an empty `init` (null) is dropped as
287
+ // an empty statement — shifting cond→init/step→cond and miscompiling the loop
288
+ // (`for (; i < n; i++)` ran zero/garbage iterations). for-of/for-in heads aren't
289
+ // `;`-lists, so they pass through transform unchanged.
290
+ 'for'(head, body) {
291
+ if (Array.isArray(head) && head[0] === ';')
292
+ return ['for', [';', ...head.slice(1).map(s => s == null ? s : transform(s))], transform(body)]
293
+ return ['for', transform(head), transform(body)]
294
+ },
295
+
296
+ // A bare statement sequence is a block scope too. `parse` only wraps
297
+ // function/arrow bodies in `{}`; loop/conditional bodies arrive as a raw
298
+ // `;`. Route them through transformScope so `function` declarations nested
299
+ // in a loop/if body get hoisted (→ block-top `const f = arrow`) instead of
300
+ // falling to the discard-IIFE path, which would scope the name inside the
301
+ // IIFE and leave later `f()` references dangling.
302
+ ';'(...args) { return transformScope([';', ...args]) },
303
+
304
+ '{}'(...args) {
305
+ const loweredObject = lowerObjectLiteralThis(args)
306
+ if (loweredObject) return loweredObject
307
+
308
+ return ['{}', ...args.map((a, i) => {
309
+ const t = transformScope(a) ?? a
310
+ if (i !== 0 || a == null) return t
311
+ const blockIn = Array.isArray(a) && JZ_BLOCK_OPS.has(a[0])
312
+ if (!blockIn || t == null) return t
313
+ return Array.isArray(t) && t[0] === ';' ? t : [';', t]
314
+ })]
315
+ },
316
+
317
+ 'export'(inner) {
318
+ if (Array.isArray(inner) && inner[0] === 'function' && inner[1]) {
319
+ return ['export', hoistFnDecl(inner[1], inner[2], inner[3])]
320
+ }
321
+ if (Array.isArray(inner) && inner[0] === 'class' && inner[1]) {
322
+ return ['export', ['let', ['=', inner[1], lowerClass(inner[1], inner[2], inner[3])]]]
323
+ }
324
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
325
+ // Route a named default-export function through the named-export path: a bare
326
+ // `const NAME` lifted in a bundled module loses its recursive self-reference
327
+ // (the default-alias resolver renames the func but not in-body call sites),
328
+ // so the function is dropped. Exporting NAME as a named binding makes prepare
329
+ // mangle it and resolve self-calls correctly; alias `default` to it.
330
+ const decl = hoistFnDecl(inner[1][1], inner[1][2], inner[1][3])
331
+ return [';', ['export', decl], ['export', ['{}', ['as', inner[1][1], 'default']]]]
332
+ }
333
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'class' && inner[1][1]) {
334
+ return [';', ['let', ['=', inner[1][1], lowerClass(inner[1][1], inner[1][2], inner[1][3])]], ['export', ['default', inner[1][1]]]]
335
+ }
336
+ return ['export', transform(inner)]
337
+ },
338
+ }
339
+
340
+ function transform(node) {
341
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
342
+ const [op, ...args] = node
343
+ if (op == null) return node
344
+ const h = handlers[op]
345
+ return (h && h(...args)) ?? [op, ...args.map(transform)]
346
+ }
347
+
348
+ return { transform, transformScope }
349
+ }
package/layout.js ADDED
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Shared runtime layout: heap, NaN-box carrier, pointer tags.
3
+ *
4
+ * Compiler-free — safe for `jz/interop` and tests without pulling the compiler.
5
+ *
6
+ * @module layout
7
+ */
8
+
9
+ /** Bump-allocator cells in linear memory / globals. */
10
+ export const HEAP = { PTR_ADDR: 1020, START: 1024 }
11
+
12
+ /** NaN-box bit layout (i64 carrier). */
13
+ export const LAYOUT = {
14
+ TAG_SHIFT: 47,
15
+ TAG_MASK: 0xF,
16
+ AUX_SHIFT: 32,
17
+ AUX_MASK: 0x7FFF,
18
+ OFFSET_MASK: 0xFFFFFFFF,
19
+ NAN_PREFIX: 0x7FF8,
20
+ NAN_PREFIX_BITS: 0x7FF8000000000000n,
21
+ SSO_BIT: 0x4000,
22
+ SLICE_BIT: 0x2000,
23
+ SLICE_LEN_MASK: 0x1FFF,
24
+ }
25
+
26
+ /** 4-bit tagged-pointer type codes. */
27
+ export const PTR = {
28
+ ATOM: 0,
29
+ ARRAY: 1,
30
+ BUFFER: 2,
31
+ TYPED: 3,
32
+ STRING: 4,
33
+ OBJECT: 6,
34
+ HASH: 7,
35
+ SET: 8,
36
+ MAP: 9,
37
+ CLOSURE: 10,
38
+ EXTERNAL: 11,
39
+ }
40
+
41
+ /** Reserved atom aux ids (PTR.ATOM). */
42
+ export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
43
+
44
+ // =============================================================================
45
+ // PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
46
+ // field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
47
+ // layout it encodes, shared by the compiler (type/analyze/narrow/infer) and the
48
+ // `module/typedarray` stdlib.
49
+ // =============================================================================
50
+
51
+ /** Base element-type codes for PTR.TYPED aux (0–7). BigInt ctors share 7 + TYPED_ELEM_BIGINT_FLAG. */
52
+ export const TYPED_ELEM_CODE = {
53
+ Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
54
+ Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
55
+ BigInt64Array: 7, BigUint64Array: 7,
56
+ }
57
+ export const TYPED_ELEM_VIEW_FLAG = 8
58
+ export const TYPED_ELEM_BIGINT_FLAG = 16
59
+
60
+ export const TYPED_ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
61
+ 'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']
62
+
63
+ /** Encode element-type name (+ optional view/bigint flags) to PTR.TYPED aux bits. */
64
+ export function encodeTypedElemAux(name, isView = false) {
65
+ const et = TYPED_ELEM_CODE[name]
66
+ if (et == null) return null
67
+ return et | (isView ? TYPED_ELEM_VIEW_FLAG : 0) |
68
+ (name === 'BigInt64Array' || name === 'BigUint64Array' ? TYPED_ELEM_BIGINT_FLAG : 0)
69
+ }
70
+
71
+ /** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
72
+ * aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
73
+ export function typedElemAux(ctor) {
74
+ if (!ctor || !ctor.startsWith('new.')) return null
75
+ const isView = ctor.endsWith('.view')
76
+ const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
77
+ return encodeTypedElemAux(name, isView)
78
+ }
79
+
80
+ /** Reverse of typedElemAux: pick a canonical ctor string for a 4-bit elem aux. Used
81
+ * to round-trip TYPED-narrowed call results through ctx.types.typedElem so the
82
+ * unboxed local's rep picks up the same aux. aux=7 is shared with BigInt typed
83
+ * arrays — Float64Array is canonical (read-side compares aux only). */
84
+ export function ctorFromElemAux(aux) {
85
+ if (aux == null) return null
86
+ const isView = (aux & 8) !== 0
87
+ const name = (aux & 16) !== 0 ? 'BigInt64Array' : TYPED_ELEM_NAMES[aux & 7]
88
+ if (!name) return null
89
+ return isView ? `new.${name}.view` : `new.${name}`
90
+ }
91
+
92
+ /** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop). */
93
+ export const encodePtrHi = (type, aux) =>
94
+ (0x7FF80000 | ((type & 0xF) << 15) | (aux & 0x7FFF)) >>> 0
95
+
96
+ export const decodePtrType = hi => (hi >>> 15) & 0xF
97
+ export const decodePtrAux = hi => hi & 0x7FFF
98
+
99
+ /** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
100
+ export const nanPrefixHex = () =>
101
+ '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
102
+
103
+ /** Atom sentinel as i64 hex (compiler WAT templates). */
104
+ export const atomNanHex = atomId =>
105
+ '0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
106
+
107
+ /** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
108
+ * CANONICAL interned string — the static-pool copy (or an intern-table hit
109
+ * resolving to it). Two canonicals are bit-equal iff content-equal, so
110
+ * __str_eq answers unequal canonicals without touching bytes, and interned
111
+ * statics carry a cached FNV hash at offset-8 ([hash u32][len u32][bytes])
112
+ * that __str_hash loads instead of re-hashing. Inert elsewhere: slice-length
113
+ * bits are only read under SLICE_BIT, SSO length under SSO_BIT, and plain-
114
+ * heap consumers read the len header at -4 regardless of aux. */
115
+ export const STR_INTERN_BIT = 0x1
116
+
117
+ /** Pre-shifted STRING SSO aux bit as i64 hex. */
118
+ export const ssoBitI64Hex = () =>
119
+ '0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
120
+
121
+ /** Pre-shifted STRING slice/view aux bit as i64 hex. */
122
+ export const sliceBitI64Hex = () =>
123
+ '0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
124
+
125
+ /** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
126
+ export const ptrNanHex = (ptrType, aux = 0) =>
127
+ '0x' + ptrBoxPrefixBigInt(ptrType, aux).toString(16).toUpperCase().padStart(16, '0')
128
+
129
+ /** Compile-time i64 prefix for mkPtrIR (before offset OR). */
130
+ export const ptrBoxPrefixBigInt = (ptrType, aux = 0) =>
131
+ (0x7FF8n << 48n)
132
+ | ((BigInt(ptrType) & 0xFn) << 47n)
133
+ | ((BigInt(aux) & 0x7FFFn) << 32n)
134
+
135
+ /** Host-side atom sentinel high-u32 values (interop f64 decode). */
136
+ export const ATOM_HI = {
137
+ [ATOM.NULL]: encodePtrHi(PTR.ATOM, ATOM.NULL),
138
+ [ATOM.UNDEF]: encodePtrHi(PTR.ATOM, ATOM.UNDEF),
139
+ [ATOM.FALSE]: encodePtrHi(PTR.ATOM, ATOM.FALSE),
140
+ [ATOM.TRUE]: encodePtrHi(PTR.ATOM, ATOM.TRUE),
141
+ }
142
+
143
+ /** OOB / canonical quiet-NaN f64 literal for WAT and IR (`nan:0x7FF8…`). */
144
+ export const oobNanLiteral = () => `nan:${nanPrefixHex()}`
145
+ export const oobNanIR = () => ['f64.const', oobNanLiteral()]
146
+
147
+ /** Heap forwarding-pointer follow (WAT fragment).
148
+ * ARRAY/HASH/SET/MAP relocate on growth, leaving the old cell as a forwarding
149
+ * header: cap=-1 sentinel at off-4, relocated offset at off-8. `off` is the
150
+ * i32 local (token incl. `$`) holding the current offset — mutated in place.
151
+ * `lowGuard` adds the `off < 8` bailout; omit it when the caller already
152
+ * proved off≥8.
153
+ *
154
+ * Shape: a loop-free in-bounds + sentinel CHECK with a cold call into
155
+ * $__ptr_offset_fwd (the actual chase loop, module/core.js) only when the
156
+ * first hop is a real forward. Keeping every inline copy loop-free is what
157
+ * lets the engine inline the hot heap helpers (__ptr_offset, __len,
158
+ * __arr_idx_known, __typed_idx…) — a body containing a loop is excluded from
159
+ * V8's wasm inliner, and these helpers sit on ~25% of self-host compile time.
160
+ * Callers must list '__ptr_offset_fwd' in their deps()/wat() dependency set. */
161
+ export const followForwardingWat = (off = '$off', { lowGuard = true } = {}) =>
162
+ `(if (i32.and
163
+ ${lowGuard ? `(i32.ge_u (local.get ${off}) (i32.const 8))` : '(i32.const 1)'}
164
+ (i32.le_u (local.get ${off}) (i32.shl (memory.size) (i32.const 16))))
165
+ (then (if (i32.eq (i32.load (i32.sub (local.get ${off}) (i32.const 4))) (i32.const -1))
166
+ (then (local.set ${off} (call $__ptr_offset_fwd (local.get ${off})))))))`
167
+
168
+ /** The cold forwarding-chase loop behind followForwardingWat — the only body
169
+ * allowed to loop. Re-checks the sentinel each hop (first re-check is
170
+ * redundant with the caller's guard; the cold path doesn't care). */
171
+ export const ptrOffsetFwdWat = () =>
172
+ `(func $__ptr_offset_fwd (param $off i32) (result i32)
173
+ (block $done (loop $follow
174
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
175
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
176
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
177
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
178
+ (br $follow)))
179
+ (local.get $off))`