jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,1565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-analysis passes — type inference, local analysis, capture detection.
|
|
3
|
+
*
|
|
4
|
+
* # Stage contract
|
|
5
|
+
* IN: prepared AST + ctx.func.list (from prepare).
|
|
6
|
+
* OUT: per-function populated `ctx.func.localReps` (val field) + `ctx.func.locals` + `ctx.func.boxed`,
|
|
7
|
+
* module-global `ctx.scope.globalValTypes`, type-analysis `ctx.types.typedElem` /
|
|
8
|
+
* `.dynKeyVars` / `.anyDynKey`.
|
|
9
|
+
*
|
|
10
|
+
* # Passes (all walk AST; none mutate AST itself — only ctx)
|
|
11
|
+
* - boxedCaptures: detect mutably-captured vars → ctx.func.boxed cells
|
|
12
|
+
*
|
|
13
|
+
* Value KIND inference: src/kind.js. WASM local typing: src/type.js. Static eval: src/static.js.
|
|
14
|
+
*
|
|
15
|
+
* Ordering: all passes run per function during compile(). plan.js owns the
|
|
16
|
+
* cross-function dynKey scan via programFacts (results land in ctx.types.dynKeyVars).
|
|
17
|
+
*
|
|
18
|
+
* @module analyze
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { commaList, ASSIGN_OPS, isReassigned, STMT_OPS, isBlockBody, isLiteralStr, isFuncRef, I32_MIN, I32_MAX, isI32, T, extractParams, classifyParam, collectParamNames, alwaysReturns, returnExprs, refsName, REFS_IN_EXPR } from '../ast.js'
|
|
22
|
+
import { ctx, err } from '../ctx.js'
|
|
23
|
+
import { VAL, repOf, repOfGlobal, updateRep, updateGlobalRep, lookupValType, lookupNotString } from '../reps.js'
|
|
24
|
+
import { valTypeOf, jsonConstString, shapeOf, shapeOfObjectLiteralAst } from '../kind.js'
|
|
25
|
+
import { intLiteralValue, nonNegIntLiteral, constIntExpr, NO_VALUE, staticPropertyKey, staticValue, staticObjectProps, staticArrayElems, objLiteralSchemaId, exprSchemaId, inlineArraySid } from '../static.js'
|
|
26
|
+
import { typedElemCtor, MIXED_CTORS, isCondExpr, ternaryCtorOfRhs, scanBoundedLoops, inBoundsCharCodeAt, exprType, intCertainMap } from '../type.js'
|
|
27
|
+
import { TYPED_ELEM_CODE, TYPED_ELEM_VIEW_FLAG, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux, typedElemAux, TYPED_ELEM_NAMES, ctorFromElemAux } from '../../layout.js'
|
|
28
|
+
|
|
29
|
+
// ValueRep field docs + ParamReps lattice helpers — storage lives in src/reps.js.
|
|
30
|
+
|
|
31
|
+
// === ParamReps lattice helpers (cross-call fixpoint) ===
|
|
32
|
+
// programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>. Per-field lattice:
|
|
33
|
+
// undefined unobserved, null sticky-poison (cross-site disagreement), value = consensus.
|
|
34
|
+
|
|
35
|
+
// Cross-call argument inference helpers (`infer*`) live in src/infer.js.
|
|
36
|
+
// paramReps lattice lives in src/param-reps.js.
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-binding use-summary — the substrate the representation analyses share.
|
|
40
|
+
*
|
|
41
|
+
* One traversal classifies every mention of each `let`/`const` binding into a
|
|
42
|
+
* closed taxonomy of use-kinds (`USE.*`). The eligibility analyses below
|
|
43
|
+
* (`scanFlatObjects`, `scanSliceViews`, `unboxablePtrs`) are then *policies* —
|
|
44
|
+
* subset predicates over this summary — rather than three independent body
|
|
45
|
+
* re-walks. The classification logic is the union of those walks; a use-kind
|
|
46
|
+
* exists for every distinction at least one consumer needs.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately NOT a consumer: `analyzeBody`'s `escapes` map. It looks like a
|
|
49
|
+
* fourth escape analysis but is not — it is context-sensitive *taint* over
|
|
50
|
+
* value-expression positions (member access short-circuits, a static index
|
|
51
|
+
* does not escape but a dynamic one does), woven into the main typing walk it
|
|
52
|
+
* shares with six other facts. Re-expressing it here would need `BARE` split
|
|
53
|
+
* by enclosing construct (a plain `name;` statement vs `return name`) and a
|
|
54
|
+
* second traversal — adding a walk, not removing one. It stays where it is.
|
|
55
|
+
*
|
|
56
|
+
* Returns `Map<name, { decls, initRhs, uses }>` for every name the body
|
|
57
|
+
* `let`/`const`-declares. `uses` is a `UseRecord[]`; a record is
|
|
58
|
+
* `{ kind, key?, optional?, computed?, compound?, nullCmp?, callee?, argIndex? }`.
|
|
59
|
+
*
|
|
60
|
+
* Scoping: decls are collected only outside closures (a `let` inside a nested
|
|
61
|
+
* `=>` is a different scope), but uses ARE collected inside closures — every
|
|
62
|
+
* mention there becomes a `CAPTURE`, which every policy treats as
|
|
63
|
+
* disqualifying. A binding shadowed by an inner closure decl is conservatively
|
|
64
|
+
* still flagged `CAPTURE`; sound, since it only ever forfeits an optimization.
|
|
65
|
+
*
|
|
66
|
+
* Order-independent: uses bucket by name, so a mention before its decl is fine.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// === param helpers / AST predicates ===
|
|
72
|
+
|
|
73
|
+
// === Param / closure helpers ===
|
|
74
|
+
|
|
75
|
+
/** Find free variables in AST: referenced in node, not in `bound`, present in `scope`. */
|
|
76
|
+
import {
|
|
77
|
+
findFreeVars, findMutations, boxedCaptures,
|
|
78
|
+
collectI32SafeIndexVars, collectF64StridedIndexVars, narrowUint32, scanBindingUses,
|
|
79
|
+
scanFlatObjects, scanSliceViews, USE,
|
|
80
|
+
} from './analyze-scans.js'
|
|
81
|
+
|
|
82
|
+
export { findFreeVars, findMutations, boxedCaptures } from './analyze-scans.js'
|
|
83
|
+
|
|
84
|
+
const _bodyFactsCache = new WeakMap()
|
|
85
|
+
|
|
86
|
+
// Per-name monotone fact trackers over a pluggable {get,set,delete} store. First
|
|
87
|
+
// observation wins; a conflicting later one poisons the name (and clears the store
|
|
88
|
+
// entry) so a sibling-scope decl (jz hoists `let` to function scope) can't lock in
|
|
89
|
+
// the wrong value. The get/set/del closures abstract WHERE the slice lives, letting
|
|
90
|
+
// analyzeBody (local Map) and analyzeValTypes (ctx.func.localReps / ctx.types.typedElem)
|
|
91
|
+
// share one definition — so the two body walks can't drift. Passed as three positional
|
|
92
|
+
// closures rather than a {get,set,delete} store object: a Map satisfies that interface
|
|
93
|
+
// natively (analyzeBody's slices) but the analyzeValTypes slices need custom logic
|
|
94
|
+
// (updateRep), so the param would be polymorphic (Map | object) — which the self-host
|
|
95
|
+
// kernel cannot statically type, mis-dispatching `store.set` on the object form to the
|
|
96
|
+
// Map builtin. Direct closure calls sidestep method dispatch entirely.
|
|
97
|
+
const makeValTracker = (get, set, del) => {
|
|
98
|
+
const poison = new Set()
|
|
99
|
+
return (name, vt) => {
|
|
100
|
+
if (poison.has(name)) return
|
|
101
|
+
const prev = get(name)
|
|
102
|
+
if (!vt) { if (prev) poison.add(name); del(name); return }
|
|
103
|
+
if (prev && prev !== vt) { poison.add(name); del(name); return }
|
|
104
|
+
set(name, vt)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const makeTypedTracker = (get, set, del) => {
|
|
108
|
+
const poison = new Set()
|
|
109
|
+
const invalidate = (name) => { poison.add(name); del(name) }
|
|
110
|
+
// Resolve a variable-name ternary branch to its known typed-array ctor: a
|
|
111
|
+
// local typed binding (`get`), or a module global promoted typed by plan
|
|
112
|
+
// (`inferModuleLetTypes` populates `globalTypedElem`, copied into
|
|
113
|
+
// `ctx.types.typedElem` per-func). Lets `let cur = flip ? bufA : bufB` keep
|
|
114
|
+
// the fast typed-load path instead of decaying to `$__typed_idx`.
|
|
115
|
+
const resolveName = (n) =>
|
|
116
|
+
get(n) ?? ctx.types.typedElem?.get(n) ?? ctx.scope.globalTypedElem?.get(n) ?? null
|
|
117
|
+
return (name, rhs) => {
|
|
118
|
+
if (poison.has(name)) return
|
|
119
|
+
const setOrInvalidate = (c) => {
|
|
120
|
+
if (c === MIXED_CTORS) return invalidate(name)
|
|
121
|
+
const prev = get(name)
|
|
122
|
+
if (prev && prev !== c) invalidate(name)
|
|
123
|
+
else set(name, c)
|
|
124
|
+
}
|
|
125
|
+
const ctor = typedElemCtor(rhs)
|
|
126
|
+
if (ctor) return setOrInvalidate(ctor)
|
|
127
|
+
// TYPED-narrowed call result carries its elem aux on f.sig.ptrAux — reverse-map
|
|
128
|
+
// to a canonical ctor so the unboxed local's rep restores the same aux.
|
|
129
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
130
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
131
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
132
|
+
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
133
|
+
if (c) setOrInvalidate(c)
|
|
134
|
+
}
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
// Heterogeneous ternary (`n===16 ? new Uint8Array(16) : new Uint16Array(8)`):
|
|
138
|
+
// ctors that don't unify must invalidate so a sibling-scope decl can't lock in
|
|
139
|
+
// the wrong store width.
|
|
140
|
+
const tc = ternaryCtorOfRhs(rhs, resolveName)
|
|
141
|
+
if (tc) setOrInvalidate(tc)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Unified per-body analysis — see module header for slice overview.
|
|
147
|
+
* Returns cached facts; DO NOT MUTATE the returned maps.
|
|
148
|
+
*
|
|
149
|
+
* NOTE on the cache (root A): entries are body-keyed and CAN read ctx that mutates
|
|
150
|
+
* during narrowing (a `let x = f()` local's wasm type shifts when f's result
|
|
151
|
+
* narrows). The cache is therefore *intentionally staleable* — invalidateLocalsCache
|
|
152
|
+
* is placed at the phase boundaries where a stale read would matter, not "everywhere".
|
|
153
|
+
* A recompute-vs-cache assertion was tried (JZ_DEBUG_CACHE) and abandoned: it fires
|
|
154
|
+
* on benign staleness (the suite stays green through the divergence), so it can't
|
|
155
|
+
* tell a real missing-invalidation from a harmless one. See .work/todo.md.
|
|
156
|
+
*/
|
|
157
|
+
export function analyzeBody(body) {
|
|
158
|
+
// Non-object bodies (`() => 0`, `() => x`, missing) have nothing to observe
|
|
159
|
+
// for any slice and can't be WeakMap-keyed. Return empty maps without caching.
|
|
160
|
+
if (body === null || typeof body !== 'object') return {
|
|
161
|
+
locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
|
|
162
|
+
arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
|
|
163
|
+
flatObjects: new Map(),
|
|
164
|
+
}
|
|
165
|
+
const hit = _bodyFactsCache.get(body)
|
|
166
|
+
if (hit) return hit
|
|
167
|
+
|
|
168
|
+
const locals = new Map()
|
|
169
|
+
const valTypes = new Map()
|
|
170
|
+
const arrElemSchemas = new Map()
|
|
171
|
+
const arrElemValTypes = new Map()
|
|
172
|
+
// Nested element kind: `name`'s elements are themselves arrays whose elements
|
|
173
|
+
// share this VAL.*. Lets `chord = padChord[i]; chord[j]` (floatbeat pad voicings,
|
|
174
|
+
// `padChord = [[0,2,4],…]`) bind `chord`'s arrayElemValType through one index step,
|
|
175
|
+
// so `chord[j]` is a Number and skips __to_num. Single-level only — enough for the
|
|
176
|
+
// 2-D table pattern without a general nested-type lattice.
|
|
177
|
+
const arrElemElemValTypes = new Map()
|
|
178
|
+
const typedElems = new Map()
|
|
179
|
+
const escapes = new Map() // name → bool: local holds allocation, true if it escapes
|
|
180
|
+
|
|
181
|
+
const doSchemas = !!ctx.schema?.register
|
|
182
|
+
// Per-walk local schema map for chained `arr.push(name)` resolution.
|
|
183
|
+
const localSchemaMap = new Map()
|
|
184
|
+
|
|
185
|
+
// === Observation helpers ===
|
|
186
|
+
//
|
|
187
|
+
// These trust the AST: any `arr.push(...)` syntactically present has `arr` as
|
|
188
|
+
// a body-relevant name (decl, param, or global) since closure boundaries are
|
|
189
|
+
// skipped at walk time. Pure typo names produce harmless dead Map entries
|
|
190
|
+
// that are never queried (consumers index by known local/param names).
|
|
191
|
+
// Removing the legacy `ctx.func.locals.has(arr)` filter makes analyzeBody's
|
|
192
|
+
// output context-pure — cache hits don't depend on transient ctx state.
|
|
193
|
+
|
|
194
|
+
const observeArrSchema = (arr, sid) => {
|
|
195
|
+
if (!doSchemas) return
|
|
196
|
+
if (typeof arr !== 'string') return
|
|
197
|
+
if (arrElemSchemas.get(arr) === null) return
|
|
198
|
+
if (sid == null) { arrElemSchemas.set(arr, null); return }
|
|
199
|
+
if (!arrElemSchemas.has(arr)) arrElemSchemas.set(arr, sid)
|
|
200
|
+
else if (arrElemSchemas.get(arr) !== sid) arrElemSchemas.set(arr, null)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const observeArrValType = (arr, vt) => {
|
|
204
|
+
if (typeof arr !== 'string') return
|
|
205
|
+
if (arrElemValTypes.get(arr) === null) return
|
|
206
|
+
if (!vt) { arrElemValTypes.set(arr, null); return }
|
|
207
|
+
if (!arrElemValTypes.has(arr)) arrElemValTypes.set(arr, vt)
|
|
208
|
+
else if (arrElemValTypes.get(arr) !== vt) arrElemValTypes.set(arr, null)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const elemValOf = (name) => {
|
|
212
|
+
if (typeof name !== 'string') return null
|
|
213
|
+
const repVt = ctx.func.localReps?.get(name)?.arrayElemValType
|
|
214
|
+
if (repVt) return repVt
|
|
215
|
+
return arrElemValTypes.get(name) || null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const exprElemSourceVal = (expr) => {
|
|
219
|
+
if (typeof expr === 'string') {
|
|
220
|
+
const repVt = ctx.func.localReps?.get(expr)?.val
|
|
221
|
+
if (repVt) return repVt
|
|
222
|
+
return ctx.scope.globalValTypes?.get(expr) || null
|
|
223
|
+
}
|
|
224
|
+
return valTypeOf(expr)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Common element VAL of an array-literal node (`[a,b,c]`), or null if not a literal
|
|
228
|
+
// or its elements disagree. Used to read one level into an array-of-arrays literal.
|
|
229
|
+
const arrLitElemCommonVal = (litNode) => {
|
|
230
|
+
const raw = staticArrayElems(litNode)
|
|
231
|
+
if (!raw) return null
|
|
232
|
+
const items = raw.filter(e => e != null)
|
|
233
|
+
if (!items.length || items.length !== raw.length) return null
|
|
234
|
+
let common = exprElemSourceVal(items[0])
|
|
235
|
+
for (let k = 1; k < items.length && common != null; k++) {
|
|
236
|
+
if (exprElemSourceVal(items[k]) !== common) common = null
|
|
237
|
+
}
|
|
238
|
+
return common
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Local-Map slices: bind the Map's get/set/delete as the tracker's three ops.
|
|
242
|
+
const trackVal = makeValTracker(n => valTypes.get(n), (n, vt) => valTypes.set(n, vt), n => valTypes.delete(n))
|
|
243
|
+
const trackTyped = makeTypedTracker(n => typedElems.get(n), (n, c) => typedElems.set(n, c), n => typedElems.delete(n))
|
|
244
|
+
|
|
245
|
+
// === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
|
|
246
|
+
const processDecl = (name, rhs) => {
|
|
247
|
+
// wasm type (locals slice). A `>>> 0` result is an unsigned uint32 that doesn't fit a
|
|
248
|
+
// *signed* i32, so a binding initialized from one must be f64 — else reads and arithmetic
|
|
249
|
+
// see the value as negative for inputs ≥ 2³¹. But `x >>> k` with a constant shift k where
|
|
250
|
+
// (k & 31) ≥ 1 lands in [0, 2³¹−1] (max 0xFFFFFFFF >>> 1 = 0x7FFFFFFF), which DOES fit a
|
|
251
|
+
// signed i32 — keep it on the fast integer path (FFT index math: `nn >>> 1`, `n2 >>> 2`).
|
|
252
|
+
// Only `>>> 0` (and variable shifts, which could be 0) need widening. (ToUint32 accumulators
|
|
253
|
+
// init from a literal and narrowUint32 re-narrows them — so this only governs `let u = x >>> k`.)
|
|
254
|
+
const shr = Array.isArray(rhs) && rhs[0] === '>>>'
|
|
255
|
+
const shrFitsI32 = shr && Array.isArray(rhs[2]) && rhs[2][0] == null
|
|
256
|
+
&& typeof rhs[2][1] === 'number' && (rhs[2][1] & 31) >= 1
|
|
257
|
+
const wt = (shr && !shrFitsI32) ? 'f64' : exprType(rhs, locals)
|
|
258
|
+
if (!locals.has(name)) locals.set(name, wt)
|
|
259
|
+
else if (locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
260
|
+
|
|
261
|
+
// val type (valTypes slice)
|
|
262
|
+
trackVal(name, valTypeOf(rhs))
|
|
263
|
+
|
|
264
|
+
// typed-array element ctor (typedElems slice)
|
|
265
|
+
trackTyped(name, rhs)
|
|
266
|
+
|
|
267
|
+
// arr-elem schema (arrElemSchemas slice) — schema bindings + array-literal init + alias + call return
|
|
268
|
+
if (doSchemas) {
|
|
269
|
+
const sid = exprSchemaId(rhs, localSchemaMap)
|
|
270
|
+
if (sid != null) localSchemaMap.set(name, sid)
|
|
271
|
+
{
|
|
272
|
+
const rawElems = staticArrayElems(rhs)
|
|
273
|
+
if (rawElems) {
|
|
274
|
+
const elems = rawElems.filter(e => e != null)
|
|
275
|
+
if (elems.length && elems.length === rawElems.length) {
|
|
276
|
+
let common = exprSchemaId(elems[0], localSchemaMap)
|
|
277
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
278
|
+
if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
|
|
279
|
+
}
|
|
280
|
+
if (common != null) observeArrSchema(name, common)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
285
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
286
|
+
if (f?.arrayElemSchema != null) observeArrSchema(name, f.arrayElemSchema)
|
|
287
|
+
}
|
|
288
|
+
if (typeof rhs === 'string' && arrElemSchemas.has(rhs)) {
|
|
289
|
+
const sid2 = arrElemSchemas.get(rhs)
|
|
290
|
+
if (sid2 != null) observeArrSchema(name, sid2)
|
|
291
|
+
}
|
|
292
|
+
if (typeof rhs === 'string') {
|
|
293
|
+
const repSid = ctx.func.localReps?.get(rhs)?.arrayElemSchema
|
|
294
|
+
if (repSid != null) observeArrSchema(name, repSid)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// arr-elem val type (arrElemValTypes slice) — array-literal init + call return + alias + .map/.filter/.slice/.concat chain
|
|
299
|
+
{
|
|
300
|
+
const rawElems = staticArrayElems(rhs)
|
|
301
|
+
if (rawElems) {
|
|
302
|
+
const elems = rawElems.filter(e => e != null)
|
|
303
|
+
if (elems.length && elems.length === rawElems.length) {
|
|
304
|
+
let common = exprElemSourceVal(elems[0])
|
|
305
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
306
|
+
if (exprElemSourceVal(elems[k]) !== common) common = null
|
|
307
|
+
}
|
|
308
|
+
if (common != null) observeArrValType(name, common)
|
|
309
|
+
// Array-of-arrays literal: record the common element-of-element kind so a
|
|
310
|
+
// later `x = name[i]` binds `x`'s element type one level down.
|
|
311
|
+
if (common === VAL.ARRAY) {
|
|
312
|
+
let nested = arrLitElemCommonVal(elems[0])
|
|
313
|
+
for (let k = 1; k < elems.length && nested != null; k++) {
|
|
314
|
+
if (arrLitElemCommonVal(elems[k]) !== nested) nested = null
|
|
315
|
+
}
|
|
316
|
+
if (nested != null) arrElemElemValTypes.set(name, nested)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// `x = arr[i]` where `arr` is a known array-of-arrays → `x`'s elements take
|
|
321
|
+
// `arr`'s nested element kind (the missing index-step in observeArrValType).
|
|
322
|
+
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 && typeof rhs[1] === 'string') {
|
|
323
|
+
const nested = arrElemElemValTypes.get(rhs[1])
|
|
324
|
+
if (nested) observeArrValType(name, nested)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
328
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
329
|
+
if (f?.arrayElemValType) observeArrValType(name, f.arrayElemValType)
|
|
330
|
+
}
|
|
331
|
+
if (typeof rhs === 'string') {
|
|
332
|
+
const v = elemValOf(rhs)
|
|
333
|
+
if (v) observeArrValType(name, v)
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
336
|
+
Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
337
|
+
typeof rhs[1][1] === 'string') {
|
|
338
|
+
const recvName = rhs[1][1], method = rhs[1][2]
|
|
339
|
+
if (method === 'filter' || method === 'slice' || method === 'concat') {
|
|
340
|
+
const v = elemValOf(recvName)
|
|
341
|
+
if (v) observeArrValType(name, v)
|
|
342
|
+
} else if (method === 'split' && valTypeOf(recvName) === VAL.STRING) {
|
|
343
|
+
observeArrValType(name, VAL.STRING)
|
|
344
|
+
} else if (method === 'map') {
|
|
345
|
+
const arrowFn = rhs[2]
|
|
346
|
+
const recvVt = elemValOf(recvName)
|
|
347
|
+
const param = Array.isArray(arrowFn) && arrowFn[0] === '=>' ? arrowFn[1] : null
|
|
348
|
+
const paramName = typeof param === 'string' ? param :
|
|
349
|
+
(Array.isArray(param) && param[0] === '()' && typeof param[1] === 'string' ? param[1] : null)
|
|
350
|
+
const arrowBody = paramName ? arrowFn[2] : null
|
|
351
|
+
const exprBody = (Array.isArray(arrowBody) && arrowBody[0] === '{}' &&
|
|
352
|
+
Array.isArray(arrowBody[1]) && arrowBody[1][0] === 'return') ? arrowBody[1][1] : arrowBody
|
|
353
|
+
if (paramName && exprBody != null) {
|
|
354
|
+
const refs = ctx.func.refinements
|
|
355
|
+
const hadParam = refs?.has(paramName)
|
|
356
|
+
const prev = hadParam ? refs.get(paramName) : undefined
|
|
357
|
+
if (refs && recvVt) refs.set(paramName, { val: recvVt })
|
|
358
|
+
let bodyVt = null
|
|
359
|
+
try { bodyVt = valTypeOf(exprBody) }
|
|
360
|
+
finally {
|
|
361
|
+
if (refs && recvVt) {
|
|
362
|
+
if (hadParam) refs.set(paramName, prev); else refs.delete(paramName)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (bodyVt) observeArrValType(name, bodyVt)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
370
|
+
Array.isArray(rhs[1]) && rhs[1][0] === '.' && rhs[1][2] === 'split' &&
|
|
371
|
+
valTypeOf(rhs[1][1]) === VAL.STRING) {
|
|
372
|
+
observeArrValType(name, VAL.STRING)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// arrElem invalidation rule — fires on `=` reassign of tracked name to non-array
|
|
377
|
+
const isArrayProducingRhs = (rhs) =>
|
|
378
|
+
Array.isArray(rhs) && (staticArrayElems(rhs) != null ||
|
|
379
|
+
(rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
380
|
+
(rhs[1][2] === 'slice' || rhs[1][2] === 'concat')))
|
|
381
|
+
|
|
382
|
+
const markEscape = (name) => { if (escapes.has(name)) escapes.set(name, true) }
|
|
383
|
+
|
|
384
|
+
const isStaticIndex = (key) =>
|
|
385
|
+
typeof key === 'number' || typeof key === 'string' ||
|
|
386
|
+
(Array.isArray(key) && ((key[0] == null && Number.isInteger(key[1])) || key[0] === 'str')) ||
|
|
387
|
+
staticPropertyKey(key) != null
|
|
388
|
+
|
|
389
|
+
const markEscapeValue = (expr) => {
|
|
390
|
+
if (typeof expr === 'string') { markEscape(expr); return }
|
|
391
|
+
if (!Array.isArray(expr)) return
|
|
392
|
+
const op = expr[0]
|
|
393
|
+
if (op === 'str') return
|
|
394
|
+
if (op === ':') { markEscapeValue(expr[2]); return }
|
|
395
|
+
if ((op === '.' || op === '?.') && typeof expr[1] === 'string' && escapes.has(expr[1])) return
|
|
396
|
+
if (op === '[]' && typeof expr[1] === 'string' && escapes.has(expr[1])) {
|
|
397
|
+
if (!isStaticIndex(expr[2])) markEscape(expr[1])
|
|
398
|
+
markEscapeValue(expr[2])
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
for (let i = 1; i < expr.length; i++) markEscapeValue(expr[i])
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const markEscapeArgs = (args) => {
|
|
405
|
+
if (args == null) return
|
|
406
|
+
const list = Array.isArray(args) && args[0] === ',' ? args.slice(1) : [args]
|
|
407
|
+
for (const a of list) markEscapeValue(Array.isArray(a) && a[0] === '...' ? a[1] : a)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// === Single walk ===
|
|
411
|
+
function walk(node) {
|
|
412
|
+
if (!Array.isArray(node)) return
|
|
413
|
+
const op = node[0]
|
|
414
|
+
if (op === '=>') return // don't cross closure boundary
|
|
415
|
+
|
|
416
|
+
if (op === 'let' || op === 'const') {
|
|
417
|
+
for (let i = 1; i < node.length; i++) {
|
|
418
|
+
const a = node[i]
|
|
419
|
+
// analyzeBody: bare-name decl
|
|
420
|
+
if (typeof a === 'string') { if (!locals.has(a)) locals.set(a, 'f64'); continue }
|
|
421
|
+
if (!Array.isArray(a) || a[0] !== '=') continue
|
|
422
|
+
// analyzeBody: destructuring decl — set destructured names to f64, walk rhs only
|
|
423
|
+
if (typeof a[1] !== 'string') {
|
|
424
|
+
for (const n of collectParamNames([a[1]])) if (!locals.has(n)) locals.set(n, 'f64')
|
|
425
|
+
walk(a[2])
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
const name = a[1], rhs = a[2]
|
|
429
|
+
processDecl(name, rhs)
|
|
430
|
+
if (Array.isArray(rhs) && (rhs[0] === '[' || rhs[0] === '{}')) {
|
|
431
|
+
escapes.set(name, false)
|
|
432
|
+
}
|
|
433
|
+
markEscapeValue(rhs)
|
|
434
|
+
// Walk rhs only — never enter the `=` node so the reassignment-invalidation
|
|
435
|
+
// rule won't misfire on the binding's own initializer.
|
|
436
|
+
walk(rhs)
|
|
437
|
+
}
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (op === 'return' && node[1] != null) {
|
|
442
|
+
markEscapeValue(node[1])
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (op === '()' && node.length > 2) {
|
|
446
|
+
markEscapeArgs(node[2])
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// arr.push(...) — observe both schemas and val types in one pass
|
|
450
|
+
if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && node[1][2] === 'push' && typeof node[1][1] === 'string') {
|
|
451
|
+
const arr = node[1][1]
|
|
452
|
+
const list = commaList(node[2])
|
|
453
|
+
for (const a of list) {
|
|
454
|
+
if (Array.isArray(a) && a[0] === '...') {
|
|
455
|
+
observeArrSchema(arr, null); observeArrValType(arr, null); continue
|
|
456
|
+
}
|
|
457
|
+
observeArrSchema(arr, exprSchemaId(a, localSchemaMap))
|
|
458
|
+
observeArrValType(arr, exprElemSourceVal(a))
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// `=` reassignment — locals widen, valTypes/typedElems track,
|
|
463
|
+
// arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
|
|
464
|
+
if (op === '=' && typeof node[1] === 'string') {
|
|
465
|
+
const name = node[1], rhs = node[2]
|
|
466
|
+
walk(rhs)
|
|
467
|
+
markEscape(name)
|
|
468
|
+
markEscapeValue(rhs)
|
|
469
|
+
const wt = exprType(rhs, locals)
|
|
470
|
+
if (locals.has(name) && locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
471
|
+
trackVal(name, valTypeOf(rhs))
|
|
472
|
+
trackTyped(name, rhs)
|
|
473
|
+
if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
|
|
474
|
+
if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// compound-assign widening (locals slice)
|
|
479
|
+
if ((op === '+=' || op === '-=' || op === '*=' || op === '%=') && typeof node[1] === 'string') {
|
|
480
|
+
const name = node[1], opChar = op[0]
|
|
481
|
+
const t = exprType([opChar, node[1], node[2]], locals)
|
|
482
|
+
if (locals.has(name) && locals.get(name) === 'i32' && t === 'f64') locals.set(name, 'f64')
|
|
483
|
+
}
|
|
484
|
+
if (op === '/=' && typeof node[1] === 'string') {
|
|
485
|
+
if (locals.has(node[1])) locals.set(node[1], 'f64')
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (op === 'for' || op === 'for-in' || op === 'for-of') {
|
|
489
|
+
if (node[1] != null) markEscapeValue(node[1])
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (op === '[' || op === '{}') {
|
|
493
|
+
for (let i = 1; i < node.length; i++) {
|
|
494
|
+
const c = node[i]
|
|
495
|
+
if (Array.isArray(c) && c[0] === ',') {
|
|
496
|
+
for (let j = 1; j < c.length; j++) {
|
|
497
|
+
if (Array.isArray(c[j]) && c[j][0] === '...') markEscapeValue(c[j][1])
|
|
498
|
+
}
|
|
499
|
+
} else if (Array.isArray(c) && c[0] === '...') {
|
|
500
|
+
markEscapeValue(c[1])
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (op === '[]' && typeof node[1] === 'string' && escapes.has(node[1])) {
|
|
506
|
+
const key = node[2]
|
|
507
|
+
if (!isStaticIndex(key)) markEscape(node[1])
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Install the in-progress valTypes as a lookup overlay so successive decls
|
|
514
|
+
// resolve chains (`const a = new TypedArr(); const b = a[0]` → b: NUMBER)
|
|
515
|
+
// and shorthand-bound `{a}` props see a's type. Restored after walk completes.
|
|
516
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
517
|
+
const prevTypedOverlay = ctx.func.localTypedElemsOverlay
|
|
518
|
+
ctx.func.localValTypesOverlay = valTypes
|
|
519
|
+
ctx.func.localTypedElemsOverlay = typedElems
|
|
520
|
+
let unsignedLocals
|
|
521
|
+
try {
|
|
522
|
+
walk(body)
|
|
523
|
+
widenLocalTypes(body, locals)
|
|
524
|
+
// Narrow proven uint32 accumulator locals to unsigned i32. Runs post-widen so
|
|
525
|
+
// a local already demoted to f64 above (e.g. compared against an f64) is
|
|
526
|
+
// reconsidered with final types — and stays f64, since a relational compare
|
|
527
|
+
// is a non-transparent read that disqualifies narrowing anyway.
|
|
528
|
+
unsignedLocals = narrowUint32(body, locals)
|
|
529
|
+
} finally {
|
|
530
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
531
|
+
ctx.func.localTypedElemsOverlay = prevTypedOverlay
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// SRoA: dissolve non-escaping object-literal bindings into field locals.
|
|
535
|
+
// The dead `o` local is dropped — every `o` reference is rewritten by the
|
|
536
|
+
// codegen flat hooks, so a stray `local.get $o` becomes a loud wasm
|
|
537
|
+
// validation error instead of a silent miscompile.
|
|
538
|
+
const flatObjects = doSchemas ? scanFlatObjects(body) : new Map()
|
|
539
|
+
for (const [name, props] of flatObjects) {
|
|
540
|
+
for (let i = 0; i < props.names.length; i++) locals.set(`${name}#${i}`, 'f64')
|
|
541
|
+
locals.delete(name)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// No-copy slice views — `let t = s.slice(...)` bindings proven non-escaping.
|
|
545
|
+
// Consumed by emitDecl, which lowers the initializer to a SLICE_BIT view.
|
|
546
|
+
const sliceViews = doSchemas ? scanSliceViews(body) : new Set()
|
|
547
|
+
|
|
548
|
+
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes, flatObjects, sliceViews, unsignedLocals }
|
|
549
|
+
_bodyFactsCache.set(body, result)
|
|
550
|
+
return result
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Post-walk wasm-type widening over `locals`, in place — analyzeBody stage 2.
|
|
555
|
+
*
|
|
556
|
+
* Pass A (widenPass): i32 locals compared against f64 widen — EXCEPT integer
|
|
557
|
+
* counters used as affine array indices (collectI32SafeIndexVars: i32-range
|
|
558
|
+
* proven, direct indexing with no per-access trunc_sat) and integer-certain
|
|
559
|
+
* locals (intCertainMap: every definition integer-valued). An f64 counter
|
|
560
|
+
* would poison the loop body's arithmetic and the increment (f64.add per
|
|
561
|
+
* iteration), the dominant cost of `for (i<n) acc=(acc+i)|0` — measured ~18×
|
|
562
|
+
* vs V8 before this. The compare coerces the counter once. Sound for n ≤ 2³¹
|
|
563
|
+
* (the asm.js-style integer contract); a fractional assignment poisons
|
|
564
|
+
* intCertain → widens normally.
|
|
565
|
+
*
|
|
566
|
+
* Pass B (assignment fixpoint): re-resolve decl/assign RHS types now that
|
|
567
|
+
* pass A widened. `let x2 = zx*zx` declared i32 because zx was i32 at scan
|
|
568
|
+
* time must widen when zx re-types to f64 — else trunc_sat silently floors
|
|
569
|
+
* the fractional value (mandelbrot escape: 3.515 → 3). Re-checks `=` and
|
|
570
|
+
* compound assigns too: a single-pass walk sees each assign once with stale
|
|
571
|
+
* operand types, missing widens through loop back-edges. keepI32 vars are
|
|
572
|
+
* exempt: a hoisted product `o = y*w` types f64 but is proven integer.
|
|
573
|
+
* Monotonic (i32 → f64 only), bounded by locals count.
|
|
574
|
+
*/
|
|
575
|
+
function widenLocalTypes(body, locals) {
|
|
576
|
+
const i32SafeIdx = collectI32SafeIndexVars(body, locals)
|
|
577
|
+
const intCounters = intCertainMap(body)
|
|
578
|
+
const f64IdxVars = collectF64StridedIndexVars(body, locals) // counters that trunc anyway — don't keep i32
|
|
579
|
+
const keepI32 = (name) => i32SafeIdx.has(name) || (intCounters.get(name) === true && !f64IdxVars.has(name))
|
|
580
|
+
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
581
|
+
const widenPass = (node) => {
|
|
582
|
+
if (!Array.isArray(node)) return
|
|
583
|
+
const [op, ...args] = node
|
|
584
|
+
if (CMP_OPS.has(op)) {
|
|
585
|
+
const [a, b] = args
|
|
586
|
+
const ta = exprType(a, locals), tb = exprType(b, locals)
|
|
587
|
+
if (ta === 'i32' && tb === 'f64' && typeof a === 'string' && locals.has(a) && !keepI32(a)) locals.set(a, 'f64')
|
|
588
|
+
if (tb === 'i32' && ta === 'f64' && typeof b === 'string' && locals.has(b) && !keepI32(b)) locals.set(b, 'f64')
|
|
589
|
+
}
|
|
590
|
+
if (op !== '=>') for (const a of args) widenPass(a)
|
|
591
|
+
}
|
|
592
|
+
widenPass(body)
|
|
593
|
+
|
|
594
|
+
let widened = true
|
|
595
|
+
while (widened) {
|
|
596
|
+
widened = false
|
|
597
|
+
const recheck = (node) => {
|
|
598
|
+
if (!Array.isArray(node)) return
|
|
599
|
+
const op = node[0]
|
|
600
|
+
if (op === '=>') return
|
|
601
|
+
if (op === 'let' || op === 'const') {
|
|
602
|
+
for (let i = 1; i < node.length; i++) {
|
|
603
|
+
const a = node[i]
|
|
604
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
605
|
+
const name = a[1], rhs = a[2]
|
|
606
|
+
if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64' && !keepI32(name)) {
|
|
607
|
+
locals.set(name, 'f64'); widened = true
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (op === '=' && typeof node[1] === 'string') {
|
|
613
|
+
const name = node[1], rhs = node[2]
|
|
614
|
+
if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64' && !keepI32(name)) {
|
|
615
|
+
locals.set(name, 'f64'); widened = true
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if ((op === '+=' || op === '-=' || op === '*=' || op === '%=') && typeof node[1] === 'string') {
|
|
619
|
+
const name = node[1]
|
|
620
|
+
if (locals.get(name) === 'i32' && exprType([op[0], name, node[2]], locals) === 'f64' && !keepI32(name)) {
|
|
621
|
+
locals.set(name, 'f64'); widened = true
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (op === '/=' && typeof node[1] === 'string') {
|
|
625
|
+
const name = node[1]
|
|
626
|
+
if (locals.get(name) === 'i32') { locals.set(name, 'f64'); widened = true }
|
|
627
|
+
}
|
|
628
|
+
for (let i = 1; i < node.length; i++) recheck(node[i])
|
|
629
|
+
}
|
|
630
|
+
recheck(body)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Drop the cached analyzeBody entry for this body. Used by emitFunc after
|
|
635
|
+
* seeding cross-call param VAL facts so the next walk picks up fresh
|
|
636
|
+
* `ctx.func.localReps` (drives exprType receiver-type lookups).
|
|
637
|
+
* Same hook as `invalidateValTypesCache` — split names preserve caller intent. */
|
|
638
|
+
export function invalidateLocalsCache(body) {
|
|
639
|
+
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Can this RHS expression produce null/undefined? A direct nullish literal
|
|
643
|
+
// (`[null, null]` covers both null and undefined), or a `?:`/`&&`/`||` with a
|
|
644
|
+
// nullish branch. Drives the `nullable` rep flag so `x === null` on a binding
|
|
645
|
+
// that was ever assigned null isn't constant-folded to false (emit.js
|
|
646
|
+
// strictSentinel). Conservative — opaque sources (calls, member reads) aren't
|
|
647
|
+
// flagged; the fold they'd suppress is rare and a runtime nullish check is cheap.
|
|
648
|
+
function mayBeNullish(n) {
|
|
649
|
+
if (!Array.isArray(n)) return false
|
|
650
|
+
if (n.length === 2 && n[0] == null && n[1] == null) return true
|
|
651
|
+
if (n[0] === '?' || n[0] === '?:') return mayBeNullish(n[2]) || mayBeNullish(n[3])
|
|
652
|
+
if (n[0] === '&&' || n[0] === '||') return mayBeNullish(n[1]) || mayBeNullish(n[2])
|
|
653
|
+
return false
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Analyze all local value types from declarations and assignments.
|
|
658
|
+
* Writes the per-name `val` field of `ctx.func.localReps` for method dispatch
|
|
659
|
+
* and schema resolution.
|
|
660
|
+
*/
|
|
661
|
+
export function analyzeValTypes(body) {
|
|
662
|
+
// localReps slice: store reads/writes the rep's `val` field (updateRep clears it
|
|
663
|
+
// when set to undefined, matching the old explicit delete).
|
|
664
|
+
const setVal = makeValTracker(
|
|
665
|
+
(n) => ctx.func.localReps?.get(n)?.val,
|
|
666
|
+
(n, vt) => updateRep(n, { val: vt }),
|
|
667
|
+
(n) => updateRep(n, { val: undefined }),
|
|
668
|
+
)
|
|
669
|
+
const getVal = name => ctx.func.localReps?.get(name)?.val
|
|
670
|
+
// Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
|
|
671
|
+
// on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
|
|
672
|
+
// Parallel arrElemValTypes walk records VAL.* element kinds into
|
|
673
|
+
// rep.arrayElemValType so valTypeOf's `arr[i]` rule can elide __to_num and route
|
|
674
|
+
// method dispatch on `arr[i].method()`. Both come from a single unified walk.
|
|
675
|
+
const facts = analyzeBody(body)
|
|
676
|
+
const arrElems = facts.arrElemSchemas
|
|
677
|
+
for (const [name, vt] of facts.arrElemValTypes) {
|
|
678
|
+
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
679
|
+
}
|
|
680
|
+
// Propagate body-observed array-elem schemas to localReps so unboxablePtrs's
|
|
681
|
+
// `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
|
|
682
|
+
// to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
|
|
683
|
+
// pay an i64.reinterpret/i32.wrap on every slot access (no aliasing → CSE can't fold).
|
|
684
|
+
for (const [name, sid] of arrElems) {
|
|
685
|
+
if (sid != null) updateRep(name, { arrayElemSchema: sid })
|
|
686
|
+
}
|
|
687
|
+
// Resolve a name's array-elem-schema, preferring rep.arrayElemSchema (set from
|
|
688
|
+
// paramReps[k].arrayElemSchema at emit start) over local body observations.
|
|
689
|
+
const arrElemSchemaOf = (name) => {
|
|
690
|
+
if (typeof name !== 'string') return null
|
|
691
|
+
const repSid = ctx.func.localReps?.get(name)?.arrayElemSchema
|
|
692
|
+
if (repSid != null) return repSid
|
|
693
|
+
const localSid = arrElems.get(name)
|
|
694
|
+
return localSid != null ? localSid : null
|
|
695
|
+
}
|
|
696
|
+
function trackRegex(name, rhs) {
|
|
697
|
+
if (ctx.runtime.regex && Array.isArray(rhs) && rhs[0] === '//') ctx.runtime.regex.vars.set(name, rhs)
|
|
698
|
+
}
|
|
699
|
+
// ctx.types.typedElem slice (lazily created on first write, as before — readers
|
|
700
|
+
// tolerate null). Disagreeing decls poison the name (jz hoists `let` to function
|
|
701
|
+
// scope, so sibling-scope decls share a name and must not lock in a wrong width).
|
|
702
|
+
const trackTyped = makeTypedTracker(
|
|
703
|
+
(n) => ctx.types.typedElem?.get(n),
|
|
704
|
+
(n, c) => (ctx.types.typedElem ??= new Map()).set(n, c),
|
|
705
|
+
(n) => ctx.types.typedElem?.delete(n),
|
|
706
|
+
)
|
|
707
|
+
// Total write count for `name` across the whole body, recursing into nested
|
|
708
|
+
// closures so a closure that reassigns the var is also counted. Capped at 2 —
|
|
709
|
+
// callers only need the "exactly one write" verdict.
|
|
710
|
+
function writeCount(node, name, n) {
|
|
711
|
+
if (n > 1 || !Array.isArray(node)) return n
|
|
712
|
+
const o = node[0]
|
|
713
|
+
if ((ASSIGN_OPS.has(o) || o === '++' || o === '--') && node[1] === name) n++
|
|
714
|
+
if (o === 'let' || o === 'const') {
|
|
715
|
+
for (let i = 1; i < node.length && n <= 1; i++) {
|
|
716
|
+
const d = node[i]
|
|
717
|
+
if (Array.isArray(d) && d[0] === '=' && d[2] != null) n = writeCount(d[2], name, n)
|
|
718
|
+
}
|
|
719
|
+
return n
|
|
720
|
+
}
|
|
721
|
+
for (let i = 1; i < node.length && n <= 1; i++) n = writeCount(node[i], name, n)
|
|
722
|
+
return n
|
|
723
|
+
}
|
|
724
|
+
// Bind an object-literal's schemaId onto its holding local's rep so that
|
|
725
|
+
// `o.prop` / `o.method()` dispatch is precise instead of falling back to
|
|
726
|
+
// structural subtyping (which mis-resolves when another in-scope object
|
|
727
|
+
// shares a member at a different slot). `shapeOf` already covers plain-data
|
|
728
|
+
// literals on a direct `let o = {…}` decl, but not literals with
|
|
729
|
+
// function-valued props — and `var o = {…}` is rewritten by jzify into
|
|
730
|
+
// `let o; o = {…}`, so the schemaId never reaches `o` either way.
|
|
731
|
+
// `expectWrites` is the reassignment count that marks `o` single-assignment:
|
|
732
|
+
// 1 for the jzify `=` form (the synthesized assignment IS the only write),
|
|
733
|
+
// 0 for a direct `let`/`const` decl (the initializer is not counted as a
|
|
734
|
+
// write). A polymorphically reassigned holder keeps dynamic dispatch.
|
|
735
|
+
// A name already in `ctx.schema.vars` carries a prepare-phase schema
|
|
736
|
+
// (Object.assign merge via `inferAssignSchema`, destructure tracking) that
|
|
737
|
+
// supersedes the bare-literal one — binding here would shadow the merged
|
|
738
|
+
// schema (rep schemaId wins over `ctx.schema.vars` in `idOf`).
|
|
739
|
+
function bindObjSchema(name, rhs, expectWrites = 1) {
|
|
740
|
+
if (ctx.func.current?.params?.some(p => p.name === name)) return
|
|
741
|
+
if (ctx.schema.vars?.has(name)) return
|
|
742
|
+
const sid = objLiteralSchemaId(rhs)
|
|
743
|
+
if (sid != null && writeCount(body, name, 0) === expectWrites) updateRep(name, { schemaId: sid })
|
|
744
|
+
}
|
|
745
|
+
function walk(node) {
|
|
746
|
+
if (!Array.isArray(node)) return
|
|
747
|
+
const [op, ...args] = node
|
|
748
|
+
if (op === '=>') return // don't leak inner-closure val types
|
|
749
|
+
// Propagate typed array type through method calls (e.g. buf.map → typed)
|
|
750
|
+
function propagateTyped(name, rhs) {
|
|
751
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return
|
|
752
|
+
const callee = rhs[1]
|
|
753
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return
|
|
754
|
+
const src = callee[1], method = callee[2]
|
|
755
|
+
if (typeof src === 'string' && getVal(src) === VAL.TYPED && method === 'map') {
|
|
756
|
+
setVal(name, VAL.TYPED)
|
|
757
|
+
if (ctx.types.typedElem?.has(src)) {
|
|
758
|
+
const srcCtor = ctx.types.typedElem.get(src)
|
|
759
|
+
ctx.types.typedElem.set(name, srcCtor.endsWith('.view') ? srcCtor.slice(0, -5) : srcCtor)
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (op === 'let' || op === 'const') {
|
|
764
|
+
for (const a of args) {
|
|
765
|
+
if (!Array.isArray(a) || a[0] !== '=' || typeof a[1] !== 'string') continue
|
|
766
|
+
const vt = valTypeOf(a[2])
|
|
767
|
+
setVal(a[1], vt)
|
|
768
|
+
if (mayBeNullish(a[2])) updateRep(a[1], { nullable: true })
|
|
769
|
+
if (vt === VAL.REGEX) trackRegex(a[1], a[2])
|
|
770
|
+
// VAL gate covers definite-typed RHS; `?:`/`&&`/`||` slip through valTypeOf
|
|
771
|
+
// returning null but may still need ctor unification (or poisoning when
|
|
772
|
+
// branches disagree, since jz hoists `let` to function scope).
|
|
773
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(a[2])) trackTyped(a[1], a[2])
|
|
774
|
+
propagateTyped(a[1], a[2])
|
|
775
|
+
// JSON-shape propagation. When the RHS resolves to a known JSON shape
|
|
776
|
+
// (root: `JSON.parse(literal)`; nested: `o.meta`, `items[j]` from a known
|
|
777
|
+
// root), record it on the binding so subsequent `.prop`/`[i]` accesses
|
|
778
|
+
// skip dynamic dispatch and propagate VAL kinds. Generic for any
|
|
779
|
+
// compile-time JSON literal.
|
|
780
|
+
const sh = shapeOf(a[2])
|
|
781
|
+
if (sh) {
|
|
782
|
+
updateRep(a[1], { jsonShape: sh })
|
|
783
|
+
if (sh.val === VAL.ARRAY && sh.elem?.val) {
|
|
784
|
+
updateRep(a[1], { arrayElemValType: sh.elem.val })
|
|
785
|
+
// Array of fixed-shape OBJECTs: register elem schema so `it = items[j]`
|
|
786
|
+
// → `it.prop` lowers to slot read via the existing arr-elem-schema path.
|
|
787
|
+
if (sh.elem.val === VAL.OBJECT && sh.elem.names && ctx.schema.register) {
|
|
788
|
+
const elemSid = ctx.schema.register(sh.elem.names)
|
|
789
|
+
updateRep(a[1], { arrayElemSchema: elemSid })
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (sh.val === VAL.OBJECT && sh.names && ctx.schema.register) {
|
|
793
|
+
const sid = ctx.schema.register(sh.names)
|
|
794
|
+
updateRep(a[1], { schemaId: sid })
|
|
795
|
+
ctx.schema.vars.set(a[1], sid)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// `shapeOf` misses object literals with function-valued props; bind
|
|
799
|
+
// their schemaId here so number-hint ToPrimitive (valueOf/toString slot
|
|
800
|
+
// dispatch) resolves. expectWrites=0: a decl initializer is not a write.
|
|
801
|
+
if (vt === VAL.OBJECT) bindObjSchema(a[1], a[2], 0)
|
|
802
|
+
// Propagate schemaId from a narrowed call result so subsequent valTypeOf
|
|
803
|
+
// calls in this function body see the precise schema. emitDecl rebinds
|
|
804
|
+
// this at emission time too — analyze-time binding is what unlocks the
|
|
805
|
+
// slotVT lookup chain in `analyzeValTypes`'s own walk + per-func emit
|
|
806
|
+
// dispatch reading localReps.
|
|
807
|
+
if (vt === VAL.OBJECT && Array.isArray(a[2]) && a[2][0] === '()' && typeof a[2][1] === 'string') {
|
|
808
|
+
const f = ctx.func.map?.get(a[2][1])
|
|
809
|
+
if (f?.sig?.ptrAux != null) updateRep(a[1], { schemaId: f.sig.ptrAux })
|
|
810
|
+
}
|
|
811
|
+
// `const p = arr[i]` — when arr's element schema is known (from .push observations
|
|
812
|
+
// or from paramReps arrayElemSchema binding), p inherits the schema. Unlocks slotVT-driven
|
|
813
|
+
// numeric typing on `.prop` reads + slot-direct loads.
|
|
814
|
+
if (Array.isArray(a[2]) && a[2][0] === '[]' && typeof a[2][1] === 'string') {
|
|
815
|
+
const elemSid = arrElemSchemaOf(a[2][1])
|
|
816
|
+
if (elemSid != null) {
|
|
817
|
+
updateRep(a[1], { schemaId: elemSid })
|
|
818
|
+
// Also set the val so structural call dispatch + valTypeOf see VAL.OBJECT.
|
|
819
|
+
setVal(a[1], VAL.OBJECT)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (op === '=' && typeof args[0] === 'string') {
|
|
825
|
+
walk(args[1])
|
|
826
|
+
const vt = valTypeOf(args[1])
|
|
827
|
+
setVal(args[0], vt)
|
|
828
|
+
if (mayBeNullish(args[1])) updateRep(args[0], { nullable: true })
|
|
829
|
+
if (vt === VAL.REGEX) trackRegex(args[0], args[1])
|
|
830
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(args[1])) trackTyped(args[0], args[1])
|
|
831
|
+
propagateTyped(args[0], args[1])
|
|
832
|
+
if (vt === VAL.OBJECT) bindObjSchema(args[0], args[1])
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
// Track property assignments for auto-boxing: x.prop = val
|
|
836
|
+
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '.' && typeof args[0][1] === 'string') {
|
|
837
|
+
const [, obj, prop] = args[0]
|
|
838
|
+
const vt = getVal(obj)
|
|
839
|
+
if ((vt === VAL.NUMBER || vt === VAL.BIGINT) && ctx.func.locals?.has(obj) && ctx.schema.register) {
|
|
840
|
+
if (!ctx.func.localProps) ctx.func.localProps = new Map()
|
|
841
|
+
if (!ctx.func.localProps.has(obj)) ctx.func.localProps.set(obj, new Set())
|
|
842
|
+
ctx.func.localProps.get(obj).add(prop)
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
for (const a of args) walk(a)
|
|
846
|
+
}
|
|
847
|
+
walk(body)
|
|
848
|
+
|
|
849
|
+
// Register boxed schemas for local variables with property assignments
|
|
850
|
+
if (ctx.func.localProps) {
|
|
851
|
+
for (const [name, props] of ctx.func.localProps) {
|
|
852
|
+
if (ctx.schema.vars.has(name)) continue
|
|
853
|
+
const schema = ['__inner__', ...props]
|
|
854
|
+
const sid = ctx.schema.register(schema)
|
|
855
|
+
ctx.schema.vars.set(name, sid)
|
|
856
|
+
updateRep(name, { schemaId: sid })
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Forward-propagate `intCertain` on local bindings. Fixpoint lives in type.js. */
|
|
862
|
+
export function analyzeIntCertain(body) {
|
|
863
|
+
for (const [name, intC] of intCertainMap(body)) {
|
|
864
|
+
if (intC) updateRep(name, { intCertain: true })
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// A directly-uint32 expression: `x >>> 0` (zero-fill shift) or a call to a function
|
|
869
|
+
// already proven `unsignedResult`. Such a value lives in i32 but ranges [0, 2^32),
|
|
870
|
+
// so signed i32 ops on it are wrong — exprType widens its arithmetic to f64 to
|
|
871
|
+
// match emit (which reboxes via `f64.convert_i32_u`). Unsignedness through a local
|
|
872
|
+
// assignment is intentionally not tracked here — kept in lockstep with narrow.js's
|
|
873
|
+
// `isUnsignedTail`, so emit and exprType agree (no trunc_sat saturation).
|
|
874
|
+
|
|
875
|
+
// `analyzeBody` was inlined to `analyzeBody(body).locals` at its three real
|
|
876
|
+
// call sites in src/compile.js and src/narrow.js — the one-line facade existed
|
|
877
|
+
// only as a historical surface and obscured the unified-walk relationship.
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Identify locals that can be stored as an unboxed i32 pointer offset instead of
|
|
881
|
+
* a NaN-boxed f64. Static type is tracked out-of-band so reads skip `__ptr_offset`
|
|
882
|
+
* and `__ptr_type` entirely and writes unbox once at the assignment site.
|
|
883
|
+
*
|
|
884
|
+
* Criteria — the local must be:
|
|
885
|
+
* - declared once with `let`/`const`, never reassigned or compound-assigned
|
|
886
|
+
* - valType is an unambiguous non-forwarding pointer kind:
|
|
887
|
+
* OBJECT, SET, MAP, CLOSURE, TYPED, BUFFER
|
|
888
|
+
* (excluded: ARRAY — forwards on realloc; STRING — SSO/heap dual encoding.)
|
|
889
|
+
* - initialized from a form that guarantees a fresh, non-null pointer of that VAL:
|
|
890
|
+
* OBJECT ← `{…}`
|
|
891
|
+
* SET ← `new Set(...)`
|
|
892
|
+
* MAP ← `new Map(...)`
|
|
893
|
+
* CLOSURE← `=>` literal
|
|
894
|
+
* BUFFER ← `new ArrayBuffer(...)`
|
|
895
|
+
* TYPED ← `new XxxArray(...)` / method returning typed array
|
|
896
|
+
* (`new DataView(...)` is TYPED but stays boxed — no elem aux)
|
|
897
|
+
* - not captured in boxed storage (boxed locals stay f64 for the heap slot)
|
|
898
|
+
* - never compared to null/undefined (we lose the nullish NaN representation)
|
|
899
|
+
*
|
|
900
|
+
* Returns Map<name, VAL> of locals to unbox.
|
|
901
|
+
*/
|
|
902
|
+
export function unboxablePtrs(body, locals, boxed) {
|
|
903
|
+
const valOf = name => ctx.func.localReps?.get(name)?.val
|
|
904
|
+
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.DATE])
|
|
905
|
+
|
|
906
|
+
// RHS must produce a fresh, non-null pointer of the declared VAL kind.
|
|
907
|
+
// OBJECT ← `{…}`
|
|
908
|
+
// CLOSURE ← `=>`
|
|
909
|
+
// SET/MAP/BUFFER/TYPED ← `new X(...)`
|
|
910
|
+
// Validating the exact ctor→VAL match keeps the analysis tied to valTypeOf, so when
|
|
911
|
+
// that helper grows (e.g. `Array.from` → ARRAY), we don't drift out of sync.
|
|
912
|
+
const isFreshInit = (expr, kind) => {
|
|
913
|
+
if (!Array.isArray(expr)) return false
|
|
914
|
+
if (kind === VAL.OBJECT) {
|
|
915
|
+
if (expr[0] === '{}') return true
|
|
916
|
+
// Call to a narrow-ABI'd helper: returns i32 ptr-offset of the same VAL kind.
|
|
917
|
+
// Unboxing skips the f64-rebox at the callsite. Verifying via sig (not just
|
|
918
|
+
// valResult) ensures the call already produces an i32 — which dual-write picks
|
|
919
|
+
// up to bind ptrKind/schemaId on the local.
|
|
920
|
+
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
921
|
+
const f = ctx.func.map?.get(expr[1])
|
|
922
|
+
return f?.sig?.ptrKind === kind
|
|
923
|
+
}
|
|
924
|
+
// `let p = arr[i]` where arr has a known elem schema: the runtime helper
|
|
925
|
+
// returns f64 (NaN-box of an OBJECT pointer), but its low 32 bits are
|
|
926
|
+
// exactly the pointer offset. Dual-write coerces once via reinterpret/wrap;
|
|
927
|
+
// subsequent `p.x` reads then become direct `f64.load offset=K (local.get $p)`
|
|
928
|
+
// (since ptrOffsetIR sees ptrKind=OBJECT and skips the per-access wrap).
|
|
929
|
+
if (expr[0] === '[]' && typeof expr[1] === 'string') {
|
|
930
|
+
const repSid = ctx.func.localReps?.get(expr[1])?.arrayElemSchema
|
|
931
|
+
return repSid != null
|
|
932
|
+
}
|
|
933
|
+
return false
|
|
934
|
+
}
|
|
935
|
+
if (kind === VAL.CLOSURE) return expr[0] === '=>'
|
|
936
|
+
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
937
|
+
const callee = expr[1]
|
|
938
|
+
if (callee.startsWith('new.')) {
|
|
939
|
+
if (kind === VAL.SET) return callee === 'new.Set'
|
|
940
|
+
if (kind === VAL.MAP) return callee === 'new.Map'
|
|
941
|
+
if (kind === VAL.DATE) return callee === 'new.Date'
|
|
942
|
+
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer'
|
|
943
|
+
if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
|
|
944
|
+
}
|
|
945
|
+
// Call to narrow-ABI'd helper of matching VAL kind.
|
|
946
|
+
const f = ctx.func.map?.get(callee)
|
|
947
|
+
if (f?.sig?.ptrKind === kind) return true
|
|
948
|
+
}
|
|
949
|
+
// Method call returning TYPED: `arr.map(fn)` where `arr` is in typedElem
|
|
950
|
+
// (locally TYPED with a known elem ctor). Only `.typed:map` is registered
|
|
951
|
+
// as TYPED-returning — `.filter`/`.slice` fall back to ARRAY emit. The
|
|
952
|
+
// typedElem.has(src) gate ensures we don't accept the polymorphic-receiver
|
|
953
|
+
// path that emits a plain ARRAY result. propagateTyped already mirrored
|
|
954
|
+
// the src ctor onto the receiver, so the unbox path picks up its aux.
|
|
955
|
+
if (kind === VAL.TYPED && expr[0] === '()' &&
|
|
956
|
+
Array.isArray(expr[1]) && expr[1][0] === '.' &&
|
|
957
|
+
typeof expr[1][1] === 'string' && expr[1][2] === 'map' &&
|
|
958
|
+
ctx.types.typedElem?.has(expr[1][1])) {
|
|
959
|
+
return true
|
|
960
|
+
}
|
|
961
|
+
return false
|
|
962
|
+
}
|
|
963
|
+
// A policy over `scanBindingUses`: an UNBOXABLE-kind `let/const` local with a
|
|
964
|
+
// fresh-pointer initializer stays unboxable unless some use forbids it. The
|
|
965
|
+
// only forbidding uses are a reassignment (`=`/compound/`++`/`--`) or a
|
|
966
|
+
// null/undefined comparison (an unboxed pointer has no nullish NaN form).
|
|
967
|
+
// Closure captures do not disqualify — a capture-*mutated* local is already
|
|
968
|
+
// in `boxed`, and a capture-*read* leaves the pointer in its own slot.
|
|
969
|
+
const result = new Map()
|
|
970
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
971
|
+
const vt = valOf(name)
|
|
972
|
+
if (!UNBOXABLE_KINDS.has(vt)) continue
|
|
973
|
+
if (locals.get(name) !== 'f64') continue
|
|
974
|
+
if (boxed?.has(name)) continue
|
|
975
|
+
if (!isFreshInit(s.initRhs, vt)) continue
|
|
976
|
+
const ok = s.uses.every(u =>
|
|
977
|
+
u.kind !== USE.REASSIGN && !(u.kind === USE.COMPARE && u.nullCmp))
|
|
978
|
+
if (ok) result.set(name, vt)
|
|
979
|
+
}
|
|
980
|
+
return result
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* CSE-safe load bases — `let/const` pointer locals whose `(f64.load offset=K $X)`
|
|
985
|
+
* reads `cseScalarLoad` (src/optimize.js) may scalar-replace without a store
|
|
986
|
+
* clobbering them. `cseScalarLoad` is module-wide disabled because it scanned
|
|
987
|
+
* *every* i32 local; a store through an i32 local legitimately aliasing the load
|
|
988
|
+
* base returned stale bytes. This pass is the missing soundness gate: a
|
|
989
|
+
* per-function whitelist, each entry proven non-aliasing — guarantee, not guess.
|
|
990
|
+
*
|
|
991
|
+
* `X` qualifies iff ALL hold:
|
|
992
|
+
* (a) X is an unboxed pointer — `localReps.get(X).ptrKind` set, `locals[X]==='i32'`.
|
|
993
|
+
* (b) X is bound exactly once (no re-decl, no `=`/`++`/`--`/compound reassign).
|
|
994
|
+
* (c) Every occurrence of X is the receiver of a `.`/`?.`/`[]` *read* — never a
|
|
995
|
+
* write target, never a bare value (alias / arg / return / stored element),
|
|
996
|
+
* never captured by a closure. So X's pointer lives only in `$X`; nothing
|
|
997
|
+
* else holds it, and no store names it.
|
|
998
|
+
* (d) The allocation X's bytes live in is disjoint from every store target.
|
|
999
|
+
* jz allocations carry one kind each and distinct kinds never share bytes,
|
|
1000
|
+
* so X is store-safe when every store's base has a determinable kind ≠ X's
|
|
1001
|
+
* source kind. Any indeterminable store target disqualifies the whole set
|
|
1002
|
+
* (a store through unknown memory could alias anything).
|
|
1003
|
+
*
|
|
1004
|
+
* (c)+(d): no store in the function can touch a cell reachable via `$X + K`, so
|
|
1005
|
+
* a load on `$X` is invariant between two control-flow boundaries — exactly
|
|
1006
|
+
* `cseScalarLoad`'s straight-line region model. Method-call mutations (`.push`,
|
|
1007
|
+
* …) need no accounting here: the pass already flushes its table on every call.
|
|
1008
|
+
*
|
|
1009
|
+
* Returns `Set<name>` — names only, no `$` prefix (the caller stamps it).
|
|
1010
|
+
*/
|
|
1011
|
+
export function cseSafeLoadBases(body, locals, localReps) {
|
|
1012
|
+
if (body === null || typeof body !== 'object') return new Set()
|
|
1013
|
+
|
|
1014
|
+
// Allocation kind a pointer name's bytes live in: ptrKind (unboxed) wins,
|
|
1015
|
+
// else value-kind, else an array-schema'd binding is an ARRAY, else unknown.
|
|
1016
|
+
const kindOf = (name) => {
|
|
1017
|
+
if (typeof name !== 'string') return null
|
|
1018
|
+
const r = localReps?.get(name)
|
|
1019
|
+
return r?.ptrKind || r?.val || (r?.arrayElemSchema != null ? VAL.ARRAY : null) ||
|
|
1020
|
+
ctx.scope.globalValTypes?.get(name) || null
|
|
1021
|
+
}
|
|
1022
|
+
// X's bytes live in: the array/object an element read drew it from
|
|
1023
|
+
// (`X = src[i]` / `X = src.f`), else a fresh `{}`/`new` (X's own kind).
|
|
1024
|
+
const srcKind = (rhs) =>
|
|
1025
|
+
Array.isArray(rhs) && (rhs[0] === '[]' || rhs[0] === '.' || rhs[0] === '?.') &&
|
|
1026
|
+
typeof rhs[1] === 'string' ? kindOf(rhs[1]) : valTypeOf(rhs)
|
|
1027
|
+
|
|
1028
|
+
// Pass 1 — bound-once unboxed-pointer candidates; record each source kind.
|
|
1029
|
+
const cand = new Map() // name → source allocation kind
|
|
1030
|
+
const declCount = new Map()
|
|
1031
|
+
const collect = (node) => {
|
|
1032
|
+
if (!Array.isArray(node)) return
|
|
1033
|
+
const op = node[0]
|
|
1034
|
+
if (op === '=>') return
|
|
1035
|
+
if (op === 'let' || op === 'const') {
|
|
1036
|
+
for (let i = 1; i < node.length; i++) {
|
|
1037
|
+
const a = node[i]
|
|
1038
|
+
if (typeof a === 'string') { declCount.set(a, (declCount.get(a) || 0) + 1); continue }
|
|
1039
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
1040
|
+
const name = a[1]
|
|
1041
|
+
declCount.set(name, (declCount.get(name) || 0) + 1)
|
|
1042
|
+
if (localReps?.get(name)?.ptrKind != null && locals.get(name) === 'i32')
|
|
1043
|
+
cand.set(name, srcKind(a[2]))
|
|
1044
|
+
collect(a[2])
|
|
1045
|
+
} else collect(a)
|
|
1046
|
+
}
|
|
1047
|
+
return
|
|
1048
|
+
}
|
|
1049
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
1050
|
+
}
|
|
1051
|
+
collect(body)
|
|
1052
|
+
for (const [n, c] of declCount) if (c > 1) cand.delete(n)
|
|
1053
|
+
if (!cand.size) return new Set()
|
|
1054
|
+
|
|
1055
|
+
// Pass 2 — every occurrence must be a `.`/`?.`/`[]` read receiver (c).
|
|
1056
|
+
const live = new Set(cand.keys())
|
|
1057
|
+
const walk = (node, inClosure) => {
|
|
1058
|
+
if (!Array.isArray(node)) return
|
|
1059
|
+
const op = node[0]
|
|
1060
|
+
if (op === 'str') return
|
|
1061
|
+
const closured = inClosure || op === '=>'
|
|
1062
|
+
if (op === 'let' || op === 'const') { // decl `=` — bound name is not a use
|
|
1063
|
+
for (let i = 1; i < node.length; i++) {
|
|
1064
|
+
const a = node[i]
|
|
1065
|
+
if (typeof a === 'string') continue
|
|
1066
|
+
if (Array.isArray(a) && a[0] === '=') {
|
|
1067
|
+
if (typeof a[1] !== 'string') walk(a[1], closured)
|
|
1068
|
+
walk(a[2], closured)
|
|
1069
|
+
} else walk(a, closured)
|
|
1070
|
+
}
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
if (op === '.' || op === '?.' || op === '[]') { // member READ — receiver is safe
|
|
1074
|
+
const o = node[1]
|
|
1075
|
+
if (typeof o === 'string') { if (inClosure && cand.has(o)) live.delete(o) }
|
|
1076
|
+
else walk(o, closured)
|
|
1077
|
+
if (op === '[]' && node[2] != null) walk(node[2], closured)
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
if (ASSIGN_OPS.has(op) || op === '++' || op === '--' || op === 'delete') {
|
|
1081
|
+
const t = node[1] // write target — X here disqualifies
|
|
1082
|
+
if (typeof t === 'string') { if (cand.has(t)) live.delete(t) }
|
|
1083
|
+
else if (Array.isArray(t) && (t[0] === '.' || t[0] === '?.' || t[0] === '[]') &&
|
|
1084
|
+
typeof t[1] === 'string' && cand.has(t[1])) live.delete(t[1])
|
|
1085
|
+
else walk(t, closured)
|
|
1086
|
+
for (let i = 2; i < node.length; i++) walk(node[i], closured)
|
|
1087
|
+
return
|
|
1088
|
+
}
|
|
1089
|
+
for (let i = 1; i < node.length; i++) { // any other position — bare X escapes
|
|
1090
|
+
const c = node[i]
|
|
1091
|
+
if (typeof c === 'string') { if (cand.has(c)) live.delete(c) }
|
|
1092
|
+
else walk(c, closured)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
walk(body, false)
|
|
1096
|
+
if (!live.size) return live
|
|
1097
|
+
|
|
1098
|
+
// Pass 3 — store-target disjointness (d). A store lands in `base`'s allocation.
|
|
1099
|
+
let unknownStore = false
|
|
1100
|
+
const storeKinds = new Set()
|
|
1101
|
+
const scanStores = (node) => {
|
|
1102
|
+
if (!Array.isArray(node)) return
|
|
1103
|
+
const op = node[0]
|
|
1104
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && Array.isArray(node[1]) &&
|
|
1105
|
+
(node[1][0] === '.' || node[1][0] === '?.' || node[1][0] === '[]') &&
|
|
1106
|
+
typeof node[1][1] === 'string') {
|
|
1107
|
+
const k = kindOf(node[1][1])
|
|
1108
|
+
if (k == null) unknownStore = true
|
|
1109
|
+
else storeKinds.add(k)
|
|
1110
|
+
}
|
|
1111
|
+
for (let i = 1; i < node.length; i++) scanStores(node[i])
|
|
1112
|
+
}
|
|
1113
|
+
scanStores(body)
|
|
1114
|
+
if (unknownStore) return new Set()
|
|
1115
|
+
|
|
1116
|
+
const safe = new Set()
|
|
1117
|
+
for (const name of live) {
|
|
1118
|
+
const k = cand.get(name)
|
|
1119
|
+
if (k != null && !storeKinds.has(k)) safe.add(name)
|
|
1120
|
+
}
|
|
1121
|
+
return safe
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Whole-program SRoA eligibility — decides which object schemas may back an
|
|
1127
|
+
* `Array<S>` with the `structInline` carrier (the K f64 schema fields inlined
|
|
1128
|
+
* per element, no per-row heap object). Writes `ctx.schema.inlineArray:
|
|
1129
|
+
* Set<sid>`, read by the array push / index / length codegen.
|
|
1130
|
+
*
|
|
1131
|
+
* Default-disqualify: a schema is inlinable only when *every* observed use of
|
|
1132
|
+
* every `Array<S>` binding — across all user functions and module inits — is
|
|
1133
|
+
* one the structInline codegen handles. A missed or unrecognized use poisons
|
|
1134
|
+
* the schema, so the worst outcome is a lost optimization, never a stride
|
|
1135
|
+
* mismatch (miscompile).
|
|
1136
|
+
*
|
|
1137
|
+
* Handled uses of an `Array<S>` binding `a`:
|
|
1138
|
+
* - decl/reassign from `[]` (empty), a call returning `Array<S>`, or an alias
|
|
1139
|
+
* - `a.push({S-literal})` — struct push (K-cell store)
|
|
1140
|
+
* - `a.length` — physical len / K
|
|
1141
|
+
* - `a[i]` consumed as `const p = a[i]` cursor, or directly `a[i].field`
|
|
1142
|
+
* - `a` passed where the callee param is `Array<S>` (paramReps agreement)
|
|
1143
|
+
* - `return a` when the enclosing function returns `Array<S>`
|
|
1144
|
+
* A cursor `p` (`const p = a[i]`) may only be read/written as `p.field`.
|
|
1145
|
+
* Anything else — bare ref, value escape, other array method, `a[i] = …`
|
|
1146
|
+
* element-replace — poisons S.
|
|
1147
|
+
*
|
|
1148
|
+
* Reads codegen truth: a binding is `Array<S>` iff its settled rep
|
|
1149
|
+
* (`funcFacts.get(func).localReps`) carries `arrayElemSchema = S` — the exact
|
|
1150
|
+
* map the emitter consults — so the analysis and the emitter never disagree on
|
|
1151
|
+
* which bindings are inline-carried.
|
|
1152
|
+
*
|
|
1153
|
+
* Conservative corners (sound, give up the optimization): closures and module
|
|
1154
|
+
* inits are not walked in detail — any schema reachable as a `.push({S})`
|
|
1155
|
+
* argument, an `Array<S>`-returning call, an `[{S}, …]` literal, or a captured
|
|
1156
|
+
* tracked array inside one is poisoned.
|
|
1157
|
+
*/
|
|
1158
|
+
export function analyzeStructInline(funcFacts, programFacts) {
|
|
1159
|
+
const inlineArray = ctx.schema?.inlineArray
|
|
1160
|
+
if (!inlineArray || !ctx.schema?.list) return
|
|
1161
|
+
const { paramReps } = programFacts
|
|
1162
|
+
const cand = new Set() // sids observed as an `Array<S>` element schema
|
|
1163
|
+
const black = new Set() // sids disqualified by some use
|
|
1164
|
+
|
|
1165
|
+
const propsOf = (sid) => ctx.schema.list[sid] || []
|
|
1166
|
+
const inSchema = (sid, p) => typeof p === 'string' && propsOf(sid).includes(p)
|
|
1167
|
+
const isStrLit = (k) => Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string'
|
|
1168
|
+
|
|
1169
|
+
// Argument list of a `['()', callee, argNode]` call node.
|
|
1170
|
+
const argsOf = (node) => {
|
|
1171
|
+
const a = node[2]
|
|
1172
|
+
return a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// `name` referenced anywhere as a value (skips `:`/`.` property-name slots).
|
|
1176
|
+
const mentions = (node, name) => {
|
|
1177
|
+
if (typeof node === 'string') return node === name
|
|
1178
|
+
if (!Array.isArray(node)) return false
|
|
1179
|
+
const op = node[0]
|
|
1180
|
+
if (op === 'str') return false
|
|
1181
|
+
if (op === ':') return mentions(node[2], name)
|
|
1182
|
+
if (op === '.' || op === '?.') return mentions(node[1], name)
|
|
1183
|
+
for (let i = 1; i < node.length; i++) if (mentions(node[i], name)) return true
|
|
1184
|
+
return false
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Poison every schema whose `Array<S>` could materialize inside an un-walked
|
|
1188
|
+
// subtree (closure body / module init): `.push({S})` args, `Array<S>`-returning
|
|
1189
|
+
// calls, `[{S}, …]` array literals. Standalone `{S}` objects are independent
|
|
1190
|
+
// of array layout and intentionally left alone.
|
|
1191
|
+
const poisonAll = (node) => {
|
|
1192
|
+
if (!Array.isArray(node)) return
|
|
1193
|
+
const op = node[0]
|
|
1194
|
+
if (op === '()') {
|
|
1195
|
+
const callee = node[1]
|
|
1196
|
+
if (typeof callee === 'string') {
|
|
1197
|
+
const sid = ctx.func.map?.get(callee)?.arrayElemSchema
|
|
1198
|
+
if (sid != null) black.add(sid)
|
|
1199
|
+
} else if (Array.isArray(callee) && callee[0] === '.' && callee[2] === 'push') {
|
|
1200
|
+
for (const a of argsOf(node)) {
|
|
1201
|
+
const sid = objLiteralSchemaId(a)
|
|
1202
|
+
if (sid != null) black.add(sid)
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
} else if (op === '[' || op === '[]') {
|
|
1206
|
+
for (const el of staticArrayElems(node) || []) {
|
|
1207
|
+
const sid = objLiteralSchemaId(el)
|
|
1208
|
+
if (sid != null) black.add(sid)
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
for (let i = 1; i < node.length; i++) poisonAll(node[i])
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
for (const [func, facts] of funcFacts) {
|
|
1215
|
+
const body = func?.body
|
|
1216
|
+
const reps = facts?.localReps
|
|
1217
|
+
if (func?.raw || !reps || body == null || typeof body !== 'object') continue
|
|
1218
|
+
|
|
1219
|
+
// `Array<S>` bindings of this function (codegen truth) and their schemas.
|
|
1220
|
+
const arrName = new Map() // name → sid
|
|
1221
|
+
for (const [name, r] of reps) {
|
|
1222
|
+
const sid = r?.arrayElemSchema
|
|
1223
|
+
if (sid == null) continue
|
|
1224
|
+
if ((propsOf(sid).length || 0) < 1) continue // K=0 — not inlinable
|
|
1225
|
+
cand.add(sid)
|
|
1226
|
+
arrName.set(name, sid)
|
|
1227
|
+
}
|
|
1228
|
+
if (!arrName.size) continue
|
|
1229
|
+
|
|
1230
|
+
// A structInline `Array<S>` value is only ever born from an empty `[]`
|
|
1231
|
+
// grown by structInline `.push`. `expr` is such a producer of `Array<sid>`
|
|
1232
|
+
// iff it is: a tracked `Array<sid>` alias, an empty `[]` literal, or a call
|
|
1233
|
+
// to a user function (whose returned array is structInline whenever sid
|
|
1234
|
+
// survives this whole-program pass). Every other source — a non-empty
|
|
1235
|
+
// `[{S},…]` literal, a builtin call (`JSON.parse`, `Object.values`, `.map`,
|
|
1236
|
+
// `.slice`, a member access onto a parsed object) — yields a taggedLinear
|
|
1237
|
+
// array and must poison sid.
|
|
1238
|
+
const safeArrSource = (expr, sid) => {
|
|
1239
|
+
if (typeof expr === 'string') return arrName.get(expr) === sid
|
|
1240
|
+
if (!Array.isArray(expr)) return false
|
|
1241
|
+
const elems = staticArrayElems(expr)
|
|
1242
|
+
if (elems) return elems.length === 0
|
|
1243
|
+
return expr[0] === '()' && typeof expr[1] === 'string' && !!ctx.func.map?.has(expr[1])
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Pass 1 — collect `const p = a[i]` cursors; drop on name clash / re-decl.
|
|
1247
|
+
const cursor = new Map() // name → sid
|
|
1248
|
+
const declSeen = new Set()
|
|
1249
|
+
const collectCursors = (node) => {
|
|
1250
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
1251
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
1252
|
+
for (let i = 1; i < node.length; i++) {
|
|
1253
|
+
const d = node[i]
|
|
1254
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1255
|
+
const name = d[1], rhs = d[2]
|
|
1256
|
+
if (declSeen.has(name)) { const s = cursor.get(name); if (s != null) black.add(s) }
|
|
1257
|
+
declSeen.add(name)
|
|
1258
|
+
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 &&
|
|
1259
|
+
typeof rhs[1] === 'string' && arrName.has(rhs[1]) && !isStrLit(rhs[2])) {
|
|
1260
|
+
const sid = arrName.get(rhs[1])
|
|
1261
|
+
if (cursor.has(name) || arrName.has(name)) black.add(sid)
|
|
1262
|
+
else cursor.set(name, sid)
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
for (let i = 1; i < node.length; i++) collectCursors(node[i])
|
|
1267
|
+
}
|
|
1268
|
+
collectCursors(body)
|
|
1269
|
+
|
|
1270
|
+
// A `['[]', arrName, idx]` element read of a tracked array → its sid.
|
|
1271
|
+
const elemArrSid = (n) =>
|
|
1272
|
+
Array.isArray(n) && n[0] === '[]' && n.length === 3 &&
|
|
1273
|
+
typeof n[1] === 'string' && arrName.has(n[1]) && !isStrLit(n[2])
|
|
1274
|
+
? arrName.get(n[1]) : null
|
|
1275
|
+
|
|
1276
|
+
// Pass 2 — verify every occurrence is a structInline-handled use.
|
|
1277
|
+
const flag = (c) => {
|
|
1278
|
+
if (typeof c !== 'string') return false
|
|
1279
|
+
if (arrName.has(c)) { black.add(arrName.get(c)); return true }
|
|
1280
|
+
if (cursor.has(c)) { black.add(cursor.get(c)); return true }
|
|
1281
|
+
return false
|
|
1282
|
+
}
|
|
1283
|
+
const visitChild = (c) => { if (!flag(c)) verify(c) }
|
|
1284
|
+
|
|
1285
|
+
function verify(node) {
|
|
1286
|
+
if (!Array.isArray(node)) return
|
|
1287
|
+
const op = node[0]
|
|
1288
|
+
if (op === 'str') return
|
|
1289
|
+
if (op === '=>') { // closure — un-walked, poison
|
|
1290
|
+
for (const n of arrName.keys()) if (mentions(node, n)) black.add(arrName.get(n))
|
|
1291
|
+
for (const [n, s] of cursor) if (mentions(node, n)) black.add(s)
|
|
1292
|
+
poisonAll(node)
|
|
1293
|
+
return
|
|
1294
|
+
}
|
|
1295
|
+
if (op === ':') { visitChild(node[2]); return }
|
|
1296
|
+
|
|
1297
|
+
if (op === '.' || op === '?.') {
|
|
1298
|
+
const o = node[1], p = node[2]
|
|
1299
|
+
if (typeof o === 'string') {
|
|
1300
|
+
if (arrName.has(o)) { if (!(op === '.' && p === 'length')) black.add(arrName.get(o)) }
|
|
1301
|
+
else if (cursor.has(o)) { if (!(op === '.' && inSchema(cursor.get(o), p))) black.add(cursor.get(o)) }
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1304
|
+
const esid = elemArrSid(o)
|
|
1305
|
+
if (esid != null) {
|
|
1306
|
+
if (!(op === '.' && inSchema(esid, p))) black.add(esid)
|
|
1307
|
+
visitChild(o[2])
|
|
1308
|
+
return
|
|
1309
|
+
}
|
|
1310
|
+
visitChild(o)
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (op === '[]') {
|
|
1315
|
+
const o = node[1], k = node[2]
|
|
1316
|
+
if (typeof o === 'string') {
|
|
1317
|
+
if (arrName.has(o)) black.add(arrName.get(o)) // element value escape
|
|
1318
|
+
else if (cursor.has(o)) { if (!(isStrLit(k) && inSchema(cursor.get(o), k[1]))) black.add(cursor.get(o)) }
|
|
1319
|
+
if (k != null) visitChild(k)
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1322
|
+
const esid = elemArrSid(o)
|
|
1323
|
+
if (esid != null) {
|
|
1324
|
+
if (!(isStrLit(k) && inSchema(esid, k[1]))) black.add(esid)
|
|
1325
|
+
visitChild(o[2])
|
|
1326
|
+
} else if (o != null) visitChild(o)
|
|
1327
|
+
if (k != null) visitChild(k)
|
|
1328
|
+
return
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Reassignment of the array binding — the rhs must be a structInline
|
|
1332
|
+
// `Array<S>` producer; an alias is left un-walked (flagging it would
|
|
1333
|
+
// self-poison), other producers are walked to verify their subtree.
|
|
1334
|
+
if (op === '=' && typeof node[1] === 'string' && arrName.has(node[1])) {
|
|
1335
|
+
const sid = arrName.get(node[1])
|
|
1336
|
+
if (!safeArrSource(node[2], sid)) black.add(sid)
|
|
1337
|
+
else if (typeof node[2] !== 'string') visitChild(node[2])
|
|
1338
|
+
return
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (op === '()') {
|
|
1342
|
+
const callee = node[1]
|
|
1343
|
+
if (Array.isArray(callee) && callee[0] === '.') {
|
|
1344
|
+
const recv = callee[1], method = callee[2]
|
|
1345
|
+
if (typeof recv === 'string' && arrName.has(recv)) {
|
|
1346
|
+
const sid = arrName.get(recv)
|
|
1347
|
+
const args = argsOf(node)
|
|
1348
|
+
if (method !== 'push' || !args.length) black.add(sid)
|
|
1349
|
+
else for (const arg of args) {
|
|
1350
|
+
if (Array.isArray(arg) && arg[0] === '{}' && objLiteralSchemaId(arg) === sid) {
|
|
1351
|
+
for (let i = 1; i < arg.length; i++) {
|
|
1352
|
+
const pr = arg[i]
|
|
1353
|
+
visitChild(Array.isArray(pr) && pr[0] === ':' ? pr[2] : pr)
|
|
1354
|
+
}
|
|
1355
|
+
} else black.add(sid)
|
|
1356
|
+
}
|
|
1357
|
+
return
|
|
1358
|
+
}
|
|
1359
|
+
if (typeof recv === 'string' && cursor.has(recv)) { black.add(cursor.get(recv)); return }
|
|
1360
|
+
const esid = elemArrSid(recv)
|
|
1361
|
+
if (esid != null) { black.add(esid); visitChild(recv[2]) }
|
|
1362
|
+
else visitChild(recv)
|
|
1363
|
+
for (const a of argsOf(node)) visitChild(a)
|
|
1364
|
+
return
|
|
1365
|
+
}
|
|
1366
|
+
if (typeof callee === 'string') {
|
|
1367
|
+
const args = argsOf(node)
|
|
1368
|
+
const known = ctx.func.map?.has(callee)
|
|
1369
|
+
const cParams = paramReps?.get(callee)
|
|
1370
|
+
for (let k = 0; k < args.length; k++) {
|
|
1371
|
+
const arg = args[k]
|
|
1372
|
+
if (typeof arg === 'string' && arrName.has(arg)) {
|
|
1373
|
+
const sid = arrName.get(arg)
|
|
1374
|
+
if (!(known && cParams?.get(k)?.arrayElemSchema === sid)) black.add(sid)
|
|
1375
|
+
} else if (!flag(arg)) verify(arg)
|
|
1376
|
+
}
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1379
|
+
visitChild(callee)
|
|
1380
|
+
for (const a of argsOf(node)) visitChild(a)
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (op === 'return') {
|
|
1385
|
+
const e = node[1]
|
|
1386
|
+
if (typeof e === 'string') {
|
|
1387
|
+
if (arrName.has(e)) { if (func.arrayElemSchema !== arrName.get(e)) black.add(arrName.get(e)) }
|
|
1388
|
+
else flag(e)
|
|
1389
|
+
return
|
|
1390
|
+
}
|
|
1391
|
+
// A function typed `Array<S>` must return a structInline producer —
|
|
1392
|
+
// a non-empty literal / builtin call here yields a taggedLinear array.
|
|
1393
|
+
if (func.arrayElemSchema != null && !safeArrSource(e, func.arrayElemSchema))
|
|
1394
|
+
black.add(func.arrayElemSchema)
|
|
1395
|
+
const esid = elemArrSid(e)
|
|
1396
|
+
if (esid != null) { black.add(esid); visitChild(e[2]); return }
|
|
1397
|
+
if (e != null) visitChild(e)
|
|
1398
|
+
return
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (op === 'let' || op === 'const') {
|
|
1402
|
+
for (let i = 1; i < node.length; i++) {
|
|
1403
|
+
const d = node[i]
|
|
1404
|
+
if (!Array.isArray(d) || d[0] !== '=') { if (Array.isArray(d)) visitChild(d); continue }
|
|
1405
|
+
const name = d[1], rhs = d[2]
|
|
1406
|
+
if (typeof name === 'string' && cursor.has(name) &&
|
|
1407
|
+
Array.isArray(rhs) && rhs[0] === '[]') {
|
|
1408
|
+
if (rhs[2] != null) visitChild(rhs[2]) // cursor decl — verify index only
|
|
1409
|
+
continue
|
|
1410
|
+
}
|
|
1411
|
+
if (typeof name === 'string' && arrName.has(name)) {
|
|
1412
|
+
const sid = arrName.get(name)
|
|
1413
|
+
if (!safeArrSource(rhs, sid)) black.add(sid) // non-structInline producer
|
|
1414
|
+
else if (typeof rhs !== 'string') visitChild(rhs) // [] / user-call — verify subtree
|
|
1415
|
+
continue
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof name !== 'string') visitChild(name)
|
|
1418
|
+
visitChild(rhs)
|
|
1419
|
+
}
|
|
1420
|
+
return
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
for (let i = 1; i < node.length; i++) visitChild(node[i])
|
|
1424
|
+
}
|
|
1425
|
+
verify(body)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Module inits are not walked in detail — poison any schema whose array form
|
|
1429
|
+
// could appear there (struct-array consumed/built at module scope).
|
|
1430
|
+
if (ctx.module?.moduleInits) for (const mi of ctx.module.moduleInits) poisonAll(mi)
|
|
1431
|
+
|
|
1432
|
+
for (const sid of cand) if (!black.has(sid)) inlineArray.add(sid)
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/** Schema id when `name` is bound (codegen truth) to a structInline `Array<S>`,
|
|
1436
|
+
* else null. `ctx.func.localReps` is the per-function rep map the emitter
|
|
1437
|
+
* consults for element-schema facts; `ctx.schema.inlineArray` is the
|
|
1438
|
+
* whole-program eligibility set filled by `analyzeStructInline`. Read together
|
|
1439
|
+
* so the emitter never inline-carries a binding the analysis rejected. */
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Whole-program function-namespace SRoA analysis.
|
|
1443
|
+
*
|
|
1444
|
+
* A user function used as a property bag — `parse.space = …; parse.step()` —
|
|
1445
|
+
* is otherwise compiled as a dynamic object: each `f.prop` write becomes a
|
|
1446
|
+
* `__dyn_set` into a hash side-table keyed by the closure pointer, each read a
|
|
1447
|
+
* `__dyn_get`. But a function's property table can never be observed by the
|
|
1448
|
+
* host (the host gets only the callable; the table lives in jz linear memory),
|
|
1449
|
+
* so the property set is statically closed — jz sees every `f.PROP` site. When
|
|
1450
|
+
* `f` never escapes as a bare value, each property dissolves:
|
|
1451
|
+
* - written once, at module top level, to a known function → the property is
|
|
1452
|
+
* constant: every `f.PROP` site rewrites straight to that function name
|
|
1453
|
+
* (direct calls, no storage at all).
|
|
1454
|
+
* - otherwise → a mutable f64 module global (`global.get` / `global.set`).
|
|
1455
|
+
*
|
|
1456
|
+
* Returns `Map<funcName, { disq, props:Set, valRead:Set,
|
|
1457
|
+
* writes:Map<prop,[{rhs,atInit}]> }>` — `valRead` is the subset of props read
|
|
1458
|
+
* as a value (not merely called). `flattenFuncNamespaces` (plan.js) turns it
|
|
1459
|
+
* into the rewrite. A name carrying `disq` escapes / is reassigned / is
|
|
1460
|
+
* computed-indexed and must not be touched.
|
|
1461
|
+
*/
|
|
1462
|
+
export function analyzeFuncNamespaces(ast) {
|
|
1463
|
+
const funcNames = ctx.func.names
|
|
1464
|
+
if (!funcNames || !funcNames.size) return new Map()
|
|
1465
|
+
|
|
1466
|
+
const ns = new Map()
|
|
1467
|
+
const rec = (name) => {
|
|
1468
|
+
let r = ns.get(name)
|
|
1469
|
+
if (!r) ns.set(name, r = { disq: false, props: new Set(), valRead: new Set(), writes: new Map() })
|
|
1470
|
+
return r
|
|
1471
|
+
}
|
|
1472
|
+
// `['.'|'?.', f, P]` member where f is a known function and P a string key.
|
|
1473
|
+
const memberOf = (n) =>
|
|
1474
|
+
Array.isArray(n) && (n[0] === '.' || n[0] === '?.') &&
|
|
1475
|
+
isFuncRef(n[1], funcNames) && typeof n[2] === 'string' ? n : null
|
|
1476
|
+
|
|
1477
|
+
// `atInit` — node is a direct top-level statement (constant-fold candidate);
|
|
1478
|
+
// read only at the `=` handler, never propagated into sub-expressions.
|
|
1479
|
+
function visit(node, atInit) {
|
|
1480
|
+
if (typeof node === 'string') {
|
|
1481
|
+
// Bare mention of a known function in value position — it escapes; an
|
|
1482
|
+
// alias could reach its property table. Disqualify.
|
|
1483
|
+
if (funcNames.has(node)) rec(node).disq = true
|
|
1484
|
+
return
|
|
1485
|
+
}
|
|
1486
|
+
if (!Array.isArray(node)) return
|
|
1487
|
+
const op = node[0]
|
|
1488
|
+
|
|
1489
|
+
if (op === 'let' || op === 'const') {
|
|
1490
|
+
for (let i = 1; i < node.length; i++) {
|
|
1491
|
+
const d = node[i]
|
|
1492
|
+
if (Array.isArray(d) && d[0] === '=') {
|
|
1493
|
+
if (!isFuncRef(d[1], funcNames)) visit(d[1], false) // skip f's own decl
|
|
1494
|
+
// `let f = f` is prepare's self-name placeholder for a lifted function —
|
|
1495
|
+
// skip it (a bare visit would falsely disqualify f). Any other funcRef
|
|
1496
|
+
// rhs (`let g = f`) is a real alias and must disqualify f.
|
|
1497
|
+
if (!(isFuncRef(d[2], funcNames) && d[2] === d[1])) visit(d[2], false)
|
|
1498
|
+
} else visit(d, false)
|
|
1499
|
+
}
|
|
1500
|
+
return
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (op === 'export') {
|
|
1504
|
+
// Exporting the function value is safe — the host gets the callable,
|
|
1505
|
+
// never the linear-memory property table. Skip bare function children.
|
|
1506
|
+
for (let i = 1; i < node.length; i++)
|
|
1507
|
+
if (!isFuncRef(node[i], funcNames)) visit(node[i], false)
|
|
1508
|
+
return
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (op === '=') {
|
|
1512
|
+
const m = memberOf(node[1])
|
|
1513
|
+
if (m) {
|
|
1514
|
+
const r = rec(m[1]); r.props.add(m[2])
|
|
1515
|
+
let w = r.writes.get(m[2]); if (!w) r.writes.set(m[2], w = [])
|
|
1516
|
+
w.push({ rhs: node[2], atInit })
|
|
1517
|
+
visit(node[2], false)
|
|
1518
|
+
return
|
|
1519
|
+
}
|
|
1520
|
+
if (isFuncRef(node[1], funcNames)) rec(node[1]).disq = true // reassignment
|
|
1521
|
+
else visit(node[1], false)
|
|
1522
|
+
visit(node[2], false)
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (op === '()') {
|
|
1527
|
+
const m = memberOf(node[1])
|
|
1528
|
+
if (m) rec(m[1]).props.add(m[2])
|
|
1529
|
+
else if (!isFuncRef(node[1], funcNames)) visit(node[1], false) // bare f(...) ok
|
|
1530
|
+
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// `f.PROP` / `f?.PROP` as a plain value (read) — not the callee of a call
|
|
1535
|
+
// (those are handled by the `()` branch above). A value-read means the
|
|
1536
|
+
// property's stored value must stay retrievable; devirt cannot drop it.
|
|
1537
|
+
const m = memberOf(node)
|
|
1538
|
+
if (m) { const r = rec(m[1]); r.props.add(m[2]); r.valRead.add(m[2]); return }
|
|
1539
|
+
|
|
1540
|
+
// Computed `f[k]` — the key set is no longer static.
|
|
1541
|
+
if (op === '[]' && isFuncRef(node[1], funcNames)) {
|
|
1542
|
+
rec(node[1]).disq = true
|
|
1543
|
+
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
1544
|
+
return
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
for (let i = 1; i < node.length; i++) visit(node[i], false)
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const visitTop = (n) => {
|
|
1551
|
+
if (Array.isArray(n) && n[0] === ';')
|
|
1552
|
+
for (let i = 1; i < n.length; i++) visit(n[i], true)
|
|
1553
|
+
else visit(n, true)
|
|
1554
|
+
}
|
|
1555
|
+
visitTop(ast)
|
|
1556
|
+
// Bundled multi-module programs keep each module's top-level statements in
|
|
1557
|
+
// moduleInits, not `ast` — the `f.prop = …` writes that define a namespace
|
|
1558
|
+
// live there. Walk them at init scope so writes are recorded and an escape
|
|
1559
|
+
// inside init code still disqualifies.
|
|
1560
|
+
for (const mi of ctx.module.moduleInits || []) visitTop(mi)
|
|
1561
|
+
for (const fn of ctx.func.list) if (fn.body && !fn.raw) visit(fn.body, false)
|
|
1562
|
+
|
|
1563
|
+
return ns
|
|
1564
|
+
}
|
|
1565
|
+
|