jz 0.0.0 → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/cli.js +163 -0
- package/index.js +247 -0
- package/module/array.js +1322 -0
- package/module/collection.js +793 -0
- package/module/console.js +192 -0
- package/module/core.js +644 -0
- package/module/function.js +181 -0
- package/module/index.js +15 -0
- package/module/json.js +506 -0
- package/module/math.js +390 -0
- package/module/number.js +601 -0
- package/module/object.js +359 -0
- package/module/regex.js +914 -0
- package/module/schema.js +106 -0
- package/module/string.js +985 -0
- package/module/symbol.js +55 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +713 -0
- package/package.json +56 -5
- package/src/analyze.js +1927 -0
- package/src/autoload.js +175 -0
- package/src/compile.js +1211 -0
- package/src/ctx.js +244 -0
- package/src/emit.js +2105 -0
- package/src/host.js +536 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +418 -0
- package/src/key.js +73 -0
- package/src/narrow.js +928 -0
- package/src/optimize.js +1352 -0
- package/src/plan.js +105 -0
- package/src/prepare.js +1534 -0
- package/src/source.js +76 -0
- package/wasi.js +80 -0
package/src/prepare.js
ADDED
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST preparation: single-pass traversal that validates, resolves, and normalizes.
|
|
3
|
+
*
|
|
4
|
+
* # Stage contract
|
|
5
|
+
* IN: raw jessie AST from subscript/jessie (possibly jzified).
|
|
6
|
+
* OUT: normalized AST + populated `ctx.func.list`, `ctx.module.imports`, `ctx.schema.list`,
|
|
7
|
+
* `ctx.scope.consts`, `ctx.module.moduleInits`.
|
|
8
|
+
* POST: no `var`/`function`/`class`/`this` remain; ++/-- rewritten as +=/-=; arrow
|
|
9
|
+
* bodies carry no type metadata yet (that's analyze/compile's job).
|
|
10
|
+
*
|
|
11
|
+
* # Concerns (per-node handler table, applied together per op)
|
|
12
|
+
* 1. Validate — reject prohibited features (this, class, async, var, delete, ...)
|
|
13
|
+
* 2. Resolve — scope chain + import bindings (Math.sin → math.sin, etc.)
|
|
14
|
+
* 3. Extract — arrow functions → ctx.func.list with sig
|
|
15
|
+
* 4. Normalize — ++/-- → +=/-=, unary ± disambiguation, for-head flattening
|
|
16
|
+
* 5. Auto-import — Math/Array/etc usage triggers includeModule(...)
|
|
17
|
+
* 6. Track schemas — object literals, Object.assign inference (inferAssignSchema)
|
|
18
|
+
*
|
|
19
|
+
* Each handler may touch multiple concerns, but helpers keep each concern self-contained.
|
|
20
|
+
* Unhandled ops fall through to recursive prep() of their children.
|
|
21
|
+
*
|
|
22
|
+
* @module prepare
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { parse } from 'subscript/jessie'
|
|
26
|
+
import { ctx, err, derive } from './ctx.js'
|
|
27
|
+
import { T, STMT_OPS, VAL, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam } from './analyze.js'
|
|
28
|
+
import { staticPropertyKey } from './key.js'
|
|
29
|
+
import { normalizeSource } from './source.js'
|
|
30
|
+
import {
|
|
31
|
+
CTORS, TIMER_NAMES,
|
|
32
|
+
hasModule, includeModule,
|
|
33
|
+
includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
|
|
34
|
+
includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
|
|
35
|
+
includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
|
|
36
|
+
includeForRuntimeKeyIteration, includeForStringOnly, includeForStringValue, includeForTimerRuntime,
|
|
37
|
+
} from './autoload.js'
|
|
38
|
+
|
|
39
|
+
let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
|
|
40
|
+
let scopes = [] // block scope stack: [{names: Set, renames: Map}]
|
|
41
|
+
|
|
42
|
+
const hostReturnValType = spec => {
|
|
43
|
+
if (!spec || typeof spec === 'function') return null
|
|
44
|
+
const ret = spec.returns ?? spec.return ?? spec.result
|
|
45
|
+
if (ret === 'number' || ret === 'f64' || ret === Number) return VAL.NUMBER
|
|
46
|
+
if (ret === 'string' || ret === String) return VAL.STRING
|
|
47
|
+
if (ret === 'bigint' || ret === BigInt) return VAL.BIGINT
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const addHostImport = (mod, name, alias, spec) => {
|
|
52
|
+
const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
|
|
53
|
+
const params = Array(nParams).fill(['param', 'f64'])
|
|
54
|
+
if (!ctx.module.imports.some(i => i[3]?.[1] === `$${alias}`)) {
|
|
55
|
+
ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, ['func', `$${alias}`, ...params, ['result', 'f64']]])
|
|
56
|
+
}
|
|
57
|
+
ctx.scope.chain[alias] = alias
|
|
58
|
+
const vt = hostReturnValType(spec)
|
|
59
|
+
if (vt) ctx.module.hostImportValTypes.set(alias, vt)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordGlobalValueFact(name, expr) {
|
|
63
|
+
if (typeof name !== 'string') return
|
|
64
|
+
const vt = valTypeOf(expr)
|
|
65
|
+
if (vt) {
|
|
66
|
+
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
67
|
+
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
68
|
+
}
|
|
69
|
+
const ctor = typedElemCtor(expr)
|
|
70
|
+
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function recordModuleInitFacts(root) {
|
|
74
|
+
const facts = ctx.module.initFacts ||= {
|
|
75
|
+
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
76
|
+
hasFuncValue: false, timerNames: new Set(),
|
|
77
|
+
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
78
|
+
}
|
|
79
|
+
const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
80
|
+
const visitFuncValue = (node) => {
|
|
81
|
+
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
82
|
+
const [op, ...args] = node
|
|
83
|
+
if (op === '()') {
|
|
84
|
+
for (let i = 1; i < args.length; i++) {
|
|
85
|
+
const a = args[i]
|
|
86
|
+
if (typeof a === 'string' && ctx.func.names.has(a)) { facts.hasFuncValue = true; return }
|
|
87
|
+
visitFuncValue(a)
|
|
88
|
+
}
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (op === '.' || op === '?.') {
|
|
92
|
+
if (typeof args[0] === 'string' && ctx.func.names.has(args[0])) { facts.hasFuncValue = true; return }
|
|
93
|
+
visitFuncValue(args[0])
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (op === '=>') { visitFuncValue(args[1]); return }
|
|
97
|
+
for (const a of args) {
|
|
98
|
+
if (typeof a === 'string' && ctx.func.names.has(a)) { facts.hasFuncValue = true; return }
|
|
99
|
+
visitFuncValue(a)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const walk = (node) => {
|
|
103
|
+
if (!Array.isArray(node)) {
|
|
104
|
+
if (typeof node === 'string' && TIMER_NAMES.has(node)) facts.timerNames.add(node)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const [op, ...args] = node
|
|
108
|
+
if (op === '[]') {
|
|
109
|
+
const [obj, idx] = args
|
|
110
|
+
if (!isLiteralStr(idx)) { facts.anyDyn = true; if (typeof obj === 'string') facts.dynVars.add(obj) }
|
|
111
|
+
} else if (op === 'for-in') {
|
|
112
|
+
facts.anyDyn = true
|
|
113
|
+
if (typeof args[1] === 'string') facts.dynVars.add(args[1])
|
|
114
|
+
} else if (op === '{}') {
|
|
115
|
+
facts.hasSchemaLiterals = true
|
|
116
|
+
} else if (op === '=>') {
|
|
117
|
+
let fixedN = 0
|
|
118
|
+
for (const r of extractParams(args[0])) {
|
|
119
|
+
if (classifyParam(r).kind === 'rest') facts.hasRest = true
|
|
120
|
+
else fixedN++
|
|
121
|
+
}
|
|
122
|
+
if (fixedN > facts.maxDef) facts.maxDef = fixedN
|
|
123
|
+
} else if (op === '()') {
|
|
124
|
+
const a = args[1]
|
|
125
|
+
const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
126
|
+
if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) facts.hasSpread = true
|
|
127
|
+
if (callArgs.length > facts.maxCall) facts.maxCall = callArgs.length
|
|
128
|
+
}
|
|
129
|
+
for (const a of args) walk(a)
|
|
130
|
+
}
|
|
131
|
+
visitFuncValue(root)
|
|
132
|
+
walk(root)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @typedef {null|number|string|ASTNode[]} ASTNode
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Prepare AST node for compilation.
|
|
141
|
+
* @param {ASTNode} node - Raw AST from parser
|
|
142
|
+
* @returns {ASTNode} Normalized AST
|
|
143
|
+
*/
|
|
144
|
+
export default function prepare(node) {
|
|
145
|
+
depth = 0
|
|
146
|
+
scopes = []
|
|
147
|
+
includeModule('core')
|
|
148
|
+
fuseSparseMapReads(node)
|
|
149
|
+
const ast = prep(node)
|
|
150
|
+
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
151
|
+
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
152
|
+
// module's closure.table machinery. defFunc paths don't trigger fn-module load,
|
|
153
|
+
// so scan post-prep and include `fn` if any user func appears in a value position.
|
|
154
|
+
if (!ctx.module.modules.fn && ctx.func.list.length) {
|
|
155
|
+
const funcNames = new Set(ctx.func.list.map(f => f.name))
|
|
156
|
+
const visit = (n) => {
|
|
157
|
+
if (!Array.isArray(n)) return false
|
|
158
|
+
const [op, ...args] = n
|
|
159
|
+
if (op === '()') {
|
|
160
|
+
// callee at args[0]: skip if it's a bare func name (direct call); recurse rest
|
|
161
|
+
if (typeof args[0] !== 'string' || !funcNames.has(args[0])) {
|
|
162
|
+
if (visit(args[0])) return true
|
|
163
|
+
}
|
|
164
|
+
for (let i = 1; i < args.length; i++) {
|
|
165
|
+
const a = args[i]
|
|
166
|
+
if (typeof a === 'string' && funcNames.has(a)) return true
|
|
167
|
+
if (visit(a)) return true
|
|
168
|
+
}
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
if (op === '.' || op === '?.') {
|
|
172
|
+
// obj at args[0] can be a func ref; prop at args[1] is a name, never a ref
|
|
173
|
+
if (typeof args[0] === 'string' && funcNames.has(args[0])) return true
|
|
174
|
+
return visit(args[0])
|
|
175
|
+
}
|
|
176
|
+
if (op === '=>') {
|
|
177
|
+
// body only — params are bindings, not refs
|
|
178
|
+
return visit(args[1])
|
|
179
|
+
}
|
|
180
|
+
for (const a of args) {
|
|
181
|
+
if (typeof a === 'string' && funcNames.has(a)) return true
|
|
182
|
+
if (visit(a)) return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
let needs = visit(ast)
|
|
187
|
+
if (!needs) for (const f of ctx.func.list) if (f.body && visit(f.body)) { needs = true; break }
|
|
188
|
+
if (!needs && ctx.module.initFacts?.hasFuncValue) needs = true
|
|
189
|
+
if (needs) includeForCallableValue()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Native timers: inline WASM timer queue when referenced (no host imports needed)
|
|
193
|
+
const usedTimers = new Set(ctx.module.initFacts?.timerNames || [])
|
|
194
|
+
const scanTimers = (n) => {
|
|
195
|
+
if (!Array.isArray(n)) {
|
|
196
|
+
if (typeof n === 'string' && TIMER_NAMES.has(n)) usedTimers.add(n)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
for (let i = 0; i < n.length; i++) scanTimers(n[i])
|
|
200
|
+
}
|
|
201
|
+
const allNodes = [ast, ...ctx.func.list.map(f => f.body)]
|
|
202
|
+
for (const node of allNodes) scanTimers(node)
|
|
203
|
+
if (usedTimers.size) {
|
|
204
|
+
includeForTimerRuntime()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return ast
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Named constants → numeric literals
|
|
211
|
+
export const JZ_NULL = Symbol('null')
|
|
212
|
+
const CONSTANTS = { 'true': 1, 'false': 0, 'null': JZ_NULL, 'undefined': JZ_NULL }
|
|
213
|
+
// NaN/Infinity stay as special f64 values in emit()
|
|
214
|
+
const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
|
|
215
|
+
|
|
216
|
+
/** Resolve variable name through block scope chain (innermost rename wins). */
|
|
217
|
+
function resolveScope(name) {
|
|
218
|
+
for (let i = scopes.length - 1; i >= 0; i--)
|
|
219
|
+
if (scopes[i].has(name)) return scopes[i].get(name)
|
|
220
|
+
return name
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Check if name is declared in any current scope level. */
|
|
224
|
+
function isDeclared(name) {
|
|
225
|
+
return scopes.some(s => s.has(name))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const hasFunc = name => ctx.func.names.has(name)
|
|
229
|
+
|
|
230
|
+
const renameFunc = (func, nextName) => {
|
|
231
|
+
ctx.func.names.delete(func.name)
|
|
232
|
+
func.name = nextName
|
|
233
|
+
ctx.func.names.add(nextName)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
|
|
237
|
+
const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6 }
|
|
238
|
+
function resolveTypeof(node) {
|
|
239
|
+
const [op, a, b] = node
|
|
240
|
+
// typeof x == 'string' → type check
|
|
241
|
+
if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
|
|
242
|
+
const code = TYPEOF_MAP[b[1]]
|
|
243
|
+
if (code != null) return [op, ['typeof', a[1]], [, code]]
|
|
244
|
+
}
|
|
245
|
+
// 'string' == typeof x
|
|
246
|
+
if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
|
|
247
|
+
const code = TYPEOF_MAP[a[1]]
|
|
248
|
+
if (code != null) return [op, ['typeof', b[1]], [, code]]
|
|
249
|
+
}
|
|
250
|
+
return node
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const cloneNode = (node) => {
|
|
254
|
+
if (!Array.isArray(node)) return node
|
|
255
|
+
const copy = node.map(cloneNode)
|
|
256
|
+
if (node.loc != null) copy.loc = node.loc
|
|
257
|
+
return copy
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
|
|
261
|
+
* into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
|
|
262
|
+
* intermediate array. Pre-prep AST mutation; only fires on shapes where every use of `b` is a
|
|
263
|
+
* numeric `b[idx]` read or a `b.length` read, the arrow is pure with a single named param, and
|
|
264
|
+
* `b` is not referenced after the consumer for-loop. Preserves observable behavior because the
|
|
265
|
+
* arrow's pure-expression body has no order-dependent effects. */
|
|
266
|
+
function fuseSparseMapReads(root) {
|
|
267
|
+
walkSparse(root)
|
|
268
|
+
}
|
|
269
|
+
function walkSparse(node) {
|
|
270
|
+
if (!Array.isArray(node)) return
|
|
271
|
+
for (let i = 1; i < node.length; i++) walkSparse(node[i])
|
|
272
|
+
if (node[0] === ';') tryFuseInBlock(node)
|
|
273
|
+
}
|
|
274
|
+
function tryFuseInBlock(seq) {
|
|
275
|
+
for (let i = 1; i < seq.length - 1; i++) {
|
|
276
|
+
const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
|
|
277
|
+
if (fused) {
|
|
278
|
+
seq.splice(i, 2, ...fused)
|
|
279
|
+
i-- // re-examine same position (chained fusions)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function tryFusePair(decl, forNode, seq, declIdx) {
|
|
284
|
+
if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
|
|
285
|
+
if (decl.length !== 2) return null // single binding only
|
|
286
|
+
const bind = decl[1]
|
|
287
|
+
if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
|
|
288
|
+
const NAME = bind[1], rhs = bind[2]
|
|
289
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return null
|
|
290
|
+
const callee = rhs[1]
|
|
291
|
+
if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
|
|
292
|
+
const RECV = callee[1]
|
|
293
|
+
if (typeof RECV !== 'string' || RECV === NAME) return null
|
|
294
|
+
const arrow = rhs[2]
|
|
295
|
+
if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
|
|
296
|
+
// Single-name param only: `x => …` or `(x) => …`
|
|
297
|
+
const ap = arrow[1]
|
|
298
|
+
const PARAM = typeof ap === 'string' ? ap :
|
|
299
|
+
(Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
|
|
300
|
+
if (!PARAM || PARAM === NAME || PARAM === RECV) return null
|
|
301
|
+
// Body: single-expression arrow only (block bodies skipped — could extend later).
|
|
302
|
+
const aBody = arrow[2]
|
|
303
|
+
if (Array.isArray(aBody) && aBody[0] === '{}') return null
|
|
304
|
+
if (!isPureSparseArrowBody(aBody, PARAM)) return null
|
|
305
|
+
// For-loop: ['for', [';', initStmt, cond, inc], body]
|
|
306
|
+
if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
|
|
307
|
+
const head = forNode[1]
|
|
308
|
+
if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
|
|
309
|
+
const cond = head[2], forBody = forNode[2]
|
|
310
|
+
// Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
|
|
311
|
+
if (!hasOnlySparseUses(cond, NAME)) return null
|
|
312
|
+
if (!hasOnlySparseUses(forBody, NAME)) return null
|
|
313
|
+
if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
|
|
314
|
+
// `NAME` must not be read after the for-loop in the same block.
|
|
315
|
+
for (let k = declIdx + 2; k < seq.length; k++) {
|
|
316
|
+
if (refsName(seq[k], NAME)) return null
|
|
317
|
+
}
|
|
318
|
+
// RECV must not be reassigned inside the for-loop (would invalidate substitution).
|
|
319
|
+
if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
|
|
320
|
+
// PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
|
|
321
|
+
if (bindsName(forNode, PARAM)) return null
|
|
322
|
+
// Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
|
|
323
|
+
const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
|
|
324
|
+
const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
|
|
325
|
+
const newHead = [';', head[1], newCond, head[3]]
|
|
326
|
+
return [['for', newHead, newBody]]
|
|
327
|
+
}
|
|
328
|
+
function isPureSparseArrowBody(n, PARAM) {
|
|
329
|
+
if (typeof n === 'string') return true
|
|
330
|
+
if (!Array.isArray(n)) return true
|
|
331
|
+
const op = n[0]
|
|
332
|
+
// Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
|
|
333
|
+
if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
|
|
334
|
+
if (op === '=>') return false // nested closure is opaque
|
|
335
|
+
if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
|
|
336
|
+
if (op === '=') return false
|
|
337
|
+
for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
function hasOnlySparseUses(n, NAME) {
|
|
341
|
+
if (typeof n === 'string') return n !== NAME
|
|
342
|
+
if (!Array.isArray(n)) return true
|
|
343
|
+
const op = n[0]
|
|
344
|
+
if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
|
|
345
|
+
if (op === '.' && n[1] === NAME) {
|
|
346
|
+
if (n[2] === 'length') return true
|
|
347
|
+
return false // any other property access on NAME is opaque
|
|
348
|
+
}
|
|
349
|
+
for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
|
|
350
|
+
return true
|
|
351
|
+
}
|
|
352
|
+
function hasAnyIndexedRead(n, NAME) {
|
|
353
|
+
if (!Array.isArray(n)) return false
|
|
354
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
|
|
355
|
+
for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
function refsName(n, NAME) {
|
|
359
|
+
if (typeof n === 'string') return n === NAME
|
|
360
|
+
if (!Array.isArray(n)) return false
|
|
361
|
+
for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
function assignsName(n, NAME) {
|
|
365
|
+
if (!Array.isArray(n)) return false
|
|
366
|
+
const op = n[0]
|
|
367
|
+
if ((op === '=' || op === '++' || op === '--' ||
|
|
368
|
+
(typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
369
|
+
&& n[1] === NAME) return true
|
|
370
|
+
for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
|
|
371
|
+
return false
|
|
372
|
+
}
|
|
373
|
+
function bindsName(n, NAME) {
|
|
374
|
+
if (!Array.isArray(n)) return false
|
|
375
|
+
const op = n[0]
|
|
376
|
+
if ((op === 'let' || op === 'const')) {
|
|
377
|
+
for (let i = 1; i < n.length; i++) {
|
|
378
|
+
const bind = n[i]
|
|
379
|
+
if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (op === '=>') {
|
|
383
|
+
const p = n[1]
|
|
384
|
+
if (p === NAME) return true
|
|
385
|
+
if (Array.isArray(p)) {
|
|
386
|
+
if (p[0] === '()' && p[1] === NAME) return true
|
|
387
|
+
// skip deeper destructuring forms — conservative
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
|
|
391
|
+
return false
|
|
392
|
+
}
|
|
393
|
+
function substSparse(n, NAME, RECV, PARAM, arrowBody) {
|
|
394
|
+
if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
|
|
395
|
+
if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
|
|
396
|
+
if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
|
|
397
|
+
const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
|
|
398
|
+
return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
|
|
399
|
+
}
|
|
400
|
+
return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
|
|
401
|
+
}
|
|
402
|
+
function cloneAndBind(node, PARAM, replacement) {
|
|
403
|
+
if (node === PARAM) return replacement
|
|
404
|
+
if (!Array.isArray(node)) return node
|
|
405
|
+
return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function prep(node) {
|
|
409
|
+
if (Array.isArray(node)) includeForOp(node[0])
|
|
410
|
+
if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
|
|
411
|
+
if (node == null) return [, 0] // null/undefined → 0 literal
|
|
412
|
+
if (node === true) return [, 1]
|
|
413
|
+
if (node === false) return [, 0]
|
|
414
|
+
if (!Array.isArray(node)) {
|
|
415
|
+
if (typeof node === 'string') {
|
|
416
|
+
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
417
|
+
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
418
|
+
if (PROHIBITED[node]) err(PROHIBITED[node])
|
|
419
|
+
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
420
|
+
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
421
|
+
const resolved = ctx.scope.chain[node]
|
|
422
|
+
if (resolved?.includes('.')) return resolved
|
|
423
|
+
// Cross-module import: mangled name (e.g. __util_js$clone)
|
|
424
|
+
if (resolved && resolved !== node) return resolved
|
|
425
|
+
// Block scope: resolve renames
|
|
426
|
+
if (scopes.length) return resolveScope(node)
|
|
427
|
+
}
|
|
428
|
+
return node
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const [op, ...args] = node
|
|
432
|
+
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
|
|
433
|
+
if (op == null) {
|
|
434
|
+
if (typeof args[0] === 'string') {
|
|
435
|
+
includeForStringValue()
|
|
436
|
+
return ['str', args[0]] // string literal
|
|
437
|
+
}
|
|
438
|
+
return [, args[0]] // number literal
|
|
439
|
+
}
|
|
440
|
+
const handler = handlers[op]
|
|
441
|
+
return handler ? handler(...args) : [op, ...args.map(prep)]
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// FIXME: can we jzify some of these?
|
|
445
|
+
const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
|
|
446
|
+
'this': '`this` not supported: use explicit parameter',
|
|
447
|
+
'super': '`super` not supported: no class inheritance',
|
|
448
|
+
'arguments': '`arguments` not supported: use rest params',
|
|
449
|
+
'eval': '`eval` not supported'
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Predefined globals seeded into scope.chain at ctx.reset(). Value is the scope alias
|
|
453
|
+
// used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
|
|
454
|
+
// resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
|
|
455
|
+
// Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
|
|
456
|
+
// program (they do not live in any module). jzify auto-injecting imports would still
|
|
457
|
+
// need a list of these names to know what to emit, so the table lives here either way.
|
|
458
|
+
export const GLOBALS = {
|
|
459
|
+
Math: 'math',
|
|
460
|
+
Number: 'Number',
|
|
461
|
+
Array: 'Array',
|
|
462
|
+
Object: 'Object',
|
|
463
|
+
Symbol: 'Symbol',
|
|
464
|
+
JSON: 'JSON',
|
|
465
|
+
isNaN: 'number',
|
|
466
|
+
isFinite: 'number',
|
|
467
|
+
parseInt: 'number',
|
|
468
|
+
parseFloat: 'number',
|
|
469
|
+
Error: 'Error',
|
|
470
|
+
BigInt: 'BigInt',
|
|
471
|
+
TextEncoder: 'TextEncoder',
|
|
472
|
+
TextDecoder: 'TextDecoder',
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
|
|
476
|
+
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
477
|
+
|
|
478
|
+
function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
479
|
+
if (Array.isArray(target) && target[0] === '=') {
|
|
480
|
+
pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (isDestructPattern(target)) {
|
|
485
|
+
const tmp = `${T}d${ctx.func.uniq++}`
|
|
486
|
+
if (decls) decls.push(['=', tmp, valueExpr])
|
|
487
|
+
else out.push(['=', tmp, valueExpr])
|
|
488
|
+
expandDestruct(target, tmp, out, decls)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
out.push(['=', target, valueExpr])
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function expandDestruct(pattern, source, out, decls = null) {
|
|
496
|
+
if (!isDestructPattern(pattern)) return
|
|
497
|
+
|
|
498
|
+
if (pattern[0] === '[]') {
|
|
499
|
+
includeForArrayPattern()
|
|
500
|
+
const items = patternItems(pattern[1])
|
|
501
|
+
for (let j = 0; j < items.length; j++) {
|
|
502
|
+
const item = items[j]
|
|
503
|
+
if (item == null) continue
|
|
504
|
+
|
|
505
|
+
if (Array.isArray(item) && item[0] === '...') {
|
|
506
|
+
pushPatternAssign(item[1], ['()', ['.', source, 'slice'], [, j]], out, decls)
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
pushPatternAssign(item, ['[]', source, [, j]], out, decls)
|
|
511
|
+
}
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
includeForObjectPattern()
|
|
516
|
+
const items = patternItems(pattern[1])
|
|
517
|
+
|
|
518
|
+
// Collect explicit keys and detect rest pattern
|
|
519
|
+
let restTarget = null
|
|
520
|
+
const explicitKeys = []
|
|
521
|
+
for (const item of items) {
|
|
522
|
+
if (item == null) continue
|
|
523
|
+
if (Array.isArray(item) && item[0] === '...') { restTarget = item[1]; continue }
|
|
524
|
+
if (typeof item === 'string') explicitKeys.push(item)
|
|
525
|
+
else if (Array.isArray(item) && item[0] === '=') { if (typeof item[1] === 'string') explicitKeys.push(item[1]) }
|
|
526
|
+
else if (Array.isArray(item) && item[0] === ':') explicitKeys.push(item[1])
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (const item of items) {
|
|
530
|
+
if (item == null) continue
|
|
531
|
+
if (Array.isArray(item) && item[0] === '...') continue // handled below
|
|
532
|
+
|
|
533
|
+
if (typeof item === 'string') {
|
|
534
|
+
pushPatternAssign(item, ['.', source, item], out, decls)
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (Array.isArray(item) && item[0] === '=') {
|
|
539
|
+
if (typeof item[1] === 'string')
|
|
540
|
+
pushPatternAssign(item[1], ['??', ['.', source, item[1]], prep(item[2])], out, decls)
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (Array.isArray(item) && item[0] === ':') {
|
|
545
|
+
pushPatternAssign(item[2], ['.', source, item[1]], out, decls)
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Object rest: {x, ...rest} = obj → rest = {remaining props from source schema}
|
|
551
|
+
if (restTarget) {
|
|
552
|
+
const srcSchema = typeof source === 'string' && ctx.schema.resolve(source)
|
|
553
|
+
if (srcSchema) {
|
|
554
|
+
const remaining = srcSchema.filter(k => !explicitKeys.includes(k))
|
|
555
|
+
if (remaining.length) {
|
|
556
|
+
const restProps = remaining.map(k => [':', k, ['.', source, k]])
|
|
557
|
+
const restObj = ['{}', remaining.length === 1 ? restProps[0] : [',', ...restProps]]
|
|
558
|
+
// Register schema for the rest variable so property access works
|
|
559
|
+
if (typeof restTarget === 'string') ctx.schema.vars.set(restTarget, ctx.schema.register(remaining))
|
|
560
|
+
pushPatternAssign(restTarget, restObj, out, decls)
|
|
561
|
+
} else {
|
|
562
|
+
pushPatternAssign(restTarget, ['{}'], out, decls)
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
err('Object rest (...) requires source with known schema — destructure the object before passing to function, or use explicit property access')
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Prepare let/const declaration. */
|
|
571
|
+
function prepDecl(op, ...inits) {
|
|
572
|
+
const rest = []
|
|
573
|
+
for (const i of inits) {
|
|
574
|
+
if (Array.isArray(i) && i[0] === '()' && typeof i[1] === 'string' && Array.isArray(i[2]) && i[2][0] === '=' && isDestructPattern(i[2][1])) {
|
|
575
|
+
if (rest.length === 0 && inits.length === 1) return [';', [op, i[1]], prep(i[2])]
|
|
576
|
+
err('destructuring assignment after declaration must be a separate statement')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!Array.isArray(i) || i[0] !== '=') { rest.push(i); continue }
|
|
580
|
+
const [, name, init] = i, normed = prep(init)
|
|
581
|
+
|
|
582
|
+
if (isDestructPattern(name)) {
|
|
583
|
+
const tmp = `${T}d${ctx.func.uniq++}`
|
|
584
|
+
rest.push(['=', tmp, normed])
|
|
585
|
+
// Propagate schema to temp so rest destructuring can resolve it
|
|
586
|
+
if (typeof normed === 'string' && ctx.schema.vars.has(normed))
|
|
587
|
+
ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
|
|
588
|
+
else if (Array.isArray(normed) && normed[0] === '{}') {
|
|
589
|
+
const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
590
|
+
if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
|
|
591
|
+
}
|
|
592
|
+
expandDestruct(name, tmp, rest)
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!defFunc(name, normed)) {
|
|
597
|
+
let declName = name
|
|
598
|
+
// Block scope: rename if shadowing an outer declaration
|
|
599
|
+
if (typeof name === 'string' && scopes.length > 0 && isDeclared(name)) {
|
|
600
|
+
declName = `${name}${T}${ctx.func.uniq++}`
|
|
601
|
+
scopes[scopes.length - 1].set(name, declName)
|
|
602
|
+
} else if (typeof name === 'string' && scopes.length > 0) {
|
|
603
|
+
scopes[scopes.length - 1].set(name, name)
|
|
604
|
+
}
|
|
605
|
+
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
606
|
+
if (typeof declName === 'string' && depth === 0) {
|
|
607
|
+
if (ctx.module.currentPrefix) {
|
|
608
|
+
declName = `${ctx.module.currentPrefix}$${declName}`
|
|
609
|
+
ctx.scope.chain[name] = declName
|
|
610
|
+
}
|
|
611
|
+
if (op === 'const') {
|
|
612
|
+
if (!ctx.scope.consts) ctx.scope.consts = new Set()
|
|
613
|
+
ctx.scope.consts.add(declName)
|
|
614
|
+
if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
|
|
615
|
+
(ctx.scope.constStrs ||= new Map()).set(declName, normed[1])
|
|
616
|
+
} else if (op === 'let' && ctx.scope.consts?.has(declName)) {
|
|
617
|
+
ctx.scope.consts.delete(declName)
|
|
618
|
+
ctx.scope.constStrs?.delete(declName)
|
|
619
|
+
}
|
|
620
|
+
recordGlobalValueFact(declName, normed)
|
|
621
|
+
}
|
|
622
|
+
// Track object schemas (after prefix so schema is keyed to final name)
|
|
623
|
+
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
|
|
624
|
+
const props = []
|
|
625
|
+
for (const p of normed.slice(1)) {
|
|
626
|
+
if (Array.isArray(p) && p[0] === ':') props.push(p[1])
|
|
627
|
+
else if (Array.isArray(p) && p[0] === '...') {
|
|
628
|
+
// Merge spread source schema into this object's schema
|
|
629
|
+
const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
|
|
630
|
+
if (srcSchema) for (const n of srcSchema) { if (!props.includes(n)) props.push(n) }
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
|
|
634
|
+
}
|
|
635
|
+
// Module-scope variable → WASM global (mark as user-declared)
|
|
636
|
+
if (depth === 0 && typeof declName === 'string') {
|
|
637
|
+
if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
|
|
638
|
+
ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
|
|
639
|
+
ctx.scope.userGlobals.add(declName)
|
|
640
|
+
}
|
|
641
|
+
rest.push(['=', declName, normed])
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return rest.length ? [op, ...rest] : null
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const handlers = {
|
|
648
|
+
// Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
|
|
649
|
+
'...'(expr) {
|
|
650
|
+
includeForArrayLiteral()
|
|
651
|
+
return ['...', prep(expr)]
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
// Prohibited ops — duplicated from jzify deliberately: .jz source bypasses jzify,
|
|
655
|
+
// so prepare is the actual defense. Messages here fire for both .js and .jz.
|
|
656
|
+
'async': () => err('async/await not supported: WASM is synchronous'),
|
|
657
|
+
'await': () => err('async/await not supported: WASM is synchronous'),
|
|
658
|
+
'class': () => err('class not supported: use object literals'),
|
|
659
|
+
'yield': () => err('generators not supported: use loops'),
|
|
660
|
+
'debugger': () => null,
|
|
661
|
+
'delete': () => err('delete not supported: object shape is fixed'),
|
|
662
|
+
'in'(key, obj) { return ['in', prep(key), prep(obj)] },
|
|
663
|
+
'instanceof': () => err('instanceof not supported: use typeof'),
|
|
664
|
+
'with': () => err('`with` not supported: deprecated'),
|
|
665
|
+
':': () => err('labeled statements not supported'),
|
|
666
|
+
'var': () => err('`var` not supported: use let/const'),
|
|
667
|
+
'function': () => err('`function` not supported: use arrow functions'),
|
|
668
|
+
|
|
669
|
+
// Destructuring assignment: [a, ...b] = expr or {x, y} = expr
|
|
670
|
+
'='(lhs, rhs) {
|
|
671
|
+
// Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
|
|
672
|
+
// Distinguishing from index assignment: destructuring patterns have exactly one payload node.
|
|
673
|
+
if (isDestructPattern(lhs) && lhs.length === 2) {
|
|
674
|
+
const normed = prep(rhs)
|
|
675
|
+
const tmp = `${T}d${ctx.func.uniq++}`
|
|
676
|
+
const decls = [['=', tmp, normed]]
|
|
677
|
+
// Propagate schema to temp so rest destructuring can resolve it
|
|
678
|
+
if (typeof normed === 'string' && ctx.schema.vars.has(normed))
|
|
679
|
+
ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
|
|
680
|
+
const stmts = []
|
|
681
|
+
expandDestruct(lhs, tmp, stmts, decls)
|
|
682
|
+
return prep([';', ['let', ...decls], ...stmts])
|
|
683
|
+
}
|
|
684
|
+
// Parser ambiguity: }[pattern] = rhs mis-parsed as subscript when it's stmt; [pattern] = rhs
|
|
685
|
+
// Detect: ['[]', stmtExpr, commaExpr] with spread in comma → split into stmt + destructuring
|
|
686
|
+
if (Array.isArray(lhs) && lhs[0] === '[]' && lhs.length === 3) {
|
|
687
|
+
const hasSpr = n => Array.isArray(n) && (n[0] === '...' || n.some(hasSpr))
|
|
688
|
+
if (hasSpr(lhs[2])) {
|
|
689
|
+
const preStmt = lhs[1]
|
|
690
|
+
const pattern = ['[]', lhs[2]]
|
|
691
|
+
return prep([';', preStmt, ['=', pattern, rhs]])
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Function property assignment: fn.prop = arrow → extract as top-level function fn$prop
|
|
695
|
+
if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
|
|
696
|
+
&& hasFunc(lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
|
|
697
|
+
const name = `${lhs[1]}$${lhs[2]}`
|
|
698
|
+
if (defFunc(name, prep(rhs))) return null // extracted as function, no assignment needed
|
|
699
|
+
}
|
|
700
|
+
return ['=', prep(lhs), prep(rhs)]
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
// try/catch/throw
|
|
704
|
+
// Parser produces ['try', body, ['catch', param, handler]?, ['finally', cleanup]?]
|
|
705
|
+
'try'(body, ...clauses) {
|
|
706
|
+
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
707
|
+
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
708
|
+
const tryBody = prep(body)
|
|
709
|
+
const caught = catchClause
|
|
710
|
+
? (() => {
|
|
711
|
+
const [, errName, handler] = catchClause
|
|
712
|
+
return ['catch', tryBody, errName, prep(handler)]
|
|
713
|
+
})()
|
|
714
|
+
: tryBody
|
|
715
|
+
if (finallyClause) return ['finally', caught, prep(finallyClause[1])]
|
|
716
|
+
if (catchClause) {
|
|
717
|
+
const [, errName, handler] = catchClause
|
|
718
|
+
return ['catch', tryBody, errName, prep(handler)]
|
|
719
|
+
}
|
|
720
|
+
return tryBody
|
|
721
|
+
},
|
|
722
|
+
'throw'(expr) { return ['throw', prep(expr)] },
|
|
723
|
+
|
|
724
|
+
// Template literal: [``, part, ...] → fused single-allocation string concat.
|
|
725
|
+
'`'(...parts) {
|
|
726
|
+
includeForStringValue()
|
|
727
|
+
const nodes = parts.map(p =>
|
|
728
|
+
Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
|
|
729
|
+
return ['strcat', ...nodes]
|
|
730
|
+
},
|
|
731
|
+
|
|
732
|
+
// Tagged template: tag`a${x}b` → tag(['a','b'], x)
|
|
733
|
+
// Parser drops empty string segments; reinsert them to satisfy the strings.length === exprs.length + 1 invariant.
|
|
734
|
+
'``'(tag, ...parts) {
|
|
735
|
+
const strs = [], exprs = []
|
|
736
|
+
let prev = false
|
|
737
|
+
for (const p of parts) {
|
|
738
|
+
const isStr = Array.isArray(p) && p[0] == null && typeof p[1] === 'string'
|
|
739
|
+
if (isStr) { strs.push(p); prev = true }
|
|
740
|
+
else { if (!prev) strs.push([null, '']); exprs.push(p); prev = false }
|
|
741
|
+
}
|
|
742
|
+
if (!prev) strs.push([null, ''])
|
|
743
|
+
const arr = strs.length === 1 ? ['[]', strs[0]] : ['[]', [',', ...strs]]
|
|
744
|
+
const callArgs = exprs.length === 0 ? arr : [',', arr, ...exprs]
|
|
745
|
+
return prep(['()', tag, callArgs])
|
|
746
|
+
},
|
|
747
|
+
|
|
748
|
+
// Import
|
|
749
|
+
'import'(fromNode) {
|
|
750
|
+
if (!Array.isArray(fromNode) || fromNode[0] !== 'from')
|
|
751
|
+
return err('Dynamic import() not supported')
|
|
752
|
+
return handlers['from'](fromNode[1], fromNode[2])
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
'from'(specifiers, source) {
|
|
756
|
+
const mod = source?.[1]
|
|
757
|
+
if (!mod || typeof mod !== 'string') return err('Invalid import source')
|
|
758
|
+
|
|
759
|
+
// Host imports override built-ins for named imports
|
|
760
|
+
const hostMod = ctx.module.hostImports?.[mod]
|
|
761
|
+
let remaining = specifiers
|
|
762
|
+
if (hostMod && Array.isArray(specifiers) && specifiers[0] === '{}') {
|
|
763
|
+
const inner = specifiers[1]
|
|
764
|
+
if (inner != null) {
|
|
765
|
+
const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
|
|
766
|
+
const builtinItems = []
|
|
767
|
+
for (const item of items) {
|
|
768
|
+
const name = typeof item === 'string' ? item : item[1]
|
|
769
|
+
const alias = typeof item === 'string' ? item : item[2]
|
|
770
|
+
const spec = hostMod[name]
|
|
771
|
+
if (spec) {
|
|
772
|
+
addHostImport(mod, name, alias, spec)
|
|
773
|
+
} else {
|
|
774
|
+
builtinItems.push(item)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (builtinItems.length === 0) return null
|
|
778
|
+
if (!hasModule(mod)) {
|
|
779
|
+
const name = typeof builtinItems[0] === 'string' ? builtinItems[0] : builtinItems[0][1]
|
|
780
|
+
err(`'${name}' not declared in host module '${mod}'`)
|
|
781
|
+
}
|
|
782
|
+
remaining = ['{}', builtinItems.length === 1 ? builtinItems[0] : [',', ...builtinItems]]
|
|
783
|
+
} else {
|
|
784
|
+
return null
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Tier 1: Built-in module
|
|
789
|
+
if (hasModule(mod)) {
|
|
790
|
+
includeModule(mod)
|
|
791
|
+
const bind = (name, alias) => {
|
|
792
|
+
const key = mod + '.' + name
|
|
793
|
+
if (!ctx.core.emit[key]) err(`Unknown import: ${name} from '${mod}'`)
|
|
794
|
+
ctx.scope.chain[alias || name] = key
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (typeof remaining === 'string') { ctx.scope.chain[remaining] = mod; return null }
|
|
798
|
+
if (Array.isArray(remaining) && remaining[0] === 'as' && remaining[1] === '*') { ctx.scope.chain[remaining[2]] = mod; return null }
|
|
799
|
+
|
|
800
|
+
if (Array.isArray(remaining) && remaining[0] === '{}') {
|
|
801
|
+
const inner = remaining[1]
|
|
802
|
+
if (inner == null) return null
|
|
803
|
+
const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
|
|
804
|
+
for (const item of items)
|
|
805
|
+
if (typeof item === 'string') bind(item)
|
|
806
|
+
else if (Array.isArray(item) && item[0] === 'as') bind(item[1], item[2])
|
|
807
|
+
else err(`Invalid import specifier: ${JSON.stringify(item)}`)
|
|
808
|
+
}
|
|
809
|
+
return null
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Tier 2: Source module (bundling)
|
|
813
|
+
if (ctx.module.importSources?.[mod]) {
|
|
814
|
+
const resolved = prepareModule(mod, ctx.module.importSources[mod])
|
|
815
|
+
// Default import: import name from 'mod' → bind to default export
|
|
816
|
+
if (typeof specifiers === 'string') {
|
|
817
|
+
const mangled = resolved.exports.get('default')
|
|
818
|
+
if (!mangled) err(`'${mod}' has no default export`)
|
|
819
|
+
ctx.scope.chain[specifiers] = mangled
|
|
820
|
+
return null
|
|
821
|
+
}
|
|
822
|
+
// Namespace import: import * as X from 'mod' → bind X.prop to mangled names
|
|
823
|
+
if (Array.isArray(specifiers) && specifiers[0] === 'as' && specifiers[1] === '*') {
|
|
824
|
+
const alias = specifiers[2]
|
|
825
|
+
// Store namespace mapping so '.' handler can resolve X.prop → mangled name
|
|
826
|
+
if (!ctx.module.namespaces) ctx.module.namespaces = {}
|
|
827
|
+
ctx.module.namespaces[alias] = resolved.exports
|
|
828
|
+
return null
|
|
829
|
+
}
|
|
830
|
+
// Named imports: import { a, b } from 'mod'
|
|
831
|
+
if (Array.isArray(specifiers) && specifiers[0] === '{}') {
|
|
832
|
+
const inner = specifiers[1]
|
|
833
|
+
if (inner == null) return null
|
|
834
|
+
const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
|
|
835
|
+
for (const item of items) {
|
|
836
|
+
const name = typeof item === 'string' ? item : item[1]
|
|
837
|
+
const alias = typeof item === 'string' ? item : item[2]
|
|
838
|
+
const mangled = resolved.exports.get(name)
|
|
839
|
+
if (!mangled) err(`'${name}' is not exported from '${mod}'`)
|
|
840
|
+
ctx.scope.chain[alias] = mangled
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return null
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Tier 3: Host imports (non-built-in modules)
|
|
847
|
+
if (hostMod) {
|
|
848
|
+
if (Array.isArray(specifiers) && specifiers[0] === '{}') {
|
|
849
|
+
const inner = specifiers[1]
|
|
850
|
+
if (inner == null) return null
|
|
851
|
+
const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
|
|
852
|
+
for (const item of items) {
|
|
853
|
+
const name = typeof item === 'string' ? item : item[1]
|
|
854
|
+
const alias = typeof item === 'string' ? item : item[2]
|
|
855
|
+
const spec = hostMod[name]
|
|
856
|
+
if (!spec) err(`'${name}' not declared in host module '${mod}'`)
|
|
857
|
+
addHostImport(mod, name, alias, spec)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return null
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
err(`Unknown module '${mod}'. Provide it via { modules: { '${mod}': source } } or { imports: { '${mod}': {...} } }`)
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
// === is == in jz (all comparisons are strict). Also handle typeof x === 'type' patterns.
|
|
867
|
+
'==='(a, b) { return prep(resolveTypeof(['==', a, b])) },
|
|
868
|
+
'!=='(a, b) { return prep(resolveTypeof(['!=', a, b])) },
|
|
869
|
+
|
|
870
|
+
// Statements
|
|
871
|
+
';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
|
|
872
|
+
'let': (...inits) => prepDecl('let', ...inits),
|
|
873
|
+
'const': (...inits) => prepDecl('const', ...inits),
|
|
874
|
+
|
|
875
|
+
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
876
|
+
'if': (cond, then, els) => {
|
|
877
|
+
const c = prep(cond)
|
|
878
|
+
scopes.push(new Map()); const t = prep(then); scopes.pop()
|
|
879
|
+
if (els != null) { scopes.push(new Map()); const e = prep(els); scopes.pop(); return ['if', c, t, e] }
|
|
880
|
+
return ['if', c, t]
|
|
881
|
+
},
|
|
882
|
+
'while': (cond, body) => {
|
|
883
|
+
const c = prep(cond)
|
|
884
|
+
scopes.push(new Map()); const b = prep(body); scopes.pop()
|
|
885
|
+
return ['while', c, b]
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
'export': decl => {
|
|
889
|
+
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
890
|
+
for (const i of decl.slice(1))
|
|
891
|
+
if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
|
|
892
|
+
ctx.func.exports[i[1]] = true
|
|
893
|
+
// export { name1, name2 as alias } → register named exports
|
|
894
|
+
if (Array.isArray(decl) && decl[0] === '{}') {
|
|
895
|
+
const inner = decl[1]
|
|
896
|
+
if (inner == null) return null
|
|
897
|
+
const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
|
|
898
|
+
for (const item of items) {
|
|
899
|
+
if (typeof item === 'string') {
|
|
900
|
+
const resolved = ctx.scope.chain[item]
|
|
901
|
+
ctx.func.exports[item] = (resolved && resolved !== item) ? resolved : item
|
|
902
|
+
} else if (Array.isArray(item) && item[0] === 'as') {
|
|
903
|
+
const [, source, alias] = item
|
|
904
|
+
const resolved = ctx.scope.chain[source]
|
|
905
|
+
ctx.func.exports[alias] = (resolved && resolved !== source) ? resolved : source
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return null
|
|
909
|
+
}
|
|
910
|
+
// export default expr → mark 'default' export, rewrite to assignment
|
|
911
|
+
if (Array.isArray(decl) && decl[0] === 'default') {
|
|
912
|
+
const val = decl[1]
|
|
913
|
+
// export default name → export existing name as 'default'
|
|
914
|
+
if (typeof val === 'string' && (hasFunc(val) || ctx.scope.globals.has(val))) {
|
|
915
|
+
ctx.func.exports['default'] = val // alias
|
|
916
|
+
return null
|
|
917
|
+
}
|
|
918
|
+
// export default arrow → create function named 'default'
|
|
919
|
+
ctx.func.exports['default'] = true
|
|
920
|
+
if (Array.isArray(val) && val[0] === '=>') {
|
|
921
|
+
if (defFunc('default', prep(val))) return null
|
|
922
|
+
}
|
|
923
|
+
// export default expr → create global 'default'
|
|
924
|
+
ctx.scope.globals.set('default', `(global $default (mut f64) (f64.const 0))`)
|
|
925
|
+
ctx.scope.userGlobals.add('default')
|
|
926
|
+
return ['=', 'default', prep(val)]
|
|
927
|
+
}
|
|
928
|
+
return prep(decl)
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
// Arrow: don't prep params. Track depth for nested function detection.
|
|
932
|
+
'=>': (params, body) => {
|
|
933
|
+
if (depth > 0) { includeForCallableValue() }
|
|
934
|
+
const raw = extractParams(params)
|
|
935
|
+
const fnScope = new Map()
|
|
936
|
+
for (const n of collectParamNames(raw)) fnScope.set(n, n)
|
|
937
|
+
|
|
938
|
+
depth++
|
|
939
|
+
scopes.push(fnScope)
|
|
940
|
+
|
|
941
|
+
const nextParams = []
|
|
942
|
+
const bodyPrefix = []
|
|
943
|
+
for (const r of raw) {
|
|
944
|
+
const c = classifyParam(r)
|
|
945
|
+
if (c.kind === 'rest') {
|
|
946
|
+
nextParams.push(r)
|
|
947
|
+
if (typeof c.name === 'string') fnScope.set(c.name, c.name)
|
|
948
|
+
} else if (c.kind === 'plain') {
|
|
949
|
+
nextParams.push(c.name)
|
|
950
|
+
} else if (c.kind === 'default') {
|
|
951
|
+
nextParams.push(['=', c.name, prep(c.defValue)])
|
|
952
|
+
} else {
|
|
953
|
+
const tmp = `${T}p${ctx.func.uniq++}`
|
|
954
|
+
fnScope.set(tmp, tmp)
|
|
955
|
+
nextParams.push(c.kind === 'destruct-default' ? ['=', tmp, prep(c.defValue)] : tmp)
|
|
956
|
+
bodyPrefix.push(prep(['let', ['=', c.pattern, tmp]]))
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
let preparedBody = prep(body)
|
|
960
|
+
if (bodyPrefix.length) {
|
|
961
|
+
const prefix = bodyPrefix.filter(x => x != null)
|
|
962
|
+
if (Array.isArray(preparedBody) && preparedBody[0] === '{}' && Array.isArray(preparedBody[1]) && preparedBody[1][0] === ';')
|
|
963
|
+
preparedBody = ['{}', [';', ...prefix, ...preparedBody[1].slice(1)]]
|
|
964
|
+
else if (Array.isArray(preparedBody) && preparedBody[0] === '{}')
|
|
965
|
+
preparedBody = ['{}', [';', ...prefix, preparedBody[1]]]
|
|
966
|
+
else
|
|
967
|
+
preparedBody = ['{}', [';', ...prefix, ['return', preparedBody]]]
|
|
968
|
+
}
|
|
969
|
+
const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
|
|
970
|
+
const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
|
|
971
|
+
scopes.pop()
|
|
972
|
+
depth--
|
|
973
|
+
return result
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
// Switch: prep discriminant and case values/bodies
|
|
977
|
+
// Parser appends fall-through flag (number) to case bodies — strip it
|
|
978
|
+
'switch'(discriminant, ...cases) {
|
|
979
|
+
const prepCase = body => {
|
|
980
|
+
if (Array.isArray(body) && body[0] === ';')
|
|
981
|
+
return prep([';', ...body.slice(1).filter(s => typeof s !== 'number')])
|
|
982
|
+
return prep(body)
|
|
983
|
+
}
|
|
984
|
+
return ['switch', prep(discriminant), ...cases.map(c => {
|
|
985
|
+
if (c[0] === 'case') return ['case', prep(c[1]), prepCase(c[2])]
|
|
986
|
+
if (c[0] === 'default') return ['default', prep(c[1])]
|
|
987
|
+
return prep(c)
|
|
988
|
+
})]
|
|
989
|
+
},
|
|
990
|
+
|
|
991
|
+
// Optional chaining / typeof — need ptr module
|
|
992
|
+
'?.'(obj, prop) { return ['?.', prep(obj), prop] },
|
|
993
|
+
'?.[]'(obj, idx) { return ['?.[]', prep(obj), prep(idx)] },
|
|
994
|
+
'?.()'(callee, callArgs) {
|
|
995
|
+
// Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
|
|
996
|
+
const items = callArgs == null ? []
|
|
997
|
+
: Array.isArray(callArgs) && callArgs[0] === ',' ? callArgs.slice(1)
|
|
998
|
+
: [callArgs]
|
|
999
|
+
return ['?.()', prep(callee), ...items.map(prep)]
|
|
1000
|
+
},
|
|
1001
|
+
// Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
|
|
1002
|
+
'typeof'(a) {
|
|
1003
|
+
if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeForStringOnly(); return ['str', 'boolean'] }
|
|
1004
|
+
return ['typeof', prep(a)]
|
|
1005
|
+
},
|
|
1006
|
+
|
|
1007
|
+
// Unary +/- disambiguation
|
|
1008
|
+
'+'(a, b) {
|
|
1009
|
+
if (b === undefined) {
|
|
1010
|
+
const na = prep(a)
|
|
1011
|
+
if (isLit(na) && typeof na[1] === 'number') return na
|
|
1012
|
+
includeForNumericCoercion()
|
|
1013
|
+
return ['u+', na]
|
|
1014
|
+
}
|
|
1015
|
+
return ['+', prep(a), prep(b)]
|
|
1016
|
+
},
|
|
1017
|
+
'-'(a, b) {
|
|
1018
|
+
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
|
|
1019
|
+
return ['-', prep(a), prep(b)]
|
|
1020
|
+
},
|
|
1021
|
+
|
|
1022
|
+
// Ternary: parser emits '?' not '?:'
|
|
1023
|
+
'?'(cond, then, els) { return ['?:', prep(cond), prep(then), prep(els)] },
|
|
1024
|
+
|
|
1025
|
+
// ++/-- prefix vs postfix: parser sends trailing null for postfix
|
|
1026
|
+
// Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
|
|
1027
|
+
// Property increment: obj.prop++ → obj.prop = obj.prop + 1
|
|
1028
|
+
'++'(a, _post) {
|
|
1029
|
+
const n = prep(a)
|
|
1030
|
+
if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['+', n, [, 1]]]
|
|
1031
|
+
return _post !== undefined ? ['-', ['++', n], [, 1]] : ['++', n]
|
|
1032
|
+
},
|
|
1033
|
+
'--'(a, _post) {
|
|
1034
|
+
const n = prep(a)
|
|
1035
|
+
if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['-', n, [, 1]]]
|
|
1036
|
+
return _post !== undefined ? ['+', ['--', n], [, 1]] : ['--', n]
|
|
1037
|
+
},
|
|
1038
|
+
|
|
1039
|
+
// Regex literal: ['//','pattern','flags?'] → include regex module, pass through
|
|
1040
|
+
'//'(pattern, flags) {
|
|
1041
|
+
return ['//', pattern, flags]
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
'**'(a, b) { return ['**', prep(a), prep(b)] },
|
|
1045
|
+
|
|
1046
|
+
// Function call or grouping parens
|
|
1047
|
+
'()'(callee, ...args) {
|
|
1048
|
+
// Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
|
|
1049
|
+
if (args.length === 0) return prep(callee)
|
|
1050
|
+
|
|
1051
|
+
const hasRealArgs = args.some(a => a != null)
|
|
1052
|
+
|
|
1053
|
+
if (typeof callee === 'string') {
|
|
1054
|
+
if (PROHIBITED[callee]) err(PROHIBITED[callee])
|
|
1055
|
+
if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
|
|
1056
|
+
|
|
1057
|
+
if (includeForNamedCall(callee)) {
|
|
1058
|
+
if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
|
|
1059
|
+
return ['()', callee, ...args.filter(a => a != null).map(prep)]
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const resolved = ctx.scope.chain[callee]
|
|
1064
|
+
if (resolved?.includes('.')) callee = resolved
|
|
1065
|
+
else if (resolved && hasFunc(resolved)) callee = resolved
|
|
1066
|
+
else if (resolved && !resolved.includes('.')) {
|
|
1067
|
+
if (!ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1068
|
+
}
|
|
1069
|
+
else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
|
|
1070
|
+
includeForCallableValue()
|
|
1071
|
+
}
|
|
1072
|
+
} else if (Array.isArray(callee) && callee[0] === '.') {
|
|
1073
|
+
const [, obj, prop] = callee
|
|
1074
|
+
const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
|
|
1075
|
+
if (key && ctx.module.hostImports?.[obj]?.[prop]) {
|
|
1076
|
+
const spec = ctx.module.hostImports[obj][prop]
|
|
1077
|
+
const alias = `${obj}$${prop}`
|
|
1078
|
+
addHostImport(obj, prop, alias, spec)
|
|
1079
|
+
callee = alias
|
|
1080
|
+
} else if (key && includeForNamedCall(key)) {
|
|
1081
|
+
callee = key
|
|
1082
|
+
} else if (includeForGenericMethod(prop)) {
|
|
1083
|
+
callee = prep(callee)
|
|
1084
|
+
} else {
|
|
1085
|
+
const mod = ctx.scope.chain[obj]
|
|
1086
|
+
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
|
|
1087
|
+
callee = (includeModule(mod), mod + '.' + prop)
|
|
1088
|
+
} else {
|
|
1089
|
+
callee = prep(callee)
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
includeForCallableValue()
|
|
1094
|
+
callee = prep(callee)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const preppedArgs = args.filter(a => a != null).map(prep)
|
|
1098
|
+
for (const a of preppedArgs) {
|
|
1099
|
+
if (typeof a === 'string' && hasFunc(a)) {
|
|
1100
|
+
includeForCallableValue(); break
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const result = ['()', callee, ...preppedArgs]
|
|
1104
|
+
|
|
1105
|
+
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
1106
|
+
|
|
1107
|
+
return result
|
|
1108
|
+
},
|
|
1109
|
+
|
|
1110
|
+
// Array literal/indexing — auto-include ptr + array modules
|
|
1111
|
+
'[]'(...args) {
|
|
1112
|
+
if (args.length === 1) {
|
|
1113
|
+
const inner = args[0]
|
|
1114
|
+
includeForArrayLiteral()
|
|
1115
|
+
if (inner == null) return ['[']
|
|
1116
|
+
if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); if (items.length && items[items.length - 1] === null) items.pop(); return ['[', ...items.map(item => item == null ? [, JZ_NULL] : prep(item))] }
|
|
1117
|
+
return ['[', prep(inner)]
|
|
1118
|
+
}
|
|
1119
|
+
if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
|
|
1120
|
+
includeForStringOnly()
|
|
1121
|
+
const key = prep(args[1])
|
|
1122
|
+
const exports = [...ctx.module.namespaces[args[0]].entries()]
|
|
1123
|
+
let fallback = [, undefined]
|
|
1124
|
+
for (let i = exports.length - 1; i >= 0; i--) {
|
|
1125
|
+
const [name, resolved] = exports[i]
|
|
1126
|
+
fallback = ['?:', ['==', key, ['str', name]], resolved, fallback]
|
|
1127
|
+
}
|
|
1128
|
+
return fallback
|
|
1129
|
+
}
|
|
1130
|
+
includeForArrayAccess()
|
|
1131
|
+
return ['[]', prep(args[0]), prep(args[1])]
|
|
1132
|
+
},
|
|
1133
|
+
|
|
1134
|
+
// Bare block statement: push scope for let/const shadowing
|
|
1135
|
+
'{'(inner) {
|
|
1136
|
+
scopes.push(new Map())
|
|
1137
|
+
const result = ['{', prep(inner)]
|
|
1138
|
+
scopes.pop()
|
|
1139
|
+
return result
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
// Object literal - flatten comma, expand shorthand
|
|
1143
|
+
'{}'(inner) {
|
|
1144
|
+
// Detect block body vs object literal
|
|
1145
|
+
if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
|
|
1146
|
+
// Block body: push block scope for let/const shadowing
|
|
1147
|
+
scopes.push(new Map())
|
|
1148
|
+
const result = ['{}', prep(inner)]
|
|
1149
|
+
scopes.pop()
|
|
1150
|
+
return result
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
includeForObjectLiteral()
|
|
1154
|
+
if (inner == null) return ['{}']
|
|
1155
|
+
// Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
|
|
1156
|
+
const prop = p => {
|
|
1157
|
+
if (typeof p === 'string') return [':', p, prep(p)]
|
|
1158
|
+
if (Array.isArray(p) && p[0] === ':') {
|
|
1159
|
+
const key = typeof p[1] === 'string' ? p[1] : staticPropertyKey(p[1])
|
|
1160
|
+
if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
|
|
1161
|
+
return [':', key, prep(p[2])]
|
|
1162
|
+
}
|
|
1163
|
+
return prep(p)
|
|
1164
|
+
}
|
|
1165
|
+
const result = Array.isArray(inner) && inner[0] === ','
|
|
1166
|
+
? ['{}', ...inner.slice(1).map(prop)]
|
|
1167
|
+
: ['{}', prop(inner)]
|
|
1168
|
+
// Register schema so property access works for function params (duck typing)
|
|
1169
|
+
const props = result.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
1170
|
+
if (props.length && ctx.schema.register) ctx.schema.register(props)
|
|
1171
|
+
return result
|
|
1172
|
+
},
|
|
1173
|
+
|
|
1174
|
+
// For loop
|
|
1175
|
+
'for'(head, body) {
|
|
1176
|
+
scopes.push(new Map())
|
|
1177
|
+
let r
|
|
1178
|
+
if (Array.isArray(head) && head[0] === ';') {
|
|
1179
|
+
let [, init, cond, step] = head
|
|
1180
|
+
// Hoist .length / .size / .byteLength from for-condition:
|
|
1181
|
+
// `i < arr.length` → `let __len = arr.length; ... i < __len`
|
|
1182
|
+
// All three return i32 (TYPED/ARRAY/STRING/BUFFER/SET/MAP), so the hoisted
|
|
1183
|
+
// local stays i32 once exprType narrows it.
|
|
1184
|
+
if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
|
|
1185
|
+
const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
|
|
1186
|
+
if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
|
|
1187
|
+
(lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
|
|
1188
|
+
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
1189
|
+
const lenDecl = ['let', ['=', lenVar, lenExpr]]
|
|
1190
|
+
init = init ? [';', init, lenDecl] : lenDecl
|
|
1191
|
+
if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
|
|
1192
|
+
else cond = [cond[0], lenVar, cond[2]]
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
1196
|
+
} else if (Array.isArray(head) && head[0] === 'of') {
|
|
1197
|
+
// for (let x of arr) → hoist arr (if non-trivial) and arr.length once, iterate by index.
|
|
1198
|
+
// Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
|
|
1199
|
+
// jz philosophy: explicit > implicit; mutation during iteration is a code smell.
|
|
1200
|
+
const [, decl, src] = head
|
|
1201
|
+
const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
|
|
1202
|
+
const idx = `${T}i${ctx.func.uniq++}`
|
|
1203
|
+
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
1204
|
+
const trivial = typeof src === 'string'
|
|
1205
|
+
const arrVar = trivial ? src : `${T}arr${ctx.func.uniq++}`
|
|
1206
|
+
const decls = trivial
|
|
1207
|
+
? ['let', ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
|
|
1208
|
+
: ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
|
|
1209
|
+
const cond = ['<', idx, lenVar]
|
|
1210
|
+
const step = ['++', idx]
|
|
1211
|
+
const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
|
|
1212
|
+
r = prep(['for', [';', decls, cond, step], inner])
|
|
1213
|
+
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
1214
|
+
// for (let k in obj) → unroll at compile time when schema known, else HASH runtime iteration
|
|
1215
|
+
const [, decl, src] = head
|
|
1216
|
+
const varName = Array.isArray(decl) && decl[0] === 'let' ? decl[1] : decl
|
|
1217
|
+
const srcName = typeof src === 'string' ? (ctx.scope.chain[src] || src) : null
|
|
1218
|
+
const sid = typeof srcName === 'string' && ctx.schema.vars.get(srcName)
|
|
1219
|
+
if (sid != null) {
|
|
1220
|
+
// Known schema → compile-time unrolling with string keys
|
|
1221
|
+
const keys = ctx.schema.list[sid]
|
|
1222
|
+
if (!keys || !keys.length) { scopes.pop(); return null }
|
|
1223
|
+
includeForKnownKeyIteration()
|
|
1224
|
+
const stmts = []
|
|
1225
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1226
|
+
stmts.push(i === 0
|
|
1227
|
+
? ['let', ['=', varName, [, keys[i]]]]
|
|
1228
|
+
: ['=', varName, [, keys[i]]])
|
|
1229
|
+
stmts.push(cloneNode(body))
|
|
1230
|
+
}
|
|
1231
|
+
r = prep([';', ...stmts])
|
|
1232
|
+
} else {
|
|
1233
|
+
// Dynamic object → HASH runtime iteration
|
|
1234
|
+
includeForRuntimeKeyIteration()
|
|
1235
|
+
r = ['for-in', varName, prep(src), prep(body)]
|
|
1236
|
+
}
|
|
1237
|
+
} else {
|
|
1238
|
+
r = ['for', prep(head), prep(body)]
|
|
1239
|
+
}
|
|
1240
|
+
scopes.pop()
|
|
1241
|
+
return r
|
|
1242
|
+
},
|
|
1243
|
+
|
|
1244
|
+
// Property access - resolve namespaces or object/array properties
|
|
1245
|
+
'.'(obj, prop) {
|
|
1246
|
+
if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
|
|
1247
|
+
const mod = ctx.scope.chain[obj]
|
|
1248
|
+
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
1249
|
+
if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
|
|
1250
|
+
return includeModule(mod), mod + '.' + prop
|
|
1251
|
+
// Source module namespace: import * as X → X.prop resolved to mangled name
|
|
1252
|
+
if (typeof obj === 'string' && ctx.module.namespaces?.[obj]) {
|
|
1253
|
+
const mangled = ctx.module.namespaces[obj].get(prop)
|
|
1254
|
+
if (mangled) return mangled
|
|
1255
|
+
}
|
|
1256
|
+
includeForProperty(prop)
|
|
1257
|
+
return ['.', prep(obj), prop]
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
// new - auto-import modules, resolve constructors
|
|
1261
|
+
'new'(ctor, ...args) {
|
|
1262
|
+
let name = ctor, ctorArgs = args
|
|
1263
|
+
if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
|
|
1264
|
+
// Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
|
|
1265
|
+
if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
|
|
1266
|
+
ctorArgs = ctorArgs[0].slice(1)
|
|
1267
|
+
|
|
1268
|
+
// Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
|
|
1269
|
+
// expects callArgs as a single element (possibly comma-grouped).
|
|
1270
|
+
const wrapArgs = (args) => args.length === 0 ? [null]
|
|
1271
|
+
: args.length === 1 ? [prep(args[0])]
|
|
1272
|
+
: [[',', ...args.map(prep)]]
|
|
1273
|
+
if (includeForRuntimeCtor(name)) {
|
|
1274
|
+
return ['()', `new.${name}`, ...wrapArgs(ctorArgs)]
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const mod = ctx.scope.chain[name]
|
|
1278
|
+
if (typeof name === 'string' && mod && !mod.includes('.')) includeModule(mod)
|
|
1279
|
+
// Unknown constructor: treat as function call (jzify already strips new for known safe ones)
|
|
1280
|
+
if (typeof name === 'string') return ['()', name, ...ctorArgs.map(prep)]
|
|
1281
|
+
return ['new', prep(ctor), ...args.map(prep)]
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** Merge source schemas into target via Object.assign for compile-time schema inference. */
|
|
1286
|
+
function inferAssignSchema(callNode) {
|
|
1287
|
+
// After prep, args may be comma-grouped: ['()', callee, [',', target, s1, s2]]
|
|
1288
|
+
let assignArgs = callNode.slice(2)
|
|
1289
|
+
if (assignArgs.length === 1 && Array.isArray(assignArgs[0]) && assignArgs[0][0] === ',')
|
|
1290
|
+
assignArgs = assignArgs[0].slice(1)
|
|
1291
|
+
const [target, ...sources] = assignArgs
|
|
1292
|
+
if (typeof target !== 'string') return
|
|
1293
|
+
const existingId = ctx.schema.vars.get(target)
|
|
1294
|
+
const merged = existingId != null ? [...ctx.schema.list[existingId]] : []
|
|
1295
|
+
for (const src of sources) {
|
|
1296
|
+
let srcProps
|
|
1297
|
+
if (Array.isArray(src) && src[0] === '{}')
|
|
1298
|
+
srcProps = src.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
1299
|
+
else if (typeof src === 'string') {
|
|
1300
|
+
const srcId = ctx.schema.vars.get(src)
|
|
1301
|
+
if (srcId != null) srcProps = ctx.schema.list[srcId]
|
|
1302
|
+
}
|
|
1303
|
+
if (srcProps) for (const p of srcProps) if (!merged.includes(p)) merged.push(p)
|
|
1304
|
+
}
|
|
1305
|
+
if (merged.length) ctx.schema.vars.set(target, ctx.schema.register(merged))
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function defFunc(name, node) {
|
|
1309
|
+
if (!Array.isArray(node) || node[0] !== '=>') return false
|
|
1310
|
+
// Only extract top-level functions, not nested (closures stay as values)
|
|
1311
|
+
if (depth > 0) return false
|
|
1312
|
+
let [, rawParams, body] = node
|
|
1313
|
+
const raw = extractParams(rawParams)
|
|
1314
|
+
|
|
1315
|
+
// Extract param names and defaults via shared classifier.
|
|
1316
|
+
// Destructured params desugar to fresh tmp + let-binding prefix in body.
|
|
1317
|
+
const params = [], defaults = {}, hasRest = [], bodyPrefix = []
|
|
1318
|
+
for (const r of raw) {
|
|
1319
|
+
const c = classifyParam(r)
|
|
1320
|
+
if (c.kind === 'rest') { hasRest.push(c.name); params.push({ name: c.name, type: 'f64', rest: true }) }
|
|
1321
|
+
else if (c.kind === 'plain') params.push({ name: c.name, type: 'f64' })
|
|
1322
|
+
else if (c.kind === 'default') {
|
|
1323
|
+
params.push({ name: c.name, type: 'f64' })
|
|
1324
|
+
const defVal = prep(c.defValue)
|
|
1325
|
+
defaults[c.name] = defVal
|
|
1326
|
+
if (Array.isArray(defVal) && defVal[0] === '{}' && defVal.length > 1 && ctx.schema.register) {
|
|
1327
|
+
const props = defVal.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
1328
|
+
if (props.length) ctx.schema.vars.set(c.name, ctx.schema.register(props))
|
|
1329
|
+
}
|
|
1330
|
+
} else {
|
|
1331
|
+
const tmp = `${T}p${ctx.func.uniq++}`
|
|
1332
|
+
params.push({ name: tmp, type: 'f64' })
|
|
1333
|
+
if (c.kind === 'destruct-default') defaults[tmp] = prep(c.defValue)
|
|
1334
|
+
bodyPrefix.push(['let', ['=', c.pattern, tmp]])
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Prepend destructuring to body (body is already prepped, so prefix needs prep too)
|
|
1339
|
+
if (bodyPrefix.length) {
|
|
1340
|
+
const preppedPrefix = bodyPrefix.map(prep).filter(x => x != null)
|
|
1341
|
+
if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
|
|
1342
|
+
body = ['{}', [';', ...preppedPrefix, ...body[1].slice(1)]]
|
|
1343
|
+
else if (Array.isArray(body) && body[0] === '{}')
|
|
1344
|
+
body = ['{}', [';', ...preppedPrefix, body[1]]]
|
|
1345
|
+
else
|
|
1346
|
+
body = ['{}', [';', ...preppedPrefix, ['return', body]]]
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const sig = { params, results: detectResults(body) }
|
|
1350
|
+
const hasDefaults = Object.keys(defaults).length > 0
|
|
1351
|
+
// Only main-module top-level exports become wasm-boundary exports.
|
|
1352
|
+
// Sub-module `export let X` is just a re-importable symbol — staying internal
|
|
1353
|
+
// unlocks treeshake + type specialization once main stops referencing it.
|
|
1354
|
+
const exported = !!ctx.func.exports[name] && ctx.module.moduleStack.length === 0
|
|
1355
|
+
const funcInfo = { name, body, exported, sig, ...(hasDefaults && { defaults }) }
|
|
1356
|
+
if (hasRest.length) funcInfo.rest = hasRest[0] // track rest param name
|
|
1357
|
+
ctx.func.list.push(funcInfo)
|
|
1358
|
+
ctx.func.names.add(name)
|
|
1359
|
+
return true
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Multi-value threshold: ≤8 elements = tuple (multi-value return), >8 = memory array
|
|
1363
|
+
const MAX_MULTI = 8
|
|
1364
|
+
|
|
1365
|
+
/** Detect return arity from function body. */
|
|
1366
|
+
function detectResults(body) {
|
|
1367
|
+
// Expression body: [e1, e2, ...] → multi-return if ≤ threshold and no spreads
|
|
1368
|
+
if (Array.isArray(body) && body[0] === '[' && body.length > 2 && !body.some(e => Array.isArray(e) && e[0] === '...')) {
|
|
1369
|
+
const n = body.length - 1
|
|
1370
|
+
if (n <= MAX_MULTI) return Array(n).fill('f64')
|
|
1371
|
+
}
|
|
1372
|
+
// Block body: scan return statements
|
|
1373
|
+
if (Array.isArray(body) && body[0] === '{}') {
|
|
1374
|
+
const rets = []
|
|
1375
|
+
collectReturns(body, rets)
|
|
1376
|
+
if (rets.length) {
|
|
1377
|
+
const n = rets[0]
|
|
1378
|
+
if (n > 1 && n <= MAX_MULTI && rets.every(r => r === n)) return Array(n).fill('f64')
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return ['f64']
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** Collect return value arities from block AST. */
|
|
1385
|
+
function collectReturns(node, out) {
|
|
1386
|
+
if (!Array.isArray(node)) return
|
|
1387
|
+
if (node[0] === 'return') {
|
|
1388
|
+
const val = node[1]
|
|
1389
|
+
// Array return: count elements, but only if no spreads (spreads → runtime array, not multi-value)
|
|
1390
|
+
if (Array.isArray(val) && val[0] === '[' && val.length > 2 && !val.some(e => Array.isArray(e) && e[0] === '...'))
|
|
1391
|
+
out.push(val.length - 1)
|
|
1392
|
+
else out.push(1)
|
|
1393
|
+
return
|
|
1394
|
+
}
|
|
1395
|
+
for (let i = 1; i < node.length; i++) collectReturns(node[i], out)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const isLit = n => Array.isArray(n) && n[0] == null
|
|
1399
|
+
|
|
1400
|
+
/** Compile-time bundling: parse + prepare an imported module, collect exports. */
|
|
1401
|
+
function prepareModule(specifier, source) {
|
|
1402
|
+
includeModule('core')
|
|
1403
|
+
// Cycle detection
|
|
1404
|
+
if (ctx.module.moduleStack.includes(specifier))
|
|
1405
|
+
err(`Circular import: ${ctx.module.moduleStack.join(' -> ')} -> ${specifier}`)
|
|
1406
|
+
// Already resolved
|
|
1407
|
+
if (ctx.module.resolvedModules.has(specifier)) return ctx.module.resolvedModules.get(specifier)
|
|
1408
|
+
|
|
1409
|
+
ctx.module.moduleStack.push(specifier)
|
|
1410
|
+
|
|
1411
|
+
// Name mangling prefix: ./math.jz → _math_jz
|
|
1412
|
+
const prefix = specifier.replace(/[^a-zA-Z0-9]/g, '_')
|
|
1413
|
+
|
|
1414
|
+
// Save caller state
|
|
1415
|
+
const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
|
|
1416
|
+
const savedFuncCount = ctx.func.list.length // track new funcs from this module
|
|
1417
|
+
const savedModulePrefix = ctx.module.currentPrefix
|
|
1418
|
+
ctx.scope.chain = derive(savedScope) // inherit parent scope
|
|
1419
|
+
ctx.func.exports = {}
|
|
1420
|
+
ctx.module.currentPrefix = prefix
|
|
1421
|
+
|
|
1422
|
+
// Parse + prepare imported source (may trigger recursive imports)
|
|
1423
|
+
let ast = parse(normalizeSource(source))
|
|
1424
|
+
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
1425
|
+
const savedDepth = depth; depth = 0
|
|
1426
|
+
const moduleInit = prep(ast)
|
|
1427
|
+
depth = savedDepth
|
|
1428
|
+
|
|
1429
|
+
// Collect exports: rename exported funcs with prefix
|
|
1430
|
+
const moduleExports = new Map()
|
|
1431
|
+
for (const name of Object.keys(ctx.func.exports)) {
|
|
1432
|
+
const val = ctx.func.exports[name]
|
|
1433
|
+
// Default export alias: export default existingName → map 'default' to that name's mangled form
|
|
1434
|
+
if (name === 'default' && typeof val === 'string') {
|
|
1435
|
+
// Will resolve after all named exports are mangled
|
|
1436
|
+
continue
|
|
1437
|
+
}
|
|
1438
|
+
const mangled = `${prefix}$${name}`
|
|
1439
|
+
moduleExports.set(name, mangled)
|
|
1440
|
+
// Rename the function in ctx.func.list
|
|
1441
|
+
const func = ctx.func.list.find(f => f.name === name)
|
|
1442
|
+
if (func) renameFunc(func, mangled)
|
|
1443
|
+
// Rename globals
|
|
1444
|
+
if (ctx.scope.globals.has(name)) {
|
|
1445
|
+
const wat = ctx.scope.globals.get(name).replace(`$${name}`, `$${mangled}`)
|
|
1446
|
+
ctx.scope.globals.delete(name)
|
|
1447
|
+
ctx.scope.globals.set(mangled, wat)
|
|
1448
|
+
if (ctx.scope.userGlobals.has(name)) { ctx.scope.userGlobals.delete(name); ctx.scope.userGlobals.add(mangled) }
|
|
1449
|
+
if (ctx.scope.globalTypes.has(name)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(name)); ctx.scope.globalTypes.delete(name) }
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
// Resolve default export alias after named exports are mangled
|
|
1453
|
+
if (typeof ctx.func.exports['default'] === 'string') {
|
|
1454
|
+
const alias = ctx.func.exports['default']
|
|
1455
|
+
if (moduleExports.has(alias)) {
|
|
1456
|
+
// Already renamed as a named export
|
|
1457
|
+
moduleExports.set('default', moduleExports.get(alias))
|
|
1458
|
+
} else {
|
|
1459
|
+
// Not a named export — rename the function/global
|
|
1460
|
+
const mangled = `${prefix}$${alias}`
|
|
1461
|
+
moduleExports.set('default', mangled)
|
|
1462
|
+
const func = ctx.func.list.find(f => f.name === alias)
|
|
1463
|
+
if (func) renameFunc(func, mangled)
|
|
1464
|
+
if (ctx.scope.globals.has(alias)) {
|
|
1465
|
+
const wat = ctx.scope.globals.get(alias).replace(`$${alias}`, `$${mangled}`)
|
|
1466
|
+
ctx.scope.globals.delete(alias)
|
|
1467
|
+
ctx.scope.globals.set(mangled, wat)
|
|
1468
|
+
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Rename ALL non-exported functions created during this module's prep
|
|
1474
|
+
// (fn property assignments like f32.parse, internal helpers like cleanInt)
|
|
1475
|
+
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1476
|
+
const func = ctx.func.list[i]
|
|
1477
|
+
if (func.raw || func.name.startsWith(prefix + '$')) continue
|
|
1478
|
+
// Skip functions from sub-imports (already prefixed with another module's prefix)
|
|
1479
|
+
if (func.name.includes('__') && func.name.includes('$')) continue
|
|
1480
|
+
const mangled = `${prefix}$${func.name}`
|
|
1481
|
+
moduleExports.set(func.name, mangled)
|
|
1482
|
+
renameFunc(func, mangled)
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Add mangled non-exported globals to moduleExports for walk renaming
|
|
1486
|
+
// (e.g., module-level const/let used by functions declared before the global)
|
|
1487
|
+
for (const [mangled, wat] of ctx.scope.globals) {
|
|
1488
|
+
if (mangled.startsWith(prefix + '$')) {
|
|
1489
|
+
const original = mangled.slice(prefix.length + 1)
|
|
1490
|
+
if (!moduleExports.has(original)) moduleExports.set(original, mangled)
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Rename references in function bodies — walk ALL functions created during this module's prep
|
|
1495
|
+
if (moduleExports.size) {
|
|
1496
|
+
const walk = (node, skip) => {
|
|
1497
|
+
if (!Array.isArray(node)) return typeof node === 'string' && !skip?.has(node) && moduleExports.has(node) ? moduleExports.get(node) : node
|
|
1498
|
+
if (node[0] === 'str' || node[0] == null || node[0] === '`' || node[0] === '//') return node
|
|
1499
|
+
if (node[0] === ':') { node[2] = walk(node[2], skip); return node }
|
|
1500
|
+
if (node[0] === '=>') {
|
|
1501
|
+
node[2] = walk(node[2], collectParamNames(extractParams(node[1]), new Set(skip)))
|
|
1502
|
+
return node
|
|
1503
|
+
}
|
|
1504
|
+
for (let j = 0; j < node.length; j++) node[j] = walk(node[j], skip)
|
|
1505
|
+
return node
|
|
1506
|
+
}
|
|
1507
|
+
for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
|
|
1508
|
+
const func = ctx.func.list[i]
|
|
1509
|
+
if (!func.body) continue
|
|
1510
|
+
const funcParams = new Set(func.sig?.params?.map(p => p.name) || [])
|
|
1511
|
+
walk(func.body, funcParams)
|
|
1512
|
+
if (func.defaults) for (const [k, v] of Object.entries(func.defaults)) func.defaults[k] = walk(v, funcParams)
|
|
1513
|
+
}
|
|
1514
|
+
// Also rename init code AST
|
|
1515
|
+
if (moduleInit) walk(moduleInit)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Collect sub-module init code (variable initializations) for __start
|
|
1519
|
+
if (moduleInit) {
|
|
1520
|
+
if (!ctx.module.moduleInits) ctx.module.moduleInits = []
|
|
1521
|
+
ctx.module.moduleInits.push(moduleInit)
|
|
1522
|
+
recordModuleInitFacts(moduleInit)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Restore caller state
|
|
1526
|
+
ctx.scope.chain = savedScope
|
|
1527
|
+
ctx.func.exports = savedExports
|
|
1528
|
+
ctx.module.currentPrefix = savedModulePrefix
|
|
1529
|
+
ctx.module.moduleStack.pop()
|
|
1530
|
+
|
|
1531
|
+
const result = { exports: moduleExports }
|
|
1532
|
+
ctx.module.resolvedModules.set(specifier, result)
|
|
1533
|
+
return result
|
|
1534
|
+
}
|