jz 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
package/src/infer.js
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/infer — unified per-binding inference.
|
|
3
|
+
*
|
|
4
|
+
* Single front door for "what shape is this binding?". Evidence sources walk a
|
|
5
|
+
* function body and report facts about a candidate name set; `inferParams`
|
|
6
|
+
* runs every registered source and merges results (first source wins).
|
|
7
|
+
*
|
|
8
|
+
* ## Evidence ladder (strongest first, registration order = precedence)
|
|
9
|
+
*
|
|
10
|
+
* 1. Literal use — `let x = 0`, `let s = ''`, `let xs = []`. [done: analyzeValTypes]
|
|
11
|
+
* 2. Operator use — `s.charCodeAt(...)` → STRING. [done: method source]
|
|
12
|
+
* 3. Member access — `.push` / `.pop` → ARRAY; index/length-write
|
|
13
|
+
* → notString. [done: method + notString sources]
|
|
14
|
+
* 4. `typeof` guard — `typeof x === 'string'` flow-refines. [done: extractRefinements + B3]
|
|
15
|
+
* 5. Assignment flow — `x = y` propagates y's evidence to x. [done: analyzeValTypes]
|
|
16
|
+
* 6. Comparison shape — `x === null` proves nullable. [out of scope: no nullable rep field, see C2]
|
|
17
|
+
* 7. JSDoc `@type` — explicit hint; advisory, not enforced. [in prepare]
|
|
18
|
+
* 8. Name heuristic — last resort (e.g. `count`/`n`/`i` integer). [out of scope]
|
|
19
|
+
*
|
|
20
|
+
* Rungs 1/5 live in `analyzeValTypes` rather than as registry sources because
|
|
21
|
+
* they share the canonical body-walk machinery (regex tracking, typed-elem
|
|
22
|
+
* tracking, JSON-shape, arr-elem schema, ternary unification) and lifting
|
|
23
|
+
* them would duplicate that walker. Rung 4 lives in `extractRefinements`
|
|
24
|
+
* because flow-scoped narrowing is per-branch, not param-wide — adding it as
|
|
25
|
+
* a registry source would over-narrow params whose other branch handles a
|
|
26
|
+
* non-string (callers correctly stay polymorphic in that case).
|
|
27
|
+
*
|
|
28
|
+
* Ambiguous bindings stay nanbox-tagged f64. Default is never wrong, only
|
|
29
|
+
* sometimes wider than necessary.
|
|
30
|
+
*
|
|
31
|
+
* ## Source contract
|
|
32
|
+
*
|
|
33
|
+
* `(body, candidates: string[]) => Map<name, { val?: VAL, … }>`
|
|
34
|
+
*
|
|
35
|
+
* Sources see the full body AST and a candidate-name set. They return only
|
|
36
|
+
* names for which they have definite evidence. `{ val }` is canonical; future
|
|
37
|
+
* sources may add `arrayElemValType`, `intConst`, etc. — `inferParams` returns
|
|
38
|
+
* the full fact, and the caller passes it straight to `updateRep`.
|
|
39
|
+
*
|
|
40
|
+
* Convenience readers (`infer`, `facts`) sit at the bottom for hot-path
|
|
41
|
+
* lookups after passes have populated the per-binding rep.
|
|
42
|
+
*
|
|
43
|
+
* @module src/infer
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { ctx } from './ctx.js'
|
|
47
|
+
import {
|
|
48
|
+
VAL, collectParamNames, valTypeOf,
|
|
49
|
+
updateRep, updateGlobalRep, analyzeValTypes, analyzeIntCertain,
|
|
50
|
+
staticObjectProps, typedElemCtor, ctorFromElemAux, shapeOfObjectLiteralAst,
|
|
51
|
+
} from './analyze.js'
|
|
52
|
+
|
|
53
|
+
// === typeof predicate helper ==============================================
|
|
54
|
+
//
|
|
55
|
+
// `typeof name == lit` / `!= lit` is the canonical narrowing predicate.
|
|
56
|
+
// Two consumers — body-walk evidence (`notStringEvidence` below) and
|
|
57
|
+
// flow-sensitive refinement (`extractRefinements` in src/emit.js) — used to
|
|
58
|
+
// re-implement the recognizer independently with diverging tolerances for
|
|
59
|
+
// the literal form (raw `'string'` vs prepare-normalized typeof-code `-2`).
|
|
60
|
+
// The helper accepts both, so callers stop caring about the normalization
|
|
61
|
+
// boundary.
|
|
62
|
+
|
|
63
|
+
/** Match a `typeof name <op> lit` predicate. Returns `{ name, code, eq }` —
|
|
64
|
+
* `name` is the typeof's operand binding, `code` is either the raw type
|
|
65
|
+
* string ('string'|'number'|'function'|…) or the prepare-normalized typeof
|
|
66
|
+
* code (-1|-2|-3|-4|-5|-6, see TYPEOF_MAP in prepare.js), and `eq` is true
|
|
67
|
+
* for `==`/`===` (false for `!=`/`!==`). Returns null when the node isn't a
|
|
68
|
+
* typeof predicate. */
|
|
69
|
+
export function typeofPredicate(node) {
|
|
70
|
+
if (!Array.isArray(node)) return null
|
|
71
|
+
const op = node[0]
|
|
72
|
+
if (op !== '==' && op !== '===' && op !== '!=' && op !== '!==') return null
|
|
73
|
+
const a = node[1], b = node[2]
|
|
74
|
+
const typeofSide = Array.isArray(a) && a[0] === 'typeof' && typeof a[1] === 'string' ? a
|
|
75
|
+
: Array.isArray(b) && b[0] === 'typeof' && typeof b[1] === 'string' ? b : null
|
|
76
|
+
if (!typeofSide) return null
|
|
77
|
+
const litSide = typeofSide === a ? b : a
|
|
78
|
+
const code = Array.isArray(litSide) && litSide[0] == null ? litSide[1] : null
|
|
79
|
+
if (code == null) return null
|
|
80
|
+
return { name: typeofSide[1], code, eq: op === '==' || op === '===' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// === paramReps lattice primitives ==========================================
|
|
84
|
+
//
|
|
85
|
+
// `paramReps: Map<funcName, Map<paramIdx, ValueRep>>` is the cross-call fact
|
|
86
|
+
// lattice. Evidence sources (body-walk and call-site `infer*`) produce
|
|
87
|
+
// raw observations; these primitives apply them with sticky-null poison
|
|
88
|
+
// semantics: undefined → observe (set); equal → stay; disagreement → null
|
|
89
|
+
// (sticky). null is "no consensus" — readers treat it as missing.
|
|
90
|
+
//
|
|
91
|
+
// Lifecycle phases (chronological — readers should know which fields are
|
|
92
|
+
// valid at which point):
|
|
93
|
+
//
|
|
94
|
+
// 1. prepare ─ no paramReps yet. AST walk collects callSites with raw
|
|
95
|
+
// arg lists; programFacts.paramReps is allocated empty.
|
|
96
|
+
//
|
|
97
|
+
// 2. collectProgramFacts ─ unchanged paramReps; aggregates module-global
|
|
98
|
+
// callSites, valueUsed, hasSchemaLiterals into
|
|
99
|
+
// programFacts. paramReps still empty here.
|
|
100
|
+
//
|
|
101
|
+
// 3. narrowSignatures phase D (call-site lattice) ─
|
|
102
|
+
// runCallsiteLattice merges raw call-site facts via
|
|
103
|
+
// mergeParamFact for fields: val, schemaId, intConst,
|
|
104
|
+
// arrayElemSchema, arrayElemValType, typedCtor, wasm.
|
|
105
|
+
// After D, these may be undefined/null/value.
|
|
106
|
+
// Sticky-null reachable on any field whose call sites
|
|
107
|
+
// disagreed.
|
|
108
|
+
//
|
|
109
|
+
// 4. validateIntConstParams ─ clears intConst on any param whose body
|
|
110
|
+
// contains a write to it (intConst's contract is "no
|
|
111
|
+
// writes after the call").
|
|
112
|
+
//
|
|
113
|
+
// 5. phase E / E2 / E3 (param ABI narrowing) ─
|
|
114
|
+
// applyI32ParamSpecialization sets sig.params[k].type
|
|
115
|
+
// to 'i32' based on rep.wasm consensus. Then
|
|
116
|
+
// applyPointerParamAbi sets rep.ptrKind on
|
|
117
|
+
// consistently-OBJECT/ARRAY/etc params; this prepares
|
|
118
|
+
// the i32-offset ABI for unboxed pointer params.
|
|
119
|
+
//
|
|
120
|
+
// 6. phase F (signature fixpoint) ─ clearStickyNull on val + schemaId,
|
|
121
|
+
// then re-runs D until stable. New evidence from
|
|
122
|
+
// newly-narrowed return types (valResult) can unstick.
|
|
123
|
+
//
|
|
124
|
+
// 7. phase G (narrowReturnArrayElems) ─ propagates Array<T> element
|
|
125
|
+
// facts back through return paths into caller param
|
|
126
|
+
// reps. After G, arrayElemSchema/arrayElemValType
|
|
127
|
+
// reflect the transitive closure.
|
|
128
|
+
//
|
|
129
|
+
// 8. phase H (applyTypedPointerParamAbi) ─ sets ptrKind=TYPED + ptrAux
|
|
130
|
+
// (elem code) for params whose val converged to TYPED.
|
|
131
|
+
// Depends on F having seeded val first.
|
|
132
|
+
//
|
|
133
|
+
// 9. phase I (resetParamWasmFacts + final i32 spec) ─ last WASM-level
|
|
134
|
+
// pass. After H/I, sig.params[k].type and rep.ptrKind
|
|
135
|
+
// are frozen for emit.
|
|
136
|
+
//
|
|
137
|
+
// 10. per-function compile (emit start) ─ each func reads its
|
|
138
|
+
// paramReps[name][k] and folds the consensus facts
|
|
139
|
+
// into ctx.func.localReps via updateRep. Locals and
|
|
140
|
+
// params then share one ValueRep store for the
|
|
141
|
+
// remainder of emit.
|
|
142
|
+
|
|
143
|
+
/** Per-call-site fact merge into a param's ValueRep field. */
|
|
144
|
+
export const mergeParamFact = (rep, key, observed) => {
|
|
145
|
+
if (rep[key] === null) return // sticky poison
|
|
146
|
+
if (observed == null) { rep[key] = null; return } // unknown → poison
|
|
147
|
+
if (rep[key] === undefined) rep[key] = observed
|
|
148
|
+
else if (rep[key] !== observed) rep[key] = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get-or-create per-param rep at (funcName, paramIdx) on a paramReps map. */
|
|
152
|
+
export const ensureParamRep = (paramReps, funcName, k) => {
|
|
153
|
+
let m = paramReps.get(funcName)
|
|
154
|
+
if (!m) { m = new Map(); paramReps.set(funcName, m) }
|
|
155
|
+
let r = m.get(k)
|
|
156
|
+
if (!r) { r = {}; m.set(k, r) }
|
|
157
|
+
return r
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Reset sticky-null on a single field across all params program-wide.
|
|
161
|
+
* Used between fixpoint phases when newly-narrowed facts unblock previously-
|
|
162
|
+
* poisoned observations (e.g. valResult set after first pass). */
|
|
163
|
+
export const clearStickyNull = (paramReps, key) => {
|
|
164
|
+
for (const m of paramReps.values()) for (const r of m.values()) {
|
|
165
|
+
if (r[key] === null) r[key] = undefined
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// === Source registry =======================================================
|
|
170
|
+
|
|
171
|
+
const SOURCES = []
|
|
172
|
+
|
|
173
|
+
/** Register an evidence source. Insertion order = precedence: earlier sources
|
|
174
|
+
* win the merge for a given name. */
|
|
175
|
+
export const registerEvidence = (name, fn) => { SOURCES.push({ name, fn }) }
|
|
176
|
+
|
|
177
|
+
/** Infer per-name facts by running every registered evidence source.
|
|
178
|
+
* Returns Map<name, fact>; callers pass `fact` straight to updateRep.
|
|
179
|
+
*
|
|
180
|
+
* Merge semantics: first source wins per FIELD. Sources contribute orthogonal
|
|
181
|
+
* facts (`methodEvidence` → `{val}`, future sources may add `{notString}`,
|
|
182
|
+
* `{intConst}`, etc.); a later source's field is only kept if no earlier
|
|
183
|
+
* source set the same key on the same name. */
|
|
184
|
+
export const inferParams = (body, candidates) => {
|
|
185
|
+
if (!candidates || candidates.length === 0) return new Map()
|
|
186
|
+
const merged = new Map()
|
|
187
|
+
for (const { fn } of SOURCES) {
|
|
188
|
+
const facts = fn(body, candidates)
|
|
189
|
+
for (const [n, fact] of facts) {
|
|
190
|
+
const prev = merged.get(n)
|
|
191
|
+
merged.set(n, prev ? { ...fact, ...prev } : fact)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return merged
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// === Source: method evidence (rungs 2-3, member-access shape) ==============
|
|
198
|
+
//
|
|
199
|
+
// `name.method(...)` is the strongest cheap signal we get for the STRING vs
|
|
200
|
+
// ARRAY distinction. The method-name partition is three sets:
|
|
201
|
+
//
|
|
202
|
+
// STRING_ONLY — definite STRING (no Array/TypedArray equivalent)
|
|
203
|
+
// ARRAY_INDUCERS — definite plain Array (absent on String + TypedArray)
|
|
204
|
+
// ARRAY_ONLY_POISON— Array + TypedArray (poisons tentative STRING, doesn't
|
|
205
|
+
// induce ARRAY because the receiver may still be TYPED)
|
|
206
|
+
//
|
|
207
|
+
// Reassignment to an unambiguously-typed RHS (`x = 0`) poisons the inference
|
|
208
|
+
// regardless of prior evidence — a later method call can't re-induce a shape
|
|
209
|
+
// already contradicted by an earlier scalar assignment.
|
|
210
|
+
|
|
211
|
+
const STRING_ONLY_METHODS = new Set([
|
|
212
|
+
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
213
|
+
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
214
|
+
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
215
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
216
|
+
])
|
|
217
|
+
const ARRAY_ONLY_POISON = new Set([
|
|
218
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
219
|
+
'flat', 'flatMap', 'copyWithin',
|
|
220
|
+
])
|
|
221
|
+
const ARRAY_INDUCERS = new Set([
|
|
222
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'flat', 'flatMap',
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
const methodEvidence = (body, names) => {
|
|
226
|
+
const scope0 = new Set(names)
|
|
227
|
+
const evidence = new Map() // name → 'string' | 'array' | 'conflict'
|
|
228
|
+
const induce = (name, kind) => {
|
|
229
|
+
const prev = evidence.get(name)
|
|
230
|
+
if (prev === 'conflict') return
|
|
231
|
+
if (prev && prev !== kind) return evidence.set(name, 'conflict')
|
|
232
|
+
evidence.set(name, kind)
|
|
233
|
+
}
|
|
234
|
+
function walk(node, scope) {
|
|
235
|
+
if (!Array.isArray(node) || scope.size === 0) return
|
|
236
|
+
const op = node[0]
|
|
237
|
+
if (op === '=>') {
|
|
238
|
+
// Nested arrow: drop shadowed names so an inner param of the same name
|
|
239
|
+
// is treated independently. Captured names retain their outer evidence.
|
|
240
|
+
const shadowed = collectParamNames([node[1]])
|
|
241
|
+
let inner = scope
|
|
242
|
+
for (const s of shadowed) {
|
|
243
|
+
if (inner.has(s)) {
|
|
244
|
+
if (inner === scope) inner = new Set(scope)
|
|
245
|
+
inner.delete(s)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
walk(node[2], inner)
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (op === '.' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
252
|
+
const name = node[1]
|
|
253
|
+
const m = node[2]
|
|
254
|
+
if (typeof m === 'string') {
|
|
255
|
+
if (STRING_ONLY_METHODS.has(m)) induce(name, 'string')
|
|
256
|
+
else if (ARRAY_INDUCERS.has(m)) induce(name, 'array')
|
|
257
|
+
else if (ARRAY_ONLY_POISON.has(m) && evidence.get(name) === 'string') {
|
|
258
|
+
evidence.set(name, 'conflict')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Runtime type-discrimination — `typeof x`, `Array.isArray(x)` — proves the
|
|
263
|
+
// binding is polymorphic: the body itself branches on its tag. Narrowing it
|
|
264
|
+
// to a single pointer kind erases the very tag the check reads (`typeof`
|
|
265
|
+
// would fold to a constant, `Array.isArray` likewise), miscompiling a
|
|
266
|
+
// soundly-mixed caller. Force `conflict` so the param stays boxed.
|
|
267
|
+
if (op === 'typeof' && typeof node[1] === 'string' && scope.has(node[1]))
|
|
268
|
+
evidence.set(node[1], 'conflict')
|
|
269
|
+
if (op === '()' && node[1] === 'Array.isArray' && typeof node[2] === 'string' && scope.has(node[2]))
|
|
270
|
+
evidence.set(node[2], 'conflict')
|
|
271
|
+
if (op === '=' && typeof node[1] === 'string' && scope.has(node[1])) {
|
|
272
|
+
const name = node[1]
|
|
273
|
+
const vt = valTypeOf(node[2])
|
|
274
|
+
if (vt && vt !== VAL.STRING && vt !== VAL.ARRAY) evidence.set(name, 'conflict')
|
|
275
|
+
else if (vt === VAL.STRING && evidence.get(name) === 'array') evidence.set(name, 'conflict')
|
|
276
|
+
else if (vt === VAL.ARRAY && evidence.get(name) === 'string') evidence.set(name, 'conflict')
|
|
277
|
+
}
|
|
278
|
+
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
279
|
+
}
|
|
280
|
+
walk(body, scope0)
|
|
281
|
+
const out = new Map()
|
|
282
|
+
for (const [n, ev] of evidence) {
|
|
283
|
+
if (ev === 'string') out.set(n, { val: VAL.STRING })
|
|
284
|
+
else if (ev === 'array') out.set(n, { val: VAL.ARRAY })
|
|
285
|
+
}
|
|
286
|
+
return out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
registerEvidence('method', methodEvidence)
|
|
290
|
+
|
|
291
|
+
// === Source: not-string evidence (rung 3, write-shape) =====================
|
|
292
|
+
//
|
|
293
|
+
// Strings in JS are immutable: `s[i] = v` is silently dropped (strict throws),
|
|
294
|
+
// `s.length = n` likewise has no effect. An *unambiguous write* through
|
|
295
|
+
// `xs[i]` / `xs[i] op= v` / `xs.length = n` / `++xs.length` would prove the
|
|
296
|
+
// receiver isn't a primitive string — but param flow can mix shapes via
|
|
297
|
+
// `typeof x === 'string'` gates (e.g. watr's AST walker takes both array and
|
|
298
|
+
// string nodes). Without flow-sensitive refinement the narrowing turns
|
|
299
|
+
// soundly-mixed callers into miscompilations: a post-gate `node[0]` read
|
|
300
|
+
// would route through `__typed_idx` and return garbage on a string tag.
|
|
301
|
+
//
|
|
302
|
+
// So we require a *conservative* discharge: ANY string-shape evidence on the
|
|
303
|
+
// same name (typeof string check, STRING_ONLY_METHODS call, string-literal
|
|
304
|
+
// assignment) disables the narrow. Method evidence promoting to VAL.ARRAY
|
|
305
|
+
// (push/pop/etc.) wins via the merge regardless and subsumes notString. The
|
|
306
|
+
// remaining win: pure write+length-only params (e.g. `fill(buf, v)`) skip
|
|
307
|
+
// the runtime `__ptr_type==STRING` gate at every read.
|
|
308
|
+
|
|
309
|
+
const isLengthAccess = (n) =>
|
|
310
|
+
Array.isArray(n) && n[0] === '.' && typeof n[1] === 'string' && n[2] === 'length'
|
|
311
|
+
|
|
312
|
+
const isIndexAccess = (n) =>
|
|
313
|
+
Array.isArray(n) && n[0] === '[]' && typeof n[1] === 'string'
|
|
314
|
+
|
|
315
|
+
const isAssignOp = (op) =>
|
|
316
|
+
typeof op === 'string' && (op === '=' || (op.length > 1 && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
|
|
317
|
+
|
|
318
|
+
const isStringLiteralRhs = (rhs) =>
|
|
319
|
+
Array.isArray(rhs) && (rhs[0] === 'str' || (rhs[0] == null && typeof rhs[1] === 'string'))
|
|
320
|
+
|
|
321
|
+
const notStringEvidence = (body, names) => {
|
|
322
|
+
const scope0 = new Set(names)
|
|
323
|
+
const writes = new Set() // candidates with at least one index/length-write site
|
|
324
|
+
const stringy = new Set() // candidates with positive string-shape evidence (disqualified)
|
|
325
|
+
const markStringy = (n) => { stringy.add(n); writes.delete(n) }
|
|
326
|
+
function walk(node, scope) {
|
|
327
|
+
if (!Array.isArray(node) || scope.size === 0) return
|
|
328
|
+
const op = node[0]
|
|
329
|
+
if (op === '=>') {
|
|
330
|
+
const shadowed = collectParamNames([node[1]])
|
|
331
|
+
let inner = scope
|
|
332
|
+
for (const s of shadowed) {
|
|
333
|
+
if (inner.has(s)) {
|
|
334
|
+
if (inner === scope) inner = new Set(scope)
|
|
335
|
+
inner.delete(s)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
walk(node[2], inner)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
// typeof x === 'string' / 'string' === typeof x → x is sometimes a string.
|
|
342
|
+
// The helper handles both raw-string and prepare-normalized -2 forms; the
|
|
343
|
+
// `eq` flag is intentionally ignored — `!=` 'string' is also positive
|
|
344
|
+
// evidence the binding *can* be string in some flow.
|
|
345
|
+
const tp = typeofPredicate(node)
|
|
346
|
+
if (tp && (tp.code === 'string' || tp.code === -2) && scope.has(tp.name)) markStringy(tp.name)
|
|
347
|
+
// STRING_ONLY method call: x.charCodeAt(...), x.split(...), etc.
|
|
348
|
+
if (op === '.' && typeof node[1] === 'string' && scope.has(node[1]) &&
|
|
349
|
+
typeof node[2] === 'string' && STRING_ONLY_METHODS.has(node[2])) {
|
|
350
|
+
markStringy(node[1])
|
|
351
|
+
}
|
|
352
|
+
// String-literal assignment: `x = 'foo'` — re-binds to a string.
|
|
353
|
+
if (op === '=' && typeof node[1] === 'string' && scope.has(node[1]) && isStringLiteralRhs(node[2])) {
|
|
354
|
+
markStringy(node[1])
|
|
355
|
+
}
|
|
356
|
+
// Index write: `xs[i] = v` or compound `xs[i] op= v`.
|
|
357
|
+
if (isAssignOp(op) && isIndexAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
358
|
+
writes.add(node[1][1])
|
|
359
|
+
}
|
|
360
|
+
// Length mutation: `xs.length = n`, `xs.length += k`, `xs.length++`.
|
|
361
|
+
if (isAssignOp(op) && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
362
|
+
writes.add(node[1][1])
|
|
363
|
+
}
|
|
364
|
+
if ((op === '++' || op === '--') && isLengthAccess(node[1]) && scope.has(node[1][1]) && !stringy.has(node[1][1])) {
|
|
365
|
+
writes.add(node[1][1])
|
|
366
|
+
}
|
|
367
|
+
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
368
|
+
}
|
|
369
|
+
walk(body, scope0)
|
|
370
|
+
const out = new Map()
|
|
371
|
+
for (const n of writes) if (!stringy.has(n)) out.set(n, { notString: true })
|
|
372
|
+
return out
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
registerEvidence('notString', notStringEvidence)
|
|
376
|
+
|
|
377
|
+
// === Per-function orchestration ============================================
|
|
378
|
+
//
|
|
379
|
+
// Single front door for everything that narrows local + param shape from a
|
|
380
|
+
// function body. Two layers fold together here:
|
|
381
|
+
//
|
|
382
|
+
// • Registry sources (above) seed undecided params with `{ val: VAL.* }`
|
|
383
|
+
// evidence merged across all registered fact-returners.
|
|
384
|
+
// • Body-wide ctx-mutating passes (`analyzeValTypes`, `analyzeIntCertain`)
|
|
385
|
+
// walk the AST and write directly to `ctx.func.localReps` — they also
|
|
386
|
+
// populate `ctx.types.typedElem`, `ctx.schema.vars`, regex tracking, etc.
|
|
387
|
+
// and stay in analyze.js where their helpers live.
|
|
388
|
+
//
|
|
389
|
+
// Callers in compile.js used to repeat the merge boilerplate at every emit
|
|
390
|
+
// entry; centralizing it here keeps the ordering invariant in one place
|
|
391
|
+
// (param facts before body walk — `analyzeValTypes`'s `valTypeOf` consults
|
|
392
|
+
// rep, so seeded params must be visible before the walk starts).
|
|
393
|
+
|
|
394
|
+
/** Run the full per-function inference pipeline against `body`.
|
|
395
|
+
* `candidates` is the param-name set eligible for shape seeding (skip names
|
|
396
|
+
* already typed by an upstream source such as `paramReps`). Side-effects
|
|
397
|
+
* only — facts flow into `ctx.func.localReps` via `updateRep`. */
|
|
398
|
+
export const inferLocals = (body, candidates) => {
|
|
399
|
+
if (candidates && candidates.length) {
|
|
400
|
+
const inferred = inferParams(body, candidates)
|
|
401
|
+
for (const [n, fact] of inferred) updateRep(n, fact)
|
|
402
|
+
}
|
|
403
|
+
analyzeValTypes(body)
|
|
404
|
+
analyzeIntCertain(body)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// === Module-global value-fact recording ===================================
|
|
408
|
+
//
|
|
409
|
+
// Top-level `const X = …` / `let X = …` produces module-global facts the
|
|
410
|
+
// emitter consults at every call/read site: VAL.* for tagged-pointer dispatch,
|
|
411
|
+
// a typed-array ctor for elem-load fast paths, and a regex var registration.
|
|
412
|
+
// Single per-decl atomic — prepare.js calls it inline during its depth-0 walk
|
|
413
|
+
// (the unique authoritative pass). plan.js used to re-walk the top-level
|
|
414
|
+
// statement list with the same logic; that duplicate was deleted once tests
|
|
415
|
+
// confirmed prepare's depth-0 catch is a strict superset.
|
|
416
|
+
export function recordGlobalRep(name, expr) {
|
|
417
|
+
if (typeof name !== 'string') return
|
|
418
|
+
const vt = valTypeOf(expr)
|
|
419
|
+
if (vt) {
|
|
420
|
+
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
421
|
+
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
422
|
+
}
|
|
423
|
+
const ctor = typedElemCtor(expr)
|
|
424
|
+
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
425
|
+
// Static-shape capture for module-level object literals — lets `{ ...G.path }`
|
|
426
|
+
// resolve its source schema by walking the global rep's shape tree at the
|
|
427
|
+
// spread site (see shape walk in analyze.js / resolveSchema in object.js).
|
|
428
|
+
const sh = shapeOfObjectLiteralAst(expr)
|
|
429
|
+
if (sh) updateGlobalRep(name, { jsonShape: sh })
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// === Call-site argument inference =========================================
|
|
433
|
+
//
|
|
434
|
+
// Each `infer*(expr, ...callerCtx)` resolves an argument expression to a
|
|
435
|
+
// single fact (val / schemaId / elem-schema / elem-VAL / typedCtor) using the
|
|
436
|
+
// caller's body-local observations plus module-level program facts. Returns
|
|
437
|
+
// null when the fact can't be pinned down at this call site.
|
|
438
|
+
//
|
|
439
|
+
// These are the call-site mirror of the body-walk evidence sources above:
|
|
440
|
+
// body sources answer "what shape does this binding have?"; call-site
|
|
441
|
+
// extractors answer "what shape does this argument carry into a callee?".
|
|
442
|
+
// Both feed the same `paramReps` lattice via narrow.js' signature fixpoint.
|
|
443
|
+
|
|
444
|
+
/** Infer arg val type using caller's body-local valTypes and module globals. */
|
|
445
|
+
export function inferValType(expr, callerValTypes) {
|
|
446
|
+
if (typeof expr === 'string') return callerValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
447
|
+
return valTypeOf(expr)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Resolve a constant schemaId for an expression in a caller-or-return scope.
|
|
451
|
+
* Sources (in order): per-name `lookupMap` (caller's per-param schemaId map),
|
|
452
|
+
* module-level `ctx.schema.vars` binding, static-key `{}` literal,
|
|
453
|
+
* call to an OBJECT-narrowed function (carries schemaId in `f.sig.ptrAux`),
|
|
454
|
+
* recursive descent through `?:` / `&&` / `||` when both branches agree.
|
|
455
|
+
* Returns the schemaId (number) or null when no constant exists.
|
|
456
|
+
*
|
|
457
|
+
* Used at both call sites (narrow.js D-phase mergeRule for `schemaId`) and
|
|
458
|
+
* return sites (narrow.js phase G's `narrowReturnArrayElems` and the per-fn
|
|
459
|
+
* return-schema narrowing). At early D-iterations the call-result branch
|
|
460
|
+
* is a no-op (valResult not yet seeded by phase F); strictly accretive. */
|
|
461
|
+
export function inferSchemaId(expr, lookupMap) {
|
|
462
|
+
if (typeof expr === 'string') {
|
|
463
|
+
if (lookupMap?.has(expr)) return lookupMap.get(expr)
|
|
464
|
+
const id = ctx.schema.vars.get(expr)
|
|
465
|
+
return id != null ? id : null
|
|
466
|
+
}
|
|
467
|
+
if (!Array.isArray(expr)) return null
|
|
468
|
+
const op = expr[0]
|
|
469
|
+
if (op === '{}') {
|
|
470
|
+
const parsed = staticObjectProps(expr.slice(1))
|
|
471
|
+
return parsed ? ctx.schema.register(parsed.names) : null
|
|
472
|
+
}
|
|
473
|
+
if (op === '()' && typeof expr[1] === 'string') {
|
|
474
|
+
const f = ctx.func.map?.get(expr[1])
|
|
475
|
+
if (f?.valResult === VAL.OBJECT && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
478
|
+
if (op === '?:') {
|
|
479
|
+
const a = inferSchemaId(expr[2], lookupMap)
|
|
480
|
+
const b = inferSchemaId(expr[3], lookupMap)
|
|
481
|
+
return a != null && a === b ? a : null
|
|
482
|
+
}
|
|
483
|
+
if (op === '&&' || op === '||') {
|
|
484
|
+
const a = inferSchemaId(expr[1], lookupMap)
|
|
485
|
+
const b = inferSchemaId(expr[2], lookupMap)
|
|
486
|
+
return a != null && a === b ? a : null
|
|
487
|
+
}
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Infer arg arr-elem-schema. Sources: caller's body-local arr-elem map, caller's
|
|
492
|
+
* per-param arr-elem (transitive), or a call to an arr-narrowed user fn. */
|
|
493
|
+
export function inferArrElemSchema(expr, callerArrElems, callerArrParams) {
|
|
494
|
+
if (typeof expr === 'string') {
|
|
495
|
+
if (callerArrElems?.has(expr)) {
|
|
496
|
+
const v = callerArrElems.get(expr)
|
|
497
|
+
if (v != null) return v
|
|
498
|
+
}
|
|
499
|
+
if (callerArrParams?.has(expr)) {
|
|
500
|
+
const v = callerArrParams.get(expr)
|
|
501
|
+
if (v != null) return v
|
|
502
|
+
}
|
|
503
|
+
return null
|
|
504
|
+
}
|
|
505
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
506
|
+
const f = ctx.func.map?.get(expr[1])
|
|
507
|
+
if (f?.arrayElemSchema != null) return f.arrayElemSchema
|
|
508
|
+
}
|
|
509
|
+
return null
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Infer arg arr-elem-VAL. Mirrors inferArrElemSchema but tracks VAL.* element kind. */
|
|
513
|
+
export function inferArrElemValType(expr, callerArrElemVals, callerArrValParams) {
|
|
514
|
+
if (typeof expr === 'string') {
|
|
515
|
+
if (callerArrElemVals?.has(expr)) {
|
|
516
|
+
const v = callerArrElemVals.get(expr)
|
|
517
|
+
if (v != null) return v
|
|
518
|
+
}
|
|
519
|
+
if (callerArrValParams?.has(expr)) {
|
|
520
|
+
const v = callerArrValParams.get(expr)
|
|
521
|
+
if (v != null) return v
|
|
522
|
+
}
|
|
523
|
+
return null
|
|
524
|
+
}
|
|
525
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
526
|
+
const f = ctx.func.map?.get(expr[1])
|
|
527
|
+
if (f?.arrayElemValType != null) return f.arrayElemValType
|
|
528
|
+
}
|
|
529
|
+
return null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Infer typed-array ctor (`new.Float64Array` etc.) of an arg expression at a call site.
|
|
533
|
+
* Sources: caller's body-local typedElems, caller's typed params, literal `new TypedArray(...)`,
|
|
534
|
+
* calls to typed-narrowed user funcs. Returns null when the ctor can't be determined. */
|
|
535
|
+
export function inferTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
536
|
+
if (typeof expr === 'string') {
|
|
537
|
+
if (callerTypedElems?.has(expr)) return callerTypedElems.get(expr)
|
|
538
|
+
if (callerTypedParams?.has(expr)) return callerTypedParams.get(expr)
|
|
539
|
+
return null
|
|
540
|
+
}
|
|
541
|
+
const ctor = typedElemCtor(expr)
|
|
542
|
+
if (ctor) return ctor
|
|
543
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
544
|
+
const f = ctx.func.map?.get(expr[1])
|
|
545
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) return ctorFromElemAux(f.sig.ptrAux)
|
|
546
|
+
}
|
|
547
|
+
return null
|
|
548
|
+
}
|