jz 0.5.1 → 0.7.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 +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-program fact collection — dyn keys, call sites, schema slots.
|
|
3
|
+
* @module program-facts
|
|
4
|
+
*/
|
|
5
|
+
import { commaList, isFuncRef, isLiteralStr } from '../ast.js'
|
|
6
|
+
import { ctx, err } from '../ctx.js'
|
|
7
|
+
import { VAL, lookupValType, repOf } from '../reps.js'
|
|
8
|
+
import { valTypeOf } from '../kind.js'
|
|
9
|
+
import { extractParams, classifyParam } from '../ast.js'
|
|
10
|
+
import { staticObjectProps } from '../static.js'
|
|
11
|
+
import { intExprChecker } from '../type.js'
|
|
12
|
+
import { analyzeBody } from './analyze.js'
|
|
13
|
+
|
|
14
|
+
// Assignment-shaped ops whose first arg, when a `.`/`?.` member node, is a
|
|
15
|
+
// PROPERTY WRITE — feeds `writtenProps` (any prop name ever written through
|
|
16
|
+
// ANY receiver, including expression receivers like `m.get(k).n++`).
|
|
17
|
+
const PROP_WRITE_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=',
|
|
18
|
+
'&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??=', '++', '--'])
|
|
19
|
+
|
|
20
|
+
export function observeNodeFacts(node, f) {
|
|
21
|
+
if (!Array.isArray(node)) return
|
|
22
|
+
const [op, ...args] = node
|
|
23
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) &&
|
|
24
|
+
(args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string')
|
|
25
|
+
f.writtenProps.add(args[0][2])
|
|
26
|
+
// Computed-key WRITES (`o[k]=v`, `o[k]+=v`, `o[k]++`) are the ONLY operations
|
|
27
|
+
// that add ENUMERABLE keys beyond the static schema — computed reads and dot-adds
|
|
28
|
+
// (`o.b=2`) do not enumerate in jz. Tracked separately from `dynVars` (which also
|
|
29
|
+
// counts reads) so for-in / Object.keys key pooling can trust the static schema
|
|
30
|
+
// for a receiver that is only computed-READ. (`isLiteralStr` excludes literal
|
|
31
|
+
// string keys, which place in fixed schema slots.)
|
|
32
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
33
|
+
const [, wobj, widx] = args[0]
|
|
34
|
+
// Flag the ROOT array var. `o[k]=v` → o; a NESTED write `o[i][j]=v` mutates an
|
|
35
|
+
// element of o, so walk the receiver chain to its root identifier and flag that
|
|
36
|
+
// too — else o's recorded (nested) element types would be wrongly trusted at a
|
|
37
|
+
// later `o[i][j]` read. Strictly more conservative for every dynWriteVars consumer.
|
|
38
|
+
if (!isLiteralStr(widx)) {
|
|
39
|
+
let root = wobj
|
|
40
|
+
while (Array.isArray(root) && root[0] === '[]') root = root[1]
|
|
41
|
+
if (typeof root === 'string') f.dynWriteVars?.add(root)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (op === '[]') {
|
|
45
|
+
const [obj, idx] = args
|
|
46
|
+
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
47
|
+
} else if (op === '=' && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
48
|
+
const [, obj, idx] = args[0]
|
|
49
|
+
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
50
|
+
} else if (op === 'for-in') {
|
|
51
|
+
f.anyDyn = true
|
|
52
|
+
if (typeof args[1] === 'string') f.dynVars.add(args[1])
|
|
53
|
+
} else if (op === '{}') {
|
|
54
|
+
f.hasSchemaLiterals = true
|
|
55
|
+
} else if (op === '=>') {
|
|
56
|
+
let fixedN = 0
|
|
57
|
+
for (const r of extractParams(args[0])) {
|
|
58
|
+
if (classifyParam(r).kind === 'rest') f.hasRest = true
|
|
59
|
+
else fixedN++
|
|
60
|
+
}
|
|
61
|
+
if (fixedN > f.maxDef) f.maxDef = fixedN
|
|
62
|
+
} else if (op === '()') {
|
|
63
|
+
const cargs = commaList(args[1])
|
|
64
|
+
if (cargs.some(x => Array.isArray(x) && x[0] === '...')) f.hasSpread = true
|
|
65
|
+
if (cargs.length > f.maxCall) f.maxCall = cargs.length
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const _programFactsCache = new WeakMap()
|
|
70
|
+
const _moduleInitSlotCache = new WeakMap()
|
|
71
|
+
const _bodyIntCertainCache = new WeakMap()
|
|
72
|
+
let _programFactsGen = 0
|
|
73
|
+
|
|
74
|
+
/** Drop all cached program-fact walks (called at compile entry). */
|
|
75
|
+
export function resetProgramFactsCache() { _programFactsGen++ }
|
|
76
|
+
|
|
77
|
+
/** Drop cached walks for specific AST roots (in-place module rewrites). */
|
|
78
|
+
export function invalidateProgramFactsCache(...roots) {
|
|
79
|
+
for (const r of roots) {
|
|
80
|
+
if (r == null || typeof r !== 'object') continue
|
|
81
|
+
_programFactsCache.delete(r)
|
|
82
|
+
_moduleInitSlotCache.delete(r)
|
|
83
|
+
_bodyIntCertainCache.delete(r)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function emptyWalkFacts() {
|
|
88
|
+
return {
|
|
89
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
90
|
+
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
91
|
+
propMap: new Map(), valueUsed: new Set(), callSites: [],
|
|
92
|
+
writtenProps: new Set(),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeWalkFacts(into, from) {
|
|
97
|
+
if (from.anyDyn) into.anyDyn = true
|
|
98
|
+
for (const v of from.dynVars) into.dynVars.add(v)
|
|
99
|
+
for (const v of from.dynWriteVars) into.dynWriteVars.add(v)
|
|
100
|
+
if (from.hasSchemaLiterals) into.hasSchemaLiterals = true
|
|
101
|
+
if (from.maxDef > into.maxDef) into.maxDef = from.maxDef
|
|
102
|
+
if (from.maxCall > into.maxCall) into.maxCall = from.maxCall
|
|
103
|
+
if (from.hasRest) into.hasRest = true
|
|
104
|
+
if (from.hasSpread) into.hasSpread = true
|
|
105
|
+
for (const p of from.writtenProps) into.writtenProps.add(p)
|
|
106
|
+
for (const [obj, props] of from.propMap) {
|
|
107
|
+
if (!into.propMap.has(obj)) into.propMap.set(obj, new Set())
|
|
108
|
+
for (const p of props) into.propMap.get(obj).add(p)
|
|
109
|
+
}
|
|
110
|
+
for (const v of from.valueUsed) into.valueUsed.add(v)
|
|
111
|
+
into.callSites.push(...from.callSites)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Walk one AST root and accumulate program facts. Function bodies are WeakMap-cached
|
|
115
|
+
* so plan-phase rescans skip unchanged bodies after inlining/scalarization passes.
|
|
116
|
+
* Module AST is never cached — plan may mutate it in place (flattenFuncNamespaces). */
|
|
117
|
+
function walkFactsRoot(root, full, callerFunc, doSchema, cache = true) {
|
|
118
|
+
if (cache && full && root != null && typeof root === 'object') {
|
|
119
|
+
const hit = _programFactsCache.get(root)
|
|
120
|
+
if (hit?.gen === _programFactsGen) return hit.facts
|
|
121
|
+
}
|
|
122
|
+
const acc = emptyWalkFacts()
|
|
123
|
+
const walkFacts = (node, fullWalk, inArrow, caller) => {
|
|
124
|
+
if (!Array.isArray(node)) return
|
|
125
|
+
const [op, ...args] = node
|
|
126
|
+
observeNodeFacts(node, acc)
|
|
127
|
+
if (op === 'for-in' && ctx.transform.strict) err(`strict mode: \`for (... in ...)\` is not allowed (dynamic enumeration). Pass { strict: false } to enable.`)
|
|
128
|
+
if (op === '{}' && doSchema) {
|
|
129
|
+
const parsed = staticObjectProps(args)
|
|
130
|
+
if (parsed) ctx.schema.register(parsed.names)
|
|
131
|
+
}
|
|
132
|
+
if (op === '=>') {
|
|
133
|
+
for (const a of args) walkFacts(a, fullWalk, true, caller)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (fullWalk) {
|
|
137
|
+
if (doSchema && op === '=' && Array.isArray(args[0]) && args[0][0] === '.') {
|
|
138
|
+
const [, obj, prop] = args[0]
|
|
139
|
+
// `.length =` is the structural resize op (emit-assign handles ARRAY/
|
|
140
|
+
// TYPED/unknown receivers) — NOT a schema property. Recording it here
|
|
141
|
+
// auto-boxed the binding (['__inner__','length']): reads then deref'd
|
|
142
|
+
// the box while the resize path persisted the raw array ptr into the
|
|
143
|
+
// global — a read/write protocol split that corrupted cross-module
|
|
144
|
+
// arrays (importer `arr.length = 0` between owner pushes). Only a
|
|
145
|
+
// PROVEN object/hash receiver keeps `length` as a real property.
|
|
146
|
+
const lenVt = prop === 'length' ? ctx.scope.globalValTypes?.get(obj) : null
|
|
147
|
+
const lengthIsResize = prop === 'length' && lenVt !== VAL.OBJECT && lenVt !== VAL.HASH
|
|
148
|
+
if (!lengthIsResize && typeof obj === 'string' && (ctx.scope.globals.has(obj) || ctx.func.names.has(obj))) {
|
|
149
|
+
if (!acc.propMap.has(obj)) acc.propMap.set(obj, new Set())
|
|
150
|
+
acc.propMap.get(obj).add(prop)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (op === '()' && isFuncRef(args[0], ctx.func.names)) {
|
|
154
|
+
// Record the call site even inside an arrow body. The param-inference
|
|
155
|
+
// lattice (narrow.js) must see EVERY arg a callee receives — including
|
|
156
|
+
// calls made from a closure (`mfb(() => ci(2))`) — or it over-specializes:
|
|
157
|
+
// seeing only the direct `ci(0)` site, intConst folds the param to 0 and
|
|
158
|
+
// the closure's `ci(2)` silently loses its argument. Args evaluated in the
|
|
159
|
+
// arrow's scope that the enclosing caller can't type infer as untyped →
|
|
160
|
+
// poison → conservative (no specialization), which is sound.
|
|
161
|
+
{
|
|
162
|
+
const a = args[1]
|
|
163
|
+
const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
164
|
+
acc.callSites.push({ callee: args[0], argList, callerFunc: caller, node })
|
|
165
|
+
}
|
|
166
|
+
for (let i = 1; i < args.length; i++) {
|
|
167
|
+
const a = args[i]
|
|
168
|
+
if (isFuncRef(a, ctx.func.names)) acc.valueUsed.add(a)
|
|
169
|
+
else walkFacts(a, true, inArrow, caller)
|
|
170
|
+
}
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if ((op === '.' || op === '?.') && isFuncRef(args[0], ctx.func.names)) return
|
|
174
|
+
if (op === 'let' || op === 'const') {
|
|
175
|
+
for (const decl of args) {
|
|
176
|
+
if (Array.isArray(decl) && decl[0] === '=' && decl.length >= 3) {
|
|
177
|
+
const name = decl[1]
|
|
178
|
+
if (typeof name === 'string' && ctx.func.names.has(name)) {
|
|
179
|
+
const isFuncLit = Array.isArray(decl[2]) && decl[2][0] === '=>'
|
|
180
|
+
if (isFuncLit || caller?.name !== name) acc.valueUsed.add(name)
|
|
181
|
+
}
|
|
182
|
+
walkFacts(decl[2], true, inArrow, caller)
|
|
183
|
+
} else walkFacts(decl, true, inArrow, caller)
|
|
184
|
+
}
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if (op === '=' && args.length >= 2) {
|
|
188
|
+
// RHS may be a bare function reference (`store[0] = pick3`) — record it as a
|
|
189
|
+
// value use so resolveClosureWidth sizes the closure ABI to its arity. Matches
|
|
190
|
+
// the func-ref handling in the call/let/general cases below.
|
|
191
|
+
if (isFuncRef(args[1], ctx.func.names)) acc.valueUsed.add(args[1])
|
|
192
|
+
else walkFacts(args[1], true, inArrow, caller)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
for (const a of args) {
|
|
196
|
+
if (isFuncRef(a, ctx.func.names)) acc.valueUsed.add(a)
|
|
197
|
+
else walkFacts(a, true, inArrow, caller)
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
for (const a of args) walkFacts(a, false, inArrow, caller)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
walkFacts(root, full, false, callerFunc)
|
|
204
|
+
if (cache && full && root != null && typeof root === 'object')
|
|
205
|
+
_programFactsCache.set(root, { gen: _programFactsGen, facts: acc })
|
|
206
|
+
return acc
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function collectProgramFacts(ast) {
|
|
210
|
+
const paramReps = new Map()
|
|
211
|
+
const doSchema = ast && ctx.schema.register
|
|
212
|
+
const doArity = !!ctx.closure.make
|
|
213
|
+
const f = emptyWalkFacts()
|
|
214
|
+
mergeWalkFacts(f, walkFactsRoot(ast, true, null, doSchema, false))
|
|
215
|
+
for (const func of ctx.func.list) {
|
|
216
|
+
if (func.body && !func.raw) mergeWalkFacts(f, walkFactsRoot(func.body, true, func, doSchema, true))
|
|
217
|
+
}
|
|
218
|
+
const { propMap, valueUsed, callSites } = f
|
|
219
|
+
const initFacts = ctx.module.initFacts
|
|
220
|
+
if (initFacts) {
|
|
221
|
+
if (initFacts.anyDyn) {
|
|
222
|
+
f.anyDyn = true
|
|
223
|
+
for (const v of initFacts.dynVars) f.dynVars.add(v)
|
|
224
|
+
}
|
|
225
|
+
if (initFacts.writtenProps) for (const p of initFacts.writtenProps) f.writtenProps.add(p)
|
|
226
|
+
if (doArity) {
|
|
227
|
+
if (initFacts.maxDef > f.maxDef) f.maxDef = initFacts.maxDef
|
|
228
|
+
if (initFacts.maxCall > f.maxCall) f.maxCall = initFacts.maxCall
|
|
229
|
+
if (initFacts.hasRest) f.hasRest = true
|
|
230
|
+
if (initFacts.hasSpread) f.hasSpread = true
|
|
231
|
+
}
|
|
232
|
+
if (doSchema && initFacts.hasSchemaLiterals) f.hasSchemaLiterals = true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Slot-type observation pass: walk every `{}` literal with the right scope's
|
|
236
|
+
// valTypes installed as `ctx.func.localValTypesOverlay` so shorthand `{x}`
|
|
237
|
+
// (expanded by prepare to `[':', x, x]`) and chained typed-array reads resolve
|
|
238
|
+
// through valTypeOf → lookupValType. Skips into closures — they're observed via
|
|
239
|
+
// their own func.list entry. The overlay is the per-function analyzeBody.valTypes
|
|
240
|
+
// map (already populated with the same overlay-aware walk).
|
|
241
|
+
if (doSchema && f.hasSchemaLiterals) {
|
|
242
|
+
observeProgramSlots(ast)
|
|
243
|
+
// Per-slot intCertain mirror of the per-binding lattice. Runs after slot
|
|
244
|
+
// type observation (which it does not depend on) — same trigger gate so
|
|
245
|
+
// programs without schema literals skip both. Re-runnable: subsequent
|
|
246
|
+
// collectProgramFacts invocations (E2 phase) overwrite the same map; the
|
|
247
|
+
// analysis is monotone-down so re-running can only widen poisoning, never
|
|
248
|
+
// un-poison — safe.
|
|
249
|
+
analyzeSchemaSlotIntCertain(ast)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Emit-time consumers (the static object-literal fast path) read this off
|
|
253
|
+
// ctx — a mutated prop name anywhere disqualifies sharing a static instance.
|
|
254
|
+
ctx.module.writtenProps = f.writtenProps
|
|
255
|
+
return {
|
|
256
|
+
dynVars: f.dynVars, dynWriteVars: f.dynWriteVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
257
|
+
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
258
|
+
paramReps, hasSchemaLiterals: f.hasSchemaLiterals, writtenProps: f.writtenProps,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Re-collect program facts after a mutating plan pass. Unchanged function bodies
|
|
263
|
+
* reuse WeakMap-cached walks from the prior collectProgramFacts call. */
|
|
264
|
+
export const refreshProgramFacts = (ast, _prev) => collectProgramFacts(ast)
|
|
265
|
+
|
|
266
|
+
/** Walk `ast` + every user function body + module inits, observing slot types
|
|
267
|
+
* on each `{}` literal. Per-function bodies have their analyzeBody.valTypes
|
|
268
|
+
* installed as overlay so shorthand `{x}` resolves through local consts.
|
|
269
|
+
*
|
|
270
|
+
* Re-runnable: compile.js calls this once during collectProgramFacts (before
|
|
271
|
+
* E2 valResult inference), then again after E2 — on the second pass, valTypeOf
|
|
272
|
+
* on user-function calls resolves via `f.valResult`, lifting slots whose value
|
|
273
|
+
* is `const x = userFn(...)` from `undefined` to `NUMBER`/etc.
|
|
274
|
+
* observeSlot's first-wins-then-clash rule means later precise observations
|
|
275
|
+
* upgrade undefined slots without re-poisoning already-monomorphic ones. */
|
|
276
|
+
export function observeProgramSlots(ast) {
|
|
277
|
+
if (!ctx.schema?.register) return
|
|
278
|
+
const slotTypes = ctx.schema.slotTypes
|
|
279
|
+
const observeSlot = (sid, idx, vt) => {
|
|
280
|
+
if (!vt) return
|
|
281
|
+
let arr = slotTypes.get(sid)
|
|
282
|
+
if (!arr) { arr = []; slotTypes.set(sid, arr) }
|
|
283
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
284
|
+
if (arr[idx] === null) return
|
|
285
|
+
if (arr[idx] === undefined) arr[idx] = vt
|
|
286
|
+
else if (arr[idx] !== vt) arr[idx] = null
|
|
287
|
+
}
|
|
288
|
+
const visit = (node) => {
|
|
289
|
+
if (!Array.isArray(node)) return
|
|
290
|
+
const op = node[0]
|
|
291
|
+
if (op === '=>') return
|
|
292
|
+
if (op === '{}') {
|
|
293
|
+
const parsed = staticObjectProps(node.slice(1))
|
|
294
|
+
if (parsed) {
|
|
295
|
+
const sid = ctx.schema.register(parsed.names)
|
|
296
|
+
for (let i = 0; i < parsed.values.length; i++) {
|
|
297
|
+
observeSlot(sid, i, valTypeOf(parsed.values[i]))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (let i = 1; i < node.length; i++) visit(node[i])
|
|
302
|
+
}
|
|
303
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
304
|
+
if (ast) { ctx.func.localValTypesOverlay = null; visit(ast) }
|
|
305
|
+
for (const func of ctx.func.list) {
|
|
306
|
+
if (!func.body || func.raw) continue
|
|
307
|
+
ctx.func.localValTypesOverlay = analyzeBody(func.body).valTypes
|
|
308
|
+
visit(func.body)
|
|
309
|
+
}
|
|
310
|
+
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
311
|
+
ctx.func.localValTypesOverlay = null
|
|
312
|
+
for (const mi of ctx.module.moduleInits) {
|
|
313
|
+
const hit = _moduleInitSlotCache.get(mi)
|
|
314
|
+
if (hit?.gen === _programFactsGen) {
|
|
315
|
+
for (const [sid, idx, vt] of hit.obs) observeSlot(sid, idx, vt)
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
const obs = []
|
|
319
|
+
const record = (sid, idx, vt) => { if (vt) obs.push([sid, idx, vt]); observeSlot(sid, idx, vt) }
|
|
320
|
+
const visitInit = (node) => {
|
|
321
|
+
if (!Array.isArray(node)) return
|
|
322
|
+
const op = node[0]
|
|
323
|
+
if (op === '=>') return
|
|
324
|
+
if (op === '{}') {
|
|
325
|
+
const parsed = staticObjectProps(node.slice(1))
|
|
326
|
+
if (parsed) {
|
|
327
|
+
const sid = ctx.schema.register(parsed.names)
|
|
328
|
+
for (let i = 0; i < parsed.values.length; i++) record(sid, i, valTypeOf(parsed.values[i]))
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (let i = 1; i < node.length; i++) visitInit(node[i])
|
|
332
|
+
}
|
|
333
|
+
visitInit(mi)
|
|
334
|
+
if (mi != null && typeof mi === 'object') _moduleInitSlotCache.set(mi, { gen: _programFactsGen, obs })
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Whole-program slot intCertain observation.
|
|
341
|
+
*
|
|
342
|
+
* A schema slot `(sid, idx)` is `intCertain` iff every write to it across the
|
|
343
|
+
* program is integer-shaped (literal int, bitwise op, intCertain local read,
|
|
344
|
+
* …). Mirrors `analyzeIntCertain`'s `isIntExpr` rules but works at module
|
|
345
|
+
* scope: each body gets a local `intCertain` fixpoint over its own bindings,
|
|
346
|
+
* then schema writes within that body are reduced against the fixpoint.
|
|
347
|
+
*
|
|
348
|
+
* Global poison semantics: any non-int write to a slot — in any body —
|
|
349
|
+
* permanently flips it false. Slots never observed stay `undefined`.
|
|
350
|
+
*
|
|
351
|
+
* Cross-function flow (slot written from a call's return value) is **not**
|
|
352
|
+
* tracked — those writes count as non-int and poison the slot. Conservative:
|
|
353
|
+
* produces only false negatives, never false positives. */
|
|
354
|
+
export function analyzeSchemaSlotIntCertain(ast) {
|
|
355
|
+
if (!ctx.schema?.register) return
|
|
356
|
+
const slotIntCertain = ctx.schema.slotIntCertain
|
|
357
|
+
const poisonSlot = (sid, idx) => {
|
|
358
|
+
let arr = slotIntCertain.get(sid)
|
|
359
|
+
if (!arr) { arr = []; slotIntCertain.set(sid, arr) }
|
|
360
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
361
|
+
arr[idx] = false
|
|
362
|
+
}
|
|
363
|
+
const observeSlot = (sid, idx, isInt) => {
|
|
364
|
+
let arr = slotIntCertain.get(sid)
|
|
365
|
+
if (!arr) { arr = []; slotIntCertain.set(sid, arr) }
|
|
366
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
367
|
+
if (arr[idx] === false) return
|
|
368
|
+
if (!isInt) { arr[idx] = false; return }
|
|
369
|
+
if (arr[idx] === undefined) arr[idx] = true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const bodyIntCertainOf = (body) => {
|
|
373
|
+
if (body != null && typeof body === 'object') {
|
|
374
|
+
const hit = _bodyIntCertainCache.get(body)
|
|
375
|
+
if (hit?.gen === _programFactsGen) return hit.isInt
|
|
376
|
+
}
|
|
377
|
+
const isInt = intExprChecker(body)
|
|
378
|
+
if (body != null && typeof body === 'object')
|
|
379
|
+
_bodyIntCertainCache.set(body, { gen: _programFactsGen, isInt })
|
|
380
|
+
return isInt
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Body walker: for each `{}` literal observe per-slot intCertain; for each
|
|
384
|
+
// `obj.prop = expr` write, poison-or-confirm the slot resolved via the
|
|
385
|
+
// schema attached to `obj` (ValueRep `schemaId` or `ctx.schema.vars`).
|
|
386
|
+
const visit = (node, isInt) => {
|
|
387
|
+
if (!Array.isArray(node)) return
|
|
388
|
+
const op = node[0]
|
|
389
|
+
if (op === '=>') return
|
|
390
|
+
if (op === '{}') {
|
|
391
|
+
const parsed = staticObjectProps(node.slice(1))
|
|
392
|
+
if (parsed) {
|
|
393
|
+
const sid = ctx.schema.register(parsed.names)
|
|
394
|
+
for (let i = 0; i < parsed.values.length; i++) observeSlot(sid, i, isInt(parsed.values[i]))
|
|
395
|
+
}
|
|
396
|
+
} else if (op === '=' && Array.isArray(node[1]) && node[1][0] === '.') {
|
|
397
|
+
const [, obj, prop] = node[1]
|
|
398
|
+
if (typeof obj === 'string') {
|
|
399
|
+
// Same precise-path resolution as ctx.schema.slotVT — no structural
|
|
400
|
+
// fallback (slot index could differ across schemas with the same prop).
|
|
401
|
+
// Poisoned names carry no schema (shape-disagreeing assignments).
|
|
402
|
+
const sid = ctx.schema.poisoned?.has(obj) ? undefined
|
|
403
|
+
: repOf(obj)?.schemaId ?? ctx.schema.vars.get(obj)
|
|
404
|
+
if (sid != null) {
|
|
405
|
+
const idx = ctx.schema.list[sid]?.indexOf(prop)
|
|
406
|
+
if (idx >= 0) observeSlot(sid, idx, isInt(node[2]))
|
|
407
|
+
else if (idx < 0) {/* off-schema write — irrelevant to existing slots */}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (let i = 1; i < node.length; i++) visit(node[i], isInt)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (ast) visit(ast, bodyIntCertainOf(ast))
|
|
415
|
+
for (const func of ctx.func.list) {
|
|
416
|
+
if (!func.body || func.raw) continue
|
|
417
|
+
visit(func.body, bodyIntCertainOf(func.body))
|
|
418
|
+
}
|
|
419
|
+
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
420
|
+
for (const mi of ctx.module.moduleInits) visit(mi, bodyIntCertainOf(mi))
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|