jz 0.1.0 → 0.2.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/README.md +221 -62
- package/cli.js +34 -8
- package/index.js +133 -14
- package/module/array.js +399 -131
- package/module/collection.js +457 -212
- package/module/console.js +170 -87
- package/module/core.js +247 -143
- package/module/date.js +691 -0
- package/module/function.js +84 -23
- package/module/index.js +2 -1
- package/module/json.js +485 -138
- package/module/math.js +81 -40
- package/module/number.js +189 -83
- package/module/object.js +228 -46
- package/module/regex.js +34 -30
- package/module/schema.js +17 -18
- package/module/string.js +483 -227
- package/module/symbol.js +6 -4
- package/module/timer.js +90 -32
- package/module/typedarray.js +176 -44
- package/package.json +5 -10
- package/src/analyze.js +639 -124
- package/src/autoload.js +183 -0
- package/src/compile.js +448 -1083
- package/src/ctx.js +42 -12
- package/src/emit.js +646 -147
- package/src/host.js +273 -68
- package/src/ir.js +81 -31
- package/src/jzify.js +220 -36
- package/src/narrow.js +956 -0
- package/src/optimize.js +628 -42
- package/src/plan.js +1233 -0
- package/src/prepare.js +438 -256
- package/src/vectorize.js +1016 -0
- package/wasi.js +23 -4
package/src/compile.js
CHANGED
|
@@ -26,19 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { parse as parseWat } from 'watr'
|
|
29
|
-
import { ctx, err, inc, resolveIncludes, PTR } from './ctx.js'
|
|
29
|
+
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
|
|
30
30
|
import {
|
|
31
|
-
T, VAL,
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
31
|
+
T, VAL, analyzeValTypes, analyzeIntCertain, analyzeLocals,
|
|
32
|
+
analyzePtrUnboxable, typedElemAux, invalidateLocalsCache,
|
|
33
|
+
analyzeBoxedCaptures, updateRep, inferStringParams,
|
|
40
34
|
} from './analyze.js'
|
|
41
|
-
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake } from './optimize.js'
|
|
35
|
+
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake, arenaRewindModule } from './optimize.js'
|
|
42
36
|
import { emit, emitter, emitFlat, emitBody } from './emit.js'
|
|
43
37
|
import {
|
|
44
38
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
@@ -48,39 +42,27 @@ import {
|
|
|
48
42
|
isLit, litVal, isNullishLit, isPureIR, isPostfix, emitNum,
|
|
49
43
|
temp, tempI32, tempI64, f64rem, toNumF64, truthyIR, toBoolFromEmitted,
|
|
50
44
|
keyValType, usesDynProps, needsDynShadow,
|
|
51
|
-
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish,
|
|
45
|
+
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish, isUndef,
|
|
52
46
|
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
53
47
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
54
48
|
valKindToPtr,
|
|
55
49
|
} from './ir.js'
|
|
50
|
+
import plan from './plan.js'
|
|
56
51
|
|
|
57
|
-
|
|
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'
|
|
52
|
+
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
76
53
|
|
|
77
54
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
78
55
|
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
79
56
|
|
|
80
|
-
// NaN-
|
|
81
|
-
// identify pointer slots in the data segment.
|
|
82
|
-
|
|
83
|
-
const
|
|
57
|
+
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
58
|
+
// below to identify pointer slots in the data segment.
|
|
59
|
+
const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
|
|
60
|
+
const TAG_MASK_BIG = BigInt(LAYOUT.TAG_MASK)
|
|
61
|
+
const AUX_MASK_BIG = BigInt(LAYOUT.AUX_MASK)
|
|
62
|
+
const OFFSET_MASK_BIG = BigInt(LAYOUT.OFFSET_MASK)
|
|
63
|
+
const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
|
|
64
|
+
const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
|
|
65
|
+
const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
|
|
84
66
|
|
|
85
67
|
// Low-level IR helpers previously lived here. Pure ones moved to src/ir.js;
|
|
86
68
|
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
@@ -163,911 +145,120 @@ const tcoTailRewrite = (ir, resultType) => {
|
|
|
163
145
|
return ir
|
|
164
146
|
}
|
|
165
147
|
|
|
166
|
-
|
|
148
|
+
const ARENA_SAFE_CALLS = new Set([
|
|
149
|
+
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
150
|
+
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
151
|
+
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
152
|
+
])
|
|
167
153
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
}
|
|
154
|
+
const findFuncBodyStart = (fn) => {
|
|
155
|
+
for (let i = 2; i < fn.length; i++) {
|
|
156
|
+
const n = fn[i]
|
|
157
|
+
if (Array.isArray(n) && (n[0] === 'param' || n[0] === 'result' || n[0] === 'local' || n[0] === 'export')) continue
|
|
158
|
+
return i
|
|
777
159
|
}
|
|
160
|
+
return fn.length
|
|
778
161
|
}
|
|
779
162
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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)
|
|
163
|
+
const heapGetIR = () => ctx.memory.shared
|
|
164
|
+
? ['i32.load', ['i32.const', 1020]]
|
|
165
|
+
: ['global.get', '$__heap']
|
|
166
|
+
|
|
167
|
+
const heapSetIR = value => ctx.memory.shared
|
|
168
|
+
? ['i32.store', ['i32.const', 1020], value]
|
|
169
|
+
: ['global.set', '$__heap', value]
|
|
170
|
+
|
|
171
|
+
function applyArenaRewind(func, fn, safeCallees) {
|
|
172
|
+
if (ctx.transform.optimize?.arenaRewind === false) return false
|
|
173
|
+
if (func.raw || func.sig.params.length !== 0 || func.sig.results.length !== 1) return false
|
|
174
|
+
if (func.sig.ptrKind != null) return false
|
|
175
|
+
if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER) return false
|
|
176
|
+
if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
|
|
177
|
+
|
|
178
|
+
const bodyStart = findFuncBodyStart(fn)
|
|
179
|
+
let hasAlloc = false
|
|
180
|
+
let unsafe = false
|
|
181
|
+
const scan = node => {
|
|
182
|
+
if (unsafe || !Array.isArray(node)) return
|
|
183
|
+
const op = node[0]
|
|
184
|
+
if (op === 'global.set' || op === 'return_call' || op === 'call_indirect' || op === 'call_ref') {
|
|
185
|
+
unsafe = true
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (op === 'call') {
|
|
189
|
+
const name = node[1]
|
|
190
|
+
if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
|
|
191
|
+
if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
|
|
192
|
+
unsafe = true
|
|
193
|
+
return
|
|
924
194
|
}
|
|
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
195
|
}
|
|
935
|
-
|
|
196
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
197
|
+
}
|
|
198
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
199
|
+
if (unsafe || !hasAlloc) return false
|
|
200
|
+
|
|
201
|
+
let id = 0
|
|
202
|
+
const hasLocal = name => fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === name)
|
|
203
|
+
while (hasLocal(`$${T}heap_save${id}`) || hasLocal(`$${T}arena_ret${id}`)) id++
|
|
204
|
+
const save = `$${T}heap_save${id}`
|
|
205
|
+
const ret = `$${T}arena_ret${id}`
|
|
206
|
+
const restore = () => heapSetIR(['local.get', save])
|
|
207
|
+
const resultType = func.sig.results[0]
|
|
208
|
+
|
|
209
|
+
const rewriteReturns = node => {
|
|
210
|
+
if (!Array.isArray(node)) return node
|
|
211
|
+
if (node[0] === 'return' && node.length > 1) {
|
|
212
|
+
return ['block',
|
|
213
|
+
['result', resultType],
|
|
214
|
+
['local.set', ret, node[1]],
|
|
215
|
+
restore(),
|
|
216
|
+
['return', ['local.get', ret]],
|
|
217
|
+
['unreachable']]
|
|
218
|
+
}
|
|
219
|
+
for (let i = 1; i < node.length; i++) node[i] = rewriteReturns(node[i])
|
|
220
|
+
return node
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const endsWithReturn = fn.at(-1)?.[0] === 'return' || fn.at(-1)?.[0] === 'return_call'
|
|
224
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = rewriteReturns(fn[i])
|
|
225
|
+
const newBodyStart = findFuncBodyStart(fn)
|
|
226
|
+
fn.splice(newBodyStart, 0,
|
|
227
|
+
['local', save, 'i32'],
|
|
228
|
+
['local', ret, resultType],
|
|
229
|
+
['local.set', save, heapGetIR()])
|
|
230
|
+
if (!endsWithReturn) {
|
|
231
|
+
const last = fn.pop()
|
|
232
|
+
fn.push(['local.set', ret, last], restore(), ['local.get', ret])
|
|
233
|
+
}
|
|
234
|
+
return true
|
|
936
235
|
}
|
|
937
236
|
|
|
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))
|
|
237
|
+
// === Module compilation ===
|
|
1018
238
|
|
|
1019
|
-
|
|
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
|
-
}
|
|
239
|
+
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
1029
240
|
|
|
1030
|
-
|
|
241
|
+
function enterFunc(func) {
|
|
242
|
+
ctx.func.stack = []
|
|
243
|
+
ctx.func.uniq = 0
|
|
244
|
+
ctx.func.current = func.sig
|
|
245
|
+
ctx.func.body = func.body
|
|
246
|
+
ctx.func.directClosures = null
|
|
247
|
+
ctx.func.localProps = null
|
|
1031
248
|
}
|
|
1032
249
|
|
|
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) {
|
|
250
|
+
function analyzeFuncForEmit(func, programFacts) {
|
|
1043
251
|
const { paramReps } = programFacts
|
|
252
|
+
if (func.raw) return null
|
|
1044
253
|
|
|
1045
|
-
|
|
1046
|
-
|
|
254
|
+
const { name, body, sig } = func
|
|
255
|
+
enterFunc(func)
|
|
1047
256
|
|
|
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
257
|
const block = Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
|
|
1061
|
-
ctx.func.boxed = new Map()
|
|
1062
|
-
ctx.func.
|
|
1063
|
-
ctx.func.repByLocal = null // Map<name, ValueRep> — populated lazily; reset per function
|
|
258
|
+
ctx.func.boxed = new Map()
|
|
259
|
+
ctx.func.repByLocal = null
|
|
1064
260
|
ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
|
|
1065
|
-
|
|
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.
|
|
261
|
+
|
|
1071
262
|
const _reps = paramReps.get(name)
|
|
1072
263
|
if (_reps) {
|
|
1073
264
|
for (const [k, r] of _reps) {
|
|
@@ -1089,20 +280,39 @@ function emitFunc(func, programFacts) {
|
|
|
1089
280
|
// cached widths reflect the pre-narrow state. Re-walk now with reps in place.
|
|
1090
281
|
invalidateLocalsCache(body)
|
|
1091
282
|
ctx.func.locals = block ? analyzeLocals(body) : new Map()
|
|
283
|
+
// Usage-based VAL.STRING inference for params not already typed by paramReps.
|
|
284
|
+
// Descends into nested closures so a param used as STRING only inside an inner
|
|
285
|
+
// arrow (e.g. parseLevel's `str` capture in watr) still gets seeded — the
|
|
286
|
+
// closure capture path then propagates VAL.STRING via captureValTypes.
|
|
287
|
+
if (block) {
|
|
288
|
+
const candidates = sig.params
|
|
289
|
+
.filter(p => !ctx.func.repByLocal?.get(p.name)?.val)
|
|
290
|
+
.map(p => p.name)
|
|
291
|
+
if (candidates.length) {
|
|
292
|
+
const inferred = inferStringParams(body, candidates)
|
|
293
|
+
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
1092
296
|
if (block) {
|
|
1093
297
|
analyzeValTypes(body)
|
|
1094
298
|
analyzeIntCertain(body)
|
|
1095
299
|
analyzeBoxedCaptures(body)
|
|
1096
300
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
301
|
+
// VAL.TYPED unbox requires a known element ctor (aux byte) — without it,
|
|
302
|
+
// the use site can't pick the right i32.store{8,16}/i32.store width and
|
|
303
|
+
// the rebox path can't reconstruct the NaN-box. Heterogeneous decls (two
|
|
304
|
+
// `let arr = ...` with different ctors, or a multi-ctor ternary) leave
|
|
305
|
+
// typedElem unset; skip unbox so reads/writes go through `__typed_set_idx`.
|
|
1097
306
|
const unbox = analyzePtrUnboxable(body, ctx.func.locals, ctx.func.boxed)
|
|
1098
307
|
if (unbox.size > 0) {
|
|
1099
308
|
for (const [n, kind] of unbox) {
|
|
1100
|
-
ctx.func.locals.set(n, 'i32')
|
|
1101
309
|
const fields = { ptrKind: kind }
|
|
1102
310
|
if (kind === VAL.TYPED) {
|
|
1103
311
|
const aux = typedElemAux(ctx.types.typedElem?.get(n))
|
|
1104
|
-
if (aux
|
|
312
|
+
if (aux == null) continue
|
|
313
|
+
fields.ptrAux = aux
|
|
1105
314
|
}
|
|
315
|
+
ctx.func.locals.set(n, 'i32')
|
|
1106
316
|
updateRep(n, fields)
|
|
1107
317
|
}
|
|
1108
318
|
}
|
|
@@ -1117,6 +327,39 @@ function emitFunc(func, programFacts) {
|
|
|
1117
327
|
if (p.ptrAux != null) fields.ptrAux = p.ptrAux
|
|
1118
328
|
updateRep(p.name, fields)
|
|
1119
329
|
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
block,
|
|
333
|
+
locals: new Map(ctx.func.locals),
|
|
334
|
+
boxed: new Map(ctx.func.boxed),
|
|
335
|
+
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
336
|
+
repByLocal: cloneRepMap(ctx.func.repByLocal),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Phase: emit one user function to WAT IR.
|
|
342
|
+
*
|
|
343
|
+
* Reads precomputed `funcFacts` and the narrowed `func.sig`; applies scoped
|
|
344
|
+
* schema param bindings during emission so they cannot leak between functions.
|
|
345
|
+
*/
|
|
346
|
+
function emitFunc(func, funcFacts, programFacts) {
|
|
347
|
+
const { paramReps } = programFacts
|
|
348
|
+
|
|
349
|
+
// Raw WAT functions (e.g., _alloc, _clear from memory module)
|
|
350
|
+
if (func.raw) return parseWat(func.raw)
|
|
351
|
+
|
|
352
|
+
const { name, body, exported, sig } = func
|
|
353
|
+
const multi = sig.results.length > 1
|
|
354
|
+
const _reps = paramReps.get(name)
|
|
355
|
+
|
|
356
|
+
enterFunc(func)
|
|
357
|
+
const block = funcFacts.block
|
|
358
|
+
ctx.func.locals = new Map(funcFacts.locals)
|
|
359
|
+
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
360
|
+
ctx.func.repByLocal = cloneRepMap(funcFacts.repByLocal)
|
|
361
|
+
ctx.types.typedElem = funcFacts.typedElem ? new Map(funcFacts.typedElem) : null
|
|
362
|
+
|
|
1120
363
|
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
1121
364
|
// Schema bindings additionally write into ctx.schema.vars so prop-access dispatch
|
|
1122
365
|
// hits the slot map. ctx.schema.vars is saved/restored so bindings don't leak.
|
|
@@ -1145,24 +388,28 @@ function emitFunc(func, programFacts) {
|
|
|
1145
388
|
fn.push(...sig.params.map(p => ['param', `$${p.name}`, p.type]))
|
|
1146
389
|
fn.push(...sig.results.map(t => ['result', t]))
|
|
1147
390
|
|
|
1148
|
-
// Default params:
|
|
1149
|
-
//
|
|
391
|
+
// Default params: ES spec says default applies only when arg is `undefined`
|
|
392
|
+
// (or missing). `null`, `0`, `false`, etc. all skip the default.
|
|
1150
393
|
const defaults = func.defaults || {}
|
|
1151
394
|
const defaultInits = []
|
|
1152
395
|
for (const [pname, defVal] of Object.entries(defaults)) {
|
|
1153
396
|
const p = sig.params.find(p => p.name === pname)
|
|
1154
397
|
const t = p?.type || 'f64'
|
|
1155
398
|
defaultInits.push(
|
|
1156
|
-
['if',
|
|
399
|
+
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
1157
400
|
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emit(defVal)) : asI32(emit(defVal))]]])
|
|
1158
401
|
}
|
|
1159
402
|
|
|
1160
403
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
1161
404
|
const boxedParamInits = []
|
|
405
|
+
const preboxedLocalInits = []
|
|
406
|
+
ctx.func.preboxed = new Set()
|
|
407
|
+
const paramNames = new Set(sig.params.map(p => p.name))
|
|
1162
408
|
for (const p of sig.params) {
|
|
1163
409
|
if (ctx.func.boxed.has(p.name)) {
|
|
1164
410
|
const cell = ctx.func.boxed.get(p.name)
|
|
1165
411
|
ctx.func.locals.set(cell, 'i32')
|
|
412
|
+
ctx.func.preboxed.add(p.name)
|
|
1166
413
|
const lget = typed(['local.get', `$${p.name}`], p.type)
|
|
1167
414
|
if (p.ptrKind != null) lget.ptrKind = p.ptrKind
|
|
1168
415
|
boxedParamInits.push(
|
|
@@ -1170,6 +417,14 @@ function emitFunc(func, programFacts) {
|
|
|
1170
417
|
['f64.store', ['local.get', `$${cell}`], asF64(lget)])
|
|
1171
418
|
}
|
|
1172
419
|
}
|
|
420
|
+
for (const [name, cell] of ctx.func.boxed) {
|
|
421
|
+
if (paramNames.has(name)) continue
|
|
422
|
+
ctx.func.locals.set(cell, 'i32')
|
|
423
|
+
ctx.func.preboxed.add(name)
|
|
424
|
+
preboxedLocalInits.push(
|
|
425
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
426
|
+
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
427
|
+
}
|
|
1173
428
|
|
|
1174
429
|
if (block) {
|
|
1175
430
|
const stmts = emitBody(body)
|
|
@@ -1177,16 +432,16 @@ function emitFunc(func, programFacts) {
|
|
|
1177
432
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
1178
433
|
const lastStmt = stmts.at(-1)
|
|
1179
434
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
1180
|
-
fn.push(...defaultInits, ...boxedParamInits, ...stmts, ...(endsWithReturn ? [] : sig.results.map(t => [`${t}.const`, 0])))
|
|
435
|
+
fn.push(...defaultInits, ...boxedParamInits, ...preboxedLocalInits, ...stmts, ...(endsWithReturn ? [] : sig.results.map(t => [`${t}.const`, 0])))
|
|
1181
436
|
} else if (multi && body[0] === '[') {
|
|
1182
437
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
1183
438
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1184
|
-
fn.push(...boxedParamInits, ...values)
|
|
439
|
+
fn.push(...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
1185
440
|
} else {
|
|
1186
441
|
const ir = emit(body)
|
|
1187
442
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1188
443
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
1189
|
-
fn.push(...defaultInits, ...boxedParamInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
444
|
+
fn.push(...defaultInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
1190
445
|
}
|
|
1191
446
|
|
|
1192
447
|
// Restore schema.vars so param bindings don't leak to next function.
|
|
@@ -1199,18 +454,22 @@ function emitFunc(func, programFacts) {
|
|
|
1199
454
|
*
|
|
1200
455
|
* For each `isBoundaryWrapped(func)`, emit a sibling `$${name}$exp` that:
|
|
1201
456
|
* - holds the (export "name") attribute (JS sees the wrapper)
|
|
1202
|
-
* - takes
|
|
457
|
+
* - takes i64 params always — JS-side carrier is BigInt that reinterprets to
|
|
458
|
+
* f64 NaN-box bits. i64 dodges V8's spec-permitted NaN canonicalization at
|
|
459
|
+
* the wasm↔JS boundary (see ToJSValue / ToWebAssemblyValue). Host wrap()
|
|
460
|
+
* in src/host.js pairs by converting BigInt↔f64 via reinterpret bits.
|
|
1203
461
|
* - converts each narrowed param at the call: f64 → i32 (truncate-sat) for
|
|
1204
462
|
* numeric narrowed, f64 → i32-offset (`i32.wrap_i64 + i64.reinterpret_f64`)
|
|
1205
|
-
* for pointer narrowed
|
|
463
|
+
* for pointer narrowed. The reinterpret happens once at param decode and
|
|
464
|
+
* once at result encode; numeric exports without narrowing skip wrapping
|
|
465
|
+
* entirely (no NaN-class values).
|
|
1206
466
|
* - forwards args to the inner $${name}
|
|
1207
|
-
* - reboxes the narrowed result
|
|
467
|
+
* - reboxes the narrowed result and reinterprets to i64 for the boundary
|
|
1208
468
|
*
|
|
1209
|
-
* Param
|
|
1210
|
-
*
|
|
1211
|
-
* - p.type = 'i32', ptrKind set → i32.wrap_i64(i64.reinterpret_f64(local.get $p))
|
|
469
|
+
* Param decode (i64 → f64): each param gets `f64.reinterpret_i64` before the
|
|
470
|
+
* existing narrowing convert. f64 inner params just need the reinterpret.
|
|
1212
471
|
*
|
|
1213
|
-
* Result rebox cases:
|
|
472
|
+
* Result rebox cases (then reinterpret to i64 at the boundary):
|
|
1214
473
|
* - sig.ptrKind != null → mkPtrIR(ptrKind, ptrAux ?? 0, callIR)
|
|
1215
474
|
* - sig.results[0] = i32 → f64.convert_i32_s(callIR)
|
|
1216
475
|
* - sig.results[0] = f64 → callIR (some params narrowed but result stayed f64)
|
|
@@ -1220,18 +479,23 @@ function synthesizeBoundaryWrappers() {
|
|
|
1220
479
|
for (const func of ctx.func.list) {
|
|
1221
480
|
if (!isBoundaryWrapped(func)) continue
|
|
1222
481
|
const { name, sig } = func
|
|
482
|
+
// Per-position i64 carrier: only swap to i64 where a NaN-boxed pointer
|
|
483
|
+
// actually crosses the boundary (param.ptrKind set, or result with
|
|
484
|
+
// sig.ptrKind set). Numeric narrowing (i32 trunc-sat / convert) keeps f64
|
|
485
|
+
// so callers seeing the raw export get a plain Number for numerics.
|
|
486
|
+
const paramI64 = sig.params.map(p => p.ptrKind != null)
|
|
487
|
+
const resultI64 = sig.ptrKind != null
|
|
1223
488
|
const wrapNode = ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const args = sig.params.map(p => {
|
|
489
|
+
sig.params.forEach((p, i) => wrapNode.push(['param', `$${p.name}`, paramI64[i] ? 'i64' : 'f64']))
|
|
490
|
+
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
491
|
+
const args = sig.params.map((p, i) => {
|
|
1228
492
|
const get = ['local.get', `$${p.name}`]
|
|
1229
|
-
if (p.type === 'f64') return get
|
|
1230
493
|
if (p.ptrKind != null) {
|
|
1231
|
-
// NaN-
|
|
1232
|
-
return ['i32.wrap_i64',
|
|
494
|
+
// ptrKind: i64 carrier carries NaN-box bits → wrap to i32 offset
|
|
495
|
+
return ['i32.wrap_i64', get]
|
|
1233
496
|
}
|
|
1234
|
-
|
|
497
|
+
if (p.type === 'f64') return get
|
|
498
|
+
// Numeric narrowing: f64 → i32 truncate
|
|
1235
499
|
return ['i32.trunc_sat_f64_s', get]
|
|
1236
500
|
})
|
|
1237
501
|
const callIR = ['call', `$${name}`, ...args]
|
|
@@ -1244,7 +508,9 @@ function synthesizeBoundaryWrappers() {
|
|
|
1244
508
|
} else {
|
|
1245
509
|
body = callIR
|
|
1246
510
|
}
|
|
1247
|
-
wrapNode.push(body)
|
|
511
|
+
wrapNode.push(resultI64 ? ['i64.reinterpret_f64', body] : body)
|
|
512
|
+
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
513
|
+
func._exportI64Sig = { params: paramI64, result: resultI64 }
|
|
1248
514
|
wrappers.push(wrapNode)
|
|
1249
515
|
}
|
|
1250
516
|
return wrappers
|
|
@@ -1268,6 +534,7 @@ function emitClosureBody(cb) {
|
|
|
1268
534
|
// Reset per-function state for closure body
|
|
1269
535
|
ctx.func.locals = new Map()
|
|
1270
536
|
ctx.func.repByLocal = null
|
|
537
|
+
if (cb.intConsts) for (const [name, v] of cb.intConsts) updateRep(name, { intConst: v })
|
|
1271
538
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
1272
539
|
if (cb.schemaVars) {
|
|
1273
540
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -1283,6 +550,8 @@ function emitClosureBody(cb) {
|
|
|
1283
550
|
}
|
|
1284
551
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
1285
552
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
553
|
+
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
554
|
+
ctx.func.preboxed = new Set()
|
|
1286
555
|
ctx.func.stack = []
|
|
1287
556
|
ctx.func.uniq = Math.max(ctx.func.uniq, 100) // avoid label collisions
|
|
1288
557
|
ctx.func.body = cb.body
|
|
@@ -1315,11 +584,35 @@ function emitClosureBody(cb) {
|
|
|
1315
584
|
const block = Array.isArray(cb.body) && cb.body[0] === '{}' && cb.body[1]?.[0] !== ':'
|
|
1316
585
|
let bodyIR
|
|
1317
586
|
if (block) {
|
|
587
|
+
invalidateLocalsCache(cb.body)
|
|
1318
588
|
for (const [k, v] of analyzeLocals(cb.body)) if (!ctx.func.locals.has(k)) ctx.func.locals.set(k, v)
|
|
589
|
+
// Usage-based STRING inference for closure params not seeded by captureValTypes.
|
|
590
|
+
// (Captures already have their parent's val type via cb.valTypes above.)
|
|
591
|
+
{
|
|
592
|
+
const candidates = cb.params.filter(p => !ctx.func.repByLocal?.get(p)?.val)
|
|
593
|
+
if (candidates.length) {
|
|
594
|
+
const inferred = inferStringParams(cb.body, candidates)
|
|
595
|
+
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
analyzeValTypes(cb.body)
|
|
599
|
+
analyzeIntCertain(cb.body)
|
|
1319
600
|
// Detect captures from deeper nested arrows that mutate this body's locals/params/captures
|
|
1320
601
|
analyzeBoxedCaptures(cb.body)
|
|
1321
602
|
for (const name of ctx.func.boxed.keys()) {
|
|
1322
|
-
if (ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
603
|
+
if (parentBoxedCaptures.has(name) && ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
604
|
+
}
|
|
605
|
+
const unbox = analyzePtrUnboxable(cb.body, ctx.func.locals, ctx.func.boxed)
|
|
606
|
+
for (const [name, kind] of unbox) {
|
|
607
|
+
if (cb.params.includes(name) || cb.captures.includes(name)) continue
|
|
608
|
+
const fields = { ptrKind: kind }
|
|
609
|
+
if (kind === VAL.TYPED) {
|
|
610
|
+
const aux = typedElemAux(ctx.types.typedElem?.get(name))
|
|
611
|
+
if (aux == null) continue
|
|
612
|
+
fields.ptrAux = aux
|
|
613
|
+
}
|
|
614
|
+
ctx.func.locals.set(name, 'i32')
|
|
615
|
+
updateRep(name, fields)
|
|
1323
616
|
}
|
|
1324
617
|
bodyIR = emitBody(cb.body)
|
|
1325
618
|
} else {
|
|
@@ -1339,6 +632,28 @@ function emitClosureBody(cb) {
|
|
|
1339
632
|
inc('__alloc_hdr', '__mkptr')
|
|
1340
633
|
}
|
|
1341
634
|
|
|
635
|
+
const boxedCaptureNames = new Set(cb.captures.filter(name => parentBoxedCaptures.has(name)))
|
|
636
|
+
for (const name of boxedCaptureNames) ctx.func.preboxed.add(name)
|
|
637
|
+
const boxedValueCaptureNames = new Set(cb.captures.filter(name => ctx.func.boxed.has(name) && !parentBoxedCaptures.has(name)))
|
|
638
|
+
for (const name of boxedValueCaptureNames) {
|
|
639
|
+
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
640
|
+
ctx.func.preboxed.add(name)
|
|
641
|
+
}
|
|
642
|
+
const boxedParamNames = new Set(cb.params.filter(name => ctx.func.boxed.has(name)))
|
|
643
|
+
for (const name of boxedParamNames) {
|
|
644
|
+
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
645
|
+
ctx.func.preboxed.add(name)
|
|
646
|
+
}
|
|
647
|
+
const preboxedLocalInits = []
|
|
648
|
+
for (const [name, cell] of ctx.func.boxed) {
|
|
649
|
+
if (boxedCaptureNames.has(name) || boxedValueCaptureNames.has(name) || boxedParamNames.has(name)) continue
|
|
650
|
+
ctx.func.locals.set(cell, 'i32')
|
|
651
|
+
ctx.func.preboxed.add(name)
|
|
652
|
+
preboxedLocalInits.push(
|
|
653
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
654
|
+
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
655
|
+
}
|
|
656
|
+
|
|
1342
657
|
// Insert locals (captures + params + declared)
|
|
1343
658
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
1344
659
|
|
|
@@ -1351,8 +666,15 @@ function emitClosureBody(cb) {
|
|
|
1351
666
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
1352
667
|
const name = cb.captures[i]
|
|
1353
668
|
const addr = ['i32.add', ['local.get', `$${envBase}`], ['i32.const', i * 8]]
|
|
1354
|
-
|
|
1355
|
-
|
|
669
|
+
if (parentBoxedCaptures.has(name)) {
|
|
670
|
+
fn.push(['local.set', `$${name}`, ['i32.load', addr]])
|
|
671
|
+
} else if (boxedValueCaptureNames.has(name)) {
|
|
672
|
+
fn.push(
|
|
673
|
+
['local.set', `$${ctx.func.boxed.get(name)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
674
|
+
['f64.store', boxedAddr(name), ['f64.load', addr]])
|
|
675
|
+
} else {
|
|
676
|
+
fn.push(['local.set', `$${name}`, ['f64.load', addr]])
|
|
677
|
+
}
|
|
1356
678
|
}
|
|
1357
679
|
}
|
|
1358
680
|
|
|
@@ -1360,7 +682,14 @@ function emitClosureBody(cb) {
|
|
|
1360
682
|
// Rest name (if present) is last in cb.params — handled separately below.
|
|
1361
683
|
const fixedParamN = cb.params.length - (cb.rest ? 1 : 0)
|
|
1362
684
|
for (let i = 0; i < fixedParamN && i < W; i++) {
|
|
1363
|
-
|
|
685
|
+
const pname = cb.params[i]
|
|
686
|
+
if (boxedParamNames.has(pname)) {
|
|
687
|
+
fn.push(
|
|
688
|
+
['local.set', `$${ctx.func.boxed.get(pname)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
689
|
+
['f64.store', boxedAddr(pname), ['local.get', `$__a${i}`]])
|
|
690
|
+
} else {
|
|
691
|
+
fn.push(['local.set', `$${pname}`, ['local.get', `$__a${i}`]])
|
|
692
|
+
}
|
|
1364
693
|
}
|
|
1365
694
|
|
|
1366
695
|
// Rest param: pack slots a[fixedParams..argc-1] into fresh array.
|
|
@@ -1386,20 +715,34 @@ function emitClosureBody(cb) {
|
|
|
1386
715
|
['i32.add', ['local.get', `$${restOff}`], ['i32.const', i * 8]],
|
|
1387
716
|
['local.get', `$__a${fixedN + i}`]]]])
|
|
1388
717
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
718
|
+
const restValue = ['call', '$__mkptr', ['i32.const', PTR.ARRAY], ['i32.const', 0], ['local.get', `$${restOff}`]]
|
|
719
|
+
if (boxedParamNames.has(cb.rest)) {
|
|
720
|
+
fn.push(
|
|
721
|
+
['local.set', `$${ctx.func.boxed.get(cb.rest)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
722
|
+
['f64.store', boxedAddr(cb.rest), restValue])
|
|
723
|
+
} else {
|
|
724
|
+
fn.push(['local.set', `$${cb.rest}`, restValue])
|
|
725
|
+
}
|
|
1391
726
|
}
|
|
1392
727
|
|
|
1393
728
|
// Default params for closures (check sentinel after unpack)
|
|
729
|
+
// Only `undefined` triggers default per spec — `null`/`0`/`false` pass through.
|
|
1394
730
|
if (cb.defaults) {
|
|
1395
731
|
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
1396
|
-
|
|
1397
|
-
['
|
|
732
|
+
if (boxedParamNames.has(pname)) {
|
|
733
|
+
fn.push(['if', isUndef(['f64.load', boxedAddr(pname)]),
|
|
734
|
+
['then', ['f64.store', boxedAddr(pname), asF64(emit(defVal))]]])
|
|
735
|
+
} else {
|
|
736
|
+
fn.push(['if', isUndef(['local.get', `$${pname}`]),
|
|
737
|
+
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
738
|
+
}
|
|
1398
739
|
}
|
|
1399
740
|
}
|
|
741
|
+
fn.push(...preboxedLocalInits)
|
|
1400
742
|
fn.push(...bodyIR)
|
|
1401
743
|
// I: Skip trailing fallback when last statement is return
|
|
1402
|
-
|
|
744
|
+
// Implicit fall-through return is `undefined` per JS spec, not 0.
|
|
745
|
+
if (block && !(bodyIR.at(-1)?.[0] === 'return' || bodyIR.at(-1)?.[0] === 'return_call')) fn.push(undefExpr())
|
|
1403
746
|
ctx.schema.vars = prevSchemaVars
|
|
1404
747
|
ctx.types.typedElem = prevTypedElems
|
|
1405
748
|
return fn
|
|
@@ -1452,9 +795,9 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
1452
795
|
const bt = `${T}box`
|
|
1453
796
|
ctx.func.locals.set(bt, 'i32')
|
|
1454
797
|
for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
|
|
1455
|
-
inc('
|
|
798
|
+
inc('__alloc_hdr', '__mkptr')
|
|
1456
799
|
boxInit.push(
|
|
1457
|
-
['local.set', `$${bt}`, ['call', '$
|
|
800
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
|
|
1458
801
|
['f64.store', ['local.get', `$${bt}`],
|
|
1459
802
|
ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
|
|
1460
803
|
...schema.slice(1).map((_, i) =>
|
|
@@ -1472,7 +815,12 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
1472
815
|
// __dyn_get (set transitively by resolveIncludes() later) are listed
|
|
1473
816
|
// explicitly here because the dep graph hasn't been expanded yet at
|
|
1474
817
|
// start-fn build time.
|
|
1475
|
-
|
|
818
|
+
// __jp_obj registers schemas at runtime, so the table must be allocated
|
|
819
|
+
// (and __schema_next initialized) even with zero compile-time schemas.
|
|
820
|
+
// __jp pulls __jp_obj transitively via the include-graph but resolveIncludes
|
|
821
|
+
// runs after buildStartFn, so check the top-level __jp parser as a proxy.
|
|
822
|
+
const hasJpObj = ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
|
|
823
|
+
const needsSchemaTbl = (ctx.schema.list.length && (
|
|
1476
824
|
ctx.core.includes.has('__stringify') ||
|
|
1477
825
|
ctx.core.includes.has('__dyn_get') ||
|
|
1478
826
|
ctx.core.includes.has('__dyn_get_t') ||
|
|
@@ -1480,17 +828,24 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
1480
828
|
ctx.core.includes.has('__dyn_get_any_t') ||
|
|
1481
829
|
ctx.core.includes.has('__dyn_get_expr') ||
|
|
1482
830
|
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
1483
|
-
ctx.core.includes.has('__dyn_get_or'))
|
|
831
|
+
ctx.core.includes.has('__dyn_get_or'))) ||
|
|
832
|
+
hasJpObj
|
|
1484
833
|
if (needsSchemaTbl) {
|
|
1485
834
|
const nSchemas = ctx.schema.list.length
|
|
835
|
+
// Reserve trailing slots for runtime-registered schemas (used by JSON.parse
|
|
836
|
+
// shape caching). __schema_next tracks the first free runtime slot.
|
|
837
|
+
const runtimeReserve = hasJpObj ? 256 : 0
|
|
1486
838
|
const stbl = `${T}stbl`
|
|
1487
839
|
const sarr = `${T}sarr`
|
|
1488
840
|
ctx.func.locals.set(stbl, 'i32')
|
|
1489
841
|
ctx.func.locals.set(sarr, 'i32')
|
|
1490
842
|
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
1491
843
|
schemaInit.push(
|
|
1492
|
-
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', nSchemas * 8]]],
|
|
844
|
+
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
|
|
1493
845
|
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
846
|
+
if (runtimeReserve) {
|
|
847
|
+
schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
|
|
848
|
+
}
|
|
1494
849
|
for (let s = 0; s < nSchemas; s++) {
|
|
1495
850
|
const keys = ctx.schema.list[s]
|
|
1496
851
|
const n = keys.length
|
|
@@ -1521,12 +876,15 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
1521
876
|
for (const s of ctx.runtime.typeofStrs)
|
|
1522
877
|
typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
|
|
1523
878
|
}
|
|
1524
|
-
|
|
879
|
+
// WASI timer queue needs an init call in __start; JS-host mode delegates
|
|
880
|
+
// scheduling to the host so no init/loop is needed.
|
|
881
|
+
const wasiTimers = ctx.features.timers && ctx.transform.host === 'wasi'
|
|
882
|
+
if (moduleInits.length || init?.length || boxInit.length || schemaInit.length || typeofInit.length || strPoolInit.length || wasiTimers) {
|
|
1525
883
|
const initIR = normalizeIR(init)
|
|
1526
884
|
const startFn = ['func', '$__start']
|
|
1527
885
|
for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
|
|
1528
886
|
startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
|
|
1529
|
-
...(
|
|
887
|
+
...(wasiTimers ? [['call', '$__timer_init']] : []),
|
|
1530
888
|
...moduleInits, ...initIR,
|
|
1531
889
|
...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
|
|
1532
890
|
)
|
|
@@ -1591,12 +949,21 @@ function dedupClosureBodies(closureFuncs, sec) {
|
|
|
1591
949
|
else { hashToName.set(key, name); keepSet.add(name) }
|
|
1592
950
|
}
|
|
1593
951
|
if (!redirect.size) return
|
|
1594
|
-
|
|
952
|
+
// Remove duplicate declarations BEFORE rewriting references — otherwise redirectRefs
|
|
953
|
+
// renames the declaration itself and the filter can't find the original name.
|
|
1595
954
|
const kept = sec.funcs.filter(fn => {
|
|
1596
955
|
if (!Array.isArray(fn) || fn[0] !== 'func') return true
|
|
1597
956
|
const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
1598
957
|
return !name || !redirect.has(name)
|
|
1599
958
|
})
|
|
959
|
+
const redirectRefs = node => {
|
|
960
|
+
if (typeof node === 'string') return node[0] === '$' && redirect.has(node.slice(1)) ? `$${redirect.get(node.slice(1))}` : node
|
|
961
|
+
if (!Array.isArray(node)) return node
|
|
962
|
+
for (let i = 0; i < node.length; i++) node[i] = redirectRefs(node[i])
|
|
963
|
+
return node
|
|
964
|
+
}
|
|
965
|
+
for (const fn of kept) redirectRefs(fn)
|
|
966
|
+
ctx.closure.table = ctx.closure.table.map(n => redirect.get(n) || n)
|
|
1600
967
|
sec.funcs.length = 0
|
|
1601
968
|
sec.funcs.push(...kept)
|
|
1602
969
|
}
|
|
@@ -1622,8 +989,7 @@ function dedupClosureBodies(closureFuncs, sec) {
|
|
|
1622
989
|
* Both `call` and `return_call` (tail call) sites are rewritten in the same walk.
|
|
1623
990
|
*/
|
|
1624
991
|
function finalizeClosureTable(sec) {
|
|
1625
|
-
|
|
1626
|
-
let indirectUsed = false
|
|
992
|
+
let indirectUsed = ctx.transform.host === 'wasi'
|
|
1627
993
|
const scan = (n) => {
|
|
1628
994
|
if (!Array.isArray(n) || indirectUsed) return
|
|
1629
995
|
if (n[0] === 'call_indirect') { indirectUsed = true; return }
|
|
@@ -1637,8 +1003,9 @@ function finalizeClosureTable(sec) {
|
|
|
1637
1003
|
}
|
|
1638
1004
|
// Keep table if call_indirect is used (closures, timer dispatch, etc.)
|
|
1639
1005
|
if (indirectUsed) {
|
|
1006
|
+
if (!ctx.closure.table) ctx.closure.table = []
|
|
1640
1007
|
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}`)]]
|
|
1008
|
+
sec.elem = ctx.closure.table.length ? [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]] : []
|
|
1642
1009
|
return
|
|
1643
1010
|
}
|
|
1644
1011
|
sec.table = []
|
|
@@ -1702,7 +1069,7 @@ function finalizeClosureTable(sec) {
|
|
|
1702
1069
|
* 1. resolveIncludes() — close the include set under stdlib dependencies.
|
|
1703
1070
|
* 2. Emit memory section ONLY when some included helper uses memory ops
|
|
1704
1071
|
* (G optimization: pure scalar programs ship without memory + __heap).
|
|
1705
|
-
* When memory is needed, the allocator (__alloc + __alloc_hdr +
|
|
1072
|
+
* When memory is needed, the allocator (__alloc + __alloc_hdr + __clear)
|
|
1706
1073
|
* is force-included since stdlib funcs may call into it.
|
|
1707
1074
|
* 3. Pull external (host) stdlibs into sec.extStdlib (must precede normal
|
|
1708
1075
|
* imports in the module byte order).
|
|
@@ -1718,11 +1085,11 @@ function pullStdlib(sec) {
|
|
|
1718
1085
|
const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
|
|
1719
1086
|
if (!needsMemory) ctx.scope.globals.delete('__heap')
|
|
1720
1087
|
if (needsMemory && ctx.module.modules.core) {
|
|
1721
|
-
for (const fn of ['__alloc', '__alloc_hdr', '
|
|
1088
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
|
|
1722
1089
|
const pages = ctx.memory.pages || 1
|
|
1723
1090
|
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
|
|
1724
1091
|
else sec.memory.push(['memory', ['export', '"memory"'], pages])
|
|
1725
|
-
if (ctx.transform.
|
|
1092
|
+
if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
1726
1093
|
sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
|
|
1727
1094
|
}
|
|
1728
1095
|
|
|
@@ -1730,10 +1097,15 @@ function pullStdlib(sec) {
|
|
|
1730
1097
|
const v = ctx.core.stdlib[name]
|
|
1731
1098
|
return typeof v === 'function' ? v() : v
|
|
1732
1099
|
}
|
|
1100
|
+
// Track __ext_* names emitted to host imports. The cleanup below deletes them
|
|
1101
|
+
// from ctx.core.includes (moved into sec.extStdlib instead), so any post-compile
|
|
1102
|
+
// audit (e.g. host: 'wasi') needs to read this list rather than the includes set.
|
|
1103
|
+
ctx.core.extImports ??= new Set()
|
|
1733
1104
|
for (const name of Object.keys(ctx.core.stdlib)) {
|
|
1734
1105
|
if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
|
|
1735
1106
|
const parsed = parseWat(stdlibStr(name))
|
|
1736
1107
|
sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
|
|
1108
|
+
ctx.core.extImports.add(name)
|
|
1737
1109
|
ctx.core.includes.delete(name)
|
|
1738
1110
|
}
|
|
1739
1111
|
}
|
|
@@ -1741,6 +1113,12 @@ function pullStdlib(sec) {
|
|
|
1741
1113
|
sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
|
|
1742
1114
|
}
|
|
1743
1115
|
|
|
1116
|
+
function syncImports(sec) {
|
|
1117
|
+
for (const imp of ctx.module.imports) {
|
|
1118
|
+
if (!sec.imports.some(i => i[1] === imp[1] && i[2] === imp[2])) sec.imports.push(imp)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1744
1122
|
/**
|
|
1745
1123
|
* Phase: whole-module + per-function optimization passes.
|
|
1746
1124
|
*
|
|
@@ -1774,6 +1152,19 @@ function optimizeModule(sec) {
|
|
|
1774
1152
|
ctx.runtime.strPool = poolRef.pool
|
|
1775
1153
|
}
|
|
1776
1154
|
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg)
|
|
1155
|
+
if (!cfg || cfg.arenaRewind !== false) {
|
|
1156
|
+
const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
1157
|
+
// Build name → WAT IR map for user functions
|
|
1158
|
+
const fnByName = new Map()
|
|
1159
|
+
for (const fn of sec.funcs) {
|
|
1160
|
+
if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
|
|
1161
|
+
fnByName.set(fn[1], fn)
|
|
1162
|
+
}
|
|
1163
|
+
for (const func of ctx.func.list) {
|
|
1164
|
+
const fn = fnByName.get(`$${func.name}`)
|
|
1165
|
+
if (fn) applyArenaRewind(func, fn, safeCallees)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1777
1168
|
if (!cfg || cfg.hoistConstantPool !== false)
|
|
1778
1169
|
hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
|
|
1779
1170
|
|
|
@@ -1781,8 +1172,10 @@ function optimizeModule(sec) {
|
|
|
1781
1172
|
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
1782
1173
|
const heapBase = (dataLen + 7) & ~7
|
|
1783
1174
|
ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
|
|
1175
|
+
if (ctx.scope.globals.has('__heap_start'))
|
|
1176
|
+
ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
|
|
1784
1177
|
for (const s of sec.stdlib)
|
|
1785
|
-
if (s[0] === 'func' && s[1] === '$
|
|
1178
|
+
if (s[0] === 'func' && s[1] === '$__clear')
|
|
1786
1179
|
for (let i = 2; i < s.length; i++)
|
|
1787
1180
|
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
1788
1181
|
s[i][2][1] = `${heapBase}`
|
|
@@ -1812,12 +1205,14 @@ function stripStaticDataPrefix(sec) {
|
|
|
1812
1205
|
for (const slotOff of ctx.runtime.staticPtrSlots) {
|
|
1813
1206
|
if (slotOff < prefix) continue
|
|
1814
1207
|
const bits = dv.getBigUint64(slotOff, true)
|
|
1815
|
-
if (((bits >> 48n) & 0xFFF8n) !==
|
|
1816
|
-
const ty = Number((bits >>
|
|
1208
|
+
if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
|
|
1209
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
1817
1210
|
if (!SHIFTABLE.has(ty)) continue
|
|
1818
|
-
|
|
1211
|
+
// SSO STRING: "offset" holds packed bytes, not a heap address — never shift.
|
|
1212
|
+
if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
|
|
1213
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
1819
1214
|
if (off < prefix) continue
|
|
1820
|
-
const hi = bits & ~
|
|
1215
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
1821
1216
|
dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
|
|
1822
1217
|
}
|
|
1823
1218
|
}
|
|
@@ -1835,16 +1230,21 @@ function stripStaticDataPrefix(sec) {
|
|
|
1835
1230
|
Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
|
|
1836
1231
|
Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
|
|
1837
1232
|
typeof child[4][1] === 'number' && child[4][1] >= prefix) {
|
|
1838
|
-
|
|
1233
|
+
// SSO STRING: aux carries SSO_BIT, "offset" holds packed bytes — don't shift.
|
|
1234
|
+
const isSsoString = child[2][1] === PTR.STRING &&
|
|
1235
|
+
Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
|
|
1236
|
+
typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
|
|
1237
|
+
if (!isSsoString) child[4][1] -= prefix
|
|
1839
1238
|
} else if (child[0] === 'f64.const' &&
|
|
1840
1239
|
typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
|
|
1841
1240
|
const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
|
|
1842
|
-
if (((bits >> 48n) & 0xFFF8n) ===
|
|
1843
|
-
const ty = Number((bits >>
|
|
1844
|
-
if (SHIFTABLE.has(ty)
|
|
1845
|
-
|
|
1241
|
+
if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX) {
|
|
1242
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
1243
|
+
if (SHIFTABLE.has(ty) &&
|
|
1244
|
+
!(ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG))) {
|
|
1245
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
1846
1246
|
if (off >= prefix) {
|
|
1847
|
-
const hi = bits & ~
|
|
1247
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
1848
1248
|
const newBits = hi | BigInt(off - prefix)
|
|
1849
1249
|
child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
|
|
1850
1250
|
}
|
|
@@ -1862,14 +1262,29 @@ function stripStaticDataPrefix(sec) {
|
|
|
1862
1262
|
* @param {import('./prepare.js').ASTNode} ast - Prepared AST
|
|
1863
1263
|
* @returns {Array} Complete WASM module as S-expression
|
|
1864
1264
|
*/
|
|
1865
|
-
export default function compile(ast) {
|
|
1265
|
+
export default function compile(ast, profiler) {
|
|
1866
1266
|
// Populate known function names + lookup map on ctx.func for direct call detection
|
|
1867
1267
|
ctx.func.names.clear()
|
|
1868
1268
|
ctx.func.map.clear()
|
|
1869
1269
|
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
|
-
|
|
1872
|
-
|
|
1270
|
+
// Include imported functions for call resolution (e.g. template interpolations).
|
|
1271
|
+
// Also register a synthesized sig in func.map so emit's arity-aware branches see
|
|
1272
|
+
// the import's declared param count — needed for arg pad/truncate to match it.
|
|
1273
|
+
for (const imp of ctx.module.imports) {
|
|
1274
|
+
if (imp[3]?.[0] !== 'func') continue
|
|
1275
|
+
const fname = imp[3][1].replace(/^\$/, '')
|
|
1276
|
+
ctx.func.names.add(fname)
|
|
1277
|
+
if (!ctx.func.map.has(fname)) {
|
|
1278
|
+
const params = []
|
|
1279
|
+
let result = 'f64'
|
|
1280
|
+
for (let k = 2; k < imp[3].length; k++) {
|
|
1281
|
+
const part = imp[3][k]
|
|
1282
|
+
if (Array.isArray(part) && part[0] === 'param') params.push({ type: part[1] || 'f64' })
|
|
1283
|
+
else if (Array.isArray(part) && part[0] === 'result') result = part[1] || 'f64'
|
|
1284
|
+
}
|
|
1285
|
+
ctx.func.map.set(fname, { name: fname, sig: { params, results: [result] } })
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1873
1288
|
|
|
1874
1289
|
// Check user globals don't conflict with runtime globals (modules loaded after user decls)
|
|
1875
1290
|
for (const name of ctx.scope.userGlobals)
|
|
@@ -1918,105 +1333,11 @@ export default function compile(ast) {
|
|
|
1918
1333
|
}
|
|
1919
1334
|
}
|
|
1920
1335
|
|
|
1921
|
-
|
|
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
|
-
}
|
|
1336
|
+
const programFacts = timePhase(profiler, 'plan', () => plan(ast))
|
|
1967
1337
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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))
|
|
1338
|
+
const funcFacts = new Map()
|
|
1339
|
+
for (const func of ctx.func.list) if (!func.raw) funcFacts.set(func, analyzeFuncForEmit(func, programFacts))
|
|
1340
|
+
const funcs = ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts))
|
|
2020
1341
|
funcs.push(...synthesizeBoundaryWrappers())
|
|
2021
1342
|
|
|
2022
1343
|
const closureFuncs = []
|
|
@@ -2070,11 +1391,33 @@ export default function compile(ast) {
|
|
|
2070
1391
|
|
|
2071
1392
|
sec.funcs.push(...closureFuncs, ...funcs)
|
|
2072
1393
|
|
|
1394
|
+
// WASI command-mode entries (`run`, `_start`) must export as () -> ();
|
|
1395
|
+
// wasmtime/wasmer reject f64-returning functions under those names.
|
|
1396
|
+
// Parametric entries skip this — a CLI invocation has no way to supply args.
|
|
1397
|
+
if (ctx.transform.host === 'wasi') {
|
|
1398
|
+
const WASI_ENTRIES = new Set(['run', '_start'])
|
|
1399
|
+
for (const func of ctx.func.list) {
|
|
1400
|
+
if (!func.exported || !WASI_ENTRIES.has(func.name)) continue
|
|
1401
|
+
if (func.sig.params.length) continue
|
|
1402
|
+
const inner = isBoundaryWrapped(func) ? `$${func.name}$exp` : `$${func.name}`
|
|
1403
|
+
for (const f of sec.funcs) {
|
|
1404
|
+
if (f[1] === inner || f[1] === `$${func.name}`) {
|
|
1405
|
+
const expIdx = f.findIndex(n => Array.isArray(n) && n[0] === 'export')
|
|
1406
|
+
if (expIdx >= 0) f.splice(expIdx, 1)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
sec.funcs.push(['func', `$${func.name}$wasi`, ['export', `"${func.name}"`],
|
|
1410
|
+
['drop', ['call', inner]]])
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
2073
1414
|
if (ctx.closure.table?.length)
|
|
2074
1415
|
sec.elem.push(['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)])
|
|
2075
1416
|
|
|
2076
1417
|
buildStartFn(ast, sec, closureFuncs, compilePendingClosures)
|
|
2077
1418
|
|
|
1419
|
+
syncImports(sec)
|
|
1420
|
+
|
|
2078
1421
|
dedupClosureBodies(closureFuncs, sec)
|
|
2079
1422
|
|
|
2080
1423
|
finalizeClosureTable(sec)
|
|
@@ -2128,6 +1471,28 @@ export default function compile(ast) {
|
|
|
2128
1471
|
if (restParamFuncs.length)
|
|
2129
1472
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
2130
1473
|
|
|
1474
|
+
// Custom section: per-export i64 ABI map. Each entry describes an export
|
|
1475
|
+
// whose boundary wrapper carries NaN-boxed pointers via i64 (rather than
|
|
1476
|
+
// f64) to dodge V8's NaN canonicalization. Format: { name, p, r } where p
|
|
1477
|
+
// is an array of i64 param indices and r is 1 if result is i64. host.js
|
|
1478
|
+
// wrap() reinterprets BigInt↔f64 at i64 positions; numeric f64 positions
|
|
1479
|
+
// stay as Numbers on the JS side.
|
|
1480
|
+
const i64Exports = []
|
|
1481
|
+
for (const f of ctx.func.list) {
|
|
1482
|
+
if (!f.exported || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
1483
|
+
const p = []
|
|
1484
|
+
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
1485
|
+
const r = f._exportI64Sig.result ? 1 : 0
|
|
1486
|
+
i64Exports.push({ name: f.name, p, r })
|
|
1487
|
+
// Aliases (export { foo as bar }) re-export the same wrapper under a
|
|
1488
|
+
// different JS-visible name; list each alias too so wrap() finds it.
|
|
1489
|
+
for (const [alias, val] of Object.entries(ctx.func.exports)) {
|
|
1490
|
+
if (val === f.name && alias !== f.name) i64Exports.push({ name: alias, p, r })
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (i64Exports.length)
|
|
1494
|
+
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1495
|
+
|
|
2131
1496
|
// Named export aliases: export { name } or export { source as alias }
|
|
2132
1497
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
2133
1498
|
if (val === true) {
|