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.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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
+