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