jz 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +266 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +414 -186
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +586 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +589 -202
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/analyze.js
DELETED
|
@@ -1,3818 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-analysis passes — type inference, local analysis, capture detection.
|
|
3
|
-
*
|
|
4
|
-
* # Stage contract
|
|
5
|
-
* IN: prepared AST + ctx.func.list (from prepare).
|
|
6
|
-
* OUT: per-function populated `ctx.func.localReps` (val field) + `ctx.func.locals` + `ctx.func.boxed`,
|
|
7
|
-
* module-global `ctx.scope.globalValTypes`, type-analysis `ctx.types.typedElem` /
|
|
8
|
-
* `.dynKeyVars` / `.anyDynKey`.
|
|
9
|
-
*
|
|
10
|
-
* # Passes (all walk AST; none mutate AST itself — only ctx)
|
|
11
|
-
* - 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
|
-
* - boxedCaptures: detect mutably-captured vars → ctx.func.boxed cells
|
|
17
|
-
* - extractParams/classifyParam/collectParamNames: arrow param AST normalization helpers
|
|
18
|
-
*
|
|
19
|
-
* Ordering: all passes run per function during compile(). plan.js owns the
|
|
20
|
-
* cross-function dynKey scan via programFacts (results land in ctx.types.dynKeyVars).
|
|
21
|
-
*
|
|
22
|
-
* @module analyze
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { ctx, err } from './ctx.js'
|
|
26
|
-
import { isLiteralStr, isFuncRef, I32_MIN, I32_MAX, isI32 } from './ir.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
|
-
/** Assignment operators — shared across analyze, plan, emit. */
|
|
36
|
-
export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
37
|
-
|
|
38
|
-
/** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
|
|
39
|
-
* Both share the `'{}'` op tag; blocks start with statement ops, while object literals start with `:` or `...`. */
|
|
40
|
-
export const isBlockBody = (body) =>
|
|
41
|
-
Array.isArray(body) && body[0] === '{}' && (body.length === 1 || STMT_OPS.has(body[1]?.[0]))
|
|
42
|
-
|
|
43
|
-
/** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
|
|
44
|
-
export function intLiteralValue(expr) {
|
|
45
|
-
let v = null
|
|
46
|
-
if (typeof expr === 'number') v = expr
|
|
47
|
-
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
48
|
-
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
49
|
-
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
50
|
-
return v != null && Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX ? v : null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
54
|
-
export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
|
|
55
|
-
|
|
56
|
-
/** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
|
|
57
|
-
* Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
|
|
58
|
-
* coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
|
|
59
|
-
const collectReturnExprs = (node, out) => {
|
|
60
|
-
if (!Array.isArray(node)) return
|
|
61
|
-
const [op, ...args] = node
|
|
62
|
-
if (op === '=>') return
|
|
63
|
-
if (op === 'return') { if (args[0] != null) out.push(args[0]); return }
|
|
64
|
-
for (const a of args) collectReturnExprs(a, out)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** True if every control-flow path through `n` is guaranteed to terminate via return/throw.
|
|
68
|
-
* Conservative: only recognizes block-trailing return, both arms of complete if/else. Loops/switches
|
|
69
|
-
* count as non-terminating since fall-through is possible. Used by ptr-narrowing to ensure
|
|
70
|
-
* fallthrough fallback won't produce a wrong-typed undef. */
|
|
71
|
-
export const alwaysReturns = (n) => {
|
|
72
|
-
if (!Array.isArray(n)) return false
|
|
73
|
-
const op = n[0]
|
|
74
|
-
if (op === '=>') return false
|
|
75
|
-
if (op === 'return' || op === 'throw') return true
|
|
76
|
-
if (op === '{}' || op === ';') return alwaysReturns(n[n.length - 1])
|
|
77
|
-
if (op === 'if') return n.length >= 4 && alwaysReturns(n[2]) && alwaysReturns(n[3])
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** True if `n` contains a bare `return;` (no value → undefined).
|
|
82
|
-
* Bare returns force the result type to f64 (undef sentinel) — narrowing must skip such bodies. */
|
|
83
|
-
export const hasBareReturn = (n) => {
|
|
84
|
-
if (!Array.isArray(n)) return false
|
|
85
|
-
if (n[0] === '=>') return false
|
|
86
|
-
if (n[0] === 'return' && n[1] == null) return true
|
|
87
|
-
return n.some(hasBareReturn)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Unify body→return-expressions: block bodies collect via `collectReturnExprs`,
|
|
91
|
-
* expression bodies wrap into `[body]`. Pure convenience over the
|
|
92
|
-
* `if (isBlock) collect(...) else exprs.push(body)` pattern repeated across
|
|
93
|
-
* narrowing passes. */
|
|
94
|
-
export const returnExprs = (body) => {
|
|
95
|
-
if (isBlockBody(body)) {
|
|
96
|
-
const out = []
|
|
97
|
-
collectReturnExprs(body, out)
|
|
98
|
-
return out
|
|
99
|
-
}
|
|
100
|
-
return [body]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Value types — what a variable holds (for method dispatch, schema resolution)
|
|
104
|
-
export const VAL = {
|
|
105
|
-
NUMBER: 'number', ARRAY: 'array', STRING: 'string',
|
|
106
|
-
OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
|
|
107
|
-
CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
|
|
108
|
-
BIGINT: 'bigint', BUFFER: 'buffer', DATE: 'date',
|
|
109
|
-
// BOOL is a *type fact* only: the working representation stays i32/f64 0/1
|
|
110
|
-
// (so branches, arithmetic and the optimizer are untouched). The boxed-atom
|
|
111
|
-
// carrier (TRUE_NAN/FALSE_NAN, see src/ir.js) is materialized lazily, only at
|
|
112
|
-
// observation/escape sites — `typeof`, `String`, `JSON.stringify`, the host
|
|
113
|
-
// return boundary — where boolean identity must survive. Everywhere else a
|
|
114
|
-
// VAL.BOOL value is just a 0/1 number, so no perf is paid for the distinction.
|
|
115
|
-
BOOL: 'boolean',
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* ValueRep — unified per-local + per-param representation record. (S2.)
|
|
120
|
-
*
|
|
121
|
-
* One shape, two storages:
|
|
122
|
-
* - per-local (current func): ctx.func.localReps: Map<name, ValueRep>
|
|
123
|
-
* - per-param (cross-call): programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>
|
|
124
|
-
*
|
|
125
|
-
* Lattice per field: undefined = unobserved, null = sticky-poison
|
|
126
|
-
* (cross-site disagreement), value = consensus. Local reps don't use the null
|
|
127
|
-
* sentinel (locals are intra-function — single point of truth). Param reps do
|
|
128
|
-
* (cross-call fixpoint convergence).
|
|
129
|
-
*
|
|
130
|
-
* Fields:
|
|
131
|
-
* val: VAL.* — value-type for method dispatch / schema / length
|
|
132
|
-
* wasm: 'i32'|'f64' — narrowed wasm type at param boundary (param-only today)
|
|
133
|
-
* ptrKind: VAL.* — local stores unboxed i32 pointer offset (local-only today)
|
|
134
|
-
* ptrAux: i32 — kind-dependent aux (TYPED elem code, schemaId, …)
|
|
135
|
-
* schemaId: i32 — schema binding for known-shape OBJECTs
|
|
136
|
-
* arrayElemSchema: i32 — Array<schemaId> element shape
|
|
137
|
-
* arrayElemValType: VAL.* — Array<VAL.*> element val-kind
|
|
138
|
-
* jsonShape: obj — { val, props?, elem? } for HASH/ARRAY trees parsed
|
|
139
|
-
* from a compile-time JSON.parse source. Propagates
|
|
140
|
-
* through `.prop` and `[i]` so nested chains stay typed.
|
|
141
|
-
* typedCtor: str — TypedArray ctor name (`Float64Array`, …)
|
|
142
|
-
* intCertain: bool — proven integer-valued (every defining RHS is integer-shaped).
|
|
143
|
-
* Pure analysis fact; codegen extensions may use it to choose
|
|
144
|
-
* i32-shaped emission inside hot regions where range fits.
|
|
145
|
-
* Boundary ABI is NOT narrowed by this fact alone — narrowing
|
|
146
|
-
* at param/result level remains a separate, opt-in decision.
|
|
147
|
-
* intConst: number — proven same integer literal at every static call site.
|
|
148
|
-
* Param-only (cross-call fixpoint). Drives constant substitution
|
|
149
|
-
* at readVar: every `local.get $param` lowers to `i32.const N`
|
|
150
|
-
* (or `f64.const N`), letting the WAT optimizer fold guards,
|
|
151
|
-
* unroll fixed-bound loops, and treeshake the read entirely.
|
|
152
|
-
* Cleared if the param is written inside the body.
|
|
153
|
-
* notString: bool — write-shape evidence proves the binding isn't a primitive
|
|
154
|
-
* string. Gates STRING-vs-typed dispatch elision at `.length`
|
|
155
|
-
* and `xs[i]` reads. Body-walk source: `notStringEvidence` in
|
|
156
|
-
* src/infer.js. Flow-scoped overlay: see `lookupNotString`.
|
|
157
|
-
*
|
|
158
|
-
* Out-of-band tracking (not rep fields):
|
|
159
|
-
* - boxed captures: `ctx.func.boxed: Map<name, cellName>` — set by boxedCaptures
|
|
160
|
-
* for mutably-captured locals. Parallel to rep because the cell-based storage is a
|
|
161
|
-
* storage decision, not a value-shape fact.
|
|
162
|
-
* - flow-sensitive refinements: `ctx.func.refinements: Map<name, {val?, notString?}>` —
|
|
163
|
-
* set by emitBody's post-terminator narrowing for the duration of a syntactic suffix.
|
|
164
|
-
*/
|
|
165
|
-
|
|
166
|
-
// === ParamReps lattice helpers (cross-call fixpoint) ===
|
|
167
|
-
// programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>. Per-field lattice:
|
|
168
|
-
// undefined unobserved, null sticky-poison (cross-site disagreement), value = consensus.
|
|
169
|
-
|
|
170
|
-
// Lattice primitives for `paramReps` (`mergeParamFact`, `ensureParamRep`,
|
|
171
|
-
// `clearStickyNull`) live in src/infer.js with the call-site evidence
|
|
172
|
-
// extractors that produce facts for them. `paramFactsOf` (next) stays
|
|
173
|
-
// here because `narrowReturnArrayElems` below consumes it; moving it would
|
|
174
|
-
// invert the analyze.js↔infer.js import direction.
|
|
175
|
-
|
|
176
|
-
/** Build `paramName → fact` lookup for a caller's already-narrowed param facts.
|
|
177
|
-
* Used to flow caller's param info into its callees during the cross-call
|
|
178
|
-
* fixpoint (transitive propagation). Returns null if caller has no facts. */
|
|
179
|
-
export const paramFactsOf = (paramReps, callerFunc, key) => {
|
|
180
|
-
if (!callerFunc) return null
|
|
181
|
-
const m = paramReps.get(callerFunc.name)
|
|
182
|
-
if (!m) return null
|
|
183
|
-
let out = null
|
|
184
|
-
for (const [k, r] of m) {
|
|
185
|
-
const v = r[key]
|
|
186
|
-
if (v != null && k < callerFunc.sig.params.length) {
|
|
187
|
-
out ||= new Map()
|
|
188
|
-
out.set(callerFunc.sig.params[k].name, v)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return out
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Get the rep for a local name, or undefined if not tracked. */
|
|
195
|
-
export const repOf = name => ctx.func.localReps?.get(name)
|
|
196
|
-
|
|
197
|
-
/** Merge fields into a local's rep. Lazily allocates the map and the rep.
|
|
198
|
-
* Field set to `undefined` removes that field; empty rep is dropped from the map. */
|
|
199
|
-
export const updateRep = (name, fields) => {
|
|
200
|
-
const m = ctx.func.localReps ||= new Map()
|
|
201
|
-
const prev = m.get(name) || {}
|
|
202
|
-
const next = { ...prev, ...fields }
|
|
203
|
-
for (const k of Object.keys(next)) if (next[k] === undefined) delete next[k]
|
|
204
|
-
if (Object.keys(next).length === 0) m.delete(name)
|
|
205
|
-
else m.set(name, next)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Get the rep for a global name, or undefined if not tracked. */
|
|
209
|
-
export const repOfGlobal = name => ctx.scope.globalReps?.get(name)
|
|
210
|
-
|
|
211
|
-
/** Merge fields into a global's rep. Lazily allocates the map and the rep. */
|
|
212
|
-
export const updateGlobalRep = (name, fields) => {
|
|
213
|
-
const m = ctx.scope.globalReps ||= new Map()
|
|
214
|
-
const prev = m.get(name)
|
|
215
|
-
m.set(name, prev ? { ...prev, ...fields } : { ...fields })
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Look up value type for a variable name. Order: flow-sensitive refinement (if any) →
|
|
219
|
-
* in-progress analyzeBody overlay (if any) → function-local scope → module-global scope.
|
|
220
|
-
* Refinements are pushed by the 'if' emitter when the condition is a type guard
|
|
221
|
-
* (typeof x === 't', Array.isArray(x), etc.) and popped after the then-branch.
|
|
222
|
-
* The overlay (`ctx.func.localValTypesOverlay`) is set by analyzeBody/observeSlots passes
|
|
223
|
-
* pre-emit, when `localReps` isn't populated yet but a local Map<name, VAL.*> is
|
|
224
|
-
* available — lets `const x = new Float64Array(); const y = x[0]` resolve y as NUMBER. */
|
|
225
|
-
export const lookupValType = name => {
|
|
226
|
-
const r = ctx.func.refinements
|
|
227
|
-
if (r && r.size) { const v = r.get(name)?.val; if (v) return v }
|
|
228
|
-
const ov = ctx.func.localValTypesOverlay
|
|
229
|
-
if (ov) { const v = ov.get(name); if (v) return v }
|
|
230
|
-
return ctx.func.localReps?.get(name)?.val || ctx.scope.globalValTypes?.get(name) || null
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Resolve `notString` for a binding, overlaying flow-sensitive refinements
|
|
234
|
-
* on top of the function-global rep proof. Mirrors `lookupValType`'s precedence:
|
|
235
|
-
* refinement first (scope-local), then rep (whole-function). */
|
|
236
|
-
export const lookupNotString = name => {
|
|
237
|
-
const r = ctx.func.refinements
|
|
238
|
-
if (r && r.size && r.get(name)?.notString) return true
|
|
239
|
-
return ctx.func.localReps?.get(name)?.notString === true
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** Infer value type of an AST expression (without emitting). */
|
|
243
|
-
export function valTypeOf(expr) {
|
|
244
|
-
if (expr == null) return null
|
|
245
|
-
if (typeof expr === 'number') return VAL.NUMBER
|
|
246
|
-
if (typeof expr === 'boolean') return VAL.BOOL
|
|
247
|
-
if (typeof expr === 'bigint') return VAL.BIGINT
|
|
248
|
-
if (typeof expr === 'string') return lookupValType(expr)
|
|
249
|
-
if (!Array.isArray(expr)) return null
|
|
250
|
-
|
|
251
|
-
const [op, ...args] = expr
|
|
252
|
-
if (op == null) {
|
|
253
|
-
// Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint, [, bool] = boolean
|
|
254
|
-
if (args.length === 0) return null // undefined literal
|
|
255
|
-
if (args[0] == null) return null // null literal
|
|
256
|
-
if (typeof args[0] === 'boolean') return VAL.BOOL
|
|
257
|
-
if (typeof args[0] === 'symbol') return null // prepared null sentinel
|
|
258
|
-
return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Boolean-result operators: relational/equality compares and logical-not always
|
|
262
|
-
// yield a boolean. (`&&`/`||` are value-preserving, not boolean — excluded.)
|
|
263
|
-
if (op === '!' || op === '<' || op === '<=' || op === '>' || op === '>=' ||
|
|
264
|
-
op === '==' || op === '!=' || op === '===' || op === '!==' || op === 'in' || op === 'instanceof')
|
|
265
|
-
return VAL.BOOL
|
|
266
|
-
|
|
267
|
-
if (op === '[') return VAL.ARRAY
|
|
268
|
-
if (op === 'str' || op === 'strcat') return VAL.STRING
|
|
269
|
-
if (op === '=>') return VAL.CLOSURE
|
|
270
|
-
if (op === '//') return VAL.REGEX
|
|
271
|
-
if (op === '{}' && args[0]?.[0] === ':') return VAL.OBJECT
|
|
272
|
-
if (op === '?:') {
|
|
273
|
-
const ta = valTypeOf(args[1]), tb = valTypeOf(args[2])
|
|
274
|
-
return ta && ta === tb ? ta : null
|
|
275
|
-
}
|
|
276
|
-
// `[]` op covers both array literals (1 arg) and index access (2 args).
|
|
277
|
-
// Array literal: `[]` → ['[]', null]; `[1,2]` → ['[]', [',', ...]]; `[x]` → ['[]', x].
|
|
278
|
-
// Index access: `arr[i]` → ['[]', arr, i].
|
|
279
|
-
if (op === '[]') {
|
|
280
|
-
if (args.length < 2) return VAL.ARRAY
|
|
281
|
-
// Indexed read on a known typed-array receiver yields Number except for
|
|
282
|
-
// BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
|
|
283
|
-
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
|
|
284
|
-
return typedCtorElemValType(ctx.types.typedElem?.get(args[0])) || VAL.NUMBER
|
|
285
|
-
// Indexed read on a STRING returns a 1-char string (SSO at runtime).
|
|
286
|
-
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.STRING) return VAL.STRING
|
|
287
|
-
if (Array.isArray(args[0]) && valTypeOf(args[0]) === VAL.STRING) return VAL.STRING
|
|
288
|
-
// Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
|
|
289
|
-
// Set by analyzeValTypes from body observations + emitFunc preseed for params.
|
|
290
|
-
if (typeof args[0] === 'string') {
|
|
291
|
-
const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
|
|
292
|
-
if (elemVt) return elemVt
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
|
|
296
|
-
// to a slot whose VAL kind is monomorphic across program-wide observations,
|
|
297
|
-
// return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
|
|
298
|
-
// checks on numeric properties of known shapes. Precise-only — see
|
|
299
|
-
// ctx.schema.slotVT for why structural subtyping is intentionally off.
|
|
300
|
-
if (op === '.' && typeof args[1] === 'string' && ctx.schema?.slotVT) {
|
|
301
|
-
const slotVT = ctx.schema.slotVT(args[0], args[1])
|
|
302
|
-
if (slotVT) return slotVT
|
|
303
|
-
}
|
|
304
|
-
// OBJECT `.prop` propagation: when the receiver chain roots at a binding
|
|
305
|
-
// sourced from `JSON.parse(stringConst)`, walk the shape tree to recover the
|
|
306
|
-
// child's val-type. Generic for any compile-time-known JSON literal.
|
|
307
|
-
if (op === '.' && typeof args[1] === 'string') {
|
|
308
|
-
const sh = shapeOf(args[0])
|
|
309
|
-
if (sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) {
|
|
310
|
-
const child = sh.props[args[1]]
|
|
311
|
-
if (child) return child.val
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// Arithmetic expressions: BigInt if either operand is BigInt, else number
|
|
315
|
-
if (['-', 'u-', '*', '/', '%', '&', '|', '^', '<<', '>>'].includes(op)) {
|
|
316
|
-
if (valTypeOf(args[0]) === VAL.BIGINT || valTypeOf(args[1]) === VAL.BIGINT) return VAL.BIGINT
|
|
317
|
-
return VAL.NUMBER
|
|
318
|
-
}
|
|
319
|
-
if (['**', '++', '--', '~', '>>>', 'u+'].includes(op)) return VAL.NUMBER
|
|
320
|
-
if (op === '+') {
|
|
321
|
-
const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
|
|
322
|
-
if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
|
|
323
|
-
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
324
|
-
return VAL.NUMBER
|
|
325
|
-
}
|
|
326
|
-
// Assignment & compound-assign expressions return the rhs value. Without this,
|
|
327
|
-
// `(a = x*x) + (b = y*y)` falls through to null and `+` emits the polymorphic
|
|
328
|
-
// string-concat dispatch on two pure-numeric subexpressions.
|
|
329
|
-
if (op === '=') return valTypeOf(args[1])
|
|
330
|
-
if (op === '+=') {
|
|
331
|
-
const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
|
|
332
|
-
const tb = valTypeOf(args[1])
|
|
333
|
-
if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
|
|
334
|
-
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
335
|
-
return VAL.NUMBER
|
|
336
|
-
}
|
|
337
|
-
if (['-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>='].includes(op)) {
|
|
338
|
-
const ta = typeof args[0] === 'string' ? lookupValType(args[0]) : null
|
|
339
|
-
const tb = valTypeOf(args[1])
|
|
340
|
-
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
341
|
-
return VAL.NUMBER
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (op === '()') {
|
|
345
|
-
const callee = args[0]
|
|
346
|
-
// Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
|
|
347
|
-
if (Array.isArray(callee) && callee[0] === '?') {
|
|
348
|
-
const ta = valTypeOf(callee[2]), tb = valTypeOf(callee[3])
|
|
349
|
-
return ta && ta === tb ? ta : null
|
|
350
|
-
}
|
|
351
|
-
// Constructor results + user function return-type inference
|
|
352
|
-
if (typeof callee === 'string') {
|
|
353
|
-
if (callee === 'new.Set') return VAL.SET
|
|
354
|
-
if (callee === 'new.Map') return VAL.MAP
|
|
355
|
-
if (callee === 'new.Date') return VAL.DATE
|
|
356
|
-
if (callee === 'new.ArrayBuffer') return VAL.BUFFER
|
|
357
|
-
// `new Array(...)` is a plain growable Array, not a TypedArray — index
|
|
358
|
-
// stores must route through __arr_set_idx_ptr (grow + persist), so it
|
|
359
|
-
// must NOT fall into the new.* → VAL.TYPED catch-all below.
|
|
360
|
-
if (callee === 'new.Array') return VAL.ARRAY
|
|
361
|
-
// `new DataView(...)` falls through to VAL.TYPED: a DataView is a proper
|
|
362
|
-
// view object (TYPED|view ptr over a 16-byte descriptor), same shape as a
|
|
363
|
-
// typed-array subview — see module/typedarray.js `new.DataView`.
|
|
364
|
-
if (callee.startsWith('new.')) return VAL.TYPED
|
|
365
|
-
if (callee === 'String.fromCharCode' || callee === 'String') return VAL.STRING
|
|
366
|
-
if (callee === 'Boolean') return VAL.BOOL
|
|
367
|
-
if (callee === 'BigInt' || callee === 'BigInt.asIntN' || callee === 'BigInt.asUintN') return VAL.BIGINT
|
|
368
|
-
if (callee === 'JSON.parse') {
|
|
369
|
-
const src = jsonConstString(args[1])
|
|
370
|
-
if (src != null) {
|
|
371
|
-
const c = src.trimStart()[0]
|
|
372
|
-
// Objects emit as fixed-shape OBJECT (slot-based) — see
|
|
373
|
-
// module/json.js:emitJsonConstValue. The downstream `.prop` reads
|
|
374
|
-
// hit emitSchemaSlotRead via ctx.schema.find, bypassing hash probes.
|
|
375
|
-
if (c === '{') return VAL.OBJECT
|
|
376
|
-
if (c === '[') return VAL.ARRAY
|
|
377
|
-
if (c === '"') return VAL.STRING
|
|
378
|
-
if (c === 't' || c === 'f' || c === '-' || (c >= '0' && c <= '9')) return VAL.NUMBER
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Math.* always returns Number — let `+` skip string-concat dispatch and
|
|
382
|
-
// let exprType propagate i32 for the integer-returning subset.
|
|
383
|
-
if (typeof callee === 'string' && callee.startsWith('math.')) return VAL.NUMBER
|
|
384
|
-
// Clock helpers always return Number — lets `t0 = performance.now()` propagate
|
|
385
|
-
// VAL.NUMBER through subsequent reads, eliding `__to_num` wrappers in arithmetic.
|
|
386
|
-
if (callee === 'performance.now' || callee === 'Date.now') return VAL.NUMBER
|
|
387
|
-
const hostVT = ctx.module.hostImportValTypes?.get(callee)
|
|
388
|
-
if (hostVT) return hostVT
|
|
389
|
-
// User-defined func with monomorphic VAL return (populated in compile.js E2 pass).
|
|
390
|
-
const f = ctx.func.map?.get(callee)
|
|
391
|
-
if (f?.valResult) return f.valResult
|
|
392
|
-
}
|
|
393
|
-
// Method return types
|
|
394
|
-
if (Array.isArray(callee) && callee[0] === '.') {
|
|
395
|
-
const [, obj, method] = callee
|
|
396
|
-
if (method === 'map' || method === 'filter') {
|
|
397
|
-
// Typed-array .map/.filter preserve element type → return VAL.TYPED.
|
|
398
|
-
// Unknown receiver: don't claim (stay null) — runtime-dispatched index handles both.
|
|
399
|
-
const objType = valTypeOf(obj)
|
|
400
|
-
if (objType === VAL.TYPED) return VAL.TYPED
|
|
401
|
-
if (objType === VAL.ARRAY) return VAL.ARRAY
|
|
402
|
-
return null
|
|
403
|
-
}
|
|
404
|
-
if (method === 'push') return VAL.ARRAY
|
|
405
|
-
if ((method === 'shift' || method === 'pop') && typeof obj === 'string') {
|
|
406
|
-
const elemVt = ctx.func.localReps?.get(obj)?.arrayElemValType
|
|
407
|
-
if (elemVt) return elemVt
|
|
408
|
-
}
|
|
409
|
-
if (method === 'add' || method === 'delete') return VAL.SET
|
|
410
|
-
if (method === 'set') return VAL.MAP
|
|
411
|
-
// String-returning methods
|
|
412
|
-
if (['toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'trim', 'trimStart', 'trimEnd',
|
|
413
|
-
'repeat', 'padStart', 'padEnd', 'replace', 'replaceAll', 'charAt', 'substring'].includes(method)) return VAL.STRING
|
|
414
|
-
// `charCodeAt`/`codePointAt` always yield a number (a UTF-16 code unit or
|
|
415
|
-
// NaN for an out-of-range index — both VAL.NUMBER). Without this, the f64
|
|
416
|
-
// OOB-NaN result has an unknown val-type and any reader (`+`, `-`, ...)
|
|
417
|
-
// wraps it in a runtime `__to_num` coercion, pulling the whole number↔
|
|
418
|
-
// string stdlib tree (`__ftoa`/`__str_concat`/…) for nothing.
|
|
419
|
-
if (method === 'charCodeAt' || method === 'codePointAt') return VAL.NUMBER
|
|
420
|
-
if (method === 'split') return VAL.ARRAY
|
|
421
|
-
// slice/concat preserve caller type (string.slice → string, array.slice → array)
|
|
422
|
-
if (method === 'slice' || method === 'concat') {
|
|
423
|
-
const objType = valTypeOf(obj)
|
|
424
|
-
if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
|
|
425
|
-
return null
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return null
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// JSON-shape inference (formerly in src/shape.js) — `shapeOf` + `jsonConstString`
|
|
433
|
-
// are defined further down in this file and exported for module consumers.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
/** Static property-key evaluation for computed member names: folds a node into
|
|
437
|
-
* its constant value (numeric, string, boolean, null) and stringifies it. Returns
|
|
438
|
-
* null if any sub-expression isn't statically known. Handles literals, named
|
|
439
|
-
* string constants, String()/Number() casts, ternaries, short-circuit ops, and
|
|
440
|
-
* unary/binary arithmetic and bit ops. */
|
|
441
|
-
const NO_VALUE = Symbol('no-static-property-key')
|
|
442
|
-
|
|
443
|
-
export function staticPropertyKey(node) {
|
|
444
|
-
const value = staticValue(node)
|
|
445
|
-
return value === NO_VALUE ? null : String(value)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function staticValue(node) {
|
|
449
|
-
if (node === undefined) return undefined
|
|
450
|
-
if (node === null || typeof node === 'number' || typeof node === 'boolean') return node
|
|
451
|
-
if (typeof node === 'string') return ctx.scope.constStrs?.get(node) ?? NO_VALUE
|
|
452
|
-
if (!Array.isArray(node)) return NO_VALUE
|
|
453
|
-
|
|
454
|
-
const [op, ...args] = node
|
|
455
|
-
if (op == null) return args.length ? args[0] : undefined
|
|
456
|
-
if (op === 'str') return args[0]
|
|
457
|
-
if (op === '[]' && args.length === 1) return staticValue(args[0])
|
|
458
|
-
if (op === '()' && args[0] === 'String' && args.length === 2) {
|
|
459
|
-
const value = staticValue(args[1])
|
|
460
|
-
return value === NO_VALUE ? NO_VALUE : String(value)
|
|
461
|
-
}
|
|
462
|
-
if (op === '()' && args[0] === 'Number' && args.length === 2) {
|
|
463
|
-
const value = staticValue(args[1])
|
|
464
|
-
return value === NO_VALUE ? NO_VALUE : Number(value)
|
|
465
|
-
}
|
|
466
|
-
if (op === '?:' || op === '?') {
|
|
467
|
-
const cond = staticValue(args[0])
|
|
468
|
-
return cond === NO_VALUE ? NO_VALUE : staticValue(cond ? args[1] : args[2])
|
|
469
|
-
}
|
|
470
|
-
if (op === '&&' || op === '||') {
|
|
471
|
-
const left = staticValue(args[0])
|
|
472
|
-
if (left === NO_VALUE) return NO_VALUE
|
|
473
|
-
return op === '&&' ? (left ? staticValue(args[1]) : left) : (left ? left : staticValue(args[1]))
|
|
474
|
-
}
|
|
475
|
-
if (op === '??') {
|
|
476
|
-
const left = staticValue(args[0])
|
|
477
|
-
return left === NO_VALUE ? NO_VALUE : left == null ? staticValue(args[1]) : left
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (args.length === 1) {
|
|
481
|
-
const value = staticValue(args[0])
|
|
482
|
-
if (value === NO_VALUE) return NO_VALUE
|
|
483
|
-
// Parser emits raw `-`/`+` for both unary and binary; prep later normalizes
|
|
484
|
-
// unary to `u-`/`u+`, but staticPropertyKey runs on raw parser AST.
|
|
485
|
-
if (op === 'u+' || op === '+') return +value
|
|
486
|
-
if (op === 'u-' || op === '-') return -value
|
|
487
|
-
if (op === '!') return !value
|
|
488
|
-
if (op === '~') return ~value
|
|
489
|
-
return NO_VALUE
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (args.length === 2) {
|
|
493
|
-
const left = staticValue(args[0])
|
|
494
|
-
const right = staticValue(args[1])
|
|
495
|
-
if (left === NO_VALUE || right === NO_VALUE) return NO_VALUE
|
|
496
|
-
switch (op) {
|
|
497
|
-
case '+': return typeof left === 'string' || typeof right === 'string' ? String(left) + String(right) : Number(left) + Number(right)
|
|
498
|
-
case '-': return Number(left) - Number(right)
|
|
499
|
-
case '*': return Number(left) * Number(right)
|
|
500
|
-
case '/': return Number(left) / Number(right)
|
|
501
|
-
case '%': return Number(left) % Number(right)
|
|
502
|
-
case '**': return Number(left) ** Number(right)
|
|
503
|
-
case '&': return Number(left) & Number(right)
|
|
504
|
-
case '|': return Number(left) | Number(right)
|
|
505
|
-
case '^': return Number(left) ^ Number(right)
|
|
506
|
-
case '<<': return Number(left) << Number(right)
|
|
507
|
-
case '>>': return Number(left) >> Number(right)
|
|
508
|
-
case '>>>': return Number(left) >>> Number(right)
|
|
509
|
-
default: return NO_VALUE
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return NO_VALUE
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** Decode a `['{}', ...]` AST's children into `{names, values}`, or null if any
|
|
517
|
-
* property is non-static-key (computed key, spread, shorthand). Matches the
|
|
518
|
-
* emitter's flatten rule for comma-grouped props. Used by collectProgramFacts,
|
|
519
|
-
* narrowSignatures, and objLiteralSchemaId; the emitter (module/object.js)
|
|
520
|
-
* does its own decoding because it must handle the spread/computed-key paths. */
|
|
521
|
-
export function staticObjectProps(args) {
|
|
522
|
-
const raw = args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
|
|
523
|
-
const names = [], values = []
|
|
524
|
-
for (const p of raw) {
|
|
525
|
-
if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
|
|
526
|
-
names.push(p[1]); values.push(p[2])
|
|
527
|
-
}
|
|
528
|
-
return names.length ? { names, values } : null
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function staticArrayElems(expr) {
|
|
532
|
-
if (!Array.isArray(expr)) return null
|
|
533
|
-
if (expr[0] === '[') return expr.slice(1)
|
|
534
|
-
if (expr[0] !== '[]' || expr.length >= 3) return null
|
|
535
|
-
const arg = expr[1]
|
|
536
|
-
if (arg == null) return []
|
|
537
|
-
return Array.isArray(arg) && arg[0] === ',' ? arg.slice(1) : [arg]
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
|
|
541
|
-
export function objLiteralSchemaId(expr) {
|
|
542
|
-
if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
|
|
543
|
-
const parsed = staticObjectProps(expr.slice(1))
|
|
544
|
-
return parsed ? ctx.schema.register(parsed.names) : null
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/** Resolve schemaId of an expression, given a per-function schemaId map for locals.
|
|
548
|
-
* Used for both intra-function arr elem-schema observation and func.arrayElemSchema
|
|
549
|
-
* return inference. Recognizes: object literals, var names with bound schemaId,
|
|
550
|
-
* user fn calls with narrowed result schema, ?: / && / || when both branches agree. */
|
|
551
|
-
function exprSchemaId(expr, localSchemaMap) {
|
|
552
|
-
if (typeof expr === 'string') {
|
|
553
|
-
if (localSchemaMap?.has(expr)) return localSchemaMap.get(expr)
|
|
554
|
-
return ctx.schema?.idOf?.(expr) ?? null
|
|
555
|
-
}
|
|
556
|
-
if (!Array.isArray(expr)) return null
|
|
557
|
-
const op = expr[0]
|
|
558
|
-
if (op === '{}') return objLiteralSchemaId(expr)
|
|
559
|
-
if (op === '()' && typeof expr[1] === 'string') {
|
|
560
|
-
const f = ctx.func.map?.get(expr[1])
|
|
561
|
-
if (f?.valResult === VAL.OBJECT && f.sig?.ptrAux != null) return f.sig.ptrAux
|
|
562
|
-
return null
|
|
563
|
-
}
|
|
564
|
-
if (op === '?:') {
|
|
565
|
-
const a = exprSchemaId(expr[2], localSchemaMap)
|
|
566
|
-
const b = exprSchemaId(expr[3], localSchemaMap)
|
|
567
|
-
return a != null && a === b ? a : null
|
|
568
|
-
}
|
|
569
|
-
if (op === '&&' || op === '||') {
|
|
570
|
-
const a = exprSchemaId(expr[1], localSchemaMap)
|
|
571
|
-
const b = exprSchemaId(expr[2], localSchemaMap)
|
|
572
|
-
return a != null && a === b ? a : null
|
|
573
|
-
}
|
|
574
|
-
return null
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/** Extract typed-array ctor name ('new.Float32Array', 'new.Int8Array.view', etc) from RHS,
|
|
578
|
-
* or null if RHS isn't a typed-array/ArrayBuffer/DataView constructor. */
|
|
579
|
-
export function typedElemCtor(rhs) {
|
|
580
|
-
if (!Array.isArray(rhs) || rhs[0] !== '()' || typeof rhs[1] !== 'string' || !rhs[1].startsWith('new.')) return null
|
|
581
|
-
const args = rhs[2]
|
|
582
|
-
const isView = rhs[1].endsWith('Array') && rhs[1] !== 'new.ArrayBuffer'
|
|
583
|
-
&& Array.isArray(args) && args[0] === ',' && args.length >= 4
|
|
584
|
-
return isView ? rhs[1] + '.view' : rhs[1]
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Element-type byte mapping (mirror of module/typedarray.js ELEM). Bit 3 (|8) marks a view.
|
|
588
|
-
const _ELEM_AUX = {
|
|
589
|
-
Int8Array: 0, Uint8Array: 1, Int16Array: 2, Uint16Array: 3,
|
|
590
|
-
Int32Array: 4, Uint32Array: 5, Float32Array: 6, Float64Array: 7,
|
|
591
|
-
BigInt64Array: 23, BigUint64Array: 23,
|
|
592
|
-
}
|
|
593
|
-
/** Encode a `typedElemCtor` string ('new.Int32Array' | 'new.Int32Array.view') to the 4-bit
|
|
594
|
-
* aux value used in PTR.TYPED NaN-boxing. Returns null for unknown ctors (ArrayBuffer/DataView). */
|
|
595
|
-
export function typedElemAux(ctor) {
|
|
596
|
-
if (!ctor || !ctor.startsWith('new.')) return null
|
|
597
|
-
const isView = ctor.endsWith('.view')
|
|
598
|
-
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
599
|
-
const et = _ELEM_AUX[name]
|
|
600
|
-
if (et == null) return null
|
|
601
|
-
return isView ? et | 8 : et
|
|
602
|
-
}
|
|
603
|
-
const _ELEM_NAMES = ['Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array',
|
|
604
|
-
'Int32Array', 'Uint32Array', 'Float32Array', 'Float64Array']
|
|
605
|
-
/** Reverse of typedElemAux: pick a canonical ctor string for a 4-bit elem aux. Used
|
|
606
|
-
* to round-trip TYPED-narrowed call results through ctx.types.typedElem so the
|
|
607
|
-
* unboxed local's rep picks up the same aux. aux=7 is shared with BigInt typed
|
|
608
|
-
* arrays — Float64Array is canonical (read-side compares aux only). */
|
|
609
|
-
export function ctorFromElemAux(aux) {
|
|
610
|
-
if (aux == null) return null
|
|
611
|
-
const isView = (aux & 8) !== 0
|
|
612
|
-
const name = (aux & 16) !== 0 ? 'BigInt64Array' : _ELEM_NAMES[aux & 7]
|
|
613
|
-
if (!name) return null
|
|
614
|
-
return isView ? `new.${name}.view` : `new.${name}`
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
|
|
618
|
-
* different typed-array ctors — caller should drop any cached entry rather
|
|
619
|
-
* than leave a stale ctor (which would lock the wrong store width). */
|
|
620
|
-
const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
621
|
-
|
|
622
|
-
const typedCtorElemValType = (ctor) => {
|
|
623
|
-
if (!ctor) return null
|
|
624
|
-
const isView = ctor.endsWith('.view')
|
|
625
|
-
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
626
|
-
return name === 'BigInt64Array' || name === 'BigUint64Array' ? VAL.BIGINT : VAL.NUMBER
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** A `?:`/`&&`/`||` expression — value depends on a condition, so its ctor
|
|
630
|
-
* must be derived by walking branches (handled by `ternaryCtorOfRhs`). */
|
|
631
|
-
const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e[0] === '||')
|
|
632
|
-
|
|
633
|
-
/** Walk a `?:`/`&&`/`||` expression and return:
|
|
634
|
-
* - a single ctor string when every branch resolves to the same ctor,
|
|
635
|
-
* - MIXED_CTORS when branches resolve to different ctors,
|
|
636
|
-
* - null when no branch resolves (caller's behavior unchanged). */
|
|
637
|
-
function ternaryCtorOfRhs(rhs) {
|
|
638
|
-
if (!Array.isArray(rhs)) return null
|
|
639
|
-
const op = rhs[0]
|
|
640
|
-
const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
|
|
641
|
-
if (!lo) return null
|
|
642
|
-
const a = ternaryCtorOfRhs(rhs[lo]) ?? typedElemCtor(rhs[lo])
|
|
643
|
-
const b = ternaryCtorOfRhs(rhs[lo + 1]) ?? typedElemCtor(rhs[lo + 1])
|
|
644
|
-
return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Cross-call argument inference helpers (`infer*`) live in src/infer.js —
|
|
648
|
-
// they're the call-site mirror of the body-walk evidence sources and pair
|
|
649
|
-
// naturally with that registry. Consumed by src/narrow.js' signature fixpoint.
|
|
650
|
-
|
|
651
|
-
// Per-body memoization: analyzeBody is a pure function of `body` plus a small
|
|
652
|
-
// set of ctx fields (func.locals, func.localReps, func.map[*][field]). compile.js
|
|
653
|
-
// calls slices of it many times per function (scan-fixpoint, narrowing, final
|
|
654
|
-
// lowering); the unified cache absorbs that traffic. Caller-mutation safety is
|
|
655
|
-
// preserved by cloning every Map on read (entry value stored once, copies handed out).
|
|
656
|
-
// Invalidation: emitFunc calls `invalidateLocalsCache` after seeding cross-call
|
|
657
|
-
// param facts; compile.js' E2 pass calls `invalidateValTypesCache` after valResult
|
|
658
|
-
// narrowing; narrowReturnArrayElems clears entries between fixpoint iters.
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Per-binding use-summary — the substrate the representation analyses share.
|
|
662
|
-
*
|
|
663
|
-
* One traversal classifies every mention of each `let`/`const` binding into a
|
|
664
|
-
* closed taxonomy of use-kinds (`USE.*`). The eligibility analyses below
|
|
665
|
-
* (`scanFlatObjects`, `scanSliceViews`, `unboxablePtrs`) are then *policies* —
|
|
666
|
-
* subset predicates over this summary — rather than three independent body
|
|
667
|
-
* re-walks. The classification logic is the union of those walks; a use-kind
|
|
668
|
-
* exists for every distinction at least one consumer needs.
|
|
669
|
-
*
|
|
670
|
-
* Deliberately NOT a consumer: `analyzeBody`'s `escapes` map. It looks like a
|
|
671
|
-
* fourth escape analysis but is not — it is context-sensitive *taint* over
|
|
672
|
-
* value-expression positions (member access short-circuits, a static index
|
|
673
|
-
* does not escape but a dynamic one does), woven into the main typing walk it
|
|
674
|
-
* shares with six other facts. Re-expressing it here would need `BARE` split
|
|
675
|
-
* by enclosing construct (a plain `name;` statement vs `return name`) and a
|
|
676
|
-
* second traversal — adding a walk, not removing one. It stays where it is.
|
|
677
|
-
*
|
|
678
|
-
* Returns `Map<name, { decls, initRhs, uses }>` for every name the body
|
|
679
|
-
* `let`/`const`-declares. `uses` is a `UseRecord[]`; a record is
|
|
680
|
-
* `{ kind, key?, optional?, computed?, compound?, nullCmp?, callee?, argIndex? }`.
|
|
681
|
-
*
|
|
682
|
-
* Scoping: decls are collected only outside closures (a `let` inside a nested
|
|
683
|
-
* `=>` is a different scope), but uses ARE collected inside closures — every
|
|
684
|
-
* mention there becomes a `CAPTURE`, which every policy treats as
|
|
685
|
-
* disqualifying. A binding shadowed by an inner closure decl is conservatively
|
|
686
|
-
* still flagged `CAPTURE`; sound, since it only ever forfeits an optimization.
|
|
687
|
-
*
|
|
688
|
-
* Order-independent: uses bucket by name, so a mention before its decl is fine.
|
|
689
|
-
*/
|
|
690
|
-
const USE = {
|
|
691
|
-
MEMBER_R: 1, // receiver of a `.`/`?.`/`[]` READ — {key, optional, computed}
|
|
692
|
-
MEMBER_W: 2, // base of a `.`/`[]` WRITE — {key, computed, compound}
|
|
693
|
-
REASSIGN: 3, // `=`(non-init) / `++` / `--` / compound-assign of the name
|
|
694
|
-
CALL_ARG: 4, // passed as a call argument — {callee, argIndex}
|
|
695
|
-
CALL_CALLEE: 5, // invoked: `name(...)`
|
|
696
|
-
RETURN: 6, // `return name`
|
|
697
|
-
CAPTURE: 7, // mentioned inside a nested `=>`
|
|
698
|
-
COMPARE: 8, // operand of a comparison — {nullCmp}
|
|
699
|
-
CONCAT: 9, // operand of `+`
|
|
700
|
-
BOOL_TEST: 10, // operand of `!`/`typeof`/`void`, or an `if`/`while`/`?:` test
|
|
701
|
-
DELETE_MEMBER: 11, // `delete name.member`
|
|
702
|
-
BARE: 12, // any other value position — the conservative catch-all
|
|
703
|
-
}
|
|
704
|
-
const _bindingUsesCache = new WeakMap()
|
|
705
|
-
const _CMP_OPS = new Set(['==', '!=', '===', '!==', '<', '>', '<=', '>='])
|
|
706
|
-
const _isNullishLit = (e) =>
|
|
707
|
-
e === 'null' || e === 'undefined' ||
|
|
708
|
-
(Array.isArray(e) && e[0] == null && (e[1] === null || e[1] === undefined))
|
|
709
|
-
|
|
710
|
-
function scanBindingUses(body) {
|
|
711
|
-
const hit = _bindingUsesCache.get(body)
|
|
712
|
-
if (hit) return hit
|
|
713
|
-
|
|
714
|
-
const summary = new Map() // name → { decls, initRhs, uses }
|
|
715
|
-
const slot = (name) => {
|
|
716
|
-
let s = summary.get(name)
|
|
717
|
-
if (!s) { s = { decls: 0, initRhs: undefined, uses: [] }; summary.set(name, s) }
|
|
718
|
-
return s
|
|
719
|
-
}
|
|
720
|
-
const use = (name, kind, extra) => slot(name).uses.push(extra ? { kind, ...extra } : { kind })
|
|
721
|
-
|
|
722
|
-
// Static string key of a `[]` index node, else null (computed).
|
|
723
|
-
const litKey = (k) => (Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string') ? k[1] : null
|
|
724
|
-
|
|
725
|
-
// A child sitting in a value position. A bare string there is a real use —
|
|
726
|
-
// `walk` alone silently drops non-array children, so every value-position
|
|
727
|
-
// child (let-rhs, assign-rhs, call/index args, closure body, …) must route
|
|
728
|
-
// through here or its use goes unrecorded (a latent miscompile: the binding
|
|
729
|
-
// looks unused and an optimization fires unsoundly).
|
|
730
|
-
const val = (child, inClosure) => {
|
|
731
|
-
if (typeof child === 'string') use(child, inClosure ? USE.CAPTURE : USE.BARE)
|
|
732
|
-
else walk(child, inClosure)
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Classify the target of an assignment-like node (`=`, compound, `++`, `--`).
|
|
736
|
-
const assignTarget = (t, compound) => {
|
|
737
|
-
if (typeof t === 'string') { use(t, USE.REASSIGN); return }
|
|
738
|
-
if (!Array.isArray(t)) return
|
|
739
|
-
const o = t[0]
|
|
740
|
-
if ((o === '.' || o === '?.') && typeof t[1] === 'string') {
|
|
741
|
-
use(t[1], USE.MEMBER_W, { key: typeof t[2] === 'string' ? t[2] : null, computed: false, compound })
|
|
742
|
-
return
|
|
743
|
-
}
|
|
744
|
-
if (o === '[]' && typeof t[1] === 'string') {
|
|
745
|
-
const k = litKey(t[2])
|
|
746
|
-
use(t[1], USE.MEMBER_W, { key: k, computed: k == null, compound })
|
|
747
|
-
if (t[2] != null) val(t[2])
|
|
748
|
-
return
|
|
749
|
-
}
|
|
750
|
-
walk(t) // some other LHS shape — generic
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function walk(node, inClosure) {
|
|
754
|
-
if (!Array.isArray(node)) return
|
|
755
|
-
const op = node[0]
|
|
756
|
-
if (typeof op !== 'string') return // literal node `[null, value]`
|
|
757
|
-
if (op === 'str') return // string literal
|
|
758
|
-
if (op === '=>') { for (let i = 1; i < node.length; i++) val(node[i], true); return }
|
|
759
|
-
|
|
760
|
-
if (op === 'let' || op === 'const') {
|
|
761
|
-
for (let i = 1; i < node.length; i++) {
|
|
762
|
-
const d = node[i]
|
|
763
|
-
if (typeof d === 'string') { if (!inClosure) slot(d).decls++; continue }
|
|
764
|
-
if (Array.isArray(d) && d[0] === '=') {
|
|
765
|
-
const lhs = d[1], rhs = d[2]
|
|
766
|
-
if (typeof lhs === 'string') {
|
|
767
|
-
if (!inClosure) { const s = slot(lhs); s.decls++; if (s.initRhs === undefined) s.initRhs = rhs }
|
|
768
|
-
} else {
|
|
769
|
-
walk(lhs, inClosure) // pattern — computed keys/defaults are real uses
|
|
770
|
-
}
|
|
771
|
-
val(rhs, inClosure)
|
|
772
|
-
} else walk(d, inClosure)
|
|
773
|
-
}
|
|
774
|
-
return
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (inClosure) { // every mention here is a CAPTURE
|
|
778
|
-
for (let i = 1; i < node.length; i++) {
|
|
779
|
-
const c = node[i]
|
|
780
|
-
if (typeof c === 'string') use(c, USE.CAPTURE)
|
|
781
|
-
else walk(c, true)
|
|
782
|
-
}
|
|
783
|
-
return
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// === precise classification (outside any closure) ===
|
|
787
|
-
if (ASSIGN_OPS.has(op)) { assignTarget(node[1], op !== '='); val(node[2]); return }
|
|
788
|
-
if (op === '++' || op === '--') { assignTarget(node[1], true); return }
|
|
789
|
-
if (op === 'delete') {
|
|
790
|
-
const t = node[1]
|
|
791
|
-
if (Array.isArray(t) && (t[0] === '.' || t[0] === '?.' || t[0] === '[]') && typeof t[1] === 'string') {
|
|
792
|
-
use(t[1], USE.DELETE_MEMBER)
|
|
793
|
-
if (t[0] === '[]' && t[2] != null) val(t[2])
|
|
794
|
-
} else val(t)
|
|
795
|
-
return
|
|
796
|
-
}
|
|
797
|
-
if (op === '.' || op === '?.') {
|
|
798
|
-
const recv = node[1]
|
|
799
|
-
if (typeof recv === 'string')
|
|
800
|
-
use(recv, USE.MEMBER_R, { key: typeof node[2] === 'string' ? node[2] : null, optional: op === '?.', computed: false })
|
|
801
|
-
else walk(recv)
|
|
802
|
-
return // node[2] is the property name
|
|
803
|
-
}
|
|
804
|
-
if (op === '[]') {
|
|
805
|
-
const recv = node[1], k = litKey(node[2])
|
|
806
|
-
if (typeof recv === 'string') use(recv, USE.MEMBER_R, { key: k, optional: false, computed: k == null })
|
|
807
|
-
else walk(recv)
|
|
808
|
-
if (node[2] != null) val(node[2])
|
|
809
|
-
return
|
|
810
|
-
}
|
|
811
|
-
if (op === ':') { // object property `{k:v}` / labeled statement
|
|
812
|
-
if (Array.isArray(node[1])) walk(node[1]) // computed key `{[expr]:v}` — a real use
|
|
813
|
-
val(node[2]) // property value (or the labeled statement)
|
|
814
|
-
return // string node[1] = plain key / label — not a use
|
|
815
|
-
}
|
|
816
|
-
if (op === 'return') {
|
|
817
|
-
const e = node[1]
|
|
818
|
-
if (typeof e === 'string') use(e, USE.RETURN)
|
|
819
|
-
else walk(e)
|
|
820
|
-
return
|
|
821
|
-
}
|
|
822
|
-
if (op === '()') {
|
|
823
|
-
const callee = node[1]
|
|
824
|
-
if (typeof callee === 'string') use(callee, USE.CALL_CALLEE)
|
|
825
|
-
else walk(callee)
|
|
826
|
-
const argNode = node[2]
|
|
827
|
-
if (argNode != null) {
|
|
828
|
-
const args = (Array.isArray(argNode) && argNode[0] === ',') ? argNode.slice(1) : [argNode]
|
|
829
|
-
for (let ai = 0; ai < args.length; ai++) {
|
|
830
|
-
const a = args[ai]
|
|
831
|
-
if (Array.isArray(a) && a[0] === '...') { val(a[1]); continue }
|
|
832
|
-
if (typeof a === 'string') use(a, USE.CALL_ARG, { callee: typeof callee === 'string' ? callee : null, argIndex: ai })
|
|
833
|
-
else walk(a)
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
return
|
|
837
|
-
}
|
|
838
|
-
if (_CMP_OPS.has(op) && node.length === 3) {
|
|
839
|
-
for (let i = 1; i <= 2; i++) {
|
|
840
|
-
const side = node[i]
|
|
841
|
-
if (typeof side === 'string') use(side, USE.COMPARE, { nullCmp: _isNullishLit(node[3 - i]) })
|
|
842
|
-
else walk(side)
|
|
843
|
-
}
|
|
844
|
-
return
|
|
845
|
-
}
|
|
846
|
-
if (op === '+') {
|
|
847
|
-
for (let i = 1; i < node.length; i++) {
|
|
848
|
-
const c = node[i]
|
|
849
|
-
if (typeof c === 'string') use(c, USE.CONCAT)
|
|
850
|
-
else walk(c)
|
|
851
|
-
}
|
|
852
|
-
return
|
|
853
|
-
}
|
|
854
|
-
if (op === '!' || op === 'typeof' || op === 'void') {
|
|
855
|
-
const c = node[1]
|
|
856
|
-
if (typeof c === 'string') use(c, USE.BOOL_TEST)
|
|
857
|
-
else walk(c)
|
|
858
|
-
return
|
|
859
|
-
}
|
|
860
|
-
if (op === 'if' || op === 'while' || op === '?:') { // `prepare` normalizes `?` → `?:`
|
|
861
|
-
const c = node[1]
|
|
862
|
-
if (typeof c === 'string') use(c, USE.BOOL_TEST)
|
|
863
|
-
else walk(c)
|
|
864
|
-
for (let i = 2; i < node.length; i++) val(node[i])
|
|
865
|
-
return
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// generic — every string child is a BARE value use
|
|
869
|
-
for (let i = 1; i < node.length; i++) {
|
|
870
|
-
const c = node[i]
|
|
871
|
-
if (typeof c === 'string') use(c, USE.BARE)
|
|
872
|
-
else walk(c)
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
walk(body, false)
|
|
877
|
-
|
|
878
|
-
for (const [name, s] of summary) if (s.decls === 0) summary.delete(name)
|
|
879
|
-
_bindingUsesCache.set(body, summary)
|
|
880
|
-
return summary
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
/**
|
|
884
|
-
* SRoA eligibility scan — which `let/const o = {staticLiteral}` bindings can
|
|
885
|
-
* have their fields dissolved into plain WASM locals (`flat` carrier): no heap
|
|
886
|
-
* alloc, no field load/store, `o.prop` becomes `local.get`.
|
|
887
|
-
*
|
|
888
|
-
* A binding is flat-eligible iff `o` appears ONLY as a literal-key `.`/`[]`
|
|
889
|
-
* READ of an in-schema prop, or the member LHS of a literal-key `.`/`[]` WRITE
|
|
890
|
-
* of an in-schema prop. Any other mention — bare ref, dynamic/numeric key,
|
|
891
|
-
* off-schema prop, `?.`, reassignment, compound assign, `++`/`--`, `delete`,
|
|
892
|
-
* closure capture, self-referential initializer, duplicate keys, or a second
|
|
893
|
-
* declaration — disqualifies it. A non-escaping object is never observed by
|
|
894
|
-
* any object walk (keys/values/entries/assign/spread/JSON/for-in/dyn), so the
|
|
895
|
-
* transform is additive and sound. Conservative: any doubt → not flat.
|
|
896
|
-
*
|
|
897
|
-
* A policy over `scanBindingUses`: the shared traversal classifies every
|
|
898
|
-
* mention; this scan keeps a binding only if its initializer is a self-
|
|
899
|
-
* contained static literal and every use is an in-schema literal-key access.
|
|
900
|
-
*
|
|
901
|
-
* Returns `Map<name, {names, values}>` — the literal's parallel prop arrays.
|
|
902
|
-
* Field `i` of binding `o` lives in WASM local `o#${i}` (`#` cannot occur in a
|
|
903
|
-
* jz identifier, so the name is collision-free).
|
|
904
|
-
*/
|
|
905
|
-
function scanFlatObjects(body) {
|
|
906
|
-
const cand = new Map() // name → {names, values}
|
|
907
|
-
|
|
908
|
-
// A binding referenced as a value inside `node` (skips `:`/`.` property-name
|
|
909
|
-
// slots). Used only to reject a self-referential initializer — a literal
|
|
910
|
-
// whose own field values mention the binding is not a self-contained object.
|
|
911
|
-
const referencesName = (node, name) => {
|
|
912
|
-
if (typeof node === 'string') return node === name
|
|
913
|
-
if (!Array.isArray(node)) return false
|
|
914
|
-
const op = node[0]
|
|
915
|
-
if (op === 'str') return false
|
|
916
|
-
if (op === ':') return referencesName(node[2], name)
|
|
917
|
-
if (op === '.' || op === '?.') return referencesName(node[1], name)
|
|
918
|
-
for (let i = 1; i < node.length; i++) if (referencesName(node[i], name)) return true
|
|
919
|
-
return false
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
for (const [name, s] of scanBindingUses(body)) {
|
|
923
|
-
// Candidate: exactly one `let/const name = {staticLiteral}` decl, unique
|
|
924
|
-
// keys, and an initializer that does not reference the binding itself.
|
|
925
|
-
if (s.decls !== 1 || !Array.isArray(s.initRhs) || s.initRhs[0] !== '{}') continue
|
|
926
|
-
const props = staticObjectProps(s.initRhs.slice(1))
|
|
927
|
-
if (!props || new Set(props.names).size !== props.names.length) continue
|
|
928
|
-
if (props.values.some(v => referencesName(v, name))) continue
|
|
929
|
-
|
|
930
|
-
// Schema = literal keys ∪ plain literal-key member writes. Such a write
|
|
931
|
-
// monotonically extends the static field universe (the new field reads
|
|
932
|
-
// `undefined` until the write runs, exactly as JS does); the schema stays
|
|
933
|
-
// closed because any computed/off-schema access disqualifies below.
|
|
934
|
-
const schema = new Set(props.names)
|
|
935
|
-
for (const u of s.uses)
|
|
936
|
-
if (u.kind === USE.MEMBER_W && !u.compound && !u.computed && u.key != null)
|
|
937
|
-
schema.add(u.key)
|
|
938
|
-
|
|
939
|
-
// Flat iff every mention is an in-schema literal-key `.`/`[]` READ, or an
|
|
940
|
-
// in-schema literal-key plain `.`/`[]` WRITE. Any other use kind — `?.`,
|
|
941
|
-
// computed/off-schema key, reassignment, compound or `delete` member write,
|
|
942
|
-
// `++`/`--`, call arg, closure capture, bare ref — leaves the object live.
|
|
943
|
-
const flat = s.uses.every(u =>
|
|
944
|
-
(u.kind === USE.MEMBER_R && !u.optional && !u.computed && schema.has(u.key)) ||
|
|
945
|
-
(u.kind === USE.MEMBER_W && !u.compound && !u.computed && schema.has(u.key)))
|
|
946
|
-
if (!flat) continue
|
|
947
|
-
|
|
948
|
-
// Materialize the parallel {names, values}: literal props first, then each
|
|
949
|
-
// extension field (value `undefined`), in first-write order.
|
|
950
|
-
const names = props.names.slice(), values = props.values.slice()
|
|
951
|
-
for (const k of schema)
|
|
952
|
-
if (!names.includes(k)) { names.push(k); values.push(undefined) }
|
|
953
|
-
cand.set(name, { names, values })
|
|
954
|
-
}
|
|
955
|
-
return cand
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* No-copy slice scan — which `let/const t = s.slice(...)` bindings can be a
|
|
960
|
-
* VIEW (a SLICE_BIT pointer straight into `s`'s buffer) instead of a fresh
|
|
961
|
-
* byte copy.
|
|
962
|
-
*
|
|
963
|
-
* jz rewinds the bump arena only at function exit, so every string the
|
|
964
|
-
* function can observe stays alive until it returns. A view is therefore sound
|
|
965
|
-
* exactly when its binding does NOT escape the function: `t` must never be
|
|
966
|
-
* returned, passed as a call argument, stored into a heap object/array,
|
|
967
|
-
* captured by a closure, aliased to another binding, reassigned, or
|
|
968
|
-
* compound-assigned. The permitted uses — receiver of a `.`/`[]`, operand of a
|
|
969
|
-
* comparison or `+`, a boolean test — read `t` synchronously and never persist
|
|
970
|
-
* it past the function.
|
|
971
|
-
*
|
|
972
|
-
* Declared exactly once as `let/const`. The result is purely structural —
|
|
973
|
-
* whether the receiver is actually a string (so `.slice` lowers to the string
|
|
974
|
-
* view) is settled later, at emit time, when param types are known; emitDecl
|
|
975
|
-
* keeps the ordinary copying slice for any non-string receiver. Conservative:
|
|
976
|
-
* any unrecognised position disqualifies the binding.
|
|
977
|
-
*
|
|
978
|
-
* Returns `Set<name>` of view-eligible binding names.
|
|
979
|
-
*/
|
|
980
|
-
// Permitted use-kinds for a slice view — the value is read synchronously and
|
|
981
|
-
// never persisted past the function. `MEMBER_R`/`MEMBER_W` cover any `.`/`[]`
|
|
982
|
-
// receiver; `COMPARE` any comparison; `CONCAT`/`BOOL_TEST` the copy / test
|
|
983
|
-
// positions. Any other kind (reassign, call arg, return, capture, bare alias)
|
|
984
|
-
// escapes and disqualifies the binding.
|
|
985
|
-
const _SLICE_VIEW_OK = new Set([USE.MEMBER_R, USE.MEMBER_W, USE.COMPARE, USE.CONCAT, USE.BOOL_TEST])
|
|
986
|
-
|
|
987
|
-
function scanSliceViews(body) {
|
|
988
|
-
const isSliceCall = (n) =>
|
|
989
|
-
Array.isArray(n) && n[0] === '()' && Array.isArray(n[1])
|
|
990
|
-
&& n[1][0] === '.' && n[1][2] === 'slice'
|
|
991
|
-
|
|
992
|
-
const views = new Set()
|
|
993
|
-
for (const [name, s] of scanBindingUses(body)) {
|
|
994
|
-
if (s.decls !== 1 || !isSliceCall(s.initRhs)) continue
|
|
995
|
-
if (s.uses.every(u => _SLICE_VIEW_OK.has(u.kind))) views.add(name)
|
|
996
|
-
}
|
|
997
|
-
return views
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* Unified per-body analysis. Single AST traversal producing every per-binding
|
|
1002
|
-
* fact the emitter needs:
|
|
1003
|
-
*
|
|
1004
|
-
* {
|
|
1005
|
-
* locals: Map<name, 'i32'|'f64'> // wasm type per local
|
|
1006
|
-
* valTypes: Map<name, VAL.*> // value-type for dispatch
|
|
1007
|
-
* arrElemSchemas: Map<name, schemaId|null> // Array<schema> facts
|
|
1008
|
-
* arrElemValTypes: Map<name, VAL.*|null> // Array<val-kind> facts
|
|
1009
|
-
* typedElems: Map<name, ctorString> // typed-array ctor binding
|
|
1010
|
-
* }
|
|
1011
|
-
*
|
|
1012
|
-
* Recursion shape: after a `let`/`const` decl, the rhs is walked but the `=`
|
|
1013
|
-
* node itself is skipped — arrElemSchemas/ValTypes have a reassignment
|
|
1014
|
-
* invalidation rule that would misfire on init. Other slices' `=`-visit is
|
|
1015
|
-
* idempotent with the decl handler, so skipping it is safe for them too.
|
|
1016
|
-
*
|
|
1017
|
-
* Forward-only observation order: every rule reads only state already produced
|
|
1018
|
-
* earlier in the same walk (alias chains, push observations, etc.), so a single
|
|
1019
|
-
* traversal is sound.
|
|
1020
|
-
*
|
|
1021
|
-
* After the walk a `widenPass` runs to widen `i32` locals compared against `f64`
|
|
1022
|
-
* operands.
|
|
1023
|
-
*
|
|
1024
|
-
* Caching: body-keyed via `_bodyFactsCache`. See
|
|
1025
|
-
* `invalidateLocalsCache` / `invalidateValTypesCache` for the invalidation hooks.
|
|
1026
|
-
*/
|
|
1027
|
-
const _bodyFactsCache = new WeakMap()
|
|
1028
|
-
|
|
1029
|
-
/**
|
|
1030
|
-
* Narrow uint32 accumulator locals to unsigned i32. A local qualifies when its
|
|
1031
|
-
* initializer is a non-negative integer literal in [0, 2^32), every
|
|
1032
|
-
* reassignment is `name = (…) >>> k` (so it always holds a canonical uint32),
|
|
1033
|
-
* and every read sits inside a `>>>` (ToUint32) sink reached only through
|
|
1034
|
-
* bit-faithful operators (`^ & | ~ << >> + - *`). Under those constraints the
|
|
1035
|
-
* raw i32 bit pattern reproduces JS semantics exactly — every observable use is
|
|
1036
|
-
* funnelled through ToUint32 — so the f64 round-trip on the hot path is pure
|
|
1037
|
-
* overhead. Names that escape (closures, bare `return`, signed-sensitive
|
|
1038
|
-
* operands) keep their wider type. Returns the qualifying set; callers retype
|
|
1039
|
-
* `locals` to 'i32' and tag `readVar` reads `.unsigned` for convert_i32_u.
|
|
1040
|
-
*/
|
|
1041
|
-
function narrowUint32(body, locals) {
|
|
1042
|
-
const TRANSPARENT = new Set(['^', '&', '|', '~', '<<', '>>', '+', '-', '*'])
|
|
1043
|
-
const initLit = new Set() // names with a valid u32-literal initializer
|
|
1044
|
-
const disq = new Set() // names disqualified by an unsafe occurrence
|
|
1045
|
-
const seen = new Set()
|
|
1046
|
-
const isU32Lit = e => {
|
|
1047
|
-
const v = typeof e === 'number' ? e
|
|
1048
|
-
: Array.isArray(e) && e[0] == null && typeof e[1] === 'number' ? e[1] : NaN
|
|
1049
|
-
return Number.isInteger(v) && v >= 0 && v < 4294967296
|
|
1050
|
-
}
|
|
1051
|
-
const isAssignOp = op => op[op.length - 1] === '=' &&
|
|
1052
|
-
op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='
|
|
1053
|
-
const banNames = n => {
|
|
1054
|
-
if (typeof n === 'string') disq.add(n)
|
|
1055
|
-
else if (Array.isArray(n)) for (let i = 1; i < n.length; i++) banNames(n[i])
|
|
1056
|
-
}
|
|
1057
|
-
const walk = (node, underShr, inClosure) => {
|
|
1058
|
-
if (typeof node === 'string') { if (inClosure) disq.add(node); return }
|
|
1059
|
-
if (!Array.isArray(node)) return
|
|
1060
|
-
const op = node[0]
|
|
1061
|
-
if (typeof op !== 'string') {
|
|
1062
|
-
for (let i = 1; i < node.length; i++) walk(node[i], false, inClosure)
|
|
1063
|
-
return
|
|
1064
|
-
}
|
|
1065
|
-
if (op === '=>') { for (let i = 1; i < node.length; i++) walk(node[i], false, true); return }
|
|
1066
|
-
if (op === 'let' || op === 'const') {
|
|
1067
|
-
for (let i = 1; i < node.length; i++) {
|
|
1068
|
-
const d = node[i]
|
|
1069
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
|
|
1070
|
-
const nm = d[1]
|
|
1071
|
-
if (seen.has(nm) || inClosure || !isU32Lit(d[2])) disq.add(nm)
|
|
1072
|
-
else initLit.add(nm)
|
|
1073
|
-
seen.add(nm)
|
|
1074
|
-
walk(d[2], false, inClosure)
|
|
1075
|
-
} else if (typeof d === 'string') { disq.add(d); seen.add(d) }
|
|
1076
|
-
else if (Array.isArray(d) && d[0] === '=') { banNames(d[1]); walk(d[2], false, inClosure) }
|
|
1077
|
-
}
|
|
1078
|
-
return
|
|
1079
|
-
}
|
|
1080
|
-
if ((op === '++' || op === '--') && typeof node[1] === 'string') { disq.add(node[1]); return }
|
|
1081
|
-
if (isAssignOp(op)) {
|
|
1082
|
-
const lhs = node[1]
|
|
1083
|
-
if (typeof lhs === 'string') {
|
|
1084
|
-
if (op !== '=' || inClosure || !(Array.isArray(node[2]) && node[2][0] === '>>>')) disq.add(lhs)
|
|
1085
|
-
} else banNames(lhs)
|
|
1086
|
-
walk(node[2], false, inClosure)
|
|
1087
|
-
return
|
|
1088
|
-
}
|
|
1089
|
-
const childShr = op === '>>>' ? true : TRANSPARENT.has(op) ? underShr : false
|
|
1090
|
-
for (let i = 1; i < node.length; i++) {
|
|
1091
|
-
const c = node[i]
|
|
1092
|
-
if (typeof c === 'string') { if (inClosure || !childShr) disq.add(c) }
|
|
1093
|
-
else walk(c, childShr, inClosure)
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
walk(body, false, false)
|
|
1097
|
-
const result = new Set()
|
|
1098
|
-
for (const nm of initLit) {
|
|
1099
|
-
if (disq.has(nm)) continue
|
|
1100
|
-
const t = locals.get(nm)
|
|
1101
|
-
if (t !== 'i32' && t !== 'f64') continue
|
|
1102
|
-
locals.set(nm, 'i32')
|
|
1103
|
-
result.add(nm)
|
|
1104
|
-
}
|
|
1105
|
-
return result
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* Returns the cached facts object directly — DO NOT MUTATE the returned maps.
|
|
1110
|
-
* Callers that need to extend (e.g. add params to locals) must clone explicitly
|
|
1111
|
-
* before mutating. Slice reads via `analyzeBody(body).<slice>`.
|
|
1112
|
-
*/
|
|
1113
|
-
export function analyzeBody(body) {
|
|
1114
|
-
// Non-object bodies (`() => 0`, `() => x`, missing) have nothing to observe
|
|
1115
|
-
// for any slice and can't be WeakMap-keyed. Return empty maps without caching.
|
|
1116
|
-
if (body === null || typeof body !== 'object') return {
|
|
1117
|
-
locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
|
|
1118
|
-
arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
|
|
1119
|
-
flatObjects: new Map(),
|
|
1120
|
-
}
|
|
1121
|
-
const hit = _bodyFactsCache.get(body)
|
|
1122
|
-
if (hit) return hit
|
|
1123
|
-
|
|
1124
|
-
const locals = new Map()
|
|
1125
|
-
const valTypes = new Map()
|
|
1126
|
-
const arrElemSchemas = new Map()
|
|
1127
|
-
const arrElemValTypes = new Map()
|
|
1128
|
-
const typedElems = new Map()
|
|
1129
|
-
const escapes = new Map() // name → bool: local holds allocation, true if it escapes
|
|
1130
|
-
const valPoison = new Set()
|
|
1131
|
-
|
|
1132
|
-
const doSchemas = !!ctx.schema?.register
|
|
1133
|
-
// Per-walk local schema map for chained `arr.push(name)` resolution.
|
|
1134
|
-
const localSchemaMap = new Map()
|
|
1135
|
-
|
|
1136
|
-
// === Observation helpers ===
|
|
1137
|
-
//
|
|
1138
|
-
// These trust the AST: any `arr.push(...)` syntactically present has `arr` as
|
|
1139
|
-
// a body-relevant name (decl, param, or global) since closure boundaries are
|
|
1140
|
-
// skipped at walk time. Pure typo names produce harmless dead Map entries
|
|
1141
|
-
// that are never queried (consumers index by known local/param names).
|
|
1142
|
-
// Removing the legacy `ctx.func.locals.has(arr)` filter makes analyzeBody's
|
|
1143
|
-
// output context-pure — cache hits don't depend on transient ctx state.
|
|
1144
|
-
|
|
1145
|
-
const observeArrSchema = (arr, sid) => {
|
|
1146
|
-
if (!doSchemas) return
|
|
1147
|
-
if (typeof arr !== 'string') return
|
|
1148
|
-
if (arrElemSchemas.get(arr) === null) return
|
|
1149
|
-
if (sid == null) { arrElemSchemas.set(arr, null); return }
|
|
1150
|
-
if (!arrElemSchemas.has(arr)) arrElemSchemas.set(arr, sid)
|
|
1151
|
-
else if (arrElemSchemas.get(arr) !== sid) arrElemSchemas.set(arr, null)
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const observeArrValType = (arr, vt) => {
|
|
1155
|
-
if (typeof arr !== 'string') return
|
|
1156
|
-
if (arrElemValTypes.get(arr) === null) return
|
|
1157
|
-
if (!vt) { arrElemValTypes.set(arr, null); return }
|
|
1158
|
-
if (!arrElemValTypes.has(arr)) arrElemValTypes.set(arr, vt)
|
|
1159
|
-
else if (arrElemValTypes.get(arr) !== vt) arrElemValTypes.set(arr, null)
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const elemValOf = (name) => {
|
|
1163
|
-
if (typeof name !== 'string') return null
|
|
1164
|
-
const repVt = ctx.func.localReps?.get(name)?.arrayElemValType
|
|
1165
|
-
if (repVt) return repVt
|
|
1166
|
-
return arrElemValTypes.get(name) || null
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const exprElemSourceVal = (expr) => {
|
|
1170
|
-
if (typeof expr === 'string') {
|
|
1171
|
-
const repVt = ctx.func.localReps?.get(expr)?.val
|
|
1172
|
-
if (repVt) return repVt
|
|
1173
|
-
return ctx.scope.globalValTypes?.get(expr) || null
|
|
1174
|
-
}
|
|
1175
|
-
return valTypeOf(expr)
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const typedPoison = new Set()
|
|
1179
|
-
const trackVal = (name, vt) => {
|
|
1180
|
-
if (valPoison.has(name)) return
|
|
1181
|
-
const prev = valTypes.get(name)
|
|
1182
|
-
if (!vt) {
|
|
1183
|
-
if (prev) valPoison.add(name)
|
|
1184
|
-
valTypes.delete(name)
|
|
1185
|
-
return
|
|
1186
|
-
}
|
|
1187
|
-
if (prev && prev !== vt) {
|
|
1188
|
-
valPoison.add(name)
|
|
1189
|
-
valTypes.delete(name)
|
|
1190
|
-
return
|
|
1191
|
-
}
|
|
1192
|
-
valTypes.set(name, vt)
|
|
1193
|
-
}
|
|
1194
|
-
const invalidateTyped = (name) => {
|
|
1195
|
-
typedPoison.add(name)
|
|
1196
|
-
typedElems.delete(name)
|
|
1197
|
-
}
|
|
1198
|
-
const trackTyped = (name, rhs) => {
|
|
1199
|
-
if (typedPoison.has(name)) return
|
|
1200
|
-
const setOrInvalidate = (c) => {
|
|
1201
|
-
if (c === MIXED_CTORS) return invalidateTyped(name)
|
|
1202
|
-
const prev = typedElems.get(name)
|
|
1203
|
-
if (prev && prev !== c) invalidateTyped(name)
|
|
1204
|
-
else typedElems.set(name, c)
|
|
1205
|
-
}
|
|
1206
|
-
const ctor = typedElemCtor(rhs)
|
|
1207
|
-
if (ctor) return setOrInvalidate(ctor)
|
|
1208
|
-
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
1209
|
-
const f = ctx.func.map?.get(rhs[1])
|
|
1210
|
-
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
1211
|
-
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
1212
|
-
if (c) setOrInvalidate(c)
|
|
1213
|
-
}
|
|
1214
|
-
return
|
|
1215
|
-
}
|
|
1216
|
-
// Heterogeneous ternary: ctors that don't unify must invalidate so a sibling
|
|
1217
|
-
// decl (jz hoists `let` to function scope) can't lock in the wrong width.
|
|
1218
|
-
const tc = ternaryCtorOfRhs(rhs)
|
|
1219
|
-
if (tc) setOrInvalidate(tc)
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
|
|
1223
|
-
const processDecl = (name, rhs) => {
|
|
1224
|
-
// wasm type (locals slice)
|
|
1225
|
-
const wt = exprType(rhs, locals)
|
|
1226
|
-
if (!locals.has(name)) locals.set(name, wt)
|
|
1227
|
-
else if (locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
1228
|
-
|
|
1229
|
-
// val type (valTypes slice)
|
|
1230
|
-
trackVal(name, valTypeOf(rhs))
|
|
1231
|
-
|
|
1232
|
-
// typed-array element ctor (typedElems slice)
|
|
1233
|
-
trackTyped(name, rhs)
|
|
1234
|
-
|
|
1235
|
-
// arr-elem schema (arrElemSchemas slice) — schema bindings + array-literal init + alias + call return
|
|
1236
|
-
if (doSchemas) {
|
|
1237
|
-
const sid = exprSchemaId(rhs, localSchemaMap)
|
|
1238
|
-
if (sid != null) localSchemaMap.set(name, sid)
|
|
1239
|
-
{
|
|
1240
|
-
const rawElems = staticArrayElems(rhs)
|
|
1241
|
-
if (rawElems) {
|
|
1242
|
-
const elems = rawElems.filter(e => e != null)
|
|
1243
|
-
if (elems.length && elems.length === rawElems.length) {
|
|
1244
|
-
let common = exprSchemaId(elems[0], localSchemaMap)
|
|
1245
|
-
for (let k = 1; k < elems.length && common != null; k++) {
|
|
1246
|
-
if (exprSchemaId(elems[k], localSchemaMap) !== common) common = null
|
|
1247
|
-
}
|
|
1248
|
-
if (common != null) observeArrSchema(name, common)
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
1253
|
-
const f = ctx.func.map?.get(rhs[1])
|
|
1254
|
-
if (f?.arrayElemSchema != null) observeArrSchema(name, f.arrayElemSchema)
|
|
1255
|
-
}
|
|
1256
|
-
if (typeof rhs === 'string' && arrElemSchemas.has(rhs)) {
|
|
1257
|
-
const sid2 = arrElemSchemas.get(rhs)
|
|
1258
|
-
if (sid2 != null) observeArrSchema(name, sid2)
|
|
1259
|
-
}
|
|
1260
|
-
if (typeof rhs === 'string') {
|
|
1261
|
-
const repSid = ctx.func.localReps?.get(rhs)?.arrayElemSchema
|
|
1262
|
-
if (repSid != null) observeArrSchema(name, repSid)
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
// arr-elem val type (arrElemValTypes slice) — array-literal init + call return + alias + .map/.filter/.slice/.concat chain
|
|
1267
|
-
{
|
|
1268
|
-
const rawElems = staticArrayElems(rhs)
|
|
1269
|
-
if (rawElems) {
|
|
1270
|
-
const elems = rawElems.filter(e => e != null)
|
|
1271
|
-
if (elems.length && elems.length === rawElems.length) {
|
|
1272
|
-
let common = exprElemSourceVal(elems[0])
|
|
1273
|
-
for (let k = 1; k < elems.length && common != null; k++) {
|
|
1274
|
-
if (exprElemSourceVal(elems[k]) !== common) common = null
|
|
1275
|
-
}
|
|
1276
|
-
if (common != null) observeArrValType(name, common)
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
1281
|
-
const f = ctx.func.map?.get(rhs[1])
|
|
1282
|
-
if (f?.arrayElemValType) observeArrValType(name, f.arrayElemValType)
|
|
1283
|
-
}
|
|
1284
|
-
if (typeof rhs === 'string') {
|
|
1285
|
-
const v = elemValOf(rhs)
|
|
1286
|
-
if (v) observeArrValType(name, v)
|
|
1287
|
-
}
|
|
1288
|
-
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
1289
|
-
Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
1290
|
-
typeof rhs[1][1] === 'string') {
|
|
1291
|
-
const recvName = rhs[1][1], method = rhs[1][2]
|
|
1292
|
-
if (method === 'filter' || method === 'slice' || method === 'concat') {
|
|
1293
|
-
const v = elemValOf(recvName)
|
|
1294
|
-
if (v) observeArrValType(name, v)
|
|
1295
|
-
} else if (method === 'split' && valTypeOf(recvName) === VAL.STRING) {
|
|
1296
|
-
observeArrValType(name, VAL.STRING)
|
|
1297
|
-
} else if (method === 'map') {
|
|
1298
|
-
const arrowFn = rhs[2]
|
|
1299
|
-
const recvVt = elemValOf(recvName)
|
|
1300
|
-
const param = Array.isArray(arrowFn) && arrowFn[0] === '=>' ? arrowFn[1] : null
|
|
1301
|
-
const paramName = typeof param === 'string' ? param :
|
|
1302
|
-
(Array.isArray(param) && param[0] === '()' && typeof param[1] === 'string' ? param[1] : null)
|
|
1303
|
-
const arrowBody = paramName ? arrowFn[2] : null
|
|
1304
|
-
const exprBody = (Array.isArray(arrowBody) && arrowBody[0] === '{}' &&
|
|
1305
|
-
Array.isArray(arrowBody[1]) && arrowBody[1][0] === 'return') ? arrowBody[1][1] : arrowBody
|
|
1306
|
-
if (paramName && exprBody != null) {
|
|
1307
|
-
const refs = ctx.func.refinements
|
|
1308
|
-
const hadParam = refs?.has(paramName)
|
|
1309
|
-
const prev = hadParam ? refs.get(paramName) : undefined
|
|
1310
|
-
if (refs && recvVt) refs.set(paramName, { val: recvVt })
|
|
1311
|
-
let bodyVt = null
|
|
1312
|
-
try { bodyVt = valTypeOf(exprBody) }
|
|
1313
|
-
finally {
|
|
1314
|
-
if (refs && recvVt) {
|
|
1315
|
-
if (hadParam) refs.set(paramName, prev); else refs.delete(paramName)
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
if (bodyVt) observeArrValType(name, bodyVt)
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
if (Array.isArray(rhs) && rhs[0] === '()' &&
|
|
1323
|
-
Array.isArray(rhs[1]) && rhs[1][0] === '.' && rhs[1][2] === 'split' &&
|
|
1324
|
-
valTypeOf(rhs[1][1]) === VAL.STRING) {
|
|
1325
|
-
observeArrValType(name, VAL.STRING)
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// arrElem invalidation rule — fires on `=` reassign of tracked name to non-array
|
|
1330
|
-
const isArrayProducingRhs = (rhs) =>
|
|
1331
|
-
Array.isArray(rhs) && (staticArrayElems(rhs) != null ||
|
|
1332
|
-
(rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.' &&
|
|
1333
|
-
(rhs[1][2] === 'slice' || rhs[1][2] === 'concat')))
|
|
1334
|
-
|
|
1335
|
-
const markEscape = (name) => { if (escapes.has(name)) escapes.set(name, true) }
|
|
1336
|
-
|
|
1337
|
-
const isStaticIndex = (key) =>
|
|
1338
|
-
typeof key === 'number' || typeof key === 'string' ||
|
|
1339
|
-
(Array.isArray(key) && ((key[0] == null && Number.isInteger(key[1])) || key[0] === 'str')) ||
|
|
1340
|
-
staticPropertyKey(key) != null
|
|
1341
|
-
|
|
1342
|
-
const markEscapeValue = (expr) => {
|
|
1343
|
-
if (typeof expr === 'string') { markEscape(expr); return }
|
|
1344
|
-
if (!Array.isArray(expr)) return
|
|
1345
|
-
const op = expr[0]
|
|
1346
|
-
if (op === 'str') return
|
|
1347
|
-
if (op === ':') { markEscapeValue(expr[2]); return }
|
|
1348
|
-
if ((op === '.' || op === '?.') && typeof expr[1] === 'string' && escapes.has(expr[1])) return
|
|
1349
|
-
if (op === '[]' && typeof expr[1] === 'string' && escapes.has(expr[1])) {
|
|
1350
|
-
if (!isStaticIndex(expr[2])) markEscape(expr[1])
|
|
1351
|
-
markEscapeValue(expr[2])
|
|
1352
|
-
return
|
|
1353
|
-
}
|
|
1354
|
-
for (let i = 1; i < expr.length; i++) markEscapeValue(expr[i])
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const markEscapeArgs = (args) => {
|
|
1358
|
-
if (args == null) return
|
|
1359
|
-
const list = Array.isArray(args) && args[0] === ',' ? args.slice(1) : [args]
|
|
1360
|
-
for (const a of list) markEscapeValue(Array.isArray(a) && a[0] === '...' ? a[1] : a)
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// === Single walk ===
|
|
1364
|
-
function walk(node) {
|
|
1365
|
-
if (!Array.isArray(node)) return
|
|
1366
|
-
const op = node[0]
|
|
1367
|
-
if (op === '=>') return // don't cross closure boundary
|
|
1368
|
-
|
|
1369
|
-
if (op === 'let' || op === 'const') {
|
|
1370
|
-
for (let i = 1; i < node.length; i++) {
|
|
1371
|
-
const a = node[i]
|
|
1372
|
-
// analyzeBody: bare-name decl
|
|
1373
|
-
if (typeof a === 'string') { if (!locals.has(a)) locals.set(a, 'f64'); continue }
|
|
1374
|
-
if (!Array.isArray(a) || a[0] !== '=') continue
|
|
1375
|
-
// analyzeBody: destructuring decl — set destructured names to f64, walk rhs only
|
|
1376
|
-
if (typeof a[1] !== 'string') {
|
|
1377
|
-
for (const n of collectParamNames([a[1]])) if (!locals.has(n)) locals.set(n, 'f64')
|
|
1378
|
-
walk(a[2])
|
|
1379
|
-
continue
|
|
1380
|
-
}
|
|
1381
|
-
const name = a[1], rhs = a[2]
|
|
1382
|
-
processDecl(name, rhs)
|
|
1383
|
-
if (Array.isArray(rhs) && (rhs[0] === '[' || rhs[0] === '{}')) {
|
|
1384
|
-
escapes.set(name, false)
|
|
1385
|
-
}
|
|
1386
|
-
markEscapeValue(rhs)
|
|
1387
|
-
// Walk rhs only — never enter the `=` node so the reassignment-invalidation
|
|
1388
|
-
// rule won't misfire on the binding's own initializer.
|
|
1389
|
-
walk(rhs)
|
|
1390
|
-
}
|
|
1391
|
-
return
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
if (op === 'return' && node[1] != null) {
|
|
1395
|
-
markEscapeValue(node[1])
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
if (op === '()' && node.length > 2) {
|
|
1399
|
-
markEscapeArgs(node[2])
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// arr.push(...) — observe both schemas and val types in one pass
|
|
1403
|
-
if (op === '()' && Array.isArray(node[1]) && node[1][0] === '.' && node[1][2] === 'push' && typeof node[1][1] === 'string') {
|
|
1404
|
-
const arr = node[1][1]
|
|
1405
|
-
const callArgs = node[2]
|
|
1406
|
-
const list = callArgs == null ? [] :
|
|
1407
|
-
(Array.isArray(callArgs) && callArgs[0] === ',') ? callArgs.slice(1) : [callArgs]
|
|
1408
|
-
for (const a of list) {
|
|
1409
|
-
if (Array.isArray(a) && a[0] === '...') {
|
|
1410
|
-
observeArrSchema(arr, null); observeArrValType(arr, null); continue
|
|
1411
|
-
}
|
|
1412
|
-
observeArrSchema(arr, exprSchemaId(a, localSchemaMap))
|
|
1413
|
-
observeArrValType(arr, exprElemSourceVal(a))
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// `=` reassignment — locals widen, valTypes/typedElems track,
|
|
1418
|
-
// arrElemSchemas/ValTypes invalidate when rhs isn't array-producing.
|
|
1419
|
-
if (op === '=' && typeof node[1] === 'string') {
|
|
1420
|
-
const name = node[1], rhs = node[2]
|
|
1421
|
-
walk(rhs)
|
|
1422
|
-
markEscape(name)
|
|
1423
|
-
markEscapeValue(rhs)
|
|
1424
|
-
const wt = exprType(rhs, locals)
|
|
1425
|
-
if (locals.has(name) && locals.get(name) === 'i32' && wt === 'f64') locals.set(name, 'f64')
|
|
1426
|
-
trackVal(name, valTypeOf(rhs))
|
|
1427
|
-
trackTyped(name, rhs)
|
|
1428
|
-
if (arrElemSchemas.has(name) && !isArrayProducingRhs(rhs)) observeArrSchema(name, null)
|
|
1429
|
-
if (arrElemValTypes.has(name) && !isArrayProducingRhs(rhs)) observeArrValType(name, null)
|
|
1430
|
-
return
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// compound-assign widening (locals slice)
|
|
1434
|
-
if ((op === '+=' || op === '-=' || op === '*=' || op === '%=') && typeof node[1] === 'string') {
|
|
1435
|
-
const name = node[1], opChar = op[0]
|
|
1436
|
-
const t = exprType([opChar, node[1], node[2]], locals)
|
|
1437
|
-
if (locals.has(name) && locals.get(name) === 'i32' && t === 'f64') locals.set(name, 'f64')
|
|
1438
|
-
}
|
|
1439
|
-
if (op === '/=' && typeof node[1] === 'string') {
|
|
1440
|
-
if (locals.has(node[1])) locals.set(node[1], 'f64')
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (op === 'for' || op === 'for-in' || op === 'for-of') {
|
|
1444
|
-
if (node[1] != null) markEscapeValue(node[1])
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
if (op === '[' || op === '{}') {
|
|
1448
|
-
for (let i = 1; i < node.length; i++) {
|
|
1449
|
-
const c = node[i]
|
|
1450
|
-
if (Array.isArray(c) && c[0] === ',') {
|
|
1451
|
-
for (let j = 1; j < c.length; j++) {
|
|
1452
|
-
if (Array.isArray(c[j]) && c[j][0] === '...') markEscapeValue(c[j][1])
|
|
1453
|
-
}
|
|
1454
|
-
} else if (Array.isArray(c) && c[0] === '...') {
|
|
1455
|
-
markEscapeValue(c[1])
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
if (op === '[]' && typeof node[1] === 'string' && escapes.has(node[1])) {
|
|
1461
|
-
const key = node[2]
|
|
1462
|
-
if (!isStaticIndex(key)) markEscape(node[1])
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// Install the in-progress valTypes as a lookup overlay so successive decls
|
|
1469
|
-
// resolve chains (`const a = new TypedArr(); const b = a[0]` → b: NUMBER)
|
|
1470
|
-
// and shorthand-bound `{a}` props see a's type. Restored after walk completes.
|
|
1471
|
-
const prevOverlay = ctx.func.localValTypesOverlay
|
|
1472
|
-
const prevTypedOverlay = ctx.func.localTypedElemsOverlay
|
|
1473
|
-
ctx.func.localValTypesOverlay = valTypes
|
|
1474
|
-
ctx.func.localTypedElemsOverlay = typedElems
|
|
1475
|
-
let unsignedLocals
|
|
1476
|
-
try {
|
|
1477
|
-
walk(body)
|
|
1478
|
-
|
|
1479
|
-
// Second pass: widen i32 locals compared against f64.
|
|
1480
|
-
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
1481
|
-
function widenPass(node) {
|
|
1482
|
-
if (!Array.isArray(node)) return
|
|
1483
|
-
const [op, ...args] = node
|
|
1484
|
-
if (CMP_OPS.has(op)) {
|
|
1485
|
-
const [a, b] = args
|
|
1486
|
-
const ta = exprType(a, locals), tb = exprType(b, locals)
|
|
1487
|
-
if (ta === 'i32' && tb === 'f64' && typeof a === 'string' && locals.has(a)) locals.set(a, 'f64')
|
|
1488
|
-
if (tb === 'i32' && ta === 'f64' && typeof b === 'string' && locals.has(b)) locals.set(b, 'f64')
|
|
1489
|
-
}
|
|
1490
|
-
if (op !== '=>') for (const a of args) widenPass(a)
|
|
1491
|
-
}
|
|
1492
|
-
widenPass(body)
|
|
1493
|
-
|
|
1494
|
-
// Re-resolve let-decl RHS types now that widenPass has widened. A `let x2 =
|
|
1495
|
-
// zx*zx` declared at i32 because zx was i32 at scan time must widen if zx
|
|
1496
|
-
// was later re-typed to f64. Without this, integer-init locals shadowed
|
|
1497
|
-
// by f64-arithmetic RHSs end up with `i32.trunc_sat_f64_s` truncating the
|
|
1498
|
-
// fractional value (e.g. mandelbrot escape: `x2 = zx*zx` losing 3.515 → 3).
|
|
1499
|
-
// Also re-checks `=` and compound-assign reassignments — single-pass walk
|
|
1500
|
-
// visits each assign once with stale operand types, missing widens through
|
|
1501
|
-
// back-edges (`iy = 2.0 * ix * iy + 1.0` where `ix` widens later in the loop
|
|
1502
|
-
// body, demanding `iy` widen on the next iteration).
|
|
1503
|
-
// Monotonic: only widens i32 → f64. Bound by locals count so no infinite loop.
|
|
1504
|
-
let widened = true
|
|
1505
|
-
while (widened) {
|
|
1506
|
-
widened = false
|
|
1507
|
-
const recheck = (node) => {
|
|
1508
|
-
if (!Array.isArray(node)) return
|
|
1509
|
-
const op = node[0]
|
|
1510
|
-
if (op === '=>') return
|
|
1511
|
-
if (op === 'let' || op === 'const') {
|
|
1512
|
-
for (let i = 1; i < node.length; i++) {
|
|
1513
|
-
const a = node[i]
|
|
1514
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
1515
|
-
const name = a[1], rhs = a[2]
|
|
1516
|
-
if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64') {
|
|
1517
|
-
locals.set(name, 'f64'); widened = true
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
if (op === '=' && typeof node[1] === 'string') {
|
|
1523
|
-
const name = node[1], rhs = node[2]
|
|
1524
|
-
if (locals.get(name) === 'i32' && exprType(rhs, locals) === 'f64') {
|
|
1525
|
-
locals.set(name, 'f64'); widened = true
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
if ((op === '+=' || op === '-=' || op === '*=' || op === '%=') && typeof node[1] === 'string') {
|
|
1529
|
-
const name = node[1]
|
|
1530
|
-
if (locals.get(name) === 'i32' && exprType([op[0], name, node[2]], locals) === 'f64') {
|
|
1531
|
-
locals.set(name, 'f64'); widened = true
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
if (op === '/=' && typeof node[1] === 'string') {
|
|
1535
|
-
const name = node[1]
|
|
1536
|
-
if (locals.get(name) === 'i32') { locals.set(name, 'f64'); widened = true }
|
|
1537
|
-
}
|
|
1538
|
-
for (let i = 1; i < node.length; i++) recheck(node[i])
|
|
1539
|
-
}
|
|
1540
|
-
recheck(body)
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// Narrow proven uint32 accumulator locals to unsigned i32. Runs post-widen so
|
|
1544
|
-
// a local already demoted to f64 above (e.g. compared against an f64) is
|
|
1545
|
-
// reconsidered with final types — and stays f64, since a relational compare
|
|
1546
|
-
// is a non-transparent read that disqualifies narrowing anyway.
|
|
1547
|
-
unsignedLocals = narrowUint32(body, locals)
|
|
1548
|
-
} finally {
|
|
1549
|
-
ctx.func.localValTypesOverlay = prevOverlay
|
|
1550
|
-
ctx.func.localTypedElemsOverlay = prevTypedOverlay
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// SRoA: dissolve non-escaping object-literal bindings into field locals.
|
|
1554
|
-
// The dead `o` local is dropped — every `o` reference is rewritten by the
|
|
1555
|
-
// codegen flat hooks, so a stray `local.get $o` becomes a loud wasm
|
|
1556
|
-
// validation error instead of a silent miscompile.
|
|
1557
|
-
const flatObjects = doSchemas ? scanFlatObjects(body) : new Map()
|
|
1558
|
-
for (const [name, props] of flatObjects) {
|
|
1559
|
-
for (let i = 0; i < props.names.length; i++) locals.set(`${name}#${i}`, 'f64')
|
|
1560
|
-
locals.delete(name)
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
// No-copy slice views — `let t = s.slice(...)` bindings proven non-escaping.
|
|
1564
|
-
// Consumed by emitDecl, which lowers the initializer to a SLICE_BIT view.
|
|
1565
|
-
const sliceViews = doSchemas ? scanSliceViews(body) : new Set()
|
|
1566
|
-
|
|
1567
|
-
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes, flatObjects, sliceViews, unsignedLocals }
|
|
1568
|
-
_bodyFactsCache.set(body, result)
|
|
1569
|
-
return result
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
/** Drop the cached analyzeBody entry for this body. Used by emitFunc after
|
|
1573
|
-
* seeding cross-call param VAL facts so the next walk picks up fresh
|
|
1574
|
-
* `ctx.func.localReps` (drives exprType receiver-type lookups).
|
|
1575
|
-
* Same hook as `invalidateValTypesCache` — split names preserve caller intent. */
|
|
1576
|
-
export function invalidateLocalsCache(body) {
|
|
1577
|
-
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
/** Drop the cached analyzeBody entry. Used after E2-phase valResult narrowing
|
|
1581
|
-
* so the next walk re-evaluates `valTypeOf(call)` with up-to-date `f.valResult`
|
|
1582
|
-
* — required for the D-pass paramReps val/arrayElemSchema re-fixpoint to see
|
|
1583
|
-
* `const rows = initRows()` as VAL.ARRAY (initRows.valResult set by E2). */
|
|
1584
|
-
export function invalidateValTypesCache(body) {
|
|
1585
|
-
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
/**
|
|
1589
|
-
* Analyze all local value types from declarations and assignments.
|
|
1590
|
-
* Writes the per-name `val` field of `ctx.func.localReps` for method dispatch
|
|
1591
|
-
* and schema resolution.
|
|
1592
|
-
*/
|
|
1593
|
-
export function analyzeValTypes(body) {
|
|
1594
|
-
const valPoison = new Set()
|
|
1595
|
-
const setVal = (name, vt) => {
|
|
1596
|
-
if (valPoison.has(name)) return
|
|
1597
|
-
const prev = ctx.func.localReps?.get(name)?.val
|
|
1598
|
-
if (!vt) {
|
|
1599
|
-
if (prev) valPoison.add(name)
|
|
1600
|
-
updateRep(name, { val: undefined })
|
|
1601
|
-
return
|
|
1602
|
-
}
|
|
1603
|
-
if (prev && prev !== vt) {
|
|
1604
|
-
valPoison.add(name)
|
|
1605
|
-
updateRep(name, { val: undefined })
|
|
1606
|
-
return
|
|
1607
|
-
}
|
|
1608
|
-
updateRep(name, { val: vt })
|
|
1609
|
-
}
|
|
1610
|
-
const getVal = name => ctx.func.localReps?.get(name)?.val
|
|
1611
|
-
// Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
|
|
1612
|
-
// on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
|
|
1613
|
-
// Parallel arrElemValTypes walk records VAL.* element kinds into
|
|
1614
|
-
// rep.arrayElemValType so valTypeOf's `arr[i]` rule can elide __to_num and route
|
|
1615
|
-
// method dispatch on `arr[i].method()`. Both come from a single unified walk.
|
|
1616
|
-
const facts = analyzeBody(body)
|
|
1617
|
-
const arrElems = facts.arrElemSchemas
|
|
1618
|
-
for (const [name, vt] of facts.arrElemValTypes) {
|
|
1619
|
-
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
1620
|
-
}
|
|
1621
|
-
// Propagate body-observed array-elem schemas to localReps so unboxablePtrs's
|
|
1622
|
-
// `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
|
|
1623
|
-
// to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
|
|
1624
|
-
// pay an i64.reinterpret/i32.wrap on every slot access (no aliasing → CSE can't fold).
|
|
1625
|
-
for (const [name, sid] of arrElems) {
|
|
1626
|
-
if (sid != null) updateRep(name, { arrayElemSchema: sid })
|
|
1627
|
-
}
|
|
1628
|
-
// Resolve a name's array-elem-schema, preferring rep.arrayElemSchema (set from
|
|
1629
|
-
// paramReps[k].arrayElemSchema at emit start) over local body observations.
|
|
1630
|
-
const arrElemSchemaOf = (name) => {
|
|
1631
|
-
if (typeof name !== 'string') return null
|
|
1632
|
-
const repSid = ctx.func.localReps?.get(name)?.arrayElemSchema
|
|
1633
|
-
if (repSid != null) return repSid
|
|
1634
|
-
const localSid = arrElems.get(name)
|
|
1635
|
-
return localSid != null ? localSid : null
|
|
1636
|
-
}
|
|
1637
|
-
function trackRegex(name, rhs) {
|
|
1638
|
-
if (ctx.runtime.regex && Array.isArray(rhs) && rhs[0] === '//') ctx.runtime.regex.vars.set(name, rhs)
|
|
1639
|
-
}
|
|
1640
|
-
// Names whose decls disagree on element ctor (e.g. two `let arr = ...` decls
|
|
1641
|
-
// in different scopes — jz hoists `let` to function scope so they share a name).
|
|
1642
|
-
// Once invalidated, no later setter can re-establish a definite ctor.
|
|
1643
|
-
const typedPoison = new Set()
|
|
1644
|
-
const invalidate = (name) => {
|
|
1645
|
-
typedPoison.add(name)
|
|
1646
|
-
ctx.types.typedElem?.delete(name)
|
|
1647
|
-
}
|
|
1648
|
-
function trackTyped(name, rhs) {
|
|
1649
|
-
if (!ctx.types.typedElem) ctx.types.typedElem = new Map() // first use in this function scope
|
|
1650
|
-
if (typedPoison.has(name)) return
|
|
1651
|
-
const setOrInvalidate = (c) => {
|
|
1652
|
-
if (c === MIXED_CTORS) return invalidate(name)
|
|
1653
|
-
const prev = ctx.types.typedElem.get(name)
|
|
1654
|
-
if (prev && prev !== c) invalidate(name)
|
|
1655
|
-
else ctx.types.typedElem.set(name, c)
|
|
1656
|
-
}
|
|
1657
|
-
const ctor = typedElemCtor(rhs)
|
|
1658
|
-
if (ctor) return setOrInvalidate(ctor)
|
|
1659
|
-
// TYPED-narrowed call result carries elem aux on f.sig.ptrAux — reverse-map it
|
|
1660
|
-
// back to a canonical ctor string so unboxablePtrs's typedElemAux lookup
|
|
1661
|
-
// (compile.js) restores the same aux on the unboxed local's rep.
|
|
1662
|
-
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
1663
|
-
const f = ctx.func.map?.get(rhs[1])
|
|
1664
|
-
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) {
|
|
1665
|
-
const c = ctorFromElemAux(f.sig.ptrAux)
|
|
1666
|
-
if (c) setOrInvalidate(c)
|
|
1667
|
-
}
|
|
1668
|
-
return
|
|
1669
|
-
}
|
|
1670
|
-
// Heterogeneous ternary (e.g. `n === 16 ? new Uint8Array(16) : new Uint16Array(8)`):
|
|
1671
|
-
// typedElemCtor returns null for `?:`. When branches don't unify to the same ctor,
|
|
1672
|
-
// poison the name so a later sibling-scope decl (jz hoists `let` to function scope)
|
|
1673
|
-
// can't lock in the wrong store width.
|
|
1674
|
-
const tc = ternaryCtorOfRhs(rhs)
|
|
1675
|
-
if (tc) setOrInvalidate(tc)
|
|
1676
|
-
}
|
|
1677
|
-
// Total write count for `name` across the whole body, recursing into nested
|
|
1678
|
-
// closures so a closure that reassigns the var is also counted. Capped at 2 —
|
|
1679
|
-
// callers only need the "exactly one write" verdict.
|
|
1680
|
-
function writeCount(node, name, n) {
|
|
1681
|
-
if (n > 1 || !Array.isArray(node)) return n
|
|
1682
|
-
const o = node[0]
|
|
1683
|
-
if ((ASSIGN_OPS.has(o) || o === '++' || o === '--') && node[1] === name) n++
|
|
1684
|
-
if (o === 'let' || o === 'const') {
|
|
1685
|
-
for (let i = 1; i < node.length && n <= 1; i++) {
|
|
1686
|
-
const d = node[i]
|
|
1687
|
-
if (Array.isArray(d) && d[0] === '=' && d[2] != null) n = writeCount(d[2], name, n)
|
|
1688
|
-
}
|
|
1689
|
-
return n
|
|
1690
|
-
}
|
|
1691
|
-
for (let i = 1; i < node.length && n <= 1; i++) n = writeCount(node[i], name, n)
|
|
1692
|
-
return n
|
|
1693
|
-
}
|
|
1694
|
-
// Bind an object-literal's schemaId onto its holding local's rep so that
|
|
1695
|
-
// `o.prop` / `o.method()` dispatch is precise instead of falling back to
|
|
1696
|
-
// structural subtyping (which mis-resolves when another in-scope object
|
|
1697
|
-
// shares a member at a different slot). `shapeOf` already covers plain-data
|
|
1698
|
-
// literals on a direct `let o = {…}` decl, but not literals with
|
|
1699
|
-
// function-valued props — and `var o = {…}` is rewritten by jzify into
|
|
1700
|
-
// `let o; o = {…}`, so the schemaId never reaches `o` either way.
|
|
1701
|
-
// `expectWrites` is the reassignment count that marks `o` single-assignment:
|
|
1702
|
-
// 1 for the jzify `=` form (the synthesized assignment IS the only write),
|
|
1703
|
-
// 0 for a direct `let`/`const` decl (the initializer is not counted as a
|
|
1704
|
-
// write). A polymorphically reassigned holder keeps dynamic dispatch.
|
|
1705
|
-
// A name already in `ctx.schema.vars` carries a prepare-phase schema
|
|
1706
|
-
// (Object.assign merge via `inferAssignSchema`, destructure tracking) that
|
|
1707
|
-
// supersedes the bare-literal one — binding here would shadow the merged
|
|
1708
|
-
// schema (rep schemaId wins over `ctx.schema.vars` in `idOf`).
|
|
1709
|
-
function bindObjSchema(name, rhs, expectWrites = 1) {
|
|
1710
|
-
if (ctx.func.current?.params?.some(p => p.name === name)) return
|
|
1711
|
-
if (ctx.schema.vars?.has(name)) return
|
|
1712
|
-
const sid = objLiteralSchemaId(rhs)
|
|
1713
|
-
if (sid != null && writeCount(body, name, 0) === expectWrites) updateRep(name, { schemaId: sid })
|
|
1714
|
-
}
|
|
1715
|
-
function walk(node) {
|
|
1716
|
-
if (!Array.isArray(node)) return
|
|
1717
|
-
const [op, ...args] = node
|
|
1718
|
-
if (op === '=>') return // don't leak inner-closure val types
|
|
1719
|
-
// Propagate typed array type through method calls (e.g. buf.map → typed)
|
|
1720
|
-
function propagateTyped(name, rhs) {
|
|
1721
|
-
if (!Array.isArray(rhs) || rhs[0] !== '()') return
|
|
1722
|
-
const callee = rhs[1]
|
|
1723
|
-
if (!Array.isArray(callee) || callee[0] !== '.') return
|
|
1724
|
-
const src = callee[1], method = callee[2]
|
|
1725
|
-
if (typeof src === 'string' && getVal(src) === VAL.TYPED && method === 'map') {
|
|
1726
|
-
setVal(name, VAL.TYPED)
|
|
1727
|
-
if (ctx.types.typedElem?.has(src)) {
|
|
1728
|
-
const srcCtor = ctx.types.typedElem.get(src)
|
|
1729
|
-
ctx.types.typedElem.set(name, srcCtor.endsWith('.view') ? srcCtor.slice(0, -5) : srcCtor)
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
if (op === 'let' || op === 'const') {
|
|
1734
|
-
for (const a of args) {
|
|
1735
|
-
if (!Array.isArray(a) || a[0] !== '=' || typeof a[1] !== 'string') continue
|
|
1736
|
-
const vt = valTypeOf(a[2])
|
|
1737
|
-
setVal(a[1], vt)
|
|
1738
|
-
if (vt === VAL.REGEX) trackRegex(a[1], a[2])
|
|
1739
|
-
// VAL gate covers definite-typed RHS; `?:`/`&&`/`||` slip through valTypeOf
|
|
1740
|
-
// returning null but may still need ctor unification (or poisoning when
|
|
1741
|
-
// branches disagree, since jz hoists `let` to function scope).
|
|
1742
|
-
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(a[2])) trackTyped(a[1], a[2])
|
|
1743
|
-
propagateTyped(a[1], a[2])
|
|
1744
|
-
// JSON-shape propagation. When the RHS resolves to a known JSON shape
|
|
1745
|
-
// (root: `JSON.parse(literal)`; nested: `o.meta`, `items[j]` from a known
|
|
1746
|
-
// root), record it on the binding so subsequent `.prop`/`[i]` accesses
|
|
1747
|
-
// skip dynamic dispatch and propagate VAL kinds. Generic for any
|
|
1748
|
-
// compile-time JSON literal.
|
|
1749
|
-
const sh = shapeOf(a[2])
|
|
1750
|
-
if (sh) {
|
|
1751
|
-
updateRep(a[1], { jsonShape: sh })
|
|
1752
|
-
if (sh.val === VAL.ARRAY && sh.elem?.val) {
|
|
1753
|
-
updateRep(a[1], { arrayElemValType: sh.elem.val })
|
|
1754
|
-
// Array of fixed-shape OBJECTs: register elem schema so `it = items[j]`
|
|
1755
|
-
// → `it.prop` lowers to slot read via the existing arr-elem-schema path.
|
|
1756
|
-
if (sh.elem.val === VAL.OBJECT && sh.elem.names && ctx.schema.register) {
|
|
1757
|
-
const elemSid = ctx.schema.register(sh.elem.names)
|
|
1758
|
-
updateRep(a[1], { arrayElemSchema: elemSid })
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
if (sh.val === VAL.OBJECT && sh.names && ctx.schema.register) {
|
|
1762
|
-
const sid = ctx.schema.register(sh.names)
|
|
1763
|
-
updateRep(a[1], { schemaId: sid })
|
|
1764
|
-
ctx.schema.vars.set(a[1], sid)
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
// `shapeOf` misses object literals with function-valued props; bind
|
|
1768
|
-
// their schemaId here so number-hint ToPrimitive (valueOf/toString slot
|
|
1769
|
-
// dispatch) resolves. expectWrites=0: a decl initializer is not a write.
|
|
1770
|
-
if (vt === VAL.OBJECT) bindObjSchema(a[1], a[2], 0)
|
|
1771
|
-
// Propagate schemaId from a narrowed call result so subsequent valTypeOf
|
|
1772
|
-
// calls in this function body see the precise schema. emitDecl rebinds
|
|
1773
|
-
// this at emission time too — analyze-time binding is what unlocks the
|
|
1774
|
-
// slotVT lookup chain in `analyzeValTypes`'s own walk + per-func emit
|
|
1775
|
-
// dispatch reading localReps.
|
|
1776
|
-
if (vt === VAL.OBJECT && Array.isArray(a[2]) && a[2][0] === '()' && typeof a[2][1] === 'string') {
|
|
1777
|
-
const f = ctx.func.map?.get(a[2][1])
|
|
1778
|
-
if (f?.sig?.ptrAux != null) updateRep(a[1], { schemaId: f.sig.ptrAux })
|
|
1779
|
-
}
|
|
1780
|
-
// `const p = arr[i]` — when arr's element schema is known (from .push observations
|
|
1781
|
-
// or from paramReps arrayElemSchema binding), p inherits the schema. Unlocks slotVT-driven
|
|
1782
|
-
// numeric typing on `.prop` reads + slot-direct loads.
|
|
1783
|
-
if (Array.isArray(a[2]) && a[2][0] === '[]' && typeof a[2][1] === 'string') {
|
|
1784
|
-
const elemSid = arrElemSchemaOf(a[2][1])
|
|
1785
|
-
if (elemSid != null) {
|
|
1786
|
-
updateRep(a[1], { schemaId: elemSid })
|
|
1787
|
-
// Also set the val so structural call dispatch + valTypeOf see VAL.OBJECT.
|
|
1788
|
-
setVal(a[1], VAL.OBJECT)
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
if (op === '=' && typeof args[0] === 'string') {
|
|
1794
|
-
walk(args[1])
|
|
1795
|
-
const vt = valTypeOf(args[1])
|
|
1796
|
-
setVal(args[0], vt)
|
|
1797
|
-
if (vt === VAL.REGEX) trackRegex(args[0], args[1])
|
|
1798
|
-
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(args[1])) trackTyped(args[0], args[1])
|
|
1799
|
-
propagateTyped(args[0], args[1])
|
|
1800
|
-
if (vt === VAL.OBJECT) bindObjSchema(args[0], args[1])
|
|
1801
|
-
return
|
|
1802
|
-
}
|
|
1803
|
-
// Track property assignments for auto-boxing: x.prop = val
|
|
1804
|
-
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '.' && typeof args[0][1] === 'string') {
|
|
1805
|
-
const [, obj, prop] = args[0]
|
|
1806
|
-
const vt = getVal(obj)
|
|
1807
|
-
if ((vt === VAL.NUMBER || vt === VAL.BIGINT) && ctx.func.locals?.has(obj) && ctx.schema.register) {
|
|
1808
|
-
if (!ctx.func.localProps) ctx.func.localProps = new Map()
|
|
1809
|
-
if (!ctx.func.localProps.has(obj)) ctx.func.localProps.set(obj, new Set())
|
|
1810
|
-
ctx.func.localProps.get(obj).add(prop)
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
for (const a of args) walk(a)
|
|
1814
|
-
}
|
|
1815
|
-
walk(body)
|
|
1816
|
-
|
|
1817
|
-
// Register boxed schemas for local variables with property assignments
|
|
1818
|
-
if (ctx.func.localProps) {
|
|
1819
|
-
for (const [name, props] of ctx.func.localProps) {
|
|
1820
|
-
if (ctx.schema.vars.has(name)) continue
|
|
1821
|
-
const schema = ['__inner__', ...props]
|
|
1822
|
-
const sid = ctx.schema.register(schema)
|
|
1823
|
-
ctx.schema.vars.set(name, sid)
|
|
1824
|
-
updateRep(name, { schemaId: sid })
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
|
|
1830
|
-
const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
|
|
1831
|
-
const INT_CLOSED_OPS = new Set(['+', '-', '*', '%'])
|
|
1832
|
-
const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
|
|
1833
|
-
|
|
1834
|
-
/**
|
|
1835
|
-
* Forward-propagate `intCertain` across local bindings (S2 Stage 4a — pure analysis).
|
|
1836
|
-
*
|
|
1837
|
-
* A binding is `intCertain` iff every defining RHS evaluates to an integer-valued
|
|
1838
|
-
* expression. Reassignments widen — any non-int RHS poisons the binding, regardless
|
|
1839
|
-
* of order in source. Multi-pass fixpoint converges when RHSs read other bindings
|
|
1840
|
-
* transitively (`let j = i + 1` resolves only after `i` is known intCertain).
|
|
1841
|
-
*
|
|
1842
|
-
* Integer-shaped RHS (closed under composition):
|
|
1843
|
-
* - integer Number literal, boolean literal
|
|
1844
|
-
* - bitwise ops `& | ^ ~ << >> >>>` — i32 result by spec
|
|
1845
|
-
* - comparisons `< > <= >= == != === !== !` — 0/1 result
|
|
1846
|
-
* - `.length` / `.byteLength` on TYPED/ARRAY/STRING/BUFFER receiver
|
|
1847
|
-
* - `+ - * %` and unary `+ -` of intCertain operands (overflow OK — value is mathematically integer)
|
|
1848
|
-
* - `?: && ||` when both branches are intCertain
|
|
1849
|
-
* - `Math.{imul, clz32, floor, ceil, round, trunc}`
|
|
1850
|
-
* - self-mutation ops `++` `--` `+=` `-=` `*=` `%=` (preserve when operand is int);
|
|
1851
|
-
* `&= |= ^= <<= >>= >>>=` (always int by op result type);
|
|
1852
|
-
* `/=` `**=` poison.
|
|
1853
|
-
*
|
|
1854
|
-
* Writes `intCertain: true` on `ctx.func.localReps[name]`. Consumers:
|
|
1855
|
-
* • `toNumF64` (src/ir.js) — skips the `__to_num` wrapper since an intCertain
|
|
1856
|
-
* local never carries a NaN-boxed pointer.
|
|
1857
|
-
* • `Math.floor/ceil/trunc/round` (module/math.js) — short-circuits to the
|
|
1858
|
-
* identity, eliding the wasm rounding op on an already-integer operand.
|
|
1859
|
-
*/
|
|
1860
|
-
export function analyzeIntCertain(body) {
|
|
1861
|
-
// Pass 1: collect every defining RHS per binding name. Compound assignments
|
|
1862
|
-
// are desugared to their `=` equivalent (`x += y` → `x = x + y`) so the
|
|
1863
|
-
// existing `isIntExpr` op rules apply uniformly.
|
|
1864
|
-
const defs = new Map()
|
|
1865
|
-
const pushDef = (name, rhs) => {
|
|
1866
|
-
let list = defs.get(name)
|
|
1867
|
-
if (!list) { list = []; defs.set(name, list) }
|
|
1868
|
-
list.push(rhs)
|
|
1869
|
-
}
|
|
1870
|
-
const collect = (node) => {
|
|
1871
|
-
if (!Array.isArray(node)) return
|
|
1872
|
-
const [op, ...args] = node
|
|
1873
|
-
if (op === '=>') return
|
|
1874
|
-
if (op === 'let' || op === 'const') {
|
|
1875
|
-
for (const a of args) {
|
|
1876
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
|
|
1877
|
-
}
|
|
1878
|
-
} else if (op === '=' && typeof args[0] === 'string') {
|
|
1879
|
-
pushDef(args[0], args[1])
|
|
1880
|
-
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
1881
|
-
!INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
1882
|
-
// Compound assign: desugar `x <op>= rhs` → `x = x <op> rhs`. The base op
|
|
1883
|
-
// result is fed back through isIntExpr — bitwise compounds become int by
|
|
1884
|
-
// the bitwise rule; +=/-=/*=/%= preserve via int-closed rule.
|
|
1885
|
-
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
1886
|
-
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
1887
|
-
// `x++` / `x--` desugars to `x = x ± 1`. 1 is int → preserves intCertain.
|
|
1888
|
-
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
1889
|
-
}
|
|
1890
|
-
for (const a of args) collect(a)
|
|
1891
|
-
}
|
|
1892
|
-
collect(body)
|
|
1893
|
-
if (defs.size === 0) return
|
|
1894
|
-
|
|
1895
|
-
// Pass 2: monotone-down fixpoint. Start optimistic (every defined binding
|
|
1896
|
-
// assumed intCertain), then for each iteration mark false any binding whose
|
|
1897
|
-
// RHS list contains a non-int expression. Once false, stays false — defs is
|
|
1898
|
-
// fixed and isIntExpr only reads back through bindings that themselves can
|
|
1899
|
-
// only flip true→false. Converges when no further bindings flip.
|
|
1900
|
-
//
|
|
1901
|
-
// (Naive bottom-up `false→true` direction is unsound for recursive bindings
|
|
1902
|
-
// like `let i = 0; i = i + 1` — first iteration sees i unobserved → false →
|
|
1903
|
-
// i+1 false → i stays false, missing the fact that all RHSs are int.)
|
|
1904
|
-
const intCertain = new Map()
|
|
1905
|
-
for (const name of defs.keys()) intCertain.set(name, true)
|
|
1906
|
-
|
|
1907
|
-
const isIntExpr = (expr) => {
|
|
1908
|
-
// -0 is integer-valued mathematically but i32 has no signed zero — must stay f64.
|
|
1909
|
-
if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
|
|
1910
|
-
if (typeof expr === 'boolean') return true
|
|
1911
|
-
if (typeof expr === 'string') return intCertain.get(expr) === true
|
|
1912
|
-
if (!Array.isArray(expr)) return false
|
|
1913
|
-
// Statically evaluable to -0 (e.g. -1 * 0, 0 / -1) — i32 would lose the sign.
|
|
1914
|
-
const sv = staticValue(expr)
|
|
1915
|
-
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
|
|
1916
|
-
const [op, ...args] = expr
|
|
1917
|
-
if (op == null) {
|
|
1918
|
-
// `[, value]` / `[null, value]` literal form
|
|
1919
|
-
const v = args[0]
|
|
1920
|
-
if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
|
|
1921
|
-
if (typeof v === 'boolean') return true
|
|
1922
|
-
return false
|
|
1923
|
-
}
|
|
1924
|
-
if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
|
|
1925
|
-
if (op === '.') {
|
|
1926
|
-
if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
|
|
1927
|
-
const vt = lookupValType(args[0])
|
|
1928
|
-
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
1929
|
-
}
|
|
1930
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
1931
|
-
const vt = lookupValType(args[0])
|
|
1932
|
-
return vt === VAL.SET || vt === VAL.MAP
|
|
1933
|
-
}
|
|
1934
|
-
return false
|
|
1935
|
-
}
|
|
1936
|
-
if (INT_CLOSED_OPS.has(op)) {
|
|
1937
|
-
const a = isIntExpr(args[0])
|
|
1938
|
-
const b = args[1] != null ? isIntExpr(args[1]) : a
|
|
1939
|
-
return a && b
|
|
1940
|
-
}
|
|
1941
|
-
if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
|
|
1942
|
-
if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
|
|
1943
|
-
if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
|
|
1944
|
-
// Math.{imul,clz32,floor,ceil,round,trunc} — prepare normalizes the callee to
|
|
1945
|
-
// the string `math.<fn>`. The pre-prepare `['.', 'Math', '<fn>']` shape is
|
|
1946
|
-
// matched too so this analyzer is robust if invoked on a non-normalized AST.
|
|
1947
|
-
if (op === '()') {
|
|
1948
|
-
const c = args[0]
|
|
1949
|
-
if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
|
|
1950
|
-
if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
|
|
1951
|
-
}
|
|
1952
|
-
return false
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
let changed = true
|
|
1956
|
-
while (changed) {
|
|
1957
|
-
changed = false
|
|
1958
|
-
for (const [name, rhsList] of defs) {
|
|
1959
|
-
if (!intCertain.get(name)) continue
|
|
1960
|
-
if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
for (const [name, intC] of intCertain) {
|
|
1965
|
-
if (intC) updateRep(name, { intCertain: true })
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// A directly-uint32 expression: `x >>> 0` (zero-fill shift) or a call to a function
|
|
1970
|
-
// already proven `unsignedResult`. Such a value lives in i32 but ranges [0, 2^32),
|
|
1971
|
-
// so signed i32 ops on it are wrong — exprType widens its arithmetic to f64 to
|
|
1972
|
-
// match emit (which reboxes via `f64.convert_i32_u`). Unsignedness through a local
|
|
1973
|
-
// assignment is intentionally not tracked here — kept in lockstep with narrow.js's
|
|
1974
|
-
// `isUnsignedTail`, so emit and exprType agree (no trunc_sat saturation).
|
|
1975
|
-
const isUnsignedI32Expr = (e) => Array.isArray(e) && (
|
|
1976
|
-
e[0] === '>>>' ||
|
|
1977
|
-
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
1978
|
-
)
|
|
1979
|
-
|
|
1980
|
-
/**
|
|
1981
|
-
* Infer expression result type from AST (without emitting).
|
|
1982
|
-
* Used to determine local variable types before compilation.
|
|
1983
|
-
* Looks up `locals` first, then current-function params (for i32-specialized params).
|
|
1984
|
-
*/
|
|
1985
|
-
export function exprType(expr, locals) {
|
|
1986
|
-
if (expr == null) return 'f64'
|
|
1987
|
-
if (typeof expr === 'number')
|
|
1988
|
-
return isI32(expr) ? 'i32' : 'f64'
|
|
1989
|
-
if (typeof expr === 'string') {
|
|
1990
|
-
if (locals?.has?.(expr)) return locals.get(expr)
|
|
1991
|
-
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
1992
|
-
if (paramType) return paramType
|
|
1993
|
-
// Module-level numeric consts (top-level `const N = 128`) are emitted as
|
|
1994
|
-
// wasm globals with a known wasm type. Without this lookup, references to
|
|
1995
|
-
// them inside functions fall back to f64, widening counters bounded by the
|
|
1996
|
-
// const (`for (let r = 0; r < N_ROUNDS; r++)`) to f64 via the comparison
|
|
1997
|
-
// pass. Only propagate primitive numeric kinds — i64 globals are reserved
|
|
1998
|
-
// for the NaN-box carrier ABI and shouldn't influence local typing.
|
|
1999
|
-
const gt = ctx.scope?.globalTypes?.get?.(expr)
|
|
2000
|
-
if (gt === 'i32' || gt === 'f64') return gt
|
|
2001
|
-
return 'f64'
|
|
2002
|
-
}
|
|
2003
|
-
if (!Array.isArray(expr)) return 'f64'
|
|
2004
|
-
|
|
2005
|
-
const [op, ...args] = expr
|
|
2006
|
-
if (op == null) return exprType(args[0], locals) // literal [, value]
|
|
2007
|
-
|
|
2008
|
-
// Statically evaluable to -0 (e.g. -1 * 0) — i32 would lose the sign.
|
|
2009
|
-
const sv = staticValue(expr)
|
|
2010
|
-
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 'f64'
|
|
2011
|
-
|
|
2012
|
-
// Always f64
|
|
2013
|
-
if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
|
|
2014
|
-
// arr[i] — typed integer arrays return i32. Only Int8/Uint8/Int16/Uint16/Int32
|
|
2015
|
-
// (every value fits in signed i32). Skip Uint32: 0..2^32-1 overflows signed.
|
|
2016
|
-
// During analyzeBody the in-progress typedElems is in localTypedElemsOverlay;
|
|
2017
|
-
// post-analyze passes read from ctx.types.typedElem.
|
|
2018
|
-
if (op === '[]') {
|
|
2019
|
-
if (typeof args[0] === 'string') {
|
|
2020
|
-
const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
|
|
2021
|
-
if (ctor) {
|
|
2022
|
-
const aux = typedElemAux(ctor)
|
|
2023
|
-
if (aux != null && (aux & 7) <= 4) return 'i32'
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
return 'f64'
|
|
2027
|
-
}
|
|
2028
|
-
// `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
|
|
2029
|
-
// both return i32). Letting it stay i32 lets analyzeBody keep the counter
|
|
2030
|
-
// local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
|
|
2031
|
-
// the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
|
|
2032
|
-
// Only safe when receiver type is statically known to expose an integer length.
|
|
2033
|
-
if (op === '.') {
|
|
2034
|
-
if (args[1] === 'length' && typeof args[0] === 'string') {
|
|
2035
|
-
const vt = lookupValType(args[0])
|
|
2036
|
-
if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
|
|
2037
|
-
}
|
|
2038
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
2039
|
-
const vt = lookupValType(args[0])
|
|
2040
|
-
if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
|
|
2041
|
-
}
|
|
2042
|
-
if (args[1] === 'byteLength' && typeof args[0] === 'string') {
|
|
2043
|
-
const vt = lookupValType(args[0])
|
|
2044
|
-
if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
|
|
2045
|
-
}
|
|
2046
|
-
return 'f64'
|
|
2047
|
-
}
|
|
2048
|
-
// Always i32
|
|
2049
|
-
if (['>', '<', '>=', '<=', '==', '!=', '!', '&', '|', '^', '~', '<<', '>>', '>>>'].includes(op)) return 'i32'
|
|
2050
|
-
// Preserve i32 if both operands i32
|
|
2051
|
-
if (op === '+' || op === '-' || op === '%') {
|
|
2052
|
-
const ta = exprType(args[0], locals)
|
|
2053
|
-
const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
|
|
2054
|
-
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
2055
|
-
// A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
|
|
2056
|
-
// emit widens to f64 (see emit.js `+`/`-`/`%`). exprType must agree — else
|
|
2057
|
-
// narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
|
|
2058
|
-
if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
|
|
2059
|
-
return 'i32'
|
|
2060
|
-
}
|
|
2061
|
-
// `*` — a JS multiply is an f64 operation; `i32.mul` reproduces it faithfully
|
|
2062
|
-
// only while the exact product is f64-exact. Stay i32 when both operands are
|
|
2063
|
-
// i32 *and* the product provably fits: a fully-static product checked
|
|
2064
|
-
// directly, otherwise a literal operand small enough that |literal|·2^31 ≤
|
|
2065
|
-
// 2^53 (mirrors emit.js `mulFitsI32` — keeps `i*4` i32, widens `h*16777619`).
|
|
2066
|
-
if (op === '*') {
|
|
2067
|
-
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
2068
|
-
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
2069
|
-
// uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
|
|
2070
|
-
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
2071
|
-
if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
|
|
2072
|
-
const small = e => {
|
|
2073
|
-
const v = staticValue(e)
|
|
2074
|
-
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= 0x400000
|
|
2075
|
-
}
|
|
2076
|
-
return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
|
|
2077
|
-
}
|
|
2078
|
-
// Unary preserves type
|
|
2079
|
-
if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
|
|
2080
|
-
// Ternary / logical: conciliate
|
|
2081
|
-
if (op === '?:' || op === '&&' || op === '||') {
|
|
2082
|
-
const branches = op === '?:' ? [args[1], args[2]] : [args[0], args[1]]
|
|
2083
|
-
const ta = exprType(branches[0], locals), tb = exprType(branches[1], locals)
|
|
2084
|
-
return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
|
|
2085
|
-
}
|
|
2086
|
-
if (op === '[') return 'f64'
|
|
2087
|
-
// Builtin calls with known i32 result. Math.imul / Math.clz32 always produce
|
|
2088
|
-
// a 32-bit integer; recognising this here keeps `let x = Math.imul(...)` (and
|
|
2089
|
-
// chains like `x = Math.imul(x, k) + 12345`) on the i32 ABI all the way
|
|
2090
|
-
// through, instead of widening the local to f64 because exprType defaulted.
|
|
2091
|
-
if (op === '()') {
|
|
2092
|
-
if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
|
|
2093
|
-
// charCodeAt: i32 when the index is provably in `[0, recv.length)` (an
|
|
2094
|
-
// induction variable bounded by `recv.length` — OOB impossible). Otherwise
|
|
2095
|
-
// f64: the JS-spec OOB result is NaN, which is not representable as i32.
|
|
2096
|
-
if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt'
|
|
2097
|
-
&& inBoundsCharCodeAt(ctx).has(args[0])) return 'i32'
|
|
2098
|
-
// User-function call: consult the callee's narrowed result type. By the time
|
|
2099
|
-
// analyzeBody runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
|
|
2100
|
-
// on every body-i32-only func. Propagating this lets `let h = userFn(...)`
|
|
2101
|
-
// (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
|
|
2102
|
-
// to f64 and round-tripping i32↔f64 every iteration.
|
|
2103
|
-
if (typeof args[0] === 'string') {
|
|
2104
|
-
const f = ctx.func.map?.get(args[0])
|
|
2105
|
-
if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'i32' && f.sig.ptrKind == null) return 'i32'
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return 'f64'
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
// `analyzeBody` was inlined to `analyzeBody(body).locals` at its three real
|
|
2112
|
-
// call sites in src/compile.js and src/narrow.js — the one-line facade existed
|
|
2113
|
-
// only as a historical surface and obscured the unified-walk relationship.
|
|
2114
|
-
|
|
2115
|
-
/**
|
|
2116
|
-
* Identify locals that can be stored as an unboxed i32 pointer offset instead of
|
|
2117
|
-
* a NaN-boxed f64. Static type is tracked out-of-band so reads skip `__ptr_offset`
|
|
2118
|
-
* and `__ptr_type` entirely and writes unbox once at the assignment site.
|
|
2119
|
-
*
|
|
2120
|
-
* Criteria — the local must be:
|
|
2121
|
-
* - declared once with `let`/`const`, never reassigned or compound-assigned
|
|
2122
|
-
* - valType is an unambiguous non-forwarding pointer kind:
|
|
2123
|
-
* OBJECT, SET, MAP, CLOSURE, TYPED, BUFFER
|
|
2124
|
-
* (excluded: ARRAY — forwards on realloc; STRING — SSO/heap dual encoding.)
|
|
2125
|
-
* - initialized from a form that guarantees a fresh, non-null pointer of that VAL:
|
|
2126
|
-
* OBJECT ← `{…}`
|
|
2127
|
-
* SET ← `new Set(...)`
|
|
2128
|
-
* MAP ← `new Map(...)`
|
|
2129
|
-
* CLOSURE← `=>` literal
|
|
2130
|
-
* BUFFER ← `new ArrayBuffer(...)`
|
|
2131
|
-
* TYPED ← `new XxxArray(...)` / method returning typed array
|
|
2132
|
-
* (`new DataView(...)` is TYPED but stays boxed — no elem aux)
|
|
2133
|
-
* - not captured in boxed storage (boxed locals stay f64 for the heap slot)
|
|
2134
|
-
* - never compared to null/undefined (we lose the nullish NaN representation)
|
|
2135
|
-
*
|
|
2136
|
-
* Returns Map<name, VAL> of locals to unbox.
|
|
2137
|
-
*/
|
|
2138
|
-
export function unboxablePtrs(body, locals, boxed) {
|
|
2139
|
-
const valOf = name => ctx.func.localReps?.get(name)?.val
|
|
2140
|
-
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.DATE])
|
|
2141
|
-
|
|
2142
|
-
// RHS must produce a fresh, non-null pointer of the declared VAL kind.
|
|
2143
|
-
// OBJECT ← `{…}`
|
|
2144
|
-
// CLOSURE ← `=>`
|
|
2145
|
-
// SET/MAP/BUFFER/TYPED ← `new X(...)`
|
|
2146
|
-
// Validating the exact ctor→VAL match keeps the analysis tied to valTypeOf, so when
|
|
2147
|
-
// that helper grows (e.g. `Array.from` → ARRAY), we don't drift out of sync.
|
|
2148
|
-
const isFreshInit = (expr, kind) => {
|
|
2149
|
-
if (!Array.isArray(expr)) return false
|
|
2150
|
-
if (kind === VAL.OBJECT) {
|
|
2151
|
-
if (expr[0] === '{}') return true
|
|
2152
|
-
// Call to a narrow-ABI'd helper: returns i32 ptr-offset of the same VAL kind.
|
|
2153
|
-
// Unboxing skips the f64-rebox at the callsite. Verifying via sig (not just
|
|
2154
|
-
// valResult) ensures the call already produces an i32 — which dual-write picks
|
|
2155
|
-
// up to bind ptrKind/schemaId on the local.
|
|
2156
|
-
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
2157
|
-
const f = ctx.func.map?.get(expr[1])
|
|
2158
|
-
return f?.sig?.ptrKind === kind
|
|
2159
|
-
}
|
|
2160
|
-
// `let p = arr[i]` where arr has a known elem schema: the runtime helper
|
|
2161
|
-
// returns f64 (NaN-box of an OBJECT pointer), but its low 32 bits are
|
|
2162
|
-
// exactly the pointer offset. Dual-write coerces once via reinterpret/wrap;
|
|
2163
|
-
// subsequent `p.x` reads then become direct `f64.load offset=K (local.get $p)`
|
|
2164
|
-
// (since ptrOffsetIR sees ptrKind=OBJECT and skips the per-access wrap).
|
|
2165
|
-
if (expr[0] === '[]' && typeof expr[1] === 'string') {
|
|
2166
|
-
const repSid = ctx.func.localReps?.get(expr[1])?.arrayElemSchema
|
|
2167
|
-
return repSid != null
|
|
2168
|
-
}
|
|
2169
|
-
return false
|
|
2170
|
-
}
|
|
2171
|
-
if (kind === VAL.CLOSURE) return expr[0] === '=>'
|
|
2172
|
-
if (expr[0] === '()' && typeof expr[1] === 'string') {
|
|
2173
|
-
const callee = expr[1]
|
|
2174
|
-
if (callee.startsWith('new.')) {
|
|
2175
|
-
if (kind === VAL.SET) return callee === 'new.Set'
|
|
2176
|
-
if (kind === VAL.MAP) return callee === 'new.Map'
|
|
2177
|
-
if (kind === VAL.DATE) return callee === 'new.Date'
|
|
2178
|
-
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer'
|
|
2179
|
-
if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
|
|
2180
|
-
}
|
|
2181
|
-
// Call to narrow-ABI'd helper of matching VAL kind.
|
|
2182
|
-
const f = ctx.func.map?.get(callee)
|
|
2183
|
-
if (f?.sig?.ptrKind === kind) return true
|
|
2184
|
-
}
|
|
2185
|
-
// Method call returning TYPED: `arr.map(fn)` where `arr` is in typedElem
|
|
2186
|
-
// (locally TYPED with a known elem ctor). Only `.typed:map` is registered
|
|
2187
|
-
// as TYPED-returning — `.filter`/`.slice` fall back to ARRAY emit. The
|
|
2188
|
-
// typedElem.has(src) gate ensures we don't accept the polymorphic-receiver
|
|
2189
|
-
// path that emits a plain ARRAY result. propagateTyped already mirrored
|
|
2190
|
-
// the src ctor onto the receiver, so the unbox path picks up its aux.
|
|
2191
|
-
if (kind === VAL.TYPED && expr[0] === '()' &&
|
|
2192
|
-
Array.isArray(expr[1]) && expr[1][0] === '.' &&
|
|
2193
|
-
typeof expr[1][1] === 'string' && expr[1][2] === 'map' &&
|
|
2194
|
-
ctx.types.typedElem?.has(expr[1][1])) {
|
|
2195
|
-
return true
|
|
2196
|
-
}
|
|
2197
|
-
return false
|
|
2198
|
-
}
|
|
2199
|
-
// A policy over `scanBindingUses`: an UNBOXABLE-kind `let/const` local with a
|
|
2200
|
-
// fresh-pointer initializer stays unboxable unless some use forbids it. The
|
|
2201
|
-
// only forbidding uses are a reassignment (`=`/compound/`++`/`--`) or a
|
|
2202
|
-
// null/undefined comparison (an unboxed pointer has no nullish NaN form).
|
|
2203
|
-
// Closure captures do not disqualify — a capture-*mutated* local is already
|
|
2204
|
-
// in `boxed`, and a capture-*read* leaves the pointer in its own slot.
|
|
2205
|
-
const result = new Map()
|
|
2206
|
-
for (const [name, s] of scanBindingUses(body)) {
|
|
2207
|
-
const vt = valOf(name)
|
|
2208
|
-
if (!UNBOXABLE_KINDS.has(vt)) continue
|
|
2209
|
-
if (locals.get(name) !== 'f64') continue
|
|
2210
|
-
if (boxed?.has(name)) continue
|
|
2211
|
-
if (!isFreshInit(s.initRhs, vt)) continue
|
|
2212
|
-
const ok = s.uses.every(u =>
|
|
2213
|
-
u.kind !== USE.REASSIGN && !(u.kind === USE.COMPARE && u.nullCmp))
|
|
2214
|
-
if (ok) result.set(name, vt)
|
|
2215
|
-
}
|
|
2216
|
-
return result
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
/**
|
|
2220
|
-
* CSE-safe load bases — `let/const` pointer locals whose `(f64.load offset=K $X)`
|
|
2221
|
-
* reads `cseScalarLoad` (src/optimize.js) may scalar-replace without a store
|
|
2222
|
-
* clobbering them. `cseScalarLoad` is module-wide disabled because it scanned
|
|
2223
|
-
* *every* i32 local; a store through an i32 local legitimately aliasing the load
|
|
2224
|
-
* base returned stale bytes. This pass is the missing soundness gate: a
|
|
2225
|
-
* per-function whitelist, each entry proven non-aliasing — guarantee, not guess.
|
|
2226
|
-
*
|
|
2227
|
-
* `X` qualifies iff ALL hold:
|
|
2228
|
-
* (a) X is an unboxed pointer — `localReps.get(X).ptrKind` set, `locals[X]==='i32'`.
|
|
2229
|
-
* (b) X is bound exactly once (no re-decl, no `=`/`++`/`--`/compound reassign).
|
|
2230
|
-
* (c) Every occurrence of X is the receiver of a `.`/`?.`/`[]` *read* — never a
|
|
2231
|
-
* write target, never a bare value (alias / arg / return / stored element),
|
|
2232
|
-
* never captured by a closure. So X's pointer lives only in `$X`; nothing
|
|
2233
|
-
* else holds it, and no store names it.
|
|
2234
|
-
* (d) The allocation X's bytes live in is disjoint from every store target.
|
|
2235
|
-
* jz allocations carry one kind each and distinct kinds never share bytes,
|
|
2236
|
-
* so X is store-safe when every store's base has a determinable kind ≠ X's
|
|
2237
|
-
* source kind. Any indeterminable store target disqualifies the whole set
|
|
2238
|
-
* (a store through unknown memory could alias anything).
|
|
2239
|
-
*
|
|
2240
|
-
* (c)+(d): no store in the function can touch a cell reachable via `$X + K`, so
|
|
2241
|
-
* a load on `$X` is invariant between two control-flow boundaries — exactly
|
|
2242
|
-
* `cseScalarLoad`'s straight-line region model. Method-call mutations (`.push`,
|
|
2243
|
-
* …) need no accounting here: the pass already flushes its table on every call.
|
|
2244
|
-
*
|
|
2245
|
-
* Returns `Set<name>` — names only, no `$` prefix (the caller stamps it).
|
|
2246
|
-
*/
|
|
2247
|
-
export function cseSafeLoadBases(body, locals, localReps) {
|
|
2248
|
-
if (body === null || typeof body !== 'object') return new Set()
|
|
2249
|
-
|
|
2250
|
-
// Allocation kind a pointer name's bytes live in: ptrKind (unboxed) wins,
|
|
2251
|
-
// else value-kind, else an array-schema'd binding is an ARRAY, else unknown.
|
|
2252
|
-
const kindOf = (name) => {
|
|
2253
|
-
if (typeof name !== 'string') return null
|
|
2254
|
-
const r = localReps?.get(name)
|
|
2255
|
-
return r?.ptrKind || r?.val || (r?.arrayElemSchema != null ? VAL.ARRAY : null) ||
|
|
2256
|
-
ctx.scope.globalValTypes?.get(name) || null
|
|
2257
|
-
}
|
|
2258
|
-
// X's bytes live in: the array/object an element read drew it from
|
|
2259
|
-
// (`X = src[i]` / `X = src.f`), else a fresh `{}`/`new` (X's own kind).
|
|
2260
|
-
const srcKind = (rhs) =>
|
|
2261
|
-
Array.isArray(rhs) && (rhs[0] === '[]' || rhs[0] === '.' || rhs[0] === '?.') &&
|
|
2262
|
-
typeof rhs[1] === 'string' ? kindOf(rhs[1]) : valTypeOf(rhs)
|
|
2263
|
-
|
|
2264
|
-
// Pass 1 — bound-once unboxed-pointer candidates; record each source kind.
|
|
2265
|
-
const cand = new Map() // name → source allocation kind
|
|
2266
|
-
const declCount = new Map()
|
|
2267
|
-
const collect = (node) => {
|
|
2268
|
-
if (!Array.isArray(node)) return
|
|
2269
|
-
const op = node[0]
|
|
2270
|
-
if (op === '=>') return
|
|
2271
|
-
if (op === 'let' || op === 'const') {
|
|
2272
|
-
for (let i = 1; i < node.length; i++) {
|
|
2273
|
-
const a = node[i]
|
|
2274
|
-
if (typeof a === 'string') { declCount.set(a, (declCount.get(a) || 0) + 1); continue }
|
|
2275
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
2276
|
-
const name = a[1]
|
|
2277
|
-
declCount.set(name, (declCount.get(name) || 0) + 1)
|
|
2278
|
-
if (localReps?.get(name)?.ptrKind != null && locals.get(name) === 'i32')
|
|
2279
|
-
cand.set(name, srcKind(a[2]))
|
|
2280
|
-
collect(a[2])
|
|
2281
|
-
} else collect(a)
|
|
2282
|
-
}
|
|
2283
|
-
return
|
|
2284
|
-
}
|
|
2285
|
-
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
2286
|
-
}
|
|
2287
|
-
collect(body)
|
|
2288
|
-
for (const [n, c] of declCount) if (c > 1) cand.delete(n)
|
|
2289
|
-
if (!cand.size) return new Set()
|
|
2290
|
-
|
|
2291
|
-
// Pass 2 — every occurrence must be a `.`/`?.`/`[]` read receiver (c).
|
|
2292
|
-
const live = new Set(cand.keys())
|
|
2293
|
-
const walk = (node, inClosure) => {
|
|
2294
|
-
if (!Array.isArray(node)) return
|
|
2295
|
-
const op = node[0]
|
|
2296
|
-
if (op === 'str') return
|
|
2297
|
-
const closured = inClosure || op === '=>'
|
|
2298
|
-
if (op === 'let' || op === 'const') { // decl `=` — bound name is not a use
|
|
2299
|
-
for (let i = 1; i < node.length; i++) {
|
|
2300
|
-
const a = node[i]
|
|
2301
|
-
if (typeof a === 'string') continue
|
|
2302
|
-
if (Array.isArray(a) && a[0] === '=') {
|
|
2303
|
-
if (typeof a[1] !== 'string') walk(a[1], closured)
|
|
2304
|
-
walk(a[2], closured)
|
|
2305
|
-
} else walk(a, closured)
|
|
2306
|
-
}
|
|
2307
|
-
return
|
|
2308
|
-
}
|
|
2309
|
-
if (op === '.' || op === '?.' || op === '[]') { // member READ — receiver is safe
|
|
2310
|
-
const o = node[1]
|
|
2311
|
-
if (typeof o === 'string') { if (inClosure && cand.has(o)) live.delete(o) }
|
|
2312
|
-
else walk(o, closured)
|
|
2313
|
-
if (op === '[]' && node[2] != null) walk(node[2], closured)
|
|
2314
|
-
return
|
|
2315
|
-
}
|
|
2316
|
-
if (ASSIGN_OPS.has(op) || op === '++' || op === '--' || op === 'delete') {
|
|
2317
|
-
const t = node[1] // write target — X here disqualifies
|
|
2318
|
-
if (typeof t === 'string') { if (cand.has(t)) live.delete(t) }
|
|
2319
|
-
else if (Array.isArray(t) && (t[0] === '.' || t[0] === '?.' || t[0] === '[]') &&
|
|
2320
|
-
typeof t[1] === 'string' && cand.has(t[1])) live.delete(t[1])
|
|
2321
|
-
else walk(t, closured)
|
|
2322
|
-
for (let i = 2; i < node.length; i++) walk(node[i], closured)
|
|
2323
|
-
return
|
|
2324
|
-
}
|
|
2325
|
-
for (let i = 1; i < node.length; i++) { // any other position — bare X escapes
|
|
2326
|
-
const c = node[i]
|
|
2327
|
-
if (typeof c === 'string') { if (cand.has(c)) live.delete(c) }
|
|
2328
|
-
else walk(c, closured)
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
walk(body, false)
|
|
2332
|
-
if (!live.size) return live
|
|
2333
|
-
|
|
2334
|
-
// Pass 3 — store-target disjointness (d). A store lands in `base`'s allocation.
|
|
2335
|
-
let unknownStore = false
|
|
2336
|
-
const storeKinds = new Set()
|
|
2337
|
-
const scanStores = (node) => {
|
|
2338
|
-
if (!Array.isArray(node)) return
|
|
2339
|
-
const op = node[0]
|
|
2340
|
-
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && Array.isArray(node[1]) &&
|
|
2341
|
-
(node[1][0] === '.' || node[1][0] === '?.' || node[1][0] === '[]') &&
|
|
2342
|
-
typeof node[1][1] === 'string') {
|
|
2343
|
-
const k = kindOf(node[1][1])
|
|
2344
|
-
if (k == null) unknownStore = true
|
|
2345
|
-
else storeKinds.add(k)
|
|
2346
|
-
}
|
|
2347
|
-
for (let i = 1; i < node.length; i++) scanStores(node[i])
|
|
2348
|
-
}
|
|
2349
|
-
scanStores(body)
|
|
2350
|
-
if (unknownStore) return new Set()
|
|
2351
|
-
|
|
2352
|
-
const safe = new Set()
|
|
2353
|
-
for (const name of live) {
|
|
2354
|
-
const k = cand.get(name)
|
|
2355
|
-
if (k != null && !storeKinds.has(k)) safe.add(name)
|
|
2356
|
-
}
|
|
2357
|
-
return safe
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
// === Param / closure helpers ===
|
|
2361
|
-
|
|
2362
|
-
export function extractParams(rawParams) {
|
|
2363
|
-
let p = rawParams
|
|
2364
|
-
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
2365
|
-
return p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
export function classifyParam(r) {
|
|
2369
|
-
if (Array.isArray(r) && r[0] === '...') return { kind: 'rest', name: r[1] }
|
|
2370
|
-
if (Array.isArray(r) && r[0] === '=') {
|
|
2371
|
-
if (typeof r[1] === 'string') return { kind: 'default', name: r[1], defValue: r[2] }
|
|
2372
|
-
return { kind: 'destruct-default', pattern: r[1], defValue: r[2] }
|
|
2373
|
-
}
|
|
2374
|
-
if (Array.isArray(r) && (r[0] === '[]' || r[0] === '{}')) return { kind: 'destruct', pattern: r }
|
|
2375
|
-
return { kind: 'plain', name: r }
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
export function collectParamNames(raw, out = new Set()) {
|
|
2379
|
-
for (const r of raw) {
|
|
2380
|
-
if (typeof r === 'string') out.add(r)
|
|
2381
|
-
else if (Array.isArray(r)) {
|
|
2382
|
-
if (r[0] === '=' && typeof r[1] === 'string') out.add(r[1])
|
|
2383
|
-
else if (r[0] === '...' && typeof r[1] === 'string') out.add(r[1])
|
|
2384
|
-
else if (r[0] === '=' && Array.isArray(r[1])) collectParamNames([r[1]], out)
|
|
2385
|
-
else if (r[0] === '[]' || r[0] === '{}' || r[0] === ',') collectParamNames(r.slice(1), out)
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
return out
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
/** Observe shared AST facts on a single node (dyn, schema, arity).
|
|
2392
|
-
* Mutates `f`: { anyDyn, dynVars:Set, hasSchemaLiterals, maxDef, maxCall, hasRest, hasSpread }.
|
|
2393
|
-
* Used by both prepare.js walk and analyze.js walkFacts. */
|
|
2394
|
-
export function observeNodeFacts(node, f) {
|
|
2395
|
-
if (!Array.isArray(node)) return
|
|
2396
|
-
const [op, ...args] = node
|
|
2397
|
-
if (op === '[]') {
|
|
2398
|
-
const [obj, idx] = args
|
|
2399
|
-
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
2400
|
-
} else if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
2401
|
-
const [, obj, idx] = args[0]
|
|
2402
|
-
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
2403
|
-
} else if (op === 'for-in') {
|
|
2404
|
-
f.anyDyn = true
|
|
2405
|
-
if (typeof args[1] === 'string') f.dynVars.add(args[1])
|
|
2406
|
-
} else if (op === '{}') {
|
|
2407
|
-
f.hasSchemaLiterals = true
|
|
2408
|
-
} else if (op === '=>') {
|
|
2409
|
-
let fixedN = 0
|
|
2410
|
-
for (const r of extractParams(args[0])) {
|
|
2411
|
-
if (classifyParam(r).kind === 'rest') f.hasRest = true
|
|
2412
|
-
else fixedN++
|
|
2413
|
-
}
|
|
2414
|
-
if (fixedN > f.maxDef) f.maxDef = fixedN
|
|
2415
|
-
} else if (op === '()') {
|
|
2416
|
-
const a = args[1]
|
|
2417
|
-
const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
2418
|
-
if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) f.hasSpread = true
|
|
2419
|
-
if (callArgs.length > f.maxCall) f.maxCall = callArgs.length
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
/** Find free variables in AST: referenced in node, not in `bound`, present in `scope`. */
|
|
2424
|
-
export function findFreeVars(node, bound, free, scope) {
|
|
2425
|
-
if (node == null) return
|
|
2426
|
-
if (typeof node === 'string') {
|
|
2427
|
-
if (bound.has(node) || free.includes(node)) return
|
|
2428
|
-
const inScope = scope
|
|
2429
|
-
? scope.has(node)
|
|
2430
|
-
: (ctx.func.locals?.has(node) || ctx.func.current?.params.some(p => p.name === node))
|
|
2431
|
-
if (inScope) free.push(node)
|
|
2432
|
-
return
|
|
2433
|
-
}
|
|
2434
|
-
if (!Array.isArray(node)) return
|
|
2435
|
-
const [op, ...args] = node
|
|
2436
|
-
if (op === '=>') {
|
|
2437
|
-
const innerBound = collectParamNames(extractParams(args[0]), new Set(bound))
|
|
2438
|
-
findFreeVars(args[1], innerBound, free, scope)
|
|
2439
|
-
return
|
|
2440
|
-
}
|
|
2441
|
-
if (op === 'catch') {
|
|
2442
|
-
// ['catch', tryBody, errName, handler] — errName is a binding occurrence,
|
|
2443
|
-
// not a reference, and is in scope only inside the handler. Recursing into
|
|
2444
|
-
// it as a plain string would mis-capture an outer var of the same name.
|
|
2445
|
-
findFreeVars(args[0], bound, free, scope)
|
|
2446
|
-
const errName = args[1]
|
|
2447
|
-
const handlerBound = typeof errName === 'string' && errName
|
|
2448
|
-
? new Set(bound).add(errName) : bound
|
|
2449
|
-
findFreeVars(args[2], handlerBound, free, scope)
|
|
2450
|
-
return
|
|
2451
|
-
}
|
|
2452
|
-
if (op === 'let' || op === 'const') {
|
|
2453
|
-
collectParamNames(args, bound)
|
|
2454
|
-
if (scope) collectParamNames(args, scope)
|
|
2455
|
-
}
|
|
2456
|
-
if (op === 'for' && Array.isArray(args[0]) && (args[0][0] === 'let' || args[0][0] === 'const')) {
|
|
2457
|
-
collectParamNames(args[0].slice(1), bound)
|
|
2458
|
-
if (scope) collectParamNames(args[0].slice(1), scope)
|
|
2459
|
-
}
|
|
2460
|
-
for (const a of args) findFreeVars(a, bound, free, scope)
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
/** Check if any of the given variable names are assigned anywhere in the AST. */
|
|
2464
|
-
export function findMutations(node, names, mutated) {
|
|
2465
|
-
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return
|
|
2466
|
-
const [op, ...args] = node
|
|
2467
|
-
if (op === 'let' || op === 'const') {
|
|
2468
|
-
for (const decl of args)
|
|
2469
|
-
if (Array.isArray(decl) && decl[0] === '=') findMutations(decl[2], names, mutated)
|
|
2470
|
-
return
|
|
2471
|
-
}
|
|
2472
|
-
if (ASSIGN_OPS.has(op) && typeof args[0] === 'string' && names.has(args[0]))
|
|
2473
|
-
mutated.add(args[0])
|
|
2474
|
-
if ((op === '++' || op === '--') && typeof args[0] === 'string' && names.has(args[0]))
|
|
2475
|
-
mutated.add(args[0])
|
|
2476
|
-
for (const a of args) findMutations(a, names, mutated)
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* Pre-scan function body for captured variables that are mutated.
|
|
2481
|
-
* Marks mutably-captured vars in ctx.func.boxed for cell-based capture.
|
|
2482
|
-
*/
|
|
2483
|
-
export function boxedCaptures(body) {
|
|
2484
|
-
const outerScope = new Set()
|
|
2485
|
-
;(function collectDecls(node) {
|
|
2486
|
-
if (!Array.isArray(node)) return
|
|
2487
|
-
const [op, ...args] = node
|
|
2488
|
-
if (op === '=>') return
|
|
2489
|
-
if (op === 'let' || op === 'const')
|
|
2490
|
-
collectParamNames(args, outerScope)
|
|
2491
|
-
for (const a of args) collectDecls(a)
|
|
2492
|
-
})(body)
|
|
2493
|
-
if (ctx.func.current?.params) for (const p of ctx.func.current.params) outerScope.add(p.name)
|
|
2494
|
-
if (ctx.func.locals) for (const k of ctx.func.locals.keys()) outerScope.add(k)
|
|
2495
|
-
|
|
2496
|
-
const markArrowCaptures = (node, assignTarget, seen) => {
|
|
2497
|
-
const pnode = node[1]
|
|
2498
|
-
let p = pnode
|
|
2499
|
-
if (Array.isArray(p) && p[0] === '()') p = p[1]
|
|
2500
|
-
const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : [p]) : [p]
|
|
2501
|
-
const paramSet = new Set(raw.map(r => Array.isArray(r) && r[0] === '...' ? r[1] : r))
|
|
2502
|
-
const captures = []
|
|
2503
|
-
findFreeVars(node[2], paramSet, captures, outerScope)
|
|
2504
|
-
if (captures.length === 0) return
|
|
2505
|
-
const captureSet = new Set(captures)
|
|
2506
|
-
const boxed = new Set()
|
|
2507
|
-
findMutations(body, captureSet, boxed)
|
|
2508
|
-
for (const v of captures) if (!seen.has(v)) boxed.add(v)
|
|
2509
|
-
if (assignTarget && captureSet.has(assignTarget)) boxed.add(assignTarget)
|
|
2510
|
-
for (const v of boxed) if (!ctx.func.boxed.has(v)) ctx.func.boxed.set(v, `${T}cell_${v}`)
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
;(function walk(node, assignTarget, seen = new Set(ctx.func.current?.params?.map(p => p.name) || [])) {
|
|
2514
|
-
if (!Array.isArray(node)) return
|
|
2515
|
-
const [op, ...args] = node
|
|
2516
|
-
if (op === '=>') {
|
|
2517
|
-
markArrowCaptures(node, assignTarget, seen)
|
|
2518
|
-
return
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
if (op === ';' || op === '{}') {
|
|
2522
|
-
const blockSeen = new Set(seen)
|
|
2523
|
-
for (const a of args) walk(a, null, blockSeen)
|
|
2524
|
-
return
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
if (op === 'let' || op === 'const') {
|
|
2528
|
-
for (const decl of args) {
|
|
2529
|
-
if (Array.isArray(decl) && decl[0] === '=') walk(decl[2], typeof decl[1] === 'string' ? decl[1] : null, seen)
|
|
2530
|
-
else walk(decl, null, seen)
|
|
2531
|
-
collectParamNames([decl], seen)
|
|
2532
|
-
}
|
|
2533
|
-
return
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
if (op === '=' && typeof args[0] === 'string' && Array.isArray(args[1]) && args[1][0] === '=>')
|
|
2537
|
-
return walk(args[1], args[0], seen)
|
|
2538
|
-
for (const a of args) walk(a, null, seen)
|
|
2539
|
-
})(body)
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
/**
|
|
2543
|
-
* Narrow return arr-elem-{schema|valType}: for each non-exported, non-value-used
|
|
2544
|
-
* user func with `valResult === VAL.ARRAY` and `func[field] == null`, walk return
|
|
2545
|
-
* exprs (and trailing-fallthrough literal), resolve each via body-local elem map
|
|
2546
|
-
* + caller-param facts + transitive user-fn results, and if all agree set `func[field]`.
|
|
2547
|
-
* Lets callers' `const rows = initRows()` gain the elem fact, propagating to
|
|
2548
|
-
* runKernel params via paramReps. `field` selects which fact ('arrayElemSchema'
|
|
2549
|
-
* | 'arrayElemValType') — slice key is derived.
|
|
2550
|
-
*/
|
|
2551
|
-
const _FIELD_TO_SLICE = {
|
|
2552
|
-
arrayElemSchema: 'arrElemSchemas',
|
|
2553
|
-
arrayElemValType: 'arrElemValTypes',
|
|
2554
|
-
}
|
|
2555
|
-
export function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
2556
|
-
const sliceKey = _FIELD_TO_SLICE[field]
|
|
2557
|
-
const targets = ctx.func.list.filter(f =>
|
|
2558
|
-
!f.raw && !f.exported && !valueUsed.has(f.name) &&
|
|
2559
|
-
f.valResult === VAL.ARRAY && f[field] == null
|
|
2560
|
-
)
|
|
2561
|
-
let changed = true
|
|
2562
|
-
while (changed) {
|
|
2563
|
-
changed = false
|
|
2564
|
-
// Cache-staleness barrier: the fixpoint mutates target funcs' [field]
|
|
2565
|
-
// between iterations. analyzeBody reads ctx.func.map[*][field] when
|
|
2566
|
-
// resolving `const x = callee()` and similar chains, so any cached entry
|
|
2567
|
-
// from a prior iter would freeze cross-func propagation. Clear all target
|
|
2568
|
-
// bodies before each sweep.
|
|
2569
|
-
for (const f of targets) _bodyFactsCache.delete(f.body)
|
|
2570
|
-
for (const func of targets) {
|
|
2571
|
-
if (func[field] != null) continue
|
|
2572
|
-
const isBlock = isBlockBody(func.body)
|
|
2573
|
-
if (isBlock && !alwaysReturns(func.body)) continue
|
|
2574
|
-
const exprs = returnExprs(func.body)
|
|
2575
|
-
if (!exprs.length) continue
|
|
2576
|
-
// analyzeBody is context-pure for the arrElem slices, so a single walk
|
|
2577
|
-
// gives both `locals` (for ctx.func.locals seeding — observe filter for
|
|
2578
|
-
// param-aware downstream consumers) and the requested slice.
|
|
2579
|
-
const savedLocals = ctx.func.locals
|
|
2580
|
-
const facts = analyzeBody(func.body)
|
|
2581
|
-
ctx.func.locals = new Map(facts.locals)
|
|
2582
|
-
for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
|
|
2583
|
-
const localElems = facts[sliceKey]
|
|
2584
|
-
ctx.func.locals = savedLocals
|
|
2585
|
-
const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
|
|
2586
|
-
const resolveExpr = (expr) => {
|
|
2587
|
-
if (typeof expr === 'string') {
|
|
2588
|
-
if (localElems.has(expr)) {
|
|
2589
|
-
const v = localElems.get(expr)
|
|
2590
|
-
if (v != null) return v
|
|
2591
|
-
}
|
|
2592
|
-
if (paramElemMap.has(expr)) return paramElemMap.get(expr)
|
|
2593
|
-
return null
|
|
2594
|
-
}
|
|
2595
|
-
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
2596
|
-
const f = ctx.func.map?.get(expr[1])
|
|
2597
|
-
if (f?.[field] != null) return f[field]
|
|
2598
|
-
}
|
|
2599
|
-
if (Array.isArray(expr) && expr[0] === '?:') {
|
|
2600
|
-
const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
|
|
2601
|
-
return a != null && a === b ? a : null
|
|
2602
|
-
}
|
|
2603
|
-
if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
|
|
2604
|
-
const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
|
|
2605
|
-
return a != null && a === b ? a : null
|
|
2606
|
-
}
|
|
2607
|
-
return null
|
|
2608
|
-
}
|
|
2609
|
-
const v0 = resolveExpr(exprs[0])
|
|
2610
|
-
if (v0 == null) continue
|
|
2611
|
-
if (!exprs.every(e => resolveExpr(e) === v0)) continue
|
|
2612
|
-
func[field] = v0
|
|
2613
|
-
changed = true
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
/**
|
|
2619
|
-
* Whole-program SRoA eligibility — decides which object schemas may back an
|
|
2620
|
-
* `Array<S>` with the `structInline` carrier (the K f64 schema fields inlined
|
|
2621
|
-
* per element, no per-row heap object). Writes `ctx.schema.inlineArray:
|
|
2622
|
-
* Set<sid>`, read by the array push / index / length codegen.
|
|
2623
|
-
*
|
|
2624
|
-
* Default-disqualify: a schema is inlinable only when *every* observed use of
|
|
2625
|
-
* every `Array<S>` binding — across all user functions and module inits — is
|
|
2626
|
-
* one the structInline codegen handles. A missed or unrecognized use poisons
|
|
2627
|
-
* the schema, so the worst outcome is a lost optimization, never a stride
|
|
2628
|
-
* mismatch (miscompile).
|
|
2629
|
-
*
|
|
2630
|
-
* Handled uses of an `Array<S>` binding `a`:
|
|
2631
|
-
* - decl/reassign from `[]` (empty), a call returning `Array<S>`, or an alias
|
|
2632
|
-
* - `a.push({S-literal})` — struct push (K-cell store)
|
|
2633
|
-
* - `a.length` — physical len / K
|
|
2634
|
-
* - `a[i]` consumed as `const p = a[i]` cursor, or directly `a[i].field`
|
|
2635
|
-
* - `a` passed where the callee param is `Array<S>` (paramReps agreement)
|
|
2636
|
-
* - `return a` when the enclosing function returns `Array<S>`
|
|
2637
|
-
* A cursor `p` (`const p = a[i]`) may only be read/written as `p.field`.
|
|
2638
|
-
* Anything else — bare ref, value escape, other array method, `a[i] = …`
|
|
2639
|
-
* element-replace — poisons S.
|
|
2640
|
-
*
|
|
2641
|
-
* Reads codegen truth: a binding is `Array<S>` iff its settled rep
|
|
2642
|
-
* (`funcFacts.get(func).localReps`) carries `arrayElemSchema = S` — the exact
|
|
2643
|
-
* map the emitter consults — so the analysis and the emitter never disagree on
|
|
2644
|
-
* which bindings are inline-carried.
|
|
2645
|
-
*
|
|
2646
|
-
* Conservative corners (sound, give up the optimization): closures and module
|
|
2647
|
-
* inits are not walked in detail — any schema reachable as a `.push({S})`
|
|
2648
|
-
* argument, an `Array<S>`-returning call, an `[{S}, …]` literal, or a captured
|
|
2649
|
-
* tracked array inside one is poisoned.
|
|
2650
|
-
*/
|
|
2651
|
-
export function analyzeStructInline(funcFacts, programFacts) {
|
|
2652
|
-
const inlineArray = ctx.schema?.inlineArray
|
|
2653
|
-
if (!inlineArray || !ctx.schema?.list) return
|
|
2654
|
-
const { paramReps } = programFacts
|
|
2655
|
-
const cand = new Set() // sids observed as an `Array<S>` element schema
|
|
2656
|
-
const black = new Set() // sids disqualified by some use
|
|
2657
|
-
|
|
2658
|
-
const propsOf = (sid) => ctx.schema.list[sid] || []
|
|
2659
|
-
const inSchema = (sid, p) => typeof p === 'string' && propsOf(sid).includes(p)
|
|
2660
|
-
const isStrLit = (k) => Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string'
|
|
2661
|
-
|
|
2662
|
-
// Argument list of a `['()', callee, argNode]` call node.
|
|
2663
|
-
const argsOf = (node) => {
|
|
2664
|
-
const a = node[2]
|
|
2665
|
-
return a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
// `name` referenced anywhere as a value (skips `:`/`.` property-name slots).
|
|
2669
|
-
const mentions = (node, name) => {
|
|
2670
|
-
if (typeof node === 'string') return node === name
|
|
2671
|
-
if (!Array.isArray(node)) return false
|
|
2672
|
-
const op = node[0]
|
|
2673
|
-
if (op === 'str') return false
|
|
2674
|
-
if (op === ':') return mentions(node[2], name)
|
|
2675
|
-
if (op === '.' || op === '?.') return mentions(node[1], name)
|
|
2676
|
-
for (let i = 1; i < node.length; i++) if (mentions(node[i], name)) return true
|
|
2677
|
-
return false
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
// Poison every schema whose `Array<S>` could materialize inside an un-walked
|
|
2681
|
-
// subtree (closure body / module init): `.push({S})` args, `Array<S>`-returning
|
|
2682
|
-
// calls, `[{S}, …]` array literals. Standalone `{S}` objects are independent
|
|
2683
|
-
// of array layout and intentionally left alone.
|
|
2684
|
-
const poisonAll = (node) => {
|
|
2685
|
-
if (!Array.isArray(node)) return
|
|
2686
|
-
const op = node[0]
|
|
2687
|
-
if (op === '()') {
|
|
2688
|
-
const callee = node[1]
|
|
2689
|
-
if (typeof callee === 'string') {
|
|
2690
|
-
const sid = ctx.func.map?.get(callee)?.arrayElemSchema
|
|
2691
|
-
if (sid != null) black.add(sid)
|
|
2692
|
-
} else if (Array.isArray(callee) && callee[0] === '.' && callee[2] === 'push') {
|
|
2693
|
-
for (const a of argsOf(node)) {
|
|
2694
|
-
const sid = objLiteralSchemaId(a)
|
|
2695
|
-
if (sid != null) black.add(sid)
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
} else if (op === '[' || op === '[]') {
|
|
2699
|
-
for (const el of staticArrayElems(node) || []) {
|
|
2700
|
-
const sid = objLiteralSchemaId(el)
|
|
2701
|
-
if (sid != null) black.add(sid)
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
for (let i = 1; i < node.length; i++) poisonAll(node[i])
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
for (const [func, facts] of funcFacts) {
|
|
2708
|
-
const body = func?.body
|
|
2709
|
-
const reps = facts?.localReps
|
|
2710
|
-
if (func?.raw || !reps || body == null || typeof body !== 'object') continue
|
|
2711
|
-
|
|
2712
|
-
// `Array<S>` bindings of this function (codegen truth) and their schemas.
|
|
2713
|
-
const arrName = new Map() // name → sid
|
|
2714
|
-
for (const [name, r] of reps) {
|
|
2715
|
-
const sid = r?.arrayElemSchema
|
|
2716
|
-
if (sid == null) continue
|
|
2717
|
-
if ((propsOf(sid).length || 0) < 1) continue // K=0 — not inlinable
|
|
2718
|
-
cand.add(sid)
|
|
2719
|
-
arrName.set(name, sid)
|
|
2720
|
-
}
|
|
2721
|
-
if (!arrName.size) continue
|
|
2722
|
-
|
|
2723
|
-
// A structInline `Array<S>` value is only ever born from an empty `[]`
|
|
2724
|
-
// grown by structInline `.push`. `expr` is such a producer of `Array<sid>`
|
|
2725
|
-
// iff it is: a tracked `Array<sid>` alias, an empty `[]` literal, or a call
|
|
2726
|
-
// to a user function (whose returned array is structInline whenever sid
|
|
2727
|
-
// survives this whole-program pass). Every other source — a non-empty
|
|
2728
|
-
// `[{S},…]` literal, a builtin call (`JSON.parse`, `Object.values`, `.map`,
|
|
2729
|
-
// `.slice`, a member access onto a parsed object) — yields a taggedLinear
|
|
2730
|
-
// array and must poison sid.
|
|
2731
|
-
const safeArrSource = (expr, sid) => {
|
|
2732
|
-
if (typeof expr === 'string') return arrName.get(expr) === sid
|
|
2733
|
-
if (!Array.isArray(expr)) return false
|
|
2734
|
-
const elems = staticArrayElems(expr)
|
|
2735
|
-
if (elems) return elems.length === 0
|
|
2736
|
-
return expr[0] === '()' && typeof expr[1] === 'string' && !!ctx.func.map?.has(expr[1])
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// Pass 1 — collect `const p = a[i]` cursors; drop on name clash / re-decl.
|
|
2740
|
-
const cursor = new Map() // name → sid
|
|
2741
|
-
const declSeen = new Set()
|
|
2742
|
-
const collectCursors = (node) => {
|
|
2743
|
-
if (!Array.isArray(node) || node[0] === '=>') return
|
|
2744
|
-
if (node[0] === 'let' || node[0] === 'const') {
|
|
2745
|
-
for (let i = 1; i < node.length; i++) {
|
|
2746
|
-
const d = node[i]
|
|
2747
|
-
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
2748
|
-
const name = d[1], rhs = d[2]
|
|
2749
|
-
if (declSeen.has(name)) { const s = cursor.get(name); if (s != null) black.add(s) }
|
|
2750
|
-
declSeen.add(name)
|
|
2751
|
-
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 &&
|
|
2752
|
-
typeof rhs[1] === 'string' && arrName.has(rhs[1]) && !isStrLit(rhs[2])) {
|
|
2753
|
-
const sid = arrName.get(rhs[1])
|
|
2754
|
-
if (cursor.has(name) || arrName.has(name)) black.add(sid)
|
|
2755
|
-
else cursor.set(name, sid)
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
for (let i = 1; i < node.length; i++) collectCursors(node[i])
|
|
2760
|
-
}
|
|
2761
|
-
collectCursors(body)
|
|
2762
|
-
|
|
2763
|
-
// A `['[]', arrName, idx]` element read of a tracked array → its sid.
|
|
2764
|
-
const elemArrSid = (n) =>
|
|
2765
|
-
Array.isArray(n) && n[0] === '[]' && n.length === 3 &&
|
|
2766
|
-
typeof n[1] === 'string' && arrName.has(n[1]) && !isStrLit(n[2])
|
|
2767
|
-
? arrName.get(n[1]) : null
|
|
2768
|
-
|
|
2769
|
-
// Pass 2 — verify every occurrence is a structInline-handled use.
|
|
2770
|
-
const flag = (c) => {
|
|
2771
|
-
if (typeof c !== 'string') return false
|
|
2772
|
-
if (arrName.has(c)) { black.add(arrName.get(c)); return true }
|
|
2773
|
-
if (cursor.has(c)) { black.add(cursor.get(c)); return true }
|
|
2774
|
-
return false
|
|
2775
|
-
}
|
|
2776
|
-
const visitChild = (c) => { if (!flag(c)) verify(c) }
|
|
2777
|
-
|
|
2778
|
-
function verify(node) {
|
|
2779
|
-
if (!Array.isArray(node)) return
|
|
2780
|
-
const op = node[0]
|
|
2781
|
-
if (op === 'str') return
|
|
2782
|
-
if (op === '=>') { // closure — un-walked, poison
|
|
2783
|
-
for (const n of arrName.keys()) if (mentions(node, n)) black.add(arrName.get(n))
|
|
2784
|
-
for (const [n, s] of cursor) if (mentions(node, n)) black.add(s)
|
|
2785
|
-
poisonAll(node)
|
|
2786
|
-
return
|
|
2787
|
-
}
|
|
2788
|
-
if (op === ':') { visitChild(node[2]); return }
|
|
2789
|
-
|
|
2790
|
-
if (op === '.' || op === '?.') {
|
|
2791
|
-
const o = node[1], p = node[2]
|
|
2792
|
-
if (typeof o === 'string') {
|
|
2793
|
-
if (arrName.has(o)) { if (!(op === '.' && p === 'length')) black.add(arrName.get(o)) }
|
|
2794
|
-
else if (cursor.has(o)) { if (!(op === '.' && inSchema(cursor.get(o), p))) black.add(cursor.get(o)) }
|
|
2795
|
-
return
|
|
2796
|
-
}
|
|
2797
|
-
const esid = elemArrSid(o)
|
|
2798
|
-
if (esid != null) {
|
|
2799
|
-
if (!(op === '.' && inSchema(esid, p))) black.add(esid)
|
|
2800
|
-
visitChild(o[2])
|
|
2801
|
-
return
|
|
2802
|
-
}
|
|
2803
|
-
visitChild(o)
|
|
2804
|
-
return
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
if (op === '[]') {
|
|
2808
|
-
const o = node[1], k = node[2]
|
|
2809
|
-
if (typeof o === 'string') {
|
|
2810
|
-
if (arrName.has(o)) black.add(arrName.get(o)) // element value escape
|
|
2811
|
-
else if (cursor.has(o)) { if (!(isStrLit(k) && inSchema(cursor.get(o), k[1]))) black.add(cursor.get(o)) }
|
|
2812
|
-
if (k != null) visitChild(k)
|
|
2813
|
-
return
|
|
2814
|
-
}
|
|
2815
|
-
const esid = elemArrSid(o)
|
|
2816
|
-
if (esid != null) {
|
|
2817
|
-
if (!(isStrLit(k) && inSchema(esid, k[1]))) black.add(esid)
|
|
2818
|
-
visitChild(o[2])
|
|
2819
|
-
} else if (o != null) visitChild(o)
|
|
2820
|
-
if (k != null) visitChild(k)
|
|
2821
|
-
return
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
// Reassignment of the array binding — the rhs must be a structInline
|
|
2825
|
-
// `Array<S>` producer; an alias is left un-walked (flagging it would
|
|
2826
|
-
// self-poison), other producers are walked to verify their subtree.
|
|
2827
|
-
if (op === '=' && typeof node[1] === 'string' && arrName.has(node[1])) {
|
|
2828
|
-
const sid = arrName.get(node[1])
|
|
2829
|
-
if (!safeArrSource(node[2], sid)) black.add(sid)
|
|
2830
|
-
else if (typeof node[2] !== 'string') visitChild(node[2])
|
|
2831
|
-
return
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
if (op === '()') {
|
|
2835
|
-
const callee = node[1]
|
|
2836
|
-
if (Array.isArray(callee) && callee[0] === '.') {
|
|
2837
|
-
const recv = callee[1], method = callee[2]
|
|
2838
|
-
if (typeof recv === 'string' && arrName.has(recv)) {
|
|
2839
|
-
const sid = arrName.get(recv)
|
|
2840
|
-
const args = argsOf(node)
|
|
2841
|
-
if (method !== 'push' || !args.length) black.add(sid)
|
|
2842
|
-
else for (const arg of args) {
|
|
2843
|
-
if (Array.isArray(arg) && arg[0] === '{}' && objLiteralSchemaId(arg) === sid) {
|
|
2844
|
-
for (let i = 1; i < arg.length; i++) {
|
|
2845
|
-
const pr = arg[i]
|
|
2846
|
-
visitChild(Array.isArray(pr) && pr[0] === ':' ? pr[2] : pr)
|
|
2847
|
-
}
|
|
2848
|
-
} else black.add(sid)
|
|
2849
|
-
}
|
|
2850
|
-
return
|
|
2851
|
-
}
|
|
2852
|
-
if (typeof recv === 'string' && cursor.has(recv)) { black.add(cursor.get(recv)); return }
|
|
2853
|
-
const esid = elemArrSid(recv)
|
|
2854
|
-
if (esid != null) { black.add(esid); visitChild(recv[2]) }
|
|
2855
|
-
else visitChild(recv)
|
|
2856
|
-
for (const a of argsOf(node)) visitChild(a)
|
|
2857
|
-
return
|
|
2858
|
-
}
|
|
2859
|
-
if (typeof callee === 'string') {
|
|
2860
|
-
const args = argsOf(node)
|
|
2861
|
-
const known = ctx.func.map?.has(callee)
|
|
2862
|
-
const cParams = paramReps?.get(callee)
|
|
2863
|
-
for (let k = 0; k < args.length; k++) {
|
|
2864
|
-
const arg = args[k]
|
|
2865
|
-
if (typeof arg === 'string' && arrName.has(arg)) {
|
|
2866
|
-
const sid = arrName.get(arg)
|
|
2867
|
-
if (!(known && cParams?.get(k)?.arrayElemSchema === sid)) black.add(sid)
|
|
2868
|
-
} else if (!flag(arg)) verify(arg)
|
|
2869
|
-
}
|
|
2870
|
-
return
|
|
2871
|
-
}
|
|
2872
|
-
visitChild(callee)
|
|
2873
|
-
for (const a of argsOf(node)) visitChild(a)
|
|
2874
|
-
return
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
|
-
if (op === 'return') {
|
|
2878
|
-
const e = node[1]
|
|
2879
|
-
if (typeof e === 'string') {
|
|
2880
|
-
if (arrName.has(e)) { if (func.arrayElemSchema !== arrName.get(e)) black.add(arrName.get(e)) }
|
|
2881
|
-
else flag(e)
|
|
2882
|
-
return
|
|
2883
|
-
}
|
|
2884
|
-
// A function typed `Array<S>` must return a structInline producer —
|
|
2885
|
-
// a non-empty literal / builtin call here yields a taggedLinear array.
|
|
2886
|
-
if (func.arrayElemSchema != null && !safeArrSource(e, func.arrayElemSchema))
|
|
2887
|
-
black.add(func.arrayElemSchema)
|
|
2888
|
-
const esid = elemArrSid(e)
|
|
2889
|
-
if (esid != null) { black.add(esid); visitChild(e[2]); return }
|
|
2890
|
-
if (e != null) visitChild(e)
|
|
2891
|
-
return
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
if (op === 'let' || op === 'const') {
|
|
2895
|
-
for (let i = 1; i < node.length; i++) {
|
|
2896
|
-
const d = node[i]
|
|
2897
|
-
if (!Array.isArray(d) || d[0] !== '=') { if (Array.isArray(d)) visitChild(d); continue }
|
|
2898
|
-
const name = d[1], rhs = d[2]
|
|
2899
|
-
if (typeof name === 'string' && cursor.has(name) &&
|
|
2900
|
-
Array.isArray(rhs) && rhs[0] === '[]') {
|
|
2901
|
-
if (rhs[2] != null) visitChild(rhs[2]) // cursor decl — verify index only
|
|
2902
|
-
continue
|
|
2903
|
-
}
|
|
2904
|
-
if (typeof name === 'string' && arrName.has(name)) {
|
|
2905
|
-
const sid = arrName.get(name)
|
|
2906
|
-
if (!safeArrSource(rhs, sid)) black.add(sid) // non-structInline producer
|
|
2907
|
-
else if (typeof rhs !== 'string') visitChild(rhs) // [] / user-call — verify subtree
|
|
2908
|
-
continue
|
|
2909
|
-
}
|
|
2910
|
-
if (typeof name !== 'string') visitChild(name)
|
|
2911
|
-
visitChild(rhs)
|
|
2912
|
-
}
|
|
2913
|
-
return
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
for (let i = 1; i < node.length; i++) visitChild(node[i])
|
|
2917
|
-
}
|
|
2918
|
-
verify(body)
|
|
2919
|
-
}
|
|
2920
|
-
|
|
2921
|
-
// Module inits are not walked in detail — poison any schema whose array form
|
|
2922
|
-
// could appear there (struct-array consumed/built at module scope).
|
|
2923
|
-
if (ctx.module?.moduleInits) for (const mi of ctx.module.moduleInits) poisonAll(mi)
|
|
2924
|
-
|
|
2925
|
-
for (const sid of cand) if (!black.has(sid)) inlineArray.add(sid)
|
|
2926
|
-
}
|
|
2927
|
-
|
|
2928
|
-
/** Schema id when `name` is bound (codegen truth) to a structInline `Array<S>`,
|
|
2929
|
-
* else null. `ctx.func.localReps` is the per-function rep map the emitter
|
|
2930
|
-
* consults for element-schema facts; `ctx.schema.inlineArray` is the
|
|
2931
|
-
* whole-program eligibility set filled by `analyzeStructInline`. Read together
|
|
2932
|
-
* so the emitter never inline-carries a binding the analysis rejected. */
|
|
2933
|
-
export function inlineArraySid(name) {
|
|
2934
|
-
if (typeof name !== 'string') return null
|
|
2935
|
-
const sid = ctx.func.localReps?.get(name)?.arrayElemSchema
|
|
2936
|
-
return sid != null && ctx.schema.inlineArray?.has(sid) ? sid : null
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
/**
|
|
2940
|
-
* Whole-program function-namespace SRoA analysis.
|
|
2941
|
-
*
|
|
2942
|
-
* A user function used as a property bag — `parse.space = …; parse.step()` —
|
|
2943
|
-
* is otherwise compiled as a dynamic object: each `f.prop` write becomes a
|
|
2944
|
-
* `__dyn_set` into a hash side-table keyed by the closure pointer, each read a
|
|
2945
|
-
* `__dyn_get`. But a function's property table can never be observed by the
|
|
2946
|
-
* host (the host gets only the callable; the table lives in jz linear memory),
|
|
2947
|
-
* so the property set is statically closed — jz sees every `f.PROP` site. When
|
|
2948
|
-
* `f` never escapes as a bare value, each property dissolves:
|
|
2949
|
-
* - written once, at module top level, to a known function → the property is
|
|
2950
|
-
* constant: every `f.PROP` site rewrites straight to that function name
|
|
2951
|
-
* (direct calls, no storage at all).
|
|
2952
|
-
* - otherwise → a mutable f64 module global (`global.get` / `global.set`).
|
|
2953
|
-
*
|
|
2954
|
-
* Returns `Map<funcName, { disq, props:Set, valRead:Set,
|
|
2955
|
-
* writes:Map<prop,[{rhs,atInit}]> }>` — `valRead` is the subset of props read
|
|
2956
|
-
* as a value (not merely called). `flattenFuncNamespaces` (plan.js) turns it
|
|
2957
|
-
* into the rewrite. A name carrying `disq` escapes / is reassigned / is
|
|
2958
|
-
* computed-indexed and must not be touched.
|
|
2959
|
-
*/
|
|
2960
|
-
export function analyzeFuncNamespaces(ast) {
|
|
2961
|
-
const funcNames = ctx.func.names
|
|
2962
|
-
if (!funcNames || !funcNames.size) return new Map()
|
|
2963
|
-
|
|
2964
|
-
const ns = new Map()
|
|
2965
|
-
const rec = (name) => {
|
|
2966
|
-
let r = ns.get(name)
|
|
2967
|
-
if (!r) ns.set(name, r = { disq: false, props: new Set(), valRead: new Set(), writes: new Map() })
|
|
2968
|
-
return r
|
|
2969
|
-
}
|
|
2970
|
-
// `['.'|'?.', f, P]` member where f is a known function and P a string key.
|
|
2971
|
-
const memberOf = (n) =>
|
|
2972
|
-
Array.isArray(n) && (n[0] === '.' || n[0] === '?.') &&
|
|
2973
|
-
isFuncRef(n[1], funcNames) && typeof n[2] === 'string' ? n : null
|
|
2974
|
-
|
|
2975
|
-
// `atInit` — node is a direct top-level statement (constant-fold candidate);
|
|
2976
|
-
// read only at the `=` handler, never propagated into sub-expressions.
|
|
2977
|
-
function visit(node, atInit) {
|
|
2978
|
-
if (typeof node === 'string') {
|
|
2979
|
-
// Bare mention of a known function in value position — it escapes; an
|
|
2980
|
-
// alias could reach its property table. Disqualify.
|
|
2981
|
-
if (funcNames.has(node)) rec(node).disq = true
|
|
2982
|
-
return
|
|
2983
|
-
}
|
|
2984
|
-
if (!Array.isArray(node)) return
|
|
2985
|
-
const op = node[0]
|
|
2986
|
-
|
|
2987
|
-
if (op === 'let' || op === 'const') {
|
|
2988
|
-
for (let i = 1; i < node.length; i++) {
|
|
2989
|
-
const d = node[i]
|
|
2990
|
-
if (Array.isArray(d) && d[0] === '=') {
|
|
2991
|
-
if (!isFuncRef(d[1], funcNames)) visit(d[1], false) // skip f's own decl
|
|
2992
|
-
// `let f = f` is prepare's self-name placeholder for a lifted function —
|
|
2993
|
-
// skip it (a bare visit would falsely disqualify f). Any other funcRef
|
|
2994
|
-
// rhs (`let g = f`) is a real alias and must disqualify f.
|
|
2995
|
-
if (!(isFuncRef(d[2], funcNames) && d[2] === d[1])) visit(d[2], false)
|
|
2996
|
-
} else visit(d, false)
|
|
2997
|
-
}
|
|
2998
|
-
return
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
|
-
if (op === 'export') {
|
|
3002
|
-
// Exporting the function value is safe — the host gets the callable,
|
|
3003
|
-
// never the linear-memory property table. Skip bare function children.
|
|
3004
|
-
for (let i = 1; i < node.length; i++)
|
|
3005
|
-
if (!isFuncRef(node[i], funcNames)) visit(node[i], false)
|
|
3006
|
-
return
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
if (op === '=') {
|
|
3010
|
-
const m = memberOf(node[1])
|
|
3011
|
-
if (m) {
|
|
3012
|
-
const r = rec(m[1]); r.props.add(m[2])
|
|
3013
|
-
let w = r.writes.get(m[2]); if (!w) r.writes.set(m[2], w = [])
|
|
3014
|
-
w.push({ rhs: node[2], atInit })
|
|
3015
|
-
visit(node[2], false)
|
|
3016
|
-
return
|
|
3017
|
-
}
|
|
3018
|
-
if (isFuncRef(node[1], funcNames)) rec(node[1]).disq = true // reassignment
|
|
3019
|
-
else visit(node[1], false)
|
|
3020
|
-
visit(node[2], false)
|
|
3021
|
-
return
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
if (op === '()') {
|
|
3025
|
-
const m = memberOf(node[1])
|
|
3026
|
-
if (m) rec(m[1]).props.add(m[2])
|
|
3027
|
-
else if (!isFuncRef(node[1], funcNames)) visit(node[1], false) // bare f(...) ok
|
|
3028
|
-
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
3029
|
-
return
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
|
-
// `f.PROP` / `f?.PROP` as a plain value (read) — not the callee of a call
|
|
3033
|
-
// (those are handled by the `()` branch above). A value-read means the
|
|
3034
|
-
// property's stored value must stay retrievable; devirt cannot drop it.
|
|
3035
|
-
const m = memberOf(node)
|
|
3036
|
-
if (m) { const r = rec(m[1]); r.props.add(m[2]); r.valRead.add(m[2]); return }
|
|
3037
|
-
|
|
3038
|
-
// Computed `f[k]` — the key set is no longer static.
|
|
3039
|
-
if (op === '[]' && isFuncRef(node[1], funcNames)) {
|
|
3040
|
-
rec(node[1]).disq = true
|
|
3041
|
-
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
3042
|
-
return
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
for (let i = 1; i < node.length; i++) visit(node[i], false)
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
const visitTop = (n) => {
|
|
3049
|
-
if (Array.isArray(n) && n[0] === ';')
|
|
3050
|
-
for (let i = 1; i < n.length; i++) visit(n[i], true)
|
|
3051
|
-
else visit(n, true)
|
|
3052
|
-
}
|
|
3053
|
-
visitTop(ast)
|
|
3054
|
-
// Bundled multi-module programs keep each module's top-level statements in
|
|
3055
|
-
// moduleInits, not `ast` — the `f.prop = …` writes that define a namespace
|
|
3056
|
-
// live there. Walk them at init scope so writes are recorded and an escape
|
|
3057
|
-
// inside init code still disqualifies.
|
|
3058
|
-
for (const mi of ctx.module.moduleInits || []) visitTop(mi)
|
|
3059
|
-
for (const fn of ctx.func.list) if (fn.body && !fn.raw) visit(fn.body, false)
|
|
3060
|
-
|
|
3061
|
-
return ns
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3064
|
-
/**
|
|
3065
|
-
* Phase: program-fact collection.
|
|
3066
|
-
*
|
|
3067
|
-
* Single whole-program walk over the module AST + each user function body
|
|
3068
|
-
* + all moduleInits. Collects:
|
|
3069
|
-
* dynVars/anyDyn — vars accessed via runtime key (drives strict mode +
|
|
3070
|
-
* 1 __dyn_get fallback gating)
|
|
3071
|
-
* propMap — property assignments per receiver (auto-box schemas)
|
|
3072
|
-
* valueUsed — ctx.func.names passed as first-class values (excluded
|
|
3073
|
-
* from internal narrowing — they need uniform $ftN ABI)
|
|
3074
|
-
* maxDef/maxCall — closure ABI width inputs
|
|
3075
|
-
* hasRest/hasSpread
|
|
3076
|
-
* callSites — `{ callee, argList, callerFunc, node }` for static-name
|
|
3077
|
-
* calls (drives the type/schema fixpoint without
|
|
3078
|
-
* re-walking the AST). `node` is the call AST itself,
|
|
3079
|
-
* mutable for bimorphic-typed clone routing.
|
|
3080
|
-
* paramReps — Map<funcName, Map<paramIdx, ValueRep>>, empty here;
|
|
3081
|
-
* populated by narrowSignatures (per-field lattice) and
|
|
3082
|
-
* read by emitFunc.
|
|
3083
|
-
*
|
|
3084
|
-
* Also writes ctx.schema.slotTypes (static-key object literal slot val types).
|
|
3085
|
-
*
|
|
3086
|
-
* Three visit modes:
|
|
3087
|
-
* full=true (ast + user funcs) → all facts including call-site collection
|
|
3088
|
-
* full=false (moduleInits) → dyn + arity only (no propMap/valueUsed/
|
|
3089
|
-
* callSites: moduleInits don't own user
|
|
3090
|
-
* props/funcs)
|
|
3091
|
-
* inArrow=true → flips off call-site collection so
|
|
3092
|
-
* closure-internal calls don't poison
|
|
3093
|
-
* caller-context type inference.
|
|
3094
|
-
*/
|
|
3095
|
-
export function collectProgramFacts(ast) {
|
|
3096
|
-
const paramReps = new Map()
|
|
3097
|
-
const valueUsed = new Set()
|
|
3098
|
-
const propMap = new Map()
|
|
3099
|
-
const callSites = []
|
|
3100
|
-
const doSchema = ast && ctx.schema.register
|
|
3101
|
-
const doArity = !!ctx.closure.make
|
|
3102
|
-
const f = { dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false, maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false }
|
|
3103
|
-
// Slot-type observation lives in the dedicated `observeProgramSlots` pass below;
|
|
3104
|
-
// walkFacts only registers schemas (which is local to the AST node).
|
|
3105
|
-
const walkFacts = (node, full, inArrow, callerFunc) => {
|
|
3106
|
-
if (!Array.isArray(node)) return
|
|
3107
|
-
const [op, ...args] = node
|
|
3108
|
-
// shared dyn/schema/arity facts (duplicated pattern synced with prepare.js)
|
|
3109
|
-
observeNodeFacts(node, f)
|
|
3110
|
-
// strict for-in check
|
|
3111
|
-
if (op === 'for-in' && ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
|
|
3112
|
-
// schema registration (analyze-specific)
|
|
3113
|
-
if (op === '{}' && doSchema) {
|
|
3114
|
-
const parsed = staticObjectProps(args)
|
|
3115
|
-
if (parsed) ctx.schema.register(parsed.names)
|
|
3116
|
-
}
|
|
3117
|
-
// Crossing into a closure body: from now on, no call-site collection (matches the
|
|
3118
|
-
// pre-fusion scanCalls bailing at '=>'). Still walks children for arity/dyn.
|
|
3119
|
-
if (op === '=>') {
|
|
3120
|
-
for (const a of args) walkFacts(a, full, true, callerFunc)
|
|
3121
|
-
return
|
|
3122
|
-
}
|
|
3123
|
-
if (full) {
|
|
3124
|
-
// property-assignment scan for auto-box
|
|
3125
|
-
if (doSchema && op === '=' && Array.isArray(args[0]) && args[0][0] === '.') {
|
|
3126
|
-
const [, obj, prop] = args[0]
|
|
3127
|
-
if (typeof obj === 'string' && (ctx.scope.globals.has(obj) || ctx.func.names.has(obj))) {
|
|
3128
|
-
if (!propMap.has(obj)) propMap.set(obj, new Set())
|
|
3129
|
-
propMap.get(obj).add(prop)
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
// first-class function-value + static-call-site scan
|
|
3133
|
-
if (op === '()' && isFuncRef(args[0], ctx.func.names)) {
|
|
3134
|
-
if (!inArrow) {
|
|
3135
|
-
const a = args[1]
|
|
3136
|
-
const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
3137
|
-
callSites.push({ callee: args[0], argList, callerFunc, node })
|
|
3138
|
-
}
|
|
3139
|
-
for (let i = 1; i < args.length; i++) {
|
|
3140
|
-
const a = args[i]
|
|
3141
|
-
if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
|
|
3142
|
-
else walkFacts(a, true, inArrow, callerFunc)
|
|
3143
|
-
}
|
|
3144
|
-
return
|
|
3145
|
-
}
|
|
3146
|
-
if ((op === '.' || op === '?.') && isFuncRef(args[0], ctx.func.names)) return
|
|
3147
|
-
for (const a of args) {
|
|
3148
|
-
if (isFuncRef(a, ctx.func.names)) valueUsed.add(a)
|
|
3149
|
-
else walkFacts(a, true, inArrow, callerFunc)
|
|
3150
|
-
}
|
|
3151
|
-
} else {
|
|
3152
|
-
for (const a of args) walkFacts(a, false, inArrow, callerFunc)
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
walkFacts(ast, true, false, null)
|
|
3156
|
-
for (const func of ctx.func.list) if (func.body && !func.raw) walkFacts(func.body, true, false, func)
|
|
3157
|
-
const initFacts = ctx.module.initFacts
|
|
3158
|
-
if (initFacts) {
|
|
3159
|
-
if (initFacts.anyDyn) {
|
|
3160
|
-
f.anyDyn = true
|
|
3161
|
-
for (const v of initFacts.dynVars) f.dynVars.add(v)
|
|
3162
|
-
}
|
|
3163
|
-
if (doArity) {
|
|
3164
|
-
if (initFacts.maxDef > f.maxDef) f.maxDef = initFacts.maxDef
|
|
3165
|
-
if (initFacts.maxCall > f.maxCall) f.maxCall = initFacts.maxCall
|
|
3166
|
-
if (initFacts.hasRest) f.hasRest = true
|
|
3167
|
-
if (initFacts.hasSpread) f.hasSpread = true
|
|
3168
|
-
}
|
|
3169
|
-
if (doSchema && initFacts.hasSchemaLiterals) f.hasSchemaLiterals = true
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
// Slot-type observation pass: walk every `{}` literal with the right scope's
|
|
3173
|
-
// valTypes installed as `ctx.func.localValTypesOverlay` so shorthand `{x}`
|
|
3174
|
-
// (expanded by prepare to `[':', x, x]`) and chained typed-array reads resolve
|
|
3175
|
-
// through valTypeOf → lookupValType. Skips into closures — they're observed via
|
|
3176
|
-
// their own func.list entry. The overlay is the per-function analyzeBody.valTypes
|
|
3177
|
-
// map (already populated with the same overlay-aware walk).
|
|
3178
|
-
if (doSchema && f.hasSchemaLiterals) {
|
|
3179
|
-
observeProgramSlots(ast)
|
|
3180
|
-
// Per-slot intCertain mirror of the per-binding lattice. Runs after slot
|
|
3181
|
-
// type observation (which it does not depend on) — same trigger gate so
|
|
3182
|
-
// programs without schema literals skip both. Re-runnable: subsequent
|
|
3183
|
-
// collectProgramFacts invocations (E2 phase) overwrite the same map; the
|
|
3184
|
-
// analysis is monotone-down so re-running can only widen poisoning, never
|
|
3185
|
-
// un-poison — safe.
|
|
3186
|
-
analyzeSchemaSlotIntCertain(ast)
|
|
3187
|
-
}
|
|
3188
|
-
|
|
3189
|
-
return {
|
|
3190
|
-
dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
3191
|
-
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
3192
|
-
paramReps, hasSchemaLiterals: f.hasSchemaLiterals,
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
/** Walk `ast` + every user function body + module inits, observing slot types
|
|
3197
|
-
* on each `{}` literal. Per-function bodies have their analyzeBody.valTypes
|
|
3198
|
-
* installed as overlay so shorthand `{x}` resolves through local consts.
|
|
3199
|
-
*
|
|
3200
|
-
* Re-runnable: compile.js calls this once during collectProgramFacts (before
|
|
3201
|
-
* E2 valResult inference), then again after E2 — on the second pass, valTypeOf
|
|
3202
|
-
* on user-function calls resolves via `f.valResult`, lifting slots whose value
|
|
3203
|
-
* is `const x = userFn(...)` from `undefined` to `NUMBER`/etc.
|
|
3204
|
-
* observeSlot's first-wins-then-clash rule means later precise observations
|
|
3205
|
-
* upgrade undefined slots without re-poisoning already-monomorphic ones. */
|
|
3206
|
-
export function observeProgramSlots(ast) {
|
|
3207
|
-
if (!ctx.schema?.register) return
|
|
3208
|
-
const slotTypes = ctx.schema.slotTypes
|
|
3209
|
-
const observeSlot = (sid, idx, vt) => {
|
|
3210
|
-
if (!vt) return
|
|
3211
|
-
let arr = slotTypes.get(sid)
|
|
3212
|
-
if (!arr) { arr = []; slotTypes.set(sid, arr) }
|
|
3213
|
-
while (arr.length <= idx) arr.push(undefined)
|
|
3214
|
-
if (arr[idx] === null) return
|
|
3215
|
-
if (arr[idx] === undefined) arr[idx] = vt
|
|
3216
|
-
else if (arr[idx] !== vt) arr[idx] = null
|
|
3217
|
-
}
|
|
3218
|
-
const visit = (node) => {
|
|
3219
|
-
if (!Array.isArray(node)) return
|
|
3220
|
-
const op = node[0]
|
|
3221
|
-
if (op === '=>') return
|
|
3222
|
-
if (op === '{}') {
|
|
3223
|
-
const parsed = staticObjectProps(node.slice(1))
|
|
3224
|
-
if (parsed) {
|
|
3225
|
-
const sid = ctx.schema.register(parsed.names)
|
|
3226
|
-
for (let i = 0; i < parsed.values.length; i++) {
|
|
3227
|
-
observeSlot(sid, i, valTypeOf(parsed.values[i]))
|
|
3228
|
-
}
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
for (let i = 1; i < node.length; i++) visit(node[i])
|
|
3232
|
-
}
|
|
3233
|
-
const prevOverlay = ctx.func.localValTypesOverlay
|
|
3234
|
-
if (ast) { ctx.func.localValTypesOverlay = null; visit(ast) }
|
|
3235
|
-
for (const func of ctx.func.list) {
|
|
3236
|
-
if (!func.body || func.raw) continue
|
|
3237
|
-
ctx.func.localValTypesOverlay = analyzeBody(func.body).valTypes
|
|
3238
|
-
visit(func.body)
|
|
3239
|
-
}
|
|
3240
|
-
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
3241
|
-
ctx.func.localValTypesOverlay = null
|
|
3242
|
-
for (const mi of ctx.module.moduleInits) visit(mi)
|
|
3243
|
-
}
|
|
3244
|
-
ctx.func.localValTypesOverlay = prevOverlay
|
|
3245
|
-
}
|
|
3246
|
-
|
|
3247
|
-
/** Whole-program slot intCertain observation.
|
|
3248
|
-
*
|
|
3249
|
-
* A schema slot `(sid, idx)` is `intCertain` iff every write to it across the
|
|
3250
|
-
* program is integer-shaped (literal int, bitwise op, intCertain local read,
|
|
3251
|
-
* …). Mirrors `analyzeIntCertain`'s `isIntExpr` rules but works at module
|
|
3252
|
-
* scope: each body gets a local `intCertain` fixpoint over its own bindings,
|
|
3253
|
-
* then schema writes within that body are reduced against the fixpoint.
|
|
3254
|
-
*
|
|
3255
|
-
* Global poison semantics: any non-int write to a slot — in any body —
|
|
3256
|
-
* permanently flips it false. Slots never observed stay `undefined`.
|
|
3257
|
-
*
|
|
3258
|
-
* Cross-function flow (slot written from a call's return value) is **not**
|
|
3259
|
-
* tracked — those writes count as non-int and poison the slot. Conservative:
|
|
3260
|
-
* produces only false negatives, never false positives. */
|
|
3261
|
-
export function analyzeSchemaSlotIntCertain(ast) {
|
|
3262
|
-
if (!ctx.schema?.register) return
|
|
3263
|
-
const slotIntCertain = ctx.schema.slotIntCertain
|
|
3264
|
-
const poisonSlot = (sid, idx) => {
|
|
3265
|
-
let arr = slotIntCertain.get(sid)
|
|
3266
|
-
if (!arr) { arr = []; slotIntCertain.set(sid, arr) }
|
|
3267
|
-
while (arr.length <= idx) arr.push(undefined)
|
|
3268
|
-
arr[idx] = false
|
|
3269
|
-
}
|
|
3270
|
-
const observeSlot = (sid, idx, isInt) => {
|
|
3271
|
-
let arr = slotIntCertain.get(sid)
|
|
3272
|
-
if (!arr) { arr = []; slotIntCertain.set(sid, arr) }
|
|
3273
|
-
while (arr.length <= idx) arr.push(undefined)
|
|
3274
|
-
if (arr[idx] === false) return
|
|
3275
|
-
if (!isInt) { arr[idx] = false; return }
|
|
3276
|
-
if (arr[idx] === undefined) arr[idx] = true
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
// Per-body fixpoint: replicates analyzeIntCertain's two-pass collect/iterate
|
|
3280
|
-
// but stays local to the body so it can run during collectProgramFacts
|
|
3281
|
-
// (before emit-time inferLocals sets per-function intCertain reps).
|
|
3282
|
-
const analyzeBodyLocally = (body) => {
|
|
3283
|
-
const defs = new Map()
|
|
3284
|
-
const pushDef = (name, rhs) => {
|
|
3285
|
-
let list = defs.get(name)
|
|
3286
|
-
if (!list) { list = []; defs.set(name, list) }
|
|
3287
|
-
list.push(rhs)
|
|
3288
|
-
}
|
|
3289
|
-
const collect = (node) => {
|
|
3290
|
-
if (!Array.isArray(node)) return
|
|
3291
|
-
const [op, ...args] = node
|
|
3292
|
-
if (op === '=>') return
|
|
3293
|
-
if (op === 'let' || op === 'const') {
|
|
3294
|
-
for (const a of args)
|
|
3295
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
|
|
3296
|
-
} else if (op === '=' && typeof args[0] === 'string') {
|
|
3297
|
-
pushDef(args[0], args[1])
|
|
3298
|
-
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
3299
|
-
!INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
3300
|
-
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
3301
|
-
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
3302
|
-
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
3303
|
-
}
|
|
3304
|
-
for (const a of args) collect(a)
|
|
3305
|
-
}
|
|
3306
|
-
collect(body)
|
|
3307
|
-
const intCertain = new Map()
|
|
3308
|
-
for (const name of defs.keys()) intCertain.set(name, true)
|
|
3309
|
-
const isIntExpr = (expr) => {
|
|
3310
|
-
if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
|
|
3311
|
-
if (typeof expr === 'boolean') return true
|
|
3312
|
-
if (typeof expr === 'string') return intCertain.get(expr) === true
|
|
3313
|
-
if (!Array.isArray(expr)) return false
|
|
3314
|
-
const sv = staticValue(expr)
|
|
3315
|
-
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
|
|
3316
|
-
const [op, ...args] = expr
|
|
3317
|
-
if (op == null) {
|
|
3318
|
-
const v = args[0]
|
|
3319
|
-
if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
|
|
3320
|
-
if (typeof v === 'boolean') return true
|
|
3321
|
-
return false
|
|
3322
|
-
}
|
|
3323
|
-
if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
|
|
3324
|
-
if (op === '.') {
|
|
3325
|
-
if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
|
|
3326
|
-
const vt = lookupValType(args[0])
|
|
3327
|
-
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
3328
|
-
}
|
|
3329
|
-
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
3330
|
-
const vt = lookupValType(args[0])
|
|
3331
|
-
return vt === VAL.SET || vt === VAL.MAP
|
|
3332
|
-
}
|
|
3333
|
-
return false
|
|
3334
|
-
}
|
|
3335
|
-
if (INT_CLOSED_OPS.has(op)) {
|
|
3336
|
-
const a = isIntExpr(args[0])
|
|
3337
|
-
const b = args[1] != null ? isIntExpr(args[1]) : a
|
|
3338
|
-
return a && b
|
|
3339
|
-
}
|
|
3340
|
-
if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
|
|
3341
|
-
if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
|
|
3342
|
-
if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
|
|
3343
|
-
if (op === '()') {
|
|
3344
|
-
const c = args[0]
|
|
3345
|
-
if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
|
|
3346
|
-
if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
|
|
3347
|
-
}
|
|
3348
|
-
return false
|
|
3349
|
-
}
|
|
3350
|
-
let changed = true
|
|
3351
|
-
while (changed) {
|
|
3352
|
-
changed = false
|
|
3353
|
-
for (const [name, rhsList] of defs) {
|
|
3354
|
-
if (!intCertain.get(name)) continue
|
|
3355
|
-
if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
return isIntExpr
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
// Body walker: for each `{}` literal observe per-slot intCertain; for each
|
|
3362
|
-
// `obj.prop = expr` write, poison-or-confirm the slot resolved via the
|
|
3363
|
-
// schema attached to `obj` (ValueRep `schemaId` or `ctx.schema.vars`).
|
|
3364
|
-
const visit = (node, isInt) => {
|
|
3365
|
-
if (!Array.isArray(node)) return
|
|
3366
|
-
const op = node[0]
|
|
3367
|
-
if (op === '=>') return
|
|
3368
|
-
if (op === '{}') {
|
|
3369
|
-
const parsed = staticObjectProps(node.slice(1))
|
|
3370
|
-
if (parsed) {
|
|
3371
|
-
const sid = ctx.schema.register(parsed.names)
|
|
3372
|
-
for (let i = 0; i < parsed.values.length; i++) observeSlot(sid, i, isInt(parsed.values[i]))
|
|
3373
|
-
}
|
|
3374
|
-
} else if (op === '=' && Array.isArray(node[1]) && node[1][0] === '.') {
|
|
3375
|
-
const [, obj, prop] = node[1]
|
|
3376
|
-
if (typeof obj === 'string') {
|
|
3377
|
-
// Same precise-path resolution as ctx.schema.slotVT — no structural
|
|
3378
|
-
// fallback (slot index could differ across schemas with the same prop).
|
|
3379
|
-
const sid = repOf(obj)?.schemaId ?? ctx.schema.vars.get(obj)
|
|
3380
|
-
if (sid != null) {
|
|
3381
|
-
const idx = ctx.schema.list[sid]?.indexOf(prop)
|
|
3382
|
-
if (idx >= 0) observeSlot(sid, idx, isInt(node[2]))
|
|
3383
|
-
else if (idx < 0) {/* off-schema write — irrelevant to existing slots */}
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
for (let i = 1; i < node.length; i++) visit(node[i], isInt)
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
if (ast) visit(ast, analyzeBodyLocally(ast))
|
|
3391
|
-
for (const func of ctx.func.list) {
|
|
3392
|
-
if (!func.body || func.raw) continue
|
|
3393
|
-
visit(func.body, analyzeBodyLocally(func.body))
|
|
3394
|
-
}
|
|
3395
|
-
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
3396
|
-
for (const mi of ctx.module.moduleInits) visit(mi, analyzeBodyLocally(mi))
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
// =============================================================================
|
|
3401
|
-
// AST predicate helpers — pure functions over jz AST arrays
|
|
3402
|
-
// =============================================================================
|
|
3403
|
-
// AST nodes are strings (identifiers), numbers (literals), or arrays where [0]
|
|
3404
|
-
// is the operator tag (e.g. ['+', a, b], ['=>', params, body]). [null, value]
|
|
3405
|
-
// denotes a parenthesized/boxed literal.
|
|
3406
|
-
|
|
3407
|
-
export const MAX_SMALL_FOR_UNROLL = 8
|
|
3408
|
-
export const MAX_NESTED_FOR_UNROLL = 64
|
|
3409
|
-
|
|
3410
|
-
/** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`.
|
|
3411
|
-
* Conservative over-reject: if unsure, treat as written.
|
|
3412
|
-
* `let`/`const` declarations are NOT reassignments — only the initializer expressions
|
|
3413
|
-
* inside them are scanned. */
|
|
3414
|
-
export function isReassigned(body, name) {
|
|
3415
|
-
if (!Array.isArray(body)) return false
|
|
3416
|
-
const op = body[0]
|
|
3417
|
-
if (ASSIGN_OPS.has(op) && body[1] === name) return true
|
|
3418
|
-
if ((op === '++' || op === '--') && body[1] === name) return true
|
|
3419
|
-
if (op === 'let' || op === 'const') {
|
|
3420
|
-
for (let i = 1; i < body.length; i++) {
|
|
3421
|
-
const d = body[i]
|
|
3422
|
-
if (Array.isArray(d) && d[0] === '=' && d[2] != null && isReassigned(d[2], name)) return true
|
|
3423
|
-
}
|
|
3424
|
-
return false
|
|
3425
|
-
}
|
|
3426
|
-
for (let i = 1; i < body.length; i++) if (isReassigned(body[i], name)) return true
|
|
3427
|
-
return false
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
/** Does `body` contain a `continue` that targets THIS loop?
|
|
3431
|
-
* A `continue` inside a nested `for`/`while`/`do` targets the inner loop, so we don't count it. */
|
|
3432
|
-
export function hasOwnContinue(body) {
|
|
3433
|
-
if (!Array.isArray(body)) return false
|
|
3434
|
-
const op = body[0]
|
|
3435
|
-
if (op === 'continue') return true
|
|
3436
|
-
if (op === 'for' || op === 'while' || op === 'do') return false
|
|
3437
|
-
for (let i = 1; i < body.length; i++) if (hasOwnContinue(body[i])) return true
|
|
3438
|
-
return false
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
export function hasOwnBreakOrContinue(body) {
|
|
3442
|
-
if (!Array.isArray(body)) return false
|
|
3443
|
-
const op = body[0]
|
|
3444
|
-
if (op === 'break' || op === 'continue') return true
|
|
3445
|
-
if (op === 'for' || op === 'while' || op === 'do' || op === '=>') return false
|
|
3446
|
-
for (let i = 1; i < body.length; i++) if (hasOwnBreakOrContinue(body[i])) return true
|
|
3447
|
-
return false
|
|
3448
|
-
}
|
|
3449
|
-
|
|
3450
|
-
export function containsNestedClosure(body) {
|
|
3451
|
-
if (!Array.isArray(body)) return false
|
|
3452
|
-
if (body[0] === '=>') return true
|
|
3453
|
-
for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
|
|
3454
|
-
return false
|
|
3455
|
-
}
|
|
3456
|
-
|
|
3457
|
-
export function containsNestedLoop(body) {
|
|
3458
|
-
if (!Array.isArray(body)) return false
|
|
3459
|
-
const op = body[0]
|
|
3460
|
-
if (op === 'for' || op === 'while' || op === 'do') return true
|
|
3461
|
-
if (op === '=>') return false
|
|
3462
|
-
for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
|
|
3463
|
-
return false
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
/** Recursive loop size estimator — product of trip counts for nested `for (let i=0; i<N; i++)` loops. */
|
|
3467
|
-
export function nestedSmallLoopBudget(body) {
|
|
3468
|
-
if (!Array.isArray(body)) return 1
|
|
3469
|
-
if (body[0] === '=>') return 1
|
|
3470
|
-
if (body[0] === 'for') {
|
|
3471
|
-
const [, init, cond, step, loopBody] = body
|
|
3472
|
-
const n = smallConstForTripCount(init, cond, step)
|
|
3473
|
-
return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
|
|
3474
|
-
}
|
|
3475
|
-
let max = 1
|
|
3476
|
-
for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
|
|
3477
|
-
return max
|
|
3478
|
-
}
|
|
3479
|
-
|
|
3480
|
-
export function containsDeclOf(body, name) {
|
|
3481
|
-
if (!Array.isArray(body)) return false
|
|
3482
|
-
const op = body[0]
|
|
3483
|
-
if (op === '=>') return false
|
|
3484
|
-
if (op === 'let' || op === 'const') {
|
|
3485
|
-
for (let i = 1; i < body.length; i++) {
|
|
3486
|
-
const d = body[i]
|
|
3487
|
-
if (d === name) return true
|
|
3488
|
-
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
3489
|
-
}
|
|
3490
|
-
}
|
|
3491
|
-
for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
|
|
3492
|
-
return false
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
/** Clone AST node, substituting bare-name matches with [null, value]. Skips into closures. */
|
|
3496
|
-
export function cloneWithSubst(node, name, value) {
|
|
3497
|
-
if (node === name) return [null, value]
|
|
3498
|
-
if (!Array.isArray(node)) return node
|
|
3499
|
-
if (node[0] === '=>') return node
|
|
3500
|
-
return node.map(x => cloneWithSubst(x, name, value))
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
|
-
/** Does `body` access a typed-array element by string name known to the type system? */
|
|
3504
|
-
export function containsKnownTypedArrayIndex(body) {
|
|
3505
|
-
if (!Array.isArray(body)) return false
|
|
3506
|
-
if (body[0] === '=>') return false
|
|
3507
|
-
if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
|
|
3508
|
-
for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
|
|
3509
|
-
return false
|
|
3510
|
-
}
|
|
3511
|
-
|
|
3512
|
-
/** Analyze `for (let i=0; i<N; i++)` trip count. Returns N if structurally matches, else null. */
|
|
3513
|
-
export function smallConstForTripCount(init, cond, step) {
|
|
3514
|
-
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
3515
|
-
const decl = init[1]
|
|
3516
|
-
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
3517
|
-
const name = decl[1]
|
|
3518
|
-
const start = intLiteralValue(decl[2])
|
|
3519
|
-
if (start !== 0) return null
|
|
3520
|
-
|
|
3521
|
-
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
3522
|
-
const end = intLiteralValue(cond[2])
|
|
3523
|
-
if (end == null || end < 0 || end > MAX_SMALL_FOR_UNROLL) return null
|
|
3524
|
-
|
|
3525
|
-
const stepOk = Array.isArray(step) && (
|
|
3526
|
-
(step[0] === '++' && step[1] === name) ||
|
|
3527
|
-
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
3528
|
-
)
|
|
3529
|
-
return stepOk ? end : null
|
|
3530
|
-
}
|
|
3531
|
-
|
|
3532
|
-
// =============================================================================
|
|
3533
|
-
// charCodeAt in-bounds proof
|
|
3534
|
-
// =============================================================================
|
|
3535
|
-
// `String.prototype.charCodeAt` returns NaN for an out-of-range index, so the
|
|
3536
|
-
// generic codegen contract is an f64 result (see module/string.js). When the
|
|
3537
|
-
// index is the induction variable of a `for (let i = C; i < recv.length; i++)`
|
|
3538
|
-
// loop, every `recv.charCodeAt(i)` in the loop body is statically inside
|
|
3539
|
-
// `[0, recv.length)` — OOB is impossible — so the call may use the cheaper i32
|
|
3540
|
-
// (raw-byte) contract instead. This is a static guarantee, not a guess.
|
|
3541
|
-
|
|
3542
|
-
/** Step expression of a `for` that increments `name` by exactly 1. */
|
|
3543
|
-
function isUnitIncrement(step, name) {
|
|
3544
|
-
if (!Array.isArray(step)) return false
|
|
3545
|
-
if (step[0] === '++' && step[1] === name) return true
|
|
3546
|
-
// postfix `i++` in value position lowers to `(++i) - 1`
|
|
3547
|
-
if (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++'
|
|
3548
|
-
&& step[1][1] === name && intLiteralValue(step[2]) === 1) return true
|
|
3549
|
-
return false
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
/** `let`/`const` re-declaration of `name` within `node` — does not cross `=>`
|
|
3553
|
-
* (a closure has its own scope; collection already stops at closure boundaries). */
|
|
3554
|
-
function redeclaresName(node, name) {
|
|
3555
|
-
if (!Array.isArray(node) || node[0] === '=>') return false
|
|
3556
|
-
if (node[0] === 'let' || node[0] === 'const') {
|
|
3557
|
-
for (let k = 1; k < node.length; k++) {
|
|
3558
|
-
const d = node[k]
|
|
3559
|
-
if (d === name) return true
|
|
3560
|
-
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
for (let k = 1; k < node.length; k++) if (redeclaresName(node[k], name)) return true
|
|
3564
|
-
return false
|
|
3565
|
-
}
|
|
3566
|
-
|
|
3567
|
-
/** Collect `recv.charCodeAt(idxVar)` callee nodes within `node`. Stops at `=>`:
|
|
3568
|
-
* a closure may run after the loop, when `idxVar` has reached `recv.length`. */
|
|
3569
|
-
function collectBoundedCC(node, recv, idxVar, set) {
|
|
3570
|
-
if (!Array.isArray(node) || node[0] === '=>') return
|
|
3571
|
-
if (node[0] === '()' && node.length === 3 && node[2] === idxVar
|
|
3572
|
-
&& Array.isArray(node[1]) && node[1][0] === '.'
|
|
3573
|
-
&& node[1][1] === recv && node[1][2] === 'charCodeAt')
|
|
3574
|
-
set.add(node[1])
|
|
3575
|
-
for (let k = 1; k < node.length; k++) collectBoundedCC(node[k], recv, idxVar, set)
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
/** Receiver of a `.length` expression, possibly wrapped in `(… | 0)` — the
|
|
3579
|
-
* shape `prepare` produces when it hoists a for-cond bound. */
|
|
3580
|
-
function lengthRecv(expr) {
|
|
3581
|
-
if (Array.isArray(expr) && expr[0] === '|' && intLiteralValue(expr[2]) === 0) expr = expr[1]
|
|
3582
|
-
if (Array.isArray(expr) && expr[0] === '.' && expr[2] === 'length'
|
|
3583
|
-
&& typeof expr[1] === 'string') return expr[1]
|
|
3584
|
-
return null
|
|
3585
|
-
}
|
|
3586
|
-
|
|
3587
|
-
/** Flatten `let`/`const` declarations (incl. `;`-joined groups) into `out`,
|
|
3588
|
-
* mapping each declared name to its initializer expression. */
|
|
3589
|
-
function collectDecls(node, out) {
|
|
3590
|
-
if (!Array.isArray(node)) return
|
|
3591
|
-
if (node[0] === ';') { for (let k = 1; k < node.length; k++) collectDecls(node[k], out); return }
|
|
3592
|
-
if (node[0] === 'let' || node[0] === 'const') {
|
|
3593
|
-
for (let k = 1; k < node.length; k++) {
|
|
3594
|
-
const d = node[k]
|
|
3595
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') out.set(d[1], d[2])
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
/** Walk `node`, recording in `set` the `charCodeAt` callee nodes proven in-bounds
|
|
3601
|
-
* by an enclosing canonical induction loop `for (let i = C; i < recv.length; i++)`.
|
|
3602
|
-
* Matches the post-`prepare` shape, where the `.length` bound is hoisted into a
|
|
3603
|
-
* temp (`cond` becomes `i < lenTmp`, `lenTmp` declared in `init`). */
|
|
3604
|
-
export function scanBoundedLoops(node, set) {
|
|
3605
|
-
if (!Array.isArray(node)) return
|
|
3606
|
-
if (node[0] === 'for' && node.length === 5) {
|
|
3607
|
-
const [, init, cond, step, body] = node
|
|
3608
|
-
let idx = null, recv = null, boundVar = null
|
|
3609
|
-
if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
|
|
3610
|
-
const decls = new Map()
|
|
3611
|
-
collectDecls(init, decls)
|
|
3612
|
-
idx = cond[1]
|
|
3613
|
-
// index must be declared in `init` as `let i = C`, C an integer literal ≥ 0
|
|
3614
|
-
const start = decls.has(idx) ? intLiteralValue(decls.get(idx)) : null
|
|
3615
|
-
if (start == null || start < 0) idx = null
|
|
3616
|
-
// bound is `recv.length`, directly or via a hoisted temp declared in `init`
|
|
3617
|
-
let bound = cond[2]
|
|
3618
|
-
if (typeof bound === 'string') { boundVar = bound; bound = decls.get(bound) }
|
|
3619
|
-
recv = lengthRecv(bound)
|
|
3620
|
-
}
|
|
3621
|
-
// step `i++`; body never writes `i`/`recv`/the bound temp (incl. via
|
|
3622
|
-
// closures) and never re-declares `i`. Then every bare `i` in the body
|
|
3623
|
-
// satisfies `0 ≤ C ≤ i < recv.length`.
|
|
3624
|
-
if (idx && recv && idx !== recv && isUnitIncrement(step, idx)
|
|
3625
|
-
&& !isReassigned(body, idx) && !isReassigned(body, recv)
|
|
3626
|
-
&& (boundVar == null || !isReassigned(body, boundVar))
|
|
3627
|
-
&& !redeclaresName(body, idx))
|
|
3628
|
-
collectBoundedCC(body, recv, idx, set)
|
|
3629
|
-
}
|
|
3630
|
-
for (let k = 1; k < node.length; k++) scanBoundedLoops(node[k], set)
|
|
3631
|
-
}
|
|
3632
|
-
|
|
3633
|
-
const NO_BOUNDED_CC = new Set() // shared immutable empty result
|
|
3634
|
-
|
|
3635
|
-
/** Set of `['.', recv, 'charCodeAt']` callee nodes in the current function whose
|
|
3636
|
-
* index argument is provably within `[0, recv.length)`. Memoised per body. */
|
|
3637
|
-
export function inBoundsCharCodeAt(ctx) {
|
|
3638
|
-
const body = ctx.func?.body
|
|
3639
|
-
if (!Array.isArray(body)) return NO_BOUNDED_CC
|
|
3640
|
-
if (ctx.func._ccBody === body) return ctx.func.ccInBounds
|
|
3641
|
-
const set = new Set()
|
|
3642
|
-
scanBoundedLoops(body, set)
|
|
3643
|
-
ctx.func.ccInBounds = set
|
|
3644
|
-
ctx.func._ccBody = body
|
|
3645
|
-
return set
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
/** Does `body` always exit the enclosing scope (return / throw / break / continue)? */
|
|
3649
|
-
export function isTerminator(body) {
|
|
3650
|
-
if (!Array.isArray(body)) return false
|
|
3651
|
-
const op = body[0]
|
|
3652
|
-
if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
|
|
3653
|
-
if (op === '{}' || op === ';') {
|
|
3654
|
-
for (let i = body.length - 1; i >= 1; i--) {
|
|
3655
|
-
const s = body[i]
|
|
3656
|
-
if (s == null) continue
|
|
3657
|
-
return isTerminator(s)
|
|
3658
|
-
}
|
|
3659
|
-
return false
|
|
3660
|
-
}
|
|
3661
|
-
return false
|
|
3662
|
-
}
|
|
3663
|
-
|
|
3664
|
-
// =============================================================================
|
|
3665
|
-
// JSON-shape inference
|
|
3666
|
-
// =============================================================================
|
|
3667
|
-
// What a binding looks like when its provenance is a compile-time-known
|
|
3668
|
-
// `JSON.parse(stringConst)`. Building this tree at compile time lets
|
|
3669
|
-
// `.prop` and `[i]` reads on the result recover their VAL kind without a
|
|
3670
|
-
// runtime probe.
|
|
3671
|
-
|
|
3672
|
-
/** Resolve a string-constant source for an expression: literal forms, or a
|
|
3673
|
-
* binding the scope tracker has recorded as effectively-const. Module/json's
|
|
3674
|
-
* static-fold path keeps a constStrs-only resolver to avoid folding `let`-bound
|
|
3675
|
-
* initializers; shape inference is sound on the broader shapeStrs because an
|
|
3676
|
-
* effectively-const literal's value is invariant. */
|
|
3677
|
-
export function jsonConstString(expr) {
|
|
3678
|
-
if (Array.isArray(expr) && expr[0] === 'str' && typeof expr[1] === 'string') return expr[1]
|
|
3679
|
-
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
|
|
3680
|
-
if (typeof expr === 'string') {
|
|
3681
|
-
return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
|
|
3682
|
-
}
|
|
3683
|
-
return null
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
function jsonShapeStrings(expr) {
|
|
3687
|
-
const single = jsonConstString(expr)
|
|
3688
|
-
if (single != null) return [single]
|
|
3689
|
-
if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
|
|
3690
|
-
return null
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
/** Build a structural shape tree from a parsed JSON value. Each node is
|
|
3694
|
-
* `{ val, props?, elem? }` — `val` is the inferred VAL kind (matches
|
|
3695
|
-
* rep.val in localReps entries). Lets `valTypeOf` propagate VAL kinds
|
|
3696
|
-
* through `.prop` chains and `[i]` reads on bindings sourced from
|
|
3697
|
-
* `JSON.parse` of a compile-time-known string. Polymorphic arrays drop
|
|
3698
|
-
* their `elem`. */
|
|
3699
|
-
function shapeOfJsonValue(v) {
|
|
3700
|
-
if (v === null || v === undefined) return null
|
|
3701
|
-
if (typeof v === 'number') return { val: VAL.NUMBER }
|
|
3702
|
-
if (typeof v === 'string') return { val: VAL.STRING }
|
|
3703
|
-
if (typeof v === 'boolean') return { val: VAL.NUMBER }
|
|
3704
|
-
if (Array.isArray(v)) {
|
|
3705
|
-
let elem = null
|
|
3706
|
-
for (const x of v) {
|
|
3707
|
-
const s = shapeOfJsonValue(x)
|
|
3708
|
-
if (!s) { elem = null; break }
|
|
3709
|
-
if (!elem) elem = s
|
|
3710
|
-
else if (!shapeUnifies(elem, s)) { elem = null; break }
|
|
3711
|
-
}
|
|
3712
|
-
return { val: VAL.ARRAY, elem }
|
|
3713
|
-
}
|
|
3714
|
-
if (typeof v === 'object') {
|
|
3715
|
-
const props = Object.create(null)
|
|
3716
|
-
const names = Object.keys(v)
|
|
3717
|
-
for (const k of names) {
|
|
3718
|
-
const s = shapeOfJsonValue(v[k])
|
|
3719
|
-
if (s) props[k] = s
|
|
3720
|
-
}
|
|
3721
|
-
return { val: VAL.OBJECT, props, names }
|
|
3722
|
-
}
|
|
3723
|
-
return null
|
|
3724
|
-
}
|
|
3725
|
-
|
|
3726
|
-
function shapeUnifies(a, b) {
|
|
3727
|
-
if (!a || !b || a.val !== b.val) return false
|
|
3728
|
-
if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
|
|
3729
|
-
const ak = Object.keys(a.props), bk = Object.keys(b.props)
|
|
3730
|
-
if (ak.length !== bk.length) return false
|
|
3731
|
-
for (const k of ak) {
|
|
3732
|
-
if (!b.props[k] || !shapeUnifies(a.props[k], b.props[k])) return false
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
if (a.val === VAL.ARRAY) {
|
|
3736
|
-
if ((a.elem == null) !== (b.elem == null)) return false
|
|
3737
|
-
if (a.elem && !shapeUnifies(a.elem, b.elem)) return false
|
|
3738
|
-
}
|
|
3739
|
-
return true
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
function shapeLayoutUnifies(a, b) {
|
|
3743
|
-
if (!shapeUnifies(a, b)) return false
|
|
3744
|
-
if (a.val === VAL.OBJECT || a.val === VAL.HASH) {
|
|
3745
|
-
if (a.names?.length !== b.names?.length) return false
|
|
3746
|
-
for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
|
|
3747
|
-
}
|
|
3748
|
-
if (a.val === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
|
|
3749
|
-
return true
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
|
-
function parseJsonShape(src) {
|
|
3753
|
-
if (typeof src !== 'string') return null
|
|
3754
|
-
let parsed
|
|
3755
|
-
try { parsed = JSON.parse(src) } catch { return null }
|
|
3756
|
-
return shapeOfJsonValue(parsed)
|
|
3757
|
-
}
|
|
3758
|
-
|
|
3759
|
-
function parseUnifiedJsonShape(srcs) {
|
|
3760
|
-
if (!srcs?.length) return null
|
|
3761
|
-
let out = null
|
|
3762
|
-
for (const src of srcs) {
|
|
3763
|
-
const sh = parseJsonShape(src)
|
|
3764
|
-
if (!sh) return null
|
|
3765
|
-
if (!out) out = sh
|
|
3766
|
-
else if (!shapeLayoutUnifies(out, sh)) return null
|
|
3767
|
-
}
|
|
3768
|
-
return out
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
/** Resolve the json shape for an expression by walking name → rep.jsonShape and
|
|
3772
|
-
* `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
|
|
3773
|
-
export function shapeOf(expr) {
|
|
3774
|
-
if (typeof expr === 'string')
|
|
3775
|
-
return ctx.func.localReps?.get(expr)?.jsonShape
|
|
3776
|
-
?? ctx.scope.globalReps?.get(expr)?.jsonShape
|
|
3777
|
-
?? null
|
|
3778
|
-
if (!Array.isArray(expr)) return null
|
|
3779
|
-
const [op, ...args] = expr
|
|
3780
|
-
if (op === '()' && args[0] === 'JSON.parse') {
|
|
3781
|
-
const srcs = jsonShapeStrings(args[1])
|
|
3782
|
-
if (srcs) return parseUnifiedJsonShape(srcs)
|
|
3783
|
-
}
|
|
3784
|
-
if (op === '.' && typeof args[1] === 'string') {
|
|
3785
|
-
const parent = shapeOf(args[0])
|
|
3786
|
-
if (parent?.val === VAL.OBJECT || parent?.val === VAL.HASH) return parent.props[args[1]] || null
|
|
3787
|
-
}
|
|
3788
|
-
if (op === '[]' && args.length === 2) {
|
|
3789
|
-
const parent = shapeOf(args[0])
|
|
3790
|
-
if (parent?.val === VAL.ARRAY) return parent.elem || null
|
|
3791
|
-
}
|
|
3792
|
-
return null
|
|
3793
|
-
}
|
|
3794
|
-
|
|
3795
|
-
/** Build a structural shape from a `{}` AST node — recursive for nested
|
|
3796
|
-
* object/array literals + propagating shapes through identifier references
|
|
3797
|
-
* (so `let G = {…}; let H = {x: G}` carries G's shape under H.x). Returns
|
|
3798
|
-
* null when any property breaks the static-shape contract (computed key,
|
|
3799
|
-
* spread, non-shape value). Only called from `recordGlobalRep` — local
|
|
3800
|
-
* bindings keep relying on `shapeOf` whose narrower contract (JSON.parse /
|
|
3801
|
-
* traversal only) lets `Object.assign(a, …)` extend `a`'s schema without
|
|
3802
|
-
* locking a static jsonShape onto it. */
|
|
3803
|
-
export function shapeOfObjectLiteralAst(expr) {
|
|
3804
|
-
if (typeof expr === 'string') return shapeOf(expr)
|
|
3805
|
-
if (!Array.isArray(expr) || expr[0] !== '{}') return shapeOf(expr)
|
|
3806
|
-
const raw = expr.length === 2 && Array.isArray(expr[1]) && expr[1][0] === ','
|
|
3807
|
-
? expr[1].slice(1)
|
|
3808
|
-
: expr.slice(1)
|
|
3809
|
-
const props = Object.create(null)
|
|
3810
|
-
const names = []
|
|
3811
|
-
for (const p of raw) {
|
|
3812
|
-
if (!Array.isArray(p) || p[0] !== ':' || typeof p[1] !== 'string') return null
|
|
3813
|
-
names.push(p[1])
|
|
3814
|
-
const child = shapeOfObjectLiteralAst(p[2])
|
|
3815
|
-
if (child) props[p[1]] = child
|
|
3816
|
-
}
|
|
3817
|
-
return names.length ? { val: VAL.OBJECT, props, names } : null
|
|
3818
|
-
}
|