jz 0.5.1 → 0.6.0

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