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/compile.js
ADDED
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile prepared AST to WASM module (S-expression arrays for watr).
|
|
3
|
+
*
|
|
4
|
+
* # Stage contract
|
|
5
|
+
* IN: prepared AST (from prepare) + `ctx.func.list` with raw bodies.
|
|
6
|
+
* OUT: WAT IR `['module', ...sections]` ready for watrCompile/watrPrint.
|
|
7
|
+
* FLOW: orchestrator only. Calls analyze passes per function, then emit(body) via
|
|
8
|
+
* src/emit.js's dispatch, then optimizeFunc (src/optimize.js) per function,
|
|
9
|
+
* finally assembles module sections in canonical order.
|
|
10
|
+
*
|
|
11
|
+
* # Core abstraction
|
|
12
|
+
* Emitter table (ctx.core.emit) maps AST ops → WASM IR generators. Base operators defined
|
|
13
|
+
* in `emitter` export (src/emit.js); on reset, ctx.core.emit starts as a flat copy of emitter
|
|
14
|
+
* and modules add/override entries directly. No prototype chain.
|
|
15
|
+
* emit(node) dispatches: numbers → i32/f64.const, strings → local.get, arrays → ctx.core.emit[op].
|
|
16
|
+
*
|
|
17
|
+
* # Type system
|
|
18
|
+
* Every emitted node carries .type ('i32' | 'f64').
|
|
19
|
+
* Operators preserve i32 when both operands are i32.
|
|
20
|
+
* Division/power always produce f64. Bitwise/comparisons always produce i32.
|
|
21
|
+
* Variables are typed by pre-analysis: if any assignment is f64, local is f64.
|
|
22
|
+
*
|
|
23
|
+
* Per-function state on ctx: locals (Map name→type), stack (loop labels), uniq (counter), sig.
|
|
24
|
+
*
|
|
25
|
+
* @module compile
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { parse as parseWat } from 'watr'
|
|
29
|
+
import { ctx, err, inc, resolveIncludes, PTR } from './ctx.js'
|
|
30
|
+
import {
|
|
31
|
+
T, VAL, valTypeOf, lookupValType, analyzeValTypes, analyzeIntCertain, analyzeLocals, analyzeBody, analyzePtrUnboxable, typedElemAux, exprType, invalidateLocalsCache, invalidateValTypesCache,
|
|
32
|
+
extractParams, classifyParam, collectParamNames,
|
|
33
|
+
findFreeVars, analyzeBoxedCaptures, analyzeDynKeys, typedElemCtor,
|
|
34
|
+
repOf, updateRep, repOfGlobal, updateGlobalRep,
|
|
35
|
+
staticObjectProps, mergeParamFact, ensureParamRep, callerParamFactMap, clearStickyNull,
|
|
36
|
+
inferArgType, inferArgSchema, inferArgArrElemSchema, inferArgArrElemValType, inferArgTypedCtor,
|
|
37
|
+
ctorFromElemAux, collectProgramFacts, observeProgramSlots, narrowReturnArrayElems,
|
|
38
|
+
isBlockBody, alwaysReturns, hasBareReturn, returnExprs, collectReturnExprs,
|
|
39
|
+
findMutations,
|
|
40
|
+
} from './analyze.js'
|
|
41
|
+
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake } from './optimize.js'
|
|
42
|
+
import { emit, emitter, emitFlat, emitBody } from './emit.js'
|
|
43
|
+
import {
|
|
44
|
+
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
45
|
+
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
46
|
+
MAX_CLOSURE_ARITY, MEM_OPS, WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
47
|
+
mkPtrIR, ptrOffsetIR, ptrTypeIR, extractF64Bits, appendStaticSlots,
|
|
48
|
+
isLit, litVal, isNullishLit, isPureIR, isPostfix, emitNum,
|
|
49
|
+
temp, tempI32, tempI64, f64rem, toNumF64, truthyIR, toBoolFromEmitted,
|
|
50
|
+
keyValType, usesDynProps, needsDynShadow,
|
|
51
|
+
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish,
|
|
52
|
+
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
53
|
+
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
54
|
+
valKindToPtr,
|
|
55
|
+
} from './ir.js'
|
|
56
|
+
|
|
57
|
+
// Re-export for backward compatibility (modules import from compile.js)
|
|
58
|
+
export { T, VAL, valTypeOf, lookupValType, extractParams, classifyParam, collectParamNames, repOf, updateRep, repOfGlobal, updateGlobalRep }
|
|
59
|
+
export { emit, emitter, emitFlat }
|
|
60
|
+
// IR helpers — re-export from ir.js so module/*.js keep their existing import paths.
|
|
61
|
+
export {
|
|
62
|
+
typed, asF64, asI32, asParamType, toI32, asI64, fromI64,
|
|
63
|
+
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
64
|
+
MAX_CLOSURE_ARITY, MEM_OPS, WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
|
|
65
|
+
mkPtrIR, ptrOffsetIR, ptrTypeIR, extractF64Bits, appendStaticSlots,
|
|
66
|
+
isLit, litVal, isNullishLit, isPureIR, isPostfix, emitNum,
|
|
67
|
+
temp, tempI32, tempI64, f64rem, toNumF64, truthyIR, toBoolFromEmitted,
|
|
68
|
+
keyValType, usesDynProps, needsDynShadow,
|
|
69
|
+
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish,
|
|
70
|
+
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
71
|
+
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
72
|
+
}
|
|
73
|
+
// Emit-dependent helpers (emitTypeofCmp, toBool, materializeMulti, emitDecl,
|
|
74
|
+
// buildArrayWithSpreads) live in emit.js and are re-exported there for modules.
|
|
75
|
+
export { emitTypeofCmp, toBool, materializeMulti, emitDecl, buildArrayWithSpreads } from './emit.js'
|
|
76
|
+
|
|
77
|
+
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
78
|
+
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
79
|
+
|
|
80
|
+
// NaN-box high-bits mask: used by the static-prefix-strip pass below to
|
|
81
|
+
// identify pointer slots in the data segment. Kept local (ir.js owns the
|
|
82
|
+
// runtime packing via mkPtrIR).
|
|
83
|
+
const NAN_PREFIX_BITS = 0x7FF8n
|
|
84
|
+
|
|
85
|
+
// Low-level IR helpers previously lived here. Pure ones moved to src/ir.js;
|
|
86
|
+
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
87
|
+
// buildArrayWithSpreads) moved to src/emit.js.
|
|
88
|
+
|
|
89
|
+
// AST-analysis primitives (staticObjectProps, paramReps lattice helpers,
|
|
90
|
+
// inferArg* cross-call inference, collectProgramFacts) moved to src/analyze.js.
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Boundary-wrap predicate: exports whose body-driven result OR any param narrowed
|
|
94
|
+
* away from the JS-visible f64 ABI need a wrapper that re-/un-boxes at the JS↔WASM
|
|
95
|
+
* edge so the inner func can keep its raw type while exports preserve Number /
|
|
96
|
+
* pointer semantics for JS callers.
|
|
97
|
+
*
|
|
98
|
+
* Numeric param narrowing on exports IS enabled when all internal call sites pass
|
|
99
|
+
* i32 — the wrapper does `i32.trunc_sat_f64_s` at the boundary (matches JS i32
|
|
100
|
+
* coercion `n | 0` semantics for integer-shaped values; a JS caller passing a
|
|
101
|
+
* fractional Number gets the same truncation it would get from `arr[n]`).
|
|
102
|
+
*/
|
|
103
|
+
const isBoundaryWrapped = (func) => {
|
|
104
|
+
if (!func.exported || func.raw || func.sig.results.length !== 1) return false
|
|
105
|
+
if (func.sig.results[0] !== 'f64' || func.sig.ptrKind != null) return true
|
|
106
|
+
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tail-call rewrite: walks tail positions of an emitted IR tree and replaces
|
|
111
|
+
* direct `(call $name args...)` ops with `(return_call $name args...)`.
|
|
112
|
+
*
|
|
113
|
+
* Tail positions, recursively from the IR root:
|
|
114
|
+
* - the root itself (function's terminal value-producing expression)
|
|
115
|
+
* - both arms of `(if (result T) cond (then ...) (else ...))`
|
|
116
|
+
* - last instruction of `(block (result T) ...)`
|
|
117
|
+
*
|
|
118
|
+
* Only fires when caller and callee result types match — if they didn't match,
|
|
119
|
+
* `asParamType`/`asPtrOffset` would have wrapped the call in a conversion op,
|
|
120
|
+
* pushing the `call` away from the tail position. We don't recurse into
|
|
121
|
+
* arithmetic / select / loop ops: their results aren't standalone-tail control
|
|
122
|
+
* transfers.
|
|
123
|
+
*
|
|
124
|
+
* Mirrors the existing `'return'` op handler in emit.js (which already does
|
|
125
|
+
* TCO when the return statement is explicit). This pass closes the gap for
|
|
126
|
+
* expression-bodied arrows like `(n, acc) => n <= 0 ? acc : sum(n-1, acc+n)`
|
|
127
|
+
* — the AST has no `return` keyword so the emit-time handler never fires.
|
|
128
|
+
*/
|
|
129
|
+
const tcoTailRewrite = (ir, resultType) => {
|
|
130
|
+
if (ctx.transform.noTailCall || ctx.func.inTry) return ir
|
|
131
|
+
if (!Array.isArray(ir)) return ir
|
|
132
|
+
const op = ir[0]
|
|
133
|
+
if (op === 'call' && typeof ir[1] === 'string') {
|
|
134
|
+
// IR call name is `$name`; func.map keys are bare `name`.
|
|
135
|
+
const calleeName = ir[1].startsWith('$') ? ir[1].slice(1) : ir[1]
|
|
136
|
+
const callee = ctx.func.map.get(calleeName)
|
|
137
|
+
if (!callee || callee.raw) return ir
|
|
138
|
+
const calleeRT = callee.sig?.results?.[0] ?? 'f64'
|
|
139
|
+
if (calleeRT !== resultType) return ir
|
|
140
|
+
return typed(['return_call', ...ir.slice(1)], resultType)
|
|
141
|
+
}
|
|
142
|
+
if (op === 'if' && Array.isArray(ir[1]) && ir[1][0] === 'result') {
|
|
143
|
+
let changed = false
|
|
144
|
+
const newIr = ir.slice()
|
|
145
|
+
for (let i = 3; i < newIr.length; i++) {
|
|
146
|
+
const arm = newIr[i]
|
|
147
|
+
if (Array.isArray(arm) && (arm[0] === 'then' || arm[0] === 'else') && arm.length > 1) {
|
|
148
|
+
const last = arm[arm.length - 1]
|
|
149
|
+
const rewritten = tcoTailRewrite(last, resultType)
|
|
150
|
+
if (rewritten !== last) {
|
|
151
|
+
newIr[i] = [...arm.slice(0, -1), rewritten]
|
|
152
|
+
changed = true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return changed ? typed(newIr, ir.type) : ir
|
|
157
|
+
}
|
|
158
|
+
if (op === 'block' && ir.length > 1) {
|
|
159
|
+
const last = ir[ir.length - 1]
|
|
160
|
+
const rewritten = tcoTailRewrite(last, resultType)
|
|
161
|
+
if (rewritten !== last) return typed([...ir.slice(0, -1), rewritten], ir.type)
|
|
162
|
+
}
|
|
163
|
+
return ir
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// === Module compilation ===
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Phase: signature narrowing.
|
|
170
|
+
*
|
|
171
|
+
* Reads programFacts.callSites + valueUsed; mutates each user func's `sig`:
|
|
172
|
+
* - param types (f64 → i32 / pointer-ABI i32+ptrKind, when call sites agree)
|
|
173
|
+
* - param schemas (per-arg schemaId, recorded into programFacts.paramReps[k].schemaId)
|
|
174
|
+
* - result type (f64 → i32, when body always returns i32)
|
|
175
|
+
* - result valType (`func.valResult`) and pointer narrowing (sig.ptrKind)
|
|
176
|
+
*
|
|
177
|
+
* Pure w.r.t. the AST — only the function `sig` records change. The unified
|
|
178
|
+
* paramReps record is populated here (per-field lattice) and consumed by the
|
|
179
|
+
* per-function emit phase below.
|
|
180
|
+
*
|
|
181
|
+
* Encoded structurally as a phase so future S3 work can move it into a
|
|
182
|
+
* pipeline runner without re-deriving the in/out contract from comments.
|
|
183
|
+
*/
|
|
184
|
+
function narrowSignatures(programFacts, ast) {
|
|
185
|
+
const { callSites, valueUsed, paramReps } = programFacts
|
|
186
|
+
|
|
187
|
+
// Reachability filter: dead callerFuncs (e.g. unused stdlib helpers from bundled
|
|
188
|
+
// modules) shouldn't poison narrowing of live functions. Without this, a never-
|
|
189
|
+
// executed call like `checksumF64 → mix(h, u[i])` would force mix's `x` rep to
|
|
190
|
+
// bimorphic (f64 ∪ i32) and block i32 narrowing of mix's hot caller (runKernel).
|
|
191
|
+
// Live = exported ∪ value-used ∪ transitively reached from those + top-level.
|
|
192
|
+
// Top-level call sites have callerFunc === null and are unconditionally live.
|
|
193
|
+
if (callSites.length) {
|
|
194
|
+
const live = new Set()
|
|
195
|
+
for (const f of ctx.func.list) {
|
|
196
|
+
if (f.exported || valueUsed.has(f.name)) live.add(f.name)
|
|
197
|
+
}
|
|
198
|
+
let changed = true
|
|
199
|
+
while (changed) {
|
|
200
|
+
changed = false
|
|
201
|
+
for (const cs of callSites) {
|
|
202
|
+
if (cs.callerFunc === null || live.has(cs.callerFunc.name)) {
|
|
203
|
+
if (!live.has(cs.callee)) { live.add(cs.callee); changed = true }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Mutate in place — every later phase reads the same array.
|
|
208
|
+
let w = 0
|
|
209
|
+
for (let r = 0; r < callSites.length; r++) {
|
|
210
|
+
const cs = callSites[r]
|
|
211
|
+
if (cs.callerFunc === null || live.has(cs.callerFunc.name)) callSites[w++] = cs
|
|
212
|
+
}
|
|
213
|
+
callSites.length = w
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// D: Call-site type propagation — infer param types from how functions are called.
|
|
217
|
+
// Drives off `callSites` collected during the ProgramFacts walk; no AST re-walking.
|
|
218
|
+
// For non-exported internal functions, if all call sites agree on a param's type,
|
|
219
|
+
// seed the param's val rep (ctx.func.repByLocal) during per-function compilation.
|
|
220
|
+
// Also infer i32/f64 WASM type — when all call sites pass i32 for a param, specialize
|
|
221
|
+
// sig.params[k].type to i32 (no default, no rest, not exported, not value-used).
|
|
222
|
+
// Also propagate schema ID — when all call sites pass objects with the same schema,
|
|
223
|
+
// bind the callee's param to that schema so `p.x` becomes a direct slot load.
|
|
224
|
+
// Inference helpers (inferArgType/inferArgSchema/inferArgArr*/inferArgTypedCtor)
|
|
225
|
+
// live in analyze.js — pure AST→fact resolvers shared across fixpoint phases.
|
|
226
|
+
// Per-caller analysis is stable across fixpoint iterations — precompute once.
|
|
227
|
+
// callerCtx[null] (top-level) uses module globals for both locals and valTypes.
|
|
228
|
+
const callerCtx = new Map() // funcObj | null → { callerLocals, callerValTypes }
|
|
229
|
+
callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes })
|
|
230
|
+
for (const func of ctx.func.list) {
|
|
231
|
+
if (!func.body || func.raw) continue
|
|
232
|
+
// Single unified walk — locals + valTypes from the same traversal.
|
|
233
|
+
const facts = analyzeBody(func.body)
|
|
234
|
+
for (const p of func.sig.params) if (!facts.locals.has(p.name)) facts.locals.set(p.name, p.type)
|
|
235
|
+
callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes })
|
|
236
|
+
}
|
|
237
|
+
// Per-caller arr-elem observations. Recomputed each fixpoint iteration so
|
|
238
|
+
// newly-narrowed func.arrayElemSchema/.arrayElemValType results propagate
|
|
239
|
+
// from `const rows = initRows()` observations. Two-pass fixpoint: first
|
|
240
|
+
// pass learns from literals + module vars; second pass forwards through
|
|
241
|
+
// chained helpers (f → addXY → {getX, getY}).
|
|
242
|
+
const buildCallerElems = (sliceKey) => {
|
|
243
|
+
const m = new Map()
|
|
244
|
+
m.set(null, new Map())
|
|
245
|
+
for (const func of ctx.func.list) {
|
|
246
|
+
if (!func.body || func.raw) continue
|
|
247
|
+
m.set(func, analyzeBody(func.body)[sliceKey])
|
|
248
|
+
}
|
|
249
|
+
return m
|
|
250
|
+
}
|
|
251
|
+
let callerArrElemsCtx = buildCallerElems('arrElemSchemas')
|
|
252
|
+
const rebuildArrElems = () => { callerArrElemsCtx = buildCallerElems('arrElemSchemas') }
|
|
253
|
+
let callerArrElemValsCtx = buildCallerElems('arrElemValTypes')
|
|
254
|
+
const rebuildArrElemVals = () => { callerArrElemValsCtx = buildCallerElems('arrElemValTypes') }
|
|
255
|
+
const runFixpoint = () => {
|
|
256
|
+
for (let s = 0; s < callSites.length; s++) {
|
|
257
|
+
const { callee, argList, callerFunc } = callSites[s]
|
|
258
|
+
const func = ctx.func.map.get(callee)
|
|
259
|
+
if (!func || func.exported || valueUsed.has(callee)) continue
|
|
260
|
+
const ctxEntry = callerCtx.get(callerFunc)
|
|
261
|
+
if (!ctxEntry) continue
|
|
262
|
+
const { callerLocals, callerValTypes } = ctxEntry
|
|
263
|
+
const callerSchemas = callerParamFactMap(paramReps, callerFunc, 'schemaId')
|
|
264
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
265
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
266
|
+
const r = ensureParamRep(paramReps, callee, k)
|
|
267
|
+
if (k < argList.length) {
|
|
268
|
+
if (r.val !== null) mergeParamFact(r, 'val', inferArgType(argList[k], callerValTypes))
|
|
269
|
+
// Wasm-type lattice: exprType always returns 'i32'|'f64' — no null sentinel.
|
|
270
|
+
if (r.wasm !== null) {
|
|
271
|
+
const wt = exprType(argList[k], callerLocals)
|
|
272
|
+
if (r.wasm === undefined) r.wasm = wt
|
|
273
|
+
else if (r.wasm !== wt) r.wasm = null
|
|
274
|
+
}
|
|
275
|
+
if (r.schemaId !== null) mergeParamFact(r, 'schemaId', inferArgSchema(argList[k], callerSchemas))
|
|
276
|
+
// intConst lattice: bare-integer literal at every site → param has fixed value.
|
|
277
|
+
// Skip rest position — argList[restIdx] is just the first packed arg, not the
|
|
278
|
+
// whole array. Drop intConst for the rest param so it's never substituted.
|
|
279
|
+
if (k === restIdx) r.intConst = null
|
|
280
|
+
else if (r.intConst !== null) {
|
|
281
|
+
// Literal forms after prepare: bare number, `[null, n]` (literal wrap),
|
|
282
|
+
// or `['u-', n]` (negative literal). A bare string referencing a known
|
|
283
|
+
// module-scope `const NAME = <int-literal>` resolves through ctx.scope.constInts.
|
|
284
|
+
// Anything else → no-consensus.
|
|
285
|
+
const a = argList[k]
|
|
286
|
+
let raw = null
|
|
287
|
+
if (typeof a === 'number') raw = a
|
|
288
|
+
else if (Array.isArray(a) && a[0] == null && typeof a[1] === 'number') raw = a[1]
|
|
289
|
+
else if (Array.isArray(a) && a[0] === 'u-' && typeof a[1] === 'number') raw = -a[1]
|
|
290
|
+
else if (typeof a === 'string' && ctx.scope.constInts?.has(a)) raw = ctx.scope.constInts.get(a)
|
|
291
|
+
const v = (raw != null && Number.isInteger(raw) && raw >= -2147483648 && raw <= 2147483647) ? raw : null
|
|
292
|
+
mergeParamFact(r, 'intConst', v)
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
// Missing arg — call pads with nullExpr (f64). Prevents narrowing.
|
|
296
|
+
r.val = null; r.wasm = null; r.schemaId = null; r.intConst = null
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Generic arr-elem fixpoint: same shape for arrayElemSchema (schema-id),
|
|
302
|
+
// arrayElemValType (VAL.*), and typedCtor. `field` selects which fact;
|
|
303
|
+
// `inferFn` and `elemsCtxMap` provide per-callee inference.
|
|
304
|
+
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
305
|
+
for (let s = 0; s < callSites.length; s++) {
|
|
306
|
+
const { callee, argList, callerFunc } = callSites[s]
|
|
307
|
+
const func = ctx.func.map.get(callee)
|
|
308
|
+
if (!func || func.exported || valueUsed.has(callee)) continue
|
|
309
|
+
if (!callerCtx.get(callerFunc)) continue
|
|
310
|
+
const callerParams = callerParamFactMap(paramReps, callerFunc, field)
|
|
311
|
+
const callerElems = elemsCtxMap.get(callerFunc)
|
|
312
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
313
|
+
const r = ensureParamRep(paramReps, callee, k)
|
|
314
|
+
if (k >= argList.length) { r[field] = null; continue }
|
|
315
|
+
if (r[field] === null) continue
|
|
316
|
+
mergeParamFact(r, field, inferFn(argList[k], callerElems, callerParams))
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArgArrElemSchema, callerArrElemsCtx)
|
|
321
|
+
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArgArrElemValType, callerArrElemValsCtx)
|
|
322
|
+
runFixpoint()
|
|
323
|
+
runFixpoint()
|
|
324
|
+
|
|
325
|
+
// Apply i32 specialization: for non-value-used funcs with consistent i32 call
|
|
326
|
+
// sites and no defaults/rest at that position, narrow sig.params[k].type.
|
|
327
|
+
// Exports too — boundary wrapper handles the f64→i32 truncation at the JS edge.
|
|
328
|
+
for (const func of ctx.func.list) {
|
|
329
|
+
if (func.raw || valueUsed.has(func.name)) continue
|
|
330
|
+
const reps = paramReps.get(func.name)
|
|
331
|
+
if (!reps) continue
|
|
332
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
333
|
+
for (const [k, r] of reps) {
|
|
334
|
+
if (r.wasm !== 'i32' || k === restIdx) continue
|
|
335
|
+
const pname = func.sig.params[k].name
|
|
336
|
+
if (func.defaults?.[pname] != null) continue // defaults need nullish-sentinel f64
|
|
337
|
+
func.sig.params[k].type = 'i32'
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// intConst validation: a param marked with a unanimous integer literal at every call
|
|
342
|
+
// site is only safe to substitute if the body never reassigns it. Clear intConst on any
|
|
343
|
+
// param whose name appears on the LHS of an assignment / `++` / `--`. Skip exported
|
|
344
|
+
// (callable from JS with arbitrary value), value-used (closure callees), raw, defaulted,
|
|
345
|
+
// and rest params — same exclusions as the wasm-narrowing pass above.
|
|
346
|
+
for (const func of ctx.func.list) {
|
|
347
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
348
|
+
if (!func.body) continue
|
|
349
|
+
const reps = paramReps.get(func.name)
|
|
350
|
+
if (!reps) continue
|
|
351
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
352
|
+
let candidates = null
|
|
353
|
+
for (const [k, r] of reps) {
|
|
354
|
+
if (r.intConst == null || k === restIdx) continue
|
|
355
|
+
if (k >= func.sig.params.length) { r.intConst = null; continue }
|
|
356
|
+
const pname = func.sig.params[k].name
|
|
357
|
+
if (func.defaults?.[pname] != null) { r.intConst = null; continue }
|
|
358
|
+
;(candidates ||= new Map()).set(pname, r)
|
|
359
|
+
}
|
|
360
|
+
if (!candidates) continue
|
|
361
|
+
const mutated = new Set()
|
|
362
|
+
findMutations(func.body, new Set(candidates.keys()), mutated)
|
|
363
|
+
for (const name of mutated) candidates.get(name).intConst = null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Pointer-ABI specialization: for non-forwarding pointer params consistent across
|
|
367
|
+
// call sites, narrow from NaN-boxed f64 to i32 offset. Eliminates per-call __ptr_offset
|
|
368
|
+
// extraction + f64→i64→i32 reinterpret chains that dominate watr-style compilers.
|
|
369
|
+
// Safety:
|
|
370
|
+
// - exclude ARRAY (forwards on realloc — f64 NaN-box is a stable identity) and
|
|
371
|
+
// STRING (SSO vs heap dual encoding depends on ptr-type bits we'd drop).
|
|
372
|
+
// - exclude CLOSURE/TYPED (aux bits carry schema/element-type, lost with offset).
|
|
373
|
+
// - exclude params with defaults (nullish sentinel needs the f64 NaN space).
|
|
374
|
+
// - exclude rest position (array pack/unpack stays f64).
|
|
375
|
+
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
376
|
+
for (const func of ctx.func.list) {
|
|
377
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
378
|
+
const reps = paramReps.get(func.name)
|
|
379
|
+
if (!reps) continue
|
|
380
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
381
|
+
for (const [k, r] of reps) {
|
|
382
|
+
if (!PTR_ABI_KINDS.has(r.val)) continue
|
|
383
|
+
if (k === restIdx) continue
|
|
384
|
+
if (k >= func.sig.params.length) continue
|
|
385
|
+
const p = func.sig.params[k]
|
|
386
|
+
if (p.type === 'i32') continue // already narrowed by numeric pass
|
|
387
|
+
if (func.defaults?.[p.name] != null) continue
|
|
388
|
+
p.type = 'i32'
|
|
389
|
+
p.ptrKind = r.val
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// E: Result-type monomorphization — narrow sig.results[0] to 'i32' when body only
|
|
394
|
+
// produces i32 values. Fixpoint: a call to another narrowed func now contributes i32;
|
|
395
|
+
// iterate until stable so chains of i32-only helpers all narrow together.
|
|
396
|
+
// Safety: skip exported (JS boundary preserves number semantics), value-used (closure
|
|
397
|
+
// trampolines assume f64 result), raw WAT, multi-value. `undefined` return = skip.
|
|
398
|
+
// exprType already consults ctx.func.map for narrowed user-function results
|
|
399
|
+
// (analyze.js exprType `()` branch), plus the Math.imul/Math.clz32/charCodeAt
|
|
400
|
+
// stdlib subset and primitive-op rules. Earlier we had a local shim here that
|
|
401
|
+
// shadowed exprType's stdlib rules with `return 'f64'` for any non-user call;
|
|
402
|
+
// unifying through exprType lets a single rule (math.imul → i32) flow through
|
|
403
|
+
// to mix-style helpers (`(h, x) => Math.imul(h ^ (x|0), C)`) and unblocks the
|
|
404
|
+
// E-phase result narrowing on every call site that consumes them.
|
|
405
|
+
const exprTypeWithCalls = exprType
|
|
406
|
+
// Body-driven: safe for exports — the result type is determined by what the body
|
|
407
|
+
// computes, not by what JS callers might pass. JS-visible f64 ABI is restored at
|
|
408
|
+
// the boundary via a synthesized wrapper (see synthesizeBoundaryWrappers below).
|
|
409
|
+
// Shared pool for E (numeric), E2 (valType) and E3 (ptr) narrowing — same predicate.
|
|
410
|
+
const narrowableFuncs = ctx.func.list.filter(f =>
|
|
411
|
+
!f.raw && !valueUsed.has(f.name) && f.sig.results.length === 1
|
|
412
|
+
)
|
|
413
|
+
let changed = true
|
|
414
|
+
while (changed) {
|
|
415
|
+
changed = false
|
|
416
|
+
for (const func of narrowableFuncs) {
|
|
417
|
+
if (func.sig.results[0] === 'i32') continue
|
|
418
|
+
const body = func.body
|
|
419
|
+
// Bare `return;` produces undef (f64) — narrowing to i32 would lose that.
|
|
420
|
+
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
421
|
+
const exprs = returnExprs(body)
|
|
422
|
+
if (!exprs.length) continue
|
|
423
|
+
// Skip narrowing when any return-tail is `>>>` (unsigned uint32). Narrowing to i32
|
|
424
|
+
// loses the unsigned interpretation: the wrapper rebox via `f64.convert_i32_s` would
|
|
425
|
+
// sign-flip values with bit 31 set, breaking the canonical `(x >>> 0)` uint32 idiom.
|
|
426
|
+
// A future pass could track sig.unsignedResult and emit `f64.convert_i32_u` instead.
|
|
427
|
+
if (exprs.some(e => Array.isArray(e) && e[0] === '>>>')) continue
|
|
428
|
+
const savedCurrent = ctx.func.current
|
|
429
|
+
ctx.func.current = func.sig
|
|
430
|
+
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
431
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
432
|
+
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
433
|
+
ctx.func.current = savedCurrent
|
|
434
|
+
if (allI32) { func.sig.results = ['i32']; changed = true }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// E2: VAL-type result inference — if a function always returns the same VAL kind,
|
|
439
|
+
// record it so callers inherit that type (enables static dispatch on .length, .[],
|
|
440
|
+
// .prop through a call chain). Fixpoint propagates through helper chains.
|
|
441
|
+
// Safety: skip exported (host sees raw f64), value-used (indirect call signature).
|
|
442
|
+
// Shim so calls to already-typed funcs contribute their result type.
|
|
443
|
+
const valTypeOfWithCalls = (expr, localValTypes) => {
|
|
444
|
+
if (expr == null) return null
|
|
445
|
+
if (typeof expr === 'string') return localValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
446
|
+
if (!Array.isArray(expr)) return valTypeOf(expr)
|
|
447
|
+
const [op, ...args] = expr
|
|
448
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
449
|
+
const f = ctx.func.map.get(args[0])
|
|
450
|
+
if (f?.valResult) return f.valResult
|
|
451
|
+
}
|
|
452
|
+
if (op === '?:') {
|
|
453
|
+
const a = valTypeOfWithCalls(args[1], localValTypes), b = valTypeOfWithCalls(args[2], localValTypes)
|
|
454
|
+
return a && a === b ? a : null
|
|
455
|
+
}
|
|
456
|
+
if (op === '&&' || op === '||') {
|
|
457
|
+
const a = valTypeOfWithCalls(args[0], localValTypes), b = valTypeOfWithCalls(args[1], localValTypes)
|
|
458
|
+
return a && a === b ? a : null
|
|
459
|
+
}
|
|
460
|
+
return valTypeOf(expr)
|
|
461
|
+
}
|
|
462
|
+
// Body-driven valResult inference: same safety analysis as numeric narrowing
|
|
463
|
+
// above — exports OK because boundary wrapper restores f64 ABI for JS callers.
|
|
464
|
+
changed = true
|
|
465
|
+
while (changed) {
|
|
466
|
+
changed = false
|
|
467
|
+
for (const func of narrowableFuncs) {
|
|
468
|
+
if (func.valResult) continue
|
|
469
|
+
const body = func.body
|
|
470
|
+
const isBlock = isBlockBody(body)
|
|
471
|
+
if (isBlock && hasBareReturn(body)) continue
|
|
472
|
+
const exprs = returnExprs(body)
|
|
473
|
+
if (!exprs.length) continue
|
|
474
|
+
const localValTypes = isBlock ? analyzeBody(body).valTypes : new Map()
|
|
475
|
+
// Params of this function contribute no known VAL type yet (paramReps may help later).
|
|
476
|
+
const vt0 = valTypeOfWithCalls(exprs[0], localValTypes)
|
|
477
|
+
if (!vt0) continue
|
|
478
|
+
const allSame = exprs.every(e => valTypeOfWithCalls(e, localValTypes) === vt0)
|
|
479
|
+
if (allSame) { func.valResult = vt0; changed = true }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
484
|
+
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
485
|
+
// D-pass arrayElemSchema/val fixpoints so `const rows = initRows()` in main
|
|
486
|
+
// resolves to VAL.ARRAY (lets runKernel pick up r.val=ARRAY) and its arr-elem
|
|
487
|
+
// schema (sets paramReps[runKernel][0].arrayElemSchema=sid).
|
|
488
|
+
// Cache invalidation: analyzeBody.valTypes is body-keyed, and entries cached
|
|
489
|
+
// during the first D pass have stale (null) `valTypeOf(call)` results because
|
|
490
|
+
// valResult was unset back then.
|
|
491
|
+
narrowReturnArrayElems('arrayElemSchema', paramReps, valueUsed)
|
|
492
|
+
narrowReturnArrayElems('arrayElemValType', paramReps, valueUsed)
|
|
493
|
+
for (const func of ctx.func.list) {
|
|
494
|
+
if (func.body && !func.raw) invalidateValTypesCache(func.body)
|
|
495
|
+
}
|
|
496
|
+
for (const func of ctx.func.list) {
|
|
497
|
+
if (!func.body || func.raw) continue
|
|
498
|
+
const entry = callerCtx.get(func)
|
|
499
|
+
if (entry) entry.callerValTypes = analyzeBody(func.body).valTypes
|
|
500
|
+
}
|
|
501
|
+
// Re-observe schema slot val-types now that E2 has set `valResult` on user
|
|
502
|
+
// funcs. First pass runs in collectProgramFacts before valResult is known, so
|
|
503
|
+
// a slot like `cs` in `{ ..., cs }` (where `cs = checksum(out)`) gets observed
|
|
504
|
+
// as null. observeSlot's first-wins-then-clash rule lets a later precise
|
|
505
|
+
// observation upgrade `undefined` → NUMBER without poisoning earlier
|
|
506
|
+
// monomorphic observations.
|
|
507
|
+
observeProgramSlots(ast)
|
|
508
|
+
rebuildArrElems()
|
|
509
|
+
rebuildArrElemVals()
|
|
510
|
+
// Clear sticky-null on val/schemaId — first 2 passes ran with valResult unset, so
|
|
511
|
+
// call args resolving via `f.valResult` returned null and got stuck. Re-running
|
|
512
|
+
// with refreshed callerValTypes lets these flow.
|
|
513
|
+
clearStickyNull(paramReps, 'val')
|
|
514
|
+
clearStickyNull(paramReps, 'schemaId')
|
|
515
|
+
runFixpoint()
|
|
516
|
+
// Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
|
|
517
|
+
runArrFixpoint()
|
|
518
|
+
runArrFixpoint()
|
|
519
|
+
// Parallel arr-elem-val fixpoint (NUMBER/STRING/…). Twice for transitive closure
|
|
520
|
+
// through helper chains: `init()→main→runKernel`.
|
|
521
|
+
runArrValTypeFixpoint()
|
|
522
|
+
runArrValTypeFixpoint()
|
|
523
|
+
// E3: Result-type pointer narrowing — when valResult is a non-ambiguous pointer kind
|
|
524
|
+
// with constant aux, narrow sig.results[0] from f64 to i32 and tag sig.ptrKind/.ptrAux.
|
|
525
|
+
// Eliminates the f64.reinterpret_i64+i64.or rebox at every return and the
|
|
526
|
+
// i32.wrap_i64+i64.reinterpret_f64 unbox at every callsite that uses the value as a
|
|
527
|
+
// pointer (load .[], .length, .prop slot dispatch).
|
|
528
|
+
// - SET/MAP/BUFFER: aux always 0 — no per-callsite aux preservation needed.
|
|
529
|
+
// - OBJECT: aux is schema-id; narrow only when all return exprs share a constant
|
|
530
|
+
// schema (literal `{a,b,c}`, schemaId-bound param, module-bound var, or call to
|
|
531
|
+
// another OBJECT-narrowed func). Caller picks aux up via callIR.ptrAux → readVar →
|
|
532
|
+
// repByLocal.schemaId, restoring property-slot dispatch through the call boundary.
|
|
533
|
+
// Safety: ARRAY forwards on realloc (no narrowing). STRING dual-encoded SSO/heap.
|
|
534
|
+
// CLOSURE/TYPED also carry meaningful aux — TYPED narrowing is a follow-up. Body must
|
|
535
|
+
// be a guaranteed-return form — fallthrough fallback i32.const 0 would be a valid
|
|
536
|
+
// offset 0 of the narrowed kind, not undefined.
|
|
537
|
+
const PTR_RESULT_KINDS_NOAUX = new Set([VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
538
|
+
// Schema-id inference for a return expression. Returns id (number), or null if unknown
|
|
539
|
+
// / not constant. Mirrors inferArgSchema but extends with calls to already-narrowed
|
|
540
|
+
// OBJECT-result funcs (fixpoint propagation through helper chains).
|
|
541
|
+
const schemaIdOfReturn = (expr, paramSchemasMap) => {
|
|
542
|
+
if (typeof expr === 'string') {
|
|
543
|
+
if (paramSchemasMap?.has(expr)) return paramSchemasMap.get(expr)
|
|
544
|
+
if (ctx.schema.vars.has(expr)) return ctx.schema.vars.get(expr)
|
|
545
|
+
return null
|
|
546
|
+
}
|
|
547
|
+
if (!Array.isArray(expr)) return null
|
|
548
|
+
const [op, ...args] = expr
|
|
549
|
+
if (op === '{}') {
|
|
550
|
+
// Object literal: bail to null on block body, dynamic key, or spread.
|
|
551
|
+
const parsed = staticObjectProps(args)
|
|
552
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
553
|
+
}
|
|
554
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
555
|
+
const f = ctx.func.map.get(args[0])
|
|
556
|
+
if (f?.valResult === VAL.OBJECT && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
557
|
+
return null
|
|
558
|
+
}
|
|
559
|
+
if (op === '?:') {
|
|
560
|
+
const a = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
561
|
+
const b = schemaIdOfReturn(args[2], paramSchemasMap)
|
|
562
|
+
return a != null && a === b ? a : null
|
|
563
|
+
}
|
|
564
|
+
if (op === '&&' || op === '||') {
|
|
565
|
+
const a = schemaIdOfReturn(args[0], paramSchemasMap)
|
|
566
|
+
const b = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
567
|
+
return a != null && a === b ? a : null
|
|
568
|
+
}
|
|
569
|
+
return null
|
|
570
|
+
}
|
|
571
|
+
// Per-body local elemAux map: scans `let/const x = new TypedArray(...)` decls so
|
|
572
|
+
// a return like `let a = new Float64Array(...); return a` resolves to a constant
|
|
573
|
+
// aux. Result calls + ?: are handled inline in typedAuxOfReturn.
|
|
574
|
+
const localElemAuxMap = (body) => {
|
|
575
|
+
const m = new Map()
|
|
576
|
+
const walk = (n) => {
|
|
577
|
+
if (!Array.isArray(n)) return
|
|
578
|
+
const op = n[0]
|
|
579
|
+
if (op === '=>') return
|
|
580
|
+
if ((op === 'let' || op === 'const') && n.length > 1) {
|
|
581
|
+
for (let i = 1; i < n.length; i++) {
|
|
582
|
+
const a = n[i]
|
|
583
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
584
|
+
const aux = typedElemAux(typedElemCtor(a[2]))
|
|
585
|
+
if (aux != null) m.set(a[1], aux)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
590
|
+
}
|
|
591
|
+
walk(body)
|
|
592
|
+
return m
|
|
593
|
+
}
|
|
594
|
+
const typedAuxOfReturn = (expr, localElemMap) => {
|
|
595
|
+
if (typeof expr === 'string') return localElemMap?.get(expr) ?? null
|
|
596
|
+
if (!Array.isArray(expr)) return null
|
|
597
|
+
const [op, ...args] = expr
|
|
598
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
599
|
+
if (args[0].startsWith('new.')) {
|
|
600
|
+
const ctor = typedElemCtor(expr)
|
|
601
|
+
return ctor != null ? typedElemAux(ctor) : null
|
|
602
|
+
}
|
|
603
|
+
const f = ctx.func.map.get(args[0])
|
|
604
|
+
if (f?.valResult === VAL.TYPED && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
if (op === '?:') {
|
|
608
|
+
const a = typedAuxOfReturn(args[1], localElemMap)
|
|
609
|
+
const b = typedAuxOfReturn(args[2], localElemMap)
|
|
610
|
+
return a != null && a === b ? a : null
|
|
611
|
+
}
|
|
612
|
+
if (op === '&&' || op === '||') {
|
|
613
|
+
const a = typedAuxOfReturn(args[0], localElemMap)
|
|
614
|
+
const b = typedAuxOfReturn(args[1], localElemMap)
|
|
615
|
+
return a != null && a === b ? a : null
|
|
616
|
+
}
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
// Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so outer's
|
|
620
|
+
// call to inner contributes a known schema-id.
|
|
621
|
+
let narrowChanged = true
|
|
622
|
+
while (narrowChanged) {
|
|
623
|
+
narrowChanged = false
|
|
624
|
+
for (const func of narrowableFuncs) {
|
|
625
|
+
if (!func.valResult) continue
|
|
626
|
+
if (func.sig.results[0] !== 'f64') continue
|
|
627
|
+
const isBlock = isBlockBody(func.body)
|
|
628
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
629
|
+
if (PTR_RESULT_KINDS_NOAUX.has(func.valResult)) {
|
|
630
|
+
func.sig.results = ['i32']
|
|
631
|
+
func.sig.ptrKind = func.valResult
|
|
632
|
+
narrowChanged = true
|
|
633
|
+
continue
|
|
634
|
+
}
|
|
635
|
+
const exprs = returnExprs(func.body)
|
|
636
|
+
if (!exprs.length) continue
|
|
637
|
+
if (func.valResult === VAL.OBJECT) {
|
|
638
|
+
const paramSchemasMap = callerParamFactMap(paramReps, func, 'schemaId')
|
|
639
|
+
const sid0 = schemaIdOfReturn(exprs[0], paramSchemasMap)
|
|
640
|
+
if (sid0 == null) continue
|
|
641
|
+
if (!exprs.every(e => schemaIdOfReturn(e, paramSchemasMap) === sid0)) continue
|
|
642
|
+
func.sig.results = ['i32']
|
|
643
|
+
func.sig.ptrKind = VAL.OBJECT
|
|
644
|
+
func.sig.ptrAux = sid0
|
|
645
|
+
narrowChanged = true
|
|
646
|
+
} else if (func.valResult === VAL.TYPED) {
|
|
647
|
+
const localMap = isBlock ? localElemAuxMap(func.body) : null
|
|
648
|
+
const aux0 = typedAuxOfReturn(exprs[0], localMap)
|
|
649
|
+
if (aux0 == null) continue
|
|
650
|
+
if (!exprs.every(e => typedAuxOfReturn(e, localMap) === aux0)) continue
|
|
651
|
+
func.sig.results = ['i32']
|
|
652
|
+
func.sig.ptrKind = VAL.TYPED
|
|
653
|
+
func.sig.ptrAux = aux0
|
|
654
|
+
narrowChanged = true
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// F: Cross-call typed-array element ctor propagation. Runs AFTER E3 so that
|
|
660
|
+
// calls to user functions returning a TYPED-narrowed pointer (with constant
|
|
661
|
+
// ptrAux, e.g. mkInput → Float64Array) contribute their element type to the
|
|
662
|
+
// caller's local typedElem map. Result: callees pick up `ctx.types.typedElem`
|
|
663
|
+
// for their own params and `arr[i]` reads emit a direct `f64.load` instead of
|
|
664
|
+
// the runtime `__is_str_key + __typed_idx` dispatch — closes the largest
|
|
665
|
+
// chunk of the JS→wasm gap on f64-heavy hot loops.
|
|
666
|
+
// (Helpers `inferArgTypedCtor`/`ctorFromElemAux` live in analyze.js so the
|
|
667
|
+
// bimorphic-typed specialization pass below can reuse them.)
|
|
668
|
+
// Per-caller typed-elem map, recomputed now that E3 has tagged helper sigs.
|
|
669
|
+
// Cache invalidation: analyzeBody.typedElems reads `ctx.func.map.get(...).sig.ptrKind`
|
|
670
|
+
// for `let x = mkInput(...)` decls; entries cached during the initial walk
|
|
671
|
+
// (before E3 ran) are stale (mkInput's ptrKind was unset then).
|
|
672
|
+
for (const func of ctx.func.list) {
|
|
673
|
+
if (func.body && !func.raw) invalidateValTypesCache(func.body)
|
|
674
|
+
}
|
|
675
|
+
const callerTypedCtx = new Map()
|
|
676
|
+
callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
|
|
677
|
+
for (const func of ctx.func.list) {
|
|
678
|
+
if (!func.body || func.raw) continue
|
|
679
|
+
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
680
|
+
}
|
|
681
|
+
// Two-pass fixpoint: lets a caller's params, once typed, propagate further to
|
|
682
|
+
// its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
|
|
683
|
+
// for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
|
|
684
|
+
// (same shape — field/inferFn/elemsCtxMap parameterization).
|
|
685
|
+
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferArgTypedCtor, callerTypedCtx)
|
|
686
|
+
runTypedFixpoint()
|
|
687
|
+
runTypedFixpoint()
|
|
688
|
+
|
|
689
|
+
// G: TYPED pointer-ABI narrowing — once .typedCtor agrees on a single
|
|
690
|
+
// ctor across all call sites, narrow the param from NaN-boxed f64 to raw
|
|
691
|
+
// i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
|
|
692
|
+
// per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
|
|
693
|
+
// that today dominates hot loops dominated by typed-array indexing.
|
|
694
|
+
// Call sites coerce via emitArgForParam → ptrOffsetIR(arg, VAL.TYPED).
|
|
695
|
+
// Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
|
|
696
|
+
// exported, value-used, raw, defaults, rest position.
|
|
697
|
+
for (const func of ctx.func.list) {
|
|
698
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
699
|
+
const reps = paramReps.get(func.name)
|
|
700
|
+
if (!reps) continue
|
|
701
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
702
|
+
for (const [k, r] of reps) {
|
|
703
|
+
const ctor = r.typedCtor
|
|
704
|
+
if (ctor == null) continue
|
|
705
|
+
if (k === restIdx) continue
|
|
706
|
+
if (k >= func.sig.params.length) continue
|
|
707
|
+
const p = func.sig.params[k]
|
|
708
|
+
if (p.type === 'i32') continue
|
|
709
|
+
if (func.defaults?.[p.name] != null) continue
|
|
710
|
+
const aux = typedElemAux(ctor)
|
|
711
|
+
if (aux == null) continue
|
|
712
|
+
p.type = 'i32'
|
|
713
|
+
p.ptrKind = VAL.TYPED
|
|
714
|
+
p.ptrAux = aux
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
719
|
+
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
720
|
+
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
721
|
+
// for caller's params, so inferArgType returns null and paramReps[callee][k].val is
|
|
722
|
+
// sticky null. With ptrKind enriching callerValTypes, sum's arr gets val=TYPED in
|
|
723
|
+
// its rep, letting array.js skip __is_str_key + __str_idx dispatch on `arr[i]`.
|
|
724
|
+
for (const func of ctx.func.list) {
|
|
725
|
+
if (!func.body || func.raw) continue
|
|
726
|
+
const entry = callerCtx.get(func)
|
|
727
|
+
if (!entry) continue
|
|
728
|
+
for (const p of func.sig.params) {
|
|
729
|
+
if (p.ptrKind == null) continue
|
|
730
|
+
if (entry.callerValTypes.has(p.name)) continue
|
|
731
|
+
entry.callerValTypes.set(p.name, p.ptrKind)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
clearStickyNull(paramReps, 'val')
|
|
735
|
+
runFixpoint()
|
|
736
|
+
|
|
737
|
+
// I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
|
|
738
|
+
// ran before E narrowed any result types, so callerLocals saw `let h = mix(...)`
|
|
739
|
+
// as f64 (mix's result was f64 then). After E narrowed mix's result to i32,
|
|
740
|
+
// exprType (which now consults func.sig.results for user calls) sees `h` as i32.
|
|
741
|
+
// Refresh callerLocals + clear sticky-null wasm + re-run fixpoint + re-apply
|
|
742
|
+
// numeric narrowing to propagate i32 through chains of i32-only helpers
|
|
743
|
+
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
744
|
+
// only after E phase narrowed mix's result).
|
|
745
|
+
for (const func of ctx.func.list) {
|
|
746
|
+
if (!func.body || func.raw) continue
|
|
747
|
+
invalidateLocalsCache(func.body)
|
|
748
|
+
const fresh = analyzeLocals(func.body)
|
|
749
|
+
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
750
|
+
callerCtx.get(func).callerLocals = fresh
|
|
751
|
+
}
|
|
752
|
+
// Reset wasm field unconditionally — first pass populated it from stale callerLocals
|
|
753
|
+
// (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
|
|
754
|
+
// yet). clearStickyNull only resets null; here we need to reset f64-observed too
|
|
755
|
+
// so the refreshed exprType view propagates.
|
|
756
|
+
for (const m of paramReps.values()) for (const r of m.values()) r.wasm = undefined
|
|
757
|
+
runFixpoint()
|
|
758
|
+
for (const func of ctx.func.list) {
|
|
759
|
+
if (func.raw || valueUsed.has(func.name)) continue
|
|
760
|
+
const reps = paramReps.get(func.name)
|
|
761
|
+
if (!reps) continue
|
|
762
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
763
|
+
for (const [k, r] of reps) {
|
|
764
|
+
if (r.wasm !== 'i32' || k === restIdx) continue
|
|
765
|
+
if (k >= func.sig.params.length) continue
|
|
766
|
+
const p = func.sig.params[k]
|
|
767
|
+
if (p.type === 'i32') continue // already narrowed (incl. ptr-ABI)
|
|
768
|
+
if (func.defaults?.[p.name] != null) continue
|
|
769
|
+
// Don't steal typed-array params from specializeBimorphicTyped: F phase parks
|
|
770
|
+
// bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
|
|
771
|
+
// ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
|
|
772
|
+
// so r.wasm flips to 'i32' here — but narrowing now breaks the clone path
|
|
773
|
+
// that still needs to mint per-ctor sigs with ptrKind=TYPED, ptrAux=ctor-aux.
|
|
774
|
+
if (r.val === VAL.TYPED) continue
|
|
775
|
+
p.type = 'i32'
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Phase: bimorphic typed-array param specialization.
|
|
782
|
+
*
|
|
783
|
+
* For each non-exported user function with a typed-array param that F/G-phase
|
|
784
|
+
* left bimorphic (paramReps[name][k].typedCtor === null because two or more call sites
|
|
785
|
+
* disagreed on the elem-ctor — e.g. `sum(f64)` and `sum(i32)`), clone the
|
|
786
|
+
* function once per concrete ctor seen at the call sites, narrow each clone's
|
|
787
|
+
* sig.params[k] to a monomorphic typed pointer ABI (type='i32', ptrKind=TYPED,
|
|
788
|
+
* ptrAux=ctor's aux), and rewrite the call AST nodes to dispatch to the right
|
|
789
|
+
* clone. The original survives as a fallback for any non-static call sites
|
|
790
|
+
* (e.g. inside arrow bodies); treeshake removes it if every site got rewritten.
|
|
791
|
+
*
|
|
792
|
+
* Why this matters: without specialization, `arr[i]` inside `sum` falls into
|
|
793
|
+
* the runtime `__typed_idx` path on every iteration — V8 can't inline a wasm
|
|
794
|
+
* call dominated by a switch on elem type. After specialization, each clone's
|
|
795
|
+
* `arr[i]` lowers to a direct `f64.load` (or `i32.load + f64.convert`) with
|
|
796
|
+
* the elem-ctor known at compile time. On poly bench this is the difference
|
|
797
|
+
* between ~5 ms and matching AS at ~1 ms.
|
|
798
|
+
*
|
|
799
|
+
* Safety mirrors G-phase: skip exported, raw, value-used, defaulted, rest, or
|
|
800
|
+
* already-i32 params. Bounded by MAX_CLONES_PER_FN to guard against polymorphic
|
|
801
|
+
* blow-up (≥5 distinct ctors at one site → no specialization).
|
|
802
|
+
*/
|
|
803
|
+
function specializeBimorphicTyped(programFacts) {
|
|
804
|
+
const { callSites, valueUsed, paramReps } = programFacts
|
|
805
|
+
const MAX_CLONES_PER_FN = 4
|
|
806
|
+
|
|
807
|
+
// Per-callee static-call-site index. Built once; cheap.
|
|
808
|
+
const sitesByCallee = new Map()
|
|
809
|
+
for (const cs of callSites) {
|
|
810
|
+
const list = sitesByCallee.get(cs.callee)
|
|
811
|
+
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Per-caller typedElem map (literal `new TypedArray(N)` bindings inside body).
|
|
815
|
+
const callerTypedCtx = new Map()
|
|
816
|
+
callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
|
|
817
|
+
for (const func of ctx.func.list) {
|
|
818
|
+
if (!func.body || func.raw) continue
|
|
819
|
+
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
820
|
+
}
|
|
821
|
+
// Per-caller typed-param map: caller's own params that F/G already narrowed
|
|
822
|
+
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
823
|
+
const callerTypedParamsCtx = new Map()
|
|
824
|
+
for (const func of ctx.func.list) {
|
|
825
|
+
const m = callerParamFactMap(paramReps, func, 'typedCtor') || null
|
|
826
|
+
let acc = m
|
|
827
|
+
if (func.sig?.params) for (const p of func.sig.params) {
|
|
828
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
829
|
+
acc ||= new Map()
|
|
830
|
+
if (!acc.has(p.name)) acc.set(p.name, ctorFromElemAux(p.ptrAux))
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (acc) callerTypedParamsCtx.set(func, acc)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Snapshot ctx.func.list — we'll be appending clones during the loop.
|
|
837
|
+
const originals = ctx.func.list.slice()
|
|
838
|
+
for (const func of originals) {
|
|
839
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
840
|
+
if (!func.body) continue
|
|
841
|
+
if (func.rest) continue
|
|
842
|
+
const reps = paramReps.get(func.name)
|
|
843
|
+
if (!reps) continue
|
|
844
|
+
const sites = sitesByCallee.get(func.name)
|
|
845
|
+
if (!sites || sites.length < 2) continue
|
|
846
|
+
|
|
847
|
+
// Find sticky-bimorphic typed-param positions left by F-phase.
|
|
848
|
+
const bimorphic = []
|
|
849
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
850
|
+
if (reps.get(k)?.typedCtor !== null) continue
|
|
851
|
+
const p = func.sig.params[k]
|
|
852
|
+
if (p.type === 'i32') continue
|
|
853
|
+
if (func.defaults?.[p.name] != null) continue
|
|
854
|
+
bimorphic.push(k)
|
|
855
|
+
}
|
|
856
|
+
if (bimorphic.length === 0) continue
|
|
857
|
+
|
|
858
|
+
// For each site, infer the ctor combination across bimorphic positions.
|
|
859
|
+
// Abort if any site has unknown ctor at any bimorphic position — we can't
|
|
860
|
+
// route that call to a specific clone without it.
|
|
861
|
+
const siteCombos = []
|
|
862
|
+
let abort = false
|
|
863
|
+
for (const site of sites) {
|
|
864
|
+
const callerTypedElems = callerTypedCtx.get(site.callerFunc)
|
|
865
|
+
const callerTypedParams = callerTypedParamsCtx.get(site.callerFunc)
|
|
866
|
+
const combo = []
|
|
867
|
+
for (const k of bimorphic) {
|
|
868
|
+
if (k >= site.argList.length) { abort = true; break }
|
|
869
|
+
const c = inferArgTypedCtor(site.argList[k], callerTypedElems, callerTypedParams)
|
|
870
|
+
if (c == null || typedElemAux(c) == null) { abort = true; break }
|
|
871
|
+
combo.push(c)
|
|
872
|
+
}
|
|
873
|
+
if (abort) break
|
|
874
|
+
siteCombos.push(combo)
|
|
875
|
+
}
|
|
876
|
+
if (abort) continue
|
|
877
|
+
|
|
878
|
+
// Distinct combos seen across call sites.
|
|
879
|
+
const distinct = new Map()
|
|
880
|
+
for (const combo of siteCombos) {
|
|
881
|
+
const key = combo.join('|')
|
|
882
|
+
if (!distinct.has(key)) distinct.set(key, combo)
|
|
883
|
+
}
|
|
884
|
+
if (distinct.size < 2) continue // F-phase already mono — nothing to do
|
|
885
|
+
if (distinct.size > MAX_CLONES_PER_FN) continue // polymorphic blow-up
|
|
886
|
+
|
|
887
|
+
// Build one clone per distinct combo.
|
|
888
|
+
const cloneByKey = new Map()
|
|
889
|
+
for (const [key, combo] of distinct) {
|
|
890
|
+
const suffix = combo.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
|
|
891
|
+
let cloneName = `${func.name}$${suffix}`
|
|
892
|
+
let n = 0
|
|
893
|
+
while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
|
|
894
|
+
|
|
895
|
+
const cloneSig = {
|
|
896
|
+
...func.sig,
|
|
897
|
+
params: func.sig.params.map(p => ({ ...p })),
|
|
898
|
+
results: [...func.sig.results],
|
|
899
|
+
}
|
|
900
|
+
for (let i = 0; i < bimorphic.length; i++) {
|
|
901
|
+
const k = bimorphic[i]
|
|
902
|
+
const aux = typedElemAux(combo[i])
|
|
903
|
+
const p = cloneSig.params[k]
|
|
904
|
+
p.type = 'i32'
|
|
905
|
+
p.ptrKind = VAL.TYPED
|
|
906
|
+
p.ptrAux = aux
|
|
907
|
+
}
|
|
908
|
+
const clone = { ...func, name: cloneName, sig: cloneSig }
|
|
909
|
+
ctx.func.list.push(clone)
|
|
910
|
+
ctx.func.map.set(cloneName, clone)
|
|
911
|
+
ctx.func.names.add(cloneName)
|
|
912
|
+
|
|
913
|
+
// Mirror per-param reps under the clone's name with mono ctors at bimorphic
|
|
914
|
+
// positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
|
|
915
|
+
// `arr[i]` lowers to direct typed load.
|
|
916
|
+
const cloneReps = new Map()
|
|
917
|
+
for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
918
|
+
for (let i = 0; i < bimorphic.length; i++) {
|
|
919
|
+
const k = bimorphic[i]
|
|
920
|
+
const r = cloneReps.get(k) || {}
|
|
921
|
+
r.typedCtor = combo[i]
|
|
922
|
+
r.val = VAL.TYPED
|
|
923
|
+
cloneReps.set(k, r)
|
|
924
|
+
}
|
|
925
|
+
paramReps.set(cloneName, cloneReps)
|
|
926
|
+
|
|
927
|
+
cloneByKey.set(key, clone)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Rewrite each site's call AST to point at the matching clone.
|
|
931
|
+
for (let i = 0; i < sites.length; i++) {
|
|
932
|
+
const clone = cloneByKey.get(siteCombos[i].join('|'))
|
|
933
|
+
sites[i].node[1] = clone.name
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Phase: refine ctx.types.anyDynKey using post-narrowSignatures type info.
|
|
940
|
+
*
|
|
941
|
+
* collectProgramFacts conservatively flags `anyDynKey=true` whenever it sees
|
|
942
|
+
* `obj[idx]` with a non-literal-string index — but typed-array / array /
|
|
943
|
+
* string `[]` is element access (sound for that base type), not a true
|
|
944
|
+
* dyn-key lookup that needs the hash-table shadow on object literals.
|
|
945
|
+
*
|
|
946
|
+
* After narrowSignatures populates paramReps (call-site fixpoint), we can
|
|
947
|
+
* type each `obj` in `obj[idx]` and skip the ones that are provably non-object.
|
|
948
|
+
* If no genuine dyn-key access remains program-wide, drop anyDynKey to false
|
|
949
|
+
* — object literals then skip the __dyn_set shadow loop (large code + perf win,
|
|
950
|
+
* especially on hot allocators like aos.initRows).
|
|
951
|
+
*
|
|
952
|
+
* Live-function gate: walking dead funcs (e.g. unused benchlib helpers) would
|
|
953
|
+
* pollute analysis with `out` params we never narrowed. Restrict to functions
|
|
954
|
+
* reachable from exports / first-class value uses.
|
|
955
|
+
*/
|
|
956
|
+
const NON_DYN_VTS = new Set([VAL.TYPED, VAL.ARRAY, VAL.STRING, VAL.BUFFER])
|
|
957
|
+
const TYPED_ARRAY_CTOR = /^(Float|Int|Uint|BigInt|BigUint)(8|16|32|64)(Clamped)?Array$/
|
|
958
|
+
|
|
959
|
+
function refineDynKeys(programFacts) {
|
|
960
|
+
if (!ctx.types.anyDynKey) return
|
|
961
|
+
const { paramReps, valueUsed } = programFacts
|
|
962
|
+
const isLitStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
963
|
+
|
|
964
|
+
// Per-function type map: param vtypes from paramReps, plus locals
|
|
965
|
+
// we can prove are typed arrays from `let v = new TypedArray(...)`. After
|
|
966
|
+
// prepare, that node is `['()', 'new.Float64Array', ...args]`.
|
|
967
|
+
const buildTypeMap = (funcName, body, params) => {
|
|
968
|
+
const map = new Map()
|
|
969
|
+
if (params) {
|
|
970
|
+
const reps = paramReps.get(funcName)
|
|
971
|
+
if (reps) for (let i = 0; i < params.length; i++) {
|
|
972
|
+
const t = reps.get(i)?.val
|
|
973
|
+
if (t != null) map.set(params[i].name, t)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const walk = (node) => {
|
|
977
|
+
if (!Array.isArray(node)) return
|
|
978
|
+
const op = node[0]
|
|
979
|
+
if (op === 'let' || op === 'const') {
|
|
980
|
+
for (let i = 1; i < node.length; i++) {
|
|
981
|
+
const d = node[i]
|
|
982
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
983
|
+
const init = d[2]
|
|
984
|
+
let ctor = null
|
|
985
|
+
if (Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string' && init[1].startsWith('new.'))
|
|
986
|
+
ctor = init[1].slice(4)
|
|
987
|
+
if (ctor && TYPED_ARRAY_CTOR.test(ctor)) map.set(d[1], VAL.TYPED)
|
|
988
|
+
else if (typeof init === 'string' && map.has(init)) map.set(d[1], map.get(init))
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (op === '=>') return // don't cross into nested arrows; they're separate funcs
|
|
992
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
993
|
+
}
|
|
994
|
+
walk(body)
|
|
995
|
+
return map
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
let real = false
|
|
999
|
+
const visit = (typeMap, node) => {
|
|
1000
|
+
if (real || !Array.isArray(node)) return
|
|
1001
|
+
const op = node[0]
|
|
1002
|
+
if (op === '[]') {
|
|
1003
|
+
const idx = node[2]
|
|
1004
|
+
if (!isLitStr(idx)) {
|
|
1005
|
+
const obj = node[1]
|
|
1006
|
+
const vt = typeof obj === 'string' ? typeMap.get(obj) : null
|
|
1007
|
+
if (!NON_DYN_VTS.has(vt)) real = true
|
|
1008
|
+
}
|
|
1009
|
+
} else if (op === 'for-in') real = true
|
|
1010
|
+
if (op === '=>') return
|
|
1011
|
+
for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Live: anything reachable from exports/first-class value uses. Skipping
|
|
1015
|
+
// dead helpers (unused benchlib imports) keeps their generic params from
|
|
1016
|
+
// pretending to be dyn-key access.
|
|
1017
|
+
const isLive = f => f.exported || paramReps.has(f.name) || (valueUsed && valueUsed.has(f.name))
|
|
1018
|
+
|
|
1019
|
+
const topMap = buildTypeMap(null, null, null)
|
|
1020
|
+
for (const f of ctx.func.list) {
|
|
1021
|
+
if (real) break
|
|
1022
|
+
if (!f.body || !isLive(f)) continue
|
|
1023
|
+
visit(buildTypeMap(f.name, f.body, f.sig?.params), f.body)
|
|
1024
|
+
}
|
|
1025
|
+
if (!real && ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) {
|
|
1026
|
+
if (real) break
|
|
1027
|
+
visit(topMap, mi)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (!real) ctx.types.anyDynKey = false
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Phase: emit one user function to WAT IR.
|
|
1035
|
+
*
|
|
1036
|
+
* Reads the (already-narrowed) `func.sig` and `programFacts.paramReps[name]`
|
|
1037
|
+
* to seed per-param val reps / schema bindings; emits body via emit / emitBody.
|
|
1038
|
+
*
|
|
1039
|
+
* Mutates ctx.func.* per-function state (locals, boxed, repByLocal, …) and
|
|
1040
|
+
* ctx.schema.vars (restored on exit so bindings don't leak across functions).
|
|
1041
|
+
*/
|
|
1042
|
+
function emitFunc(func, programFacts) {
|
|
1043
|
+
const { paramReps } = programFacts
|
|
1044
|
+
|
|
1045
|
+
// Raw WAT functions (e.g., _alloc, _reset from memory module)
|
|
1046
|
+
if (func.raw) return parseWat(func.raw)
|
|
1047
|
+
|
|
1048
|
+
const { name, body, exported, sig } = func
|
|
1049
|
+
const multi = sig.results.length > 1
|
|
1050
|
+
|
|
1051
|
+
// Reset per-function state
|
|
1052
|
+
ctx.func.stack = []
|
|
1053
|
+
ctx.func.uniq = 0
|
|
1054
|
+
ctx.func.current = sig
|
|
1055
|
+
ctx.func.body = body
|
|
1056
|
+
ctx.func.directClosures = null
|
|
1057
|
+
|
|
1058
|
+
// Pre-analyze local types from body
|
|
1059
|
+
// Block body vs object literal: object has ':' property nodes
|
|
1060
|
+
const block = Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
|
|
1061
|
+
ctx.func.boxed = new Map() // variable name → cell local name (i32) for mutable capture
|
|
1062
|
+
ctx.func.localProps = null // reset per function
|
|
1063
|
+
ctx.func.repByLocal = null // Map<name, ValueRep> — populated lazily; reset per function
|
|
1064
|
+
ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
|
|
1065
|
+
// Pre-seed cross-call param facts BEFORE analyzeLocals/analyzeValTypes(body) so that
|
|
1066
|
+
// when the walker sees `const b0 = arr[i]` or `let n = arr.length`, lookupValType(arr)
|
|
1067
|
+
// already resolves to VAL.TYPED — letting valTypeOf's `[]` rule propagate VAL.NUMBER
|
|
1068
|
+
// to b0 (skips __to_num) and exprType's `.length` rule keep n as i32 (skips per-iter
|
|
1069
|
+
// f64.convert_i32_s + i32.trunc_sat_f64_s on the loop counter). Without this seed,
|
|
1070
|
+
// params don't gain VAL.TYPED until after analyzeLocals freezes counter widths.
|
|
1071
|
+
const _reps = paramReps.get(name)
|
|
1072
|
+
if (_reps) {
|
|
1073
|
+
for (const [k, r] of _reps) {
|
|
1074
|
+
if (k >= sig.params.length) continue
|
|
1075
|
+
const pname = sig.params[k].name
|
|
1076
|
+
if (r.typedCtor) {
|
|
1077
|
+
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
1078
|
+
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
1079
|
+
updateRep(pname, { val: VAL.TYPED })
|
|
1080
|
+
}
|
|
1081
|
+
if (r.val && !ctx.func.repByLocal?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
1082
|
+
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
1083
|
+
if (r.arrayElemValType != null) updateRep(pname, { arrayElemValType: r.arrayElemValType })
|
|
1084
|
+
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
// Drop any earlier-cached analyzeLocals for this body — narrowSignatures called
|
|
1088
|
+
// it before our pre-seed, when params still had no inferred VAL.TYPED, so the
|
|
1089
|
+
// cached widths reflect the pre-narrow state. Re-walk now with reps in place.
|
|
1090
|
+
invalidateLocalsCache(body)
|
|
1091
|
+
ctx.func.locals = block ? analyzeLocals(body) : new Map()
|
|
1092
|
+
if (block) {
|
|
1093
|
+
analyzeValTypes(body)
|
|
1094
|
+
analyzeIntCertain(body)
|
|
1095
|
+
analyzeBoxedCaptures(body)
|
|
1096
|
+
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
1097
|
+
const unbox = analyzePtrUnboxable(body, ctx.func.locals, ctx.func.boxed)
|
|
1098
|
+
if (unbox.size > 0) {
|
|
1099
|
+
for (const [n, kind] of unbox) {
|
|
1100
|
+
ctx.func.locals.set(n, 'i32')
|
|
1101
|
+
const fields = { ptrKind: kind }
|
|
1102
|
+
if (kind === VAL.TYPED) {
|
|
1103
|
+
const aux = typedElemAux(ctx.types.typedElem?.get(n))
|
|
1104
|
+
if (aux != null) fields.ptrAux = aux
|
|
1105
|
+
}
|
|
1106
|
+
updateRep(n, fields)
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Pointer-ABI params (from narrowing loop above): params already have type='i32' and
|
|
1111
|
+
// ptrKind set. Register them in ctx.func.repByLocal so readVar tags local.gets correctly.
|
|
1112
|
+
// Boxed capture still works: the boxed-init path (below) uses a ptrKind-tagged local.get
|
|
1113
|
+
// so asF64 reboxes to NaN-form before f64.store to the cell.
|
|
1114
|
+
for (const p of sig.params) {
|
|
1115
|
+
if (p.ptrKind == null) continue
|
|
1116
|
+
const fields = { ptrKind: p.ptrKind }
|
|
1117
|
+
if (p.ptrAux != null) fields.ptrAux = p.ptrAux
|
|
1118
|
+
updateRep(p.name, fields)
|
|
1119
|
+
}
|
|
1120
|
+
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
1121
|
+
// Schema bindings additionally write into ctx.schema.vars so prop-access dispatch
|
|
1122
|
+
// hits the slot map. ctx.schema.vars is saved/restored so bindings don't leak.
|
|
1123
|
+
const schemaVarsPrev = new Map(ctx.schema.vars)
|
|
1124
|
+
if (_reps) {
|
|
1125
|
+
for (const [k, r] of _reps) {
|
|
1126
|
+
if (k >= sig.params.length) continue
|
|
1127
|
+
const pname = sig.params[k].name
|
|
1128
|
+
if (r.val && !ctx.func.repByLocal?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
1129
|
+
if (r.typedCtor) {
|
|
1130
|
+
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
1131
|
+
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
1132
|
+
if (!ctx.func.repByLocal?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
1133
|
+
}
|
|
1134
|
+
if (r.schemaId != null && !exported && !ctx.schema.vars.has(pname)) {
|
|
1135
|
+
ctx.schema.vars.set(pname, r.schemaId)
|
|
1136
|
+
updateRep(pname, { schemaId: r.schemaId })
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const fn = ['func', `$${name}`]
|
|
1142
|
+
// Boundary-wrapped exports defer the export attribute to a synthesized
|
|
1143
|
+
// wrapper ($${name}$exp) that reboxes the narrowed result back to f64.
|
|
1144
|
+
if (exported && !isBoundaryWrapped(func)) fn.push(['export', `"${name}"`])
|
|
1145
|
+
fn.push(...sig.params.map(p => ['param', `$${p.name}`, p.type]))
|
|
1146
|
+
fn.push(...sig.results.map(t => ['result', t]))
|
|
1147
|
+
|
|
1148
|
+
// Default params: missing JS args become canonical NaN (0x7FF8000000000000) in WASM f64 params.
|
|
1149
|
+
// Check for canonical NaN specifically — NaN-boxed pointers are also NaN but have non-zero payload.
|
|
1150
|
+
const defaults = func.defaults || {}
|
|
1151
|
+
const defaultInits = []
|
|
1152
|
+
for (const [pname, defVal] of Object.entries(defaults)) {
|
|
1153
|
+
const p = sig.params.find(p => p.name === pname)
|
|
1154
|
+
const t = p?.type || 'f64'
|
|
1155
|
+
defaultInits.push(
|
|
1156
|
+
['if', isNullish(typed(['local.get', `$${pname}`], 'f64')),
|
|
1157
|
+
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emit(defVal)) : asI32(emit(defVal))]]])
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Box params that are mutably captured: allocate cell, copy param value
|
|
1161
|
+
const boxedParamInits = []
|
|
1162
|
+
for (const p of sig.params) {
|
|
1163
|
+
if (ctx.func.boxed.has(p.name)) {
|
|
1164
|
+
const cell = ctx.func.boxed.get(p.name)
|
|
1165
|
+
ctx.func.locals.set(cell, 'i32')
|
|
1166
|
+
const lget = typed(['local.get', `$${p.name}`], p.type)
|
|
1167
|
+
if (p.ptrKind != null) lget.ptrKind = p.ptrKind
|
|
1168
|
+
boxedParamInits.push(
|
|
1169
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
1170
|
+
['f64.store', ['local.get', `$${cell}`], asF64(lget)])
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (block) {
|
|
1175
|
+
const stmts = emitBody(body)
|
|
1176
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1177
|
+
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
1178
|
+
const lastStmt = stmts.at(-1)
|
|
1179
|
+
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
1180
|
+
fn.push(...defaultInits, ...boxedParamInits, ...stmts, ...(endsWithReturn ? [] : sig.results.map(t => [`${t}.const`, 0])))
|
|
1181
|
+
} else if (multi && body[0] === '[') {
|
|
1182
|
+
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
1183
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1184
|
+
fn.push(...boxedParamInits, ...values)
|
|
1185
|
+
} else {
|
|
1186
|
+
const ir = emit(body)
|
|
1187
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1188
|
+
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
1189
|
+
fn.push(...defaultInits, ...boxedParamInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Restore schema.vars so param bindings don't leak to next function.
|
|
1193
|
+
ctx.schema.vars = schemaVarsPrev
|
|
1194
|
+
return fn
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Phase: synthesize JS-boundary wrappers for narrowed exports.
|
|
1199
|
+
*
|
|
1200
|
+
* For each `isBoundaryWrapped(func)`, emit a sibling `$${name}$exp` that:
|
|
1201
|
+
* - holds the (export "name") attribute (JS sees the wrapper)
|
|
1202
|
+
* - takes f64 params always (JS calling convention via host.js wrap)
|
|
1203
|
+
* - converts each narrowed param at the call: f64 → i32 (truncate-sat) for
|
|
1204
|
+
* numeric narrowed, f64 → i32-offset (`i32.wrap_i64 + i64.reinterpret_f64`)
|
|
1205
|
+
* for pointer narrowed
|
|
1206
|
+
* - forwards args to the inner $${name}
|
|
1207
|
+
* - reboxes the narrowed result back to f64 so JS sees Number / NaN-boxed ptr
|
|
1208
|
+
*
|
|
1209
|
+
* Param convert cases (each narrowed inner-param):
|
|
1210
|
+
* - p.type = 'i32', no ptrKind → i32.trunc_sat_f64_s(local.get $p)
|
|
1211
|
+
* - p.type = 'i32', ptrKind set → i32.wrap_i64(i64.reinterpret_f64(local.get $p))
|
|
1212
|
+
*
|
|
1213
|
+
* Result rebox cases:
|
|
1214
|
+
* - sig.ptrKind != null → mkPtrIR(ptrKind, ptrAux ?? 0, callIR)
|
|
1215
|
+
* - sig.results[0] = i32 → f64.convert_i32_s(callIR)
|
|
1216
|
+
* - sig.results[0] = f64 → callIR (some params narrowed but result stayed f64)
|
|
1217
|
+
*/
|
|
1218
|
+
function synthesizeBoundaryWrappers() {
|
|
1219
|
+
const wrappers = []
|
|
1220
|
+
for (const func of ctx.func.list) {
|
|
1221
|
+
if (!isBoundaryWrapped(func)) continue
|
|
1222
|
+
const { name, sig } = func
|
|
1223
|
+
const wrapNode = ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
1224
|
+
// External ABI: every param is f64 (JS Number / NaN-boxed ptr).
|
|
1225
|
+
for (const p of sig.params) wrapNode.push(['param', `$${p.name}`, 'f64'])
|
|
1226
|
+
wrapNode.push(['result', 'f64'])
|
|
1227
|
+
const args = sig.params.map(p => {
|
|
1228
|
+
const get = ['local.get', `$${p.name}`]
|
|
1229
|
+
if (p.type === 'f64') return get
|
|
1230
|
+
if (p.ptrKind != null) {
|
|
1231
|
+
// NaN-boxed f64 → raw i32 offset
|
|
1232
|
+
return ['i32.wrap_i64', ['i64.reinterpret_f64', get]]
|
|
1233
|
+
}
|
|
1234
|
+
// Numeric i32 — JS Number → i32 truncation (matches `n | 0` for integers).
|
|
1235
|
+
return ['i32.trunc_sat_f64_s', get]
|
|
1236
|
+
})
|
|
1237
|
+
const callIR = ['call', `$${name}`, ...args]
|
|
1238
|
+
let body
|
|
1239
|
+
if (sig.ptrKind != null) {
|
|
1240
|
+
const ptrType = valKindToPtr(sig.ptrKind)
|
|
1241
|
+
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
1242
|
+
} else if (sig.results[0] === 'i32') {
|
|
1243
|
+
body = ['f64.convert_i32_s', callIR]
|
|
1244
|
+
} else {
|
|
1245
|
+
body = callIR
|
|
1246
|
+
}
|
|
1247
|
+
wrapNode.push(body)
|
|
1248
|
+
wrappers.push(wrapNode)
|
|
1249
|
+
}
|
|
1250
|
+
return wrappers
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Phase: emit one closure body to WAT IR.
|
|
1256
|
+
*
|
|
1257
|
+
* Closures share a uniform signature (env f64, argc i32, a0..a{W-1} f64) → f64
|
|
1258
|
+
* so any closure can be invoked via call_indirect on $ftN. This function
|
|
1259
|
+
* builds one body fn given the body record (cb) created by ctx.closure.make.
|
|
1260
|
+
*
|
|
1261
|
+
* Mutates ctx.func.* per-body state (locals, boxed, repByLocal) and
|
|
1262
|
+
* ctx.schema.vars / ctx.types.typedElem (restored on exit so capture-binding
|
|
1263
|
+
* leaks don't poison the next body). Returns the WAT IR for the func node.
|
|
1264
|
+
*/
|
|
1265
|
+
function emitClosureBody(cb) {
|
|
1266
|
+
const prevSchemaVars = ctx.schema.vars
|
|
1267
|
+
const prevTypedElems = ctx.types.typedElem
|
|
1268
|
+
// Reset per-function state for closure body
|
|
1269
|
+
ctx.func.locals = new Map()
|
|
1270
|
+
ctx.func.repByLocal = null
|
|
1271
|
+
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
1272
|
+
if (cb.schemaVars) {
|
|
1273
|
+
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
1274
|
+
for (const [name, sid] of cb.schemaVars) updateRep(name, { schemaId: sid })
|
|
1275
|
+
}
|
|
1276
|
+
const globalTE = ctx.scope.globalTypedElem
|
|
1277
|
+
if (cb.typedElems) {
|
|
1278
|
+
ctx.types.typedElem = globalTE ? new Map([...globalTE, ...cb.typedElems]) : new Map(cb.typedElems)
|
|
1279
|
+
} else if (globalTE) {
|
|
1280
|
+
ctx.types.typedElem = new Map(globalTE)
|
|
1281
|
+
} else {
|
|
1282
|
+
ctx.types.typedElem = prevTypedElems
|
|
1283
|
+
}
|
|
1284
|
+
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
1285
|
+
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
1286
|
+
ctx.func.stack = []
|
|
1287
|
+
ctx.func.uniq = Math.max(ctx.func.uniq, 100) // avoid label collisions
|
|
1288
|
+
ctx.func.body = cb.body
|
|
1289
|
+
// Seed direct-call dispatch for captured const-bound closures (A3 across capture boundary).
|
|
1290
|
+
// closure.make snapshotted the parent's directClosures for each capture; here we restore
|
|
1291
|
+
// them so calls to a captured `peek` lower to `call $closureN` instead of call_indirect.
|
|
1292
|
+
ctx.func.directClosures = cb.directClosures ? new Map(cb.directClosures) : null
|
|
1293
|
+
// Uniform convention: (env f64, argc i32, a0..a{width-1} f64) → f64
|
|
1294
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
1295
|
+
const paramDecls = [{ name: '__env', type: 'f64' }, { name: '__argc', type: 'i32' }]
|
|
1296
|
+
for (let i = 0; i < W; i++) paramDecls.push({ name: `__a${i}`, type: 'f64' })
|
|
1297
|
+
ctx.func.current = { params: paramDecls, results: ['f64'] }
|
|
1298
|
+
|
|
1299
|
+
const fn = ['func', `$${cb.name}`]
|
|
1300
|
+
fn.push(['param', '$__env', 'f64'])
|
|
1301
|
+
fn.push(['param', '$__argc', 'i32'])
|
|
1302
|
+
for (let i = 0; i < W; i++) fn.push(['param', `$__a${i}`, 'f64'])
|
|
1303
|
+
fn.push(['result', 'f64'])
|
|
1304
|
+
|
|
1305
|
+
// Params are locals, assigned directly from inline slots
|
|
1306
|
+
for (const p of cb.params) ctx.func.locals.set(p, 'f64')
|
|
1307
|
+
|
|
1308
|
+
// Register captured variable locals: boxed = i32 cell pointer, otherwise f64 value
|
|
1309
|
+
for (let i = 0; i < cb.captures.length; i++) {
|
|
1310
|
+
const name = cb.captures[i]
|
|
1311
|
+
ctx.func.locals.set(name, ctx.func.boxed.has(name) ? 'i32' : 'f64')
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Emit body
|
|
1315
|
+
const block = Array.isArray(cb.body) && cb.body[0] === '{}' && cb.body[1]?.[0] !== ':'
|
|
1316
|
+
let bodyIR
|
|
1317
|
+
if (block) {
|
|
1318
|
+
for (const [k, v] of analyzeLocals(cb.body)) if (!ctx.func.locals.has(k)) ctx.func.locals.set(k, v)
|
|
1319
|
+
// Detect captures from deeper nested arrows that mutate this body's locals/params/captures
|
|
1320
|
+
analyzeBoxedCaptures(cb.body)
|
|
1321
|
+
for (const name of ctx.func.boxed.keys()) {
|
|
1322
|
+
if (ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
1323
|
+
}
|
|
1324
|
+
bodyIR = emitBody(cb.body)
|
|
1325
|
+
} else {
|
|
1326
|
+
bodyIR = [asF64(emit(cb.body))]
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Pre-allocate cache locals for env unpacking
|
|
1330
|
+
const envBase = cb.captures.length > 0 ? `${T}envBase${ctx.func.uniq++}` : null
|
|
1331
|
+
if (envBase) ctx.func.locals.set(envBase, 'i32')
|
|
1332
|
+
// Rest param: allocate helper locals (len + offset) before emitting decls
|
|
1333
|
+
let restOff, restLen
|
|
1334
|
+
if (cb.rest) {
|
|
1335
|
+
restOff = `${T}restOff${ctx.func.uniq++}`
|
|
1336
|
+
restLen = `${T}restLen${ctx.func.uniq++}`
|
|
1337
|
+
ctx.func.locals.set(restOff, 'i32')
|
|
1338
|
+
ctx.func.locals.set(restLen, 'i32')
|
|
1339
|
+
inc('__alloc_hdr', '__mkptr')
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Insert locals (captures + params + declared)
|
|
1343
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1344
|
+
|
|
1345
|
+
// Load captures from env: boxed → i32.load (raw cell pointer), immutable → f64.load value.
|
|
1346
|
+
// env is the CLOSURE pointer (PTR.CLOSURE) — never an ARRAY, no forwarding chain.
|
|
1347
|
+
// Inline the offset extraction (low 32 bits) instead of calling __ptr_offset per invocation.
|
|
1348
|
+
if (envBase) {
|
|
1349
|
+
fn.push(['local.set', `$${envBase}`,
|
|
1350
|
+
['i32.wrap_i64', ['i64.reinterpret_f64', ['local.get', '$__env']]]])
|
|
1351
|
+
for (let i = 0; i < cb.captures.length; i++) {
|
|
1352
|
+
const name = cb.captures[i]
|
|
1353
|
+
const addr = ['i32.add', ['local.get', `$${envBase}`], ['i32.const', i * 8]]
|
|
1354
|
+
fn.push(['local.set', `$${name}`,
|
|
1355
|
+
ctx.func.boxed.has(name) ? ['i32.load', addr] : ['f64.load', addr]])
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Unpack fixed params directly from inline slots (caller padded missing with UNDEF_NAN).
|
|
1360
|
+
// Rest name (if present) is last in cb.params — handled separately below.
|
|
1361
|
+
const fixedParamN = cb.params.length - (cb.rest ? 1 : 0)
|
|
1362
|
+
for (let i = 0; i < fixedParamN && i < W; i++) {
|
|
1363
|
+
fn.push(['local.set', `$${cb.params[i]}`, ['local.get', `$__a${i}`]])
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Rest param: pack slots a[fixedParams..argc-1] into fresh array.
|
|
1367
|
+
// len = clamp(argc - fixedParams, 0, restSlots). Rest-param closures receive
|
|
1368
|
+
// at most (width - fixedParams) rest args — spread callers with
|
|
1369
|
+
// more dynamic elements lose the overflow (documented limitation).
|
|
1370
|
+
if (cb.rest) {
|
|
1371
|
+
const fixedN = fixedParamN
|
|
1372
|
+
const restSlots = W - fixedN
|
|
1373
|
+
fn.push(['local.set', `$${restLen}`,
|
|
1374
|
+
['select',
|
|
1375
|
+
['i32.sub', ['local.get', '$__argc'], ['i32.const', fixedN]],
|
|
1376
|
+
['i32.const', 0],
|
|
1377
|
+
['i32.gt_s', ['local.get', '$__argc'], ['i32.const', fixedN]]]])
|
|
1378
|
+
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', restSlots]],
|
|
1379
|
+
['then', ['local.set', `$${restLen}`, ['i32.const', restSlots]]]])
|
|
1380
|
+
fn.push(['local.set', `$${restOff}`,
|
|
1381
|
+
['call', '$__alloc_hdr',
|
|
1382
|
+
['local.get', `$${restLen}`], ['local.get', `$${restLen}`], ['i32.const', 8]]])
|
|
1383
|
+
for (let i = 0; i < restSlots; i++) {
|
|
1384
|
+
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', i]],
|
|
1385
|
+
['then', ['f64.store',
|
|
1386
|
+
['i32.add', ['local.get', `$${restOff}`], ['i32.const', i * 8]],
|
|
1387
|
+
['local.get', `$__a${fixedN + i}`]]]])
|
|
1388
|
+
}
|
|
1389
|
+
fn.push(['local.set', `$${cb.rest}`,
|
|
1390
|
+
['call', '$__mkptr', ['i32.const', PTR.ARRAY], ['i32.const', 0], ['local.get', `$${restOff}`]]])
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Default params for closures (check sentinel after unpack)
|
|
1394
|
+
if (cb.defaults) {
|
|
1395
|
+
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
1396
|
+
fn.push(['if', isNullish(['local.get', `$${pname}`]),
|
|
1397
|
+
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
fn.push(...bodyIR)
|
|
1401
|
+
// I: Skip trailing fallback when last statement is return
|
|
1402
|
+
if (block && !(bodyIR.at(-1)?.[0] === 'return' || bodyIR.at(-1)?.[0] === 'return_call')) fn.push(['f64.const', 0])
|
|
1403
|
+
ctx.schema.vars = prevSchemaVars
|
|
1404
|
+
ctx.types.typedElem = prevTypedElems
|
|
1405
|
+
return fn
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Phase: build module-init function `__start`.
|
|
1410
|
+
*
|
|
1411
|
+
* `__start` is the WebAssembly start function: runs once at instantiation, after
|
|
1412
|
+
* imports/globals are bound but before any export is called. It threads together
|
|
1413
|
+
* everything that must happen before user code observes a ready module:
|
|
1414
|
+
*
|
|
1415
|
+
* 1. Reset per-function emit state (locals/repByLocal/boxed/stack) — __start is
|
|
1416
|
+
* a fresh function context with no params.
|
|
1417
|
+
* 2. analyzeValTypes(ast) so emit sees correct ptrKind on top-level decls.
|
|
1418
|
+
* 3. Sub-module init (foreign module bootstrap) emits first — its globals
|
|
1419
|
+
* must be assigned before main-module code reads them.
|
|
1420
|
+
* 4. emit(ast) — user top-level statements (let/const, call expressions, …).
|
|
1421
|
+
* 5. boxInit — auto-boxing globals (vars with prop assignments lifted to OBJECT).
|
|
1422
|
+
* 6. schemaInit — runtime schema-name table for JSON.stringify.
|
|
1423
|
+
* 7. strPoolInit — copy passive string-pool segment to heap (shared memory).
|
|
1424
|
+
* 8. typeofInit — preallocate typeof-result string globals.
|
|
1425
|
+
*
|
|
1426
|
+
* Order in the assembled body: strPool → typeof → box → schema → moduleInits → init.
|
|
1427
|
+
*
|
|
1428
|
+
* Late closures (those compiled during __start emit, e.g. arrows declared at
|
|
1429
|
+
* module scope) are flushed via `compilePendingClosures` and prepended to
|
|
1430
|
+
* `sec.funcs` so closure indices stay stable across the table.
|
|
1431
|
+
*/
|
|
1432
|
+
function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
1433
|
+
ctx.func.locals = new Map()
|
|
1434
|
+
ctx.func.repByLocal = null
|
|
1435
|
+
ctx.func.boxed = new Map()
|
|
1436
|
+
ctx.func.stack = []
|
|
1437
|
+
ctx.func.current = { params: [], results: [] }
|
|
1438
|
+
analyzeValTypes(ast)
|
|
1439
|
+
const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
|
|
1440
|
+
|
|
1441
|
+
const moduleInits = []
|
|
1442
|
+
if (ctx.module.moduleInits) {
|
|
1443
|
+
for (const mi of ctx.module.moduleInits) {
|
|
1444
|
+
analyzeValTypes(mi)
|
|
1445
|
+
moduleInits.push(...normalizeIR(emit(mi)))
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const init = emit(ast)
|
|
1449
|
+
|
|
1450
|
+
const boxInit = []
|
|
1451
|
+
if (ctx.schema.autoBox) {
|
|
1452
|
+
const bt = `${T}box`
|
|
1453
|
+
ctx.func.locals.set(bt, 'i32')
|
|
1454
|
+
for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
|
|
1455
|
+
inc('__alloc', '__mkptr')
|
|
1456
|
+
boxInit.push(
|
|
1457
|
+
['local.set', `$${bt}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]],
|
|
1458
|
+
['f64.store', ['local.get', `$${bt}`],
|
|
1459
|
+
ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
|
|
1460
|
+
...schema.slice(1).map((_, i) =>
|
|
1461
|
+
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (i + 1) * 8]], ['f64.const', 0]]),
|
|
1462
|
+
['global.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const schemaInit = []
|
|
1467
|
+
// Schema name table is needed by JSON.stringify (legacy), and by __dyn_get's
|
|
1468
|
+
// OBJECT-schema fallback for polymorphic-receiver `.prop` access. Lift the
|
|
1469
|
+
// gate to also populate when any __dyn_get* family helper is included so
|
|
1470
|
+
// polymorphic OBJECT patterns (mismatched-schema `?:`, unknown-schema
|
|
1471
|
+
// params) resolve via runtime aux→sid lookup. Direct dependents of
|
|
1472
|
+
// __dyn_get (set transitively by resolveIncludes() later) are listed
|
|
1473
|
+
// explicitly here because the dep graph hasn't been expanded yet at
|
|
1474
|
+
// start-fn build time.
|
|
1475
|
+
const needsSchemaTbl = ctx.schema.list.length && (
|
|
1476
|
+
ctx.core.includes.has('__stringify') ||
|
|
1477
|
+
ctx.core.includes.has('__dyn_get') ||
|
|
1478
|
+
ctx.core.includes.has('__dyn_get_t') ||
|
|
1479
|
+
ctx.core.includes.has('__dyn_get_any') ||
|
|
1480
|
+
ctx.core.includes.has('__dyn_get_any_t') ||
|
|
1481
|
+
ctx.core.includes.has('__dyn_get_expr') ||
|
|
1482
|
+
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
1483
|
+
ctx.core.includes.has('__dyn_get_or'))
|
|
1484
|
+
if (needsSchemaTbl) {
|
|
1485
|
+
const nSchemas = ctx.schema.list.length
|
|
1486
|
+
const stbl = `${T}stbl`
|
|
1487
|
+
const sarr = `${T}sarr`
|
|
1488
|
+
ctx.func.locals.set(stbl, 'i32')
|
|
1489
|
+
ctx.func.locals.set(sarr, 'i32')
|
|
1490
|
+
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
1491
|
+
schemaInit.push(
|
|
1492
|
+
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', nSchemas * 8]]],
|
|
1493
|
+
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
1494
|
+
for (let s = 0; s < nSchemas; s++) {
|
|
1495
|
+
const keys = ctx.schema.list[s]
|
|
1496
|
+
const n = keys.length
|
|
1497
|
+
schemaInit.push(
|
|
1498
|
+
['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
|
|
1499
|
+
for (let k = 0; k < n; k++)
|
|
1500
|
+
schemaInit.push(
|
|
1501
|
+
['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
|
|
1502
|
+
emit(['str', String(keys[k])])])
|
|
1503
|
+
schemaInit.push(
|
|
1504
|
+
['f64.store', ['i32.add', ['local.get', `$${stbl}`], ['i32.const', s * 8]],
|
|
1505
|
+
mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${sarr}`])])
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const strPoolInit = []
|
|
1510
|
+
if (ctx.runtime.strPool) {
|
|
1511
|
+
const total = ctx.runtime.strPool.length
|
|
1512
|
+
strPoolInit.push(
|
|
1513
|
+
['global.set', '$__strBase', ['call', '$__alloc', ['i32.const', total]]],
|
|
1514
|
+
['memory.init', '$__strPool', ['global.get', '$__strBase'], ['i32.const', 0], ['i32.const', total]],
|
|
1515
|
+
['data.drop', '$__strPool'],
|
|
1516
|
+
)
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const typeofInit = []
|
|
1520
|
+
if (ctx.runtime.typeofStrs) {
|
|
1521
|
+
for (const s of ctx.runtime.typeofStrs)
|
|
1522
|
+
typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
|
|
1523
|
+
}
|
|
1524
|
+
if (moduleInits.length || init?.length || boxInit.length || schemaInit.length || typeofInit.length || strPoolInit.length || ctx.features.timers) {
|
|
1525
|
+
const initIR = normalizeIR(init)
|
|
1526
|
+
const startFn = ['func', '$__start']
|
|
1527
|
+
for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
|
|
1528
|
+
startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
|
|
1529
|
+
...(ctx.features.timers ? [['call', '$__timer_init']] : []),
|
|
1530
|
+
...moduleInits, ...initIR,
|
|
1531
|
+
...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
|
|
1532
|
+
)
|
|
1533
|
+
sec.start.push(startFn, ['start', '$__start'])
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const beforeLen = closureFuncs.length
|
|
1537
|
+
compilePendingClosures()
|
|
1538
|
+
if (closureFuncs.length > beforeLen)
|
|
1539
|
+
sec.funcs.unshift(...closureFuncs.slice(beforeLen))
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Phase: closure-body dedup.
|
|
1544
|
+
*
|
|
1545
|
+
* Two closures with structurally-equal bodies (same shape after alpha-renaming
|
|
1546
|
+
* locals/params) are emitted as a single function — duplicates redirect through
|
|
1547
|
+
* the elem table to the canonical name. Closure bodies often share shape because
|
|
1548
|
+
* the same inner arrow can be instantiated in many places (e.g. parser combinators).
|
|
1549
|
+
*
|
|
1550
|
+
* IN: closureFuncs (the WAT IR list emitted by emitClosureBody),
|
|
1551
|
+
* sec.funcs (already contains closureFuncs + regular funcs),
|
|
1552
|
+
* ctx.closure.table (elem-section names).
|
|
1553
|
+
* OUT: sec.funcs filtered to canonical bodies, ctx.closure.table redirected.
|
|
1554
|
+
*
|
|
1555
|
+
* Runs AFTER all closures (including those compiled during __start emit) are
|
|
1556
|
+
* collected so structural duplicates across batches collapse together.
|
|
1557
|
+
*/
|
|
1558
|
+
function dedupClosureBodies(closureFuncs, sec) {
|
|
1559
|
+
if (closureFuncs.length <= 1) return
|
|
1560
|
+
const canonicalize = (fn) => {
|
|
1561
|
+
const localNames = new Set()
|
|
1562
|
+
const collect = (node) => {
|
|
1563
|
+
if (!Array.isArray(node)) return
|
|
1564
|
+
if ((node[0] === 'local' || node[0] === 'param') && typeof node[1] === 'string' && node[1][0] === '$')
|
|
1565
|
+
localNames.add(node[1])
|
|
1566
|
+
for (const c of node) collect(c)
|
|
1567
|
+
}
|
|
1568
|
+
collect(fn)
|
|
1569
|
+
let counter = 0
|
|
1570
|
+
const renameMap = new Map()
|
|
1571
|
+
const walk = node => {
|
|
1572
|
+
if (typeof node === 'string') {
|
|
1573
|
+
if (!localNames.has(node)) return node
|
|
1574
|
+
let r = renameMap.get(node)
|
|
1575
|
+
if (!r) { r = `$_c${counter++}`; renameMap.set(node, r) }
|
|
1576
|
+
return r
|
|
1577
|
+
}
|
|
1578
|
+
if (!Array.isArray(node)) return node
|
|
1579
|
+
return node.map(walk)
|
|
1580
|
+
}
|
|
1581
|
+
return JSON.stringify(['func', ...fn.slice(2).map(walk)])
|
|
1582
|
+
}
|
|
1583
|
+
const hashToName = new Map()
|
|
1584
|
+
const redirect = new Map()
|
|
1585
|
+
const keepSet = new Set()
|
|
1586
|
+
for (const fn of closureFuncs) {
|
|
1587
|
+
const key = canonicalize(fn)
|
|
1588
|
+
const name = fn[1].slice(1)
|
|
1589
|
+
const canonical = hashToName.get(key)
|
|
1590
|
+
if (canonical) redirect.set(name, canonical)
|
|
1591
|
+
else { hashToName.set(key, name); keepSet.add(name) }
|
|
1592
|
+
}
|
|
1593
|
+
if (!redirect.size) return
|
|
1594
|
+
ctx.closure.table = ctx.closure.table.map(n => redirect.get(n) || n)
|
|
1595
|
+
const kept = sec.funcs.filter(fn => {
|
|
1596
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return true
|
|
1597
|
+
const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
1598
|
+
return !name || !redirect.has(name)
|
|
1599
|
+
})
|
|
1600
|
+
sec.funcs.length = 0
|
|
1601
|
+
sec.funcs.push(...kept)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Phase: closure-table finalize + ABI shrink.
|
|
1606
|
+
*
|
|
1607
|
+
* Two opportunities, both gated on a post-emit scan for `call_indirect`:
|
|
1608
|
+
*
|
|
1609
|
+
* 1. Drop dead $ftN type / table / elem when the scan finds zero call_indirect
|
|
1610
|
+
* sites (every closure call was direct-dispatched via A3 + capture-boundary
|
|
1611
|
+
* propagation, AND no top-level fn was taken as a value). Closure pointers
|
|
1612
|
+
* still carry funcIdx in their NaN-box aux bits, but those bits become dead
|
|
1613
|
+
* state with no reader.
|
|
1614
|
+
*
|
|
1615
|
+
* 2. Per-body ABI shrink: with no call_indirect, every closure is direct-only,
|
|
1616
|
+
* so the uniform `(env, argc, a0..a{W-1})` ABI is no longer required.
|
|
1617
|
+
* Each body sheds:
|
|
1618
|
+
* • $__env when captures.length === 0
|
|
1619
|
+
* • $__argc when no rest param (defaults check param value, not argc)
|
|
1620
|
+
* • $__a{i} for i ≥ fixedN when no rest (caller's UNDEF padding is dead)
|
|
1621
|
+
* Rest closures keep all W slots — argc + slot{fixedN..W-1} are how rest packs.
|
|
1622
|
+
* Both `call` and `return_call` (tail call) sites are rewritten in the same walk.
|
|
1623
|
+
*/
|
|
1624
|
+
function finalizeClosureTable(sec) {
|
|
1625
|
+
if (!ctx.closure.table?.length) return
|
|
1626
|
+
let indirectUsed = false
|
|
1627
|
+
const scan = (n) => {
|
|
1628
|
+
if (!Array.isArray(n) || indirectUsed) return
|
|
1629
|
+
if (n[0] === 'call_indirect') { indirectUsed = true; return }
|
|
1630
|
+
for (const c of n) if (Array.isArray(c)) scan(c)
|
|
1631
|
+
}
|
|
1632
|
+
for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
|
|
1633
|
+
if (!indirectUsed) for (const fn of sec.start) scan(fn)
|
|
1634
|
+
// Also scan raw stdlib strings (pullStdlib hasn't run yet, so stdlib funcs aren't in sec.funcs)
|
|
1635
|
+
if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
|
|
1636
|
+
if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
|
|
1637
|
+
}
|
|
1638
|
+
// Keep table if call_indirect is used (closures, timer dispatch, etc.)
|
|
1639
|
+
if (indirectUsed) {
|
|
1640
|
+
sec.table = [['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref']]
|
|
1641
|
+
sec.elem = [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]]
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
sec.table = []
|
|
1645
|
+
sec.elem = []
|
|
1646
|
+
sec.types = sec.types.filter(t => !(Array.isArray(t) && t[1] === '$ftN'))
|
|
1647
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
1648
|
+
const abiOf = new Map()
|
|
1649
|
+
for (const cb of (ctx.closure.bodies || [])) {
|
|
1650
|
+
const fixedN = cb.params.length - (cb.rest ? 1 : 0)
|
|
1651
|
+
abiOf.set(cb.name, {
|
|
1652
|
+
needEnv: cb.captures.length > 0,
|
|
1653
|
+
needArgc: !!cb.rest,
|
|
1654
|
+
usedSlots: cb.rest ? W : fixedN,
|
|
1655
|
+
rest: !!cb.rest,
|
|
1656
|
+
})
|
|
1657
|
+
}
|
|
1658
|
+
for (const fn of sec.funcs) {
|
|
1659
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') continue
|
|
1660
|
+
const fnName = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
1661
|
+
const abi = abiOf.get(fnName)
|
|
1662
|
+
if (!abi) continue
|
|
1663
|
+
for (let i = fn.length - 1; i >= 0; i--) {
|
|
1664
|
+
const node = fn[i]
|
|
1665
|
+
if (!Array.isArray(node) || node[0] !== 'param') continue
|
|
1666
|
+
const pname = node[1]
|
|
1667
|
+
if (pname === '$__env' && !abi.needEnv) fn.splice(i, 1)
|
|
1668
|
+
else if (pname === '$__argc' && !abi.needArgc) fn.splice(i, 1)
|
|
1669
|
+
else if (typeof pname === 'string' && pname.startsWith('$__a') && !abi.rest) {
|
|
1670
|
+
const idx = parseInt(pname.slice(4), 10)
|
|
1671
|
+
if (Number.isFinite(idx) && idx >= abi.usedSlots) fn.splice(i, 1)
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const rewriteCalls = (node) => {
|
|
1676
|
+
if (!Array.isArray(node)) return
|
|
1677
|
+
for (const c of node) if (Array.isArray(c)) rewriteCalls(c)
|
|
1678
|
+
if ((node[0] === 'call' || node[0] === 'return_call') && typeof node[1] === 'string') {
|
|
1679
|
+
const callee = node[1].slice(1)
|
|
1680
|
+
const abi = abiOf.get(callee)
|
|
1681
|
+
if (!abi) return
|
|
1682
|
+
const newArgs = []
|
|
1683
|
+
if (abi.needEnv) newArgs.push(node[2])
|
|
1684
|
+
if (abi.needArgc) newArgs.push(node[3])
|
|
1685
|
+
for (let i = 0; i < abi.usedSlots; i++) newArgs.push(node[4 + i])
|
|
1686
|
+
node.splice(2, node.length - 2, ...newArgs)
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
for (const fn of sec.funcs) rewriteCalls(fn)
|
|
1690
|
+
for (const fn of sec.start) rewriteCalls(fn)
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Phase: pull stdlib + memory.
|
|
1695
|
+
*
|
|
1696
|
+
* Runs AFTER __start is built — emit calls during __start (e.g. typeofStrs,
|
|
1697
|
+
* boxInit, schemaInit) trigger `inc()` for any helpers they need, and those
|
|
1698
|
+
* additions must be observed before resolveIncludes() expands the dependency
|
|
1699
|
+
* closure.
|
|
1700
|
+
*
|
|
1701
|
+
* Steps:
|
|
1702
|
+
* 1. resolveIncludes() — close the include set under stdlib dependencies.
|
|
1703
|
+
* 2. Emit memory section ONLY when some included helper uses memory ops
|
|
1704
|
+
* (G optimization: pure scalar programs ship without memory + __heap).
|
|
1705
|
+
* When memory is needed, the allocator (__alloc + __alloc_hdr + __reset)
|
|
1706
|
+
* is force-included since stdlib funcs may call into it.
|
|
1707
|
+
* 3. Pull external (host) stdlibs into sec.extStdlib (must precede normal
|
|
1708
|
+
* imports in the module byte order).
|
|
1709
|
+
* 4. Pull resolved factory stdlibs (those keyed by feature gates) into
|
|
1710
|
+
* sec.stdlib via parseWat.
|
|
1711
|
+
*
|
|
1712
|
+
* Also reports any unresolved stdlib name (logged, not thrown — keeps test
|
|
1713
|
+
* output readable when a missing helper is the actual bug).
|
|
1714
|
+
*/
|
|
1715
|
+
function pullStdlib(sec) {
|
|
1716
|
+
resolveIncludes()
|
|
1717
|
+
|
|
1718
|
+
const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
|
|
1719
|
+
if (!needsMemory) ctx.scope.globals.delete('__heap')
|
|
1720
|
+
if (needsMemory && ctx.module.modules.core) {
|
|
1721
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__reset']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
|
|
1722
|
+
const pages = ctx.memory.pages || 1
|
|
1723
|
+
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
|
|
1724
|
+
else sec.memory.push(['memory', ['export', '"memory"'], pages])
|
|
1725
|
+
if (ctx.transform.runtimeExports !== false && ctx.core._allocRawFuncs)
|
|
1726
|
+
sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const stdlibStr = (name) => {
|
|
1730
|
+
const v = ctx.core.stdlib[name]
|
|
1731
|
+
return typeof v === 'function' ? v() : v
|
|
1732
|
+
}
|
|
1733
|
+
for (const name of Object.keys(ctx.core.stdlib)) {
|
|
1734
|
+
if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
|
|
1735
|
+
const parsed = parseWat(stdlibStr(name))
|
|
1736
|
+
sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
|
|
1737
|
+
ctx.core.includes.delete(name)
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
|
|
1741
|
+
sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Phase: whole-module + per-function optimization passes.
|
|
1746
|
+
*
|
|
1747
|
+
* Order matters and is non-obvious — fixed deliberately:
|
|
1748
|
+
*
|
|
1749
|
+
* 1. specializeMkptr — replaces `call $__mkptr (T, A, off)` with `$__mkptr_T_A_d`
|
|
1750
|
+
* for known (T, A) pairs (saves ~4 B/site). Must run BEFORE per-function
|
|
1751
|
+
* passes so the new variants exist when fusedRewrite folds calls into them.
|
|
1752
|
+
* 2. specializePtrBase — folds `call F (add (global G) const)` to a `_p`
|
|
1753
|
+
* variant (saves ~3 B/site). After specializeMkptr so mkptr variants
|
|
1754
|
+
* ($__mkptr_T_A_d) are visible to it.
|
|
1755
|
+
* 3. sortStrPoolByFreq — reorders string-pool entries so hot strings get low
|
|
1756
|
+
* offsets (shrinking i32.const LEB128). Shared-memory only (passive segment).
|
|
1757
|
+
* 4. optimizeFunc per fn — hoistPtrType + fusedRewrite + sortLocalsByUse.
|
|
1758
|
+
* Must run after specializeMkptr/specializePtrBase introduce new helpers.
|
|
1759
|
+
* 5. hoistConstantPool — repeated f64 literals → mutable globals.
|
|
1760
|
+
* Last because earlier passes might fold/eliminate constants.
|
|
1761
|
+
*
|
|
1762
|
+
* Also adjusts $__heap base when data segment exceeds 1024 bytes (default
|
|
1763
|
+
* heap base) — keeps user code at offset 0 from clobbering the data segment.
|
|
1764
|
+
*/
|
|
1765
|
+
function optimizeModule(sec) {
|
|
1766
|
+
const cfg = ctx.transform.optimize // null → all on (back-compat for direct compile() callers)
|
|
1767
|
+
if (!cfg || cfg.specializeMkptr !== false)
|
|
1768
|
+
specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
1769
|
+
if (!cfg || cfg.specializePtrBase !== false)
|
|
1770
|
+
specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
1771
|
+
if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
|
|
1772
|
+
const poolRef = { pool: ctx.runtime.strPool }
|
|
1773
|
+
sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
|
|
1774
|
+
ctx.runtime.strPool = poolRef.pool
|
|
1775
|
+
}
|
|
1776
|
+
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg)
|
|
1777
|
+
if (!cfg || cfg.hoistConstantPool !== false)
|
|
1778
|
+
hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
|
|
1779
|
+
|
|
1780
|
+
const dataLen = ctx.runtime.data?.length || 0
|
|
1781
|
+
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
1782
|
+
const heapBase = (dataLen + 7) & ~7
|
|
1783
|
+
ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
|
|
1784
|
+
for (const s of sec.stdlib)
|
|
1785
|
+
if (s[0] === 'func' && s[1] === '$__reset')
|
|
1786
|
+
for (let i = 2; i < s.length; i++)
|
|
1787
|
+
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
1788
|
+
s[i][2][1] = `${heapBase}`
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* Phase: strip static-data prefix.
|
|
1794
|
+
*
|
|
1795
|
+
* R: when `__static_str` runtime helper isn't included, the leading prefix of the
|
|
1796
|
+
* data segment (the static string-table header) is dead — strip it and shift all
|
|
1797
|
+
* pointer offsets in user code, embedded data slots, and constant-folded NaN-box
|
|
1798
|
+
* literals down by `prefix` bytes. ATOM/SSO have no offset, so they're unaffected.
|
|
1799
|
+
*
|
|
1800
|
+
* Patches both runtime-call form (`__mkptr(T, A, off)`) and the constant-folded
|
|
1801
|
+
* form (`f64.reinterpret_i64 (i64.const ...)`) when offset >= prefix.
|
|
1802
|
+
*/
|
|
1803
|
+
function stripStaticDataPrefix(sec) {
|
|
1804
|
+
if (!ctx.runtime.staticDataLen || ctx.core.includes.has('__static_str')) return
|
|
1805
|
+
const prefix = ctx.runtime.staticDataLen
|
|
1806
|
+
const SHIFTABLE = new Set([PTR.STRING, PTR.OBJECT, PTR.ARRAY, PTR.HASH, PTR.SET, PTR.MAP, PTR.BUFFER, PTR.TYPED, PTR.CLOSURE])
|
|
1807
|
+
const data = ctx.runtime.data || ''
|
|
1808
|
+
const buf = new Uint8Array(data.length)
|
|
1809
|
+
for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
|
|
1810
|
+
const dv = new DataView(buf.buffer)
|
|
1811
|
+
if (ctx.runtime.staticPtrSlots) {
|
|
1812
|
+
for (const slotOff of ctx.runtime.staticPtrSlots) {
|
|
1813
|
+
if (slotOff < prefix) continue
|
|
1814
|
+
const bits = dv.getBigUint64(slotOff, true)
|
|
1815
|
+
if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX_BITS) continue
|
|
1816
|
+
const ty = Number((bits >> 47n) & 0xFn)
|
|
1817
|
+
if (!SHIFTABLE.has(ty)) continue
|
|
1818
|
+
const off = Number(bits & 0xFFFFFFFFn)
|
|
1819
|
+
if (off < prefix) continue
|
|
1820
|
+
const hi = bits & ~0xFFFFFFFFn
|
|
1821
|
+
dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
let s = ''
|
|
1825
|
+
for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
|
|
1826
|
+
ctx.runtime.data = s
|
|
1827
|
+
if (ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = ctx.runtime.staticPtrSlots
|
|
1828
|
+
.filter(o => o >= prefix).map(o => o - prefix)
|
|
1829
|
+
const shift = (node) => {
|
|
1830
|
+
if (!Array.isArray(node)) return
|
|
1831
|
+
for (let i = 0; i < node.length; i++) {
|
|
1832
|
+
const child = node[i]
|
|
1833
|
+
if (!Array.isArray(child)) continue
|
|
1834
|
+
if (child[0] === 'call' && child[1] === '$__mkptr' &&
|
|
1835
|
+
Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
|
|
1836
|
+
Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
|
|
1837
|
+
typeof child[4][1] === 'number' && child[4][1] >= prefix) {
|
|
1838
|
+
child[4][1] -= prefix
|
|
1839
|
+
} else if (child[0] === 'f64.const' &&
|
|
1840
|
+
typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
|
|
1841
|
+
const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
|
|
1842
|
+
if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX_BITS) {
|
|
1843
|
+
const ty = Number((bits >> 47n) & 0xFn)
|
|
1844
|
+
if (SHIFTABLE.has(ty)) {
|
|
1845
|
+
const off = Number(bits & 0xFFFFFFFFn)
|
|
1846
|
+
if (off >= prefix) {
|
|
1847
|
+
const hi = bits & ~0xFFFFFFFFn
|
|
1848
|
+
const newBits = hi | BigInt(off - prefix)
|
|
1849
|
+
child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
shift(child)
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) shift(s)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Compile prepared AST to WASM module IR.
|
|
1862
|
+
* @param {import('./prepare.js').ASTNode} ast - Prepared AST
|
|
1863
|
+
* @returns {Array} Complete WASM module as S-expression
|
|
1864
|
+
*/
|
|
1865
|
+
export default function compile(ast) {
|
|
1866
|
+
// Populate known function names + lookup map on ctx.func for direct call detection
|
|
1867
|
+
ctx.func.names.clear()
|
|
1868
|
+
ctx.func.map.clear()
|
|
1869
|
+
for (const f of ctx.func.list) { ctx.func.names.add(f.name); ctx.func.map.set(f.name, f) }
|
|
1870
|
+
// Include imported functions for call resolution (e.g. template interpolations)
|
|
1871
|
+
for (const imp of ctx.module.imports)
|
|
1872
|
+
if (imp[3]?.[0] === 'func') ctx.func.names.add(imp[3][1].replace(/^\$/, ''))
|
|
1873
|
+
|
|
1874
|
+
// Check user globals don't conflict with runtime globals (modules loaded after user decls)
|
|
1875
|
+
for (const name of ctx.scope.userGlobals)
|
|
1876
|
+
if (!ctx.scope.globals.get(name)?.includes('mut f64'))
|
|
1877
|
+
err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
1878
|
+
|
|
1879
|
+
// Pre-fold const globals: evaluate constant initializers before function compilation
|
|
1880
|
+
// so functions see the correct global types (i32 vs f64).
|
|
1881
|
+
if (ast) {
|
|
1882
|
+
const evalConst = n => {
|
|
1883
|
+
if (typeof n === 'number') return n
|
|
1884
|
+
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return n[1]
|
|
1885
|
+
if (!Array.isArray(n)) return null
|
|
1886
|
+
const [op, a, b] = n
|
|
1887
|
+
const va = evalConst(a), vb = b !== undefined ? evalConst(b) : null
|
|
1888
|
+
if (va == null) return null
|
|
1889
|
+
if (op === 'u-' || (op === '-' && b === undefined)) return -va
|
|
1890
|
+
if (vb == null) return null
|
|
1891
|
+
if (op === '+') return va + vb; if (op === '-') return va - vb
|
|
1892
|
+
if (op === '*') return va * vb; if (op === '%' && vb) return va % vb
|
|
1893
|
+
if (op === '/' && vb) return va / vb; if (op === '**') return va ** vb
|
|
1894
|
+
if (op === '&') return va & vb; if (op === '|') return va | vb
|
|
1895
|
+
if (op === '^') return va ^ vb; if (op === '<<') return va << vb
|
|
1896
|
+
if (op === '>>') return va >> vb; if (op === '>>>') return va >>> vb
|
|
1897
|
+
return null
|
|
1898
|
+
}
|
|
1899
|
+
const stmts = Array.isArray(ast) && ast[0] === ';' ? ast.slice(1)
|
|
1900
|
+
: Array.isArray(ast) && ast[0] === 'const' ? [ast] : []
|
|
1901
|
+
for (const s of stmts) {
|
|
1902
|
+
if (!Array.isArray(s) || s[0] !== 'const') continue
|
|
1903
|
+
for (const decl of s.slice(1)) {
|
|
1904
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
1905
|
+
const [, name, init] = decl
|
|
1906
|
+
if (!ctx.scope.globals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
1907
|
+
const v = evalConst(init)
|
|
1908
|
+
if (v == null || !isFinite(v)) continue
|
|
1909
|
+
const isInt = Number.isInteger(v) && v >= -2147483648 && v <= 2147483647
|
|
1910
|
+
ctx.scope.globals.set(name, isInt
|
|
1911
|
+
? `(global $${name} i32 (i32.const ${v}))`
|
|
1912
|
+
: `(global $${name} f64 (f64.const ${v}))`)
|
|
1913
|
+
ctx.scope.globalTypes.set(name, isInt ? 'i32' : 'f64')
|
|
1914
|
+
// Cache integer values for cross-call const-arg propagation: `f(N)` where
|
|
1915
|
+
// `const N = 8` should observe the param as intConst=8.
|
|
1916
|
+
if (isInt) (ctx.scope.constInts ||= new Map()).set(name, v)
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Pre-scan module-scope value types so functions can dispatch methods on globals.
|
|
1922
|
+
// Also scan moduleInits so cross-module imports (e.g. regex literals from util.js)
|
|
1923
|
+
// resolve to the correct static dispatch path.
|
|
1924
|
+
const scanStmts = (root) => {
|
|
1925
|
+
if (!root) return
|
|
1926
|
+
const stmts = Array.isArray(root) && root[0] === ';' ? root.slice(1) : [root]
|
|
1927
|
+
for (const s of stmts) {
|
|
1928
|
+
if (!Array.isArray(s) || (s[0] !== 'const' && s[0] !== 'let')) continue
|
|
1929
|
+
for (const decl of s.slice(1)) {
|
|
1930
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
1931
|
+
const vt = valTypeOf(decl[2])
|
|
1932
|
+
if (vt) {
|
|
1933
|
+
if (!ctx.scope.globalValTypes) ctx.scope.globalValTypes = new Map()
|
|
1934
|
+
ctx.scope.globalValTypes.set(decl[1], vt)
|
|
1935
|
+
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(decl[1], decl[2])
|
|
1936
|
+
}
|
|
1937
|
+
const ctor = typedElemCtor(decl[2])
|
|
1938
|
+
if (ctor) {
|
|
1939
|
+
if (!ctx.scope.globalTypedElem) ctx.scope.globalTypedElem = new Map()
|
|
1940
|
+
ctx.scope.globalTypedElem.set(decl[1], ctor)
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
scanStmts(ast)
|
|
1946
|
+
if (ctx.module.moduleInits) for (const init of ctx.module.moduleInits) scanStmts(init)
|
|
1947
|
+
|
|
1948
|
+
// Unbox const TYPED globals: change `(mut f64)` slot to `(mut i32)` and store the raw
|
|
1949
|
+
// pointer offset. Reads tag the global.get with ptrKind=TYPED + ptrAux=elemType so
|
|
1950
|
+
// typed-array consumers (.[]/.buffer/…) can resolve through ptrOffsetIR without ever
|
|
1951
|
+
// calling __ptr_offset on a NaN-box. Init still flows through emit.js, but the assign
|
|
1952
|
+
// coerces via asPtrOffset(val, VAL.TYPED) — one bit-extract at startup, then every
|
|
1953
|
+
// hot read is a plain `global.get` of an i32.
|
|
1954
|
+
if (ctx.scope.globalTypedElem && ctx.scope.consts) {
|
|
1955
|
+
for (const [name, ctor] of ctx.scope.globalTypedElem) {
|
|
1956
|
+
if (!ctx.scope.consts.has(name)) continue
|
|
1957
|
+
if (ctx.scope.globalValTypes?.get(name) !== VAL.TYPED) continue
|
|
1958
|
+
const aux = typedElemAux(ctor)
|
|
1959
|
+
if (aux == null) continue
|
|
1960
|
+
const decl = ctx.scope.globals.get(name)
|
|
1961
|
+
if (typeof decl !== 'string' || !decl.includes('mut f64')) continue
|
|
1962
|
+
ctx.scope.globals.set(name, `(global $${name} (mut i32) (i32.const 0))`)
|
|
1963
|
+
ctx.scope.globalTypes.set(name, 'i32')
|
|
1964
|
+
updateGlobalRep(name, { ptrKind: VAL.TYPED, ptrAux: aux })
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// === ProgramFacts: single whole-program walk over ast + user funcs + moduleInits ===
|
|
1969
|
+
// See collectProgramFacts (top of file) for the contract. ctx.types.* mirrors are
|
|
1970
|
+
// kept here because ir.js consumes them at emit time (will be replaced when emit
|
|
1971
|
+
// takes facts explicitly — S3).
|
|
1972
|
+
const programFacts = collectProgramFacts(ast)
|
|
1973
|
+
ctx.types.dynKeyVars = programFacts.dynVars
|
|
1974
|
+
ctx.types.anyDynKey = programFacts.anyDyn
|
|
1975
|
+
|
|
1976
|
+
// Materialize auto-box schemas from collected propMap
|
|
1977
|
+
if (ast && ctx.schema.register) {
|
|
1978
|
+
for (const [name, props] of programFacts.propMap) {
|
|
1979
|
+
if (ctx.schema.vars.has(name)) {
|
|
1980
|
+
const existing = ctx.schema.resolve(name)
|
|
1981
|
+
const newProps = [...props].filter(p => !existing.includes(p))
|
|
1982
|
+
if (newProps.length) {
|
|
1983
|
+
const merged = [...existing, ...newProps]
|
|
1984
|
+
const mergedId = ctx.schema.register(merged)
|
|
1985
|
+
ctx.schema.vars.set(name, mergedId)
|
|
1986
|
+
}
|
|
1987
|
+
continue
|
|
1988
|
+
}
|
|
1989
|
+
const valueProps = [...props].filter(p => !ctx.func.names.has(`${name}$${p}`))
|
|
1990
|
+
if (!valueProps.length) continue
|
|
1991
|
+
const allProps = [...props]
|
|
1992
|
+
const schema = ['__inner__', ...allProps]
|
|
1993
|
+
const schemaId = ctx.schema.register(schema)
|
|
1994
|
+
ctx.schema.vars.set(name, schemaId)
|
|
1995
|
+
if (ctx.func.names.has(name) && !ctx.scope.globals.has(name))
|
|
1996
|
+
ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
|
|
1997
|
+
if (!ctx.schema.autoBox) ctx.schema.autoBox = new Map()
|
|
1998
|
+
ctx.schema.autoBox.set(name, { schemaId, schema })
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Dynamic closure ABI width: max param count (`=>` defs), max call arity, rest/spread
|
|
2003
|
+
// accumulated by walkFacts above. $ftN type, call-site padding, and body slot decls
|
|
2004
|
+
// use this instead of the static MAX_CLOSURE_ARITY cap. hasRest adds +1 for rest
|
|
2005
|
+
// overflow. hasSpread + hasRest together force MAX (spread expands unknown element
|
|
2006
|
+
// count at runtime, and any rest receiver may consume them).
|
|
2007
|
+
if (ctx.closure.make) {
|
|
2008
|
+
const { hasSpread, hasRest, maxCall, maxDef } = programFacts
|
|
2009
|
+
const floor = ctx.closure.floor ?? 0
|
|
2010
|
+
ctx.closure.width = (hasSpread && hasRest)
|
|
2011
|
+
? MAX_CLOSURE_ARITY
|
|
2012
|
+
: Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), floor))
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
narrowSignatures(programFacts, ast)
|
|
2016
|
+
specializeBimorphicTyped(programFacts)
|
|
2017
|
+
refineDynKeys(programFacts)
|
|
2018
|
+
|
|
2019
|
+
const funcs = ctx.func.list.map(func => emitFunc(func, programFacts))
|
|
2020
|
+
funcs.push(...synthesizeBoundaryWrappers())
|
|
2021
|
+
|
|
2022
|
+
const closureFuncs = []
|
|
2023
|
+
let compiledBodyCount = 0
|
|
2024
|
+
const compilePendingClosures = () => {
|
|
2025
|
+
const bodies = ctx.closure.bodies || []
|
|
2026
|
+
for (let bodyIndex = compiledBodyCount; bodyIndex < bodies.length; bodyIndex++) {
|
|
2027
|
+
closureFuncs.push(emitClosureBody(bodies[bodyIndex]))
|
|
2028
|
+
}
|
|
2029
|
+
compiledBodyCount = bodies.length
|
|
2030
|
+
}
|
|
2031
|
+
compilePendingClosures()
|
|
2032
|
+
|
|
2033
|
+
// Build module sections — named slots, assembled at the end (no index bookkeeping)
|
|
2034
|
+
const sec = {
|
|
2035
|
+
extStdlib: [], // external stdlib (imports that must precede all other imports)
|
|
2036
|
+
imports: [...ctx.module.imports],
|
|
2037
|
+
types: [], // function types for call_indirect
|
|
2038
|
+
memory: [], // memory declaration
|
|
2039
|
+
data: [], // data segment (filled after emit)
|
|
2040
|
+
tags: [], // error tags + related exports
|
|
2041
|
+
table: [], // function table (at most one)
|
|
2042
|
+
globals: [], // globals (filled after __start)
|
|
2043
|
+
funcs: [], // closure funcs + regular funcs
|
|
2044
|
+
elem: [], // element section (table init)
|
|
2045
|
+
start: [], // __start func + start directive
|
|
2046
|
+
stdlib: [], // stdlib functions
|
|
2047
|
+
customs: [], // custom sections + exports
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// Uniform closure convention: (env f64, argc i32, a0..a{MAX-1} f64) → f64.
|
|
2051
|
+
// argc = actual arg count passed; missing slots padded with UNDEF_NAN at caller.
|
|
2052
|
+
// Rest-param bodies pack slots a[fixedParams..argc-1] into their rest array.
|
|
2053
|
+
// MAX_CLOSURE_ARITY is the fixed inline-slot count; calls with more args error.
|
|
2054
|
+
if (ctx.closure.types) {
|
|
2055
|
+
const params = [['param', 'f64'], ['param', 'i32']] // env + argc
|
|
2056
|
+
for (let i = 0; i < (ctx.closure.width ?? MAX_CLOSURE_ARITY); i++) params.push(['param', 'f64'])
|
|
2057
|
+
sec.types.push(['type', `$ftN`, ['func', ...params, ['result', 'f64']]])
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Memory section deferred — emitted after resolveIncludes() when __alloc is needed
|
|
2061
|
+
|
|
2062
|
+
if (ctx.runtime.throws) {
|
|
2063
|
+
ctx.scope.globals.set('__jz_last_err_bits', '(global $__jz_last_err_bits (mut i64) (i64.const 0))')
|
|
2064
|
+
sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
|
|
2065
|
+
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (ctx.closure.table?.length)
|
|
2069
|
+
sec.table.push(['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref'])
|
|
2070
|
+
|
|
2071
|
+
sec.funcs.push(...closureFuncs, ...funcs)
|
|
2072
|
+
|
|
2073
|
+
if (ctx.closure.table?.length)
|
|
2074
|
+
sec.elem.push(['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)])
|
|
2075
|
+
|
|
2076
|
+
buildStartFn(ast, sec, closureFuncs, compilePendingClosures)
|
|
2077
|
+
|
|
2078
|
+
dedupClosureBodies(closureFuncs, sec)
|
|
2079
|
+
|
|
2080
|
+
finalizeClosureTable(sec)
|
|
2081
|
+
|
|
2082
|
+
pullStdlib(sec)
|
|
2083
|
+
|
|
2084
|
+
stripStaticDataPrefix(sec)
|
|
2085
|
+
|
|
2086
|
+
optimizeModule(sec)
|
|
2087
|
+
|
|
2088
|
+
// Populate globals (after __start — const folding may update declarations)
|
|
2089
|
+
sec.globals.push(...[...ctx.scope.globals.values()].filter(g => g).map(g => parseWat(g)))
|
|
2090
|
+
|
|
2091
|
+
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
2092
|
+
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
2093
|
+
const escBytes = (s) => {
|
|
2094
|
+
let esc = ''
|
|
2095
|
+
for (let i = 0; i < s.length; i++) {
|
|
2096
|
+
const c = s.charCodeAt(i)
|
|
2097
|
+
if (c >= 32 && c < 127 && c !== 34 && c !== 92) esc += s[i]
|
|
2098
|
+
else esc += '\\' + c.toString(16).padStart(2, '0')
|
|
2099
|
+
}
|
|
2100
|
+
return esc
|
|
2101
|
+
}
|
|
2102
|
+
if (ctx.runtime.data && !ctx.memory.shared)
|
|
2103
|
+
sec.data.push(['data', ['i32.const', 0], '"' + escBytes(ctx.runtime.data) + '"'])
|
|
2104
|
+
// Passive segment for shared-memory string literals (copied via memory.init at runtime)
|
|
2105
|
+
if (ctx.runtime.strPool)
|
|
2106
|
+
sec.data.push(['data', '$__strPool', '"' + escBytes(ctx.runtime.strPool) + '"'])
|
|
2107
|
+
|
|
2108
|
+
// Custom section: embed object schemas for JS-side interop.
|
|
2109
|
+
// Compact binary format: varint(nSchemas); per schema: varint(nProps); per prop:
|
|
2110
|
+
// 0x00=null, 0x01=[null, <prop>], 0x02=<varint len><utf8 bytes>. Runtime decodes.
|
|
2111
|
+
if (ctx.schema.list.length) {
|
|
2112
|
+
const bytes = []
|
|
2113
|
+
const utf8 = new TextEncoder()
|
|
2114
|
+
const varint = (n) => { while (n >= 0x80) { bytes.push((n & 0x7F) | 0x80); n >>>= 7 } bytes.push(n) }
|
|
2115
|
+
const enc = (p) => {
|
|
2116
|
+
if (p === null) bytes.push(0)
|
|
2117
|
+
else if (Array.isArray(p)) { bytes.push(1); enc(p[1]) }
|
|
2118
|
+
else { bytes.push(2); const b = utf8.encode(p); varint(b.length); for (const x of b) bytes.push(x) }
|
|
2119
|
+
}
|
|
2120
|
+
varint(ctx.schema.list.length)
|
|
2121
|
+
for (const s of ctx.schema.list) { varint(s.length); for (const p of s) enc(p) }
|
|
2122
|
+
sec.customs.push(['@custom', '"jz:schema"', bytes])
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// Custom section: rest params for exported functions (JS-side wrapping)
|
|
2126
|
+
const restParamFuncs = ctx.func.list.filter(f => f.exported && f.rest)
|
|
2127
|
+
.map(f => ({ name: f.name, fixed: f.sig.params.length - 1 }))
|
|
2128
|
+
if (restParamFuncs.length)
|
|
2129
|
+
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
2130
|
+
|
|
2131
|
+
// Named export aliases: export { name } or export { source as alias }
|
|
2132
|
+
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
2133
|
+
if (val === true) {
|
|
2134
|
+
if (ctx.scope.userGlobals?.has(name)) sec.customs.push(['export', `"${name}"`, ['global', `$${name}`]])
|
|
2135
|
+
continue
|
|
2136
|
+
}
|
|
2137
|
+
if (typeof val !== 'string') continue
|
|
2138
|
+
const func = ctx.func.list.find(f => f.name === val)
|
|
2139
|
+
// Boundary-wrapped funcs export through the synthesized $${val}$exp wrapper
|
|
2140
|
+
// so the JS-visible alias preserves f64 ABI.
|
|
2141
|
+
if (func) sec.customs.push(['export', `"${name}"`, ['func', `$${isBoundaryWrapped(func) ? val + '$exp' : val}`]])
|
|
2142
|
+
else if (ctx.scope.globals.has(val)) sec.customs.push(['export', `"${name}"`, ['global', `$${val}`]])
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Whole-module: prune funcs unreachable from entry points (start, exports, elem refs).
|
|
2146
|
+
// Removes orphan top-level consts that never get called (e.g. watr's unused `hoist` = 26 KB).
|
|
2147
|
+
// Also returns callCount Map (computed during the same walk — used below for funcidx sort).
|
|
2148
|
+
// Reachability walk always runs (callCount feeds the sort even when shake is off);
|
|
2149
|
+
// actual removal gated by ctx.transform.optimize.treeshake.
|
|
2150
|
+
const optCfg = ctx.transform.optimize
|
|
2151
|
+
const { callCount } = treeshake(
|
|
2152
|
+
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
2153
|
+
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports],
|
|
2154
|
+
{ removeDead: !optCfg || optCfg.treeshake !== false }
|
|
2155
|
+
)
|
|
2156
|
+
|
|
2157
|
+
// Reorder non-import funcs by call count: hot callees get low LEB128 indices.
|
|
2158
|
+
// `call $f` encodes funcidx as ULEB128 (1 B for idx < 128, 2 B for idx < 16384).
|
|
2159
|
+
// On watr self-host this saves ~6 KB (hot specialized helpers migrate to idx < 128).
|
|
2160
|
+
// callCount was computed inline by treeshake's walk (same set of nodes).
|
|
2161
|
+
const byCalls = (a, b) => (callCount.get(b[1]) || 0) - (callCount.get(a[1]) || 0)
|
|
2162
|
+
const startFn = sec.start.find(n => n[0] === 'func')
|
|
2163
|
+
const startDir = sec.start.find(n => n[0] === 'start')
|
|
2164
|
+
const sortedFuncs = [
|
|
2165
|
+
...sec.stdlib, ...sec.funcs, ...(startFn ? [startFn] : []),
|
|
2166
|
+
].sort(byCalls)
|
|
2167
|
+
|
|
2168
|
+
// Assemble: named slots → flat section list.
|
|
2169
|
+
const sections = [
|
|
2170
|
+
...sec.extStdlib, ...sec.imports, ...sec.types, ...sec.memory, ...sec.data,
|
|
2171
|
+
...sec.tags, ...sec.table, ...sec.globals, ...sortedFuncs,
|
|
2172
|
+
...sec.elem, ...(startDir ? [startDir] : []), ...sec.customs,
|
|
2173
|
+
]
|
|
2174
|
+
return ['module', ...sections]
|
|
2175
|
+
}
|