jz 0.4.0 → 0.5.1
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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/analyze.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* # Stage contract
|
|
5
5
|
* IN: prepared AST + ctx.func.list (from prepare).
|
|
6
|
-
* OUT: per-function populated `ctx.func.
|
|
6
|
+
* OUT: per-function populated `ctx.func.localReps` (val field) + `ctx.func.locals` + `ctx.func.boxed`,
|
|
7
7
|
* module-global `ctx.scope.globalValTypes`, type-analysis `ctx.types.typedElem` /
|
|
8
8
|
* `.dynKeyVars` / `.anyDynKey`.
|
|
9
9
|
*
|
|
@@ -13,18 +13,17 @@
|
|
|
13
13
|
* - analyzeBody: single unified walk — body-keyed cache, returns
|
|
14
14
|
* { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems }
|
|
15
15
|
* - analyzeValTypes: ctx-mutating pass — writes types + tracks regex/typed + localProps
|
|
16
|
-
* -
|
|
17
|
-
* - analyzeDynKeys: cross-function scan for `obj[runtimeKey]` → sets ctx.types.dynKeyVars
|
|
18
|
-
* - analyzeBoxedCaptures:detect mutably-captured vars → ctx.func.boxed cells
|
|
16
|
+
* - boxedCaptures: detect mutably-captured vars → ctx.func.boxed cells
|
|
19
17
|
* - extractParams/classifyParam/collectParamNames: arrow param AST normalization helpers
|
|
20
18
|
*
|
|
21
|
-
* Ordering:
|
|
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).
|
|
22
21
|
*
|
|
23
22
|
* @module analyze
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
25
|
import { ctx, err } from './ctx.js'
|
|
27
|
-
import { isLiteralStr, isFuncRef } from './ir.js'
|
|
26
|
+
import { isLiteralStr, isFuncRef, I32_MIN, I32_MAX, isI32 } from './ir.js'
|
|
28
27
|
|
|
29
28
|
export const T = '\uE000'
|
|
30
29
|
|
|
@@ -48,7 +47,7 @@ export function intLiteralValue(expr) {
|
|
|
48
47
|
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
49
48
|
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
50
49
|
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
51
|
-
return v != null && Number.isInteger(v) && v >=
|
|
50
|
+
return v != null && Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX ? v : null
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
@@ -57,7 +56,7 @@ export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); ret
|
|
|
57
56
|
/** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
|
|
58
57
|
* Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
|
|
59
58
|
* coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
|
|
60
|
-
|
|
59
|
+
const collectReturnExprs = (node, out) => {
|
|
61
60
|
if (!Array.isArray(node)) return
|
|
62
61
|
const [op, ...args] = node
|
|
63
62
|
if (op === '=>') return
|
|
@@ -107,13 +106,20 @@ export const VAL = {
|
|
|
107
106
|
OBJECT: 'object', HASH: 'hash', SET: 'set', MAP: 'map',
|
|
108
107
|
CLOSURE: 'closure', TYPED: 'typed', REGEX: 'regex',
|
|
109
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',
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/**
|
|
113
119
|
* ValueRep — unified per-local + per-param representation record. (S2.)
|
|
114
120
|
*
|
|
115
121
|
* One shape, two storages:
|
|
116
|
-
* - per-local (current func): ctx.func.
|
|
122
|
+
* - per-local (current func): ctx.func.localReps: Map<name, ValueRep>
|
|
117
123
|
* - per-param (cross-call): programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>
|
|
118
124
|
*
|
|
119
125
|
* Lattice per field: undefined = unobserved, null = sticky-poison
|
|
@@ -129,7 +135,7 @@ export const VAL = {
|
|
|
129
135
|
* schemaId: i32 — schema binding for known-shape OBJECTs
|
|
130
136
|
* arrayElemSchema: i32 — Array<schemaId> element shape
|
|
131
137
|
* arrayElemValType: VAL.* — Array<VAL.*> element val-kind
|
|
132
|
-
* jsonShape: obj — {
|
|
138
|
+
* jsonShape: obj — { val, props?, elem? } for HASH/ARRAY trees parsed
|
|
133
139
|
* from a compile-time JSON.parse source. Propagates
|
|
134
140
|
* through `.prop` and `[i]` so nested chains stay typed.
|
|
135
141
|
* typedCtor: str — TypedArray ctor name (`Float64Array`, …)
|
|
@@ -144,37 +150,33 @@ export const VAL = {
|
|
|
144
150
|
* (or `f64.const N`), letting the WAT optimizer fold guards,
|
|
145
151
|
* unroll fixed-bound loops, and treeshake the read entirely.
|
|
146
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`.
|
|
147
157
|
*
|
|
148
|
-
*
|
|
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.
|
|
149
164
|
*/
|
|
150
165
|
|
|
151
166
|
// === ParamReps lattice helpers (cross-call fixpoint) ===
|
|
152
167
|
// programFacts.paramReps: Map<funcName, Map<paramIdx, ValueRep>>. Per-field lattice:
|
|
153
168
|
// undefined unobserved, null sticky-poison (cross-site disagreement), value = consensus.
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (observed == null) { rep[key] = null; return } // unknown → poison
|
|
161
|
-
if (rep[key] === undefined) rep[key] = observed
|
|
162
|
-
else if (rep[key] !== observed) rep[key] = null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Get-or-create per-param rep at (funcName, paramIdx) on a paramReps map. */
|
|
166
|
-
export const ensureParamRep = (paramReps, funcName, k) => {
|
|
167
|
-
let m = paramReps.get(funcName)
|
|
168
|
-
if (!m) { m = new Map(); paramReps.set(funcName, m) }
|
|
169
|
-
let r = m.get(k)
|
|
170
|
-
if (!r) { r = {}; m.set(k, r) }
|
|
171
|
-
return r
|
|
172
|
-
}
|
|
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.
|
|
173
175
|
|
|
174
176
|
/** Build `paramName → fact` lookup for a caller's already-narrowed param facts.
|
|
175
177
|
* Used to flow caller's param info into its callees during the cross-call
|
|
176
178
|
* fixpoint (transitive propagation). Returns null if caller has no facts. */
|
|
177
|
-
export const
|
|
179
|
+
export const paramFactsOf = (paramReps, callerFunc, key) => {
|
|
178
180
|
if (!callerFunc) return null
|
|
179
181
|
const m = paramReps.get(callerFunc.name)
|
|
180
182
|
if (!m) return null
|
|
@@ -189,22 +191,13 @@ export const callerParamFactMap = (paramReps, callerFunc, key) => {
|
|
|
189
191
|
return out
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
/** Reset sticky-null on a single field across all params program-wide.
|
|
193
|
-
* Used between fixpoint phases when newly-narrowed facts unblock previously-
|
|
194
|
-
* poisoned observations (e.g. valResult set after first pass). */
|
|
195
|
-
export const clearStickyNull = (paramReps, key) => {
|
|
196
|
-
for (const m of paramReps.values()) for (const r of m.values()) {
|
|
197
|
-
if (r[key] === null) r[key] = undefined
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
194
|
/** Get the rep for a local name, or undefined if not tracked. */
|
|
202
|
-
export const repOf = name => ctx.func.
|
|
195
|
+
export const repOf = name => ctx.func.localReps?.get(name)
|
|
203
196
|
|
|
204
197
|
/** Merge fields into a local's rep. Lazily allocates the map and the rep.
|
|
205
198
|
* Field set to `undefined` removes that field; empty rep is dropped from the map. */
|
|
206
199
|
export const updateRep = (name, fields) => {
|
|
207
|
-
const m = ctx.func.
|
|
200
|
+
const m = ctx.func.localReps ||= new Map()
|
|
208
201
|
const prev = m.get(name) || {}
|
|
209
202
|
const next = { ...prev, ...fields }
|
|
210
203
|
for (const k of Object.keys(next)) if (next[k] === undefined) delete next[k]
|
|
@@ -213,11 +206,11 @@ export const updateRep = (name, fields) => {
|
|
|
213
206
|
}
|
|
214
207
|
|
|
215
208
|
/** Get the rep for a global name, or undefined if not tracked. */
|
|
216
|
-
export const repOfGlobal = name => ctx.scope.
|
|
209
|
+
export const repOfGlobal = name => ctx.scope.globalReps?.get(name)
|
|
217
210
|
|
|
218
211
|
/** Merge fields into a global's rep. Lazily allocates the map and the rep. */
|
|
219
212
|
export const updateGlobalRep = (name, fields) => {
|
|
220
|
-
const m = ctx.scope.
|
|
213
|
+
const m = ctx.scope.globalReps ||= new Map()
|
|
221
214
|
const prev = m.get(name)
|
|
222
215
|
m.set(name, prev ? { ...prev, ...fields } : { ...fields })
|
|
223
216
|
}
|
|
@@ -227,33 +220,50 @@ export const updateGlobalRep = (name, fields) => {
|
|
|
227
220
|
* Refinements are pushed by the 'if' emitter when the condition is a type guard
|
|
228
221
|
* (typeof x === 't', Array.isArray(x), etc.) and popped after the then-branch.
|
|
229
222
|
* The overlay (`ctx.func.localValTypesOverlay`) is set by analyzeBody/observeSlots passes
|
|
230
|
-
* pre-emit, when `
|
|
223
|
+
* pre-emit, when `localReps` isn't populated yet but a local Map<name, VAL.*> is
|
|
231
224
|
* available — lets `const x = new Float64Array(); const y = x[0]` resolve y as NUMBER. */
|
|
232
225
|
export const lookupValType = name => {
|
|
233
226
|
const r = ctx.func.refinements
|
|
234
|
-
if (r && r.size) { const v = r.get(name); if (v) return v }
|
|
227
|
+
if (r && r.size) { const v = r.get(name)?.val; if (v) return v }
|
|
235
228
|
const ov = ctx.func.localValTypesOverlay
|
|
236
229
|
if (ov) { const v = ov.get(name); if (v) return v }
|
|
237
|
-
return ctx.func.
|
|
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
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
/** Infer value type of an AST expression (without emitting). */
|
|
241
243
|
export function valTypeOf(expr) {
|
|
242
244
|
if (expr == null) return null
|
|
243
245
|
if (typeof expr === 'number') return VAL.NUMBER
|
|
246
|
+
if (typeof expr === 'boolean') return VAL.BOOL
|
|
244
247
|
if (typeof expr === 'bigint') return VAL.BIGINT
|
|
245
248
|
if (typeof expr === 'string') return lookupValType(expr)
|
|
246
249
|
if (!Array.isArray(expr)) return null
|
|
247
250
|
|
|
248
251
|
const [op, ...args] = expr
|
|
249
252
|
if (op == null) {
|
|
250
|
-
// Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint
|
|
253
|
+
// Literal forms: [] = undefined, [null, null] = null, [null, n] = number/bigint, [, bool] = boolean
|
|
251
254
|
if (args.length === 0) return null // undefined literal
|
|
252
255
|
if (args[0] == null) return null // null literal
|
|
256
|
+
if (typeof args[0] === 'boolean') return VAL.BOOL
|
|
253
257
|
if (typeof args[0] === 'symbol') return null // prepared null sentinel
|
|
254
258
|
return typeof args[0] === 'bigint' ? VAL.BIGINT : VAL.NUMBER
|
|
255
259
|
}
|
|
256
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
|
+
|
|
257
267
|
if (op === '[') return VAL.ARRAY
|
|
258
268
|
if (op === 'str' || op === 'strcat') return VAL.STRING
|
|
259
269
|
if (op === '=>') return VAL.CLOSURE
|
|
@@ -278,7 +288,7 @@ export function valTypeOf(expr) {
|
|
|
278
288
|
// Indexed read on a known Array<VAL> receiver: bind by rep.arrayElemValType.
|
|
279
289
|
// Set by analyzeValTypes from body observations + emitFunc preseed for params.
|
|
280
290
|
if (typeof args[0] === 'string') {
|
|
281
|
-
const elemVt = ctx.func.
|
|
291
|
+
const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
|
|
282
292
|
if (elemVt) return elemVt
|
|
283
293
|
}
|
|
284
294
|
}
|
|
@@ -296,9 +306,9 @@ export function valTypeOf(expr) {
|
|
|
296
306
|
// child's val-type. Generic for any compile-time-known JSON literal.
|
|
297
307
|
if (op === '.' && typeof args[1] === 'string') {
|
|
298
308
|
const sh = shapeOf(args[0])
|
|
299
|
-
if (sh?.
|
|
309
|
+
if (sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) {
|
|
300
310
|
const child = sh.props[args[1]]
|
|
301
|
-
if (child) return child.
|
|
311
|
+
if (child) return child.val
|
|
302
312
|
}
|
|
303
313
|
}
|
|
304
314
|
// Arithmetic expressions: BigInt if either operand is BigInt, else number
|
|
@@ -313,6 +323,23 @@ export function valTypeOf(expr) {
|
|
|
313
323
|
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
314
324
|
return VAL.NUMBER
|
|
315
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
|
+
}
|
|
316
343
|
|
|
317
344
|
if (op === '()') {
|
|
318
345
|
const callee = args[0]
|
|
@@ -327,9 +354,16 @@ export function valTypeOf(expr) {
|
|
|
327
354
|
if (callee === 'new.Map') return VAL.MAP
|
|
328
355
|
if (callee === 'new.Date') return VAL.DATE
|
|
329
356
|
if (callee === 'new.ArrayBuffer') return VAL.BUFFER
|
|
330
|
-
|
|
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`.
|
|
331
364
|
if (callee.startsWith('new.')) return VAL.TYPED
|
|
332
365
|
if (callee === 'String.fromCharCode' || callee === 'String') return VAL.STRING
|
|
366
|
+
if (callee === 'Boolean') return VAL.BOOL
|
|
333
367
|
if (callee === 'BigInt' || callee === 'BigInt.asIntN' || callee === 'BigInt.asUintN') return VAL.BIGINT
|
|
334
368
|
if (callee === 'JSON.parse') {
|
|
335
369
|
const src = jsonConstString(args[1])
|
|
@@ -369,7 +403,7 @@ export function valTypeOf(expr) {
|
|
|
369
403
|
}
|
|
370
404
|
if (method === 'push') return VAL.ARRAY
|
|
371
405
|
if ((method === 'shift' || method === 'pop') && typeof obj === 'string') {
|
|
372
|
-
const elemVt = ctx.func.
|
|
406
|
+
const elemVt = ctx.func.localReps?.get(obj)?.arrayElemValType
|
|
373
407
|
if (elemVt) return elemVt
|
|
374
408
|
}
|
|
375
409
|
if (method === 'add' || method === 'delete') return VAL.SET
|
|
@@ -377,6 +411,12 @@ export function valTypeOf(expr) {
|
|
|
377
411
|
// String-returning methods
|
|
378
412
|
if (['toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'trim', 'trimStart', 'trimEnd',
|
|
379
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
|
|
380
420
|
if (method === 'split') return VAL.ARRAY
|
|
381
421
|
// slice/concat preserve caller type (string.slice → string, array.slice → array)
|
|
382
422
|
if (method === 'slice' || method === 'concat') {
|
|
@@ -389,128 +429,8 @@ export function valTypeOf(expr) {
|
|
|
389
429
|
return null
|
|
390
430
|
}
|
|
391
431
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'string') return expr[1]
|
|
395
|
-
if (typeof expr === 'string') {
|
|
396
|
-
// Prefer shapeStrs (broader: includes effectively-const `let` string literals
|
|
397
|
-
// at module scope) over constStrs (const-only). Module/json's static-fold
|
|
398
|
-
// path uses its own constStrs-only resolver to avoid folding `let`-bound
|
|
399
|
-
// initializers — preserving the user-controlled distinction. Shape
|
|
400
|
-
// inference is sound either way: an effectively-const literal's value is
|
|
401
|
-
// invariant, so the parsed shape it produces is too.
|
|
402
|
-
return ctx.scope.shapeStrs?.get(expr) ?? ctx.scope.constStrs?.get(expr) ?? null
|
|
403
|
-
}
|
|
404
|
-
return null
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function jsonShapeStrings(expr) {
|
|
408
|
-
const single = jsonConstString(expr)
|
|
409
|
-
if (single != null) return [single]
|
|
410
|
-
if (Array.isArray(expr) && expr[0] === '[]' && typeof expr[1] === 'string') return ctx.scope.shapeStrArrays?.get(expr[1]) ?? null
|
|
411
|
-
return null
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** Build a structural shape tree from a parsed JSON value. Each node is
|
|
415
|
-
* `{ vt, props?, elem? }`. Lets `valTypeOf` propagate VAL kinds through
|
|
416
|
-
* `.prop` chains and `[i]` reads on bindings sourced from `JSON.parse`
|
|
417
|
-
* of a compile-time-known string. Polymorphic arrays drop their `elem`. */
|
|
418
|
-
function shapeOfJsonValue(v) {
|
|
419
|
-
if (v === null || v === undefined) return null
|
|
420
|
-
if (typeof v === 'number') return { vt: VAL.NUMBER }
|
|
421
|
-
if (typeof v === 'string') return { vt: VAL.STRING }
|
|
422
|
-
if (typeof v === 'boolean') return { vt: VAL.NUMBER }
|
|
423
|
-
if (Array.isArray(v)) {
|
|
424
|
-
let elem = null
|
|
425
|
-
for (const x of v) {
|
|
426
|
-
const s = shapeOfJsonValue(x)
|
|
427
|
-
if (!s) { elem = null; break }
|
|
428
|
-
if (!elem) elem = s
|
|
429
|
-
else if (!shapeUnifies(elem, s)) { elem = null; break }
|
|
430
|
-
}
|
|
431
|
-
return { vt: VAL.ARRAY, elem }
|
|
432
|
-
}
|
|
433
|
-
if (typeof v === 'object') {
|
|
434
|
-
const props = Object.create(null)
|
|
435
|
-
const names = Object.keys(v)
|
|
436
|
-
for (const k of names) {
|
|
437
|
-
const s = shapeOfJsonValue(v[k])
|
|
438
|
-
if (s) props[k] = s
|
|
439
|
-
}
|
|
440
|
-
return { vt: VAL.OBJECT, props, names }
|
|
441
|
-
}
|
|
442
|
-
return null
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function shapeUnifies(a, b) {
|
|
446
|
-
if (!a || !b || a.vt !== b.vt) return false
|
|
447
|
-
if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
|
|
448
|
-
const ak = Object.keys(a.props), bk = Object.keys(b.props)
|
|
449
|
-
if (ak.length !== bk.length) return false
|
|
450
|
-
for (const k of ak) {
|
|
451
|
-
if (!b.props[k] || !shapeUnifies(a.props[k], b.props[k])) return false
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (a.vt === VAL.ARRAY) {
|
|
455
|
-
if ((a.elem == null) !== (b.elem == null)) return false
|
|
456
|
-
if (a.elem && !shapeUnifies(a.elem, b.elem)) return false
|
|
457
|
-
}
|
|
458
|
-
return true
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function shapeLayoutUnifies(a, b) {
|
|
462
|
-
if (!shapeUnifies(a, b)) return false
|
|
463
|
-
if (a.vt === VAL.OBJECT || a.vt === VAL.HASH) {
|
|
464
|
-
if (a.names?.length !== b.names?.length) return false
|
|
465
|
-
for (let i = 0; i < a.names.length; i++) if (a.names[i] !== b.names[i]) return false
|
|
466
|
-
}
|
|
467
|
-
if (a.vt === VAL.ARRAY && a.elem) return shapeLayoutUnifies(a.elem, b.elem)
|
|
468
|
-
return true
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const _jsonShapeCache = new WeakMap()
|
|
472
|
-
function parseJsonShape(src) {
|
|
473
|
-
if (typeof src !== 'string') return null
|
|
474
|
-
if (_jsonShapeCache.has(src)) return _jsonShapeCache.get(src)
|
|
475
|
-
let parsed
|
|
476
|
-
try { parsed = JSON.parse(src) } catch { _jsonShapeCache.set(Object(src), null); return null }
|
|
477
|
-
const sh = shapeOfJsonValue(parsed)
|
|
478
|
-
// WeakMap requires object keys; cache via a wrapper. Skip caching for cold path.
|
|
479
|
-
return sh
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function parseUnifiedJsonShape(srcs) {
|
|
483
|
-
if (!srcs?.length) return null
|
|
484
|
-
let out = null
|
|
485
|
-
for (const src of srcs) {
|
|
486
|
-
const sh = parseJsonShape(src)
|
|
487
|
-
if (!sh) return null
|
|
488
|
-
if (!out) out = sh
|
|
489
|
-
else if (!shapeLayoutUnifies(out, sh)) return null
|
|
490
|
-
}
|
|
491
|
-
return out
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/** Resolve the json shape for an expression by walking name → rep.jsonShape and
|
|
495
|
-
* `.prop` / `[i]` indirection. Returns null when shape is unknown at this site. */
|
|
496
|
-
export function shapeOf(expr) {
|
|
497
|
-
if (typeof expr === 'string') return ctx.func.repByLocal?.get(expr)?.jsonShape || null
|
|
498
|
-
if (!Array.isArray(expr)) return null
|
|
499
|
-
const [op, ...args] = expr
|
|
500
|
-
if (op === '()' && args[0] === 'JSON.parse') {
|
|
501
|
-
const srcs = jsonShapeStrings(args[1])
|
|
502
|
-
if (srcs) return parseUnifiedJsonShape(srcs)
|
|
503
|
-
}
|
|
504
|
-
if (op === '.' && typeof args[1] === 'string') {
|
|
505
|
-
const parent = shapeOf(args[0])
|
|
506
|
-
if (parent?.vt === VAL.OBJECT || parent?.vt === VAL.HASH) return parent.props[args[1]] || null
|
|
507
|
-
}
|
|
508
|
-
if (op === '[]' && args.length === 2) {
|
|
509
|
-
const parent = shapeOf(args[0])
|
|
510
|
-
if (parent?.vt === VAL.ARRAY) return parent.elem || null
|
|
511
|
-
}
|
|
512
|
-
return null
|
|
513
|
-
}
|
|
432
|
+
// JSON-shape inference (formerly in src/shape.js) — `shapeOf` + `jsonConstString`
|
|
433
|
+
// are defined further down in this file and exported for module consumers.
|
|
514
434
|
|
|
515
435
|
|
|
516
436
|
/** Static property-key evaluation for computed member names: folds a node into
|
|
@@ -560,8 +480,10 @@ function staticValue(node) {
|
|
|
560
480
|
if (args.length === 1) {
|
|
561
481
|
const value = staticValue(args[0])
|
|
562
482
|
if (value === NO_VALUE) return NO_VALUE
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
565
487
|
if (op === '!') return !value
|
|
566
488
|
if (op === '~') return ~value
|
|
567
489
|
return NO_VALUE
|
|
@@ -616,7 +538,7 @@ function staticArrayElems(expr) {
|
|
|
616
538
|
}
|
|
617
539
|
|
|
618
540
|
/** Schema-id for an object literal expression. Returns null on dynamic keys, spread, shorthand. */
|
|
619
|
-
function objLiteralSchemaId(expr) {
|
|
541
|
+
export function objLiteralSchemaId(expr) {
|
|
620
542
|
if (!Array.isArray(expr) || expr[0] !== '{}' || !ctx.schema?.register) return null
|
|
621
543
|
const parsed = staticObjectProps(expr.slice(1))
|
|
622
544
|
return parsed ? ctx.schema.register(parsed.names) : null
|
|
@@ -695,7 +617,7 @@ export function ctorFromElemAux(aux) {
|
|
|
695
617
|
/** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
|
|
696
618
|
* different typed-array ctors — caller should drop any cached entry rather
|
|
697
619
|
* than leave a stale ctor (which would lock the wrong store width). */
|
|
698
|
-
|
|
620
|
+
const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
699
621
|
|
|
700
622
|
const typedCtorElemValType = (ctor) => {
|
|
701
623
|
if (!ctor) return null
|
|
@@ -712,7 +634,7 @@ const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e
|
|
|
712
634
|
* - a single ctor string when every branch resolves to the same ctor,
|
|
713
635
|
* - MIXED_CTORS when branches resolve to different ctors,
|
|
714
636
|
* - null when no branch resolves (caller's behavior unchanged). */
|
|
715
|
-
|
|
637
|
+
function ternaryCtorOfRhs(rhs) {
|
|
716
638
|
if (!Array.isArray(rhs)) return null
|
|
717
639
|
const op = rhs[0]
|
|
718
640
|
const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
|
|
@@ -722,99 +644,358 @@ export function ternaryCtorOfRhs(rhs) {
|
|
|
722
644
|
return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
|
|
723
645
|
}
|
|
724
646
|
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
// facts, returning null when the fact can't be determined at this call site.
|
|
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.
|
|
729
650
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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.
|
|
735
659
|
|
|
736
|
-
/**
|
|
737
|
-
*
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
|
749
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
|
|
750
713
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
|
758
743
|
}
|
|
759
|
-
if (
|
|
760
|
-
const
|
|
761
|
-
|
|
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
|
|
762
749
|
}
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
766
|
-
const f = ctx.func.map?.get(expr[1])
|
|
767
|
-
if (f?.arrayElemSchema != null) return f.arrayElemSchema
|
|
750
|
+
walk(t) // some other LHS shape — generic
|
|
768
751
|
}
|
|
769
|
-
return null
|
|
770
|
-
}
|
|
771
752
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
778
784
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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)
|
|
782
873
|
}
|
|
783
|
-
return null
|
|
784
|
-
}
|
|
785
|
-
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
786
|
-
const f = ctx.func.map?.get(expr[1])
|
|
787
|
-
if (f?.arrayElemValType != null) return f.arrayElemValType
|
|
788
874
|
}
|
|
789
|
-
|
|
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
|
|
790
881
|
}
|
|
791
882
|
|
|
792
|
-
/**
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
|
806
920
|
}
|
|
807
|
-
|
|
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
|
|
808
956
|
}
|
|
809
957
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
+
}
|
|
818
999
|
|
|
819
1000
|
/**
|
|
820
1001
|
* Unified per-body analysis. Single AST traversal producing every per-binding
|
|
@@ -845,11 +1026,89 @@ export function inferArgTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
|
845
1026
|
*/
|
|
846
1027
|
const _bodyFactsCache = new WeakMap()
|
|
847
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
|
+
|
|
848
1108
|
/**
|
|
849
1109
|
* Returns the cached facts object directly — DO NOT MUTATE the returned maps.
|
|
850
|
-
* Callers that need to extend (e.g. add params to locals) must clone explicitly
|
|
851
|
-
*
|
|
852
|
-
* reads slices via `analyzeBody(body).<slice>`.
|
|
1110
|
+
* Callers that need to extend (e.g. add params to locals) must clone explicitly
|
|
1111
|
+
* before mutating. Slice reads via `analyzeBody(body).<slice>`.
|
|
853
1112
|
*/
|
|
854
1113
|
export function analyzeBody(body) {
|
|
855
1114
|
// Non-object bodies (`() => 0`, `() => x`, missing) have nothing to observe
|
|
@@ -857,6 +1116,7 @@ export function analyzeBody(body) {
|
|
|
857
1116
|
if (body === null || typeof body !== 'object') return {
|
|
858
1117
|
locals: new Map(), valTypes: new Map(), arrElemSchemas: new Map(),
|
|
859
1118
|
arrElemValTypes: new Map(), typedElems: new Map(), escapes: new Map(),
|
|
1119
|
+
flatObjects: new Map(),
|
|
860
1120
|
}
|
|
861
1121
|
const hit = _bodyFactsCache.get(body)
|
|
862
1122
|
if (hit) return hit
|
|
@@ -901,14 +1161,14 @@ export function analyzeBody(body) {
|
|
|
901
1161
|
|
|
902
1162
|
const elemValOf = (name) => {
|
|
903
1163
|
if (typeof name !== 'string') return null
|
|
904
|
-
const repVt = ctx.func.
|
|
1164
|
+
const repVt = ctx.func.localReps?.get(name)?.arrayElemValType
|
|
905
1165
|
if (repVt) return repVt
|
|
906
1166
|
return arrElemValTypes.get(name) || null
|
|
907
1167
|
}
|
|
908
1168
|
|
|
909
1169
|
const exprElemSourceVal = (expr) => {
|
|
910
1170
|
if (typeof expr === 'string') {
|
|
911
|
-
const repVt = ctx.func.
|
|
1171
|
+
const repVt = ctx.func.localReps?.get(expr)?.val
|
|
912
1172
|
if (repVt) return repVt
|
|
913
1173
|
return ctx.scope.globalValTypes?.get(expr) || null
|
|
914
1174
|
}
|
|
@@ -998,7 +1258,7 @@ export function analyzeBody(body) {
|
|
|
998
1258
|
if (sid2 != null) observeArrSchema(name, sid2)
|
|
999
1259
|
}
|
|
1000
1260
|
if (typeof rhs === 'string') {
|
|
1001
|
-
const repSid = ctx.func.
|
|
1261
|
+
const repSid = ctx.func.localReps?.get(rhs)?.arrayElemSchema
|
|
1002
1262
|
if (repSid != null) observeArrSchema(name, repSid)
|
|
1003
1263
|
}
|
|
1004
1264
|
}
|
|
@@ -1047,7 +1307,7 @@ export function analyzeBody(body) {
|
|
|
1047
1307
|
const refs = ctx.func.refinements
|
|
1048
1308
|
const hadParam = refs?.has(paramName)
|
|
1049
1309
|
const prev = hadParam ? refs.get(paramName) : undefined
|
|
1050
|
-
if (refs && recvVt) refs.set(paramName, recvVt)
|
|
1310
|
+
if (refs && recvVt) refs.set(paramName, { val: recvVt })
|
|
1051
1311
|
let bodyVt = null
|
|
1052
1312
|
try { bodyVt = valTypeOf(exprBody) }
|
|
1053
1313
|
finally {
|
|
@@ -1109,10 +1369,10 @@ export function analyzeBody(body) {
|
|
|
1109
1369
|
if (op === 'let' || op === 'const') {
|
|
1110
1370
|
for (let i = 1; i < node.length; i++) {
|
|
1111
1371
|
const a = node[i]
|
|
1112
|
-
//
|
|
1372
|
+
// analyzeBody: bare-name decl
|
|
1113
1373
|
if (typeof a === 'string') { if (!locals.has(a)) locals.set(a, 'f64'); continue }
|
|
1114
1374
|
if (!Array.isArray(a) || a[0] !== '=') continue
|
|
1115
|
-
//
|
|
1375
|
+
// analyzeBody: destructuring decl — set destructured names to f64, walk rhs only
|
|
1116
1376
|
if (typeof a[1] !== 'string') {
|
|
1117
1377
|
for (const n of collectParamNames([a[1]])) if (!locals.has(n)) locals.set(n, 'f64')
|
|
1118
1378
|
walk(a[2])
|
|
@@ -1212,12 +1472,11 @@ export function analyzeBody(body) {
|
|
|
1212
1472
|
const prevTypedOverlay = ctx.func.localTypedElemsOverlay
|
|
1213
1473
|
ctx.func.localValTypesOverlay = valTypes
|
|
1214
1474
|
ctx.func.localTypedElemsOverlay = typedElems
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1475
|
+
let unsignedLocals
|
|
1476
|
+
try {
|
|
1477
|
+
walk(body)
|
|
1219
1478
|
|
|
1220
|
-
|
|
1479
|
+
// Second pass: widen i32 locals compared against f64.
|
|
1221
1480
|
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
1222
1481
|
function widenPass(node) {
|
|
1223
1482
|
if (!Array.isArray(node)) return
|
|
@@ -1237,6 +1496,10 @@ export function analyzeBody(body) {
|
|
|
1237
1496
|
// was later re-typed to f64. Without this, integer-init locals shadowed
|
|
1238
1497
|
// by f64-arithmetic RHSs end up with `i32.trunc_sat_f64_s` truncating the
|
|
1239
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).
|
|
1240
1503
|
// Monotonic: only widens i32 → f64. Bound by locals count so no infinite loop.
|
|
1241
1504
|
let widened = true
|
|
1242
1505
|
while (widened) {
|
|
@@ -1256,87 +1519,64 @@ export function analyzeBody(body) {
|
|
|
1256
1519
|
}
|
|
1257
1520
|
}
|
|
1258
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
|
+
}
|
|
1259
1538
|
for (let i = 1; i < node.length; i++) recheck(node[i])
|
|
1260
1539
|
}
|
|
1261
1540
|
recheck(body)
|
|
1262
1541
|
}
|
|
1263
1542
|
|
|
1264
|
-
|
|
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 }
|
|
1265
1568
|
_bodyFactsCache.set(body, result)
|
|
1266
1569
|
return result
|
|
1267
1570
|
}
|
|
1268
1571
|
|
|
1269
1572
|
/** Drop the cached analyzeBody entry for this body. Used by emitFunc after
|
|
1270
1573
|
* seeding cross-call param VAL facts so the next walk picks up fresh
|
|
1271
|
-
* `ctx.func.
|
|
1574
|
+
* `ctx.func.localReps` (drives exprType receiver-type lookups).
|
|
1272
1575
|
* Same hook as `invalidateValTypesCache` — split names preserve caller intent. */
|
|
1273
1576
|
export function invalidateLocalsCache(body) {
|
|
1274
1577
|
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
1275
1578
|
}
|
|
1276
1579
|
|
|
1277
|
-
// String-only methods: presence of `name.method(...)` is definite evidence that
|
|
1278
|
-
// `name` is VAL.STRING. Excludes ambiguous methods (length, slice, indexOf, [],
|
|
1279
|
-
// concat) that strings share with arrays/typed-arrays.
|
|
1280
|
-
const STRING_ONLY_METHODS = new Set([
|
|
1281
|
-
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
1282
|
-
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
1283
|
-
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
1284
|
-
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
1285
|
-
])
|
|
1286
|
-
// Array-only methods: presence rules out STRING.
|
|
1287
|
-
const ARRAY_ONLY_METHODS = new Set([
|
|
1288
|
-
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
1289
|
-
'flat', 'flatMap', 'copyWithin',
|
|
1290
|
-
])
|
|
1291
|
-
|
|
1292
|
-
/** Infer VAL.STRING for `names` from method-call evidence in `body`.
|
|
1293
|
-
* Descends into nested closures (captured names retain shape) but respects
|
|
1294
|
-
* shadowing: a param of a nested `=>` removes that name from the inference
|
|
1295
|
-
* scope inside that closure. Returns Map<name, VAL.STRING> for names that
|
|
1296
|
-
* have at least one string-only method call and no array-only conflicts. */
|
|
1297
|
-
export function inferStringParams(body, names) {
|
|
1298
|
-
if (!names || names.length === 0) return new Map()
|
|
1299
|
-
const evidence = new Map() // name → 'string' | 'conflict'
|
|
1300
|
-
function walk(node, scope) {
|
|
1301
|
-
if (!Array.isArray(node) || scope.size === 0) return
|
|
1302
|
-
const op = node[0]
|
|
1303
|
-
if (op === '=>') {
|
|
1304
|
-
const shadowed = collectParamNames([node[1]])
|
|
1305
|
-
let inner = scope
|
|
1306
|
-
for (const s of shadowed) {
|
|
1307
|
-
if (inner.has(s)) {
|
|
1308
|
-
if (inner === scope) inner = new Set(scope)
|
|
1309
|
-
inner.delete(s)
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
walk(node[2], inner)
|
|
1313
|
-
return
|
|
1314
|
-
}
|
|
1315
|
-
// `name.method` — observe positive/negative evidence
|
|
1316
|
-
if (op === '.' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
1317
|
-
const m = node[2]
|
|
1318
|
-
if (typeof m === 'string') {
|
|
1319
|
-
if (STRING_ONLY_METHODS.has(m)) {
|
|
1320
|
-
if (evidence.get(node[1]) !== 'conflict') evidence.set(node[1], 'string')
|
|
1321
|
-
} else if (ARRAY_ONLY_METHODS.has(m)) {
|
|
1322
|
-
evidence.set(node[1], 'conflict')
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
// Reassignment to an unambiguously non-string RHS poisons the inference
|
|
1327
|
-
if (op === '=' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
1328
|
-
const rhs = node[2]
|
|
1329
|
-
const vt = valTypeOf(rhs)
|
|
1330
|
-
if (vt && vt !== VAL.STRING) evidence.set(node[1], 'conflict')
|
|
1331
|
-
}
|
|
1332
|
-
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
1333
|
-
}
|
|
1334
|
-
walk(body, new Set(names))
|
|
1335
|
-
const out = new Map()
|
|
1336
|
-
for (const [n, ev] of evidence) if (ev === 'string') out.set(n, VAL.STRING)
|
|
1337
|
-
return out
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
1580
|
/** Drop the cached analyzeBody entry. Used after E2-phase valResult narrowing
|
|
1341
1581
|
* so the next walk re-evaluates `valTypeOf(call)` with up-to-date `f.valResult`
|
|
1342
1582
|
* — required for the D-pass paramReps val/arrayElemSchema re-fixpoint to see
|
|
@@ -1347,14 +1587,14 @@ export function invalidateValTypesCache(body) {
|
|
|
1347
1587
|
|
|
1348
1588
|
/**
|
|
1349
1589
|
* Analyze all local value types from declarations and assignments.
|
|
1350
|
-
* Writes the per-name `val` field of `ctx.func.
|
|
1590
|
+
* Writes the per-name `val` field of `ctx.func.localReps` for method dispatch
|
|
1351
1591
|
* and schema resolution.
|
|
1352
1592
|
*/
|
|
1353
1593
|
export function analyzeValTypes(body) {
|
|
1354
1594
|
const valPoison = new Set()
|
|
1355
1595
|
const setVal = (name, vt) => {
|
|
1356
1596
|
if (valPoison.has(name)) return
|
|
1357
|
-
const prev = ctx.func.
|
|
1597
|
+
const prev = ctx.func.localReps?.get(name)?.val
|
|
1358
1598
|
if (!vt) {
|
|
1359
1599
|
if (prev) valPoison.add(name)
|
|
1360
1600
|
updateRep(name, { val: undefined })
|
|
@@ -1367,7 +1607,7 @@ export function analyzeValTypes(body) {
|
|
|
1367
1607
|
}
|
|
1368
1608
|
updateRep(name, { val: vt })
|
|
1369
1609
|
}
|
|
1370
|
-
const getVal = name => ctx.func.
|
|
1610
|
+
const getVal = name => ctx.func.localReps?.get(name)?.val
|
|
1371
1611
|
// Pre-walk: observe Array<schema> facts so `const p = arr[i]` can bind a schemaId
|
|
1372
1612
|
// on `p`, unlocking schema slot reads + skipping str_key dispatch on `.prop` access.
|
|
1373
1613
|
// Parallel arrElemValTypes walk records VAL.* element kinds into
|
|
@@ -1378,7 +1618,7 @@ export function analyzeValTypes(body) {
|
|
|
1378
1618
|
for (const [name, vt] of facts.arrElemValTypes) {
|
|
1379
1619
|
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
1380
1620
|
}
|
|
1381
|
-
// Propagate body-observed array-elem schemas to
|
|
1621
|
+
// Propagate body-observed array-elem schemas to localReps so unboxablePtrs's
|
|
1382
1622
|
// `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
|
|
1383
1623
|
// to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
|
|
1384
1624
|
// pay an i64.reinterpret/i32.wrap on every slot access (no aliasing → CSE can't fold).
|
|
@@ -1389,7 +1629,7 @@ export function analyzeValTypes(body) {
|
|
|
1389
1629
|
// paramReps[k].arrayElemSchema at emit start) over local body observations.
|
|
1390
1630
|
const arrElemSchemaOf = (name) => {
|
|
1391
1631
|
if (typeof name !== 'string') return null
|
|
1392
|
-
const repSid = ctx.func.
|
|
1632
|
+
const repSid = ctx.func.localReps?.get(name)?.arrayElemSchema
|
|
1393
1633
|
if (repSid != null) return repSid
|
|
1394
1634
|
const localSid = arrElems.get(name)
|
|
1395
1635
|
return localSid != null ? localSid : null
|
|
@@ -1417,7 +1657,7 @@ export function analyzeValTypes(body) {
|
|
|
1417
1657
|
const ctor = typedElemCtor(rhs)
|
|
1418
1658
|
if (ctor) return setOrInvalidate(ctor)
|
|
1419
1659
|
// TYPED-narrowed call result carries elem aux on f.sig.ptrAux — reverse-map it
|
|
1420
|
-
// back to a canonical ctor string so
|
|
1660
|
+
// back to a canonical ctor string so unboxablePtrs's typedElemAux lookup
|
|
1421
1661
|
// (compile.js) restores the same aux on the unboxed local's rep.
|
|
1422
1662
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
1423
1663
|
const f = ctx.func.map?.get(rhs[1])
|
|
@@ -1434,6 +1674,44 @@ export function analyzeValTypes(body) {
|
|
|
1434
1674
|
const tc = ternaryCtorOfRhs(rhs)
|
|
1435
1675
|
if (tc) setOrInvalidate(tc)
|
|
1436
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
|
+
}
|
|
1437
1715
|
function walk(node) {
|
|
1438
1716
|
if (!Array.isArray(node)) return
|
|
1439
1717
|
const [op, ...args] = node
|
|
@@ -1471,26 +1749,30 @@ export function analyzeValTypes(body) {
|
|
|
1471
1749
|
const sh = shapeOf(a[2])
|
|
1472
1750
|
if (sh) {
|
|
1473
1751
|
updateRep(a[1], { jsonShape: sh })
|
|
1474
|
-
if (sh.
|
|
1475
|
-
updateRep(a[1], { arrayElemValType: sh.elem.
|
|
1752
|
+
if (sh.val === VAL.ARRAY && sh.elem?.val) {
|
|
1753
|
+
updateRep(a[1], { arrayElemValType: sh.elem.val })
|
|
1476
1754
|
// Array of fixed-shape OBJECTs: register elem schema so `it = items[j]`
|
|
1477
1755
|
// → `it.prop` lowers to slot read via the existing arr-elem-schema path.
|
|
1478
|
-
if (sh.elem.
|
|
1756
|
+
if (sh.elem.val === VAL.OBJECT && sh.elem.names && ctx.schema.register) {
|
|
1479
1757
|
const elemSid = ctx.schema.register(sh.elem.names)
|
|
1480
1758
|
updateRep(a[1], { arrayElemSchema: elemSid })
|
|
1481
1759
|
}
|
|
1482
1760
|
}
|
|
1483
|
-
if (sh.
|
|
1761
|
+
if (sh.val === VAL.OBJECT && sh.names && ctx.schema.register) {
|
|
1484
1762
|
const sid = ctx.schema.register(sh.names)
|
|
1485
1763
|
updateRep(a[1], { schemaId: sid })
|
|
1486
1764
|
ctx.schema.vars.set(a[1], sid)
|
|
1487
1765
|
}
|
|
1488
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)
|
|
1489
1771
|
// Propagate schemaId from a narrowed call result so subsequent valTypeOf
|
|
1490
1772
|
// calls in this function body see the precise schema. emitDecl rebinds
|
|
1491
1773
|
// this at emission time too — analyze-time binding is what unlocks the
|
|
1492
1774
|
// slotVT lookup chain in `analyzeValTypes`'s own walk + per-func emit
|
|
1493
|
-
// dispatch reading
|
|
1775
|
+
// dispatch reading localReps.
|
|
1494
1776
|
if (vt === VAL.OBJECT && Array.isArray(a[2]) && a[2][0] === '()' && typeof a[2][1] === 'string') {
|
|
1495
1777
|
const f = ctx.func.map?.get(a[2][1])
|
|
1496
1778
|
if (f?.sig?.ptrAux != null) updateRep(a[1], { schemaId: f.sig.ptrAux })
|
|
@@ -1515,6 +1797,7 @@ export function analyzeValTypes(body) {
|
|
|
1515
1797
|
if (vt === VAL.REGEX) trackRegex(args[0], args[1])
|
|
1516
1798
|
if (vt === VAL.TYPED || vt === VAL.BUFFER || isCondExpr(args[1])) trackTyped(args[0], args[1])
|
|
1517
1799
|
propagateTyped(args[0], args[1])
|
|
1800
|
+
if (vt === VAL.OBJECT) bindObjSchema(args[0], args[1])
|
|
1518
1801
|
return
|
|
1519
1802
|
}
|
|
1520
1803
|
// Track property assignments for auto-boxing: x.prop = val
|
|
@@ -1568,8 +1851,11 @@ const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'
|
|
|
1568
1851
|
* `&= |= ^= <<= >>= >>>=` (always int by op result type);
|
|
1569
1852
|
* `/=` `**=` poison.
|
|
1570
1853
|
*
|
|
1571
|
-
* Writes `intCertain: true` on `ctx.func.
|
|
1572
|
-
*
|
|
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.
|
|
1573
1859
|
*/
|
|
1574
1860
|
export function analyzeIntCertain(body) {
|
|
1575
1861
|
// Pass 1: collect every defining RHS per binding name. Compound assignments
|
|
@@ -1680,6 +1966,17 @@ export function analyzeIntCertain(body) {
|
|
|
1680
1966
|
}
|
|
1681
1967
|
}
|
|
1682
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
|
+
|
|
1683
1980
|
/**
|
|
1684
1981
|
* Infer expression result type from AST (without emitting).
|
|
1685
1982
|
* Used to determine local variable types before compilation.
|
|
@@ -1688,7 +1985,7 @@ export function analyzeIntCertain(body) {
|
|
|
1688
1985
|
export function exprType(expr, locals) {
|
|
1689
1986
|
if (expr == null) return 'f64'
|
|
1690
1987
|
if (typeof expr === 'number')
|
|
1691
|
-
return
|
|
1988
|
+
return isI32(expr) ? 'i32' : 'f64'
|
|
1692
1989
|
if (typeof expr === 'string') {
|
|
1693
1990
|
if (locals?.has?.(expr)) return locals.get(expr)
|
|
1694
1991
|
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
@@ -1729,7 +2026,7 @@ export function exprType(expr, locals) {
|
|
|
1729
2026
|
return 'f64'
|
|
1730
2027
|
}
|
|
1731
2028
|
// `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
|
|
1732
|
-
// both return i32). Letting it stay i32 lets
|
|
2029
|
+
// both return i32). Letting it stay i32 lets analyzeBody keep the counter
|
|
1733
2030
|
// local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
|
|
1734
2031
|
// the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
|
|
1735
2032
|
// Only safe when receiver type is statically known to expose an integer length.
|
|
@@ -1751,10 +2048,32 @@ export function exprType(expr, locals) {
|
|
|
1751
2048
|
// Always i32
|
|
1752
2049
|
if (['>', '<', '>=', '<=', '==', '!=', '!', '&', '|', '^', '~', '<<', '>>', '>>>'].includes(op)) return 'i32'
|
|
1753
2050
|
// Preserve i32 if both operands i32
|
|
1754
|
-
if (
|
|
2051
|
+
if (op === '+' || op === '-' || op === '%') {
|
|
1755
2052
|
const ta = exprType(args[0], locals)
|
|
1756
2053
|
const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
|
|
1757
|
-
|
|
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'
|
|
1758
2077
|
}
|
|
1759
2078
|
// Unary preserves type
|
|
1760
2079
|
if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
|
|
@@ -1771,12 +2090,13 @@ export function exprType(expr, locals) {
|
|
|
1771
2090
|
// through, instead of widening the local to f64 because exprType defaulted.
|
|
1772
2091
|
if (op === '()') {
|
|
1773
2092
|
if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt'
|
|
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'
|
|
1778
2098
|
// User-function call: consult the callee's narrowed result type. By the time
|
|
1779
|
-
//
|
|
2099
|
+
// analyzeBody runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
|
|
1780
2100
|
// on every body-i32-only func. Propagating this lets `let h = userFn(...)`
|
|
1781
2101
|
// (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
|
|
1782
2102
|
// to f64 and round-tripping i32↔f64 every iteration.
|
|
@@ -1788,15 +2108,9 @@ export function exprType(expr, locals) {
|
|
|
1788
2108
|
return 'f64'
|
|
1789
2109
|
}
|
|
1790
2110
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
*
|
|
1795
|
-
* Thin slice of `analyzeBody` (single unified walk).
|
|
1796
|
-
*/
|
|
1797
|
-
export function analyzeLocals(body) {
|
|
1798
|
-
return analyzeBody(body).locals
|
|
1799
|
-
}
|
|
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.
|
|
1800
2114
|
|
|
1801
2115
|
/**
|
|
1802
2116
|
* Identify locals that can be stored as an unboxed i32 pointer offset instead of
|
|
@@ -1813,18 +2127,16 @@ export function analyzeLocals(body) {
|
|
|
1813
2127
|
* SET ← `new Set(...)`
|
|
1814
2128
|
* MAP ← `new Map(...)`
|
|
1815
2129
|
* CLOSURE← `=>` literal
|
|
1816
|
-
* BUFFER ← `new ArrayBuffer(...)`
|
|
2130
|
+
* BUFFER ← `new ArrayBuffer(...)`
|
|
1817
2131
|
* TYPED ← `new XxxArray(...)` / method returning typed array
|
|
2132
|
+
* (`new DataView(...)` is TYPED but stays boxed — no elem aux)
|
|
1818
2133
|
* - not captured in boxed storage (boxed locals stay f64 for the heap slot)
|
|
1819
2134
|
* - never compared to null/undefined (we lose the nullish NaN representation)
|
|
1820
2135
|
*
|
|
1821
2136
|
* Returns Map<name, VAL> of locals to unbox.
|
|
1822
2137
|
*/
|
|
1823
|
-
export function
|
|
1824
|
-
const
|
|
1825
|
-
const disqualified = new Set()
|
|
1826
|
-
const valOf = name => ctx.func.repByLocal?.get(name)?.val
|
|
1827
|
-
|
|
2138
|
+
export function unboxablePtrs(body, locals, boxed) {
|
|
2139
|
+
const valOf = name => ctx.func.localReps?.get(name)?.val
|
|
1828
2140
|
const UNBOXABLE_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER, VAL.TYPED, VAL.CLOSURE, VAL.DATE])
|
|
1829
2141
|
|
|
1830
2142
|
// RHS must produce a fresh, non-null pointer of the declared VAL kind.
|
|
@@ -1851,7 +2163,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1851
2163
|
// subsequent `p.x` reads then become direct `f64.load offset=K (local.get $p)`
|
|
1852
2164
|
// (since ptrOffsetIR sees ptrKind=OBJECT and skips the per-access wrap).
|
|
1853
2165
|
if (expr[0] === '[]' && typeof expr[1] === 'string') {
|
|
1854
|
-
const repSid = ctx.func.
|
|
2166
|
+
const repSid = ctx.func.localReps?.get(expr[1])?.arrayElemSchema
|
|
1855
2167
|
return repSid != null
|
|
1856
2168
|
}
|
|
1857
2169
|
return false
|
|
@@ -1863,7 +2175,7 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1863
2175
|
if (kind === VAL.SET) return callee === 'new.Set'
|
|
1864
2176
|
if (kind === VAL.MAP) return callee === 'new.Map'
|
|
1865
2177
|
if (kind === VAL.DATE) return callee === 'new.Date'
|
|
1866
|
-
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer'
|
|
2178
|
+
if (kind === VAL.BUFFER) return callee === 'new.ArrayBuffer'
|
|
1867
2179
|
if (kind === VAL.TYPED) return callee.endsWith('Array') && callee !== 'new.ArrayBuffer'
|
|
1868
2180
|
}
|
|
1869
2181
|
// Call to narrow-ABI'd helper of matching VAL kind.
|
|
@@ -1884,77 +2196,165 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1884
2196
|
}
|
|
1885
2197
|
return false
|
|
1886
2198
|
}
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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()
|
|
1891
2249
|
|
|
1892
|
-
|
|
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) => {
|
|
1893
2268
|
if (!Array.isArray(node)) return
|
|
1894
|
-
const
|
|
2269
|
+
const op = node[0]
|
|
1895
2270
|
if (op === '=>') return
|
|
1896
2271
|
if (op === 'let' || op === 'const') {
|
|
1897
|
-
for (
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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)
|
|
1906
2282
|
}
|
|
2283
|
+
return
|
|
1907
2284
|
}
|
|
1908
|
-
for (
|
|
2285
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
1909
2286
|
}
|
|
2287
|
+
collect(body)
|
|
2288
|
+
for (const [n, c] of declCount) if (c > 1) cand.delete(n)
|
|
2289
|
+
if (!cand.size) return new Set()
|
|
1910
2290
|
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2291
|
+
// Pass 2 — every occurrence must be a `.`/`?.`/`[]` read receiver (c).
|
|
2292
|
+
const live = new Set(cand.keys())
|
|
2293
|
+
const walk = (node, inClosure) => {
|
|
1914
2294
|
if (!Array.isArray(node)) return
|
|
1915
|
-
const
|
|
1916
|
-
if (op === '
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
for (let i = 0; i < 2; i++) {
|
|
1927
|
-
const side = args[i], other = args[1 - i]
|
|
1928
|
-
if (typeof side === 'string' && candidates.has(side) && isNullishLit(other)) disqualified.add(side)
|
|
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)
|
|
1929
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)
|
|
1930
2329
|
}
|
|
1931
|
-
for (const a of args) check(a)
|
|
1932
2330
|
}
|
|
2331
|
+
walk(body, false)
|
|
2332
|
+
if (!live.size) return live
|
|
1933
2333
|
|
|
1934
|
-
//
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
const
|
|
1938
|
-
function countAssigns(node, inLet) {
|
|
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) => {
|
|
1939
2338
|
if (!Array.isArray(node)) return
|
|
1940
|
-
const
|
|
1941
|
-
if (op === '
|
|
1942
|
-
|
|
1943
|
-
|
|
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)
|
|
1944
2346
|
}
|
|
1945
|
-
|
|
1946
|
-
for (const a of args) countAssigns(a, childInLet)
|
|
2347
|
+
for (let i = 1; i < node.length; i++) scanStores(node[i])
|
|
1947
2348
|
}
|
|
2349
|
+
scanStores(body)
|
|
2350
|
+
if (unknownStore) return new Set()
|
|
1948
2351
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
const result = new Map()
|
|
1956
|
-
for (const name of candidates) if (!disqualified.has(name)) result.set(name, valOf(name))
|
|
1957
|
-
return result
|
|
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
|
|
1958
2358
|
}
|
|
1959
2359
|
|
|
1960
2360
|
// === Param / closure helpers ===
|
|
@@ -2038,6 +2438,17 @@ export function findFreeVars(node, bound, free, scope) {
|
|
|
2038
2438
|
findFreeVars(args[1], innerBound, free, scope)
|
|
2039
2439
|
return
|
|
2040
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
|
+
}
|
|
2041
2452
|
if (op === 'let' || op === 'const') {
|
|
2042
2453
|
collectParamNames(args, bound)
|
|
2043
2454
|
if (scope) collectParamNames(args, scope)
|
|
@@ -2065,65 +2476,11 @@ export function findMutations(node, names, mutated) {
|
|
|
2065
2476
|
for (const a of args) findMutations(a, names, mutated)
|
|
2066
2477
|
}
|
|
2067
2478
|
|
|
2068
|
-
/**
|
|
2069
|
-
* Pre-scan AST for variables that need a `__dyn_props` shadow sidecar.
|
|
2070
|
-
*
|
|
2071
|
-
* The shadow exists so `obj[runtimeKey]` can read a value via `__dyn_get`,
|
|
2072
|
-
* and `obj.prop = v` keeps the sidecar in sync. Most object literals are only
|
|
2073
|
-
* accessed via `.prop` or `obj['lit']`, both of which resolve through the
|
|
2074
|
-
* schema directly and bypass the shadow. Allocating + populating the sidecar
|
|
2075
|
-
* for those literals is pure waste.
|
|
2076
|
-
*
|
|
2077
|
-
* Populates:
|
|
2078
|
-
* - ctx.types.dynKeyVars: Set<string> — names accessed via runtime key
|
|
2079
|
-
* - ctx.types.anyDynKey: boolean — any dynamic key access exists in program
|
|
2080
|
-
* (used for escaping literals where no target var is known)
|
|
2081
|
-
*/
|
|
2082
|
-
export function analyzeDynKeys(...roots) {
|
|
2083
|
-
const dynVars = new Set()
|
|
2084
|
-
let anyDyn = false
|
|
2085
|
-
|
|
2086
|
-
function walk(node) {
|
|
2087
|
-
if (!Array.isArray(node)) return
|
|
2088
|
-
const [op, ...args] = node
|
|
2089
|
-
if (op === '[]') {
|
|
2090
|
-
const [obj, idx] = args
|
|
2091
|
-
if (!isLiteralStr(idx)) {
|
|
2092
|
-
anyDyn = true
|
|
2093
|
-
if (typeof obj === 'string') dynVars.add(obj)
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
2097
|
-
const [, obj, idx] = args[0]
|
|
2098
|
-
if (!isLiteralStr(idx)) {
|
|
2099
|
-
anyDyn = true
|
|
2100
|
-
if (typeof obj === 'string') dynVars.add(obj)
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
// Runtime for-in (compile-time unroll didn't fire) → walks via shadow
|
|
2104
|
-
if (op === 'for-in') {
|
|
2105
|
-
anyDyn = true
|
|
2106
|
-
if (typeof args[1] === 'string') dynVars.add(args[1])
|
|
2107
|
-
}
|
|
2108
|
-
for (const a of args) walk(a)
|
|
2109
|
-
}
|
|
2110
|
-
for (const r of roots) walk(r)
|
|
2111
|
-
if (ctx.func.list) for (const f of ctx.func.list) if (f.body) walk(f.body)
|
|
2112
|
-
const initFacts = ctx.module.initFacts
|
|
2113
|
-
if (initFacts?.anyDyn) {
|
|
2114
|
-
anyDyn = true
|
|
2115
|
-
for (const v of initFacts.dynVars) dynVars.add(v)
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
ctx.types.dynKeyVars = dynVars
|
|
2119
|
-
ctx.types.anyDynKey = anyDyn
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
2479
|
/**
|
|
2123
2480
|
* Pre-scan function body for captured variables that are mutated.
|
|
2124
2481
|
* Marks mutably-captured vars in ctx.func.boxed for cell-based capture.
|
|
2125
2482
|
*/
|
|
2126
|
-
export function
|
|
2483
|
+
export function boxedCaptures(body) {
|
|
2127
2484
|
const outerScope = new Set()
|
|
2128
2485
|
;(function collectDecls(node) {
|
|
2129
2486
|
if (!Array.isArray(node)) return
|
|
@@ -2225,7 +2582,7 @@ export function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
|
2225
2582
|
for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
|
|
2226
2583
|
const localElems = facts[sliceKey]
|
|
2227
2584
|
ctx.func.locals = savedLocals
|
|
2228
|
-
const paramElemMap =
|
|
2585
|
+
const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
|
|
2229
2586
|
const resolveExpr = (expr) => {
|
|
2230
2587
|
if (typeof expr === 'string') {
|
|
2231
2588
|
if (localElems.has(expr)) {
|
|
@@ -2258,6 +2615,452 @@ export function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
|
2258
2615
|
}
|
|
2259
2616
|
}
|
|
2260
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
|
+
|
|
2261
3064
|
/**
|
|
2262
3065
|
* Phase: program-fact collection.
|
|
2263
3066
|
*
|
|
@@ -2372,7 +3175,16 @@ export function collectProgramFacts(ast) {
|
|
|
2372
3175
|
// through valTypeOf → lookupValType. Skips into closures — they're observed via
|
|
2373
3176
|
// their own func.list entry. The overlay is the per-function analyzeBody.valTypes
|
|
2374
3177
|
// map (already populated with the same overlay-aware walk).
|
|
2375
|
-
if (doSchema && f.hasSchemaLiterals)
|
|
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
|
+
}
|
|
2376
3188
|
|
|
2377
3189
|
return {
|
|
2378
3190
|
dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
@@ -2431,3 +3243,576 @@ export function observeProgramSlots(ast) {
|
|
|
2431
3243
|
}
|
|
2432
3244
|
ctx.func.localValTypesOverlay = prevOverlay
|
|
2433
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
|
+
}
|