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/narrow.js
ADDED
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature narrowing — fixpoint analysis that mutates each user func's `sig`
|
|
3
|
+
* based on call-site observations.
|
|
4
|
+
*
|
|
5
|
+
* Reads programFacts.callSites + valueUsed; mutates sig.params/results,
|
|
6
|
+
* func.valResult, and programFacts.paramReps. Pure w.r.t. the AST — only
|
|
7
|
+
* function `sig` records change.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ctx } from './ctx.js'
|
|
11
|
+
import { isLiteralStr } from './ir.js'
|
|
12
|
+
import {
|
|
13
|
+
VAL,
|
|
14
|
+
analyzeBody, analyzeLocals,
|
|
15
|
+
callerParamFactMap, clearStickyNull, ensureParamRep, mergeParamFact,
|
|
16
|
+
exprType, findMutations, hasBareReturn, inferArgArrElemSchema,
|
|
17
|
+
inferArgArrElemValType, inferArgSchema, inferArgType, inferArgTypedCtor,
|
|
18
|
+
invalidateLocalsCache, invalidateValTypesCache, isBlockBody, alwaysReturns,
|
|
19
|
+
narrowReturnArrayElems, observeProgramSlots, returnExprs, staticObjectProps,
|
|
20
|
+
typedElemAux, typedElemCtor, ctorFromElemAux, valTypeOf,
|
|
21
|
+
} from './analyze.js'
|
|
22
|
+
|
|
23
|
+
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
24
|
+
|
|
25
|
+
function filterLiveCallSites(callSites, valueUsed) {
|
|
26
|
+
if (!callSites.length) return
|
|
27
|
+
|
|
28
|
+
const live = new Set()
|
|
29
|
+
for (const f of ctx.func.list) {
|
|
30
|
+
if (f.exported || valueUsed.has(f.name)) live.add(f.name)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let changed = true
|
|
34
|
+
while (changed) {
|
|
35
|
+
changed = false
|
|
36
|
+
for (const cs of callSites) {
|
|
37
|
+
if (cs.callerFunc === null || live.has(cs.callerFunc.name)) {
|
|
38
|
+
if (!live.has(cs.callee)) { live.add(cs.callee); changed = true }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let w = 0
|
|
44
|
+
for (let r = 0; r < callSites.length; r++) {
|
|
45
|
+
const cs = callSites[r]
|
|
46
|
+
if (cs.callerFunc === null || live.has(cs.callerFunc.name)) callSites[w++] = cs
|
|
47
|
+
}
|
|
48
|
+
callSites.length = w
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildCallerCtx() {
|
|
52
|
+
const callerCtx = new Map()
|
|
53
|
+
callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes })
|
|
54
|
+
for (const func of ctx.func.list) {
|
|
55
|
+
if (!func.body || func.raw) continue
|
|
56
|
+
const facts = analyzeBody(func.body)
|
|
57
|
+
for (const p of func.sig.params) if (!facts.locals.has(p.name)) facts.locals.set(p.name, p.type)
|
|
58
|
+
callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes })
|
|
59
|
+
}
|
|
60
|
+
return callerCtx
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildCallerElems(sliceKey) {
|
|
64
|
+
const m = new Map()
|
|
65
|
+
m.set(null, new Map())
|
|
66
|
+
for (const func of ctx.func.list) {
|
|
67
|
+
if (!func.body || func.raw) continue
|
|
68
|
+
m.set(func, analyzeBody(func.body)[sliceKey])
|
|
69
|
+
}
|
|
70
|
+
return m
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false } = {}) {
|
|
74
|
+
for (const func of ctx.func.list) {
|
|
75
|
+
if (func.raw || valueUsed.has(func.name)) continue
|
|
76
|
+
const reps = paramReps.get(func.name)
|
|
77
|
+
if (!reps) continue
|
|
78
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
79
|
+
for (const [k, r] of reps) {
|
|
80
|
+
if (r.wasm !== 'i32' || k === restIdx) continue
|
|
81
|
+
if (k >= func.sig.params.length) continue
|
|
82
|
+
const p = func.sig.params[k]
|
|
83
|
+
if (p.type === 'i32') continue
|
|
84
|
+
if (func.defaults?.[p.name] != null) continue
|
|
85
|
+
if (skipTyped && r.val === VAL.TYPED) continue
|
|
86
|
+
p.type = 'i32'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateIntConstParams(paramReps, valueUsed) {
|
|
92
|
+
for (const func of ctx.func.list) {
|
|
93
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
94
|
+
if (!func.body) continue
|
|
95
|
+
const reps = paramReps.get(func.name)
|
|
96
|
+
if (!reps) continue
|
|
97
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
98
|
+
let candidates = null
|
|
99
|
+
for (const [k, r] of reps) {
|
|
100
|
+
if (r.intConst == null || k === restIdx) continue
|
|
101
|
+
if (k >= func.sig.params.length) { r.intConst = null; continue }
|
|
102
|
+
const pname = func.sig.params[k].name
|
|
103
|
+
if (func.defaults?.[pname] != null) { r.intConst = null; continue }
|
|
104
|
+
;(candidates ||= new Map()).set(pname, r)
|
|
105
|
+
}
|
|
106
|
+
if (!candidates) continue
|
|
107
|
+
const mutated = new Set()
|
|
108
|
+
findMutations(func.body, new Set(candidates.keys()), mutated)
|
|
109
|
+
for (const name of mutated) candidates.get(name).intConst = null
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyPointerParamAbi(paramReps, valueUsed) {
|
|
114
|
+
for (const func of ctx.func.list) {
|
|
115
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
116
|
+
const reps = paramReps.get(func.name)
|
|
117
|
+
if (!reps) continue
|
|
118
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
119
|
+
for (const [k, r] of reps) {
|
|
120
|
+
if (!PTR_ABI_KINDS.has(r.val)) continue
|
|
121
|
+
if (k === restIdx) continue
|
|
122
|
+
if (k >= func.sig.params.length) continue
|
|
123
|
+
const p = func.sig.params[k]
|
|
124
|
+
if (p.type === 'i32') continue
|
|
125
|
+
if (func.defaults?.[p.name] != null) continue
|
|
126
|
+
p.type = 'i32'
|
|
127
|
+
p.ptrKind = r.val
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function narrowableFuncs(valueUsed) {
|
|
133
|
+
return ctx.func.list.filter(f =>
|
|
134
|
+
!f.raw && !valueUsed.has(f.name) && f.sig.results.length === 1
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function refreshCallerValTypes(callerCtx) {
|
|
139
|
+
for (const func of ctx.func.list) {
|
|
140
|
+
if (!func.body || func.raw) continue
|
|
141
|
+
const entry = callerCtx.get(func)
|
|
142
|
+
if (entry) entry.callerValTypes = analyzeBody(func.body).valTypes
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildCallerTypedCtx() {
|
|
147
|
+
const callerTypedCtx = new Map()
|
|
148
|
+
callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
|
|
149
|
+
for (const func of ctx.func.list) {
|
|
150
|
+
if (!func.body || func.raw) continue
|
|
151
|
+
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
152
|
+
}
|
|
153
|
+
return callerTypedCtx
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyTypedPointerParamAbi(paramReps, valueUsed) {
|
|
157
|
+
for (const func of ctx.func.list) {
|
|
158
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
159
|
+
const reps = paramReps.get(func.name)
|
|
160
|
+
if (!reps) continue
|
|
161
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
162
|
+
for (const [k, r] of reps) {
|
|
163
|
+
const ctor = r.typedCtor
|
|
164
|
+
if (ctor == null) continue
|
|
165
|
+
if (k === restIdx) continue
|
|
166
|
+
if (k >= func.sig.params.length) continue
|
|
167
|
+
const p = func.sig.params[k]
|
|
168
|
+
if (p.type === 'i32') continue
|
|
169
|
+
if (func.defaults?.[p.name] != null) continue
|
|
170
|
+
const aux = typedElemAux(ctor)
|
|
171
|
+
if (aux == null) continue
|
|
172
|
+
p.type = 'i32'
|
|
173
|
+
p.ptrKind = VAL.TYPED
|
|
174
|
+
p.ptrAux = aux
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
180
|
+
for (const func of ctx.func.list) {
|
|
181
|
+
if (!func.body || func.raw) continue
|
|
182
|
+
const entry = callerCtx.get(func)
|
|
183
|
+
if (!entry) continue
|
|
184
|
+
for (const p of func.sig.params) {
|
|
185
|
+
if (p.ptrKind == null) continue
|
|
186
|
+
if (entry.callerValTypes.has(p.name)) continue
|
|
187
|
+
entry.callerValTypes.set(p.name, p.ptrKind)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function refreshCallerLocals(callerCtx) {
|
|
193
|
+
for (const func of ctx.func.list) {
|
|
194
|
+
if (!func.body || func.raw) continue
|
|
195
|
+
invalidateLocalsCache(func.body)
|
|
196
|
+
const fresh = analyzeLocals(func.body)
|
|
197
|
+
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
198
|
+
callerCtx.get(func).callerLocals = fresh
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resetParamWasmFacts(paramReps) {
|
|
203
|
+
for (const m of paramReps.values()) for (const r of m.values()) r.wasm = undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function createPhaseState() {
|
|
207
|
+
const callerCtx = buildCallerCtx()
|
|
208
|
+
const elemCtx = new Map()
|
|
209
|
+
let callerTypedCtx = null
|
|
210
|
+
|
|
211
|
+
const clearDerived = () => {
|
|
212
|
+
elemCtx.clear()
|
|
213
|
+
callerTypedCtx = null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
callerCtx,
|
|
218
|
+
|
|
219
|
+
callerElems(sliceKey) {
|
|
220
|
+
let m = elemCtx.get(sliceKey)
|
|
221
|
+
if (!m) { m = buildCallerElems(sliceKey); elemCtx.set(sliceKey, m) }
|
|
222
|
+
return m
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
callerTyped() {
|
|
226
|
+
callerTypedCtx ||= buildCallerTypedCtx()
|
|
227
|
+
return callerTypedCtx
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
invalidateBodyFacts() {
|
|
231
|
+
for (const func of ctx.func.list) {
|
|
232
|
+
if (func.body && !func.raw) invalidateValTypesCache(func.body)
|
|
233
|
+
}
|
|
234
|
+
clearDerived()
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
refreshValTypes() {
|
|
238
|
+
refreshCallerValTypes(callerCtx)
|
|
239
|
+
clearDerived()
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
refreshLocals() {
|
|
243
|
+
refreshCallerLocals(callerCtx)
|
|
244
|
+
clearDerived()
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default function narrowSignatures(programFacts, ast) {
|
|
250
|
+
const { callSites, valueUsed, paramReps, hasSchemaLiterals } = programFacts
|
|
251
|
+
|
|
252
|
+
// Reachability filter: dead callerFuncs (e.g. unused stdlib helpers from bundled
|
|
253
|
+
// modules) shouldn't poison narrowing of live functions. Without this, a never-
|
|
254
|
+
// executed call like `checksumF64 → mix(h, u[i])` would force mix's `x` rep to
|
|
255
|
+
// bimorphic (f64 ∪ i32) and block i32 narrowing of mix's hot caller (runKernel).
|
|
256
|
+
// Live = exported ∪ value-used ∪ transitively reached from those + top-level.
|
|
257
|
+
// Top-level call sites have callerFunc === null and are unconditionally live.
|
|
258
|
+
filterLiveCallSites(callSites, valueUsed)
|
|
259
|
+
|
|
260
|
+
// D: Call-site type propagation — infer param types from how functions are called.
|
|
261
|
+
// Drives off `callSites` collected during the ProgramFacts walk; no AST re-walking.
|
|
262
|
+
// For non-exported internal functions, if all call sites agree on a param's type,
|
|
263
|
+
// seed the param's val rep (ctx.func.repByLocal) during per-function compilation.
|
|
264
|
+
// Also infer i32/f64 WASM type — when all call sites pass i32 for a param, specialize
|
|
265
|
+
// sig.params[k].type to i32 (no default, no rest, not exported, not value-used).
|
|
266
|
+
// Also propagate schema ID — when all call sites pass objects with the same schema,
|
|
267
|
+
// bind the callee's param to that schema so `p.x` becomes a direct slot load.
|
|
268
|
+
// Inference helpers (inferArgType/inferArgSchema/inferArgArr*/inferArgTypedCtor)
|
|
269
|
+
// live in analyze.js — pure AST→fact resolvers shared across fixpoint phases.
|
|
270
|
+
// Per-caller analysis is stable across fixpoint iterations — precompute once.
|
|
271
|
+
// callerCtx[null] (top-level) uses module globals for both locals and valTypes.
|
|
272
|
+
const phase = createPhaseState()
|
|
273
|
+
const { callerCtx } = phase
|
|
274
|
+
const intConstArg = (arg) => {
|
|
275
|
+
let raw = null
|
|
276
|
+
if (typeof arg === 'number') raw = arg
|
|
277
|
+
else if (Array.isArray(arg) && arg[0] == null && typeof arg[1] === 'number') raw = arg[1]
|
|
278
|
+
else if (Array.isArray(arg) && arg[0] === 'u-' && typeof arg[1] === 'number') raw = -arg[1]
|
|
279
|
+
else if (typeof arg === 'string' && ctx.scope.constInts?.has(arg)) raw = ctx.scope.constInts.get(arg)
|
|
280
|
+
return (raw != null && Number.isInteger(raw) && raw >= -2147483648 && raw <= 2147483647) ? raw : null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const runCallsiteLattice = (rules) => {
|
|
284
|
+
for (let s = 0; s < callSites.length; s++) {
|
|
285
|
+
const { callee, argList, callerFunc } = callSites[s]
|
|
286
|
+
const func = ctx.func.map.get(callee)
|
|
287
|
+
if (!func || func.exported || valueUsed.has(callee)) continue
|
|
288
|
+
const ctxEntry = callerCtx.get(callerFunc)
|
|
289
|
+
if (!ctxEntry) continue
|
|
290
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
291
|
+
const paramFacts = new Map()
|
|
292
|
+
const state = {
|
|
293
|
+
callee, callerFunc, argList, func, restIdx,
|
|
294
|
+
callerLocals: ctxEntry.callerLocals,
|
|
295
|
+
callerValTypes: ctxEntry.callerValTypes,
|
|
296
|
+
callerParamFacts(key) {
|
|
297
|
+
if (!paramFacts.has(key)) paramFacts.set(key, callerParamFactMap(paramReps, callerFunc, key))
|
|
298
|
+
return paramFacts.get(key)
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
302
|
+
const r = ensureParamRep(paramReps, callee, k)
|
|
303
|
+
if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
|
|
304
|
+
const arg = argList[k]
|
|
305
|
+
for (const rule of rules) rule.apply(r, arg, k, state)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const poison = field => r => { r[field] = null }
|
|
311
|
+
// Default-aware val inference. Adds two fallbacks beyond inferArgType's
|
|
312
|
+
// body-local `callerValTypes` lookup so a hot recursive helper like
|
|
313
|
+
// `uleb(n, buffer = []) { ... return uleb(n, buffer) }` resolves the
|
|
314
|
+
// recursive `buffer` arg to VAL.ARRAY (via callerParamFacts on iter 2,
|
|
315
|
+
// or via the caller's own default expression on iter 1).
|
|
316
|
+
const inferValAtSite = (arg, state) => {
|
|
317
|
+
const v = inferArgType(arg, state.callerValTypes)
|
|
318
|
+
if (v != null) return v
|
|
319
|
+
if (typeof arg !== 'string') return null
|
|
320
|
+
const fromParam = state.callerParamFacts('val')?.get(arg)
|
|
321
|
+
if (fromParam != null) return fromParam
|
|
322
|
+
const def = state.callerFunc?.defaults?.[arg]
|
|
323
|
+
return def != null ? valTypeOf(def) || null : null
|
|
324
|
+
}
|
|
325
|
+
// Substitute the default expression for a missing positional arg, so
|
|
326
|
+
// `uleb(n)` doesn't poison buffer.val despite `buffer = []` provably
|
|
327
|
+
// yielding VAL.ARRAY at runtime — unblocks inline ARRAY len/push fast
|
|
328
|
+
// paths in encode.js's hot uleb/i32/i64 helpers.
|
|
329
|
+
const defaultArg = (state, k) => {
|
|
330
|
+
const pname = state.func.sig.params[k]?.name
|
|
331
|
+
return pname != null ? state.func.defaults?.[pname] : null
|
|
332
|
+
}
|
|
333
|
+
const mergeRule = (field, infer) => ({
|
|
334
|
+
missing(r, k, state) {
|
|
335
|
+
if (r[field] === null) return
|
|
336
|
+
const def = defaultArg(state, k)
|
|
337
|
+
if (def != null) mergeParamFact(r, field, infer(def, k, state))
|
|
338
|
+
else r[field] = null
|
|
339
|
+
},
|
|
340
|
+
apply(r, arg, k, state) {
|
|
341
|
+
if (r[field] !== null) mergeParamFact(r, field, infer(arg, k, state))
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
const runFixpoint = () => runCallsiteLattice([
|
|
345
|
+
mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state)),
|
|
346
|
+
{
|
|
347
|
+
missing: poison('wasm'),
|
|
348
|
+
apply(r, arg, _k, state) {
|
|
349
|
+
if (r.wasm === null) return
|
|
350
|
+
const wt = exprType(arg, state.callerLocals)
|
|
351
|
+
if (r.wasm === undefined) r.wasm = wt
|
|
352
|
+
else if (r.wasm !== wt) r.wasm = null
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
mergeRule('schemaId', (arg, _k, state) => inferArgSchema(arg, state.callerParamFacts('schemaId'))),
|
|
356
|
+
{
|
|
357
|
+
missing: poison('intConst'),
|
|
358
|
+
apply(r, arg, k, state) {
|
|
359
|
+
if (k === state.restIdx) r.intConst = null
|
|
360
|
+
else if (r.intConst !== null) mergeParamFact(r, 'intConst', intConstArg(arg))
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
])
|
|
364
|
+
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
365
|
+
runCallsiteLattice([mergeRule(field, (arg, _k, state) =>
|
|
366
|
+
inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
|
|
367
|
+
)])
|
|
368
|
+
}
|
|
369
|
+
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArgArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
370
|
+
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArgArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
371
|
+
runFixpoint()
|
|
372
|
+
runFixpoint()
|
|
373
|
+
|
|
374
|
+
// Apply i32 specialization: for non-value-used funcs with consistent i32 call
|
|
375
|
+
// sites and no defaults/rest at that position, narrow sig.params[k].type.
|
|
376
|
+
// Exports too — boundary wrapper handles the f64→i32 truncation at the JS edge.
|
|
377
|
+
applyI32ParamSpecialization(paramReps, valueUsed)
|
|
378
|
+
|
|
379
|
+
// intConst validation: a param marked with a unanimous integer literal at every call
|
|
380
|
+
// site is only safe to substitute if the body never reassigns it. Clear intConst on any
|
|
381
|
+
// param whose name appears on the LHS of an assignment / `++` / `--`. Skip exported
|
|
382
|
+
// (callable from JS with arbitrary value), value-used (closure callees), raw, defaulted,
|
|
383
|
+
// and rest params — same exclusions as the wasm-narrowing pass above.
|
|
384
|
+
validateIntConstParams(paramReps, valueUsed)
|
|
385
|
+
|
|
386
|
+
// Pointer-ABI specialization: for non-forwarding pointer params consistent across
|
|
387
|
+
// call sites, narrow from NaN-boxed f64 to i32 offset. Eliminates per-call __ptr_offset
|
|
388
|
+
// extraction + f64→i64→i32 reinterpret chains that dominate watr-style compilers.
|
|
389
|
+
// Safety:
|
|
390
|
+
// - exclude ARRAY (forwards on realloc — f64 NaN-box is a stable identity) and
|
|
391
|
+
// STRING (SSO vs heap dual encoding depends on ptr-type bits we'd drop).
|
|
392
|
+
// - exclude CLOSURE/TYPED (aux bits carry schema/element-type, lost with offset).
|
|
393
|
+
// - exclude params with defaults (nullish sentinel needs the f64 NaN space).
|
|
394
|
+
// - exclude rest position (array pack/unpack stays f64).
|
|
395
|
+
applyPointerParamAbi(paramReps, valueUsed)
|
|
396
|
+
|
|
397
|
+
// E: Result-type monomorphization — narrow sig.results[0] to 'i32' when body only
|
|
398
|
+
// produces i32 values. Fixpoint: a call to another narrowed func now contributes i32;
|
|
399
|
+
// iterate until stable so chains of i32-only helpers all narrow together.
|
|
400
|
+
// Safety: skip exported (JS boundary preserves number semantics), value-used (closure
|
|
401
|
+
// trampolines assume f64 result), raw WAT, multi-value. `undefined` return = skip.
|
|
402
|
+
// exprType already consults ctx.func.map for narrowed user-function results
|
|
403
|
+
// (analyze.js exprType `()` branch), plus the Math.imul/Math.clz32/charCodeAt
|
|
404
|
+
// stdlib subset and primitive-op rules. Earlier we had a local shim here that
|
|
405
|
+
// shadowed exprType's stdlib rules with `return 'f64'` for any non-user call;
|
|
406
|
+
// unifying through exprType lets a single rule (math.imul → i32) flow through
|
|
407
|
+
// to mix-style helpers (`(h, x) => Math.imul(h ^ (x|0), C)`) and unblocks the
|
|
408
|
+
// E-phase result narrowing on every call site that consumes them.
|
|
409
|
+
const exprTypeWithCalls = exprType
|
|
410
|
+
// Body-driven: safe for exports — the result type is determined by what the body
|
|
411
|
+
// computes, not by what JS callers might pass. JS-visible f64 ABI is restored at
|
|
412
|
+
// the boundary via a synthesized wrapper (see synthesizeBoundaryWrappers below).
|
|
413
|
+
// Shared pool for E (numeric), E2 (valType) and E3 (ptr) narrowing — same predicate.
|
|
414
|
+
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
415
|
+
let changed = true
|
|
416
|
+
while (changed) {
|
|
417
|
+
changed = false
|
|
418
|
+
for (const func of funcsWithNarrowableResult) {
|
|
419
|
+
if (func.sig.results[0] === 'i32') continue
|
|
420
|
+
const body = func.body
|
|
421
|
+
// Bare `return;` produces undef (f64) — narrowing to i32 would lose that.
|
|
422
|
+
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
423
|
+
const exprs = returnExprs(body)
|
|
424
|
+
if (!exprs.length) continue
|
|
425
|
+
// Skip narrowing when any return-tail is `>>>` (unsigned uint32). Narrowing to i32
|
|
426
|
+
// loses the unsigned interpretation: the wrapper rebox via `f64.convert_i32_s` would
|
|
427
|
+
// sign-flip values with bit 31 set, breaking the canonical `(x >>> 0)` uint32 idiom.
|
|
428
|
+
// A future pass could track sig.unsignedResult and emit `f64.convert_i32_u` instead.
|
|
429
|
+
if (exprs.some(e => Array.isArray(e) && e[0] === '>>>')) continue
|
|
430
|
+
const savedCurrent = ctx.func.current
|
|
431
|
+
ctx.func.current = func.sig
|
|
432
|
+
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
433
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
434
|
+
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
435
|
+
ctx.func.current = savedCurrent
|
|
436
|
+
if (allI32) { func.sig.results = ['i32']; changed = true }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// E2: VAL-type result inference — if a function always returns the same VAL kind,
|
|
441
|
+
// record it so callers inherit that type (enables static dispatch on .length, .[],
|
|
442
|
+
// .prop through a call chain). Fixpoint propagates through helper chains.
|
|
443
|
+
// Safety: skip exported (host sees raw f64), value-used (indirect call signature).
|
|
444
|
+
// Shim so calls to already-typed funcs contribute their result type.
|
|
445
|
+
const valTypeOfWithCalls = (expr, localValTypes) => {
|
|
446
|
+
if (expr == null) return null
|
|
447
|
+
if (typeof expr === 'string') return localValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
448
|
+
if (!Array.isArray(expr)) return valTypeOf(expr)
|
|
449
|
+
const [op, ...args] = expr
|
|
450
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
451
|
+
const f = ctx.func.map.get(args[0])
|
|
452
|
+
if (f?.valResult) return f.valResult
|
|
453
|
+
}
|
|
454
|
+
if (op === '?:') {
|
|
455
|
+
const a = valTypeOfWithCalls(args[1], localValTypes), b = valTypeOfWithCalls(args[2], localValTypes)
|
|
456
|
+
return a && a === b ? a : null
|
|
457
|
+
}
|
|
458
|
+
if (op === '&&' || op === '||') {
|
|
459
|
+
const a = valTypeOfWithCalls(args[0], localValTypes), b = valTypeOfWithCalls(args[1], localValTypes)
|
|
460
|
+
return a && a === b ? a : null
|
|
461
|
+
}
|
|
462
|
+
return valTypeOf(expr)
|
|
463
|
+
}
|
|
464
|
+
// Body-driven valResult inference: same safety analysis as numeric narrowing
|
|
465
|
+
// above — exports OK because boundary wrapper restores f64 ABI for JS callers.
|
|
466
|
+
changed = true
|
|
467
|
+
while (changed) {
|
|
468
|
+
changed = false
|
|
469
|
+
for (const func of funcsWithNarrowableResult) {
|
|
470
|
+
if (func.valResult) continue
|
|
471
|
+
const body = func.body
|
|
472
|
+
const isBlock = isBlockBody(body)
|
|
473
|
+
if (isBlock && hasBareReturn(body)) continue
|
|
474
|
+
const exprs = returnExprs(body)
|
|
475
|
+
if (!exprs.length) continue
|
|
476
|
+
const localValTypes = isBlock ? analyzeBody(body).valTypes : new Map()
|
|
477
|
+
// Params of this function contribute no known VAL type yet (paramReps may help later).
|
|
478
|
+
const vt0 = valTypeOfWithCalls(exprs[0], localValTypes)
|
|
479
|
+
if (!vt0) continue
|
|
480
|
+
const allSame = exprs.every(e => valTypeOfWithCalls(e, localValTypes) === vt0)
|
|
481
|
+
if (allSame) { func.valResult = vt0; changed = true }
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
486
|
+
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
487
|
+
// D-pass arrayElemSchema/val fixpoints so `const rows = initRows()` in main
|
|
488
|
+
// resolves to VAL.ARRAY (lets runKernel pick up r.val=ARRAY) and its arr-elem
|
|
489
|
+
// schema (sets paramReps[runKernel][0].arrayElemSchema=sid).
|
|
490
|
+
// Cache invalidation: analyzeBody.valTypes is body-keyed, and entries cached
|
|
491
|
+
// during the first D pass have stale (null) `valTypeOf(call)` results because
|
|
492
|
+
// valResult was unset back then.
|
|
493
|
+
narrowReturnArrayElems('arrayElemSchema', paramReps, valueUsed)
|
|
494
|
+
narrowReturnArrayElems('arrayElemValType', paramReps, valueUsed)
|
|
495
|
+
phase.invalidateBodyFacts()
|
|
496
|
+
phase.refreshValTypes()
|
|
497
|
+
// Re-observe schema slot val-types now that E2 has set `valResult` on user
|
|
498
|
+
// funcs. First pass runs in collectProgramFacts before valResult is known, so
|
|
499
|
+
// a slot like `cs` in `{ ..., cs }` (where `cs = checksum(out)`) gets observed
|
|
500
|
+
// as null. observeSlot's first-wins-then-clash rule lets a later precise
|
|
501
|
+
// observation upgrade `undefined` → NUMBER without poisoning earlier
|
|
502
|
+
// monomorphic observations.
|
|
503
|
+
if (hasSchemaLiterals) observeProgramSlots(ast)
|
|
504
|
+
// Clear sticky-null on val/schemaId — first 2 passes ran with valResult unset, so
|
|
505
|
+
// call args resolving via `f.valResult` returned null and got stuck. Re-running
|
|
506
|
+
// with refreshed callerValTypes lets these flow.
|
|
507
|
+
clearStickyNull(paramReps, 'val')
|
|
508
|
+
clearStickyNull(paramReps, 'schemaId')
|
|
509
|
+
runFixpoint()
|
|
510
|
+
// Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
|
|
511
|
+
runArrFixpoint()
|
|
512
|
+
runArrFixpoint()
|
|
513
|
+
// Parallel arr-elem-val fixpoint (NUMBER/STRING/…). Twice for transitive closure
|
|
514
|
+
// through helper chains: `init()→main→runKernel`.
|
|
515
|
+
runArrValTypeFixpoint()
|
|
516
|
+
runArrValTypeFixpoint()
|
|
517
|
+
// E3: Result-type pointer narrowing — when valResult is a non-ambiguous pointer kind
|
|
518
|
+
// with constant aux, narrow sig.results[0] from f64 to i32 and tag sig.ptrKind/.ptrAux.
|
|
519
|
+
// Eliminates the f64.reinterpret_i64+i64.or rebox at every return and the
|
|
520
|
+
// i32.wrap_i64+i64.reinterpret_f64 unbox at every callsite that uses the value as a
|
|
521
|
+
// pointer (load .[], .length, .prop slot dispatch).
|
|
522
|
+
// - SET/MAP/BUFFER: aux always 0 — no per-callsite aux preservation needed.
|
|
523
|
+
// - OBJECT: aux is schema-id; narrow only when all return exprs share a constant
|
|
524
|
+
// schema (literal `{a,b,c}`, schemaId-bound param, module-bound var, or call to
|
|
525
|
+
// another OBJECT-narrowed func). Caller picks aux up via callIR.ptrAux → readVar →
|
|
526
|
+
// repByLocal.schemaId, restoring property-slot dispatch through the call boundary.
|
|
527
|
+
// Safety: ARRAY forwards on realloc (no narrowing). STRING dual-encoded SSO/heap.
|
|
528
|
+
// CLOSURE/TYPED also carry meaningful aux — TYPED narrowing is a follow-up. Body must
|
|
529
|
+
// be a guaranteed-return form — fallthrough fallback i32.const 0 would be a valid
|
|
530
|
+
// offset 0 of the narrowed kind, not undefined.
|
|
531
|
+
const PTR_RESULT_KINDS_NOAUX = new Set([VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
532
|
+
// Schema-id inference for a return expression. Returns id (number), or null if unknown
|
|
533
|
+
// / not constant. Mirrors inferArgSchema but extends with calls to already-narrowed
|
|
534
|
+
// OBJECT-result funcs (fixpoint propagation through helper chains).
|
|
535
|
+
const schemaIdOfReturn = (expr, paramSchemasMap) => {
|
|
536
|
+
if (typeof expr === 'string') {
|
|
537
|
+
if (paramSchemasMap?.has(expr)) return paramSchemasMap.get(expr)
|
|
538
|
+
if (ctx.schema.vars.has(expr)) return ctx.schema.vars.get(expr)
|
|
539
|
+
return null
|
|
540
|
+
}
|
|
541
|
+
if (!Array.isArray(expr)) return null
|
|
542
|
+
const [op, ...args] = expr
|
|
543
|
+
if (op === '{}') {
|
|
544
|
+
// Object literal: bail to null on block body, dynamic key, or spread.
|
|
545
|
+
const parsed = staticObjectProps(args)
|
|
546
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
547
|
+
}
|
|
548
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
549
|
+
const f = ctx.func.map.get(args[0])
|
|
550
|
+
if (f?.valResult === VAL.OBJECT && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
if (op === '?:') {
|
|
554
|
+
const a = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
555
|
+
const b = schemaIdOfReturn(args[2], paramSchemasMap)
|
|
556
|
+
return a != null && a === b ? a : null
|
|
557
|
+
}
|
|
558
|
+
if (op === '&&' || op === '||') {
|
|
559
|
+
const a = schemaIdOfReturn(args[0], paramSchemasMap)
|
|
560
|
+
const b = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
561
|
+
return a != null && a === b ? a : null
|
|
562
|
+
}
|
|
563
|
+
return null
|
|
564
|
+
}
|
|
565
|
+
// Per-body local elemAux map: scans `let/const x = new TypedArray(...)` decls so
|
|
566
|
+
// a return like `let a = new Float64Array(...); return a` resolves to a constant
|
|
567
|
+
// aux. Result calls + ?: are handled inline in typedAuxOfReturn.
|
|
568
|
+
const localElemAuxMap = (body) => {
|
|
569
|
+
const m = new Map()
|
|
570
|
+
const walk = (n) => {
|
|
571
|
+
if (!Array.isArray(n)) return
|
|
572
|
+
const op = n[0]
|
|
573
|
+
if (op === '=>') return
|
|
574
|
+
if ((op === 'let' || op === 'const') && n.length > 1) {
|
|
575
|
+
for (let i = 1; i < n.length; i++) {
|
|
576
|
+
const a = n[i]
|
|
577
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
578
|
+
const aux = typedElemAux(typedElemCtor(a[2]))
|
|
579
|
+
if (aux != null) m.set(a[1], aux)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
584
|
+
}
|
|
585
|
+
walk(body)
|
|
586
|
+
return m
|
|
587
|
+
}
|
|
588
|
+
const typedAuxOfReturn = (expr, localElemMap) => {
|
|
589
|
+
if (typeof expr === 'string') return localElemMap?.get(expr) ?? null
|
|
590
|
+
if (!Array.isArray(expr)) return null
|
|
591
|
+
const [op, ...args] = expr
|
|
592
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
593
|
+
if (args[0].startsWith('new.')) {
|
|
594
|
+
const ctor = typedElemCtor(expr)
|
|
595
|
+
return ctor != null ? typedElemAux(ctor) : null
|
|
596
|
+
}
|
|
597
|
+
const f = ctx.func.map.get(args[0])
|
|
598
|
+
if (f?.valResult === VAL.TYPED && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
599
|
+
return null
|
|
600
|
+
}
|
|
601
|
+
if (op === '?:') {
|
|
602
|
+
const a = typedAuxOfReturn(args[1], localElemMap)
|
|
603
|
+
const b = typedAuxOfReturn(args[2], localElemMap)
|
|
604
|
+
return a != null && a === b ? a : null
|
|
605
|
+
}
|
|
606
|
+
if (op === '&&' || op === '||') {
|
|
607
|
+
const a = typedAuxOfReturn(args[0], localElemMap)
|
|
608
|
+
const b = typedAuxOfReturn(args[1], localElemMap)
|
|
609
|
+
return a != null && a === b ? a : null
|
|
610
|
+
}
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
613
|
+
// Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so outer's
|
|
614
|
+
// call to inner contributes a known schema-id.
|
|
615
|
+
let narrowChanged = true
|
|
616
|
+
while (narrowChanged) {
|
|
617
|
+
narrowChanged = false
|
|
618
|
+
for (const func of funcsWithNarrowableResult) {
|
|
619
|
+
if (!func.valResult) continue
|
|
620
|
+
if (func.sig.results[0] !== 'f64') continue
|
|
621
|
+
const isBlock = isBlockBody(func.body)
|
|
622
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
623
|
+
if (PTR_RESULT_KINDS_NOAUX.has(func.valResult)) {
|
|
624
|
+
func.sig.results = ['i32']
|
|
625
|
+
func.sig.ptrKind = func.valResult
|
|
626
|
+
narrowChanged = true
|
|
627
|
+
continue
|
|
628
|
+
}
|
|
629
|
+
const exprs = returnExprs(func.body)
|
|
630
|
+
if (!exprs.length) continue
|
|
631
|
+
if (func.valResult === VAL.OBJECT) {
|
|
632
|
+
const paramSchemasMap = callerParamFactMap(paramReps, func, 'schemaId')
|
|
633
|
+
const sid0 = schemaIdOfReturn(exprs[0], paramSchemasMap)
|
|
634
|
+
if (sid0 == null) continue
|
|
635
|
+
if (!exprs.every(e => schemaIdOfReturn(e, paramSchemasMap) === sid0)) continue
|
|
636
|
+
func.sig.results = ['i32']
|
|
637
|
+
func.sig.ptrKind = VAL.OBJECT
|
|
638
|
+
func.sig.ptrAux = sid0
|
|
639
|
+
narrowChanged = true
|
|
640
|
+
} else if (func.valResult === VAL.TYPED) {
|
|
641
|
+
const localMap = isBlock ? localElemAuxMap(func.body) : null
|
|
642
|
+
const aux0 = typedAuxOfReturn(exprs[0], localMap)
|
|
643
|
+
if (aux0 == null) continue
|
|
644
|
+
if (!exprs.every(e => typedAuxOfReturn(e, localMap) === aux0)) continue
|
|
645
|
+
func.sig.results = ['i32']
|
|
646
|
+
func.sig.ptrKind = VAL.TYPED
|
|
647
|
+
func.sig.ptrAux = aux0
|
|
648
|
+
narrowChanged = true
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// F: Cross-call typed-array element ctor propagation. Runs AFTER E3 so that
|
|
654
|
+
// calls to user functions returning a TYPED-narrowed pointer (with constant
|
|
655
|
+
// ptrAux, e.g. mkInput → Float64Array) contribute their element type to the
|
|
656
|
+
// caller's local typedElem map. Result: callees pick up `ctx.types.typedElem`
|
|
657
|
+
// for their own params and `arr[i]` reads emit a direct `f64.load` instead of
|
|
658
|
+
// the runtime `__is_str_key + __typed_idx` dispatch — closes the largest
|
|
659
|
+
// chunk of the JS→wasm gap on f64-heavy hot loops.
|
|
660
|
+
// (Helpers `inferArgTypedCtor`/`ctorFromElemAux` live in analyze.js so the
|
|
661
|
+
// bimorphic-typed specialization pass below can reuse them.)
|
|
662
|
+
// Per-caller typed-elem map, recomputed now that E3 has tagged helper sigs.
|
|
663
|
+
// Cache invalidation: analyzeBody.typedElems reads `ctx.func.map.get(...).sig.ptrKind`
|
|
664
|
+
// for `let x = mkInput(...)` decls; entries cached during the initial walk
|
|
665
|
+
// (before E3 ran) are stale (mkInput's ptrKind was unset then).
|
|
666
|
+
phase.invalidateBodyFacts()
|
|
667
|
+
const callerTypedCtx = phase.callerTyped()
|
|
668
|
+
// Two-pass fixpoint: lets a caller's params, once typed, propagate further to
|
|
669
|
+
// its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
|
|
670
|
+
// for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
|
|
671
|
+
// (same shape — field/inferFn/elemsCtxMap parameterization).
|
|
672
|
+
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferArgTypedCtor, callerTypedCtx)
|
|
673
|
+
runTypedFixpoint()
|
|
674
|
+
runTypedFixpoint()
|
|
675
|
+
|
|
676
|
+
// G: TYPED pointer-ABI narrowing — once .typedCtor agrees on a single
|
|
677
|
+
// ctor across all call sites, narrow the param from NaN-boxed f64 to raw
|
|
678
|
+
// i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
|
|
679
|
+
// per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
|
|
680
|
+
// that today dominates hot loops dominated by typed-array indexing.
|
|
681
|
+
// Call sites coerce via emitArgForParam → ptrOffsetIR(arg, VAL.TYPED).
|
|
682
|
+
// Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
|
|
683
|
+
// exported, value-used, raw, defaults, rest position.
|
|
684
|
+
applyTypedPointerParamAbi(paramReps, valueUsed)
|
|
685
|
+
|
|
686
|
+
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
687
|
+
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
688
|
+
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
689
|
+
// for caller's params, so inferArgType returns null and paramReps[callee][k].val is
|
|
690
|
+
// sticky null. With ptrKind enriching callerValTypes, sum's arr gets val=TYPED in
|
|
691
|
+
// its rep, letting array.js skip __is_str_key + __str_idx dispatch on `arr[i]`.
|
|
692
|
+
enrichCallerValTypesFromPointerParams(callerCtx)
|
|
693
|
+
clearStickyNull(paramReps, 'val')
|
|
694
|
+
runFixpoint()
|
|
695
|
+
|
|
696
|
+
// I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
|
|
697
|
+
// ran before E narrowed any result types, so callerLocals saw `let h = mix(...)`
|
|
698
|
+
// as f64 (mix's result was f64 then). After E narrowed mix's result to i32,
|
|
699
|
+
// exprType (which now consults func.sig.results for user calls) sees `h` as i32.
|
|
700
|
+
// Refresh callerLocals + clear sticky-null wasm + re-run fixpoint + re-apply
|
|
701
|
+
// numeric narrowing to propagate i32 through chains of i32-only helpers
|
|
702
|
+
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
703
|
+
// only after E phase narrowed mix's result).
|
|
704
|
+
phase.refreshLocals()
|
|
705
|
+
// Reset wasm field unconditionally — first pass populated it from stale callerLocals
|
|
706
|
+
// (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
|
|
707
|
+
// yet). clearStickyNull only resets null; here we need to reset f64-observed too
|
|
708
|
+
// so the refreshed exprType view propagates.
|
|
709
|
+
resetParamWasmFacts(paramReps)
|
|
710
|
+
runFixpoint()
|
|
711
|
+
// Don't steal typed-array params from specializeBimorphicTyped: F phase parks
|
|
712
|
+
// bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
|
|
713
|
+
// ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
|
|
714
|
+
// so r.wasm flips to 'i32' here — but narrowing now breaks the clone path
|
|
715
|
+
// that still needs to mint per-ctor sigs with ptrKind=TYPED, ptrAux=ctor-aux.
|
|
716
|
+
applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped: true })
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Phase: bimorphic typed-array param specialization.
|
|
721
|
+
*
|
|
722
|
+
* For each non-exported user function with a typed-array param that F/G-phase
|
|
723
|
+
* left bimorphic (paramReps[name][k].typedCtor === null because two or more call sites
|
|
724
|
+
* disagreed on the elem-ctor — e.g. `sum(f64)` and `sum(i32)`), clone the
|
|
725
|
+
* function once per concrete ctor seen at the call sites, narrow each clone's
|
|
726
|
+
* sig.params[k] to a monomorphic typed pointer ABI (type='i32', ptrKind=TYPED,
|
|
727
|
+
* ptrAux=ctor's aux), and rewrite the call AST nodes to dispatch to the right
|
|
728
|
+
* clone. The original survives as a fallback for any non-static call sites
|
|
729
|
+
* (e.g. inside arrow bodies); treeshake removes it if every site got rewritten.
|
|
730
|
+
*
|
|
731
|
+
* Why this matters: without specialization, `arr[i]` inside `sum` falls into
|
|
732
|
+
* the runtime `__typed_idx` path on every iteration — V8 can't inline a wasm
|
|
733
|
+
* call dominated by a switch on elem type. After specialization, each clone's
|
|
734
|
+
* `arr[i]` lowers to a direct `f64.load` (or `i32.load + f64.convert`) with
|
|
735
|
+
* the elem-ctor known at compile time. On poly bench this is the difference
|
|
736
|
+
* between ~5 ms and matching AS at ~1 ms.
|
|
737
|
+
*
|
|
738
|
+
* Safety mirrors G-phase: skip exported, raw, value-used, defaulted, rest, or
|
|
739
|
+
* already-i32 params. Bounded by MAX_CLONES_PER_FN to guard against polymorphic
|
|
740
|
+
* blow-up (≥5 distinct ctors at one site → no specialization).
|
|
741
|
+
*/
|
|
742
|
+
export function specializeBimorphicTyped(programFacts) {
|
|
743
|
+
const { callSites, valueUsed, paramReps } = programFacts
|
|
744
|
+
const MAX_CLONES_PER_FN = 4
|
|
745
|
+
|
|
746
|
+
// Per-callee static-call-site index. Built once; cheap.
|
|
747
|
+
const sitesByCallee = new Map()
|
|
748
|
+
for (const cs of callSites) {
|
|
749
|
+
const list = sitesByCallee.get(cs.callee)
|
|
750
|
+
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Per-caller typedElem map (literal `new TypedArray(N)` bindings inside body).
|
|
754
|
+
const callerTypedCtx = new Map()
|
|
755
|
+
callerTypedCtx.set(null, ctx.scope.globalTypedElem || new Map())
|
|
756
|
+
for (const func of ctx.func.list) {
|
|
757
|
+
if (!func.body || func.raw) continue
|
|
758
|
+
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
759
|
+
}
|
|
760
|
+
// Per-caller typed-param map: caller's own params that F/G already narrowed
|
|
761
|
+
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
762
|
+
const callerTypedParamsCtx = new Map()
|
|
763
|
+
for (const func of ctx.func.list) {
|
|
764
|
+
const m = callerParamFactMap(paramReps, func, 'typedCtor') || null
|
|
765
|
+
let acc = m
|
|
766
|
+
if (func.sig?.params) for (const p of func.sig.params) {
|
|
767
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
768
|
+
acc ||= new Map()
|
|
769
|
+
if (!acc.has(p.name)) acc.set(p.name, ctorFromElemAux(p.ptrAux))
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (acc) callerTypedParamsCtx.set(func, acc)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Snapshot ctx.func.list — we'll be appending clones during the loop.
|
|
776
|
+
const originals = ctx.func.list.slice()
|
|
777
|
+
for (const func of originals) {
|
|
778
|
+
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
779
|
+
if (!func.body) continue
|
|
780
|
+
if (func.rest) continue
|
|
781
|
+
const reps = paramReps.get(func.name)
|
|
782
|
+
if (!reps) continue
|
|
783
|
+
const sites = sitesByCallee.get(func.name)
|
|
784
|
+
if (!sites || sites.length < 2) continue
|
|
785
|
+
|
|
786
|
+
// Find sticky-bimorphic typed-param positions left by F-phase.
|
|
787
|
+
const bimorphic = []
|
|
788
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
789
|
+
const r = reps.get(k)
|
|
790
|
+
if (!r || r.val !== VAL.TYPED || r.typedCtor !== null) continue
|
|
791
|
+
const p = func.sig.params[k]
|
|
792
|
+
if (p.type === 'i32') continue
|
|
793
|
+
if (func.defaults?.[p.name] != null) continue
|
|
794
|
+
bimorphic.push(k)
|
|
795
|
+
}
|
|
796
|
+
if (bimorphic.length === 0) continue
|
|
797
|
+
|
|
798
|
+
// For each site, infer the ctor combination across bimorphic positions.
|
|
799
|
+
// Abort if any site has unknown ctor at any bimorphic position — we can't
|
|
800
|
+
// route that call to a specific clone without it.
|
|
801
|
+
const siteCombos = []
|
|
802
|
+
let abort = false
|
|
803
|
+
for (const site of sites) {
|
|
804
|
+
const callerTypedElems = callerTypedCtx.get(site.callerFunc)
|
|
805
|
+
const callerTypedParams = callerTypedParamsCtx.get(site.callerFunc)
|
|
806
|
+
const combo = []
|
|
807
|
+
for (const k of bimorphic) {
|
|
808
|
+
if (k >= site.argList.length) { abort = true; break }
|
|
809
|
+
const c = inferArgTypedCtor(site.argList[k], callerTypedElems, callerTypedParams)
|
|
810
|
+
if (c == null || typedElemAux(c) == null) { abort = true; break }
|
|
811
|
+
combo.push(c)
|
|
812
|
+
}
|
|
813
|
+
if (abort) break
|
|
814
|
+
siteCombos.push(combo)
|
|
815
|
+
}
|
|
816
|
+
if (abort) continue
|
|
817
|
+
|
|
818
|
+
// Distinct combos seen across call sites.
|
|
819
|
+
const distinct = new Map()
|
|
820
|
+
for (const combo of siteCombos) {
|
|
821
|
+
const key = combo.join('|')
|
|
822
|
+
if (!distinct.has(key)) distinct.set(key, combo)
|
|
823
|
+
}
|
|
824
|
+
if (distinct.size < 2) continue // F-phase already mono — nothing to do
|
|
825
|
+
if (distinct.size > MAX_CLONES_PER_FN) continue // polymorphic blow-up
|
|
826
|
+
|
|
827
|
+
// Build one clone per distinct combo.
|
|
828
|
+
const cloneByKey = new Map()
|
|
829
|
+
for (const [key, combo] of distinct) {
|
|
830
|
+
const suffix = combo.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
|
|
831
|
+
let cloneName = `${func.name}$${suffix}`
|
|
832
|
+
let n = 0
|
|
833
|
+
while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
|
|
834
|
+
|
|
835
|
+
const cloneSig = {
|
|
836
|
+
...func.sig,
|
|
837
|
+
params: func.sig.params.map(p => ({ ...p })),
|
|
838
|
+
results: [...func.sig.results],
|
|
839
|
+
}
|
|
840
|
+
for (let i = 0; i < bimorphic.length; i++) {
|
|
841
|
+
const k = bimorphic[i]
|
|
842
|
+
const aux = typedElemAux(combo[i])
|
|
843
|
+
const p = cloneSig.params[k]
|
|
844
|
+
p.type = 'i32'
|
|
845
|
+
p.ptrKind = VAL.TYPED
|
|
846
|
+
p.ptrAux = aux
|
|
847
|
+
}
|
|
848
|
+
const clone = { ...func, name: cloneName, sig: cloneSig }
|
|
849
|
+
ctx.func.list.push(clone)
|
|
850
|
+
ctx.func.map.set(cloneName, clone)
|
|
851
|
+
ctx.func.names.add(cloneName)
|
|
852
|
+
|
|
853
|
+
// Mirror per-param reps under the clone's name with mono ctors at bimorphic
|
|
854
|
+
// positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
|
|
855
|
+
// `arr[i]` lowers to direct typed load.
|
|
856
|
+
const cloneReps = new Map()
|
|
857
|
+
for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
858
|
+
for (let i = 0; i < bimorphic.length; i++) {
|
|
859
|
+
const k = bimorphic[i]
|
|
860
|
+
const r = cloneReps.get(k) || {}
|
|
861
|
+
r.typedCtor = combo[i]
|
|
862
|
+
r.val = VAL.TYPED
|
|
863
|
+
cloneReps.set(k, r)
|
|
864
|
+
}
|
|
865
|
+
paramReps.set(cloneName, cloneReps)
|
|
866
|
+
|
|
867
|
+
cloneByKey.set(key, clone)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Rewrite each site's call AST to point at the matching clone.
|
|
871
|
+
for (let i = 0; i < sites.length; i++) {
|
|
872
|
+
const clone = cloneByKey.get(siteCombos[i].join('|'))
|
|
873
|
+
sites[i].node[1] = clone.name
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Phase: refine ctx.types.anyDynKey using post-narrowSignatures type info.
|
|
880
|
+
*/
|
|
881
|
+
const NON_DYN_VTS = new Set([VAL.TYPED, VAL.ARRAY, VAL.STRING, VAL.BUFFER])
|
|
882
|
+
const TYPED_ARRAY_CTOR = /^(Float|Int|Uint|BigInt|BigUint)(8|16|32|64)(Clamped)?Array$/
|
|
883
|
+
|
|
884
|
+
export function refineDynKeys(programFacts) {
|
|
885
|
+
if (!ctx.types.anyDynKey) return
|
|
886
|
+
const { paramReps, valueUsed } = programFacts
|
|
887
|
+
|
|
888
|
+
// Per-function type map: param vtypes from paramReps, plus locals
|
|
889
|
+
// we can prove are typed arrays from `let v = new TypedArray(...)`. After
|
|
890
|
+
// prepare, that node is `['()', 'new.Float64Array', ...args]`.
|
|
891
|
+
const buildTypeMap = (funcName, body, params) => {
|
|
892
|
+
const map = new Map()
|
|
893
|
+
if (params) {
|
|
894
|
+
const reps = paramReps.get(funcName)
|
|
895
|
+
if (reps) for (let i = 0; i < params.length; i++) {
|
|
896
|
+
const t = reps.get(i)?.val
|
|
897
|
+
if (t != null) map.set(params[i].name, t)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const walk = (node) => {
|
|
901
|
+
if (!Array.isArray(node)) return
|
|
902
|
+
const op = node[0]
|
|
903
|
+
if (op === 'let' || op === 'const') {
|
|
904
|
+
for (let i = 1; i < node.length; i++) {
|
|
905
|
+
const d = node[i]
|
|
906
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
907
|
+
const init = d[2]
|
|
908
|
+
let ctor = null
|
|
909
|
+
if (Array.isArray(init) && init[0] === '()' && typeof init[1] === 'string' && init[1].startsWith('new.'))
|
|
910
|
+
ctor = init[1].slice(4)
|
|
911
|
+
if (ctor && TYPED_ARRAY_CTOR.test(ctor)) map.set(d[1], VAL.TYPED)
|
|
912
|
+
else if (Array.isArray(init) && init[0] === '[') map.set(d[1], VAL.ARRAY)
|
|
913
|
+
else if (typeof init === 'string' && map.has(init)) map.set(d[1], map.get(init))
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (op === '=>') return // don't cross into nested arrows; they're separate funcs
|
|
917
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
918
|
+
}
|
|
919
|
+
walk(body)
|
|
920
|
+
return map
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
let real = false
|
|
924
|
+
const visit = (typeMap, node) => {
|
|
925
|
+
if (real || !Array.isArray(node)) return
|
|
926
|
+
const op = node[0]
|
|
927
|
+
if (op === '[]') {
|
|
928
|
+
const idx = node[2]
|
|
929
|
+
if (!isLiteralStr(idx)) {
|
|
930
|
+
const obj = node[1]
|
|
931
|
+
const vt = typeof obj === 'string' ? typeMap.get(obj) : null
|
|
932
|
+
if (!NON_DYN_VTS.has(vt)) real = true
|
|
933
|
+
}
|
|
934
|
+
} else if (op === 'for-in') real = true
|
|
935
|
+
if (op === '=>') return
|
|
936
|
+
for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Live: anything reachable from exports/first-class value uses. Skipping
|
|
940
|
+
// dead helpers (unused benchlib imports) keeps their generic params from
|
|
941
|
+
// pretending to be dyn-key access.
|
|
942
|
+
const isLive = f => f.exported || paramReps.has(f.name) || (valueUsed && valueUsed.has(f.name))
|
|
943
|
+
|
|
944
|
+
const topMap = buildTypeMap(null, null, null)
|
|
945
|
+
for (const f of ctx.func.list) {
|
|
946
|
+
if (real) break
|
|
947
|
+
if (!f.body || !isLive(f)) continue
|
|
948
|
+
visit(buildTypeMap(f.name, f.body, f.sig?.params), f.body)
|
|
949
|
+
}
|
|
950
|
+
if (!real && ctx.module.initFacts?.anyDyn && ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) {
|
|
951
|
+
if (real) break
|
|
952
|
+
visit(topMap, mi)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (!real) ctx.types.anyDynKey = false
|
|
956
|
+
}
|