jz 0.0.0 → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/cli.js +163 -0
- package/index.js +217 -0
- package/module/array.js +1317 -0
- package/module/collection.js +791 -0
- package/module/console.js +190 -0
- package/module/core.js +642 -0
- package/module/function.js +180 -0
- package/module/index.js +15 -0
- package/module/json.js +504 -0
- package/module/math.js +389 -0
- package/module/number.js +605 -0
- package/module/object.js +357 -0
- package/module/regex.js +913 -0
- package/module/schema.js +104 -0
- package/module/string.js +928 -0
- package/module/symbol.js +54 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +711 -0
- package/package.json +54 -5
- package/src/analyze.js +1906 -0
- package/src/compile.js +2175 -0
- package/src/ctx.js +243 -0
- package/src/emit.js +2095 -0
- package/src/host.js +524 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +391 -0
- package/src/optimize.js +1352 -0
- package/src/prepare.js +1598 -0
- package/wasi.js +74 -0
package/src/emit.js
ADDED
|
@@ -0,0 +1,2095 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST → WASM IR emission.
|
|
3
|
+
*
|
|
4
|
+
* # Stage contract
|
|
5
|
+
* IN: prepared AST node + ctx state (func.locals, func.repByLocal, types.typedElem, etc.)
|
|
6
|
+
* OUT: IR node (array) with `.type` ('i32' | 'f64' | 'void'). For statements, a flat
|
|
7
|
+
* list of WASM instructions (no type tag).
|
|
8
|
+
* NO-MUTATE: emit does not rewrite the AST. Side effects go to ctx.runtime.*,
|
|
9
|
+
* ctx.core.includes (via inc()), ctx.func.uniq (local naming), and ctx.features.*.
|
|
10
|
+
*
|
|
11
|
+
* # Dispatch
|
|
12
|
+
* `emit(node, expect?)` handles literals inline and routes arrays to ctx.core.emit[op].
|
|
13
|
+
* `emitFlat(node)` emits + drops any value (statement context; routes block bodies to emitBody).
|
|
14
|
+
* `emitBody(node)` unwraps a `{}` block and concatenates flat statement IR.
|
|
15
|
+
*
|
|
16
|
+
* The emitter table (`emitter` export) is copied into ctx.core.emit by reset();
|
|
17
|
+
* language modules add/override entries to extend dispatch.
|
|
18
|
+
*
|
|
19
|
+
* Low-level IR construction helpers (typed/asF64/allocPtr/readVar/…) live in compile.js
|
|
20
|
+
* and are imported below.
|
|
21
|
+
*
|
|
22
|
+
* @module emit
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ctx, err, inc, PTR } from './ctx.js'
|
|
26
|
+
import { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, findFreeVars, STMT_OPS, repOf, updateRep, repOfGlobal } from './analyze.js'
|
|
27
|
+
import {
|
|
28
|
+
typed, asF64, asI32, asI64, asPtrOffset, asParamType, toI32, fromI64,
|
|
29
|
+
NULL_IR, nullExpr, undefExpr, MAX_CLOSURE_ARITY,
|
|
30
|
+
WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
31
|
+
mkPtrIR, ptrOffsetIR, ptrTypeIR,
|
|
32
|
+
isLit, litVal, isNullishLit, isPureIR, emitNum, f64rem, toNumF64,
|
|
33
|
+
truthyIR, toBoolFromEmitted, isPostfix,
|
|
34
|
+
isGlobal, isConst, keyValType, usesDynProps, needsDynShadow,
|
|
35
|
+
temp, tempI32, tempI64, allocPtr,
|
|
36
|
+
boxedAddr, readVar, writeVar, isNullish,
|
|
37
|
+
multiCount, loopTop, flat,
|
|
38
|
+
reconstructArgsWithSpreads,
|
|
39
|
+
} from './ir.js'
|
|
40
|
+
|
|
41
|
+
// Current emission "expect" mode ('void' or null); set by emit(), read by compound-assignment emitters
|
|
42
|
+
// to decide whether to emit a value-returning or side-effect-only form.
|
|
43
|
+
let _expect = null
|
|
44
|
+
|
|
45
|
+
/** Emit typeof comparison: typeof x == typeCode → type-aware check. */
|
|
46
|
+
export function emitTypeofCmp(a, b, cmpOp) {
|
|
47
|
+
let typeofExpr, code
|
|
48
|
+
if (Array.isArray(a) && a[0] === 'typeof' && typeof b === 'number') { typeofExpr = a[1]; code = b }
|
|
49
|
+
else if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null) { typeofExpr = a[1]; code = b[1] }
|
|
50
|
+
else return null
|
|
51
|
+
if (typeof code !== 'number') return null
|
|
52
|
+
|
|
53
|
+
const t = temp()
|
|
54
|
+
const va = asF64(emit(typeofExpr))
|
|
55
|
+
const eq = cmpOp === 'eq'
|
|
56
|
+
|
|
57
|
+
if (code === -1) {
|
|
58
|
+
return typed(eq
|
|
59
|
+
? ['f64.eq', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
60
|
+
: ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
|
|
61
|
+
}
|
|
62
|
+
if (code === -2) {
|
|
63
|
+
inc('__ptr_type')
|
|
64
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
65
|
+
const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
|
|
66
|
+
const isStr = ['i32.or',
|
|
67
|
+
['i32.eq', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
|
|
68
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]]
|
|
69
|
+
return typed(eq ? ['i32.and', isPtr, isStr]
|
|
70
|
+
: ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isStr]], 'i32')
|
|
71
|
+
}
|
|
72
|
+
if (code === -3) {
|
|
73
|
+
const check = isNullish(va)
|
|
74
|
+
return typed(eq ? check : ['i32.eqz', check], 'i32')
|
|
75
|
+
}
|
|
76
|
+
if (code === -4) {
|
|
77
|
+
return typed(['i32.const', eq ? 0 : 1], 'i32')
|
|
78
|
+
}
|
|
79
|
+
if (code === -5) {
|
|
80
|
+
inc('__ptr_type')
|
|
81
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
82
|
+
const tt = `${T}${ctx.func.uniq++}`; ctx.func.locals.set(tt, 'i32')
|
|
83
|
+
const notStrFn = ['i32.and',
|
|
84
|
+
['i32.and',
|
|
85
|
+
['i32.ne', ['local.tee', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
|
|
86
|
+
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
|
|
87
|
+
['i32.ne', ['local.get', `$${tt}`], ['i32.const', PTR.CLOSURE]]]
|
|
88
|
+
const notNullish = ['i32.eqz', isNullish(['local.get', `$${t}`])]
|
|
89
|
+
const check = ['i32.and', ['i32.and', isPtr, notStrFn], notNullish]
|
|
90
|
+
return typed(eq ? check : ['i32.eqz', check], 'i32')
|
|
91
|
+
}
|
|
92
|
+
if (code === -6) {
|
|
93
|
+
inc('__ptr_type')
|
|
94
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
95
|
+
const isFn = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', PTR.CLOSURE]]
|
|
96
|
+
return typed(eq ? ['i32.and', isPtr, isFn] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', isFn]], 'i32')
|
|
97
|
+
}
|
|
98
|
+
if (code >= 0) {
|
|
99
|
+
inc('__ptr_type')
|
|
100
|
+
const isPtr = ['f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]]
|
|
101
|
+
const check = ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', code]]
|
|
102
|
+
return typed(eq ? ['i32.and', isPtr, check] : ['i32.or', ['i32.eqz', isPtr], ['i32.eqz', check]], 'i32')
|
|
103
|
+
}
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const CMP_SET = new Set(['>', '<', '>=', '<=', '==', '!=', '!'])
|
|
108
|
+
const isCmp = n => Array.isArray(n) && CMP_SET.has(n[0])
|
|
109
|
+
|
|
110
|
+
// Pointer kinds for which JS `==` / `!=` is pure reference equality — i.e. i64 bit
|
|
111
|
+
// compare of the NaN-box is equivalent to __eq. Excludes STRING (content compare for
|
|
112
|
+
// heap strings) and BIGINT (content compare).
|
|
113
|
+
const REF_EQ_KINDS = new Set([
|
|
114
|
+
VAL.ARRAY, VAL.OBJECT, VAL.SET, VAL.MAP,
|
|
115
|
+
VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.REGEX,
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
// === Flow-sensitive type refinement ===
|
|
119
|
+
// Map typeof code (from resolveTypeof in prepare.js) → VAL kind. Undef/boolean/object have no
|
|
120
|
+
// single VAL refinement, so they're excluded. String/number/function do.
|
|
121
|
+
const TYPEOF_CODE_TO_VAL = { [-1]: VAL.NUMBER, [-2]: VAL.STRING, [-6]: VAL.CLOSURE }
|
|
122
|
+
|
|
123
|
+
/** Extract refinements from a boolean condition AST.
|
|
124
|
+
* `sense`: true = refine for then-branch, false = refine for else-branch (i.e. cond inverted).
|
|
125
|
+
* Returns a Map<name, VAL>. Walks && / || / ! accordingly. */
|
|
126
|
+
function extractRefinements(cond, out, sense = true) {
|
|
127
|
+
if (!Array.isArray(cond)) return out
|
|
128
|
+
const op = cond[0]
|
|
129
|
+
// ! flips sense
|
|
130
|
+
if (op === '!') return extractRefinements(cond[1], out, !sense)
|
|
131
|
+
// && under positive sense refines with union of both branches.
|
|
132
|
+
// || under negative sense (De Morgan) similarly refines the else-branch.
|
|
133
|
+
if (op === '&&' && sense) { extractRefinements(cond[1], out, true); extractRefinements(cond[2], out, true); return out }
|
|
134
|
+
if (op === '||' && !sense) { extractRefinements(cond[1], out, false); extractRefinements(cond[2], out, false); return out }
|
|
135
|
+
// typeof x == 'number' | 'string' | 'function' — sense must be positive for "==", negative for "!="
|
|
136
|
+
if ((op === '==' || op === '===' || op === '!=' || op === '!==')) {
|
|
137
|
+
const eq = (op === '==' || op === '===')
|
|
138
|
+
const wantPositive = eq ? sense : !sense
|
|
139
|
+
if (!wantPositive) return out
|
|
140
|
+
const a = cond[1], b = cond[2]
|
|
141
|
+
const pair = Array.isArray(a) && a[0] === 'typeof' ? [a[1], b]
|
|
142
|
+
: Array.isArray(b) && b[0] === 'typeof' ? [b[1], a] : null
|
|
143
|
+
if (pair && typeof pair[0] === 'string' && Array.isArray(pair[1]) && pair[1][0] == null) {
|
|
144
|
+
const val = TYPEOF_CODE_TO_VAL[pair[1][1]]
|
|
145
|
+
if (val) out.set(pair[0], val)
|
|
146
|
+
}
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
149
|
+
// Array.isArray(x) — only refines under positive sense.
|
|
150
|
+
// Callee may be the flattened string 'Array.isArray' or the raw ['.', 'Array', 'isArray'] pair.
|
|
151
|
+
if (op === '()' && sense && typeof cond[2] === 'string') {
|
|
152
|
+
const callee = cond[1]
|
|
153
|
+
const isArr = callee === 'Array.isArray'
|
|
154
|
+
|| (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Array' && callee[2] === 'isArray')
|
|
155
|
+
if (isArr) { out.set(cond[2], VAL.ARRAY); return out }
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
|
|
161
|
+
* Conservative over-reject: if unsure, treat as written.
|
|
162
|
+
* `let`/`const` declarations are NOT reassignments — only the initializer expressions
|
|
163
|
+
* inside them are scanned. (Treating `let g = ...` as a write of `g` would defeat A3.) */
|
|
164
|
+
export function isReassigned(body, name) {
|
|
165
|
+
if (!Array.isArray(body)) return false
|
|
166
|
+
const op = body[0]
|
|
167
|
+
if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%='
|
|
168
|
+
|| op === '&=' || op === '|=' || op === '^=' || op === '<<=' || op === '>>=' || op === '>>>='
|
|
169
|
+
|| op === '||=' || op === '&&=' || op === '??=') {
|
|
170
|
+
if (body[1] === name) return true
|
|
171
|
+
}
|
|
172
|
+
if ((op === '++' || op === '--') && body[1] === name) return true
|
|
173
|
+
if (op === 'let' || op === 'const') {
|
|
174
|
+
// Each decl item is either a bare name (string) or `['=', pattern, init]`.
|
|
175
|
+
// Only the init expression can contain real reassignments — recurse into it only.
|
|
176
|
+
for (let i = 1; i < body.length; i++) {
|
|
177
|
+
const d = body[i]
|
|
178
|
+
if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
|
|
179
|
+
}
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Does `body` contain a `continue` that targets THIS loop?
|
|
187
|
+
* A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
|
|
188
|
+
function hasOwnContinue(body) {
|
|
189
|
+
if (!Array.isArray(body)) return false
|
|
190
|
+
const op = body[0]
|
|
191
|
+
if (op === 'continue') return true
|
|
192
|
+
if (op === 'for' || op === 'while' || op === 'do') return false
|
|
193
|
+
for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function hasOwnBreakOrContinue(body) {
|
|
198
|
+
if (!Array.isArray(body)) return false
|
|
199
|
+
const op = body[0]
|
|
200
|
+
if (op === 'break' || op === 'continue') return true
|
|
201
|
+
if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
|
|
202
|
+
for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function containsNestedClosure(body) {
|
|
207
|
+
if (!Array.isArray(body)) return false
|
|
208
|
+
if (body[0] === '=>') return true
|
|
209
|
+
for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function containsNestedLoop(body) {
|
|
214
|
+
if (!Array.isArray(body)) return false
|
|
215
|
+
const op = body[0]
|
|
216
|
+
if (op === 'for' || op === 'while' || op === 'do') return true
|
|
217
|
+
if (op === '=>') return false
|
|
218
|
+
for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function containsDeclOf(body, name) {
|
|
223
|
+
if (!Array.isArray(body)) return false
|
|
224
|
+
const op = body[0]
|
|
225
|
+
if (op === '=>') return false
|
|
226
|
+
if (op === 'let' || op === 'const') {
|
|
227
|
+
for (let i = 1; i < body.length; i++) {
|
|
228
|
+
const d = body[i]
|
|
229
|
+
if (d === name) return true
|
|
230
|
+
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function intLiteralValue(expr) {
|
|
238
|
+
let v = null
|
|
239
|
+
if (typeof expr === 'number') v = expr
|
|
240
|
+
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
241
|
+
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
242
|
+
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
243
|
+
return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function cloneWithSubst(node, name, value) {
|
|
247
|
+
if (node === name) return [null, value]
|
|
248
|
+
if (!Array.isArray(node)) return node
|
|
249
|
+
if (node[0] === '=>') return node
|
|
250
|
+
return node.map(x => cloneWithSubst(x, name, value))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const MAX_SMALL_FOR_UNROLL = 8
|
|
254
|
+
|
|
255
|
+
function unrollSmallConstFor(init, cond, step, body) {
|
|
256
|
+
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
257
|
+
const decl = init[1]
|
|
258
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
259
|
+
const name = decl[1]
|
|
260
|
+
const start = intLiteralValue(decl[2])
|
|
261
|
+
if (start !== 0) return null
|
|
262
|
+
|
|
263
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
264
|
+
const end = intLiteralValue(cond[2])
|
|
265
|
+
if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
|
|
266
|
+
|
|
267
|
+
const stepOk = Array.isArray(step) && (
|
|
268
|
+
(step[0] === '++' && step[1] === name) ||
|
|
269
|
+
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
270
|
+
)
|
|
271
|
+
if (!stepOk) return null
|
|
272
|
+
if (hasOwnBreakOrContinue(body) || containsNestedClosure(body) || containsNestedLoop(body) || containsDeclOf(body, name)) return null
|
|
273
|
+
if (isReassigned(body, name)) return null
|
|
274
|
+
|
|
275
|
+
const out = []
|
|
276
|
+
for (let i = 0; i < end; i++) out.push(...emitFlat(cloneWithSubst(body, name, i)))
|
|
277
|
+
return out
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Does `body` always exit the enclosing scope (return / throw / break / continue)?
|
|
281
|
+
* Used for early-return refinement: after `if (!guard) return`, `guard` holds for the rest. */
|
|
282
|
+
function isTerminator(body) {
|
|
283
|
+
if (!Array.isArray(body)) return false
|
|
284
|
+
const op = body[0]
|
|
285
|
+
if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
|
|
286
|
+
// Block body: {} or ; — terminator if it ends with a terminator statement.
|
|
287
|
+
if (op === '{}' || op === ';') {
|
|
288
|
+
for (let i = body.length - 1; i >= 1; i--) {
|
|
289
|
+
const s = body[i]
|
|
290
|
+
if (s == null) continue
|
|
291
|
+
return isTerminator(s)
|
|
292
|
+
}
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Emit pending `finally` cleanups for an abrupt control-flow exit.
|
|
299
|
+
* Inner cleanups run before outer cleanups. While emitting each cleanup, remove
|
|
300
|
+
* it from the active stack so `return` inside `finally` does not re-enter it. */
|
|
301
|
+
function emitFinalizers() {
|
|
302
|
+
const stack = ctx.func.finallyStack || []
|
|
303
|
+
if (stack.length === 0) return []
|
|
304
|
+
const saved = stack.slice()
|
|
305
|
+
const out = []
|
|
306
|
+
for (let i = saved.length - 1; i >= 0; i--) {
|
|
307
|
+
ctx.func.finallyStack = saved.slice(0, i)
|
|
308
|
+
out.push(...emitFlat(saved[i]))
|
|
309
|
+
}
|
|
310
|
+
ctx.func.finallyStack = saved
|
|
311
|
+
return out
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function withFinallyStack(stack, fn) {
|
|
315
|
+
const prev = ctx.func.finallyStack || []
|
|
316
|
+
ctx.func.finallyStack = stack
|
|
317
|
+
try { return fn() }
|
|
318
|
+
finally { ctx.func.finallyStack = prev }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Apply refinements for the duration of `fn()`. Restores prior state on return/throw. */
|
|
322
|
+
function withRefinements(refs, body, fn) {
|
|
323
|
+
if (!refs || refs.size === 0) return fn()
|
|
324
|
+
const cur = ctx.func.refinements
|
|
325
|
+
// Drop names that are reassigned in the body — refinement would be unsound.
|
|
326
|
+
const saved = []
|
|
327
|
+
for (const [name, val] of refs) {
|
|
328
|
+
if (isReassigned(body, name)) continue
|
|
329
|
+
saved.push([name, cur.get(name)])
|
|
330
|
+
cur.set(name, val)
|
|
331
|
+
}
|
|
332
|
+
try { return fn() }
|
|
333
|
+
finally {
|
|
334
|
+
for (const [name, prev] of saved) {
|
|
335
|
+
if (prev === undefined) cur.delete(name); else cur.set(name, prev)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Coerce an AST node to an i32 boolean, folding && / || at the boolean boundary. */
|
|
341
|
+
export function toBool(node) {
|
|
342
|
+
const op = Array.isArray(node) ? node[0] : null
|
|
343
|
+
if (CMP_SET.has(op)) return emit(node)
|
|
344
|
+
if (op === '&&') {
|
|
345
|
+
const la = toBool(node[1]), lb = toBool(node[2])
|
|
346
|
+
if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.and', la, lb], 'i32')
|
|
347
|
+
return typed(['if', ['result', 'i32'], la, ['then', lb], ['else', ['i32.const', 0]]], 'i32')
|
|
348
|
+
}
|
|
349
|
+
if (op === '||') {
|
|
350
|
+
const la = toBool(node[1]), lb = toBool(node[2])
|
|
351
|
+
if (isCmp(node[1]) && isCmp(node[2])) return typed(['i32.or', la, lb], 'i32')
|
|
352
|
+
return typed(['if', ['result', 'i32'], la, ['then', ['i32.const', 1]], ['else', lb]], 'i32')
|
|
353
|
+
}
|
|
354
|
+
return toBoolFromEmitted(emit(node))
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Coerce an emitted arg IR to match a callee param. Param may carry ptrKind (pointer-ABI
|
|
358
|
+
* i32 offset), else falls back to numeric WASM type coercion. */
|
|
359
|
+
function emitArgForParam(ir, param) {
|
|
360
|
+
if (param?.ptrKind != null) return ptrOffsetIR(ir, param.ptrKind)
|
|
361
|
+
return asParamType(ir, param?.type)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Materialize a multi-value function call as a heap array.
|
|
366
|
+
* Call → store each result in temp → copy to allocated array → return pointer.
|
|
367
|
+
*/
|
|
368
|
+
export function materializeMulti(callNode) {
|
|
369
|
+
const name = callNode[1]
|
|
370
|
+
const func = ctx.func.map.get(name)
|
|
371
|
+
const n = func.sig.results.length
|
|
372
|
+
const rawArgs = callNode.slice(2)
|
|
373
|
+
const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
|
|
374
|
+
? rawArgs[0].slice(1) : rawArgs
|
|
375
|
+
const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
376
|
+
while (emittedArgs.length < func.sig.params.length)
|
|
377
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
|
|
378
|
+
const temps = Array.from({ length: n }, () => temp())
|
|
379
|
+
const out = allocPtr({ type: 1, len: n, tag: 'marr' })
|
|
380
|
+
const ir = [out.init, ['call', `$${name}`, ...emittedArgs]]
|
|
381
|
+
for (let k = n - 1; k >= 0; k--) ir.push(['local.set', `$${temps[k]}`])
|
|
382
|
+
for (let k = 0; k < n; k++)
|
|
383
|
+
ir.push(['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.const', k * 8]], ['local.get', `$${temps[k]}`]])
|
|
384
|
+
ir.push(out.ptr)
|
|
385
|
+
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Emit let/const initializations as typed local.set instructions. */
|
|
389
|
+
export function emitDecl(...inits) {
|
|
390
|
+
const result = []
|
|
391
|
+
for (let ii = 0; ii < inits.length; ii++) {
|
|
392
|
+
const i = inits[ii]
|
|
393
|
+
if (typeof i === 'string') {
|
|
394
|
+
const undef = nullExpr()
|
|
395
|
+
if (ctx.func.boxed.has(i)) {
|
|
396
|
+
const cell = ctx.func.boxed.get(i)
|
|
397
|
+
ctx.func.locals.set(cell, 'i32')
|
|
398
|
+
result.push(
|
|
399
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
400
|
+
['f64.store', ['local.get', `$${cell}`], undef])
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
if (isGlobal(i)) {
|
|
404
|
+
if (!ctx.scope.globalTypes.has(i)) result.push(['global.set', `$${i}`, undef])
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
result.push(['local.set', `$${i}`, undef])
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(i) || i[0] !== '=') continue
|
|
411
|
+
const [, name, init] = i
|
|
412
|
+
if (typeof name !== 'string' || init == null) continue
|
|
413
|
+
|
|
414
|
+
// Multi-value ephemeral destructuring — skip heap alloc when temp is
|
|
415
|
+
// assigned from a multi-value call then immediately destructured element-by-element.
|
|
416
|
+
if (name.startsWith(T) && Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string'
|
|
417
|
+
&& ctx.func.names?.has(init[1])) {
|
|
418
|
+
const func = ctx.func.map.get(init[1])
|
|
419
|
+
const n = func?.sig.results.length
|
|
420
|
+
if (n > 1) {
|
|
421
|
+
const targets = []
|
|
422
|
+
let match = true
|
|
423
|
+
for (let k = 0; k < n && match; k++) {
|
|
424
|
+
const next = inits[ii + 1 + k]
|
|
425
|
+
if (!Array.isArray(next) || next[0] !== '=' || typeof next[1] !== 'string') { match = false; break }
|
|
426
|
+
const rhs = next[2]
|
|
427
|
+
if (!Array.isArray(rhs) || rhs[0] !== '[]' || rhs[1] !== name) { match = false; break }
|
|
428
|
+
const idx = rhs[2]
|
|
429
|
+
if (!Array.isArray(idx) || idx[0] != null || idx[1] !== k) { match = false; break }
|
|
430
|
+
if (ctx.func.boxed.has(next[1]) || isGlobal(next[1])) { match = false; break }
|
|
431
|
+
targets.push(next[1])
|
|
432
|
+
}
|
|
433
|
+
if (match && targets.length === n) {
|
|
434
|
+
const rawArgs = init.slice(2)
|
|
435
|
+
const argList = rawArgs.length === 1 && Array.isArray(rawArgs[0]) && rawArgs[0][0] === ','
|
|
436
|
+
? rawArgs[0].slice(1) : rawArgs
|
|
437
|
+
const emittedArgs = argList.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
438
|
+
while (emittedArgs.length < func.sig.params.length)
|
|
439
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
|
|
440
|
+
result.push(['call', `$${init[1]}`, ...emittedArgs])
|
|
441
|
+
for (let k = n - 1; k >= 0; k--)
|
|
442
|
+
result.push(['local.set', `$${targets[k]}`])
|
|
443
|
+
ii += n
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const isObjLit = Array.isArray(init) && init[0] === '{}'
|
|
449
|
+
if (isObjLit) ctx.schema.targetStack.push(name)
|
|
450
|
+
const val = emit(init)
|
|
451
|
+
if (isObjLit) ctx.schema.targetStack.pop()
|
|
452
|
+
// Direct-call dispatch for const-bound, non-escaping local closures: skip call_indirect.
|
|
453
|
+
// Gate: not boxed (no mutable cross-fn capture), not global, not reassigned in this body.
|
|
454
|
+
// isReassigned is conservative across nested arrow shadows — we miss the optimization
|
|
455
|
+
// rather than emit a wrong direct call.
|
|
456
|
+
if (val?.closureBodyName && !ctx.func.boxed.has(name) && !isGlobal(name)
|
|
457
|
+
&& ctx.func.body && !isReassigned(ctx.func.body, name)) {
|
|
458
|
+
if (!ctx.func.directClosures) ctx.func.directClosures = new Map()
|
|
459
|
+
ctx.func.directClosures.set(name, val.closureBodyName)
|
|
460
|
+
}
|
|
461
|
+
if (ctx.func.boxed.has(name)) {
|
|
462
|
+
const cell = ctx.func.boxed.get(name)
|
|
463
|
+
ctx.func.locals.set(cell, 'i32')
|
|
464
|
+
result.push(
|
|
465
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
466
|
+
['f64.store', ['local.get', `$${cell}`], asF64(val)])
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if (isGlobal(name)) {
|
|
470
|
+
// Unboxed pointer const globals carry the raw i32 offset; init coerces via asPtrOffset.
|
|
471
|
+
const grep = repOfGlobal(name)
|
|
472
|
+
if (grep?.ptrKind != null) {
|
|
473
|
+
result.push(['global.set', `$${name}`, asPtrOffset(val, grep.ptrKind)])
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
// Pre-folded numeric const globals have their init baked into the decl — skip.
|
|
477
|
+
if (ctx.scope.globalTypes.has(name)) continue
|
|
478
|
+
result.push(['global.set', `$${name}`, asF64(val)])
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
const localType = ctx.func.locals.get(name) || 'f64'
|
|
482
|
+
let ptrKind = repOf(name)?.ptrKind
|
|
483
|
+
// Inherit ptrKind from a pointer-ABI RHS: destructure temps (`__d0 = v`) and other
|
|
484
|
+
// fresh let-bindings whose init is already an unboxed pointer. Without this, readVar
|
|
485
|
+
// returns an untyped i32 local.get and later `asF64` emits a numeric convert instead
|
|
486
|
+
// of a ptr-rebox. Safe because emitDecl runs once per let/const binding.
|
|
487
|
+
if (ptrKind == null && val.ptrKind != null && localType === 'i32' && !ctx.func.boxed?.has(name)) {
|
|
488
|
+
updateRep(name, { ptrKind: val.ptrKind })
|
|
489
|
+
ptrKind = val.ptrKind
|
|
490
|
+
if (val.ptrAux != null) {
|
|
491
|
+
updateRep(name, { ptrAux: val.ptrAux })
|
|
492
|
+
// OBJECT-only: aux *is* the schemaId; mirror to ctx.schema.vars + rep.schemaId so
|
|
493
|
+
// .prop slot resolution sees a precise binding. TYPED/CLOSURE aux carries other
|
|
494
|
+
// semantics (elem code / funcIdx) and must not leak into schema lookups.
|
|
495
|
+
if (val.ptrKind === VAL.OBJECT && !ctx.schema.vars?.has(name)) {
|
|
496
|
+
ctx.schema.vars.set(name, val.ptrAux)
|
|
497
|
+
updateRep(name, { schemaId: val.ptrAux })
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
let coerced
|
|
502
|
+
if (ptrKind != null) {
|
|
503
|
+
// Unboxed pointer local — extract i32 offset from NaN-boxed f64 via reinterpret, not numeric trunc.
|
|
504
|
+
// CLOSURE init carries funcIdx in val.closureFuncIdx; persist it on the rep so a later
|
|
505
|
+
// asF64 (escape: store, return, indirect-call rebox) reconstructs the correct table slot.
|
|
506
|
+
if (ptrKind === VAL.CLOSURE && val.closureFuncIdx != null && repOf(name)?.ptrAux == null)
|
|
507
|
+
updateRep(name, { ptrAux: val.closureFuncIdx })
|
|
508
|
+
coerced = val.ptrKind === ptrKind ? val
|
|
509
|
+
: typed(['i32.wrap_i64', ['i64.reinterpret_f64', asF64(val)]], 'i32')
|
|
510
|
+
} else {
|
|
511
|
+
coerced = localType === 'f64' ? asF64(val) : asI32(val)
|
|
512
|
+
}
|
|
513
|
+
if (!(isLit(coerced) && coerced[1] === 0 && !ctx.func.stack.length))
|
|
514
|
+
result.push(['local.set', `$${name}`, coerced])
|
|
515
|
+
|
|
516
|
+
const schemaId = ctx.schema.idOf?.(name)
|
|
517
|
+
if (ctx.func.localProps?.has(name) && schemaId != null) {
|
|
518
|
+
const schema = ctx.schema.resolve(name)
|
|
519
|
+
if (schema?.[0] === '__inner__') {
|
|
520
|
+
inc('__alloc', '__mkptr')
|
|
521
|
+
const bt = `${T}bx${ctx.func.uniq++}`
|
|
522
|
+
ctx.func.locals.set(bt, 'i32')
|
|
523
|
+
const innerName = `${name}${T}inner`
|
|
524
|
+
ctx.func.locals.set(innerName, 'f64')
|
|
525
|
+
result.push(
|
|
526
|
+
['local.set', `$${innerName}`, ['local.get', `$${name}`]],
|
|
527
|
+
['local.set', `$${bt}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]],
|
|
528
|
+
['f64.store', ['local.get', `$${bt}`], ['local.get', `$${name}`]],
|
|
529
|
+
...schema.slice(1).map((_, j) =>
|
|
530
|
+
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (j + 1) * 8]], ['f64.const', 0]]),
|
|
531
|
+
['local.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result.length === 0 ? null : result.length === 1 ? result[0] : result
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Build an array from items, handling ['__spread', expr] markers.
|
|
540
|
+
* Split into sections (normal arrays and spreads), then copy all into result.
|
|
541
|
+
*/
|
|
542
|
+
export function buildArrayWithSpreads(items) {
|
|
543
|
+
const spreads = []
|
|
544
|
+
for (let i = 0; i < items.length; i++) {
|
|
545
|
+
if (Array.isArray(items[i]) && items[i][0] === '__spread') {
|
|
546
|
+
spreads.push({ pos: i, expr: items[i][1] })
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (spreads.length === 0) {
|
|
551
|
+
return emit(['[', ...items])
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const sections = []
|
|
555
|
+
let currentArray = []
|
|
556
|
+
|
|
557
|
+
for (let i = 0; i < items.length; i++) {
|
|
558
|
+
if (Array.isArray(items[i]) && items[i][0] === '__spread') {
|
|
559
|
+
if (currentArray.length > 0) {
|
|
560
|
+
sections.push({ type: 'array', items: currentArray })
|
|
561
|
+
currentArray = []
|
|
562
|
+
}
|
|
563
|
+
sections.push({ type: 'spread', expr: items[i][1] })
|
|
564
|
+
} else {
|
|
565
|
+
currentArray.push(items[i])
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (currentArray.length > 0) {
|
|
569
|
+
sections.push({ type: 'array', items: currentArray })
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (sections.length === 1) {
|
|
573
|
+
const sec = sections[0]
|
|
574
|
+
return emit(sec.type === 'array' ? ['[', ...sec.items] : sec.expr)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const len = tempI32('len')
|
|
578
|
+
const pos = tempI32('pos')
|
|
579
|
+
const out = allocPtr({ type: 1, len: ['local.get', `$${len}`], tag: 'arr' })
|
|
580
|
+
const result = out.local
|
|
581
|
+
|
|
582
|
+
const ir = [
|
|
583
|
+
['local.set', `$${len}`, ['i32.const', 0]],
|
|
584
|
+
]
|
|
585
|
+
|
|
586
|
+
inc('__len', '__ptr_offset')
|
|
587
|
+
for (const sec of sections) {
|
|
588
|
+
if (sec.type === 'spread') {
|
|
589
|
+
sec.local = `${T}sp${ctx.func.uniq++}`
|
|
590
|
+
ctx.func.locals.set(sec.local, 'f64')
|
|
591
|
+
sec.lenLocal = `${T}spl${ctx.func.uniq++}`
|
|
592
|
+
ctx.func.locals.set(sec.lenLocal, 'i32')
|
|
593
|
+
sec.vt = valTypeOf(sec.expr)
|
|
594
|
+
// ARRAY-known source: hoist data base and inline len/load (skip per-iter dispatch).
|
|
595
|
+
if (sec.vt === VAL.ARRAY && !multiCount(sec.expr)) {
|
|
596
|
+
sec.baseLocal = `${T}spb${ctx.func.uniq++}`
|
|
597
|
+
ctx.func.locals.set(sec.baseLocal, 'i32')
|
|
598
|
+
}
|
|
599
|
+
const n = multiCount(sec.expr)
|
|
600
|
+
ir.push(['local.set', `$${sec.local}`, n ? materializeMulti(sec.expr) : asF64(emit(sec.expr))])
|
|
601
|
+
if (sec.baseLocal) {
|
|
602
|
+
ir.push(['local.set', `$${sec.baseLocal}`, ['call', '$__ptr_offset', ['local.get', `$${sec.local}`]]])
|
|
603
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['i32.load', ['i32.sub', ['local.get', `$${sec.baseLocal}`], ['i32.const', 8]]]])
|
|
604
|
+
} else {
|
|
605
|
+
// Cache __len once per spread; reused below for total-len sum and inner copy bound.
|
|
606
|
+
ir.push(['local.set', `$${sec.lenLocal}`, ['call', '$__len', ['local.get', `$${sec.local}`]]])
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const sec of sections) {
|
|
612
|
+
if (sec.type === 'array') {
|
|
613
|
+
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', sec.items.length]]])
|
|
614
|
+
} else {
|
|
615
|
+
ir.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['local.get', `$${sec.lenLocal}`]]])
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
ir.push(out.init, ['local.set', `$${pos}`, ['i32.const', 0]])
|
|
620
|
+
|
|
621
|
+
for (const sec of sections) {
|
|
622
|
+
if (sec.type === 'array') {
|
|
623
|
+
for (let i = 0; i < sec.items.length; i++) {
|
|
624
|
+
ir.push(
|
|
625
|
+
['f64.store',
|
|
626
|
+
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
627
|
+
asF64(emit(sec.items[i]))],
|
|
628
|
+
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]]
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
const slen = sec.lenLocal, sidx = `${T}sidx${ctx.func.uniq++}`
|
|
633
|
+
ctx.func.locals.set(sidx, 'i32')
|
|
634
|
+
const loopId = ctx.func.uniq++
|
|
635
|
+
const elemLoad = sec.baseLocal
|
|
636
|
+
? ['f64.load', ['i32.add', ['local.get', `$${sec.baseLocal}`], ['i32.shl', ['local.get', `$${sidx}`], ['i32.const', 3]]]]
|
|
637
|
+
: ctx.module.modules['string']
|
|
638
|
+
? ['if', ['result', 'f64'],
|
|
639
|
+
['i32.or',
|
|
640
|
+
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.STRING]],
|
|
641
|
+
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${sec.local}`]], ['i32.const', PTR.SSO]]],
|
|
642
|
+
['then', (inc('__str_idx'), ['call', '$__str_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])],
|
|
643
|
+
['else', (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])]]
|
|
644
|
+
: (inc('__typed_idx'), ['call', '$__typed_idx', ['local.get', `$${sec.local}`], ['local.get', `$${sidx}`]])
|
|
645
|
+
ir.push(
|
|
646
|
+
['local.set', `$${sidx}`, ['i32.const', 0]],
|
|
647
|
+
['block', `$break${loopId}`, ['loop', `$loop${loopId}`,
|
|
648
|
+
['br_if', `$break${loopId}`, ['i32.ge_s', ['local.get', `$${sidx}`], ['local.get', `$${slen}`]]],
|
|
649
|
+
['f64.store',
|
|
650
|
+
['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
|
|
651
|
+
elemLoad],
|
|
652
|
+
['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
|
|
653
|
+
['local.set', `$${sidx}`, ['i32.add', ['local.get', `$${sidx}`], ['i32.const', 1]]],
|
|
654
|
+
['br', `$loop${loopId}`]]]
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
ir.push(out.ptr)
|
|
660
|
+
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Check if node is a block body (statement list, not object literal/expression) */
|
|
664
|
+
const isBlockBody = n => Array.isArray(n) && n[0] === '{}' && n.length === 2 && Array.isArray(n[1]) && STMT_OPS.has(n[1]?.[0])
|
|
665
|
+
|
|
666
|
+
/** Emit node in void context: emit + drop any value. Block bodies route through emitBody. */
|
|
667
|
+
export function emitFlat(node) {
|
|
668
|
+
if (isBlockBody(node)) return emitBody(node)
|
|
669
|
+
const ir = emit(node, 'void')
|
|
670
|
+
const items = flat(ir)
|
|
671
|
+
if (ir?.type && ir.type !== 'void') items.push('drop')
|
|
672
|
+
return items
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Emit block body as flat list of WASM instructions. Unwraps {} and delegates to emitFlat per statement.
|
|
676
|
+
* Also drives early-return refinement: `if (!guard) return/throw` narrows `guard` for the
|
|
677
|
+
* rest of the enclosing block. Refinements added here are rolled back on block exit. */
|
|
678
|
+
export function emitBody(node) {
|
|
679
|
+
const inner = node[1]
|
|
680
|
+
const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
|
|
681
|
+
const out = []
|
|
682
|
+
const accumulated = []
|
|
683
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
684
|
+
const s = stmts[i]
|
|
685
|
+
if (s == null || typeof s === 'number') continue
|
|
686
|
+
out.push(...emitFlat(s))
|
|
687
|
+
// After an `if (cond) terminator` (no else), narrow types from !cond for subsequent statements.
|
|
688
|
+
// Skip names that are reassigned later — refinement would be unsound past the assignment.
|
|
689
|
+
if (Array.isArray(s) && s[0] === 'if' && s[3] == null && isTerminator(s[2])) {
|
|
690
|
+
const refs = extractRefinements(s[1], new Map(), false)
|
|
691
|
+
for (const [name, val] of refs) {
|
|
692
|
+
let reassigned = false
|
|
693
|
+
for (let j = i + 1; j < stmts.length; j++)
|
|
694
|
+
if (isReassigned(stmts[j], name)) { reassigned = true; break }
|
|
695
|
+
if (reassigned) continue
|
|
696
|
+
accumulated.push([name, ctx.func.refinements.get(name)])
|
|
697
|
+
ctx.func.refinements.set(name, val)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Restore prior refinements on block exit.
|
|
702
|
+
for (let i = accumulated.length - 1; i >= 0; i--) {
|
|
703
|
+
const [name, prev] = accumulated[i]
|
|
704
|
+
if (prev === undefined) ctx.func.refinements.delete(name); else ctx.func.refinements.set(name, prev)
|
|
705
|
+
}
|
|
706
|
+
return out
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Comparison op factory with constant folding. */
|
|
710
|
+
const cmpOp = (i32op, f64op, fn) => (a, b) => {
|
|
711
|
+
const va = emit(a), vb = emit(b)
|
|
712
|
+
if (isLit(va) && isLit(vb)) return emitNum(fn(litVal(va), litVal(vb)) ? 1 : 0)
|
|
713
|
+
const ai = intConstValue(a), bi = intConstValue(b)
|
|
714
|
+
if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
|
|
715
|
+
if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
|
|
716
|
+
return va.type === 'i32' && vb.type === 'i32'
|
|
717
|
+
? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function intConstValue(expr) {
|
|
721
|
+
if (typeof expr === 'number' && Number.isInteger(expr)) return expr
|
|
722
|
+
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
|
|
723
|
+
if (typeof expr === 'string') {
|
|
724
|
+
const v = repOf(expr)?.intConst
|
|
725
|
+
if (v != null) return v
|
|
726
|
+
}
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Compound assignment: read → op → write back (via readVar/writeVar). */
|
|
731
|
+
function compoundAssign(name, val, f64op, i32op) {
|
|
732
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
733
|
+
const void_ = _expect === 'void'
|
|
734
|
+
const va = readVar(name), vb = emit(val)
|
|
735
|
+
if (i32op && va.type === 'i32' && vb.type === 'i32')
|
|
736
|
+
return writeVar(name, i32op(va, vb), void_)
|
|
737
|
+
return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Core emitter table. Maps AST ops to WASM IR generators.
|
|
742
|
+
* ctx.core.emit is seeded with a flat copy of this object on reset;
|
|
743
|
+
* modules add or override ops on ctx.core.emit directly.
|
|
744
|
+
* @type {Record<string, (...args: any[]) => Array>}
|
|
745
|
+
*/
|
|
746
|
+
export const emitter = {
|
|
747
|
+
// === Spread operator ===
|
|
748
|
+
// Note: spread is handled specially in call contexts; this catches stray uses
|
|
749
|
+
'...': () => err('Spread (...) can only be used in function/method calls or array literals'),
|
|
750
|
+
|
|
751
|
+
// === Statements ===
|
|
752
|
+
|
|
753
|
+
';': (...args) => {
|
|
754
|
+
const out = []
|
|
755
|
+
for (const a of args) {
|
|
756
|
+
const r = emit(a, 'void')
|
|
757
|
+
if (r == null) continue
|
|
758
|
+
out.push(...flat(r))
|
|
759
|
+
if (r?.type && r.type !== 'void') out.push('drop')
|
|
760
|
+
}
|
|
761
|
+
return out
|
|
762
|
+
},
|
|
763
|
+
'{': (...args) => args.map(emit).filter(x => x != null),
|
|
764
|
+
',': (...args) => {
|
|
765
|
+
const results = args.map(emit).filter(x => x != null)
|
|
766
|
+
if (results.length === 0) return null
|
|
767
|
+
if (results.length === 1) return results[0]
|
|
768
|
+
const last = results[results.length - 1]
|
|
769
|
+
// Flatten: multi-instruction arrays (from ';') need spreading, typed nodes need drop
|
|
770
|
+
const spread = r => Array.isArray(r) && Array.isArray(r[0]) ? r : [r]
|
|
771
|
+
const dropSpread = r => r.type ? [['drop', r]] : spread(r)
|
|
772
|
+
// If last expression is void (store, etc.), add explicit return value
|
|
773
|
+
if (!last.type) {
|
|
774
|
+
return typed(['block', ['result', 'f64'],
|
|
775
|
+
...results.flatMap(dropSpread),
|
|
776
|
+
['f64.const', 0]], 'f64')
|
|
777
|
+
}
|
|
778
|
+
return typed(['block', ['result', last.type],
|
|
779
|
+
...results.slice(0, -1).flatMap(dropSpread), last], last.type)
|
|
780
|
+
},
|
|
781
|
+
'let': emitDecl,
|
|
782
|
+
'const': emitDecl,
|
|
783
|
+
'export': () => null,
|
|
784
|
+
// 'block' can appear from jzify transforming labeled blocks or as WASM block IR
|
|
785
|
+
'block': (...args) => {
|
|
786
|
+
// WASM block IR: first arg is ['result', type] → pass through, preserve type
|
|
787
|
+
if (Array.isArray(args[0]) && args[0][0] === 'result')
|
|
788
|
+
return typed(['block', ...args], args[0][1])
|
|
789
|
+
const inner = args.length === 1 ? args[0] : [';', ...args]
|
|
790
|
+
return emitFlat(['{}', inner])
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
'throw': expr => {
|
|
794
|
+
ctx.runtime.throws = true
|
|
795
|
+
const thrown = temp()
|
|
796
|
+
return typed(['block',
|
|
797
|
+
['local.set', `$${thrown}`, asF64(emit(expr))],
|
|
798
|
+
['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${thrown}`]]],
|
|
799
|
+
['throw', '$__jz_err', ['local.get', `$${thrown}`]]], 'void')
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
'catch': (body, errName, handler) => {
|
|
803
|
+
ctx.runtime.throws = true
|
|
804
|
+
const id = ctx.func.uniq++
|
|
805
|
+
ctx.func.locals.set(errName, 'f64')
|
|
806
|
+
const prev = ctx.func.inTry; ctx.func.inTry = true
|
|
807
|
+
let bodyIR; try { bodyIR = emitFlat(body) } finally { ctx.func.inTry = prev }
|
|
808
|
+
const handlerIR = emitFlat(handler)
|
|
809
|
+
return typed(['block', `$outer${id}`, ['result', 'f64'],
|
|
810
|
+
['block', `$catch${id}`, ['result', 'f64'],
|
|
811
|
+
['try_table', ['catch', '$__jz_err', `$catch${id}`],
|
|
812
|
+
...bodyIR],
|
|
813
|
+
['f64.const', 0],
|
|
814
|
+
['br', `$outer${id}`]],
|
|
815
|
+
['local.set', `$${errName}`],
|
|
816
|
+
...handlerIR,
|
|
817
|
+
['f64.const', 0]], 'f64')
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
'finally': (body, cleanup) => {
|
|
821
|
+
ctx.runtime.throws = true
|
|
822
|
+
const id = ctx.func.uniq++
|
|
823
|
+
const errLocal = temp('err')
|
|
824
|
+
const parentStack = ctx.func.finallyStack || []
|
|
825
|
+
const activeStack = parentStack.concat([cleanup])
|
|
826
|
+
|
|
827
|
+
const prevTry = ctx.func.inTry
|
|
828
|
+
ctx.func.inTry = true
|
|
829
|
+
const bodyIR = withFinallyStack(activeStack, () => {
|
|
830
|
+
try { return emitFlat(body) }
|
|
831
|
+
finally { ctx.func.inTry = prevTry }
|
|
832
|
+
})
|
|
833
|
+
const normalCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
|
|
834
|
+
const throwCleanup = withFinallyStack(parentStack, () => emitFlat(cleanup))
|
|
835
|
+
|
|
836
|
+
return ['block', `$fin_done${id}`,
|
|
837
|
+
['block', `$fin_catch${id}`, ['result', 'f64'],
|
|
838
|
+
['try_table', ['catch', '$__jz_err', `$fin_catch${id}`],
|
|
839
|
+
...bodyIR],
|
|
840
|
+
...normalCleanup,
|
|
841
|
+
['br', `$fin_done${id}`]],
|
|
842
|
+
['local.set', `$${errLocal}`],
|
|
843
|
+
...throwCleanup,
|
|
844
|
+
['global.set', '$__jz_last_err_bits', ['i64.reinterpret_f64', ['local.get', `$${errLocal}`]]],
|
|
845
|
+
['throw', '$__jz_err', ['local.get', `$${errLocal}`]]]
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
'return': expr => {
|
|
849
|
+
const finalizers = emitFinalizers()
|
|
850
|
+
if (ctx.func.current?.results.length > 1 && Array.isArray(expr) && expr[0] === '[') {
|
|
851
|
+
const vals = expr.slice(1).map(e => asF64(emit(e)))
|
|
852
|
+
if (finalizers.length === 0) return typed(['return', ...vals], 'void')
|
|
853
|
+
const names = vals.map(() => temp('ret'))
|
|
854
|
+
return [
|
|
855
|
+
...vals.map((v, i) => ['local.set', `$${names[i]}`, v]),
|
|
856
|
+
...finalizers,
|
|
857
|
+
typed(['return', ...names.map(n => ['local.get', `$${n}`])], 'void'),
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
if (expr == null) return [...finalizers, typed(['return', NULL_IR], 'void')]
|
|
861
|
+
const rt = ctx.func.current?.results[0] || 'f64'
|
|
862
|
+
const pk = ctx.func.current?.ptrKind
|
|
863
|
+
const ir = pk != null ? asPtrOffset(emit(expr), pk) : asParamType(emit(expr), rt)
|
|
864
|
+
if (!ctx.func.inTry && !ctx.transform.noTailCall &&
|
|
865
|
+
Array.isArray(ir) && ir[0] === 'call' && typeof ir[1] === 'string')
|
|
866
|
+
return typed(['return_call', ...ir.slice(1)], 'void')
|
|
867
|
+
if (finalizers.length > 0) {
|
|
868
|
+
const ty = pk != null ? 'i32' : rt
|
|
869
|
+
const name = ty === 'i32' ? tempI32('ret') : ty === 'i64' ? tempI64('ret') : temp('ret')
|
|
870
|
+
return [
|
|
871
|
+
['local.set', `$${name}`, ir],
|
|
872
|
+
...finalizers,
|
|
873
|
+
typed(['return', ['local.get', `$${name}`]], 'void'),
|
|
874
|
+
]
|
|
875
|
+
}
|
|
876
|
+
return typed(['return', ir], 'void')
|
|
877
|
+
},
|
|
878
|
+
|
|
879
|
+
// === Assignment ===
|
|
880
|
+
|
|
881
|
+
'=': (name, val) => {
|
|
882
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
883
|
+
// Array index assignment: arr[i] = x
|
|
884
|
+
if (Array.isArray(name) && name[0] === '[]') {
|
|
885
|
+
const [, arr, idx] = name
|
|
886
|
+
const keyType = keyValType(idx)
|
|
887
|
+
const useRuntimeKeyDispatch = keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING)
|
|
888
|
+
const keyExpr = asF64(emit(idx))
|
|
889
|
+
const valueExpr = asF64(emit(val))
|
|
890
|
+
const storeArrayValue = (arrExpr, idxNode, persist) => {
|
|
891
|
+
const arrTmp = `${T}asi${ctx.func.uniq++}`
|
|
892
|
+
const idxTmp = `${T}asj${ctx.func.uniq++}`
|
|
893
|
+
const valTmp = `${T}asv${ctx.func.uniq++}`
|
|
894
|
+
ctx.func.locals.set(arrTmp, 'f64')
|
|
895
|
+
ctx.func.locals.set(idxTmp, 'i32')
|
|
896
|
+
ctx.func.locals.set(valTmp, 'f64')
|
|
897
|
+
inc('__arr_set_idx_ptr')
|
|
898
|
+
const body = [
|
|
899
|
+
['local.set', `$${arrTmp}`, arrExpr],
|
|
900
|
+
['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
|
|
901
|
+
['local.set', `$${valTmp}`, valueExpr],
|
|
902
|
+
['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${arrTmp}`], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
|
|
903
|
+
]
|
|
904
|
+
if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
|
|
905
|
+
body.push(['local.get', `$${valTmp}`])
|
|
906
|
+
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
907
|
+
}
|
|
908
|
+
const setDyn = () => {
|
|
909
|
+
inc('__dyn_set')
|
|
910
|
+
return typed(['call', '$__dyn_set', asF64(emit(arr)), keyExpr, valueExpr], 'f64')
|
|
911
|
+
}
|
|
912
|
+
const dispatchKey = (numericIR) => {
|
|
913
|
+
const keyTmp = temp()
|
|
914
|
+
return typed(['block', ['result', 'f64'],
|
|
915
|
+
['local.set', `$${keyTmp}`, keyExpr],
|
|
916
|
+
['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
|
|
917
|
+
['then', ['call', '$__dyn_set', asF64(emit(arr)), ['local.get', `$${keyTmp}`], valueExpr]],
|
|
918
|
+
['else', numericIR(['local.get', `$${keyTmp}`])]]], 'f64')
|
|
919
|
+
}
|
|
920
|
+
// Literal string key on schema-known object → direct payload slot write (skip __dyn_set)
|
|
921
|
+
const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1] : null
|
|
922
|
+
if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
|
|
923
|
+
const slot = ctx.schema.find(arr, litKey, true)
|
|
924
|
+
if (slot >= 0) {
|
|
925
|
+
const t = temp()
|
|
926
|
+
return typed(['block', ['result', 'f64'],
|
|
927
|
+
['local.set', `$${t}`, valueExpr],
|
|
928
|
+
['f64.store',
|
|
929
|
+
['i32.add', ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), ['i32.const', slot * 8]],
|
|
930
|
+
['local.get', `$${t}`]],
|
|
931
|
+
['local.get', `$${t}`]], 'f64')
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
if (keyType === VAL.STRING) return setDyn()
|
|
935
|
+
if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
|
|
936
|
+
lookupValType(arr) === 'typed') {
|
|
937
|
+
const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val)
|
|
938
|
+
if (r) return r
|
|
939
|
+
}
|
|
940
|
+
if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
|
|
941
|
+
const inner = ctx.schema.emitInner(arr)
|
|
942
|
+
const arrVT = lookupValType(arr) || VAL.OBJECT
|
|
943
|
+
const storeNumeric = keyNode => storeArrayValue(inner, keyNode, ptr =>
|
|
944
|
+
['f64.store', ptrOffsetIR(asF64(emit(arr)), arrVT), ptr])
|
|
945
|
+
if (useRuntimeKeyDispatch) {
|
|
946
|
+
inc('__dyn_set', '__is_str_key')
|
|
947
|
+
return dispatchKey(storeNumeric)
|
|
948
|
+
}
|
|
949
|
+
return typed(storeNumeric(keyExpr), 'f64')
|
|
950
|
+
}
|
|
951
|
+
const va = emit(arr), vi = asI32(emit(idx)), vv = valueExpr, t = temp()
|
|
952
|
+
if (typeof arr === 'string' && keyValType(arr) === VAL.ARRAY) {
|
|
953
|
+
const persist = ptr => {
|
|
954
|
+
if (ctx.func.boxed?.has(arr)) return ['f64.store', boxedAddr(arr), ptr]
|
|
955
|
+
if (isGlobal(arr)) return ['global.set', `$${arr}`, ptr]
|
|
956
|
+
return ['local.set', `$${arr}`, ptr]
|
|
957
|
+
}
|
|
958
|
+
if (useRuntimeKeyDispatch) {
|
|
959
|
+
inc('__dyn_set', '__is_str_key')
|
|
960
|
+
return dispatchKey(keyNode => storeArrayValue(asF64(va), keyNode, persist))
|
|
961
|
+
}
|
|
962
|
+
return storeArrayValue(asF64(va), keyExpr, persist)
|
|
963
|
+
}
|
|
964
|
+
// arr is non-ARRAY here (VAL.ARRAY branch was taken above); safe to skip forwarding.
|
|
965
|
+
const arrVT = (typeof arr === 'string' ? lookupValType(arr) : null) || VAL.OBJECT
|
|
966
|
+
if (useRuntimeKeyDispatch) {
|
|
967
|
+
inc('__dyn_set', '__is_str_key')
|
|
968
|
+
return dispatchKey(keyNode => {
|
|
969
|
+
const keyI32 = asI32(typed(keyNode, 'f64'))
|
|
970
|
+
return ['block', ['result', 'f64'],
|
|
971
|
+
['local.set', `$${t}`, vv],
|
|
972
|
+
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', keyI32, ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
973
|
+
['local.get', `$${t}`]]
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
return typed(['block', ['result', 'f64'],
|
|
977
|
+
['local.set', `$${t}`, vv],
|
|
978
|
+
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), arrVT), ['i32.shl', vi, ['i32.const', 3]]], ['local.get', `$${t}`]],
|
|
979
|
+
['local.get', `$${t}`]], 'f64')
|
|
980
|
+
}
|
|
981
|
+
// Object property assignment: obj.prop = x
|
|
982
|
+
if (Array.isArray(name) && name[0] === '.') {
|
|
983
|
+
const [, obj, prop] = name
|
|
984
|
+
// Schema-based object → f64.store at fixed offset.
|
|
985
|
+
// safe=true: skip structural subtyping when variable's type is unknown,
|
|
986
|
+
// otherwise a slot write could clobber an array/string's payload.
|
|
987
|
+
if (typeof obj === 'string' && ctx.schema.find) {
|
|
988
|
+
const idx = ctx.schema.find(obj, prop, true)
|
|
989
|
+
if (idx >= 0) {
|
|
990
|
+
const va = emit(obj), vv = asF64(emit(val)), t = temp()
|
|
991
|
+
const shadow = needsDynShadow(obj)
|
|
992
|
+
if (shadow) inc('__dyn_set')
|
|
993
|
+
const stmts = [
|
|
994
|
+
['local.set', `$${t}`, vv],
|
|
995
|
+
['f64.store', ['i32.add', ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]], ['local.get', `$${t}`]],
|
|
996
|
+
]
|
|
997
|
+
if (shadow)
|
|
998
|
+
stmts.push(['drop', ['call', '$__dyn_set', asF64(va), asF64(emit(['str', prop])), ['local.get', `$${t}`]]])
|
|
999
|
+
stmts.push(['local.get', `$${t}`])
|
|
1000
|
+
return typed(['block', ['result', 'f64'], ...stmts], 'f64')
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (typeof obj === 'string') {
|
|
1004
|
+
const objType = keyValType(obj)
|
|
1005
|
+
if (usesDynProps(objType)) {
|
|
1006
|
+
inc('__dyn_set')
|
|
1007
|
+
return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
|
|
1008
|
+
}
|
|
1009
|
+
if (objType == null) ctx.features.external = true
|
|
1010
|
+
inc('__hash_set')
|
|
1011
|
+
const setCall = typed(['call', '$__hash_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
|
|
1012
|
+
if (isGlobal(obj)) return typed(['block', ['result', 'f64'],
|
|
1013
|
+
['global.set', `$${obj}`, setCall], ['global.get', `$${obj}`]], 'f64')
|
|
1014
|
+
return typed(['local.tee', `$${obj}`, setCall], 'f64')
|
|
1015
|
+
}
|
|
1016
|
+
ctx.features.external = true
|
|
1017
|
+
inc('__dyn_set')
|
|
1018
|
+
return typed(['call', '$__dyn_set', asF64(emit(obj)), asF64(emit(['str', prop])), asF64(emit(val))], 'f64')
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof name !== 'string') err(`Assignment to non-variable: ${JSON.stringify(name)}`)
|
|
1021
|
+
const void_ = _expect === 'void'
|
|
1022
|
+
return writeVar(name, emit(val), void_)
|
|
1023
|
+
},
|
|
1024
|
+
|
|
1025
|
+
// Compound assignments: read-modify-write with type coercion
|
|
1026
|
+
'+=': (name, val) => {
|
|
1027
|
+
// String concatenation: desugar to name = name + val (+ handler knows about strings)
|
|
1028
|
+
const vt = typeof name === 'string' ? keyValType(name) : null
|
|
1029
|
+
const vtB = keyValType(val)
|
|
1030
|
+
if (vt === VAL.STRING || vtB === VAL.STRING) return emit(['=', name, ['+', name, val]])
|
|
1031
|
+
return compoundAssign(name, val, (a, b) => typed(['f64.add', a, b], 'f64'), (a, b) => typed(['i32.add', a, b], 'i32'))
|
|
1032
|
+
},
|
|
1033
|
+
...Object.fromEntries([
|
|
1034
|
+
['-=', 'sub'], ['*=', 'mul'], ['/=', 'div'],
|
|
1035
|
+
].map(([op, fn]) => [op, (name, val) => compoundAssign(name, val,
|
|
1036
|
+
(a, b) => typed([`f64.${fn}`, a, b], 'f64'),
|
|
1037
|
+
fn === 'div' ? null : (a, b) => typed([`i32.${fn}`, a, b], 'i32')
|
|
1038
|
+
)])),
|
|
1039
|
+
'%=': (name, val) => compoundAssign(name, val, f64rem, (a, b) => typed(['i32.rem_s', a, b], 'i32')),
|
|
1040
|
+
|
|
1041
|
+
// Bitwise compound assignments: i32 normally, i64 when either operand is BigInt
|
|
1042
|
+
...Object.fromEntries([
|
|
1043
|
+
['&=', 'and'], ['|=', 'or'], ['^=', 'xor'],
|
|
1044
|
+
['>>=', 'shr_s'], ['<<=', 'shl'], ['>>>=', 'shr_u'],
|
|
1045
|
+
].map(([op, fn]) => [op, (name, val) => {
|
|
1046
|
+
if (valTypeOf(name) === VAL.BIGINT || valTypeOf(val) === VAL.BIGINT) {
|
|
1047
|
+
const void_ = _expect === 'void'
|
|
1048
|
+
const result = fromI64([`i64.${fn}`, asI64(readVar(name)), asI64(emit(val))])
|
|
1049
|
+
return writeVar(name, result, void_)
|
|
1050
|
+
}
|
|
1051
|
+
return compoundAssign(name, val,
|
|
1052
|
+
(a, b) => asF64(typed([`i32.${fn}`, toI32(a), toI32(b)], 'i32')),
|
|
1053
|
+
(a, b) => typed([`i32.${fn}`, a, b], 'i32')
|
|
1054
|
+
)
|
|
1055
|
+
}])),
|
|
1056
|
+
|
|
1057
|
+
// Logical compound assignments: a ||= b → a = a || b, a &&= b → a = a && b
|
|
1058
|
+
// Logical/nullish compound assignments: read → check → conditionally write
|
|
1059
|
+
// For complex LHS (obj.prop, arr[i]): emit as check(read(lhs)) ? write(lhs, val) : read(lhs)
|
|
1060
|
+
...Object.fromEntries(['||=', '&&=', '??='].map(op => [op, (name, val) => {
|
|
1061
|
+
// Complex LHS → desugar (side-effect-safe since obj/arr/idx are locals)
|
|
1062
|
+
if (typeof name !== 'string') {
|
|
1063
|
+
const baseOp = op.slice(0, -1) // '||', '&&', '??'
|
|
1064
|
+
return emit([baseOp, name, ['=', name, val]])
|
|
1065
|
+
}
|
|
1066
|
+
if (isConst(name)) err(`Assignment to const '${name}'`)
|
|
1067
|
+
const void_ = _expect === 'void'
|
|
1068
|
+
const t = temp()
|
|
1069
|
+
const va = readVar(name)
|
|
1070
|
+
// Condition: ||= → truthy check, &&= → truthy check, ??= → nullish check
|
|
1071
|
+
const cond = op === '??='
|
|
1072
|
+
? isNullish(['local.tee', `$${t}`, asF64(va)])
|
|
1073
|
+
: ['i32.and',
|
|
1074
|
+
['f64.eq', ['local.tee', `$${t}`, asF64(va)], ['local.get', `$${t}`]],
|
|
1075
|
+
['f64.ne', ['local.get', `$${t}`], ['f64.const', 0]]]
|
|
1076
|
+
// &&= and ??= assign when cond is true (truthy / nullish); ||= assigns when cond is false
|
|
1077
|
+
const [thenExpr, elseExpr] = op === '||='
|
|
1078
|
+
? [['local.get', `$${t}`], asF64(emit(val))]
|
|
1079
|
+
: [asF64(emit(val)), ['local.get', `$${t}`]]
|
|
1080
|
+
const result = typed(['if', ['result', 'f64'], cond, ['then', thenExpr], ['else', elseExpr]], 'f64')
|
|
1081
|
+
// Write back (handles boxed/global/local)
|
|
1082
|
+
if (ctx.func.boxed?.has(name)) {
|
|
1083
|
+
const bt = temp()
|
|
1084
|
+
return typed(['block', ['result', 'f64'],
|
|
1085
|
+
['local.set', `$${bt}`, result],
|
|
1086
|
+
['f64.store', boxedAddr(name), ['local.get', `$${bt}`]],
|
|
1087
|
+
['local.get', `$${bt}`]], 'f64')
|
|
1088
|
+
}
|
|
1089
|
+
return writeVar(name, result, void_)
|
|
1090
|
+
}])),
|
|
1091
|
+
|
|
1092
|
+
// === Increment/Decrement ===
|
|
1093
|
+
// Postfix resolved in prepare: i++ → (++i) - 1
|
|
1094
|
+
|
|
1095
|
+
...Object.fromEntries([['++', 'add'], ['--', 'sub']].map(([op, fn]) => [op, name => {
|
|
1096
|
+
if (typeof name === 'string' && isConst(name)) err(`Assignment to const '${name}'`)
|
|
1097
|
+
const void_ = _expect === 'void'
|
|
1098
|
+
const v = readVar(name)
|
|
1099
|
+
const one = v.type === 'i32' ? ['i32.const', 1] : ['f64.const', 1]
|
|
1100
|
+
return writeVar(name, typed([`${v.type}.${fn}`, v, one], v.type), void_)
|
|
1101
|
+
}])),
|
|
1102
|
+
|
|
1103
|
+
// === Arithmetic (type-preserving) ===
|
|
1104
|
+
|
|
1105
|
+
// Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
|
|
1106
|
+
'+': (a, b) => {
|
|
1107
|
+
if (_expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
|
|
1108
|
+
// String concatenation: pure string operands skip generic ToString coercion.
|
|
1109
|
+
const vtA = keyValType(a)
|
|
1110
|
+
const vtB = keyValType(b)
|
|
1111
|
+
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
1112
|
+
inc('__str_concat_raw')
|
|
1113
|
+
return typed(['call', '$__str_concat_raw', asF64(emit(a)), asF64(emit(b))], 'f64')
|
|
1114
|
+
}
|
|
1115
|
+
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
1116
|
+
inc('__str_concat')
|
|
1117
|
+
return typed(['call', '$__str_concat', asF64(emit(a)), asF64(emit(b))], 'f64')
|
|
1118
|
+
}
|
|
1119
|
+
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
1120
|
+
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
1121
|
+
// Runtime string dispatch when at least one side could be a string. When one side has
|
|
1122
|
+
// a known non-STRING vtype, skip its `__is_str_key` (statically false). Common in
|
|
1123
|
+
// chained additions `s + a*b + c.d` — left grows as `+` (=NUMBER), only the new right
|
|
1124
|
+
// operand needs the runtime check.
|
|
1125
|
+
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
1126
|
+
const tA = temp('add'), tB = temp('add')
|
|
1127
|
+
inc('__str_concat', '__is_str_key')
|
|
1128
|
+
const checkA = vtA == null ? ['call', '$__is_str_key', ['local.tee', `$${tA}`, asF64(emit(a))]] : null
|
|
1129
|
+
const checkB = vtB == null ? ['call', '$__is_str_key', ['local.tee', `$${tB}`, asF64(emit(b))]] : null
|
|
1130
|
+
const concat = ['call', '$__str_concat', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
1131
|
+
const add = ['f64.add', ['local.get', `$${tA}`], ['local.get', `$${tB}`]]
|
|
1132
|
+
if (checkA && checkB) {
|
|
1133
|
+
return typed(['if', ['result', 'f64'], ['i32.or', checkA, checkB], ['then', concat], ['else', add]], 'f64')
|
|
1134
|
+
}
|
|
1135
|
+
// Exactly one side is checked. Pre-eval the known side first, then the if branches on the unknown.
|
|
1136
|
+
const preEval = vtA == null ? ['local.set', `$${tB}`, asF64(emit(b))] : ['local.set', `$${tA}`, asF64(emit(a))]
|
|
1137
|
+
return typed(['block', ['result', 'f64'],
|
|
1138
|
+
preEval,
|
|
1139
|
+
['if', ['result', 'f64'], checkA ?? checkB, ['then', concat], ['else', add]]
|
|
1140
|
+
], 'f64')
|
|
1141
|
+
}
|
|
1142
|
+
const va = emit(a), vb = emit(b)
|
|
1143
|
+
if (isLit(va) && isLit(vb)) return emitNum(litVal(va) + litVal(vb))
|
|
1144
|
+
if (isLit(vb) && litVal(vb) === 0) return va
|
|
1145
|
+
if (isLit(va) && litVal(va) === 0) return vb
|
|
1146
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.add', va, vb], 'i32')
|
|
1147
|
+
return typed(['f64.add', asF64(va), asF64(vb)], 'f64')
|
|
1148
|
+
},
|
|
1149
|
+
'-': (a, b) => {
|
|
1150
|
+
if (_expect === 'void' && isPostfix(a, '++', b)) return emit(a, 'void')
|
|
1151
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1152
|
+
return b === undefined
|
|
1153
|
+
? fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
1154
|
+
: fromI64(['i64.sub', asI64(emit(a)), asI64(emit(b))])
|
|
1155
|
+
if (b === undefined) { const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64') }
|
|
1156
|
+
const va = emit(a), vb = emit(b)
|
|
1157
|
+
if (isLit(va) && isLit(vb)) return emitNum(litVal(va) - litVal(vb))
|
|
1158
|
+
if (isLit(vb) && litVal(vb) === 0) return toNumF64(a, va)
|
|
1159
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.sub', va, vb], 'i32')
|
|
1160
|
+
return typed(['f64.sub', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1161
|
+
},
|
|
1162
|
+
'u+': a => {
|
|
1163
|
+
if (valTypeOf(a) === VAL.BIGINT)
|
|
1164
|
+
return typed(['f64.convert_i64_s', asI64(emit(a))], 'f64')
|
|
1165
|
+
inc('__to_num')
|
|
1166
|
+
return typed(['call', '$__to_num', asF64(emit(a))], 'f64')
|
|
1167
|
+
},
|
|
1168
|
+
'u-': a => {
|
|
1169
|
+
if (valTypeOf(a) === VAL.BIGINT) return fromI64(['i64.sub', ['i64.const', 0], asI64(emit(a))])
|
|
1170
|
+
const v = emit(a); return isLit(v) ? emitNum(-litVal(v)) : v.type === 'i32' ? typed(['i32.sub', typed(['i32.const', 0], 'i32'), v], 'i32') : typed(['f64.neg', toNumF64(a, v)], 'f64')
|
|
1171
|
+
},
|
|
1172
|
+
'*': (a, b) => {
|
|
1173
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1174
|
+
return fromI64(['i64.mul', asI64(emit(a)), asI64(emit(b))])
|
|
1175
|
+
const va = emit(a), vb = emit(b)
|
|
1176
|
+
if (isLit(va) && isLit(vb)) return emitNum(litVal(va) * litVal(vb))
|
|
1177
|
+
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1178
|
+
if (isLit(va) && litVal(va) === 1) return toNumF64(b, vb)
|
|
1179
|
+
if (isLit(vb) && litVal(vb) === 0) return isLit(va) ? vb : typed(['block', ['result', vb.type], va, 'drop', vb], vb.type)
|
|
1180
|
+
if (isLit(va) && litVal(va) === 0) return isLit(vb) ? va : typed(['block', ['result', va.type], vb, 'drop', va], va.type)
|
|
1181
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.mul', va, vb], 'i32')
|
|
1182
|
+
return typed(['f64.mul', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1183
|
+
},
|
|
1184
|
+
'/': (a, b) => {
|
|
1185
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1186
|
+
return fromI64(['i64.div_s', asI64(emit(a)), asI64(emit(b))])
|
|
1187
|
+
const va = emit(a), vb = emit(b)
|
|
1188
|
+
if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) / litVal(vb))
|
|
1189
|
+
if (isLit(vb) && litVal(vb) === 1) return toNumF64(a, va)
|
|
1190
|
+
return typed(['f64.div', toNumF64(a, va), toNumF64(b, vb)], 'f64')
|
|
1191
|
+
},
|
|
1192
|
+
'%': (a, b) => {
|
|
1193
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1194
|
+
return fromI64(['i64.rem_s', asI64(emit(a)), asI64(emit(b))])
|
|
1195
|
+
const va = emit(a), vb = emit(b)
|
|
1196
|
+
if (isLit(va) && isLit(vb) && litVal(vb) !== 0) return emitNum(litVal(va) % litVal(vb))
|
|
1197
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.rem_s', va, vb], 'i32')
|
|
1198
|
+
return f64rem(toNumF64(a, va), toNumF64(b, vb))
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1201
|
+
// === Comparisons (always i32 result) ===
|
|
1202
|
+
|
|
1203
|
+
'==': (a, b) => {
|
|
1204
|
+
// JS loose nullish equality: x == null / x == undefined.
|
|
1205
|
+
// If the non-literal side has a known non-null VAL type, fold to 0.
|
|
1206
|
+
if (isNullishLit(a)) {
|
|
1207
|
+
if (valTypeOf(b)) return emitNum(0)
|
|
1208
|
+
return isNullish(asF64(emit(b)))
|
|
1209
|
+
}
|
|
1210
|
+
if (isNullishLit(b)) {
|
|
1211
|
+
if (valTypeOf(a)) return emitNum(0)
|
|
1212
|
+
return isNullish(asF64(emit(a)))
|
|
1213
|
+
}
|
|
1214
|
+
// typeof x == 'string' → compile-time type check (prepare rewrites string to type code)
|
|
1215
|
+
const tc = emitTypeofCmp(a, b, 'eq'); if (tc) return tc
|
|
1216
|
+
const va = emit(a), vb = emit(b)
|
|
1217
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.eq', va, vb], 'i32')
|
|
1218
|
+
// Both sides known-pure NUMBER → f64.eq (skip __eq's pointer-identity/string path).
|
|
1219
|
+
// valTypeOf handles literals/arithmetic exprs; lookupValType covers typed locals/params.
|
|
1220
|
+
const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
|
|
1221
|
+
const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
|
|
1222
|
+
if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.eq', asF64(va), asF64(vb)], 'i32')
|
|
1223
|
+
// Reference-equal pointer kinds (same kind, non-STRING, non-BIGINT): i64 bit equality.
|
|
1224
|
+
// JS `==` on objects/arrays/sets/maps/etc. is pure reference equality — no content path.
|
|
1225
|
+
// STRING needs __eq (heap strings can be equal by content but different pointers).
|
|
1226
|
+
// BIGINT needs __eq (heap-allocated, content compare).
|
|
1227
|
+
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1228
|
+
return typed(['i64.eq', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1229
|
+
}
|
|
1230
|
+
inc('__eq')
|
|
1231
|
+
return typed(['call', '$__eq', asF64(va), asF64(vb)], 'i32')
|
|
1232
|
+
},
|
|
1233
|
+
'!=': (a, b) => {
|
|
1234
|
+
if (isNullishLit(a)) {
|
|
1235
|
+
if (valTypeOf(b)) return emitNum(1)
|
|
1236
|
+
return typed(['i32.eqz', isNullish(asF64(emit(b)))], 'i32')
|
|
1237
|
+
}
|
|
1238
|
+
if (isNullishLit(b)) {
|
|
1239
|
+
if (valTypeOf(a)) return emitNum(1)
|
|
1240
|
+
return typed(['i32.eqz', isNullish(asF64(emit(a)))], 'i32')
|
|
1241
|
+
}
|
|
1242
|
+
const tc = emitTypeofCmp(a, b, 'ne'); if (tc) return tc
|
|
1243
|
+
const va = emit(a), vb = emit(b)
|
|
1244
|
+
if (va.type === 'i32' && vb.type === 'i32') return typed(['i32.ne', va, vb], 'i32')
|
|
1245
|
+
const vta = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
|
|
1246
|
+
const vtb = valTypeOf(b) ?? (typeof b === 'string' ? lookupValType(b) : null)
|
|
1247
|
+
if (vta === VAL.NUMBER && vtb === VAL.NUMBER) return typed(['f64.ne', asF64(va), asF64(vb)], 'i32')
|
|
1248
|
+
if (vta && vta === vtb && REF_EQ_KINDS.has(vta)) {
|
|
1249
|
+
return typed(['i64.ne', ['i64.reinterpret_f64', asF64(va)], ['i64.reinterpret_f64', asF64(vb)]], 'i32')
|
|
1250
|
+
}
|
|
1251
|
+
inc('__eq')
|
|
1252
|
+
return typed(['i32.eqz', ['call', '$__eq', asF64(va), asF64(vb)]], 'i32')
|
|
1253
|
+
},
|
|
1254
|
+
'<': cmpOp('lt_s', 'lt', (a, b) => a < b),
|
|
1255
|
+
'>': cmpOp('gt_s', 'gt', (a, b) => a > b),
|
|
1256
|
+
'<=': cmpOp('le_s', 'le', (a, b) => a <= b),
|
|
1257
|
+
'>=': cmpOp('ge_s', 'ge', (a, b) => a >= b),
|
|
1258
|
+
|
|
1259
|
+
// === Logical ===
|
|
1260
|
+
|
|
1261
|
+
'!': a => {
|
|
1262
|
+
const v = emit(a)
|
|
1263
|
+
if (v.type === 'i32') return typed(['i32.eqz', v], 'i32')
|
|
1264
|
+
// Unboxed pointer offsets: falsy iff zero offset.
|
|
1265
|
+
if (v.ptrKind != null) return typed(['i32.eqz', v], 'i32')
|
|
1266
|
+
// Known pointer-kinded operand: `!x` is just `x is nullish` (null/undefined).
|
|
1267
|
+
// Pointers are never 0 / NaN / false / empty-string in the boxed form.
|
|
1268
|
+
const vt = valTypeOf(a) ?? (typeof a === 'string' ? lookupValType(a) : null)
|
|
1269
|
+
if (vt && vt !== VAL.NUMBER && vt !== VAL.BIGINT) {
|
|
1270
|
+
return isNullish(asF64(v))
|
|
1271
|
+
}
|
|
1272
|
+
inc('__is_truthy')
|
|
1273
|
+
return typed(['i32.eqz', ['call', '$__is_truthy', asF64(v)]], 'i32')
|
|
1274
|
+
},
|
|
1275
|
+
|
|
1276
|
+
'?:': (a, b, c) => {
|
|
1277
|
+
// Constant condition → emit only the live branch
|
|
1278
|
+
const ca = emit(a)
|
|
1279
|
+
if (isLit(ca)) { const v = litVal(ca); return (v !== 0 && v === v) ? emit(b) : emit(c) }
|
|
1280
|
+
const cond = toBoolFromEmitted(ca)
|
|
1281
|
+
// Flow-sensitive refinement: each arm sees narrowing consistent with `a` being truthy / falsy.
|
|
1282
|
+
const thenRefs = extractRefinements(a, new Map(), true)
|
|
1283
|
+
const elseRefs = extractRefinements(a, new Map(), false)
|
|
1284
|
+
const vb = withRefinements(thenRefs, b, () => emit(b))
|
|
1285
|
+
const vc = withRefinements(elseRefs, c, () => emit(c))
|
|
1286
|
+
// L: Use WASM select for pure ternaries — branchless, smaller bytecode
|
|
1287
|
+
if (vb.type === 'i32' && vc.type === 'i32') {
|
|
1288
|
+
// Propagate matching ptrKind/ptrAux so a downstream asF64 takes the NaN-rebox
|
|
1289
|
+
// path instead of `f64.convert_i32_s`. Mismatched kinds drop both — caller's
|
|
1290
|
+
// asF64 will treat the i32 as numeric, which is correct for non-pointer i32s.
|
|
1291
|
+
// ptrKind matches but ptrAux differs (e.g. polymorphic OBJECT with two
|
|
1292
|
+
// distinct schemaIds, or TYPED with two element types) — fall through to
|
|
1293
|
+
// the f64 path. There each arm reboxes independently, preserving its own
|
|
1294
|
+
// aux in the NaN-box. The single-i32 path can only carry one aux on the
|
|
1295
|
+
// result, so `boxPtrIR` would default to 0 and lose the runtime schema /
|
|
1296
|
+
// elemType bits needed by downstream lookups (e.g. __dyn_get's OBJECT-
|
|
1297
|
+
// schema fallback uses receiver aux to resolve `.prop`).
|
|
1298
|
+
const auxMismatch = vb.ptrKind != null && vb.ptrKind === vc.ptrKind
|
|
1299
|
+
&& (vb.ptrAux ?? null) !== (vc.ptrAux ?? null)
|
|
1300
|
+
if (!auxMismatch) {
|
|
1301
|
+
const tagPtr = (n) => {
|
|
1302
|
+
if (vb.ptrKind != null && vb.ptrKind === vc.ptrKind) {
|
|
1303
|
+
n.ptrKind = vb.ptrKind
|
|
1304
|
+
if (vb.ptrAux != null && vb.ptrAux === vc.ptrAux) n.ptrAux = vb.ptrAux
|
|
1305
|
+
}
|
|
1306
|
+
return n
|
|
1307
|
+
}
|
|
1308
|
+
if (isPureIR(vb) && isPureIR(vc))
|
|
1309
|
+
return tagPtr(typed(['select', vb, vc, cond], 'i32'))
|
|
1310
|
+
return tagPtr(typed(['if', ['result', 'i32'], cond, ['then', vb], ['else', vc]], 'i32'))
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const fb = asF64(vb), fc = asF64(vc)
|
|
1314
|
+
if (isPureIR(fb) && isPureIR(fc))
|
|
1315
|
+
return typed(['select', fb, fc, cond], 'f64')
|
|
1316
|
+
return typed(['if', ['result', 'f64'], cond, ['then', fb], ['else', fc]], 'f64')
|
|
1317
|
+
},
|
|
1318
|
+
|
|
1319
|
+
'&&': (a, b) => {
|
|
1320
|
+
const va = emit(a)
|
|
1321
|
+
if (isLit(va)) { const v = litVal(va); return (v !== 0 && v === v) ? emit(b) : va }
|
|
1322
|
+
// i32 fast path: use i32 tee as cond directly (nonzero=truthy in wasm `if`),
|
|
1323
|
+
// skip f64 round-trip and __is_truthy call entirely.
|
|
1324
|
+
if (va.type === 'i32') {
|
|
1325
|
+
const vb = emit(b)
|
|
1326
|
+
const t = tempI32()
|
|
1327
|
+
if (vb.type === 'i32') {
|
|
1328
|
+
return typed(['if', ['result', 'i32'],
|
|
1329
|
+
['local.tee', `$${t}`, va],
|
|
1330
|
+
['then', vb],
|
|
1331
|
+
['else', ['local.get', `$${t}`]]], 'i32')
|
|
1332
|
+
}
|
|
1333
|
+
return typed(['if', ['result', 'f64'],
|
|
1334
|
+
['local.tee', `$${t}`, va],
|
|
1335
|
+
['then', asF64(vb)],
|
|
1336
|
+
['else', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')]], 'f64')
|
|
1337
|
+
}
|
|
1338
|
+
const t = temp()
|
|
1339
|
+
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1340
|
+
return typed(['if', ['result', 'f64'],
|
|
1341
|
+
toBoolFromEmitted(teed),
|
|
1342
|
+
['then', asF64(emit(b))],
|
|
1343
|
+
['else', ['local.get', `$${t}`]]], 'f64')
|
|
1344
|
+
},
|
|
1345
|
+
|
|
1346
|
+
'||': (a, b) => {
|
|
1347
|
+
const va = emit(a)
|
|
1348
|
+
if (isLit(va)) { const v = litVal(va); return (v !== 0 && v === v) ? va : emit(b) }
|
|
1349
|
+
if (va.type === 'i32') {
|
|
1350
|
+
const vb = emit(b)
|
|
1351
|
+
const t = tempI32()
|
|
1352
|
+
if (vb.type === 'i32') {
|
|
1353
|
+
return typed(['if', ['result', 'i32'],
|
|
1354
|
+
['local.tee', `$${t}`, va],
|
|
1355
|
+
['then', ['local.get', `$${t}`]],
|
|
1356
|
+
['else', vb]], 'i32')
|
|
1357
|
+
}
|
|
1358
|
+
return typed(['if', ['result', 'f64'],
|
|
1359
|
+
['local.tee', `$${t}`, va],
|
|
1360
|
+
['then', typed(['f64.convert_i32_s', ['local.get', `$${t}`]], 'f64')],
|
|
1361
|
+
['else', asF64(vb)]], 'f64')
|
|
1362
|
+
}
|
|
1363
|
+
const t = temp()
|
|
1364
|
+
const teed = typed(['local.tee', `$${t}`, asF64(va)], 'f64')
|
|
1365
|
+
return typed(['if', ['result', 'f64'],
|
|
1366
|
+
toBoolFromEmitted(teed),
|
|
1367
|
+
['then', ['local.get', `$${t}`]],
|
|
1368
|
+
['else', asF64(emit(b))]], 'f64')
|
|
1369
|
+
},
|
|
1370
|
+
|
|
1371
|
+
// a ?? b: returns b only if a is nullish
|
|
1372
|
+
'??': (a, b) => {
|
|
1373
|
+
const va = emit(a)
|
|
1374
|
+
const t = temp()
|
|
1375
|
+
return typed(['if', ['result', 'f64'],
|
|
1376
|
+
// Check: is a NOT nullish?
|
|
1377
|
+
['i32.eqz', isNullish(['local.tee', `$${t}`, asF64(va)])],
|
|
1378
|
+
['then', ['local.get', `$${t}`]],
|
|
1379
|
+
['else', asF64(emit(b))]], 'f64')
|
|
1380
|
+
},
|
|
1381
|
+
|
|
1382
|
+
'void': a => {
|
|
1383
|
+
const v = emit(a)
|
|
1384
|
+
const dropAndUndef = (instr) => typed(['block', ['result', 'f64'], instr, 'drop', undefExpr()], 'f64')
|
|
1385
|
+
if (v == null) return undefExpr()
|
|
1386
|
+
const op = Array.isArray(v) ? v[0] : null
|
|
1387
|
+
const wasmVoid = op === 'local.set' || (typeof op === 'string' && op.endsWith('.store'))
|
|
1388
|
+
|| op === 'memory.copy' || op === 'global.set'
|
|
1389
|
+
if (wasmVoid)
|
|
1390
|
+
return typed(['block', ['result', 'f64'], v, undefExpr()], 'f64')
|
|
1391
|
+
if (v.type && v.type !== 'void')
|
|
1392
|
+
return dropAndUndef(v)
|
|
1393
|
+
return typed(['block', ['result', 'f64'], ...flat(v), undefExpr()], 'f64')
|
|
1394
|
+
},
|
|
1395
|
+
|
|
1396
|
+
'(': a => emit(a),
|
|
1397
|
+
|
|
1398
|
+
// === Bitwise (i32 for numbers, i64 for BigInt) ===
|
|
1399
|
+
|
|
1400
|
+
'~': a => { const v = emit(a); return isLit(v) ? emitNum(~litVal(v)) : typed(['i32.xor', toI32(v), typed(['i32.const', -1], 'i32')], 'i32') },
|
|
1401
|
+
...Object.fromEntries([
|
|
1402
|
+
['&', 'and'], ['|', 'or'], ['^', 'xor'], ['<<', 'shl'], ['>>', 'shr_s'],
|
|
1403
|
+
].map(([op, fn]) => [op, (a, b) => {
|
|
1404
|
+
if (valTypeOf(a) === VAL.BIGINT || valTypeOf(b) === VAL.BIGINT)
|
|
1405
|
+
return fromI64([`i64.${fn}`, asI64(emit(a)), asI64(emit(b))])
|
|
1406
|
+
const va = emit(a), vb = emit(b)
|
|
1407
|
+
if (isLit(va) && isLit(vb)) {
|
|
1408
|
+
const la = litVal(va), lb = litVal(vb)
|
|
1409
|
+
if (op === '&') return emitNum(la & lb); if (op === '|') return emitNum(la | lb)
|
|
1410
|
+
if (op === '^') return emitNum(la ^ lb); if (op === '<<') return emitNum(la << lb)
|
|
1411
|
+
if (op === '>>') return emitNum(la >> lb)
|
|
1412
|
+
}
|
|
1413
|
+
return typed([`i32.${fn}`, toI32(va), toI32(vb)], 'i32')
|
|
1414
|
+
}])),
|
|
1415
|
+
'>>>': (a, b) => {
|
|
1416
|
+
const va = emit(a), vb = emit(b)
|
|
1417
|
+
if (isLit(va) && isLit(vb)) return emitNum(litVal(va) >>> litVal(vb))
|
|
1418
|
+
// F: Mark unsigned so `asF64` lifts via `f64.convert_i32_u` (preserving the
|
|
1419
|
+
// [0, 2^32) value range). Without this, `(s >>> 0) / 4294967296` would convert
|
|
1420
|
+
// signed for negative-high-bit s values, flipping sign and breaking the
|
|
1421
|
+
// canonical "uint32 → f64" idiom used in PRNGs and bit-manipulation code.
|
|
1422
|
+
const node = typed(['i32.shr_u', toI32(va), toI32(vb)], 'i32')
|
|
1423
|
+
node.unsigned = true
|
|
1424
|
+
return node
|
|
1425
|
+
},
|
|
1426
|
+
|
|
1427
|
+
// === Control flow ===
|
|
1428
|
+
|
|
1429
|
+
'if': (cond, then, els) => {
|
|
1430
|
+
// Dead branch elimination: constant condition → emit only the live branch
|
|
1431
|
+
const ce = emit(cond)
|
|
1432
|
+
if (isLit(ce)) {
|
|
1433
|
+
const v = litVal(ce), truthy = v !== 0 && v === v
|
|
1434
|
+
if (truthy) return emitFlat(then)
|
|
1435
|
+
if (els != null) return emitFlat(els)
|
|
1436
|
+
return null
|
|
1437
|
+
}
|
|
1438
|
+
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
1439
|
+
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
1440
|
+
const thenRefs = extractRefinements(cond, new Map(), true)
|
|
1441
|
+
const elseRefs = extractRefinements(cond, new Map(), false)
|
|
1442
|
+
const thenBody = withRefinements(thenRefs, then, () => emitFlat(then))
|
|
1443
|
+
if (els != null) {
|
|
1444
|
+
const elseBody = withRefinements(elseRefs, els, () => emitFlat(els))
|
|
1445
|
+
return ['if', c, ['then', ...thenBody], ['else', ...elseBody]]
|
|
1446
|
+
}
|
|
1447
|
+
return ['if', c, ['then', ...thenBody]]
|
|
1448
|
+
},
|
|
1449
|
+
|
|
1450
|
+
'for': (init, cond, step, body) => {
|
|
1451
|
+
if (body === undefined) return err('for-in/for-of not supported')
|
|
1452
|
+
if (!ctx.transform.optimize || ctx.transform.optimize.smallConstForUnroll !== false) {
|
|
1453
|
+
const unrolled = unrollSmallConstFor(init, cond, step, body)
|
|
1454
|
+
if (unrolled) return unrolled
|
|
1455
|
+
}
|
|
1456
|
+
const id = ctx.func.uniq++
|
|
1457
|
+
const brk = `$brk${id}`, loop = `$loop${id}`
|
|
1458
|
+
// The cont wrapper is only needed if the body has a `continue` AND there is a step
|
|
1459
|
+
// expression — `continue` must jump to before the step. Without a step, `continue`
|
|
1460
|
+
// can target the loop label directly, saving a redundant `block`.
|
|
1461
|
+
const needsCont = step && hasOwnContinue(body)
|
|
1462
|
+
const cont = needsCont ? `$cont${id}` : loop
|
|
1463
|
+
ctx.func.stack.push({ brk, loop: cont })
|
|
1464
|
+
const result = []
|
|
1465
|
+
if (init != null) result.push(...emitFlat(init))
|
|
1466
|
+
const loopBody = []
|
|
1467
|
+
if (cond) loopBody.push(['br_if', brk, ['i32.eqz', toBool(cond)]])
|
|
1468
|
+
if (needsCont) loopBody.push(['block', cont, ...emitFlat(body)])
|
|
1469
|
+
else loopBody.push(...emitFlat(body))
|
|
1470
|
+
if (step) loopBody.push(...emitFlat(step))
|
|
1471
|
+
loopBody.push(['br', loop])
|
|
1472
|
+
result.push(['block', brk, ['loop', loop, ...loopBody]])
|
|
1473
|
+
ctx.func.stack.pop()
|
|
1474
|
+
return result.length === 1 ? result[0] : result
|
|
1475
|
+
},
|
|
1476
|
+
|
|
1477
|
+
'switch': (discriminant, ...cases) => {
|
|
1478
|
+
const disc = `${T}disc${ctx.func.uniq++}`
|
|
1479
|
+
ctx.func.locals.set(disc, 'f64')
|
|
1480
|
+
|
|
1481
|
+
const result = [['local.set', `$${disc}`, asF64(emit(discriminant))]]
|
|
1482
|
+
|
|
1483
|
+
for (const c of cases) {
|
|
1484
|
+
if (c[0] === 'case') {
|
|
1485
|
+
const [, test, body] = c
|
|
1486
|
+
const skip = `$skip${ctx.func.uniq++}`
|
|
1487
|
+
// Block: skip if discriminant != test, otherwise execute body
|
|
1488
|
+
result.push(['block', skip,
|
|
1489
|
+
['br_if', skip, typed(['f64.ne', typed(['local.get', `$${disc}`], 'f64'), asF64(emit(test))], 'i32')],
|
|
1490
|
+
...emitFlat(body)])
|
|
1491
|
+
} else if (c[0] === 'default') {
|
|
1492
|
+
result.push(...emitFlat(c[1]))
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
return result
|
|
1497
|
+
},
|
|
1498
|
+
|
|
1499
|
+
'while': (cond, body) => emitter['for'](null, cond, null, body),
|
|
1500
|
+
'break': () => [...emitFinalizers(), ['br', loopTop().brk]],
|
|
1501
|
+
'continue': () => [...emitFinalizers(), ['br', loopTop().loop]],
|
|
1502
|
+
|
|
1503
|
+
// === Call ===
|
|
1504
|
+
|
|
1505
|
+
// Arrow as value → closure
|
|
1506
|
+
'=>': (rawParams, body) => {
|
|
1507
|
+
if (!ctx.closure.make) err('Closures require fn module (auto-included)')
|
|
1508
|
+
|
|
1509
|
+
const raw = extractParams(rawParams)
|
|
1510
|
+
const params = [], defaults = {}
|
|
1511
|
+
let restParam = null, bodyPrefix = []
|
|
1512
|
+
for (const r of raw) {
|
|
1513
|
+
const c = classifyParam(r)
|
|
1514
|
+
if (c.kind === 'rest') { restParam = c.name; params.push(c.name) }
|
|
1515
|
+
else if (c.kind === 'plain') params.push(c.name)
|
|
1516
|
+
else if (c.kind === 'default') { params.push(c.name); defaults[c.name] = c.defValue }
|
|
1517
|
+
else {
|
|
1518
|
+
const tmp = `${T}p${ctx.func.uniq++}`
|
|
1519
|
+
params.push(tmp)
|
|
1520
|
+
if (c.kind === 'destruct-default') defaults[tmp] = c.defValue
|
|
1521
|
+
bodyPrefix.push(['let', ['=', c.pattern, tmp]])
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Prepend destructuring to body (if any destructured params)
|
|
1526
|
+
if (bodyPrefix.length) {
|
|
1527
|
+
if (Array.isArray(body) && body[0] === '{}' && Array.isArray(body[1]) && body[1][0] === ';')
|
|
1528
|
+
body = ['{}', [';', ...bodyPrefix, ...body[1].slice(1)]]
|
|
1529
|
+
else if (Array.isArray(body) && body[0] === '{}')
|
|
1530
|
+
body = ['{}', [';', ...bodyPrefix, body[1]]]
|
|
1531
|
+
else body = ['{}', [';', ...bodyPrefix, ['return', body]]]
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Find free variables in body that aren't params → captures
|
|
1535
|
+
const paramSet = new Set(params)
|
|
1536
|
+
const captures = []
|
|
1537
|
+
findFreeVars(body, paramSet, captures)
|
|
1538
|
+
for (const def of Object.values(defaults)) findFreeVars(def, paramSet, captures)
|
|
1539
|
+
|
|
1540
|
+
// Pass closure info including rest param and defaults
|
|
1541
|
+
const closureInfo = { params, body, captures, restParam }
|
|
1542
|
+
if (Object.keys(defaults).length) closureInfo.defaults = defaults
|
|
1543
|
+
return ctx.closure.make(closureInfo)
|
|
1544
|
+
},
|
|
1545
|
+
|
|
1546
|
+
'()': (callee, callArgs) => {
|
|
1547
|
+
let argList = Array.isArray(callArgs)
|
|
1548
|
+
? (callArgs[0] === ',' ? callArgs.slice(1) : [callArgs])
|
|
1549
|
+
: callArgs ? [callArgs] : []
|
|
1550
|
+
|
|
1551
|
+
// Helper: expand spread arguments into flat list of normal arguments + spread markers
|
|
1552
|
+
// Returns { normal: [...], spreads: [(pos, expr), ...] }
|
|
1553
|
+
const parseArgs = (args) => {
|
|
1554
|
+
const normal = []
|
|
1555
|
+
const spreads = []
|
|
1556
|
+
for (let i = 0; i < args.length; i++) {
|
|
1557
|
+
const arg = args[i]
|
|
1558
|
+
if (Array.isArray(arg) && arg[0] === '...') {
|
|
1559
|
+
spreads.push({ pos: normal.length, expr: arg[1] })
|
|
1560
|
+
} else {
|
|
1561
|
+
normal.push(arg)
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return { normal, spreads, hasSpread: spreads.length > 0 }
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const parsed = parseArgs(argList)
|
|
1568
|
+
|
|
1569
|
+
// Method call: obj.method(args) → type-aware dispatch
|
|
1570
|
+
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1571
|
+
const [, obj, method] = callee
|
|
1572
|
+
|
|
1573
|
+
// Function property call: fn.prop(args) → direct call to fn$prop
|
|
1574
|
+
if (typeof obj === 'string' && ctx.func.names.has(obj)) {
|
|
1575
|
+
const fname = `${obj}$${method}`
|
|
1576
|
+
if (ctx.func.names.has(fname)) {
|
|
1577
|
+
const func = ctx.func.map.get(fname)
|
|
1578
|
+
const emittedArgs = parsed.normal.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1579
|
+
while (emittedArgs.length < func.sig.params.length)
|
|
1580
|
+
emittedArgs.push(func.sig.params[emittedArgs.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
|
|
1581
|
+
const callIR = typed(['call', `$${fname}`, ...emittedArgs], func.sig.results[0])
|
|
1582
|
+
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1583
|
+
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
1584
|
+
return callIR
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const vt = keyValType(obj)
|
|
1589
|
+
|
|
1590
|
+
// Helper to call method with arguments (handles spread expansion)
|
|
1591
|
+
const callMethod = (objArg, methodEmitter) => {
|
|
1592
|
+
if (!parsed.hasSpread) {
|
|
1593
|
+
return methodEmitter(objArg, ...parsed.normal)
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Bulk push fast path: `obj.push(...src)` — single spread, no normal args, named obj.
|
|
1597
|
+
// The generic single-spread loop below calls methodEmitter per iteration, which expands
|
|
1598
|
+
// to a full .push (grow check + ptr_offset + store + set_len) every step. Amortising the
|
|
1599
|
+
// grow + set_len across the whole spread eliminates ~3 stdlib calls per byte in watr's
|
|
1600
|
+
// hot `out.push(...HANDLER[op](...))` path (~24M bytes/iter on raycast).
|
|
1601
|
+
if (method === 'push' && parsed.normal.length === 0 &&
|
|
1602
|
+
parsed.spreads.length === 1 && typeof objArg === 'string') {
|
|
1603
|
+
const spreadExpr = parsed.spreads[0].expr
|
|
1604
|
+
inc('__len'); inc('__arr_grow'); inc('__set_len'); inc('__ptr_offset')
|
|
1605
|
+
const o = `${T}po${ctx.func.uniq++}`,
|
|
1606
|
+
sa = `${T}psa${ctx.func.uniq++}`,
|
|
1607
|
+
sl = `${T}psl${ctx.func.uniq++}`,
|
|
1608
|
+
ol = `${T}pol${ctx.func.uniq++}`,
|
|
1609
|
+
si = `${T}psi${ctx.func.uniq++}`,
|
|
1610
|
+
base = `${T}pb${ctx.func.uniq++}`
|
|
1611
|
+
ctx.func.locals.set(o, 'f64'); ctx.func.locals.set(sa, 'f64')
|
|
1612
|
+
ctx.func.locals.set(sl, 'i32'); ctx.func.locals.set(ol, 'i32')
|
|
1613
|
+
ctx.func.locals.set(si, 'i32'); ctx.func.locals.set(base, 'i32')
|
|
1614
|
+
|
|
1615
|
+
const objIsArr = lookupValType(objArg) === VAL.ARRAY
|
|
1616
|
+
// Spread source: if statically known ARRAY, inline len/load via hoisted srcBase
|
|
1617
|
+
// (skip per-iteration __arr_idx call + dispatch).
|
|
1618
|
+
const srcVT = valTypeOf(spreadExpr)
|
|
1619
|
+
const srcIsArr = !multiCount(spreadExpr) && srcVT === VAL.ARRAY
|
|
1620
|
+
const srcBase = srcIsArr ? `${T}psb${ctx.func.uniq++}` : null
|
|
1621
|
+
if (srcIsArr) ctx.func.locals.set(srcBase, 'i32')
|
|
1622
|
+
const n = multiCount(spreadExpr)
|
|
1623
|
+
const ir = []
|
|
1624
|
+
ir.push(['local.set', `$${o}`, asF64(emit(objArg))])
|
|
1625
|
+
ir.push(['local.set', `$${sa}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1626
|
+
if (srcIsArr) {
|
|
1627
|
+
ir.push(['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${sa}`]]])
|
|
1628
|
+
ir.push(['local.set', `$${sl}`, ['i32.load', ['i32.sub', ['local.get', `$${srcBase}`], ['i32.const', 8]]]])
|
|
1629
|
+
} else {
|
|
1630
|
+
ir.push(['local.set', `$${sl}`, ['call', '$__len', ['local.get', `$${sa}`]]])
|
|
1631
|
+
}
|
|
1632
|
+
// Old length: inline as `i32.load (off-8)` if obj is known ARRAY (matches .push handler).
|
|
1633
|
+
if (objIsArr) {
|
|
1634
|
+
ir.push(['local.set', `$${ol}`,
|
|
1635
|
+
['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${o}`]], ['i32.const', 8]]]])
|
|
1636
|
+
} else {
|
|
1637
|
+
ir.push(['local.set', `$${ol}`, ['call', '$__len', ['local.get', `$${o}`]]])
|
|
1638
|
+
}
|
|
1639
|
+
// Single grow for the full spread (vs per-element grow check in the generic loop).
|
|
1640
|
+
ir.push(['local.set', `$${o}`, ['call', '$__arr_grow', ['local.get', `$${o}`],
|
|
1641
|
+
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]]])
|
|
1642
|
+
// base captured AFTER grow (grow may relocate the array).
|
|
1643
|
+
ir.push(['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${o}`]]])
|
|
1644
|
+
// Tight store loop.
|
|
1645
|
+
ir.push(['local.set', `$${si}`, ['i32.const', 0]])
|
|
1646
|
+
const loopId = ctx.func.uniq++
|
|
1647
|
+
const srcLoad = srcIsArr
|
|
1648
|
+
? ['f64.load', ['i32.add', ['local.get', `$${srcBase}`], ['i32.shl', ['local.get', `$${si}`], ['i32.const', 3]]]]
|
|
1649
|
+
: asF64(emit(['[]', sa, si]))
|
|
1650
|
+
ir.push(['block', `$break${loopId}`, ['loop', `$continue${loopId}`,
|
|
1651
|
+
['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${si}`], ['local.get', `$${sl}`]]],
|
|
1652
|
+
['f64.store',
|
|
1653
|
+
['i32.add', ['local.get', `$${base}`],
|
|
1654
|
+
['i32.shl', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${si}`]], ['i32.const', 3]]],
|
|
1655
|
+
srcLoad],
|
|
1656
|
+
['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
|
|
1657
|
+
['br', `$continue${loopId}`]]])
|
|
1658
|
+
// Single set_len for the full spread.
|
|
1659
|
+
ir.push(['call', '$__set_len', ['local.get', `$${o}`],
|
|
1660
|
+
['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1661
|
+
// Update source variable: grow may have moved the pointer.
|
|
1662
|
+
if (ctx.func.boxed?.has(objArg)) {
|
|
1663
|
+
ir.push(['f64.store', ['local.get', `$${ctx.func.boxed.get(objArg)}`], ['local.get', `$${o}`]])
|
|
1664
|
+
} else if (ctx.scope.globals.has(objArg) && !ctx.func.locals?.has(objArg)) {
|
|
1665
|
+
ir.push(['global.set', `$${objArg}`, ['local.get', `$${o}`]])
|
|
1666
|
+
} else {
|
|
1667
|
+
ir.push(['local.set', `$${objArg}`, ['local.get', `$${o}`]])
|
|
1668
|
+
}
|
|
1669
|
+
ir.push(['f64.convert_i32_s', ['i32.add', ['local.get', `$${ol}`], ['local.get', `$${sl}`]]])
|
|
1670
|
+
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Single spread at end: call method with normal args, then loop spread elements
|
|
1674
|
+
if (parsed.spreads.length === 1 && parsed.spreads[0].pos === parsed.normal.length) {
|
|
1675
|
+
const spreadExpr = parsed.spreads[0].expr
|
|
1676
|
+
const acc = `${T}acc${ctx.func.uniq++}`, arr = `${T}sp${ctx.func.uniq++}`, len = `${T}splen${ctx.func.uniq++}`, idx = `${T}spidx${ctx.func.uniq++}`
|
|
1677
|
+
ctx.func.locals.set(acc, 'f64'); ctx.func.locals.set(arr, 'f64')
|
|
1678
|
+
ctx.func.locals.set(len, 'i32'); ctx.func.locals.set(idx, 'i32')
|
|
1679
|
+
const spreadVT = valTypeOf(spreadExpr)
|
|
1680
|
+
if (spreadVT) updateRep(arr, { val: spreadVT })
|
|
1681
|
+
|
|
1682
|
+
// In-place spread methods modify target; accumulating methods (concat) return new values
|
|
1683
|
+
const inPlace = SPREAD_MUTATORS.has(method)
|
|
1684
|
+
// unshift prepends each arg to the front — iterating forward reverses the
|
|
1685
|
+
// intended order, so walk the spread from end to start.
|
|
1686
|
+
const reverseIter = method === 'unshift'
|
|
1687
|
+
const ir = []
|
|
1688
|
+
ir.push(['local.set', `$${acc}`, asF64(emit(objArg))])
|
|
1689
|
+
if (parsed.normal.length > 0) {
|
|
1690
|
+
const r = asF64(methodEmitter(objArg, ...parsed.normal))
|
|
1691
|
+
ir.push(inPlace ? ['drop', r] : ['local.set', `$${acc}`, r])
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
inc('__len')
|
|
1695
|
+
const n = multiCount(spreadExpr)
|
|
1696
|
+
ir.push(['local.set', `$${arr}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))])
|
|
1697
|
+
ir.push(['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${arr}`]]])
|
|
1698
|
+
ir.push(['local.set', `$${idx}`,
|
|
1699
|
+
reverseIter ? ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]] : ['i32.const', 0]])
|
|
1700
|
+
const loopId = ctx.func.uniq++
|
|
1701
|
+
const loopBody = asF64(methodEmitter(inPlace ? objArg : acc, ['[]', arr, idx]))
|
|
1702
|
+
ir.push(['block', `$break${loopId}`,
|
|
1703
|
+
['loop', `$continue${loopId}`,
|
|
1704
|
+
['br_if', `$break${loopId}`,
|
|
1705
|
+
reverseIter
|
|
1706
|
+
? ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]
|
|
1707
|
+
: ['i32.ge_u', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
|
|
1708
|
+
inPlace ? ['drop', loopBody] : ['local.set', `$${acc}`, loopBody],
|
|
1709
|
+
['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['i32.const', reverseIter ? -1 : 1]]],
|
|
1710
|
+
['br', `$continue${loopId}`]]])
|
|
1711
|
+
|
|
1712
|
+
ir.push(inPlace ? asF64(emit(objArg)) : ['local.get', `$${acc}`])
|
|
1713
|
+
return typed(['block', ['result', 'f64'], ...ir], 'f64')
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// General spread case: iterate args in original order, batch contiguous normal
|
|
1717
|
+
// args into a single call, emit a per-element loop for each spread.
|
|
1718
|
+
//
|
|
1719
|
+
// inPlace methods (push/unshift/add/set): call methodEmitter(objArg, ...) each
|
|
1720
|
+
// time so the source variable's local gets updated (else heap grow/realloc
|
|
1721
|
+
// wouldn't be visible to subsequent uses of the variable). Final value is objArg.
|
|
1722
|
+
//
|
|
1723
|
+
// non-inPlace (concat, etc.): chain via temp acc since return value is the new
|
|
1724
|
+
// collection.
|
|
1725
|
+
const inPlaceG = SPREAD_MUTATORS.has(method)
|
|
1726
|
+
const combinedG = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1727
|
+
inc('__len')
|
|
1728
|
+
|
|
1729
|
+
if (inPlaceG) {
|
|
1730
|
+
const irG = []
|
|
1731
|
+
let batch = []
|
|
1732
|
+
const flushBatch = () => {
|
|
1733
|
+
if (!batch.length) return
|
|
1734
|
+
irG.push(['drop', asF64(methodEmitter(objArg, ...batch))])
|
|
1735
|
+
batch = []
|
|
1736
|
+
}
|
|
1737
|
+
for (const item of combinedG) {
|
|
1738
|
+
if (Array.isArray(item) && item[0] === '__spread') {
|
|
1739
|
+
flushBatch()
|
|
1740
|
+
const spreadExpr = item[1]
|
|
1741
|
+
const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
|
|
1742
|
+
ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
|
|
1743
|
+
const spreadVT = valTypeOf(spreadExpr)
|
|
1744
|
+
if (spreadVT) updateRep(arrL, { val: spreadVT })
|
|
1745
|
+
const n = multiCount(spreadExpr)
|
|
1746
|
+
irG.push(
|
|
1747
|
+
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1748
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
1749
|
+
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1750
|
+
const loopId = ctx.func.uniq++
|
|
1751
|
+
const loopBody = asF64(methodEmitter(objArg, ['[]', arrL, idxL]))
|
|
1752
|
+
irG.push(['block', `$break${loopId}`,
|
|
1753
|
+
['loop', `$continue${loopId}`,
|
|
1754
|
+
['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
|
|
1755
|
+
['drop', loopBody],
|
|
1756
|
+
['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
|
|
1757
|
+
['br', `$continue${loopId}`]]])
|
|
1758
|
+
} else {
|
|
1759
|
+
batch.push(item)
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
flushBatch()
|
|
1763
|
+
irG.push(asF64(emit(objArg)))
|
|
1764
|
+
return typed(['block', ['result', 'f64'], ...irG], 'f64')
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
const accG = `${T}acc${ctx.func.uniq++}`
|
|
1768
|
+
ctx.func.locals.set(accG, 'f64')
|
|
1769
|
+
const irG = [['local.set', `$${accG}`, asF64(emit(objArg))]]
|
|
1770
|
+
let batch = []
|
|
1771
|
+
const flushBatch = () => {
|
|
1772
|
+
if (!batch.length) return
|
|
1773
|
+
irG.push(['local.set', `$${accG}`, asF64(methodEmitter(accG, ...batch))])
|
|
1774
|
+
batch = []
|
|
1775
|
+
}
|
|
1776
|
+
for (const item of combinedG) {
|
|
1777
|
+
if (Array.isArray(item) && item[0] === '__spread') {
|
|
1778
|
+
flushBatch()
|
|
1779
|
+
const spreadExpr = item[1]
|
|
1780
|
+
const arrL = `${T}sp${ctx.func.uniq++}`, lenL = `${T}splen${ctx.func.uniq++}`, idxL = `${T}spidx${ctx.func.uniq++}`
|
|
1781
|
+
ctx.func.locals.set(arrL, 'f64'); ctx.func.locals.set(lenL, 'i32'); ctx.func.locals.set(idxL, 'i32')
|
|
1782
|
+
const spreadVT = valTypeOf(spreadExpr)
|
|
1783
|
+
if (spreadVT) updateRep(arrL, { val: spreadVT })
|
|
1784
|
+
const n = multiCount(spreadExpr)
|
|
1785
|
+
irG.push(
|
|
1786
|
+
['local.set', `$${arrL}`, n ? materializeMulti(spreadExpr) : asF64(emit(spreadExpr))],
|
|
1787
|
+
['local.set', `$${lenL}`, ['call', '$__len', ['local.get', `$${arrL}`]]],
|
|
1788
|
+
['local.set', `$${idxL}`, ['i32.const', 0]])
|
|
1789
|
+
const loopId = ctx.func.uniq++
|
|
1790
|
+
const loopBody = asF64(methodEmitter(accG, ['[]', arrL, idxL]))
|
|
1791
|
+
irG.push(['block', `$break${loopId}`,
|
|
1792
|
+
['loop', `$continue${loopId}`,
|
|
1793
|
+
['br_if', `$break${loopId}`, ['i32.ge_u', ['local.get', `$${idxL}`], ['local.get', `$${lenL}`]]],
|
|
1794
|
+
['local.set', `$${accG}`, loopBody],
|
|
1795
|
+
['local.set', `$${idxL}`, ['i32.add', ['local.get', `$${idxL}`], ['i32.const', 1]]],
|
|
1796
|
+
['br', `$continue${loopId}`]]])
|
|
1797
|
+
} else {
|
|
1798
|
+
batch.push(item)
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
flushBatch()
|
|
1802
|
+
irG.push(['local.get', `$${accG}`])
|
|
1803
|
+
return typed(['block', ['result', 'f64'], ...irG], 'f64')
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Boxed object: delegate method to inner value (slot 0)
|
|
1807
|
+
if (typeof obj === 'string' && ctx.schema.isBoxed?.(obj)) {
|
|
1808
|
+
const innerVt = repOf(obj)?.val
|
|
1809
|
+
const emitter = ctx.core.emit[`.${innerVt}:${method}`] || ctx.core.emit[`.${method}`]
|
|
1810
|
+
if (emitter) {
|
|
1811
|
+
const innerName = `${obj}${T}inner`
|
|
1812
|
+
if (!ctx.func.locals.has(innerName)) ctx.func.locals.set(innerName, 'f64')
|
|
1813
|
+
const boxBase = tempI32('bb')
|
|
1814
|
+
// Load current inner value from boxed object's slot 0 (may have been updated by prior mutations)
|
|
1815
|
+
// Boxed handle is OBJECT-kind, never ARRAY — skip forwarding.
|
|
1816
|
+
const loadInner = [
|
|
1817
|
+
['local.set', `$${boxBase}`, ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT)],
|
|
1818
|
+
['local.set', `$${innerName}`, ['f64.load', ['local.get', `$${boxBase}`]]]]
|
|
1819
|
+
const result = callMethod(innerName, emitter)
|
|
1820
|
+
// Mutating methods may reallocate; writeback inner value to boxed slot
|
|
1821
|
+
if (BOXED_MUTATORS.has(method)) {
|
|
1822
|
+
const wb = ['f64.store', ['local.get', `$${boxBase}`], ['local.get', `$${innerName}`]]
|
|
1823
|
+
return typed(['block', ['result', 'f64'], ...loadInner, asF64(result), wb], 'f64')
|
|
1824
|
+
}
|
|
1825
|
+
// Non-mutating: just load inner and call
|
|
1826
|
+
return typed(['block', ['result', 'f64'], ...loadInner, asF64(result)], 'f64')
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Known type → static dispatch
|
|
1831
|
+
if (vt && ctx.core.emit[`.${vt}:${method}`]) {
|
|
1832
|
+
return callMethod(obj, ctx.core.emit[`.${vt}:${method}`])
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Unknown / guessed-array type, both string + generic exist → runtime dispatch by ptr type.
|
|
1836
|
+
// analyze.js defaults untyped `.slice()` results to VAL.ARRAY, which is a guess, not a proof;
|
|
1837
|
+
// runtime dispatch resolves whether the operand is actually a string or an array.
|
|
1838
|
+
// Concretely-typed non-string values (BUFFER, TYPED, MAP, …) fall through to the generic
|
|
1839
|
+
// emitter which already knows how to handle them.
|
|
1840
|
+
const strKey = `.string:${method}`, genKey = `.${method}`
|
|
1841
|
+
if ((!vt || vt === VAL.ARRAY) && ctx.core.emit[strKey] && ctx.core.emit[genKey]) {
|
|
1842
|
+
const t = `${T}rt${ctx.func.uniq++}`, tt = `${T}rtt${ctx.func.uniq++}`
|
|
1843
|
+
ctx.func.locals.set(t, 'f64'); ctx.func.locals.set(tt, 'i32')
|
|
1844
|
+
const strEmitter = ctx.core.emit[strKey]
|
|
1845
|
+
const genEmitter = ctx.core.emit[genKey]
|
|
1846
|
+
return typed(['block', ['result', 'f64'],
|
|
1847
|
+
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1848
|
+
['local.set', `$${tt}`, ['call', '$__ptr_type', ['local.get', `$${t}`]]],
|
|
1849
|
+
['if', ['result', 'f64'],
|
|
1850
|
+
['i32.or',
|
|
1851
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.STRING]],
|
|
1852
|
+
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.SSO]]],
|
|
1853
|
+
['then', callMethod(t, strEmitter)],
|
|
1854
|
+
['else', callMethod(t, genEmitter)]]], 'f64')
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Schema property function call: x.prop(args) where prop is a closure in boxed schema
|
|
1858
|
+
if (typeof obj === 'string' && ctx.schema.find && ctx.closure.call && ctx.schema.isBoxed?.(obj)) {
|
|
1859
|
+
const idx = ctx.schema.find(obj, method)
|
|
1860
|
+
if (idx >= 0) {
|
|
1861
|
+
const propRead = typed(['f64.load', ['i32.add', ptrOffsetIR(asF64(emit(obj)), lookupValType(obj) || VAL.OBJECT), ['i32.const', idx * 8]]], 'f64')
|
|
1862
|
+
return ctx.closure.call(propRead, parsed.normal)
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// Generic only
|
|
1867
|
+
if (ctx.core.emit[genKey]) {
|
|
1868
|
+
return callMethod(obj, ctx.core.emit[genKey])
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Dynamic property function call on non-external values.
|
|
1872
|
+
if (ctx.closure.call) {
|
|
1873
|
+
if (ctx.transform.strict)
|
|
1874
|
+
err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type pulls dynamic dispatch stdlib. Annotate the receiver type or pass { strict: false }.`)
|
|
1875
|
+
const objTmp = `${T}mobj${ctx.func.uniq++}`
|
|
1876
|
+
ctx.func.locals.set(objTmp, 'f64')
|
|
1877
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1878
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
1879
|
+
const propRead = typed(['call', '$__dyn_get_expr', ['local.get', `$${objTmp}`], asF64(emit(['str', method]))], 'f64')
|
|
1880
|
+
if (usesDynProps(vt)) {
|
|
1881
|
+
inc('__dyn_get_expr')
|
|
1882
|
+
return typed(['block', ['result', 'f64'],
|
|
1883
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1884
|
+
ctx.closure.call(propRead, [arrayIR], true)], 'f64')
|
|
1885
|
+
}
|
|
1886
|
+
inc('__dyn_get_expr', '__ext_call')
|
|
1887
|
+
ctx.features.external = true
|
|
1888
|
+
return typed(['block', ['result', 'f64'],
|
|
1889
|
+
['local.set', `$${objTmp}`, asF64(emit(obj))],
|
|
1890
|
+
['if', ['result', 'f64'],
|
|
1891
|
+
['i32.eq', ['call', '$__ptr_type', ['local.get', `$${objTmp}`]], ['i32.const', PTR.EXTERNAL]],
|
|
1892
|
+
['then', ['call', '$__ext_call', ['local.get', `$${objTmp}`], asF64(emit(['str', method])), arrayIR]],
|
|
1893
|
+
['else', ctx.closure.call(propRead, [arrayIR], true)]]], 'f64')
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Unknown callee - assume external method
|
|
1897
|
+
if (ctx.transform.strict)
|
|
1898
|
+
err(`strict mode: method call \`${typeof obj === 'string' ? obj : '<expr>'}.${method}(...)\` on a value of unknown type falls through to host \`__ext_call\`. Annotate the receiver type or pass { strict: false }.`)
|
|
1899
|
+
inc('__ext_call')
|
|
1900
|
+
ctx.features.external = true
|
|
1901
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1902
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
1903
|
+
return typed(['call', '$__ext_call', asF64(emit(obj)), asF64(emit(['str', method])), arrayIR], 'f64');
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
if (ctx.core.emit[callee]) {
|
|
1907
|
+
// Pass spread args through to emitter (e.g. Math.max(...arr))
|
|
1908
|
+
if (parsed.hasSpread) {
|
|
1909
|
+
const allArgs = []
|
|
1910
|
+
let ni = 0
|
|
1911
|
+
for (const s of parsed.spreads) {
|
|
1912
|
+
while (ni < s.pos) allArgs.push(parsed.normal[ni++])
|
|
1913
|
+
allArgs.push(['...', s.expr])
|
|
1914
|
+
}
|
|
1915
|
+
while (ni < parsed.normal.length) allArgs.push(parsed.normal[ni++])
|
|
1916
|
+
return ctx.core.emit[callee](...allArgs)
|
|
1917
|
+
}
|
|
1918
|
+
return ctx.core.emit[callee](...parsed.normal)
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Direct call if callee is a known top-level function
|
|
1922
|
+
if (typeof callee === 'string' && ctx.func.names.has(callee)) {
|
|
1923
|
+
const func = ctx.func.map.get(callee)
|
|
1924
|
+
|
|
1925
|
+
// Rest param case: collect all args (including expanded spreads) into array
|
|
1926
|
+
if (func?.rest) {
|
|
1927
|
+
const fixedParamCount = func.sig.params.length - 1
|
|
1928
|
+
const fixedArgs = parsed.normal.slice(0, fixedParamCount)
|
|
1929
|
+
// Pad missing fixed args with sentinel for defaults
|
|
1930
|
+
const emittedFixed = fixedArgs.map((a, k) => emitArgForParam(emit(a), func.sig.params[k]))
|
|
1931
|
+
while (emittedFixed.length < fixedParamCount)
|
|
1932
|
+
emittedFixed.push(func.sig.params[emittedFixed.length].type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
|
|
1933
|
+
|
|
1934
|
+
// Reconstruct with spreads, then take rest args
|
|
1935
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1936
|
+
const restArgsFinal = combined.slice(fixedParamCount)
|
|
1937
|
+
|
|
1938
|
+
// Build array: emit code for normal args + code to expand spreads
|
|
1939
|
+
const arrayIR = buildArrayWithSpreads(restArgsFinal)
|
|
1940
|
+
const callIR = typed(['call', `$${callee}`,
|
|
1941
|
+
...emittedFixed,
|
|
1942
|
+
arrayIR], func.sig.results[0])
|
|
1943
|
+
if (func.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1944
|
+
if (func.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
1945
|
+
return callIR
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// Regular function call without rest params
|
|
1949
|
+
if (parsed.hasSpread) err(`Spread not supported in calls to non-variadic function ${callee}`)
|
|
1950
|
+
// Pad missing args with canonical NaN (triggers default param init)
|
|
1951
|
+
const args = parsed.normal.map((a, k) => emitArgForParam(emit(a), func?.sig.params[k]))
|
|
1952
|
+
const expected = func?.sig.params.length || args.length
|
|
1953
|
+
while (args.length < expected) args.push(func?.sig.params[args.length]?.type === 'i32' ? typed(['i32.const', 0], 'i32') : nullExpr())
|
|
1954
|
+
// Multi-value return: materialize as heap array (caller expects single pointer)
|
|
1955
|
+
if (func?.sig.results.length > 1) return materializeMulti(['()', callee, ...parsed.normal])
|
|
1956
|
+
const callIR = typed(['call', `$${callee}`, ...args], func?.sig.results[0] || 'f64')
|
|
1957
|
+
if (func?.sig.ptrKind != null) callIR.ptrKind = func.sig.ptrKind
|
|
1958
|
+
if (func?.sig.ptrAux != null) callIR.ptrAux = func.sig.ptrAux
|
|
1959
|
+
return callIR
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// A3: const-bound, non-escaping closure → direct call to body (skip call_indirect).
|
|
1963
|
+
// emitDecl registered name → bodyName when it saw the closure.make IR. Body signature
|
|
1964
|
+
// is uniform $ftN: (env f64, argc i32, a0..a{W-1} f64) → f64. We pass the closure
|
|
1965
|
+
// NaN-box itself as env (body extracts captures via __ptr_offset(__env)).
|
|
1966
|
+
if (typeof callee === 'string' && !parsed.hasSpread
|
|
1967
|
+
&& ctx.func.directClosures?.has(callee)) {
|
|
1968
|
+
const bodyName = ctx.func.directClosures.get(callee)
|
|
1969
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
1970
|
+
const n = parsed.normal.length
|
|
1971
|
+
if (n <= W) {
|
|
1972
|
+
const slots = parsed.normal.map(a => asF64(emit(a)))
|
|
1973
|
+
while (slots.length < W) slots.push(undefExpr())
|
|
1974
|
+
return typed(['call', `$${bodyName}`,
|
|
1975
|
+
asF64(emit(callee)),
|
|
1976
|
+
typed(['i32.const', n], 'i32'),
|
|
1977
|
+
...slots], 'f64')
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// Closure call: callee is a variable holding a NaN-boxed closure pointer
|
|
1982
|
+
// Uniform convention: fn.call packs all args into an array
|
|
1983
|
+
if (ctx.closure.call) {
|
|
1984
|
+
if (parsed.hasSpread) {
|
|
1985
|
+
// Spread: build the args array directly (handles __spread markers)
|
|
1986
|
+
const combined = reconstructArgsWithSpreads(parsed.normal, parsed.spreads)
|
|
1987
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
1988
|
+
// Pass pre-built array as single already-emitted arg
|
|
1989
|
+
return ctx.closure.call(emit(callee), [arrayIR], true)
|
|
1990
|
+
}
|
|
1991
|
+
return ctx.closure.call(emit(callee), parsed.normal)
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Unknown callee — assume direct call
|
|
1995
|
+
return typed(['call', `$${callee}`, ...argList.map(a => asF64(emit(a)))], 'f64')
|
|
1996
|
+
},
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// === Emit dispatch ===
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Emit single AST node to typed WASM IR.
|
|
2003
|
+
* Every returned node has .type = 'i32' | 'f64'.
|
|
2004
|
+
* @param {import('./prepare.js').ASTNode} node
|
|
2005
|
+
* @returns {Array} typed WASM S-expression
|
|
2006
|
+
*/
|
|
2007
|
+
export function emit(node, expect) {
|
|
2008
|
+
_expect = expect || null
|
|
2009
|
+
if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
|
|
2010
|
+
if (node == null) return null
|
|
2011
|
+
if (node === true) return typed(['i32.const', 1], 'i32')
|
|
2012
|
+
if (node === false) return typed(['i32.const', 0], 'i32')
|
|
2013
|
+
if (typeof node === 'symbol') // JZ_NULL sentinel → null NaN
|
|
2014
|
+
return nullExpr()
|
|
2015
|
+
if (typeof node === 'bigint') {
|
|
2016
|
+
// Wrap to unsigned i64 range — emit as positive hex so downstream BigInt() parsers
|
|
2017
|
+
// (e.g. watr's optimize.js getConst) don't choke on "-0x..." strings.
|
|
2018
|
+
const n = node & 0xFFFFFFFFFFFFFFFFn
|
|
2019
|
+
return typed(['f64.reinterpret_i64', ['i64.const', '0x' + n.toString(16)]], 'f64')
|
|
2020
|
+
}
|
|
2021
|
+
if (typeof node === 'number') {
|
|
2022
|
+
if (Number.isInteger(node) && node >= -2147483648 && node <= 2147483647)
|
|
2023
|
+
return typed(['i32.const', node], 'i32')
|
|
2024
|
+
return typed(['f64.const', node], 'f64')
|
|
2025
|
+
}
|
|
2026
|
+
if (typeof node === 'string') {
|
|
2027
|
+
// Variable read: boxed / local / param / global (check before emitter table to avoid name collisions)
|
|
2028
|
+
if (ctx.func.boxed?.has(node) || ctx.func.locals?.has(node) || ctx.func.current?.params?.some(p => p.name === node) || isGlobal(node))
|
|
2029
|
+
return readVar(node)
|
|
2030
|
+
// Top-level function used as value → wrap as closure pointer for call_indirect
|
|
2031
|
+
if (ctx.func.names.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && ctx.closure.table) {
|
|
2032
|
+
// Trampoline signature: uniform closure ABI (env f64, argc i32, a0..a{MAX-1} f64) → f64.
|
|
2033
|
+
// Forwards the first N inline slots to $func where N = func's fixed param count.
|
|
2034
|
+
const func = ctx.func.map.get(node)
|
|
2035
|
+
const sigParams = func?.sig.params || []
|
|
2036
|
+
if (sigParams.length > MAX_CLOSURE_ARITY) err(`Function ${node} used as closure value has ${sigParams.length} params, exceeds MAX_CLOSURE_ARITY=${MAX_CLOSURE_ARITY}`)
|
|
2037
|
+
const trampolineName = `${T}tramp_${node}`
|
|
2038
|
+
if (!ctx.core.stdlib[trampolineName]) {
|
|
2039
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
2040
|
+
const paramDecls = ['(param $__env f64)', '(param $__argc i32)']
|
|
2041
|
+
for (let i = 0; i < W; i++) paramDecls.push(`(param $__a${i} f64)`)
|
|
2042
|
+
// Forward fixed slots; if func expects i32, convert via trunc_sat
|
|
2043
|
+
const fwd = sigParams.map((p, i) =>
|
|
2044
|
+
p.type === 'i32'
|
|
2045
|
+
? `(i32.trunc_sat_f64_s (local.get $__a${i}))`
|
|
2046
|
+
: `(local.get $__a${i})`).join(' ')
|
|
2047
|
+
if ((func?.sig.results.length || 1) > 1) {
|
|
2048
|
+
const n = func.sig.results.length
|
|
2049
|
+
const arr = `${T}retarr`
|
|
2050
|
+
const temps = Array.from({ length: n }, (_, i) => `${T}ret${i}`)
|
|
2051
|
+
const tempLocals = temps.map(name => `(local $${name} f64)`).join(' ')
|
|
2052
|
+
const stores = temps.map((name, i) =>
|
|
2053
|
+
`(f64.store (i32.add (local.get $${arr}) (i32.const ${i * 8})) (local.get $${name}))`
|
|
2054
|
+
).join(' ')
|
|
2055
|
+
const capture = temps.slice().reverse().map(name => `(local.set $${name})`).join(' ')
|
|
2056
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (local $${arr} i32) ${tempLocals} (call $${node} ${fwd}) ${capture} (local.set $${arr} (call $__alloc (i32.const ${n * 8 + 8}))) (i32.store (local.get $${arr}) (i32.const ${n})) (i32.store (i32.add (local.get $${arr}) (i32.const 4)) (i32.const ${n})) (local.set $${arr} (i32.add (local.get $${arr}) (i32.const 8))) ${stores} (call $__mkptr (i32.const 1) (i32.const 0) (local.get $${arr})))`
|
|
2057
|
+
inc(trampolineName, '__alloc', '__mkptr')
|
|
2058
|
+
} else {
|
|
2059
|
+
ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) (call $${node} ${fwd}))`
|
|
2060
|
+
inc(trampolineName)
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
let idx = ctx.closure.table.indexOf(trampolineName)
|
|
2064
|
+
if (idx < 0) { idx = ctx.closure.table.length; ctx.closure.table.push(trampolineName) }
|
|
2065
|
+
return mkPtrIR(PTR.CLOSURE, idx, 0)
|
|
2066
|
+
}
|
|
2067
|
+
// Emitter table: only namespace-resolved names (contain '.', e.g. 'math.PI') — safe from user variable collision
|
|
2068
|
+
if (node.includes('.') && ctx.core.emit[node]) return ctx.core.emit[node]()
|
|
2069
|
+
// Auto-import known host globals (WebAssembly, globalThis, etc.)
|
|
2070
|
+
const HOST_GLOBALS = new Set(['WebAssembly', 'globalThis', 'self', 'window', 'global', 'process'])
|
|
2071
|
+
if (HOST_GLOBALS.has(node) && !ctx.func.locals?.has(node) && !ctx.func.current?.params?.some(p => p.name === node) && !isGlobal(node)) {
|
|
2072
|
+
ctx.features.external = true
|
|
2073
|
+
ctx.scope.globals.set(node, null)
|
|
2074
|
+
ctx.module.imports.push(['import', '"env"', `"${node}"`, ['global', `$${node}`, ['mut', 'f64']]])
|
|
2075
|
+
return typed(['global.get', `$${node}`], 'f64')
|
|
2076
|
+
}
|
|
2077
|
+
const t = ctx.func.locals?.get(node) || ctx.func.current?.params.find(p => p.name === node)?.type || 'f64'
|
|
2078
|
+
return typed(['local.get', `$${node}`], t)
|
|
2079
|
+
}
|
|
2080
|
+
if (!Array.isArray(node)) return typed(['f64.const', 0], 'f64')
|
|
2081
|
+
|
|
2082
|
+
const [op, ...args] = node
|
|
2083
|
+
// WASM IR passthrough: internally-generated IR nodes (from statement flattening) pass through
|
|
2084
|
+
if (typeof op === 'string' && !ctx.core.emit[op] && (op.includes('.') || WASM_OPS.has(op))) return node
|
|
2085
|
+
|
|
2086
|
+
// Literal node [, value] — handle null/undefined values
|
|
2087
|
+
if (op == null && args.length === 1) {
|
|
2088
|
+
const v = args[0]
|
|
2089
|
+
return v === undefined ? undefExpr() : v === null ? nullExpr() : emit(v)
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const handler = ctx.core.emit[op]
|
|
2093
|
+
if (!handler) err(`Unknown op: ${op}`)
|
|
2094
|
+
return handler(...args)
|
|
2095
|
+
}
|