jz 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/cli.js +163 -0
- package/index.js +217 -0
- package/module/array.js +1317 -0
- package/module/collection.js +791 -0
- package/module/console.js +190 -0
- package/module/core.js +642 -0
- package/module/function.js +180 -0
- package/module/index.js +15 -0
- package/module/json.js +504 -0
- package/module/math.js +389 -0
- package/module/number.js +605 -0
- package/module/object.js +357 -0
- package/module/regex.js +913 -0
- package/module/schema.js +104 -0
- package/module/string.js +928 -0
- package/module/symbol.js +54 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +711 -0
- package/package.json +54 -5
- package/src/analyze.js +1906 -0
- package/src/compile.js +2175 -0
- package/src/ctx.js +243 -0
- package/src/emit.js +2095 -0
- package/src/host.js +524 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +391 -0
- package/src/optimize.js +1352 -0
- package/src/prepare.js +1598 -0
- package/wasi.js +74 -0
package/src/analyze.js
ADDED
|
@@ -0,0 +1,1906 @@
|
|
|
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.repByLocal` (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
|
+
* - valTypeOf: expression-level value-type inference (pure)
|
|
12
|
+
* - lookupValType: name→VAL.* resolver (func scope ∪ global scope)
|
|
13
|
+
* - analyzeBody: single unified walk — body-keyed cache, returns
|
|
14
|
+
* { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems }
|
|
15
|
+
* - analyzeValTypes: ctx-mutating pass — writes types + tracks regex/typed + localProps
|
|
16
|
+
* - analyzeLocals: thin clone-and-extend facade over analyzeBody().locals
|
|
17
|
+
* - analyzeDynKeys: cross-function scan for `obj[runtimeKey]` → sets ctx.types.dynKeyVars
|
|
18
|
+
* - analyzeBoxedCaptures:detect mutably-captured vars → ctx.func.boxed cells
|
|
19
|
+
* - extractParams/classifyParam/collectParamNames: arrow param AST normalization helpers
|
|
20
|
+
*
|
|
21
|
+
* Ordering: analyzeDynKeys runs once per compile; others run per function during compile().
|
|
22
|
+
*
|
|
23
|
+
* @module analyze
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { ctx, err } from './ctx.js'
|
|
27
|
+
|
|
28
|
+
export const T = '\uE000'
|
|
29
|
+
|
|
30
|
+
/** Statement operators — used to distinguish block bodies from object literals. */
|
|
31
|
+
export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'for-in', 'while', 'break', 'continue', 'switch',
|
|
32
|
+
'=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
|
|
33
|
+
'throw', 'try', 'catch', 'finally', '++', '--', '()'])
|
|
34
|
+
|
|
35
|
+
/** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
|
|
36
|
+
* Both share the `'{}'` op tag; blocks have a non-`':'` first child (object literals start with key:val pairs). */
|
|
37
|
+
export const isBlockBody = (body) =>
|
|
38
|
+
Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
|
|
39
|
+
|
|
40
|
+
/** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
|
|
41
|
+
* Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
|
|
42
|
+
* coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
|
|
43
|
+
export const collectReturnExprs = (node, out) => {
|
|
44
|
+
if (!Array.isArray(node)) return
|
|
45
|
+
const [op, ...args] = node
|
|
46
|
+
if (op === '=>') return
|
|
47
|
+
if (op === 'return') { if (args[0] != null) out.push(args[0]); return }
|
|
48
|
+
for (const a of args) collectReturnExprs(a, out)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True if every control-flow path through `n` is guaranteed to terminate via return/throw.
|
|
52
|
+
* Conservative: only recognizes block-trailing return, both arms of complete if/else. Loops/switches
|
|
53
|
+
* count as non-terminating since fall-through is possible. Used by ptr-narrowing to ensure
|
|
54
|
+
* fallthrough fallback won't produce a wrong-typed undef. */
|
|
55
|
+
export const alwaysReturns = (n) => {
|
|
56
|
+
if (!Array.isArray(n)) return false
|
|
57
|
+
const op = n[0]
|
|
58
|
+
if (op === '=>') return false
|
|
59
|
+
if (op === 'return' || op === 'throw') return true
|
|
60
|
+
if (op === '{}' || op === ';') return alwaysReturns(n[n.length - 1])
|
|
61
|
+
if (op === 'if') return n.length >= 4 && alwaysReturns(n[2]) && alwaysReturns(n[3])
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True if `n` contains a bare `return;` (no value → undefined).
|
|
66
|
+
* Bare returns force the result type to f64 (undef sentinel) — narrowing must skip such bodies. */
|
|
67
|
+
export const hasBareReturn = (n) => {
|
|
68
|
+
if (!Array.isArray(n)) return false
|
|
69
|
+
if (n[0] === '=>') return false
|
|
70
|
+
if (n[0] === 'return' && n[1] == null) return true
|
|
71
|
+
return n.some(hasBareReturn)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Unify body→return-expressions: block bodies collect via `collectReturnExprs`,
|
|
75
|
+
* expression bodies wrap into `[body]`. Pure convenience over the
|
|
76
|
+
* `if (isBlock) collect(...) else exprs.push(body)` pattern repeated across
|
|
77
|
+
* narrowing passes. */
|
|
78
|
+
export const returnExprs = (body) => {
|
|
79
|
+
if (isBlockBody(body)) {
|
|
80
|
+
const out = []
|
|
81
|
+
collectReturnExprs(body, out)
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
return [body]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Value types — what a variable holds (for method dispatch, schema resolution)
|
|
88
|
+
export const VAL = {
|
|
89
|
+
NUMBER: 'number', ARRAY: 'array', STRING: 'string',
|
|
90
|
+
OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
|
|
91
|
+
CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
|
|
92
|
+
BIGINT: 'bigint', BUFFER: 'buffer',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ValueRep — unified per-local + per-param representation record. (S2.)
|
|
97
|
+
*
|
|
98
|
+
* One shape, two storages:
|
|
99
|
+
* - per-local (current func): ctx.func.repByLocal: Map<name, ValueRep>
|
|
100
|
+
* - per-param (cross-call): programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>
|
|
101
|
+
*
|
|
102
|
+
* Lattice per field: undefined = unobserved, null = sticky-poison
|
|
103
|
+
* (cross-site disagreement), value = consensus. Local reps don't use the null
|
|
104
|
+
* sentinel (locals are intra-function — single point of truth). Param reps do
|
|
105
|
+
* (cross-call fixpoint convergence).
|
|
106
|
+
*
|
|
107
|
+
* Fields:
|
|
108
|
+
* val: VAL.* — value-type for method dispatch / schema / length
|
|
109
|
+
* wasm: 'i32'|'f64' — narrowed wasm type at param boundary (param-only today)
|
|
110
|
+
* ptrKind: VAL.* — local stores unboxed i32 pointer offset (local-only today)
|
|
111
|
+
* ptrAux: i32 — kind-dependent aux (TYPED elem code, schemaId, …)
|
|
112
|
+
* schemaId: i32 — schema binding for known-shape OBJECTs
|
|
113
|
+
* arrayElemSchema: i32 — Array<schemaId> element shape
|
|
114
|
+
* arrayElemValType: VAL.* — Array<VAL.*> element val-kind
|
|
115
|
+
* jsonShape: obj — { vt, props?, elem? } for HASH/ARRAY trees parsed
|
|
116
|
+
* from a compile-time JSON.parse source. Propagates
|
|
117
|
+
* through `.prop` and `[i]` so nested chains stay typed.
|
|
118
|
+
* typedCtor: str — TypedArray ctor name (`Float64Array`, …)
|
|
119
|
+
* intCertain: bool — proven integer-valued (every defining RHS is integer-shaped).
|
|
120
|
+
* Pure analysis fact; codegen extensions may use it to choose
|
|
121
|
+
* i32-shaped emission inside hot regions where range fits.
|
|
122
|
+
* Boundary ABI is NOT narrowed by this fact alone — narrowing
|
|
123
|
+
* at param/result level remains a separate, opt-in decision.
|
|
124
|
+
* intConst: number — proven same integer literal at every static call site.
|
|
125
|
+
* Param-only (cross-call fixpoint). Drives constant substitution
|
|
126
|
+
* at readVar: every `local.get $param` lowers to `i32.const N`
|
|
127
|
+
* (or `f64.const N`), letting the WAT optimizer fold guards,
|
|
128
|
+
* unroll fixed-bound loops, and treeshake the read entirely.
|
|
129
|
+
* Cleared if the param is written inside the body.
|
|
130
|
+
*
|
|
131
|
+
* Future (S2 stage 4 follow-ups): boxed, intLikely, nullable.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
// === ParamReps lattice helpers (cross-call fixpoint) ===
|
|
135
|
+
// programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>. Per-field lattice:
|
|
136
|
+
// undefined unobserved, null sticky-poison (cross-site disagreement), value = consensus.
|
|
137
|
+
|
|
138
|
+
/** Per-call-site fact merge into a param's ValueRep field, with sticky-null poison
|
|
139
|
+
* on disagreement. undefined→observed (set); same value→stay; conflict→null (sticky).
|
|
140
|
+
* null is "no consensus" — readers treat it as missing. */
|
|
141
|
+
export const mergeParamFact = (rep, key, observed) => {
|
|
142
|
+
if (rep[key] === null) return // sticky poison
|
|
143
|
+
if (observed == null) { rep[key] = null; return } // unknown → poison
|
|
144
|
+
if (rep[key] === undefined) rep[key] = observed
|
|
145
|
+
else if (rep[key] !== observed) rep[key] = null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Get-or-create per-param rep at (funcName, paramIdx) on a paramReps map. */
|
|
149
|
+
export const ensureParamRep = (paramReps, funcName, k) => {
|
|
150
|
+
let m = paramReps.get(funcName)
|
|
151
|
+
if (!m) { m = new Map(); paramReps.set(funcName, m) }
|
|
152
|
+
let r = m.get(k)
|
|
153
|
+
if (!r) { r = {}; m.set(k, r) }
|
|
154
|
+
return r
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Build `paramName → fact` lookup for a caller's already-narrowed param facts.
|
|
158
|
+
* Used to flow caller's param info into its callees during the cross-call
|
|
159
|
+
* fixpoint (transitive propagation). Returns null if caller has no facts. */
|
|
160
|
+
export const callerParamFactMap = (paramReps, callerFunc, key) => {
|
|
161
|
+
if (!callerFunc) return null
|
|
162
|
+
const m = paramReps.get(callerFunc.name)
|
|
163
|
+
if (!m) return null
|
|
164
|
+
let out = null
|
|
165
|
+
for (const [k, r] of m) {
|
|
166
|
+
const v = r[key]
|
|
167
|
+
if (v != null && k < callerFunc.sig.params.length) {
|
|
168
|
+
out ||= new Map()
|
|
169
|
+
out.set(callerFunc.sig.params[k].name, v)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Reset sticky-null on a single field across all params program-wide.
|
|
176
|
+
* Used between fixpoint phases when newly-narrowed facts unblock previously-
|
|
177
|
+
* poisoned observations (e.g. valResult set after first pass). */
|
|
178
|
+
export const clearStickyNull = (paramReps, key) => {
|
|
179
|
+
for (const m of paramReps.values()) for (const r of m.values()) {
|
|
180
|
+
if (r[key] === null) r[key] = undefined
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Get the rep for a local name, or undefined if not tracked. */
|
|
185
|
+
export const repOf = name => ctx.func.repByLocal?.get(name)
|
|
186
|
+
|
|
187
|
+
/** Merge fields into a local's rep. Lazily allocates the map and the rep.
|
|
188
|
+
* Field set to `undefined` removes that field; empty rep is dropped from the map. */
|
|
189
|
+
export const updateRep = (name, fields) => {
|
|
190
|
+
const m = ctx.func.repByLocal ||= new Map()
|
|
191
|
+
const prev = m.get(name) || {}
|
|
192
|
+
const next = { ...prev, ...fields }
|
|
193
|
+
for (const k of Object.keys(next)) if (next[k] === undefined) delete next[k]
|
|
194
|
+
if (Object.keys(next).length === 0) m.delete(name)
|
|
195
|
+
else m.set(name, next)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Get the rep for a global name, or undefined if not tracked. */
|
|
199
|
+
export const repOfGlobal = name => ctx.scope.repByGlobal?.get(name)
|
|
200
|
+
|
|
201
|
+
/** Merge fields into a global's rep. Lazily allocates the map and the rep. */
|
|
202
|
+
export const updateGlobalRep = (name, fields) => {
|
|
203
|
+
const m = ctx.scope.repByGlobal ||= new Map()
|
|
204
|
+
const prev = m.get(name)
|
|
205
|
+
m.set(name, prev ? { ...prev, ...fields } : { ...fields })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Look up value type for a variable name. Order: flow-sensitive refinement (if any) →
|
|
209
|
+
* in-progress analyzeBody overlay (if any) → function-local scope → module-global scope.
|
|
210
|
+
* Refinements are pushed by the 'if' emitter when the condition is a type guard
|
|
211
|
+
* (typeof x === 't', Array.isArray(x), etc.) and popped after the then-branch.
|
|
212
|
+
* The overlay (`ctx.func.localValTypesOverlay`) is set by analyzeBody/observeSlots passes
|
|
213
|
+
* pre-emit, when `repByLocal` isn't populated yet but a local Map<name, VAL.*> is
|
|
214
|
+
* available — lets `const x = new Float64Array(); const y = x[0]` resolve y as NUMBER. */
|
|
215
|
+
export const lookupValType = name => {
|
|
216
|
+
const r = ctx.func.refinements
|
|
217
|
+
if (r && r.size) { const v = r.get(name); if (v) return v }
|
|
218
|
+
const ov = ctx.func.localValTypesOverlay
|
|
219
|
+
if (ov) { const v = ov.get(name); if (v) return v }
|
|
220
|
+
return ctx.func.repByLocal?.get(name)?.val || ctx.scope.globalValTypes?.get(name) || null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Infer value type of an AST expression (without emitting). */
|
|
224
|
+
export function valTypeOf(expr) {
|
|
225
|
+
if (expr == null) return null
|
|
226
|
+
if (typeof expr === 'number') return VAL.NUMBER
|
|
227
|
+
if (typeof expr === 'bigint') return VAL.BIGINT
|
|
228
|
+
if (typeof expr === 'string') return lookupValType(expr)
|
|
229
|
+
if (!Array.isArray(expr)) return null
|
|
230
|
+
|
|
231
|
+
const [op, ...args] = expr
|
|
232
|
+
if (op == null) {
|
|
233
|
+
// Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint
|
|
234
|
+
if (args.length === 0) return null // undefined literal
|
|
235
|
+
if (args[0] == null) return null // null literal
|
|
236
|
+
return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (op === '[') return VAL.ARRAY
|
|
240
|
+
if (op === 'str') return VAL.STRING
|
|
241
|
+
if (op === '=>') return VAL.CLOSURE
|
|
242
|
+
if (op === '//') return VAL.REGEX
|
|
243
|
+
if (op === '{}' && args[0]?.[0] === ':') return VAL.OBJECT
|
|
244
|
+
// `[]` op covers both array literals (1 arg) and index access (2 args).
|
|
245
|
+
// Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
|
|
246
|
+
// Index access: `arr[i]` → ['[]', arr, i].
|
|
247
|
+
if (op === '[]') {
|
|
248
|
+
if (args.length < 2) return VAL.ARRAY
|
|
249
|
+
// Indexed read on a known typed-array receiver yields a number (BigInt64/BigUint64Array
|
|
250
|
+
// would yield BigInt, but they're rare and we don't track per-elem type here — the
|
|
251
|
+
// .typed:[] emit path already handles their f64-cast correctly; this only affects
|
|
252
|
+
// arithmetic-time __to_num elision, where assuming Number is safe-by-construction).
|
|
253
|
+
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED) return VAL.NUMBER
|
|
254
|
+
// Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
|
|
255
|
+
// Set by analyzeValTypes from body observations + emitFunc preseed for params.
|
|
256
|
+
if (typeof args[0] === 'string') {
|
|
257
|
+
const elemVt = ctx.func.repByLocal?.get(args[0])?.arrayElemValType
|
|
258
|
+
if (elemVt) return elemVt
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
|
|
262
|
+
// to a slot whose VAL kind is monomorphic across program-wide observations,
|
|
263
|
+
// return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
|
|
264
|
+
// checks on numeric properties of known shapes. Precise-only — see
|
|
265
|
+
// ctx.schema.slotVT for why structural subtyping is intentionally off.
|
|
266
|
+
if (op === '.' && typeof args[1] === 'string' && ctx.schema?.slotVT) {
|
|
267
|
+
const slotVT = ctx.schema.slotVT(args[0], args[1])
|
|
268
|
+
if (slotVT) return slotVT
|
|
269
|
+
}
|
|
270
|
+
// VAL.HASH `.prop` propagation: when the receiver chain roots at a binding
|
|
271
|
+
// sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
|
|
272
|
+
// child's val-type. Generic for any compile-time-known JSON literal.
|
|
273
|
+
if (op === '.' && typeof args[1] === 'string') {
|
|
274
|
+
const sh = shapeOf(args[0])
|
|
275
|
+
if (sh?.vt === VAL.HASH) {
|
|
276
|
+
const child = sh.props[args[1]]
|
|
277
|
+
if (child) return child.vt
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Arithmetic expressions: BigInt if either operand is BigInt, else number
|
|
281
|
+
if (['-', 'u-', '*', '/', '%', '&', '|', '^', '<<', '>>'].includes(op)) {
|
|
282
|
+
if (valTypeOf(args[0]) === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT) return VAL.BIGINT
|
|
283
|
+
return VAL.NUMBER
|
|
284
|
+
}
|
|
285
|
+
if (['**', '++', '--', '~', '>>>', 'u+'].includes(op)) return VAL.NUMBER
|
|
286
|
+
if (op === '+') {
|
|
287
|
+
const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
|
|
288
|
+
if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
|
|
289
|
+
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
290
|
+
return VAL.NUMBER
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (op === '()') {
|
|
294
|
+
const callee = args[0]
|
|
295
|
+
// Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
|
|
296
|
+
if (Array.isArray(callee) && callee[0] === '?') {
|
|
297
|
+
const ta = valTypeOf(callee[2]), tb = valTypeOf(callee[3])
|
|
298
|
+
return ta && ta === tb ? ta : null
|
|
299
|
+
}
|
|
300
|
+
// Constructor results + user function return-type inference
|
|
301
|
+
if (typeof callee === 'string') {
|
|
302
|
+
if (callee === 'new.Set') return VAL.SET
|
|
303
|
+
if (callee === 'new.Map') return VAL.MAP
|
|
304
|
+
if (callee === 'new.ArrayBuffer') return VAL.BUFFER
|
|
305
|
+
if (callee === 'new.DataView') return VAL.BUFFER
|
|
306
|
+
if (callee.startsWith('new.')) return VAL.TYPED
|
|
307
|
+
if (callee === 'String.fromCharCode' || callee === 'String') return VAL.STRING
|
|
308
|
+
if (callee === 'BigInt' || callee === 'BigInt.asIntN' || callee === 'BigInt.asUintN') return VAL.BIGINT
|
|
309
|
+
if (callee === 'JSON.parse') {
|
|
310
|
+
const src = jsonConstString(args[1])
|
|
311
|
+
if (src != null) {
|
|
312
|
+
const c = src.trimStart()[0]
|
|
313
|
+
if (c === '{') return VAL.HASH
|
|
314
|
+
if (c === '[') return VAL.ARRAY
|
|
315
|
+
if (c === '"') return VAL.STRING
|
|
316
|
+
if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Math.* always returns Number — let `+` skip string-concat dispatch and
|
|
320
|
+
// let exprType propagate i32 for the integer-returning subset.
|
|
321
|
+
if (typeof callee === 'string' && callee.startsWith('math.')) return VAL.NUMBER
|
|
322
|
+
// Clock helpers always return Number — lets `t0 = performance.now()` propagate
|
|
323
|
+
// VAL.NUMBER through subsequent reads, eliding `__to_num` wrappers in arithmetic.
|
|
324
|
+
if (callee === 'performance.now' || callee === 'Date.now') return VAL.NUMBER
|
|
325
|
+
const hostVT = ctx.module.hostImportValTypes?.get(callee)
|
|
326
|
+
if (hostVT) return hostVT
|
|
327
|
+
// User-defined func with monomorphic VAL return (populated in compile.js E2 pass).
|
|
328
|
+
const f = ctx.func.map?.get(callee)
|
|
329
|
+
if (f?.valResult) return f.valResult
|
|
330
|
+
}
|
|
331
|
+
// Method return types
|
|
332
|
+
if (Array.isArray(callee) && callee[0] === '.') {
|
|
333
|
+
const [, obj, method] = callee
|
|
334
|
+
if (method === 'map' || method === 'filter') {
|
|
335
|
+
// Typed-array .map/.filter preserve element type → return VAL.TYPED.
|
|
336
|
+
// Unknown receiver: don't claim (stay null) — runtime-dispatched index handles both.
|
|
337
|
+
const objType = valTypeOf(obj)
|
|
338
|
+
if (objType === VAL.TYPED) return VAL.TYPED
|
|
339
|
+
if (objType === VAL.ARRAY) return VAL.ARRAY
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
if (method === 'push') return VAL.ARRAY
|
|
343
|
+
if (method === 'add' || method === 'delete') return VAL.SET
|
|
344
|
+
if (method === 'set') return VAL.MAP
|
|
345
|
+
// String-returning methods
|
|
346
|
+
if (['toUpperCase', 'toLowerCase', 'trim', 'trimStart', 'trimEnd',
|
|
347
|
+
'repeat', 'padStart', 'padEnd', 'replace', 'charAt', 'substring'].includes(method)) return VAL.STRING
|
|
348
|
+
// slice/concat preserve caller type (string.slice → string, array.slice → array)
|
|
349
|
+
if (method === 'slice' || method === 'concat') {
|
|
350
|
+
const objType = valTypeOf(obj)
|
|
351
|
+
if (objType) return objType
|
|
352
|
+
return VAL.ARRAY // default to array when unknown
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function jsonConstString(expr) {
|
|
360
|
+
if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
|
|
361
|
+
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
|
|
362
|
+
if (typeof expr === 'string') return ctx.scope.constStrs?.get(expr) ?? null
|
|
363
|
+
return null
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Build a structural shape tree from a parsed JSON value. Each node is
|
|
367
|
+
* `{ vt, props?, elem? }`. Lets `valTypeOf` propagate VAL kinds through
|
|
368
|
+
* `.prop` chains and `[i]` reads on bindings sourced from `JSON.parse`
|
|
369
|
+
* of a compile-time-known string. Polymorphic arrays drop their `elem`. */
|
|
370
|
+
function shapeOfJsonValue(v) {
|
|
371
|
+
if (v === null || v === undefined) return null
|
|
372
|
+
if (typeof v === 'number') return { vt: VAL.NUMBER }
|
|
373
|
+
if (typeof v === 'string') return { vt: VAL.STRING }
|
|
374
|
+
if (typeof v === 'boolean') return { vt: VAL.NUMBER }
|
|
375
|
+
if (Array.isArray(v)) {
|
|
376
|
+
let elem = null
|
|
377
|
+
for (const x of v) {
|
|
378
|
+
const s = shapeOfJsonValue(x)
|
|
379
|
+
if (!s) { elem = null; break }
|
|
380
|
+
if (!elem) elem = s
|
|
381
|
+
else if (!shapeUnifies(elem, s)) { elem = null; break }
|
|
382
|
+
}
|
|
383
|
+
return { vt: VAL.ARRAY, elem }
|
|
384
|
+
}
|
|
385
|
+
if (typeof v === 'object') {
|
|
386
|
+
const props = Object.create(null)
|
|
387
|
+
for (const k of Object.keys(v)) {
|
|
388
|
+
const s = shapeOfJsonValue(v[k])
|
|
389
|
+
if (s) props[k] = s
|
|
390
|
+
}
|
|
391
|
+
return { vt: VAL.HASH, props }
|
|
392
|
+
}
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function shapeUnifies(a, b) {
|
|
397
|
+
if (!a || !b || a.vt !== b.vt) return false
|
|
398
|
+
if (a.vt === VAL.HASH) {
|
|
399
|
+
const ak = Object.keys(a.props), bk = Object.keys(b.props)
|
|
400
|
+
if (ak.length !== bk.length) return false
|
|
401
|
+
for (const k of ak) {
|
|
402
|
+
if (!b.props[k] || !shapeUnifies(a.props[k], b.props[k])) return false
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (a.vt === VAL.ARRAY) {
|
|
406
|
+
if ((a.elem == null) !== (b.elem == null)) return false
|
|
407
|
+
if (a.elem && !shapeUnifies(a.elem, b.elem)) return false
|
|
408
|
+
}
|
|
409
|
+
return true
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const _jsonShapeCache = new WeakMap()
|
|
413
|
+
function parseJsonShape(src) {
|
|
414
|
+
if (typeof src !== 'string') return null
|
|
415
|
+
if (_jsonShapeCache.has(src)) return _jsonShapeCache.get(src)
|
|
416
|
+
let parsed
|
|
417
|
+
try { parsed = JSON.parse(src) } catch { _jsonShapeCache.set(Object(src), null); return null }
|
|
418
|
+
const sh = shapeOfJsonValue(parsed)
|
|
419
|
+
// WeakMap requires object keys; cache via a wrapper. Skip caching for cold path.
|
|
420
|
+
return sh
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Resolve the json shape for an expression by walking name → rep.jsonShape and
|
|
424
|
+
* `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
|
|
425
|
+
export function shapeOf(expr) {
|
|
426
|
+
if (typeof expr === 'string') return ctx.func.repByLocal?.get(expr)?.jsonShape || null
|
|
427
|
+
if (!Array.isArray(expr)) return null
|
|
428
|
+
const [op, ...args] = expr
|
|
429
|
+
if (op === '()' && args[0] === 'JSON.parse') {
|
|
430
|
+
const src = jsonConstString(args[1])
|
|
431
|
+
if (src != null) return parseJsonShape(src)
|
|
432
|
+
}
|
|
433
|
+
if (op === '.' && typeof args[1] === 'string') {
|
|
434
|
+
const parent = shapeOf(args[0])
|
|
435
|
+
if (parent?.vt === VAL.HASH) return parent.props[args[1]] || null
|
|
436
|
+
}
|
|
437
|
+
if (op === '[]' && args.length === 2) {
|
|
438
|
+
const parent = shapeOf(args[0])
|
|
439
|
+
if (parent?.vt === VAL.ARRAY) return parent.elem || null
|
|
440
|
+
}
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
/** Decode a `['{}', ...]` AST's children into `{names, values}`, or null if any
|
|
446
|
+
* property is non-static-key (computed key, spread, shorthand). Matches the
|
|
447
|
+
* emitter's flatten rule for comma-grouped props. Used by collectProgramFacts,
|
|
448
|
+
* narrowSignatures, and objLiteralSchemaId; the emitter (module/object.js)
|
|
449
|
+
* does its own decoding because it must handle the spread/computed-key paths. */
|
|
450
|
+
export function staticObjectProps(args) {
|
|
451
|
+
const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
452
|
+
const names = [], values = []
|
|
453
|
+
for (const p of raw) {
|
|
454
|
+
if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
|
|
455
|
+
names.push(p[1]); values.push(p[2])
|
|
456
|
+
}
|
|
457
|
+
return names.length ? { names, values } : null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
|
|
461
|
+
function objLiteralSchemaId(expr) {
|
|
462
|
+
if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
|
|
463
|
+
const parsed = staticObjectProps(expr.slice(1))
|
|
464
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Resolve schemaId of an expression, given a per-function schemaId map for locals.
|
|
468
|
+
* Used for both intra-function arr elem-schema observation and func.arrayElemSchema
|
|
469
|
+
* return inference. Recognizes: object literals, var names with bound schemaId,
|
|
470
|
+
* user fn calls with narrowed result schema, ?: / && / || when both branches agree. */
|
|
471
|
+
function exprSchemaId(expr, localSchemaMap) {
|
|
472
|
+
if (typeof expr === 'string') {
|
|
473
|
+
if (localSchemaMap?.has(expr)) return localSchemaMap.get(expr)
|
|
474
|
+
return ctx.schema?.idOf?.(expr) ?? null
|
|
475
|
+
}
|
|
476
|
+
if (!Array.isArray(expr)) return null
|
|
477
|
+
const op = expr[0]
|
|
478
|
+
if (op === '{}') return objLiteralSchemaId(expr)
|
|
479
|
+
if (op === '()' && typeof expr[1] === 'string') {
|
|
480
|
+
const f = ctx.func.map?.get(expr[1])
|
|
481
|
+
if (f?.valResult === VAL.OBJECT && f.sig?.ptrAux != null) return f.sig.ptrAux
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
if (op === '?:') {
|
|
485
|
+
const a = exprSchemaId(expr[2], localSchemaMap)
|
|
486
|
+
const b = exprSchemaId(expr[3], localSchemaMap)
|
|
487
|
+
return a != null && a === b ? a : null
|
|
488
|
+
}
|
|
489
|
+
if (op === '&&' || op === '||') {
|
|
490
|
+
const a = exprSchemaId(expr[1], localSchemaMap)
|
|
491
|
+
const b = exprSchemaId(expr[2], localSchemaMap)
|
|
492
|
+
return a != null && a === b ? a : null
|
|
493
|
+
}
|
|
494
|
+
return null
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Extract typed-array ctor name ('new.Float32Array', 'new.Int8Array.view', etc) from RHS,
|
|
498
|
+
* or null if RHS isn't a typed-array/ArrayBuffer/DataView constructor. */
|
|
499
|
+
export function typedElemCtor(rhs) {
|
|
500
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()' || typeof rhs[1] !== 'string' || !rhs[1].startsWith('new.')) return null
|
|
501
|
+
const args = rhs[2]
|
|
502
|
+
const isView = rhs[1].endsWith('Array') && rhs[1] !== 'new.ArrayBuffer'
|
|
503
|
+
&& Array.isArray(args) && args[0] === ',' && args.length >= 4
|
|
504
|
+
return isView ? rhs[1] + '.view' : rhs[1]
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Element-type byte mapping (mirror of module/typedarray.js ELEM). Bit 3 (|8) marks a view.
|
|
508
|
+
const _ELEM_AUX = {
|
|
509
|
+
Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
|
|
510
|
+
Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
|
|
511
|
+
BigInt64Array: 7, BigUint64Array: 7,
|
|
512
|
+
}
|
|
513
|
+
/** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
|
|
514
|
+
* aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
|
|
515
|
+
export function typedElemAux(ctor) {
|
|
516
|
+
if (!ctor || !ctor.startsWith('new.')) return null
|
|
517
|
+
const isView = ctor.endsWith('.view')
|
|
518
|
+
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
519
|
+
const et = _ELEM_AUX[name]
|
|
520
|
+
if (et == null) return null
|
|
521
|
+
return isView ? et | 8 : et
|
|
522
|
+
}
|
|
523
|
+
const _ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
|
|
524
|
+
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']
|
|
525
|
+
/** Reverse of typedElemAux: pick a canonical ctor string for a 4-bit elem aux. Used
|
|
526
|
+
* to round-trip TYPED-narrowed call results through ctx.types.typedElem so the
|
|
527
|
+
* unboxed local's rep picks up the same aux. aux=7 is shared with BigInt typed
|
|
528
|
+
* arrays — Float64Array is canonical (read-side compares aux only). */
|
|
529
|
+
export function ctorFromElemAux(aux) {
|
|
530
|
+
if (aux == null) return null
|
|
531
|
+
const isView = (aux & 8) !== 0
|
|
532
|
+
const name = _ELEM_NAMES[aux & 7]
|
|
533
|
+
if (!name) return null
|
|
534
|
+
return isView ? `new.${name}.view` : `new.${name}`
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// === Cross-call argument inference helpers (used by narrowSignatures fixpoint) ===
|
|
538
|
+
// Each `inferArg*(expr, ...callerCtx)` resolves an argument expression to a single
|
|
539
|
+
// fact (val/schemaId/elem*/typedCtor) using caller-local observations and program
|
|
540
|
+
// facts, returning null when the fact can't be determined at this call site.
|
|
541
|
+
|
|
542
|
+
/** Infer arg val type using caller's body-local valTypes and module globals. */
|
|
543
|
+
export function inferArgType(expr, callerValTypes) {
|
|
544
|
+
if (typeof expr === 'string') return callerValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
545
|
+
return valTypeOf(expr)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Infer arg schemaId. Sources: caller's per-param schemaId map, module-level
|
|
549
|
+
* ctx.schema.vars binding, or a static-key object literal. */
|
|
550
|
+
export function inferArgSchema(expr, callerSchemas) {
|
|
551
|
+
if (typeof expr === 'string') {
|
|
552
|
+
if (callerSchemas?.has(expr)) return callerSchemas.get(expr)
|
|
553
|
+
const id = ctx.schema.vars.get(expr)
|
|
554
|
+
return id != null ? id : null
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(expr) && expr[0] === '{}') {
|
|
557
|
+
const parsed = staticObjectProps(expr.slice(1))
|
|
558
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
559
|
+
}
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Infer arg arr-elem-schema. Sources: caller's body-local arr-elem map, caller's
|
|
564
|
+
* per-param arr-elem (transitive), or a call to an arr-narrowed user fn. */
|
|
565
|
+
export function inferArgArrElemSchema(expr, callerArrElems, callerArrParams) {
|
|
566
|
+
if (typeof expr === 'string') {
|
|
567
|
+
if (callerArrElems?.has(expr)) {
|
|
568
|
+
const v = callerArrElems.get(expr)
|
|
569
|
+
if (v != null) return v
|
|
570
|
+
}
|
|
571
|
+
if (callerArrParams?.has(expr)) {
|
|
572
|
+
const v = callerArrParams.get(expr)
|
|
573
|
+
if (v != null) return v
|
|
574
|
+
}
|
|
575
|
+
return null
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
578
|
+
const f = ctx.func.map?.get(expr[1])
|
|
579
|
+
if (f?.arrayElemSchema != null) return f.arrayElemSchema
|
|
580
|
+
}
|
|
581
|
+
return null
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Infer arg arr-elem-VAL. Mirrors inferArgArrElemSchema but tracks VAL.* element kind. */
|
|
585
|
+
export function inferArgArrElemValType(expr, callerArrElemVals, callerArrValParams) {
|
|
586
|
+
if (typeof expr === 'string') {
|
|
587
|
+
if (callerArrElemVals?.has(expr)) {
|
|
588
|
+
const v = callerArrElemVals.get(expr)
|
|
589
|
+
if (v != null) return v
|
|
590
|
+
}
|
|
591
|
+
if (callerArrValParams?.has(expr)) {
|
|
592
|
+
const v = callerArrValParams.get(expr)
|
|
593
|
+
if (v != null) return v
|
|
594
|
+
}
|
|
595
|
+
return null
|
|
596
|
+
}
|
|
597
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
598
|
+
const f = ctx.func.map?.get(expr[1])
|
|
599
|
+
if (f?.arrayElemValType != null) return f.arrayElemValType
|
|
600
|
+
}
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Infer typed-array ctor (`new.Float64Array` etc.) of an arg expression at a call site.
|
|
605
|
+
* Sources: caller's body-local typedElems, caller's typed params, literal `new TypedArray(...)`,
|
|
606
|
+
* calls to typed-narrowed user funcs. Returns null when the ctor can't be determined. */
|
|
607
|
+
export function inferArgTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
608
|
+
if (typeof expr === 'string') {
|
|
609
|
+
if (callerTypedElems?.has(expr)) return callerTypedElems.get(expr)
|
|
610
|
+
if (callerTypedParams?.has(expr)) return callerTypedParams.get(expr)
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
613
|
+
const ctor = typedElemCtor(expr)
|
|
614
|
+
if (ctor) return ctor
|
|
615
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
616
|
+
const f = ctx.func.map?.get(expr[1])
|
|
617
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) return ctorFromElemAux(f.sig.ptrAux)
|
|
618
|
+
}
|
|
619
|
+
return null
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Per-body memoization: analyzeBody is a pure function of `body` plus a small
|
|
623
|
+
// set of ctx fields (func.locals, func.repByLocal, func.map[*][field]). compile.js
|
|
624
|
+
// calls slices of it many times per function (scan-fixpoint, narrowing, final
|
|
625
|
+
// lowering); the unified cache absorbs that traffic. Caller-mutation safety is
|
|
626
|
+
// preserved by cloning every Map on read (entry value stored once, copies handed out).
|
|
627
|
+
// Invalidation: emitFunc calls `invalidateLocalsCache` after seeding cross-call
|
|
628
|
+
// param facts; compile.js' E2 pass calls `invalidateValTypesCache` after valResult
|
|
629
|
+
// narrowing; narrowReturnArrayElems clears entries between fixpoint iters.
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Unified per-body analysis. Single AST traversal producing every per-binding
|
|
633
|
+
* fact the emitter needs:
|
|
634
|
+
*
|
|
635
|
+
* {
|
|
636
|
+
* locals: Map<name, 'i32'|'f64'> // wasm type per local
|
|
637
|
+
* valTypes: Map<name, VAL.*> // value-type for dispatch
|
|
638
|
+
* arrElemSchemas: Map<name, schemaId|null> // Array<schema> facts
|
|
639
|
+
* arrElemValTypes: Map<name, VAL.*|null> // Array<val-kind> facts
|
|
640
|
+
* typedElems: Map<name, ctorString> // typed-array ctor binding
|
|
641
|
+
* }
|
|
642
|
+
*
|
|
643
|
+
* Recursion shape: after a `let`/`const` decl, the rhs is walked but the `=`
|
|
644
|
+
* node itself is skipped — arrElemSchemas/ValTypes have a reassignment
|
|
645
|
+
* invalidation rule that would misfire on init. Other slices' `=`-visit is
|
|
646
|
+
* idempotent with the decl handler, so skipping it is safe for them too.
|
|
647
|
+
*
|
|
648
|
+
* Forward-only observation order: every rule reads only state already produced
|
|
649
|
+
* earlier in the same walk (alias chains, push observations, etc.), so a single
|
|
650
|
+
* traversal is sound.
|
|
651
|
+
*
|
|
652
|
+
* After the walk a `widenPass` runs to widen `i32` locals compared against `f64`
|
|
653
|
+
* operands.
|
|
654
|
+
*
|
|
655
|
+
* Caching: body-keyed via `_bodyFactsCache`. See
|
|
656
|
+
* `invalidateLocalsCache` / `invalidateValTypesCache` for the invalidation hooks.
|
|
657
|
+
*/
|
|
658
|
+
const _bodyFactsCache = new WeakMap()
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Returns the cached facts object directly — DO NOT MUTATE the returned maps.
|
|
662
|
+
* Callers that need to extend (e.g. add params to locals) must clone explicitly.
|
|
663
|
+
* `analyzeLocals` is the canonical clone-then-extend facade; everywhere else
|
|
664
|
+
* reads slices via `analyzeBody(body).<slice>`.
|
|
665
|
+
*/
|
|
666
|
+
export function analyzeBody(body) {
|
|
667
|
+
// Non-object bodies (`() => 0`, `() => x`, missing) have nothing to observe
|
|
668
|
+
// for any slice and can't be WeakMap-keyed. Return empty maps without caching.
|
|
669
|
+
if (body === null || typeof body !== 'object') return {
|
|
670
|
+
locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
|
|
671
|
+
arrElemValTypes: new Map(), typedElems: new Map(),
|
|
672
|
+
}
|
|
673
|
+
const hit = _bodyFactsCache.get(body)
|
|
674
|
+
if (hit) return hit
|
|
675
|
+
|
|
676
|
+
const locals = new Map()
|
|
677
|
+
const valTypes = new Map()
|
|
678
|
+
const arrElemSchemas = new Map()
|
|
679
|
+
const arrElemValTypes = new Map()
|
|
680
|
+
const typedElems = new Map()
|
|
681
|
+
|
|
682
|
+
const doSchemas = !!ctx.schema?.register
|
|
683
|
+
// Per-walk local schema map for chained `arr.push(name)` resolution.
|
|
684
|
+
const localSchemaMap = new Map()
|
|
685
|
+
|
|
686
|
+
// === Observation helpers ===
|
|
687
|
+
//
|
|
688
|
+
// These trust the AST: any `arr.push(...)` syntactically present has `arr` as
|
|
689
|
+
// a body-relevant name (decl, param, or global) since closure boundaries are
|
|
690
|
+
// skipped at walk time. Pure typo names produce harmless dead Map entries
|
|
691
|
+
// that are never queried (consumers index by known local/param names).
|
|
692
|
+
// Removing the legacy `ctx.func.locals.has(arr)` filter makes analyzeBody's
|
|
693
|
+
// output context-pure — cache hits don't depend on transient ctx state.
|
|
694
|
+
|
|
695
|
+
const observeArrSchema = (arr, sid) => {
|
|
696
|
+
if (!doSchemas) return
|
|
697
|
+
if (typeof arr !== 'string') return
|
|
698
|
+
if (arrElemSchemas.get(arr) === null) return
|
|
699
|
+
if (sid == null) { arrElemSchemas.set(arr, null); return }
|
|
700
|
+
if (!arrElemSchemas.has(arr)) arrElemSchemas.set(arr, sid)
|
|
701
|
+
else if (arrElemSchemas.get(arr) !== sid) arrElemSchemas.set(arr, null)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const observeArrValType = (arr, vt) => {
|
|
705
|
+
if (typeof arr !== 'string') return
|
|
706
|
+
if (arrElemValTypes.get(arr) === null) return
|
|
707
|
+
if (!vt) { arrElemValTypes.set(arr, null); return }
|
|
708
|
+
if (!arrElemValTypes.has(arr)) arrElemValTypes.set(arr, vt)
|
|
709
|
+
else if (arrElemValTypes.get(arr) !== vt) arrElemValTypes.set(arr, null)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const elemValOf = (name) => {
|
|
713
|
+
if (typeof name !== 'string') return null
|
|
714
|
+
const repVt = ctx.func.repByLocal?.get(name)?.arrayElemValType
|
|
715
|
+
if (repVt) return repVt
|
|
716
|
+
return arrElemValTypes.get(name) || null
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const exprElemSourceVal = (expr) => {
|
|
720
|
+
if (typeof expr === 'string') {
|
|
721
|
+
const repVt = ctx.func.repByLocal?.get(expr)?.val
|
|
722
|
+
if (repVt) return repVt
|
|
723
|
+
return ctx.scope.globalValTypes?.get(expr) || null
|
|
724
|
+
}
|
|
725
|
+
return valTypeOf(expr)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const trackTyped = (name, rhs) => {
|
|
729
|
+
const ctor = typedElemCtor(rhs)
|
|
730
|
+
if (ctor) { typedElems.set(name, ctor); return }
|
|
731
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
732
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
733
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
734
|
+
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
735
|
+
if (c) typedElems.set(name, c)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
|
|
741
|
+
const processDecl = (name, rhs) => {
|
|
742
|
+
// wasm type (locals slice)
|
|
743
|
+
const wt = exprType(rhs, locals)
|
|
744
|
+
if (!locals.has(name)) locals.set(name, wt)
|
|
745
|
+
else if (locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
746
|
+
|
|
747
|
+
// val type (valTypes slice)
|
|
748
|
+
const vt = valTypeOf(rhs)
|
|
749
|
+
if (vt) valTypes.set(name, vt); else valTypes.delete(name)
|
|
750
|
+
|
|
751
|
+
// typed-array element ctor (typedElems slice)
|
|
752
|
+
trackTyped(name, rhs)
|
|
753
|
+
|
|
754
|
+
// arr-elem schema (arrElemSchemas slice) — schema bindings + array-literal init + alias + call return
|
|
755
|
+
if (doSchemas) {
|
|
756
|
+
const sid = exprSchemaId(rhs, localSchemaMap)
|
|
757
|
+
if (sid != null) localSchemaMap.set(name, sid)
|
|
758
|
+
if (Array.isArray(rhs) && rhs[0] === '[]') {
|
|
759
|
+
const elems = rhs.slice(1).filter(e => e != null)
|
|
760
|
+
if (elems.length) {
|
|
761
|
+
let common = exprSchemaId(elems[0], localSchemaMap)
|
|
762
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
763
|
+
if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
|
|
764
|
+
}
|
|
765
|
+
if (common != null) observeArrSchema(name, common)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
769
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
770
|
+
if (f?.arrayElemSchema != null) observeArrSchema(name, f.arrayElemSchema)
|
|
771
|
+
}
|
|
772
|
+
if (typeof rhs === 'string' && arrElemSchemas.has(rhs)) {
|
|
773
|
+
const sid2 = arrElemSchemas.get(rhs)
|
|
774
|
+
if (sid2 != null) observeArrSchema(name, sid2)
|
|
775
|
+
}
|
|
776
|
+
if (typeof rhs === 'string') {
|
|
777
|
+
const repSid = ctx.func.repByLocal?.get(rhs)?.arrayElemSchema
|
|
778
|
+
if (repSid != null) observeArrSchema(name, repSid)
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// arr-elem val type (arrElemValTypes slice) — array-literal init + call return + alias + .map/.filter/.slice/.concat chain
|
|
783
|
+
if (Array.isArray(rhs) && rhs[0] === '[]') {
|
|
784
|
+
const elems = rhs.slice(1).filter(e => e != null)
|
|
785
|
+
if (elems.length) {
|
|
786
|
+
let common = exprElemSourceVal(elems[0])
|
|
787
|
+
for (let k = 1; k < elems.length && common != null; k++) {
|
|
788
|
+
if (exprElemSourceVal(elems[k]) !== common) common = null
|
|
789
|
+
}
|
|
790
|
+
if (common != null) observeArrValType(name, common)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
794
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
795
|
+
if (f?.arrayElemValType) observeArrValType(name, f.arrayElemValType)
|
|
796
|
+
}
|
|
797
|
+
if (typeof rhs === 'string') {
|
|
798
|
+
const v = elemValOf(rhs)
|
|
799
|
+
if (v) observeArrValType(name, v)
|
|
800
|
+
}
|
|
801
|
+
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
802
|
+
Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
803
|
+
typeof rhs[1][1] === 'string') {
|
|
804
|
+
const recvName = rhs[1][1], method = rhs[1][2]
|
|
805
|
+
if (method === 'filter' || method === 'slice' || method === 'concat') {
|
|
806
|
+
const v = elemValOf(recvName)
|
|
807
|
+
if (v) observeArrValType(name, v)
|
|
808
|
+
} else if (method === 'map') {
|
|
809
|
+
const arrowFn = rhs[2]
|
|
810
|
+
const recvVt = elemValOf(recvName)
|
|
811
|
+
const param = Array.isArray(arrowFn) && arrowFn[0] === '=>' ? arrowFn[1] : null
|
|
812
|
+
const paramName = typeof param === 'string' ? param :
|
|
813
|
+
(Array.isArray(param) && param[0] === '()' && typeof param[1] === 'string' ? param[1] : null)
|
|
814
|
+
const arrowBody = paramName ? arrowFn[2] : null
|
|
815
|
+
const exprBody = (Array.isArray(arrowBody) && arrowBody[0] === '{}' &&
|
|
816
|
+
Array.isArray(arrowBody[1]) && arrowBody[1][0] === 'return') ? arrowBody[1][1] : arrowBody
|
|
817
|
+
if (paramName && exprBody != null) {
|
|
818
|
+
const refs = ctx.func.refinements
|
|
819
|
+
const hadParam = refs?.has(paramName)
|
|
820
|
+
const prev = hadParam ? refs.get(paramName) : undefined
|
|
821
|
+
if (refs && recvVt) refs.set(paramName, recvVt)
|
|
822
|
+
let bodyVt = null
|
|
823
|
+
try { bodyVt = valTypeOf(exprBody) }
|
|
824
|
+
finally {
|
|
825
|
+
if (refs && recvVt) {
|
|
826
|
+
if (hadParam) refs.set(paramName, prev); else refs.delete(paramName)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (bodyVt) observeArrValType(name, bodyVt)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// arrElem invalidation rule — fires on `=` reassign of tracked name to non-array
|
|
836
|
+
const isArrayProducingRhs = (rhs) =>
|
|
837
|
+
Array.isArray(rhs) && (rhs[0] === '[]' ||
|
|
838
|
+
(rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
839
|
+
(rhs[1][2] === 'slice' || rhs[1][2] === 'concat')))
|
|
840
|
+
|
|
841
|
+
// === Single walk ===
|
|
842
|
+
function walk(node) {
|
|
843
|
+
if (!Array.isArray(node)) return
|
|
844
|
+
const op = node[0]
|
|
845
|
+
if (op === '=>') return // don't cross closure boundary
|
|
846
|
+
|
|
847
|
+
if (op === 'let' || op === 'const') {
|
|
848
|
+
for (let i = 1; i < node.length; i++) {
|
|
849
|
+
const a = node[i]
|
|
850
|
+
// analyzeLocals: bare-name decl
|
|
851
|
+
if (typeof a === 'string') { if (!locals.has(a)) locals.set(a, 'f64'); continue }
|
|
852
|
+
if (!Array.isArray(a) || a[0] !== '=') continue
|
|
853
|
+
// analyzeLocals: destructuring decl — set destructured names to f64, walk rhs only
|
|
854
|
+
if (typeof a[1] !== 'string') {
|
|
855
|
+
for (const n of collectParamNames([a[1]])) if (!locals.has(n)) locals.set(n, 'f64')
|
|
856
|
+
walk(a[2])
|
|
857
|
+
continue
|
|
858
|
+
}
|
|
859
|
+
const name = a[1], rhs = a[2]
|
|
860
|
+
processDecl(name, rhs)
|
|
861
|
+
// Walk rhs only — never enter the `=` node so the reassignment-invalidation
|
|
862
|
+
// rule won't misfire on the binding's own initializer.
|
|
863
|
+
walk(rhs)
|
|
864
|
+
}
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// arr.push(...) — observe both schemas and val types in one pass
|
|
869
|
+
if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && node[1][2] === 'push' && typeof node[1][1] === 'string') {
|
|
870
|
+
const arr = node[1][1]
|
|
871
|
+
const callArgs = node[2]
|
|
872
|
+
const list = callArgs == null ? [] :
|
|
873
|
+
(Array.isArray(callArgs) && callArgs[0] === ',') ? callArgs.slice(1) : [callArgs]
|
|
874
|
+
for (const a of list) {
|
|
875
|
+
if (Array.isArray(a) && a[0] === '...') {
|
|
876
|
+
observeArrSchema(arr, null); observeArrValType(arr, null); continue
|
|
877
|
+
}
|
|
878
|
+
observeArrSchema(arr, exprSchemaId(a, localSchemaMap))
|
|
879
|
+
observeArrValType(arr, exprElemSourceVal(a))
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// `=` reassignment — locals widen, valTypes/typedElems track,
|
|
884
|
+
// arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
|
|
885
|
+
if (op === '=' && typeof node[1] === 'string') {
|
|
886
|
+
const name = node[1], rhs = node[2]
|
|
887
|
+
const wt = exprType(rhs, locals)
|
|
888
|
+
if (locals.has(name) && locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
889
|
+
const vt = valTypeOf(rhs)
|
|
890
|
+
if (vt) valTypes.set(name, vt); else valTypes.delete(name)
|
|
891
|
+
trackTyped(name, rhs)
|
|
892
|
+
if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
|
|
893
|
+
if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// compound-assign widening (locals slice)
|
|
897
|
+
if ((op === '+=' || op === '-=' || op === '*=' || op === '%=') && typeof node[1] === 'string') {
|
|
898
|
+
const name = node[1], opChar = op[0]
|
|
899
|
+
const t = exprType([opChar, node[1], node[2]], locals)
|
|
900
|
+
if (locals.has(name) && locals.get(name) === 'i32' && t === 'f64') locals.set(name, 'f64')
|
|
901
|
+
}
|
|
902
|
+
if (op === '/=' && typeof node[1] === 'string') {
|
|
903
|
+
if (locals.has(node[1])) locals.set(node[1], 'f64')
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Install the in-progress valTypes as a lookup overlay so successive decls
|
|
910
|
+
// resolve chains (`const a = new TypedArr(); const b = a[0]` → b: NUMBER)
|
|
911
|
+
// and shorthand-bound `{a}` props see a's type. Restored after walk completes.
|
|
912
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
913
|
+
ctx.func.localValTypesOverlay = valTypes
|
|
914
|
+
try { walk(body) } finally { ctx.func.localValTypesOverlay = prevOverlay }
|
|
915
|
+
|
|
916
|
+
// Second pass: widen i32 locals compared against f64.
|
|
917
|
+
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
918
|
+
function widenPass(node) {
|
|
919
|
+
if (!Array.isArray(node)) return
|
|
920
|
+
const [op, ...args] = node
|
|
921
|
+
if (CMP_OPS.has(op)) {
|
|
922
|
+
const [a, b] = args
|
|
923
|
+
const ta = exprType(a, locals), tb = exprType(b, locals)
|
|
924
|
+
if (ta === 'i32' && tb === 'f64' && typeof a === 'string' && locals.has(a)) locals.set(a, 'f64')
|
|
925
|
+
if (tb === 'i32' && ta === 'f64' && typeof b === 'string' && locals.has(b)) locals.set(b, 'f64')
|
|
926
|
+
}
|
|
927
|
+
if (op !== '=>') for (const a of args) widenPass(a)
|
|
928
|
+
}
|
|
929
|
+
widenPass(body)
|
|
930
|
+
|
|
931
|
+
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems }
|
|
932
|
+
_bodyFactsCache.set(body, result)
|
|
933
|
+
return result
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Drop the cached analyzeBody entry for this body. Used by emitFunc after
|
|
937
|
+
* seeding cross-call param VAL facts so the next walk picks up fresh
|
|
938
|
+
* `ctx.func.repByLocal` (drives exprType receiver-type lookups).
|
|
939
|
+
* Same hook as `invalidateValTypesCache` — split names preserve caller intent. */
|
|
940
|
+
export function invalidateLocalsCache(body) {
|
|
941
|
+
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Drop the cached analyzeBody entry. Used after E2-phase valResult narrowing
|
|
945
|
+
* so the next walk re-evaluates `valTypeOf(call)` with up-to-date `f.valResult`
|
|
946
|
+
* — required for the D-pass paramReps val/arrayElemSchema re-fixpoint to see
|
|
947
|
+
* `const rows = initRows()` as VAL.ARRAY (initRows.valResult set by E2). */
|
|
948
|
+
export function invalidateValTypesCache(body) {
|
|
949
|
+
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Analyze all local value types from declarations and assignments.
|
|
954
|
+
* Writes the per-name `val` field of `ctx.func.repByLocal` for method dispatch
|
|
955
|
+
* and schema resolution.
|
|
956
|
+
*/
|
|
957
|
+
export function analyzeValTypes(body) {
|
|
958
|
+
const setVal = (name, vt) => updateRep(name, { val: vt || undefined })
|
|
959
|
+
const getVal = name => ctx.func.repByLocal?.get(name)?.val
|
|
960
|
+
// Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
|
|
961
|
+
// on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
|
|
962
|
+
// Parallel arrElemValTypes walk records VAL.* element kinds into
|
|
963
|
+
// rep.arrayElemValType so valTypeOf's `arr[i]` rule can elide __to_num and route
|
|
964
|
+
// method dispatch on `arr[i].method()`. Both come from a single unified walk.
|
|
965
|
+
const facts = analyzeBody(body)
|
|
966
|
+
const arrElems = facts.arrElemSchemas
|
|
967
|
+
for (const [name, vt] of facts.arrElemValTypes) {
|
|
968
|
+
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
969
|
+
}
|
|
970
|
+
// Resolve a name's array-elem-schema, preferring rep.arrayElemSchema (set from
|
|
971
|
+
// paramReps[k].arrayElemSchema at emit start) over local body observations.
|
|
972
|
+
const arrElemSchemaOf = (name) => {
|
|
973
|
+
if (typeof name !== 'string') return null
|
|
974
|
+
const repSid = ctx.func.repByLocal?.get(name)?.arrayElemSchema
|
|
975
|
+
if (repSid != null) return repSid
|
|
976
|
+
const localSid = arrElems.get(name)
|
|
977
|
+
return localSid != null ? localSid : null
|
|
978
|
+
}
|
|
979
|
+
function trackRegex(name, rhs) {
|
|
980
|
+
if (ctx.runtime.regex && Array.isArray(rhs) && rhs[0] === '//') ctx.runtime.regex.vars.set(name, rhs)
|
|
981
|
+
}
|
|
982
|
+
function trackTyped(name, rhs) {
|
|
983
|
+
if (!ctx.types.typedElem) ctx.types.typedElem = new Map() // first use in this function scope
|
|
984
|
+
const ctor = typedElemCtor(rhs)
|
|
985
|
+
if (ctor) { ctx.types.typedElem.set(name, ctor); return }
|
|
986
|
+
// TYPED-narrowed call result carries elem aux on f.sig.ptrAux — reverse-map it
|
|
987
|
+
// back to a canonical ctor string so analyzePtrUnboxable's typedElemAux lookup
|
|
988
|
+
// (compile.js) restores the same aux on the unboxed local's rep.
|
|
989
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
990
|
+
const f = ctx.func.map?.get(rhs[1])
|
|
991
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
992
|
+
const ctor = ctorFromElemAux(f.sig.ptrAux)
|
|
993
|
+
if (ctor) ctx.types.typedElem.set(name, ctor)
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
function walk(node) {
|
|
998
|
+
if (!Array.isArray(node)) return
|
|
999
|
+
const [op, ...args] = node
|
|
1000
|
+
if (op === '=>') return // don't leak inner-closure val types
|
|
1001
|
+
// Propagate typed array type through method calls (e.g. buf.map → typed)
|
|
1002
|
+
function propagateTyped(name, rhs) {
|
|
1003
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()') return
|
|
1004
|
+
const callee = rhs[1]
|
|
1005
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return
|
|
1006
|
+
const src = callee[1], method = callee[2]
|
|
1007
|
+
if (typeof src === 'string' && getVal(src) === VAL.TYPED && method === 'map') {
|
|
1008
|
+
setVal(name, VAL.TYPED)
|
|
1009
|
+
if (ctx.types.typedElem?.has(src)) {
|
|
1010
|
+
const srcCtor = ctx.types.typedElem.get(src)
|
|
1011
|
+
ctx.types.typedElem.set(name, srcCtor.endsWith('.view') ? srcCtor.slice(0, -5) : srcCtor)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (op === 'let' || op === 'const') {
|
|
1016
|
+
for (const a of args) {
|
|
1017
|
+
if (!Array.isArray(a) || a[0] !== '=' || typeof a[1] !== 'string') continue
|
|
1018
|
+
const vt = valTypeOf(a[2])
|
|
1019
|
+
setVal(a[1], vt)
|
|
1020
|
+
if (vt === VAL.REGEX) trackRegex(a[1], a[2])
|
|
1021
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER) trackTyped(a[1], a[2])
|
|
1022
|
+
propagateTyped(a[1], a[2])
|
|
1023
|
+
// JSON-shape propagation. When the RHS resolves to a known JSON shape
|
|
1024
|
+
// (root: `JSON.parse(literal)`; nested: `o.meta`, `items[j]` from a known
|
|
1025
|
+
// root), record it on the binding so subsequent `.prop`/`[i]` accesses
|
|
1026
|
+
// skip dynamic dispatch and propagate VAL kinds. Generic for any
|
|
1027
|
+
// compile-time JSON literal.
|
|
1028
|
+
const sh = shapeOf(a[2])
|
|
1029
|
+
if (sh) {
|
|
1030
|
+
updateRep(a[1], { jsonShape: sh })
|
|
1031
|
+
if (sh.vt === VAL.ARRAY && sh.elem?.vt) {
|
|
1032
|
+
updateRep(a[1], { arrayElemValType: sh.elem.vt })
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Propagate schemaId from a narrowed call result so subsequent valTypeOf
|
|
1036
|
+
// calls in this function body see the precise schema. emitDecl rebinds
|
|
1037
|
+
// this at emission time too — analyze-time binding is what unlocks the
|
|
1038
|
+
// slotVT lookup chain in `analyzeValTypes`'s own walk + per-func emit
|
|
1039
|
+
// dispatch reading repByLocal.
|
|
1040
|
+
if (vt === VAL.OBJECT && Array.isArray(a[2]) && a[2][0] === '()' && typeof a[2][1] === 'string') {
|
|
1041
|
+
const f = ctx.func.map?.get(a[2][1])
|
|
1042
|
+
if (f?.sig?.ptrAux != null) updateRep(a[1], { schemaId: f.sig.ptrAux })
|
|
1043
|
+
}
|
|
1044
|
+
// `const p = arr[i]` — when arr's element schema is known (from .push observations
|
|
1045
|
+
// or from paramReps arrayElemSchema binding), p inherits the schema. Unlocks slotVT-driven
|
|
1046
|
+
// numeric typing on `.prop` reads + slot-direct loads.
|
|
1047
|
+
if (Array.isArray(a[2]) && a[2][0] === '[]' && typeof a[2][1] === 'string') {
|
|
1048
|
+
const elemSid = arrElemSchemaOf(a[2][1])
|
|
1049
|
+
if (elemSid != null) {
|
|
1050
|
+
updateRep(a[1], { schemaId: elemSid })
|
|
1051
|
+
// Also set the val so structural call dispatch + valTypeOf see VAL.OBJECT.
|
|
1052
|
+
setVal(a[1], VAL.OBJECT)
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (op === '=' && typeof args[0] === 'string') {
|
|
1058
|
+
const vt = valTypeOf(args[1])
|
|
1059
|
+
setVal(args[0], vt)
|
|
1060
|
+
if (vt === VAL.REGEX) trackRegex(args[0], args[1])
|
|
1061
|
+
if (vt === VAL.TYPED || vt === VAL.BUFFER) trackTyped(args[0], args[1])
|
|
1062
|
+
propagateTyped(args[0], args[1])
|
|
1063
|
+
}
|
|
1064
|
+
// Track property assignments for auto-boxing: x.prop = val
|
|
1065
|
+
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '.' && typeof args[0][1] === 'string') {
|
|
1066
|
+
const [, obj, prop] = args[0]
|
|
1067
|
+
const vt = getVal(obj)
|
|
1068
|
+
if ((vt === VAL.NUMBER || vt === VAL.BIGINT) && ctx.func.locals?.has(obj) && ctx.schema.register) {
|
|
1069
|
+
if (!ctx.func.localProps) ctx.func.localProps = new Map()
|
|
1070
|
+
if (!ctx.func.localProps.has(obj)) ctx.func.localProps.set(obj, new Set())
|
|
1071
|
+
ctx.func.localProps.get(obj).add(prop)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const a of args) walk(a)
|
|
1075
|
+
}
|
|
1076
|
+
walk(body)
|
|
1077
|
+
|
|
1078
|
+
// Register boxed schemas for local variables with property assignments
|
|
1079
|
+
if (ctx.func.localProps) {
|
|
1080
|
+
for (const [name, props] of ctx.func.localProps) {
|
|
1081
|
+
if (ctx.schema.vars.has(name)) continue
|
|
1082
|
+
const schema = ['__inner__', ...props]
|
|
1083
|
+
const sid = ctx.schema.register(schema)
|
|
1084
|
+
ctx.schema.vars.set(name, sid)
|
|
1085
|
+
updateRep(name, { schemaId: sid })
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
|
|
1091
|
+
const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
|
|
1092
|
+
const INT_CLOSED_OPS = new Set(['+', '-', '*', '%'])
|
|
1093
|
+
const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Forward-propagate `intCertain` across local bindings (S2 Stage 4a — pure analysis).
|
|
1097
|
+
*
|
|
1098
|
+
* A binding is `intCertain` iff every defining RHS evaluates to an integer-valued
|
|
1099
|
+
* expression. Reassignments widen — any non-int RHS poisons the binding, regardless
|
|
1100
|
+
* of order in source. Multi-pass fixpoint converges when RHSs read other bindings
|
|
1101
|
+
* transitively (`let j = i + 1` resolves only after `i` is known intCertain).
|
|
1102
|
+
*
|
|
1103
|
+
* Integer-shaped RHS (closed under composition):
|
|
1104
|
+
* - integer Number literal, boolean literal
|
|
1105
|
+
* - bitwise ops `& | ^ ~ << >> >>>` — i32 result by spec
|
|
1106
|
+
* - comparisons `< > <= >= == != === !== !` — 0/1 result
|
|
1107
|
+
* - `.length` / `.byteLength` on TYPED/ARRAY/STRING/BUFFER receiver
|
|
1108
|
+
* - `+ - * %` and unary `+ -` of intCertain operands (overflow OK — value is mathematically integer)
|
|
1109
|
+
* - `?: && ||` when both branches are intCertain
|
|
1110
|
+
* - `Math.{imul, clz32, floor, ceil, round, trunc}`
|
|
1111
|
+
* - self-mutation ops `++` `--` `+=` `-=` `*=` `%=` (preserve when operand is int);
|
|
1112
|
+
* `&= |= ^= <<= >>= >>>=` (always int by op result type);
|
|
1113
|
+
* `/=` `**=` poison.
|
|
1114
|
+
*
|
|
1115
|
+
* Writes `intCertain: true` on `ctx.func.repByLocal[name]`. No emit impact —
|
|
1116
|
+
* codegen extensions consume this in follow-up passes.
|
|
1117
|
+
*/
|
|
1118
|
+
export function analyzeIntCertain(body) {
|
|
1119
|
+
// Pass 1: collect every defining RHS per binding name. Compound assignments
|
|
1120
|
+
// are desugared to their `=` equivalent (`x += y` → `x = x + y`) so the
|
|
1121
|
+
// existing `isIntExpr` op rules apply uniformly.
|
|
1122
|
+
const defs = new Map()
|
|
1123
|
+
const pushDef = (name, rhs) => {
|
|
1124
|
+
let list = defs.get(name)
|
|
1125
|
+
if (!list) { list = []; defs.set(name, list) }
|
|
1126
|
+
list.push(rhs)
|
|
1127
|
+
}
|
|
1128
|
+
const collect = (node) => {
|
|
1129
|
+
if (!Array.isArray(node)) return
|
|
1130
|
+
const [op, ...args] = node
|
|
1131
|
+
if (op === '=>') return
|
|
1132
|
+
if (op === 'let' || op === 'const') {
|
|
1133
|
+
for (const a of args) {
|
|
1134
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
|
|
1135
|
+
}
|
|
1136
|
+
} else if (op === '=' && typeof args[0] === 'string') {
|
|
1137
|
+
pushDef(args[0], args[1])
|
|
1138
|
+
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
1139
|
+
!INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
1140
|
+
// Compound assign: desugar `x <op>= rhs` → `x = x <op> rhs`. The base op
|
|
1141
|
+
// result is fed back through isIntExpr — bitwise compounds become int by
|
|
1142
|
+
// the bitwise rule; +=/-=/*=/%= preserve via int-closed rule.
|
|
1143
|
+
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
1144
|
+
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
1145
|
+
// `x++` / `x--` desugars to `x = x ± 1`. 1 is int → preserves intCertain.
|
|
1146
|
+
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
1147
|
+
}
|
|
1148
|
+
for (const a of args) collect(a)
|
|
1149
|
+
}
|
|
1150
|
+
collect(body)
|
|
1151
|
+
if (defs.size === 0) return
|
|
1152
|
+
|
|
1153
|
+
// Pass 2: monotone-down fixpoint. Start optimistic (every defined binding
|
|
1154
|
+
// assumed intCertain), then for each iteration mark false any binding whose
|
|
1155
|
+
// RHS list contains a non-int expression. Once false, stays false — defs is
|
|
1156
|
+
// fixed and isIntExpr only reads back through bindings that themselves can
|
|
1157
|
+
// only flip true→false. Converges when no further bindings flip.
|
|
1158
|
+
//
|
|
1159
|
+
// (Naive bottom-up `false→true` direction is unsound for recursive bindings
|
|
1160
|
+
// like `let i = 0; i = i + 1` — first iteration sees i unobserved → false →
|
|
1161
|
+
// i+1 false → i stays false, missing the fact that all RHSs are int.)
|
|
1162
|
+
const intCertain = new Map()
|
|
1163
|
+
for (const name of defs.keys()) intCertain.set(name, true)
|
|
1164
|
+
|
|
1165
|
+
const isIntExpr = (expr) => {
|
|
1166
|
+
if (typeof expr === 'number') return Number.isInteger(expr)
|
|
1167
|
+
if (typeof expr === 'boolean') return true
|
|
1168
|
+
if (typeof expr === 'string') return intCertain.get(expr) === true
|
|
1169
|
+
if (!Array.isArray(expr)) return false
|
|
1170
|
+
const [op, ...args] = expr
|
|
1171
|
+
if (op == null) {
|
|
1172
|
+
// `[, value]` / `[null, value]` literal form
|
|
1173
|
+
const v = args[0]
|
|
1174
|
+
if (typeof v === 'number') return Number.isInteger(v)
|
|
1175
|
+
if (typeof v === 'boolean') return true
|
|
1176
|
+
return false
|
|
1177
|
+
}
|
|
1178
|
+
if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
|
|
1179
|
+
if (op === '.') {
|
|
1180
|
+
if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
|
|
1181
|
+
const vt = lookupValType(args[0])
|
|
1182
|
+
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
1183
|
+
}
|
|
1184
|
+
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
1185
|
+
const vt = lookupValType(args[0])
|
|
1186
|
+
return vt === VAL.SET || vt === VAL.MAP
|
|
1187
|
+
}
|
|
1188
|
+
return false
|
|
1189
|
+
}
|
|
1190
|
+
if (INT_CLOSED_OPS.has(op)) {
|
|
1191
|
+
const a = isIntExpr(args[0])
|
|
1192
|
+
const b = args[1] != null ? isIntExpr(args[1]) : a
|
|
1193
|
+
return a && b
|
|
1194
|
+
}
|
|
1195
|
+
if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
|
|
1196
|
+
if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
|
|
1197
|
+
if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
|
|
1198
|
+
// Math.{imul,clz32,floor,ceil,round,trunc} — prepare normalizes the callee to
|
|
1199
|
+
// the string `math.<fn>`. The pre-prepare `['.', 'Math', '<fn>']` shape is
|
|
1200
|
+
// matched too so this analyzer is robust if invoked on a non-normalized AST.
|
|
1201
|
+
if (op === '()') {
|
|
1202
|
+
const c = args[0]
|
|
1203
|
+
if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
|
|
1204
|
+
if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
|
|
1205
|
+
}
|
|
1206
|
+
return false
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
let changed = true
|
|
1210
|
+
while (changed) {
|
|
1211
|
+
changed = false
|
|
1212
|
+
for (const [name, rhsList] of defs) {
|
|
1213
|
+
if (!intCertain.get(name)) continue
|
|
1214
|
+
if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
for (const [name, intC] of intCertain) {
|
|
1219
|
+
if (intC) updateRep(name, { intCertain: true })
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Infer expression result type from AST (without emitting).
|
|
1225
|
+
* Used to determine local variable types before compilation.
|
|
1226
|
+
* Looks up `locals` first, then current-function params (for i32-specialized params).
|
|
1227
|
+
*/
|
|
1228
|
+
export function exprType(expr, locals) {
|
|
1229
|
+
if (expr == null) return 'f64'
|
|
1230
|
+
if (typeof expr === 'number')
|
|
1231
|
+
return Number.isInteger(expr) && expr >= -2147483648 && expr <= 2147483647 ? 'i32' : 'f64'
|
|
1232
|
+
if (typeof expr === 'string') {
|
|
1233
|
+
if (locals?.has?.(expr)) return locals.get(expr)
|
|
1234
|
+
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
1235
|
+
if (paramType) return paramType
|
|
1236
|
+
return 'f64'
|
|
1237
|
+
}
|
|
1238
|
+
if (!Array.isArray(expr)) return 'f64'
|
|
1239
|
+
|
|
1240
|
+
const [op, ...args] = expr
|
|
1241
|
+
if (op == null) return exprType(args[0], locals) // literal [, value]
|
|
1242
|
+
|
|
1243
|
+
// Always f64
|
|
1244
|
+
if (op === '/' || op === '**' || op === '[' || op === '[]' || op === '{}' || op === 'str') return 'f64'
|
|
1245
|
+
// `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
|
|
1246
|
+
// both return i32). Letting it stay i32 lets analyzeLocals keep the counter
|
|
1247
|
+
// local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
|
|
1248
|
+
// the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
|
|
1249
|
+
// Only safe when receiver type is statically known to expose an integer length.
|
|
1250
|
+
if (op === '.') {
|
|
1251
|
+
if (args[1] === 'length' && typeof args[0] === 'string') {
|
|
1252
|
+
const vt = lookupValType(args[0])
|
|
1253
|
+
if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
|
|
1254
|
+
}
|
|
1255
|
+
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
1256
|
+
const vt = lookupValType(args[0])
|
|
1257
|
+
if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
|
|
1258
|
+
}
|
|
1259
|
+
if (args[1] === 'byteLength' && typeof args[0] === 'string') {
|
|
1260
|
+
const vt = lookupValType(args[0])
|
|
1261
|
+
if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
|
|
1262
|
+
}
|
|
1263
|
+
return 'f64'
|
|
1264
|
+
}
|
|
1265
|
+
// Always i32
|
|
1266
|
+
if (['>', '<', '>=', '<=', '==', '!=', '!', '&', '|', '^', '~', '<<', '>>', '>>>'].includes(op)) return 'i32'
|
|
1267
|
+
// Preserve i32 if both operands i32
|
|
1268
|
+
if (['+', '-', '*', '%'].includes(op)) {
|
|
1269
|
+
const ta = exprType(args[0], locals)
|
|
1270
|
+
const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
|
|
1271
|
+
return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
|
|
1272
|
+
}
|
|
1273
|
+
// Unary preserves type
|
|
1274
|
+
if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
|
|
1275
|
+
// Ternary / logical: conciliate
|
|
1276
|
+
if (op === '?:' || op === '&&' || op === '||') {
|
|
1277
|
+
const branches = op === '?:' ? [args[1], args[2]] : [args[0], args[1]]
|
|
1278
|
+
const ta = exprType(branches[0], locals), tb = exprType(branches[1], locals)
|
|
1279
|
+
return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
|
|
1280
|
+
}
|
|
1281
|
+
if (op === '[') return 'f64'
|
|
1282
|
+
// Builtin calls with known i32 result. Math.imul / Math.clz32 always produce
|
|
1283
|
+
// a 32-bit integer; recognising this here keeps `let x = Math.imul(...)` (and
|
|
1284
|
+
// chains like `x = Math.imul(x, k) + 12345`) on the i32 ABI all the way
|
|
1285
|
+
// through, instead of widening the local to f64 because exprType defaulted.
|
|
1286
|
+
if (op === '()') {
|
|
1287
|
+
if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
|
|
1288
|
+
// Method calls returning i32: charCodeAt → byte (0..255). Lets tokenizer-shape
|
|
1289
|
+
// hot loops keep `c` as i32 across `c >= 48 && c <= 57`, `c - 48`, etc.,
|
|
1290
|
+
// skipping the f64.convert_i32_u widen at every char read.
|
|
1291
|
+
if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt') return 'i32'
|
|
1292
|
+
// User-function call: consult the callee's narrowed result type. By the time
|
|
1293
|
+
// analyzeLocals runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
|
|
1294
|
+
// on every body-i32-only func. Propagating this lets `let h = userFn(...)`
|
|
1295
|
+
// (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
|
|
1296
|
+
// to f64 and round-tripping i32↔f64 every iteration.
|
|
1297
|
+
if (typeof args[0] === 'string') {
|
|
1298
|
+
const f = ctx.func.map?.get(args[0])
|
|
1299
|
+
if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'i32' && f.sig.ptrKind == null) return 'i32'
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return 'f64'
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Analyze all local declarations and assignments to determine types.
|
|
1307
|
+
* A local is i32 if ALL assignments produce i32. Any f64 widens to f64.
|
|
1308
|
+
*
|
|
1309
|
+
* Thin slice of `analyzeBody` (single unified walk).
|
|
1310
|
+
*/
|
|
1311
|
+
export function analyzeLocals(body) {
|
|
1312
|
+
return analyzeBody(body).locals
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Identify locals that can be stored as an unboxed i32 pointer offset instead of
|
|
1317
|
+
* a NaN-boxed f64. Static type is tracked out-of-band so reads skip `__ptr_offset`
|
|
1318
|
+
* and `__ptr_type` entirely and writes unbox once at the assignment site.
|
|
1319
|
+
*
|
|
1320
|
+
* Criteria — the local must be:
|
|
1321
|
+
* - declared once with `let`/`const`, never reassigned or compound-assigned
|
|
1322
|
+
* - valType is an unambiguous non-forwarding pointer kind:
|
|
1323
|
+
* OBJECT, SET, MAP, CLOSURE, TYPED, BUFFER
|
|
1324
|
+
* (excluded: ARRAY — forwards on realloc; STRING — SSO/heap dual encoding.)
|
|
1325
|
+
* - initialized from a form that guarantees a fresh, non-null pointer of that VAL:
|
|
1326
|
+
* OBJECT ← `{…}`
|
|
1327
|
+
* SET ← `new Set(...)`
|
|
1328
|
+
* MAP ← `new Map(...)`
|
|
1329
|
+
* CLOSURE← `=>` literal
|
|
1330
|
+
* BUFFER ← `new ArrayBuffer(...)` / `new DataView(...)`
|
|
1331
|
+
* TYPED ← `new XxxArray(...)` / method returning typed array
|
|
1332
|
+
* - not captured in boxed storage (boxed locals stay f64 for the heap slot)
|
|
1333
|
+
* - never compared to null/undefined (we lose the nullish NaN representation)
|
|
1334
|
+
*
|
|
1335
|
+
* Returns Map<name, VAL> of locals to unbox.
|
|
1336
|
+
*/
|
|
1337
|
+
export function analyzePtrUnboxable(body, locals, boxed) {
|
|
1338
|
+
const candidates = new Set()
|
|
1339
|
+
const disqualified = new Set()
|
|
1340
|
+
const valOf = name => ctx.func.repByLocal?.get(name)?.val
|
|
1341
|
+
|
|
1342
|
+
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE])
|
|
1343
|
+
|
|
1344
|
+
// RHS must produce a fresh, non-null pointer of the declared VAL kind.
|
|
1345
|
+
// OBJECT ← `{…}`
|
|
1346
|
+
// CLOSURE ← `=>`
|
|
1347
|
+
// SET/MAP/BUFFER/TYPED ← `new X(...)`
|
|
1348
|
+
// Validating the exact ctor→VAL match keeps the analysis tied to valTypeOf, so when
|
|
1349
|
+
// that helper grows (e.g. `Array.from` → ARRAY), we don't drift out of sync.
|
|
1350
|
+
const isFreshInit = (expr, kind) => {
|
|
1351
|
+
if (!Array.isArray(expr)) return false
|
|
1352
|
+
if (kind === VAL.OBJECT) {
|
|
1353
|
+
if (expr[0] === '{}') return true
|
|
1354
|
+
// Call to a narrow-ABI'd helper: returns i32 ptr-offset of the same VAL kind.
|
|
1355
|
+
// Unboxing skips the f64-rebox at the callsite. Verifying via sig (not just
|
|
1356
|
+
// valResult) ensures the call already produces an i32 — which dual-write picks
|
|
1357
|
+
// up to bind ptrKind/schemaId on the local.
|
|
1358
|
+
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
1359
|
+
const f = ctx.func.map?.get(expr[1])
|
|
1360
|
+
return f?.sig?.ptrKind === kind
|
|
1361
|
+
}
|
|
1362
|
+
// `let p = arr[i]` where arr has a known elem schema: the runtime helper
|
|
1363
|
+
// returns f64 (NaN-box of an OBJECT pointer), but its low 32 bits are
|
|
1364
|
+
// exactly the pointer offset. Dual-write coerces once via reinterpret/wrap;
|
|
1365
|
+
// subsequent `p.x` reads then become direct `f64.load offset=K (local.get $p)`
|
|
1366
|
+
// (since ptrOffsetIR sees ptrKind=OBJECT and skips the per-access wrap).
|
|
1367
|
+
if (expr[0] === '[]' && typeof expr[1] === 'string') {
|
|
1368
|
+
const repSid = ctx.func.repByLocal?.get(expr[1])?.arrayElemSchema
|
|
1369
|
+
return repSid != null
|
|
1370
|
+
}
|
|
1371
|
+
return false
|
|
1372
|
+
}
|
|
1373
|
+
if (kind === VAL.CLOSURE) return expr[0] === '=>'
|
|
1374
|
+
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
1375
|
+
const callee = expr[1]
|
|
1376
|
+
if (callee.startsWith('new.')) {
|
|
1377
|
+
if (kind === VAL.SET) return callee === 'new.Set'
|
|
1378
|
+
if (kind === VAL.MAP) return callee === 'new.Map'
|
|
1379
|
+
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer' || callee === 'new.DataView'
|
|
1380
|
+
if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
|
|
1381
|
+
}
|
|
1382
|
+
// Call to narrow-ABI'd helper of matching VAL kind.
|
|
1383
|
+
const f = ctx.func.map?.get(callee)
|
|
1384
|
+
if (f?.sig?.ptrKind === kind) return true
|
|
1385
|
+
}
|
|
1386
|
+
// Method call returning TYPED: `arr.map(fn)` where `arr` is in typedElem
|
|
1387
|
+
// (locally TYPED with a known elem ctor). Only `.typed:map` is registered
|
|
1388
|
+
// as TYPED-returning — `.filter`/`.slice` fall back to ARRAY emit. The
|
|
1389
|
+
// typedElem.has(src) gate ensures we don't accept the polymorphic-receiver
|
|
1390
|
+
// path that emits a plain ARRAY result. propagateTyped already mirrored
|
|
1391
|
+
// the src ctor onto the receiver, so the unbox path picks up its aux.
|
|
1392
|
+
if (kind === VAL.TYPED && expr[0] === '()' &&
|
|
1393
|
+
Array.isArray(expr[1]) && expr[1][0] === '.' &&
|
|
1394
|
+
typeof expr[1][1] === 'string' && expr[1][2] === 'map' &&
|
|
1395
|
+
ctx.types.typedElem?.has(expr[1][1])) {
|
|
1396
|
+
return true
|
|
1397
|
+
}
|
|
1398
|
+
return false
|
|
1399
|
+
}
|
|
1400
|
+
const isNullishLit = (expr) =>
|
|
1401
|
+
expr === 'null' || expr === 'undefined' ||
|
|
1402
|
+
(Array.isArray(expr) && expr[0] == null &&
|
|
1403
|
+
(expr[1] === null || expr[1] === undefined))
|
|
1404
|
+
|
|
1405
|
+
function collect(node) {
|
|
1406
|
+
if (!Array.isArray(node)) return
|
|
1407
|
+
const [op, ...args] = node
|
|
1408
|
+
if (op === '=>') return
|
|
1409
|
+
if (op === 'let' || op === 'const') {
|
|
1410
|
+
for (const a of args) {
|
|
1411
|
+
if (!Array.isArray(a) || a[0] !== '=' || typeof a[1] !== 'string') continue
|
|
1412
|
+
const name = a[1]
|
|
1413
|
+
const vt = valOf(name)
|
|
1414
|
+
if (!UNBOXABLE_KINDS.has(vt)) continue
|
|
1415
|
+
if (locals.get(name) !== 'f64') continue
|
|
1416
|
+
if (boxed?.has(name)) continue
|
|
1417
|
+
if (!isFreshInit(a[2], vt)) continue
|
|
1418
|
+
candidates.add(name)
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const a of args) collect(a)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
|
|
1425
|
+
'<<=', '>>=', '>>>=', '||=', '&&=', '??='])
|
|
1426
|
+
const NULL_CMP_OPS = new Set(['==', '!=', '===', '!=='])
|
|
1427
|
+
|
|
1428
|
+
function check(node) {
|
|
1429
|
+
if (!Array.isArray(node)) return
|
|
1430
|
+
const [op, ...args] = node
|
|
1431
|
+
if (op === '=>') return
|
|
1432
|
+
if (ASSIGN_OPS.has(op) && typeof args[0] === 'string' && candidates.has(args[0])) {
|
|
1433
|
+
if (op !== '=') disqualified.add(args[0])
|
|
1434
|
+
// Initial `let x = {…}` arrives here too as op='='; the `let` pass already vetted it.
|
|
1435
|
+
// A later `x = …` in the same body is a re-assignment — disqualify unless it's the init.
|
|
1436
|
+
// We detect by tracking count: if we see '=' twice for the same name, disqualify.
|
|
1437
|
+
}
|
|
1438
|
+
if ((op === '++' || op === '--') && typeof args[0] === 'string' && candidates.has(args[0]))
|
|
1439
|
+
disqualified.add(args[0])
|
|
1440
|
+
if (NULL_CMP_OPS.has(op)) {
|
|
1441
|
+
for (let i = 0; i < 2; i++) {
|
|
1442
|
+
const side = args[i], other = args[1 - i]
|
|
1443
|
+
if (typeof side === 'string' && candidates.has(side) && isNullishLit(other)) disqualified.add(side)
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
for (const a of args) check(a)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Count bare `=` assignments per candidate. Init `let x = …` is NOT parsed as `['=', x, …]`
|
|
1450
|
+
// at the statement level — it's inside `['let', ['=', x, …]]`. A standalone `['=', x, …]`
|
|
1451
|
+
// at statement level IS a reassignment.
|
|
1452
|
+
const assignCount = new Map()
|
|
1453
|
+
function countAssigns(node, inLet) {
|
|
1454
|
+
if (!Array.isArray(node)) return
|
|
1455
|
+
const [op, ...args] = node
|
|
1456
|
+
if (op === '=>') return
|
|
1457
|
+
if (op === '=' && !inLet && typeof args[0] === 'string' && candidates.has(args[0])) {
|
|
1458
|
+
assignCount.set(args[0], (assignCount.get(args[0]) || 0) + 1)
|
|
1459
|
+
}
|
|
1460
|
+
const childInLet = op === 'let' || op === 'const'
|
|
1461
|
+
for (const a of args) countAssigns(a, childInLet)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
collect(body)
|
|
1465
|
+
check(body)
|
|
1466
|
+
countAssigns(body, false)
|
|
1467
|
+
|
|
1468
|
+
for (const [name, count] of assignCount) if (count > 0) disqualified.add(name)
|
|
1469
|
+
|
|
1470
|
+
const result = new Map()
|
|
1471
|
+
for (const name of candidates) if (!disqualified.has(name)) result.set(name, valOf(name))
|
|
1472
|
+
return result
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// === Param / closure helpers ===
|
|
1476
|
+
|
|
1477
|
+
export function extractParams(rawParams) {
|
|
1478
|
+
let p = rawParams
|
|
1479
|
+
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
1480
|
+
return p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export function classifyParam(r) {
|
|
1484
|
+
if (Array.isArray(r) && r[0] === '...') return { kind: 'rest', name: r[1] }
|
|
1485
|
+
if (Array.isArray(r) && r[0] === '=') {
|
|
1486
|
+
if (typeof r[1] === 'string') return { kind: 'default', name: r[1], defValue: r[2] }
|
|
1487
|
+
return { kind: 'destruct-default', pattern: r[1], defValue: r[2] }
|
|
1488
|
+
}
|
|
1489
|
+
if (Array.isArray(r) && (r[0] === '[]' || r[0] === '{}')) return { kind: 'destruct', pattern: r }
|
|
1490
|
+
return { kind: 'plain', name: r }
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export function collectParamNames(raw, out = new Set()) {
|
|
1494
|
+
for (const r of raw) {
|
|
1495
|
+
if (typeof r === 'string') out.add(r)
|
|
1496
|
+
else if (Array.isArray(r)) {
|
|
1497
|
+
if (r[0] === '=' && typeof r[1] === 'string') out.add(r[1])
|
|
1498
|
+
else if (r[0] === '...' && typeof r[1] === 'string') out.add(r[1])
|
|
1499
|
+
else if (r[0] === '=' && Array.isArray(r[1])) collectParamNames([r[1]], out)
|
|
1500
|
+
else if (r[0] === '[]' || r[0] === '{}' || r[0] === ',') collectParamNames(r.slice(1), out)
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return out
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/** Find free variables in AST: referenced in node, not in `bound`, present in `scope`. */
|
|
1507
|
+
export function findFreeVars(node, bound, free, scope) {
|
|
1508
|
+
if (node == null) return
|
|
1509
|
+
if (typeof node === 'string') {
|
|
1510
|
+
if (bound.has(node) || free.includes(node)) return
|
|
1511
|
+
const inScope = scope
|
|
1512
|
+
? scope.has(node)
|
|
1513
|
+
: (ctx.func.locals?.has(node) || ctx.func.current?.params.some(p => p.name === node))
|
|
1514
|
+
if (inScope) free.push(node)
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
if (!Array.isArray(node)) return
|
|
1518
|
+
const [op, ...args] = node
|
|
1519
|
+
if (op === '=>') {
|
|
1520
|
+
const innerBound = collectParamNames(extractParams(args[0]), new Set(bound))
|
|
1521
|
+
findFreeVars(args[1], innerBound, free, scope)
|
|
1522
|
+
return
|
|
1523
|
+
}
|
|
1524
|
+
if (op === 'let' || op === 'const') {
|
|
1525
|
+
collectParamNames(args, bound)
|
|
1526
|
+
if (scope) collectParamNames(args, scope)
|
|
1527
|
+
}
|
|
1528
|
+
if (op === 'for' && Array.isArray(args[0]) && (args[0][0] === 'let' || args[0][0] === 'const')) {
|
|
1529
|
+
collectParamNames(args[0].slice(1), bound)
|
|
1530
|
+
if (scope) collectParamNames(args[0].slice(1), scope)
|
|
1531
|
+
}
|
|
1532
|
+
for (const a of args) findFreeVars(a, bound, free, scope)
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
1536
|
+
|
|
1537
|
+
/** Check if any of the given variable names are assigned anywhere in the AST. */
|
|
1538
|
+
export function findMutations(node, names, mutated) {
|
|
1539
|
+
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return
|
|
1540
|
+
const [op, ...args] = node
|
|
1541
|
+
if (op === 'let' || op === 'const') {
|
|
1542
|
+
for (const decl of args)
|
|
1543
|
+
if (Array.isArray(decl) && decl[0] === '=') findMutations(decl[2], names, mutated)
|
|
1544
|
+
return
|
|
1545
|
+
}
|
|
1546
|
+
if (ASSIGN_OPS.has(op) && typeof args[0] === 'string' && names.has(args[0]))
|
|
1547
|
+
mutated.add(args[0])
|
|
1548
|
+
if ((op === '++' || op === '--') && typeof args[0] === 'string' && names.has(args[0]))
|
|
1549
|
+
mutated.add(args[0])
|
|
1550
|
+
for (const a of args) findMutations(a, names, mutated)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Pre-scan AST for variables that need a `__dyn_props` shadow sidecar.
|
|
1555
|
+
*
|
|
1556
|
+
* The shadow exists so `obj[runtimeKey]` can read a value via `__dyn_get`,
|
|
1557
|
+
* and `obj.prop = v` keeps the sidecar in sync. Most object literals are only
|
|
1558
|
+
* accessed via `.prop` or `obj['lit']`, both of which resolve through the
|
|
1559
|
+
* schema directly and bypass the shadow. Allocating + populating the sidecar
|
|
1560
|
+
* for those literals is pure waste.
|
|
1561
|
+
*
|
|
1562
|
+
* Populates:
|
|
1563
|
+
* - ctx.types.dynKeyVars: Set<string> — names accessed via runtime key
|
|
1564
|
+
* - ctx.types.anyDynKey: boolean — any dynamic key access exists in program
|
|
1565
|
+
* (used for escaping literals where no target var is known)
|
|
1566
|
+
*/
|
|
1567
|
+
export function analyzeDynKeys(...roots) {
|
|
1568
|
+
const dynVars = new Set()
|
|
1569
|
+
let anyDyn = false
|
|
1570
|
+
const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
1571
|
+
|
|
1572
|
+
function walk(node) {
|
|
1573
|
+
if (!Array.isArray(node)) return
|
|
1574
|
+
const [op, ...args] = node
|
|
1575
|
+
if (op === '[]') {
|
|
1576
|
+
const [obj, idx] = args
|
|
1577
|
+
if (!isLiteralStr(idx)) {
|
|
1578
|
+
anyDyn = true
|
|
1579
|
+
if (typeof obj === 'string') dynVars.add(obj)
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
// Runtime for-in (compile-time unroll didn't fire) → walks via shadow
|
|
1583
|
+
if (op === 'for-in') {
|
|
1584
|
+
anyDyn = true
|
|
1585
|
+
if (typeof args[1] === 'string') dynVars.add(args[1])
|
|
1586
|
+
}
|
|
1587
|
+
for (const a of args) walk(a)
|
|
1588
|
+
}
|
|
1589
|
+
for (const r of roots) walk(r)
|
|
1590
|
+
if (ctx.func.list) for (const f of ctx.func.list) if (f.body) walk(f.body)
|
|
1591
|
+
if (ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) walk(mi)
|
|
1592
|
+
|
|
1593
|
+
ctx.types.dynKeyVars = dynVars
|
|
1594
|
+
ctx.types.anyDynKey = anyDyn
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Pre-scan function body for captured variables that are mutated.
|
|
1599
|
+
* Marks mutably-captured vars in ctx.func.boxed for cell-based capture.
|
|
1600
|
+
*/
|
|
1601
|
+
export function analyzeBoxedCaptures(body) {
|
|
1602
|
+
const outerScope = new Set()
|
|
1603
|
+
;(function collectDecls(node) {
|
|
1604
|
+
if (!Array.isArray(node)) return
|
|
1605
|
+
const [op, ...args] = node
|
|
1606
|
+
if (op === '=>') return
|
|
1607
|
+
if (op === 'let' || op === 'const')
|
|
1608
|
+
for (const a of args)
|
|
1609
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') outerScope.add(a[1])
|
|
1610
|
+
for (const a of args) collectDecls(a)
|
|
1611
|
+
})(body)
|
|
1612
|
+
if (ctx.func.current?.params) for (const p of ctx.func.current.params) outerScope.add(p.name)
|
|
1613
|
+
if (ctx.func.locals) for (const k of ctx.func.locals.keys()) outerScope.add(k)
|
|
1614
|
+
|
|
1615
|
+
;(function walk(node, assignTarget) {
|
|
1616
|
+
if (!Array.isArray(node)) return
|
|
1617
|
+
const [op, ...args] = node
|
|
1618
|
+
if (op === '=>') {
|
|
1619
|
+
let p = args[0]
|
|
1620
|
+
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
1621
|
+
const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
1622
|
+
const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
|
|
1623
|
+
const captures = []
|
|
1624
|
+
findFreeVars(args[1], paramSet, captures, outerScope)
|
|
1625
|
+
if (captures.length === 0) return
|
|
1626
|
+
const captureSet = new Set(captures)
|
|
1627
|
+
const mutated = new Set()
|
|
1628
|
+
findMutations(body, captureSet, mutated)
|
|
1629
|
+
if (assignTarget && captureSet.has(assignTarget)) mutated.add(assignTarget)
|
|
1630
|
+
for (const v of mutated) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
|
|
1631
|
+
return
|
|
1632
|
+
}
|
|
1633
|
+
if (op === '=' && typeof args[0] === 'string' && Array.isArray(args[1]) && args[1][0] === '=>')
|
|
1634
|
+
return walk(args[1], args[0])
|
|
1635
|
+
for (const a of args) walk(a)
|
|
1636
|
+
})(body)
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Narrow return arr-elem-{schema|valType}: for each non-exported, non-value-used
|
|
1641
|
+
* user func with `valResult === VAL.ARRAY` and `func[field] == null`, walk return
|
|
1642
|
+
* exprs (and trailing-fallthrough literal), resolve each via body-local elem map
|
|
1643
|
+
* + caller-param facts + transitive user-fn results, and if all agree set `func[field]`.
|
|
1644
|
+
* Lets callers' `const rows = initRows()` gain the elem fact, propagating to
|
|
1645
|
+
* runKernel params via paramReps. `field` selects which fact ('arrayElemSchema'
|
|
1646
|
+
* | 'arrayElemValType') — slice key is derived.
|
|
1647
|
+
*/
|
|
1648
|
+
const _FIELD_TO_SLICE = {
|
|
1649
|
+
arrayElemSchema: 'arrElemSchemas',
|
|
1650
|
+
arrayElemValType: 'arrElemValTypes',
|
|
1651
|
+
}
|
|
1652
|
+
export function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
1653
|
+
const sliceKey = _FIELD_TO_SLICE[field]
|
|
1654
|
+
const targets = ctx.func.list.filter(f =>
|
|
1655
|
+
!f.raw && !f.exported && !valueUsed.has(f.name) &&
|
|
1656
|
+
f.valResult === VAL.ARRAY && f[field] == null
|
|
1657
|
+
)
|
|
1658
|
+
let changed = true
|
|
1659
|
+
while (changed) {
|
|
1660
|
+
changed = false
|
|
1661
|
+
// Cache-staleness barrier: the fixpoint mutates target funcs' [field]
|
|
1662
|
+
// between iterations. analyzeBody reads ctx.func.map[*][field] when
|
|
1663
|
+
// resolving `const x = callee()` and similar chains, so any cached entry
|
|
1664
|
+
// from a prior iter would freeze cross-func propagation. Clear all target
|
|
1665
|
+
// bodies before each sweep.
|
|
1666
|
+
for (const f of targets) _bodyFactsCache.delete(f.body)
|
|
1667
|
+
for (const func of targets) {
|
|
1668
|
+
if (func[field] != null) continue
|
|
1669
|
+
const isBlock = isBlockBody(func.body)
|
|
1670
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
1671
|
+
const exprs = returnExprs(func.body)
|
|
1672
|
+
if (!exprs.length) continue
|
|
1673
|
+
// analyzeBody is context-pure for the arrElem slices, so a single walk
|
|
1674
|
+
// gives both `locals` (for ctx.func.locals seeding — observe filter for
|
|
1675
|
+
// param-aware downstream consumers) and the requested slice.
|
|
1676
|
+
const savedLocals = ctx.func.locals
|
|
1677
|
+
const facts = analyzeBody(func.body)
|
|
1678
|
+
ctx.func.locals = new Map(facts.locals)
|
|
1679
|
+
for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
|
|
1680
|
+
const localElems = facts[sliceKey]
|
|
1681
|
+
ctx.func.locals = savedLocals
|
|
1682
|
+
const paramElemMap = callerParamFactMap(paramReps, func, field) || new Map()
|
|
1683
|
+
const resolveExpr = (expr) => {
|
|
1684
|
+
if (typeof expr === 'string') {
|
|
1685
|
+
if (localElems.has(expr)) {
|
|
1686
|
+
const v = localElems.get(expr)
|
|
1687
|
+
if (v != null) return v
|
|
1688
|
+
}
|
|
1689
|
+
if (paramElemMap.has(expr)) return paramElemMap.get(expr)
|
|
1690
|
+
return null
|
|
1691
|
+
}
|
|
1692
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
1693
|
+
const f = ctx.func.map?.get(expr[1])
|
|
1694
|
+
if (f?.[field] != null) return f[field]
|
|
1695
|
+
}
|
|
1696
|
+
if (Array.isArray(expr) && expr[0] === '?:') {
|
|
1697
|
+
const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
|
|
1698
|
+
return a != null && a === b ? a : null
|
|
1699
|
+
}
|
|
1700
|
+
if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
|
|
1701
|
+
const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
|
|
1702
|
+
return a != null && a === b ? a : null
|
|
1703
|
+
}
|
|
1704
|
+
return null
|
|
1705
|
+
}
|
|
1706
|
+
const v0 = resolveExpr(exprs[0])
|
|
1707
|
+
if (v0 == null) continue
|
|
1708
|
+
if (!exprs.every(e => resolveExpr(e) === v0)) continue
|
|
1709
|
+
func[field] = v0
|
|
1710
|
+
changed = true
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Phase: program-fact collection.
|
|
1717
|
+
*
|
|
1718
|
+
* Single whole-program walk over the module AST + each user function body
|
|
1719
|
+
* + all moduleInits. Collects:
|
|
1720
|
+
* dynVars/anyDyn — vars accessed via runtime key (drives strict mode +
|
|
1721
|
+
* 1 __dyn_get fallback gating)
|
|
1722
|
+
* propMap — property assignments per receiver (auto-box schemas)
|
|
1723
|
+
* valueUsed — ctx.func.names passed as first-class values (excluded
|
|
1724
|
+
* from internal narrowing — they need uniform $ftN ABI)
|
|
1725
|
+
* maxDef/maxCall — closure ABI width inputs
|
|
1726
|
+
* hasRest/hasSpread
|
|
1727
|
+
* callSites — `{ callee, argList, callerFunc, node }` for static-name
|
|
1728
|
+
* calls (drives the type/schema fixpoint without
|
|
1729
|
+
* re-walking the AST). `node` is the call AST itself,
|
|
1730
|
+
* mutable for bimorphic-typed clone routing.
|
|
1731
|
+
* paramReps — Map<funcName, Map<paramIdx, ValueRep>>, empty here;
|
|
1732
|
+
* populated by narrowSignatures (per-field lattice) and
|
|
1733
|
+
* read by emitFunc.
|
|
1734
|
+
*
|
|
1735
|
+
* Also writes ctx.schema.slotTypes (static-key object literal slot val types).
|
|
1736
|
+
*
|
|
1737
|
+
* Three visit modes:
|
|
1738
|
+
* full=true (ast + user funcs) → all facts including call-site collection
|
|
1739
|
+
* full=false (moduleInits) → dyn + arity only (no propMap/valueUsed/
|
|
1740
|
+
* callSites: moduleInits don't own user
|
|
1741
|
+
* props/funcs)
|
|
1742
|
+
* inArrow=true → flips off call-site collection so
|
|
1743
|
+
* closure-internal calls don't poison
|
|
1744
|
+
* caller-context type inference.
|
|
1745
|
+
*/
|
|
1746
|
+
export function collectProgramFacts(ast) {
|
|
1747
|
+
const paramReps = new Map()
|
|
1748
|
+
const valueUsed = new Set()
|
|
1749
|
+
const dynVars = new Set()
|
|
1750
|
+
let anyDyn = false
|
|
1751
|
+
const propMap = new Map()
|
|
1752
|
+
const callSites = []
|
|
1753
|
+
const doSchema = ast && ctx.schema.register
|
|
1754
|
+
const doArity = !!ctx.closure.make
|
|
1755
|
+
let maxDef = 0, maxCall = 0, hasRest = false, hasSpread = false
|
|
1756
|
+
const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
1757
|
+
// Slot-type observation lives in the dedicated `observeProgramSlots` pass below;
|
|
1758
|
+
// walkFacts only registers schemas (which is local to the AST node).
|
|
1759
|
+
const walkFacts = (node, full, inArrow, callerFunc) => {
|
|
1760
|
+
if (!Array.isArray(node)) return
|
|
1761
|
+
const [op, ...args] = node
|
|
1762
|
+
// dyn-key detection. Strict check deferred to emit time (e.g. `buf[i]` on a
|
|
1763
|
+
// Float64Array uses typed-array load, not __dyn_get — only the actual
|
|
1764
|
+
// dynamic-dispatch fallback should error in strict mode).
|
|
1765
|
+
if (op === '[]') {
|
|
1766
|
+
const [obj, idx] = args
|
|
1767
|
+
if (!isLiteralStr(idx)) { anyDyn = true; if (typeof obj === 'string') dynVars.add(obj) }
|
|
1768
|
+
} else if (op === 'for-in') {
|
|
1769
|
+
if (ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
|
|
1770
|
+
anyDyn = true
|
|
1771
|
+
if (typeof args[1] === 'string') dynVars.add(args[1])
|
|
1772
|
+
}
|
|
1773
|
+
// Object literal: register schema. Slot val-type observation is deferred to a
|
|
1774
|
+
// second pass (observeSlotsIn below) so that shorthand `{x}` (expanded by prepare
|
|
1775
|
+
// to `[':', x, x]`) resolves x's val type via per-function locals, not just globals.
|
|
1776
|
+
if (op === '{}' && doSchema) {
|
|
1777
|
+
const parsed = staticObjectProps(args)
|
|
1778
|
+
if (parsed) ctx.schema.register(parsed.names)
|
|
1779
|
+
}
|
|
1780
|
+
// closure ABI arity
|
|
1781
|
+
if (doArity) {
|
|
1782
|
+
if (op === '=>') {
|
|
1783
|
+
let fixedN = 0
|
|
1784
|
+
for (const r of extractParams(args[0])) {
|
|
1785
|
+
if (classifyParam(r).kind === 'rest') hasRest = true
|
|
1786
|
+
else fixedN++
|
|
1787
|
+
}
|
|
1788
|
+
if (fixedN > maxDef) maxDef = fixedN
|
|
1789
|
+
} else if (op === '()') {
|
|
1790
|
+
const a = args[1]
|
|
1791
|
+
const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
1792
|
+
if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) hasSpread = true
|
|
1793
|
+
if (callArgs.length > maxCall) maxCall = callArgs.length
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
// Crossing into a closure body: from now on, no call-site collection (matches the
|
|
1797
|
+
// pre-fusion scanCalls bailing at '=>'). Still walks children for arity/dyn.
|
|
1798
|
+
if (op === '=>') {
|
|
1799
|
+
for (const a of args) walkFacts(a, full, true, callerFunc)
|
|
1800
|
+
return
|
|
1801
|
+
}
|
|
1802
|
+
if (full) {
|
|
1803
|
+
// property-assignment scan for auto-box
|
|
1804
|
+
if (doSchema && op === '=' && Array.isArray(args[0]) && args[0][0] === '.') {
|
|
1805
|
+
const [, obj, prop] = args[0]
|
|
1806
|
+
if (typeof obj === 'string' && (ctx.scope.globals.has(obj) || ctx.func.names.has(obj))) {
|
|
1807
|
+
if (!propMap.has(obj)) propMap.set(obj, new Set())
|
|
1808
|
+
propMap.get(obj).add(prop)
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
// first-class function-value + static-call-site scan
|
|
1812
|
+
if (op === '()' && typeof args[0] === 'string' && ctx.func.names.has(args[0])) {
|
|
1813
|
+
if (!inArrow) {
|
|
1814
|
+
// Record call site for the type/schema fixpoint. Filtering by
|
|
1815
|
+
// exported/raw/valueUsed happens later (valueUsed isn't fully populated yet).
|
|
1816
|
+
// `node` is the call AST node itself; specializeBimorphicTyped mutates
|
|
1817
|
+
// node[1] (the callee name) to point at a per-ctor clone.
|
|
1818
|
+
const a = args[1]
|
|
1819
|
+
const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
1820
|
+
callSites.push({ callee: args[0], argList, callerFunc, node })
|
|
1821
|
+
}
|
|
1822
|
+
for (let i = 1; i < args.length; i++) {
|
|
1823
|
+
const a = args[i]
|
|
1824
|
+
if (typeof a === 'string' && ctx.func.names.has(a)) valueUsed.add(a)
|
|
1825
|
+
else walkFacts(a, true, inArrow, callerFunc)
|
|
1826
|
+
}
|
|
1827
|
+
return
|
|
1828
|
+
}
|
|
1829
|
+
if ((op === '.' || op === '?.') && typeof args[0] === 'string' && ctx.func.names.has(args[0])) return
|
|
1830
|
+
for (const a of args) {
|
|
1831
|
+
if (typeof a === 'string' && ctx.func.names.has(a)) valueUsed.add(a)
|
|
1832
|
+
else walkFacts(a, true, inArrow, callerFunc)
|
|
1833
|
+
}
|
|
1834
|
+
} else {
|
|
1835
|
+
for (const a of args) walkFacts(a, false, inArrow, callerFunc)
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
walkFacts(ast, true, false, null)
|
|
1839
|
+
for (const func of ctx.func.list) if (func.body && !func.raw) walkFacts(func.body, true, false, func)
|
|
1840
|
+
if (ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) walkFacts(mi, false, false, null)
|
|
1841
|
+
|
|
1842
|
+
// Slot-type observation pass: walk every `{}` literal with the right scope's
|
|
1843
|
+
// valTypes installed as `ctx.func.localValTypesOverlay` so shorthand `{x}`
|
|
1844
|
+
// (expanded by prepare to `[':', x, x]`) and chained typed-array reads resolve
|
|
1845
|
+
// through valTypeOf → lookupValType. Skips into closures — they're observed via
|
|
1846
|
+
// their own func.list entry. The overlay is the per-function analyzeBody.valTypes
|
|
1847
|
+
// map (already populated with the same overlay-aware walk).
|
|
1848
|
+
if (doSchema) observeProgramSlots(ast)
|
|
1849
|
+
|
|
1850
|
+
return {
|
|
1851
|
+
dynVars, anyDyn, propMap, valueUsed, callSites,
|
|
1852
|
+
maxDef, maxCall, hasRest, hasSpread,
|
|
1853
|
+
paramReps,
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/** Walk `ast` + every user function body + module inits, observing slot types
|
|
1858
|
+
* on each `{}` literal. Per-function bodies have their analyzeBody.valTypes
|
|
1859
|
+
* installed as overlay so shorthand `{x}` resolves through local consts.
|
|
1860
|
+
*
|
|
1861
|
+
* Re-runnable: compile.js calls this once during collectProgramFacts (before
|
|
1862
|
+
* E2 valResult inference), then again after E2 — on the second pass, valTypeOf
|
|
1863
|
+
* on user-function calls resolves via `f.valResult`, lifting slots whose value
|
|
1864
|
+
* is `const x = userFn(...)` from `undefined` to `NUMBER`/etc.
|
|
1865
|
+
* observeSlot's first-wins-then-clash rule means later precise observations
|
|
1866
|
+
* upgrade undefined slots without re-poisoning already-monomorphic ones. */
|
|
1867
|
+
export function observeProgramSlots(ast) {
|
|
1868
|
+
if (!ctx.schema?.register) return
|
|
1869
|
+
const slotTypes = ctx.schema.slotTypes
|
|
1870
|
+
const observeSlot = (sid, idx, vt) => {
|
|
1871
|
+
if (!vt) return
|
|
1872
|
+
let arr = slotTypes.get(sid)
|
|
1873
|
+
if (!arr) { arr = []; slotTypes.set(sid, arr) }
|
|
1874
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
1875
|
+
if (arr[idx] === null) return
|
|
1876
|
+
if (arr[idx] === undefined) arr[idx] = vt
|
|
1877
|
+
else if (arr[idx] !== vt) arr[idx] = null
|
|
1878
|
+
}
|
|
1879
|
+
const visit = (node) => {
|
|
1880
|
+
if (!Array.isArray(node)) return
|
|
1881
|
+
const op = node[0]
|
|
1882
|
+
if (op === '=>') return
|
|
1883
|
+
if (op === '{}') {
|
|
1884
|
+
const parsed = staticObjectProps(node.slice(1))
|
|
1885
|
+
if (parsed) {
|
|
1886
|
+
const sid = ctx.schema.register(parsed.names)
|
|
1887
|
+
for (let i = 0; i < parsed.values.length; i++) {
|
|
1888
|
+
observeSlot(sid, i, valTypeOf(parsed.values[i]))
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
for (let i = 1; i < node.length; i++) visit(node[i])
|
|
1893
|
+
}
|
|
1894
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
1895
|
+
if (ast) { ctx.func.localValTypesOverlay = null; visit(ast) }
|
|
1896
|
+
for (const func of ctx.func.list) {
|
|
1897
|
+
if (!func.body || func.raw) continue
|
|
1898
|
+
ctx.func.localValTypesOverlay = analyzeBody(func.body).valTypes
|
|
1899
|
+
visit(func.body)
|
|
1900
|
+
}
|
|
1901
|
+
if (ctx.module.moduleInits) {
|
|
1902
|
+
ctx.func.localValTypesOverlay = null
|
|
1903
|
+
for (const mi of ctx.module.moduleInits) visit(mi)
|
|
1904
|
+
}
|
|
1905
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
1906
|
+
}
|