jz 0.8.1 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +299 -44
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +892 -32
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
|
@@ -8,8 +8,10 @@ import { VAL, lookupValType, repOf } from '../reps.js'
|
|
|
8
8
|
import { valTypeOf } from '../kind.js'
|
|
9
9
|
import { extractParams, classifyParam } from '../ast.js'
|
|
10
10
|
import { staticObjectProps } from '../static.js'
|
|
11
|
-
import {
|
|
11
|
+
import { intLevelChecker, typedElemCtor } from '../type.js'
|
|
12
|
+
import { ctorFromElemAux } from '../../layout.js'
|
|
12
13
|
import { analyzeBody } from './analyze.js'
|
|
14
|
+
import { safeReads } from './analyze-scans.js'
|
|
13
15
|
|
|
14
16
|
// Assignment-shaped ops whose first arg, when a `.`/`?.` member node, is a
|
|
15
17
|
// PROPERTY WRITE — feeds `writtenProps` (any prop name ever written through
|
|
@@ -17,12 +19,94 @@ import { analyzeBody } from './analyze.js'
|
|
|
17
19
|
const PROP_WRITE_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=',
|
|
18
20
|
'&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??=', '++', '--'])
|
|
19
21
|
|
|
22
|
+
// Array methods that can change length or relocate the payload (grow copies to a
|
|
23
|
+
// new arena block and forwards the header). sort/reverse/fill/copyWithin mutate
|
|
24
|
+
// elements IN PLACE — base and len stay put — so they are deliberately absent.
|
|
25
|
+
const ARR_RESIZE_METHODS = new Set(['push', 'pop', 'shift', 'unshift', 'splice'])
|
|
26
|
+
|
|
27
|
+
// Per-op arg slots where a bare string is a NAME BINDING or receiver — not a value
|
|
28
|
+
// read. Everything else marks nameEscapes (see below). `true` = skip all slots.
|
|
29
|
+
// Missing a binding-shaped op here only over-marks (a lost fold), never unsound.
|
|
30
|
+
const ESCAPE_SKIP = {
|
|
31
|
+
'.': true, '?.': true, // receiver never escapes via the read itself; slot2 is a prop NAME
|
|
32
|
+
'str': true, // payload
|
|
33
|
+
'[]': new Set([0]), // receiver safe; a bare INDEX name still marks (keys coerce so it's over-marking, but harmless)
|
|
34
|
+
'=>': new Set([0]), // params are bindings; a bare-name BODY is a returned value → marks
|
|
35
|
+
'let': true, 'const': true, 'var': true, // decl heads; initializers are '='-nodes pre-registered below
|
|
36
|
+
'import': true, 'export': true, // module wiring: exported arrays are host/importer-reachable — see explicit mark below
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
export function observeNodeFacts(node, f) {
|
|
21
40
|
if (!Array.isArray(node)) return
|
|
22
41
|
const [op, ...args] = node
|
|
42
|
+
// ---- const-array stability lattice (module/array.js static base/len fold) ----
|
|
43
|
+
// arrResized: names whose array may change length or relocate — any indexed write
|
|
44
|
+
// (an out-of-range write grows), `.length =`, or a resizing method call.
|
|
45
|
+
// nameEscapes: bare names read in a VALUE position — the reference may alias, so
|
|
46
|
+
// mutations through the alias are invisible to per-name facts. Sound direction:
|
|
47
|
+
// over-marking loses a fold; the SAFE (unmarked) positions are only the receiver
|
|
48
|
+
// slots of '[]'/'.'/'?.' and binding slots.
|
|
49
|
+
if (op === '()' && Array.isArray(args[0]) && (args[0][0] === '.' || args[0][0] === '?.') &&
|
|
50
|
+
typeof args[0][1] === 'string' && ARR_RESIZE_METHODS.has(args[0][2]))
|
|
51
|
+
f.arrResized.add(args[0][1])
|
|
52
|
+
if (op === 'let' || op === 'const' || op === 'var') {
|
|
53
|
+
// Pre-register decl '=' children: their slot-0 is a BINDING, not a reassignment,
|
|
54
|
+
// so the '=' marking below must not flag the declared name as escaped.
|
|
55
|
+
for (const d of args) if (Array.isArray(d) && d[0] === '=') (f._declEq ??= new WeakSet()).add(d)
|
|
56
|
+
}
|
|
57
|
+
if (op === 'export') {
|
|
58
|
+
// An exported binding is reachable by importers and the host — writes through
|
|
59
|
+
// that path are outside this walk, so exported names count as escaped.
|
|
60
|
+
for (const d of args) {
|
|
61
|
+
if (typeof d === 'string') f.nameEscapes.add(d)
|
|
62
|
+
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') f.nameEscapes.add(d[1])
|
|
63
|
+
else if (Array.isArray(d) && (d[0] === 'let' || d[0] === 'const' || d[0] === 'var'))
|
|
64
|
+
for (const dd of d.slice(1)) { if (Array.isArray(dd) && dd[0] === '=' && typeof dd[1] === 'string') f.nameEscapes.add(dd[1]) }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
{
|
|
68
|
+
const skip = ESCAPE_SKIP[op]
|
|
69
|
+
if (skip !== true && op != null) {
|
|
70
|
+
const declEq = op === '=' && f._declEq?.has(node)
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
if (typeof args[i] !== 'string') continue
|
|
73
|
+
if (skip instanceof Set && skip.has(i)) continue
|
|
74
|
+
if (declEq && i === 0) continue
|
|
75
|
+
f.nameEscapes.add(args[i])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0])) {
|
|
80
|
+
if (args[0][0] === '[]') {
|
|
81
|
+
let root = args[0][1]
|
|
82
|
+
while (Array.isArray(root) && root[0] === '[]') root = root[1]
|
|
83
|
+
if (typeof root === 'string') f.arrResized.add(root)
|
|
84
|
+
} else if ((args[0][0] === '.' || args[0][0] === '?.') && args[0][2] === 'length' && typeof args[0][1] === 'string')
|
|
85
|
+
f.arrResized.add(args[0][1])
|
|
86
|
+
}
|
|
23
87
|
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) &&
|
|
24
|
-
(args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string')
|
|
88
|
+
(args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string') {
|
|
25
89
|
f.writtenProps.add(args[0][2])
|
|
90
|
+
// Per-receiver literal-key writes: a key OUTSIDE the receiver's schema lands
|
|
91
|
+
// in the dyn-props sidecar (locals get no propMap/autoBox schema merge), so
|
|
92
|
+
// static enumeration (for-in pool/unroll, Object.keys/values/entries schema
|
|
93
|
+
// fold) must deopt to the runtime merge for such receivers or the added key
|
|
94
|
+
// is invisible. Bare-var receivers only — expression receivers already take
|
|
95
|
+
// the runtime path (aliasing keeps dynWriteVars' per-name precision).
|
|
96
|
+
if (typeof args[0][1] === 'string') {
|
|
97
|
+
let s = f.literalWriteKeys.get(args[0][1])
|
|
98
|
+
if (!s) f.literalWriteKeys.set(args[0][1], s = new Set())
|
|
99
|
+
s.add(args[0][2])
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Bracket form of the same: `o['zz'] = v` (isLiteralStr keeps it out of
|
|
103
|
+
// dynWriteVars, so it must be recorded here or it's invisible to every gate).
|
|
104
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) && args[0][0] === '[]' &&
|
|
105
|
+
isLiteralStr(args[0][2]) && typeof args[0][1] === 'string') {
|
|
106
|
+
let s = f.literalWriteKeys.get(args[0][1])
|
|
107
|
+
if (!s) f.literalWriteKeys.set(args[0][1], s = new Set())
|
|
108
|
+
s.add(args[0][2][1])
|
|
109
|
+
}
|
|
26
110
|
// Computed-key WRITES (`o[k]=v`, `o[k]+=v`, `o[k]++`) are the ONLY operations
|
|
27
111
|
// that add ENUMERABLE keys beyond the static schema — computed reads and dot-adds
|
|
28
112
|
// (`o.b=2`) do not enumerate in jz. Tracked separately from `dynVars` (which also
|
|
@@ -66,13 +150,23 @@ export function observeNodeFacts(node, f) {
|
|
|
66
150
|
}
|
|
67
151
|
}
|
|
68
152
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
153
|
+
let _programFactsCache = new WeakMap()
|
|
154
|
+
let _moduleInitSlotCache = new WeakMap()
|
|
155
|
+
let _bodyIntCertainCache = new WeakMap()
|
|
72
156
|
let _programFactsGen = 0
|
|
73
157
|
|
|
74
|
-
/** Drop all cached program-fact walks (called at compile entry).
|
|
75
|
-
|
|
158
|
+
/** Drop all cached program-fact walks (called at compile entry).
|
|
159
|
+
* Natively the gen bump alone is enough (stale entries just go unreachable on a
|
|
160
|
+
* real GC heap). In the self-host kernel these WeakMaps' own backing storage is
|
|
161
|
+
* itself an arena allocation that `_clear` rewinds between compiles in a warm-
|
|
162
|
+
* instance loop — a post-`_clear` alloc can overwrite the WeakMap's internal
|
|
163
|
+
* bytes, so we also swap in fresh WeakMap instances (cheap: O(1), no traversal). */
|
|
164
|
+
export function resetProgramFactsCache() {
|
|
165
|
+
_programFactsGen++
|
|
166
|
+
_programFactsCache = new WeakMap()
|
|
167
|
+
_moduleInitSlotCache = new WeakMap()
|
|
168
|
+
_bodyIntCertainCache = new WeakMap()
|
|
169
|
+
}
|
|
76
170
|
|
|
77
171
|
/** Drop cached walks for specific AST roots (in-place module rewrites). */
|
|
78
172
|
export function invalidateProgramFactsCache(...roots) {
|
|
@@ -89,7 +183,8 @@ function emptyWalkFacts() {
|
|
|
89
183
|
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
90
184
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
91
185
|
propMap: new Map(), valueUsed: new Set(), callSites: [],
|
|
92
|
-
writtenProps: new Set(),
|
|
186
|
+
writtenProps: new Set(), literalWriteKeys: new Map(),
|
|
187
|
+
arrResized: new Set(), nameEscapes: new Set(),
|
|
93
188
|
}
|
|
94
189
|
}
|
|
95
190
|
|
|
@@ -103,6 +198,12 @@ function mergeWalkFacts(into, from) {
|
|
|
103
198
|
if (from.hasRest) into.hasRest = true
|
|
104
199
|
if (from.hasSpread) into.hasSpread = true
|
|
105
200
|
for (const p of from.writtenProps) into.writtenProps.add(p)
|
|
201
|
+
for (const v of from.arrResized) into.arrResized.add(v)
|
|
202
|
+
for (const v of from.nameEscapes) into.nameEscapes.add(v)
|
|
203
|
+
for (const [obj, keys] of from.literalWriteKeys) {
|
|
204
|
+
if (!into.literalWriteKeys.has(obj)) into.literalWriteKeys.set(obj, new Set())
|
|
205
|
+
for (const k of keys) into.literalWriteKeys.get(obj).add(k)
|
|
206
|
+
}
|
|
106
207
|
for (const [obj, props] of from.propMap) {
|
|
107
208
|
if (!into.propMap.has(obj)) into.propMap.set(obj, new Set())
|
|
108
209
|
for (const p of props) into.propMap.get(obj).add(p)
|
|
@@ -179,7 +280,12 @@ function walkFactsRoot(root, full, callerFunc, doSchema, cache = true) {
|
|
|
179
280
|
const isFuncLit = Array.isArray(decl[2]) && decl[2][0] === '=>'
|
|
180
281
|
if (isFuncLit || caller?.name !== name) acc.valueUsed.add(name)
|
|
181
282
|
}
|
|
182
|
-
|
|
283
|
+
// A bare func-ref RHS (`let c = taylor` — the fn-attached-memo idiom)
|
|
284
|
+
// is a VALUE use: resolveClosureWidth must size the uniform ABI to the
|
|
285
|
+
// referenced function's full arity, or its boundary trampoline forwards
|
|
286
|
+
// $__a{k} slots it never declared. Mirrors the '=' handler below.
|
|
287
|
+
if (isFuncRef(decl[2], ctx.func.names)) acc.valueUsed.add(decl[2])
|
|
288
|
+
else walkFacts(decl[2], true, inArrow, caller)
|
|
183
289
|
} else walkFacts(decl, true, inArrow, caller)
|
|
184
290
|
}
|
|
185
291
|
return
|
|
@@ -216,6 +322,30 @@ export function collectProgramFacts(ast) {
|
|
|
216
322
|
if (func.body && !func.raw) mergeWalkFacts(f, walkFactsRoot(func.body, true, func, doSchema, true))
|
|
217
323
|
}
|
|
218
324
|
const { propMap, valueUsed, callSites } = f
|
|
325
|
+
// Bundled sub-module inits live OUTSIDE `ast` (ctx.module.moduleInits — the
|
|
326
|
+
// main walk never sees them) and prepare's recordModuleInitFacts collects a
|
|
327
|
+
// REDUCED set with no call sites. But init code is a first-class CALLER:
|
|
328
|
+
// const tables of arrows (watr's FOLD/FOLD2) call helpers with args visible
|
|
329
|
+
// only here, and without these sites the param lattice settles callees
|
|
330
|
+
// blind — _i64Arith.r never proved BIGINT, _i64Hex16's radix-toString
|
|
331
|
+
// misformatted raw i64 bits (the speed-tier lab throw), and
|
|
332
|
+
// filterLiveCallSites culled both as dead code. Collect ONLY '()' sites
|
|
333
|
+
// (callerFunc = null — module scope; narrow's callerCtx already carries a
|
|
334
|
+
// null entry and filterLiveCallSites keeps null-caller sites and marks
|
|
335
|
+
// their callees live). Everything else stays on the reduced initFacts
|
|
336
|
+
// path: a full walkFactsRoot here would re-register schemas and promote
|
|
337
|
+
// init-stored func REFS into valueUsed — a program-wide dispatch behavior
|
|
338
|
+
// change this census repair must not smuggle in.
|
|
339
|
+
const initCallSites = (node) => {
|
|
340
|
+
if (!Array.isArray(node)) return
|
|
341
|
+
if (node[0] === '()' && isFuncRef(node[1], ctx.func.names)) {
|
|
342
|
+
const a = node[2]
|
|
343
|
+
const argList = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
|
|
344
|
+
f.callSites.push({ callee: node[1], argList, callerFunc: null, node })
|
|
345
|
+
}
|
|
346
|
+
for (let i = 1; i < node.length; i++) initCallSites(node[i])
|
|
347
|
+
}
|
|
348
|
+
if (ctx.module.moduleInits) for (const init of ctx.module.moduleInits) initCallSites(init)
|
|
219
349
|
const initFacts = ctx.module.initFacts
|
|
220
350
|
if (initFacts) {
|
|
221
351
|
if (initFacts.anyDyn) {
|
|
@@ -223,6 +353,12 @@ export function collectProgramFacts(ast) {
|
|
|
223
353
|
for (const v of initFacts.dynVars) f.dynVars.add(v)
|
|
224
354
|
}
|
|
225
355
|
if (initFacts.writtenProps) for (const p of initFacts.writtenProps) f.writtenProps.add(p)
|
|
356
|
+
if (initFacts.arrResized) for (const v of initFacts.arrResized) f.arrResized.add(v)
|
|
357
|
+
if (initFacts.nameEscapes) for (const v of initFacts.nameEscapes) f.nameEscapes.add(v)
|
|
358
|
+
if (initFacts.literalWriteKeys) for (const [obj, keys] of initFacts.literalWriteKeys) {
|
|
359
|
+
if (!f.literalWriteKeys.has(obj)) f.literalWriteKeys.set(obj, new Set())
|
|
360
|
+
for (const k of keys) f.literalWriteKeys.get(obj).add(k)
|
|
361
|
+
}
|
|
226
362
|
if (doArity) {
|
|
227
363
|
if (initFacts.maxDef > f.maxDef) f.maxDef = initFacts.maxDef
|
|
228
364
|
if (initFacts.maxCall > f.maxCall) f.maxCall = initFacts.maxCall
|
|
@@ -256,6 +392,8 @@ export function collectProgramFacts(ast) {
|
|
|
256
392
|
dynVars: f.dynVars, dynWriteVars: f.dynWriteVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
257
393
|
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
258
394
|
paramReps, hasSchemaLiterals: f.hasSchemaLiterals, writtenProps: f.writtenProps,
|
|
395
|
+
literalWriteKeys: f.literalWriteKeys,
|
|
396
|
+
arrResized: f.arrResized, nameEscapes: f.nameEscapes,
|
|
259
397
|
}
|
|
260
398
|
}
|
|
261
399
|
|
|
@@ -273,9 +411,10 @@ export const refreshProgramFacts = (ast, _prev) => collectProgramFacts(ast)
|
|
|
273
411
|
* is `const x = userFn(...)` from `undefined` to `NUMBER`/etc.
|
|
274
412
|
* observeSlot's first-wins-then-clash rule means later precise observations
|
|
275
413
|
* upgrade undefined slots without re-poisoning already-monomorphic ones. */
|
|
276
|
-
export function observeProgramSlots(ast) {
|
|
414
|
+
export function observeProgramSlots(ast, opts) {
|
|
277
415
|
if (!ctx.schema?.register) return
|
|
278
416
|
const slotTypes = ctx.schema.slotTypes
|
|
417
|
+
const slotCtors = ctx.schema.slotTypedCtors
|
|
279
418
|
const observeSlot = (sid, idx, vt) => {
|
|
280
419
|
if (!vt) return
|
|
281
420
|
let arr = slotTypes.get(sid)
|
|
@@ -285,6 +424,84 @@ export function observeProgramSlots(ast) {
|
|
|
285
424
|
if (arr[idx] === undefined) arr[idx] = vt
|
|
286
425
|
else if (arr[idx] !== vt) arr[idx] = null
|
|
287
426
|
}
|
|
427
|
+
// Hard kind-poison (observeSlot's `!vt` arm is a SKIP, not a poison): a write
|
|
428
|
+
// whose kind can't be independently proven forces the slot polymorphic.
|
|
429
|
+
const poisonSlot = (sid, idx) => {
|
|
430
|
+
let arr = slotTypes.get(sid)
|
|
431
|
+
if (!arr) { arr = []; slotTypes.set(sid, arr) }
|
|
432
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
433
|
+
arr[idx] = null
|
|
434
|
+
}
|
|
435
|
+
const poisonCtor = (sid, idx) => {
|
|
436
|
+
let arr = slotCtors.get(sid)
|
|
437
|
+
if (!arr) { arr = []; slotCtors.set(sid, arr) }
|
|
438
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
439
|
+
arr[idx] = null
|
|
440
|
+
}
|
|
441
|
+
// Strict write-kind resolver: valTypeOf EXCEPT (a) `.prop` reads answer null —
|
|
442
|
+
// consulting the live slotTypes state mid-census would make observations
|
|
443
|
+
// order-dependent (a source slot poisoned LATER would leave a stale kind
|
|
444
|
+
// standing), and (b) `+`/`+=` never guess — VT['+']'s NUMBER-for-unknowns
|
|
445
|
+
// optimism is fine for expression typing (the emitter still dispatches at
|
|
446
|
+
// runtime) but would durably misclassify a slot a string flows into.
|
|
447
|
+
// Non-plus arithmetic stays trustworthy through plain valTypeOf: ToNumber
|
|
448
|
+
// semantics make `* - / % << >> & | ^` NUMBER whatever the operands.
|
|
449
|
+
const writeVT = (n) => {
|
|
450
|
+
if (Array.isArray(n)) {
|
|
451
|
+
const op = n[0]
|
|
452
|
+
if (op === '.' || op === '?.') return null
|
|
453
|
+
if (op === '+' || op === '+=') {
|
|
454
|
+
const ta = writeVT(n[1]), tb = writeVT(n[2])
|
|
455
|
+
if (ta === VAL.STRING || tb === VAL.STRING) return VAL.STRING
|
|
456
|
+
if (ta == null || tb == null) return null
|
|
457
|
+
if (ta === VAL.BIGINT || tb === VAL.BIGINT) return VAL.BIGINT
|
|
458
|
+
return VAL.NUMBER
|
|
459
|
+
}
|
|
460
|
+
if (op === '?:') { const a = writeVT(n[2]), b = writeVT(n[3]); return a === b ? a : null }
|
|
461
|
+
if (op === '&&' || op === '||' || op === '??') { const a = writeVT(n[1]), b = writeVT(n[2]); return a === b ? a : null }
|
|
462
|
+
}
|
|
463
|
+
return valTypeOf(n)
|
|
464
|
+
}
|
|
465
|
+
// Poison every hazarded slot's kind AND elem-ctor up front (unresolvable
|
|
466
|
+
// receivers, computed-key writes, extern constructors — see
|
|
467
|
+
// collectSlotWriteHazards). Sticky: observeSlot never upgrades null.
|
|
468
|
+
// Kind-safe sids (JSON shaped/const parsers) OBSERVE their sample kinds
|
|
469
|
+
// instead — clash with a same-sid literal still nulls, exactly right; their
|
|
470
|
+
// elem-ctors poison regardless (JSON never carries typed arrays).
|
|
471
|
+
// opts.fresh (plan's post-narrowing refine): REBUILD from scratch — the late
|
|
472
|
+
// hazard recompute resolves receivers the early pass poisoned wholesale
|
|
473
|
+
// (fftplan's `re[j] = tr` on a then-unnarrowed param poisoned the world).
|
|
474
|
+
// Sound to rebuild: every kind consumer left reads at emit, after this.
|
|
475
|
+
if (opts?.fresh) { slotTypes.clear(); slotCtors.clear() }
|
|
476
|
+
const hazards = collectSlotWriteHazards(ast, opts?.fresh ? { paramReps: opts.paramReps } : undefined)
|
|
477
|
+
applySlotWriteHazards(hazards,
|
|
478
|
+
(sid, idx) => { poisonSlot(sid, idx); poisonCtor(sid, idx) },
|
|
479
|
+
{ observe: (sid, idx, vt) => { vt ? observeSlot(sid, idx, vt) : poisonSlot(sid, idx); poisonCtor(sid, idx) } })
|
|
480
|
+
// Elem-ctor sibling of observeSlot — same first-wins-then-clash lattice. A
|
|
481
|
+
// slot whose every observed value is one typed-array kind keeps that kind for
|
|
482
|
+
// `plan.tw[i]`-style reads (consumption additionally gates on the prop never
|
|
483
|
+
// being written program-wide — see schema.slotTypedCtorAt).
|
|
484
|
+
const observeCtor = (sid, idx, ctor) => {
|
|
485
|
+
if (!ctor) return
|
|
486
|
+
let arr = slotCtors.get(sid)
|
|
487
|
+
if (!arr) { arr = []; slotCtors.set(sid, arr) }
|
|
488
|
+
while (arr.length <= idx) arr.push(undefined)
|
|
489
|
+
if (arr[idx] === null) return
|
|
490
|
+
if (arr[idx] === undefined) arr[idx] = ctor
|
|
491
|
+
else if (arr[idx] !== ctor) arr[idx] = null
|
|
492
|
+
}
|
|
493
|
+
let teOverlay = null
|
|
494
|
+
const ctorOfValue = (expr) => {
|
|
495
|
+
if (typeof expr === 'string')
|
|
496
|
+
return teOverlay?.get(expr) ?? ctx.scope.globalTypedElem?.get(expr) ?? null
|
|
497
|
+
const c = typedElemCtor(expr)
|
|
498
|
+
if (c) return c
|
|
499
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
500
|
+
const f = ctx.func.map?.get(expr[1])
|
|
501
|
+
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) return ctorFromElemAux(f.sig.ptrAux)
|
|
502
|
+
}
|
|
503
|
+
return null
|
|
504
|
+
}
|
|
288
505
|
const visit = (node) => {
|
|
289
506
|
if (!Array.isArray(node)) return
|
|
290
507
|
const op = node[0]
|
|
@@ -295,28 +512,51 @@ export function observeProgramSlots(ast) {
|
|
|
295
512
|
const sid = ctx.schema.register(parsed.names)
|
|
296
513
|
for (let i = 0; i < parsed.values.length; i++) {
|
|
297
514
|
observeSlot(sid, i, valTypeOf(parsed.values[i]))
|
|
515
|
+
observeCtor(sid, i, ctorOfValue(parsed.values[i]))
|
|
298
516
|
}
|
|
299
517
|
}
|
|
518
|
+
} else if (PROP_WRITE_OPS.has(op) && Array.isArray(node[1]) &&
|
|
519
|
+
(node[1][0] === '.' || node[1][0] === '?.') && typeof node[1][1] === 'string' && typeof node[1][2] === 'string') {
|
|
520
|
+
// Resolvable `.prop` writes: observe the written kind when it's
|
|
521
|
+
// independently provable (writeVT), hard-poison otherwise — a slot's
|
|
522
|
+
// censused kind must reflect EVERY write, not just literal init values
|
|
523
|
+
// (`o.x = 'oops'` on a NUMBER-observed slot was a live miscompile).
|
|
524
|
+
// Unresolvable receivers are hazard-poisoned; elem-ctor consumers are
|
|
525
|
+
// already fail-closed on writtenProps, so no ctor action here.
|
|
526
|
+
const sid = repOf(node[1][1])?.schemaId ?? ctx.schema.vars.get(node[1][1])
|
|
527
|
+
const idx = sid != null ? (ctx.schema.list[sid]?.indexOf(node[1][2]) ?? -1) : -1
|
|
528
|
+
if (idx >= 0) {
|
|
529
|
+
const vt = writeVT(effectiveWriteValue(op, node[1], node[2]))
|
|
530
|
+
if (vt) observeSlot(sid, idx, vt)
|
|
531
|
+
else poisonSlot(sid, idx)
|
|
532
|
+
}
|
|
300
533
|
}
|
|
301
534
|
for (let i = 1; i < node.length; i++) visit(node[i])
|
|
302
535
|
}
|
|
303
536
|
const prevOverlay = ctx.func.localValTypesOverlay
|
|
304
|
-
if (ast) { ctx.func.localValTypesOverlay = null; visit(ast) }
|
|
537
|
+
if (ast) { ctx.func.localValTypesOverlay = null; teOverlay = null; visit(ast) }
|
|
305
538
|
for (const func of ctx.func.list) {
|
|
306
539
|
if (!func.body || func.raw) continue
|
|
307
|
-
|
|
540
|
+
const facts = analyzeBody(func.body)
|
|
541
|
+
ctx.func.localValTypesOverlay = facts.valTypes
|
|
542
|
+
teOverlay = facts.typedElems
|
|
308
543
|
visit(func.body)
|
|
309
544
|
}
|
|
545
|
+
teOverlay = null
|
|
310
546
|
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
311
547
|
ctx.func.localValTypesOverlay = null
|
|
312
548
|
for (const mi of ctx.module.moduleInits) {
|
|
313
549
|
const hit = _moduleInitSlotCache.get(mi)
|
|
314
550
|
if (hit?.gen === _programFactsGen) {
|
|
315
|
-
for (const [sid, idx, vt] of hit.obs) observeSlot(sid, idx, vt)
|
|
551
|
+
for (const [sid, idx, vt, ctor] of hit.obs) { observeSlot(sid, idx, vt); observeCtor(sid, idx, ctor) }
|
|
316
552
|
continue
|
|
317
553
|
}
|
|
318
554
|
const obs = []
|
|
319
|
-
const record = (sid, idx, vt) => {
|
|
555
|
+
const record = (sid, idx, vt, ctor) => {
|
|
556
|
+
if (vt || ctor) obs.push([sid, idx, vt, ctor])
|
|
557
|
+
observeSlot(sid, idx, vt)
|
|
558
|
+
observeCtor(sid, idx, ctor)
|
|
559
|
+
}
|
|
320
560
|
const visitInit = (node) => {
|
|
321
561
|
if (!Array.isArray(node)) return
|
|
322
562
|
const op = node[0]
|
|
@@ -325,11 +565,13 @@ export function observeProgramSlots(ast) {
|
|
|
325
565
|
const parsed = staticObjectProps(node.slice(1))
|
|
326
566
|
if (parsed) {
|
|
327
567
|
const sid = ctx.schema.register(parsed.names)
|
|
328
|
-
for (let i = 0; i < parsed.values.length; i++)
|
|
568
|
+
for (let i = 0; i < parsed.values.length; i++)
|
|
569
|
+
record(sid, i, valTypeOf(parsed.values[i]), ctorOfValue(parsed.values[i]))
|
|
329
570
|
}
|
|
330
571
|
}
|
|
331
572
|
for (let i = 1; i < node.length; i++) visitInit(node[i])
|
|
332
573
|
}
|
|
574
|
+
teOverlay = null
|
|
333
575
|
visitInit(mi)
|
|
334
576
|
if (mi != null && typeof mi === 'object') _moduleInitSlotCache.set(mi, { gen: _programFactsGen, obs })
|
|
335
577
|
}
|
|
@@ -337,6 +579,369 @@ export function observeProgramSlots(ast) {
|
|
|
337
579
|
ctx.func.localValTypesOverlay = prevOverlay
|
|
338
580
|
}
|
|
339
581
|
|
|
582
|
+
// ————————————————————————————— slot-write hazards —————————————————————————————
|
|
583
|
+
// The slot censuses (slotIntCertain here, slotTypes/slotTypedCtors in
|
|
584
|
+
// observeProgramSlots) observe `{}` literals and resolvable `obj.prop =`
|
|
585
|
+
// writes — every OTHER way a schema slot's value can change is a HAZARD the
|
|
586
|
+
// censuses must poison, or a consumer bakes a stale fact into codegen
|
|
587
|
+
// (Math.floor elision on a 1.5, raw arithmetic on a string box — live
|
|
588
|
+
// miscompiles, each probed):
|
|
589
|
+
// - `.prop` writes through an UNRESOLVABLE receiver (expression receivers,
|
|
590
|
+
// params no caller agreement pins) → by-prop poison across all schemas.
|
|
591
|
+
// - computed-key writes `o[k] = v` / `delete o[k]` — a resolvable OBJECT
|
|
592
|
+
// receiver poisons its whole sid; HASH/ARRAY/TYPED/MAP/SET/STRING
|
|
593
|
+
// receivers never hit schema slots (dict/element/sidecar homes); an
|
|
594
|
+
// unknown receiver with a provably-NUMERIC key can only hit slots with
|
|
595
|
+
// canonical-integer names; anything else poisons everything.
|
|
596
|
+
// - destructuring assignment into member targets (value shapes unknown).
|
|
597
|
+
// - extern slot writers — the JSON const emitter / shaped parser and
|
|
598
|
+
// spread / Object.assign slot copies (ctx.schema.externSlotSids +
|
|
599
|
+
// Object.assign / spread / JSON.parse discovery here).
|
|
600
|
+
// Fail-closed: under-resolution only loses precision, never soundness.
|
|
601
|
+
const _numericName = (s) => /^(0|[1-9][0-9]*)$/.test(String(s))
|
|
602
|
+
|
|
603
|
+
/** Body-local element-alias sids: single-`=` bindings whose init is a whole
|
|
604
|
+
* element read of an array with a known element schema (local decl facts or a
|
|
605
|
+
* narrowed param's arrayElemSchema — the latter exists only post-narrowing).
|
|
606
|
+
* Shared by the late slot-int census and the hazard scan so both resolve
|
|
607
|
+
* receivers equally (a hazard scan weaker than the census would poison the
|
|
608
|
+
* very slots the census just proved). */
|
|
609
|
+
function collectBodyElemSids(func, paramReps) {
|
|
610
|
+
if (!paramReps || !func?.body || func.raw) return null
|
|
611
|
+
const facts = analyzeBody(func.body)
|
|
612
|
+
const reps = paramReps.get(func.name)
|
|
613
|
+
const paramIdx = new Map((func.sig?.params || []).map((p, k) => [p.name, k]))
|
|
614
|
+
const elemSidOf = (arr) => facts.arrElemSchemas?.get(arr)
|
|
615
|
+
?? (paramIdx.has(arr) ? reps?.get(paramIdx.get(arr))?.arrayElemSchema : null)
|
|
616
|
+
const sids = new Map(), writes = new Map()
|
|
617
|
+
const scan = (n) => {
|
|
618
|
+
if (!Array.isArray(n)) return
|
|
619
|
+
if (n[0] === '=' && typeof n[1] === 'string') {
|
|
620
|
+
writes.set(n[1], (writes.get(n[1]) || 0) + 1)
|
|
621
|
+
const rhs = n[2]
|
|
622
|
+
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 && typeof rhs[1] === 'string') {
|
|
623
|
+
const sid = elemSidOf(rhs[1])
|
|
624
|
+
if (sid != null) sids.set(n[1], sid)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
628
|
+
}
|
|
629
|
+
scan(func.body)
|
|
630
|
+
for (const [name, c] of writes) if (c > 1) sids.delete(name)
|
|
631
|
+
return sids.size ? sids : null
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** The value a compound assignment / inc-dec effectively stores — synthesized
|
|
635
|
+
* so census value-analyses (isIntExpr, kind checks) see the real shape:
|
|
636
|
+
* `o.n++` → `['+', o.n, 1]` (self-referential, resolved by the censuses' own
|
|
637
|
+
* optimistic fixpoint), `o.f ||= x` → either arm. */
|
|
638
|
+
export function effectiveWriteValue(op, lhs, rhs) {
|
|
639
|
+
if (op === '=') return rhs
|
|
640
|
+
if (op === '++' || op === '--') return [op === '++' ? '+' : '-', lhs, [null, 1]]
|
|
641
|
+
if (op === '&&=' || op === '||=' || op === '??=') return ['?:', lhs, lhs, rhs]
|
|
642
|
+
return [op.slice(0, -1), lhs, rhs]
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const KEYED_EXEMPT_VALS = new Set([VAL.ARRAY, VAL.TYPED, VAL.HASH, VAL.MAP, VAL.SET, VAL.STRING])
|
|
646
|
+
let _hazardCache = null
|
|
647
|
+
|
|
648
|
+
/** Program-wide slot-write hazard scan → `{ all, sids, props, numeric,
|
|
649
|
+
* kindSafeSids }`, stashed on `ctx.schema.slotWriteHazards` for the census
|
|
650
|
+
* readers' belt checks. Recomputed per program-facts generation, and again
|
|
651
|
+
* post-narrowing (plan's refine step, opts.paramReps) — narrowed param reps
|
|
652
|
+
* resolve receivers the early pass can't (`re[j] = tr` on a TYPED param is an
|
|
653
|
+
* element write, not a world-poison). `kindSafeSids` maps a sid to its
|
|
654
|
+
* guarded-constructor slot KINDS (the JSON shaped/const parsers — any runtime
|
|
655
|
+
* shape divergence falls back to the generic parser's disjoint runtime sids,
|
|
656
|
+
* so the sample's kinds hold for every object carrying the sid): slotTypes
|
|
657
|
+
* OBSERVES those kinds, slotIntCertain still poisons (a JSON number is any
|
|
658
|
+
* double). */
|
|
659
|
+
export function collectSlotWriteHazards(ast, opts) {
|
|
660
|
+
const late = !!opts?.paramReps
|
|
661
|
+
if (_hazardCache && _hazardCache.gen === _programFactsGen && _hazardCache.late === late)
|
|
662
|
+
return (ctx.schema.slotWriteHazards = _hazardCache.hz)
|
|
663
|
+
const hz = { all: false, sids: new Set(), props: new Set(), numeric: false, kindSafeSids: new Map() }
|
|
664
|
+
let curSids = null, curParamVts = null
|
|
665
|
+
const sidOf = (obj) => {
|
|
666
|
+
if (typeof obj !== 'string' || ctx.schema.poisoned?.has(obj)) return null
|
|
667
|
+
return curSids?.get(obj) ?? repOf(obj)?.schemaId ?? ctx.schema.vars.get(obj) ?? null
|
|
668
|
+
}
|
|
669
|
+
const kindOf = (obj) => typeof obj === 'string'
|
|
670
|
+
? (curParamVts?.get(obj) ?? repOf(obj)?.val ?? valTypeOf(obj))
|
|
671
|
+
: valTypeOf(obj)
|
|
672
|
+
const propWrite = (obj, prop) => {
|
|
673
|
+
// Resolvable string receivers are the censuses' own precise territory.
|
|
674
|
+
if (sidOf(obj) == null) hz.props.add(prop)
|
|
675
|
+
}
|
|
676
|
+
const keyedWrite = (obj, key) => {
|
|
677
|
+
if (isLiteralStr(key)) return propWrite(obj, key[1])
|
|
678
|
+
const sid = sidOf(obj)
|
|
679
|
+
if (sid != null) { hz.sids.add(sid); return }
|
|
680
|
+
const vt = kindOf(obj)
|
|
681
|
+
if (vt != null && vt !== VAL.OBJECT && KEYED_EXEMPT_VALS.has(vt)) return
|
|
682
|
+
if (valTypeOf(key) === VAL.NUMBER || (typeof key === 'string' && repOf(key)?.intCertain === true)) hz.numeric = true
|
|
683
|
+
else hz.all = true
|
|
684
|
+
}
|
|
685
|
+
// Member targets buried in a destructuring pattern — written with values the
|
|
686
|
+
// censuses can't see; hazard them like opaque writes.
|
|
687
|
+
const patternTargets = (pat) => {
|
|
688
|
+
if (!Array.isArray(pat)) return
|
|
689
|
+
const op = pat[0]
|
|
690
|
+
if (op === '.' || op === '?.') {
|
|
691
|
+
if (typeof pat[2] === 'string') {
|
|
692
|
+
const sid = sidOf(pat[1])
|
|
693
|
+
if (sid != null) hz.sids.add(sid)
|
|
694
|
+
else hz.props.add(pat[2])
|
|
695
|
+
}
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
if (op === '[]') return keyedWrite(pat[1], pat[2])
|
|
699
|
+
for (let i = 1; i < pat.length; i++) patternTargets(pat[i])
|
|
700
|
+
}
|
|
701
|
+
const visit = (node) => {
|
|
702
|
+
if (!Array.isArray(node)) return
|
|
703
|
+
const op = node[0]
|
|
704
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(node[1])) {
|
|
705
|
+
const lhs = node[1]
|
|
706
|
+
if ((lhs[0] === '.' || lhs[0] === '?.') && typeof lhs[2] === 'string') propWrite(lhs[1], lhs[2])
|
|
707
|
+
else if (lhs[0] === '[]') keyedWrite(lhs[1], lhs[2])
|
|
708
|
+
else if (op === '=' && (lhs[0] === '{}' || lhs[0] === '[')) patternTargets(lhs)
|
|
709
|
+
} else if (op === 'delete') {
|
|
710
|
+
// prepare only lets computed-key deletes through (['delete', obj, key]);
|
|
711
|
+
// __dyn_del's schema arm writes UNDEF into a matching slot.
|
|
712
|
+
keyedWrite(node[1], node[2])
|
|
713
|
+
} else if (op === '{}') {
|
|
714
|
+
// Spread literal: the emitter slot-copies source schemas into the merged
|
|
715
|
+
// sid — writes outside the census's view. Resolve the merged name-set the
|
|
716
|
+
// same way (explicit `: names` + spread source schemas); an unresolvable
|
|
717
|
+
// source builds a HASH / __obj_clone result instead (no censused sid).
|
|
718
|
+
// The `{}` emitter's own extern belt covers any resolution divergence.
|
|
719
|
+
const entries = node.length === 2 && Array.isArray(node[1]) && node[1][0] === ','
|
|
720
|
+
? node[1].slice(1) : node.slice(1)
|
|
721
|
+
if (entries.some(p => Array.isArray(p) && p[0] === '...')) {
|
|
722
|
+
const names = []
|
|
723
|
+
let known = true
|
|
724
|
+
for (const p of entries) {
|
|
725
|
+
if (!Array.isArray(p)) continue
|
|
726
|
+
if (p[0] === '...') {
|
|
727
|
+
const sid = sidOf(p[1])
|
|
728
|
+
const src = sid != null ? ctx.schema.list[sid] : null
|
|
729
|
+
if (src) { for (const n of src) if (!names.includes(n)) names.push(n) }
|
|
730
|
+
else known = false
|
|
731
|
+
} else if (p[0] === ':' && (typeof p[1] === 'string' || typeof p[1] === 'number')) {
|
|
732
|
+
if (!names.includes(String(p[1]))) names.push(String(p[1]))
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (known && names.length) hz.sids.add(ctx.schema.register(names))
|
|
736
|
+
}
|
|
737
|
+
} else if (op === '()' && node[1] === 'Object.assign') {
|
|
738
|
+
const target = node[2]
|
|
739
|
+
const sid = sidOf(target)
|
|
740
|
+
if (sid != null) hz.sids.add(sid)
|
|
741
|
+
else {
|
|
742
|
+
const vt = kindOf(target)
|
|
743
|
+
if (vt == null || vt === VAL.OBJECT) hz.all = true
|
|
744
|
+
}
|
|
745
|
+
} else if (op === '()' && (node[1] === 'JSON.parse' ||
|
|
746
|
+
(Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'JSON' && node[1][2] === 'parse'))) {
|
|
747
|
+
// Plan-time mirror of the JSON.parse dispatch (module/json.js hook): every
|
|
748
|
+
// key-set the const emitter / shaped parser will register gets its sid
|
|
749
|
+
// KIND-SAFE-marked here with the sample's slot kinds, before any census
|
|
750
|
+
// consumer reads it (a null kind entry poisons that slot's kind too).
|
|
751
|
+
const keysets = ctx.schema.jsonParseKeysets?.(node[2])
|
|
752
|
+
if (keysets) for (const { keys, kinds } of keysets)
|
|
753
|
+
hz.kindSafeSids.set(ctx.schema.register(keys), kinds)
|
|
754
|
+
}
|
|
755
|
+
for (let i = 1; i < node.length; i++) visit(node[i])
|
|
756
|
+
}
|
|
757
|
+
// Per-body valTypes overlays (mirrors observeProgramSlots): receiver/key
|
|
758
|
+
// resolution must see local kinds — `ps[i] = {…}` with ps a local ARRAY and
|
|
759
|
+
// i an int counter is an ELEMENT write, not a slot hazard; without the
|
|
760
|
+
// overlay both fall to unknown and the scan poisons the world.
|
|
761
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
762
|
+
if (ast) { ctx.func.localValTypesOverlay = null; curSids = null; visit(ast) }
|
|
763
|
+
for (const func of ctx.func.list) {
|
|
764
|
+
if (!func.body || func.raw) continue
|
|
765
|
+
ctx.func.localValTypesOverlay = analyzeBody(func.body).valTypes
|
|
766
|
+
curSids = late ? collectBodyElemSids(func, opts.paramReps) : null
|
|
767
|
+
// Late mode: narrowed param reps type this body's params (the early pass
|
|
768
|
+
// can't — `re[j] = tr` on a TYPED param must classify as an element write).
|
|
769
|
+
if (late) {
|
|
770
|
+
const reps = opts.paramReps.get(func.name)
|
|
771
|
+
curParamVts = reps
|
|
772
|
+
? new Map((func.sig?.params || []).map((p, k) => [p.name, reps.get(k)?.val]).filter(([, v]) => v != null))
|
|
773
|
+
: null
|
|
774
|
+
}
|
|
775
|
+
visit(func.body)
|
|
776
|
+
curSids = curParamVts = null
|
|
777
|
+
}
|
|
778
|
+
ctx.func.localValTypesOverlay = null
|
|
779
|
+
if (ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) visit(mi)
|
|
780
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
781
|
+
_hazardCache = { gen: _programFactsGen, late, hz }
|
|
782
|
+
return (ctx.schema.slotWriteHazards = hz)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Apply hazards (+ the extern-sid belt set) to a census map: `poison(sid, idx)`
|
|
786
|
+
* for every hazarded slot. Idempotent; each census calls it at (re)build entry.
|
|
787
|
+
* `opts.kindSafe` (slotTypes only): kind-safe sids' sample kinds are OBSERVED
|
|
788
|
+
* via the callback instead of poisoned — `observe(sid, idx, vtOrNull)`; the
|
|
789
|
+
* int census omits it, so kind-safe sids fully poison there (JSON numbers are
|
|
790
|
+
* arbitrary doubles). */
|
|
791
|
+
export function applySlotWriteHazards(hz, poison, opts) {
|
|
792
|
+
const list = ctx.schema.list || []
|
|
793
|
+
const externs = ctx.schema.externSlotSids
|
|
794
|
+
for (let sid = 0; sid < list.length; sid++) {
|
|
795
|
+
const names = list[sid]
|
|
796
|
+
if (!names) continue
|
|
797
|
+
const kindSafe = opts?.observe ? hz.kindSafeSids?.get(sid) : undefined
|
|
798
|
+
const whole = hz.all || hz.sids.has(sid) || externs?.has(sid) ||
|
|
799
|
+
(hz.kindSafeSids?.has(sid) && kindSafe == null)
|
|
800
|
+
for (let i = 0; i < names.length; i++) {
|
|
801
|
+
if (whole || hz.props.has(String(names[i])) || (hz.numeric && _numericName(names[i]))) { poison(sid, i); continue }
|
|
802
|
+
if (kindSafe) opts.observe(sid, i, kindSafe[i] ?? null)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ————————————————————————— param neverGrown (cross-function) —————————————————————————
|
|
808
|
+
// scanNeverGrown proves never-relocation for fresh-literal LOCALS only; a
|
|
809
|
+
// read-only array PARAM (the word-frequency kernel's `words`) re-resolves its
|
|
810
|
+
// base through `__ptr_offset` on every element read because the param-holding
|
|
811
|
+
// function can't see its callers. The cross-function proof: during any
|
|
812
|
+
// activation of f, the array a param holds can only relocate if some code
|
|
813
|
+
// RUNNING WITHIN that activation grows an array it can reach — so a param is
|
|
814
|
+
// never-grown iff (a) the body only ever purely READS it (safeReads: index /
|
|
815
|
+
// .length, no aliasing, no passing on), and (b) f's body and every transitive
|
|
816
|
+
// callee are ARRAY-GROWTH-FREE: no resize-method call / .length write /
|
|
817
|
+
// non-literal-key indexed write on a possibly-ARRAY receiver, and no call
|
|
818
|
+
// whose callee we can't resolve (computed callees, closure params, unknown
|
|
819
|
+
// methods — any of which could reach user code that grows an alias).
|
|
820
|
+
// Name-keyed caller facts (arrResized/nameEscapes) can't express this — the
|
|
821
|
+
// builder's `words.push` (its own local) would collide with the kernel's
|
|
822
|
+
// read-only param of the same name; the activation-scoped argument doesn't.
|
|
823
|
+
// MEMORY-SAFETY CRITICAL (same class as scanNeverGrown): default-deny —
|
|
824
|
+
// nested arrows are walked as part of the enclosing body (builtin-invoked
|
|
825
|
+
// callbacks run within the activation), unknown callees poison.
|
|
826
|
+
const _NG_SAFE_CALLEES = new Set([
|
|
827
|
+
'JSON.parse', 'JSON.stringify', 'String.fromCharCode', 'performance.now',
|
|
828
|
+
'Object.keys', 'Object.values', 'Object.entries', 'Number.isInteger',
|
|
829
|
+
'Number.isFinite', 'Number.isNaN', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
830
|
+
])
|
|
831
|
+
// Builtin methods that never RELOCATE their receiver nor call user code
|
|
832
|
+
// (beyond function-valued args, which are handled separately): pure reads,
|
|
833
|
+
// fresh-allocating transforms, and the in-place non-relocating mutators.
|
|
834
|
+
const _NG_SAFE_METHODS = new Set([
|
|
835
|
+
'length', 'charCodeAt', 'charAt', 'codePointAt', 'indexOf', 'lastIndexOf',
|
|
836
|
+
'includes', 'slice', 'substring', 'concat', 'join', 'split', 'toString',
|
|
837
|
+
'toLowerCase', 'toUpperCase', 'trim', 'startsWith', 'endsWith', 'repeat',
|
|
838
|
+
'padStart', 'padEnd', 'get', 'set', 'has', 'add', 'delete', 'keys', 'values',
|
|
839
|
+
'entries', 'sort', 'reverse', 'fill', 'copyWithin', 'subarray', 'at',
|
|
840
|
+
'map', 'filter', 'forEach', 'reduce', 'reduceRight', 'some', 'every',
|
|
841
|
+
'find', 'findIndex', 'flat', 'flatMap', 'now',
|
|
842
|
+
])
|
|
843
|
+
/** Compute per-function array-growth-freedom (poison fixpoint over the direct
|
|
844
|
+
* call graph) and stamp `paramReps[f][k].neverGrown` for safe-read params.
|
|
845
|
+
* Consumed at emit via localReps (module/array.js's raw-base fast path). */
|
|
846
|
+
export function analyzeParamNeverGrown(paramReps) {
|
|
847
|
+
if (!ctx.func?.list?.length) return
|
|
848
|
+
const poisoned = new Set(), edges = new Map()
|
|
849
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
850
|
+
for (const func of ctx.func.list) {
|
|
851
|
+
if (!func.body || func.raw) continue
|
|
852
|
+
const facts = analyzeBody(func.body)
|
|
853
|
+
ctx.func.localValTypesOverlay = facts.valTypes
|
|
854
|
+
// Receiver kinds the body facts miss: narrowed param kinds (post-
|
|
855
|
+
// narrowSignatures paramReps) and `{}`-literal decl locals — the
|
|
856
|
+
// dictionary idiom's `const counts = {}` carries no valTypes entry, but
|
|
857
|
+
// ANY object-literal binding is a non-ARRAY receiver.
|
|
858
|
+
const reps = paramReps?.get(func.name)
|
|
859
|
+
const paramIdx = new Map((func.sig?.params || []).map((p, k) => [p.name, k]))
|
|
860
|
+
const objLocals = new Set()
|
|
861
|
+
const collectObjDecls = (n) => {
|
|
862
|
+
if (!Array.isArray(n)) return
|
|
863
|
+
if (n[0] === 'let' || n[0] === 'const' || n[0] === 'var') {
|
|
864
|
+
for (let i = 1; i < n.length; i++) {
|
|
865
|
+
const d = n[i]
|
|
866
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
867
|
+
Array.isArray(d[2]) && d[2][0] === '{}') objLocals.add(d[1])
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (let i = 1; i < n.length; i++) collectObjDecls(n[i])
|
|
871
|
+
}
|
|
872
|
+
collectObjDecls(func.body)
|
|
873
|
+
const out = new Set()
|
|
874
|
+
let dirty = false
|
|
875
|
+
const kindOf = (x) => typeof x === 'string'
|
|
876
|
+
? (objLocals.has(x) ? VAL.OBJECT
|
|
877
|
+
: paramIdx.has(x) ? (reps?.get(paramIdx.get(x))?.val ?? null)
|
|
878
|
+
: repOf(x)?.val ?? valTypeOf(x))
|
|
879
|
+
: valTypeOf(x)
|
|
880
|
+
// Only ARRAY receivers relocate on growth; an unknown kind could be one.
|
|
881
|
+
// (OBJECT/HASH keyed writes land in slots / dict tables — arena-bump
|
|
882
|
+
// allocation never moves an existing array.)
|
|
883
|
+
const maybeArray = (x) => { const v = kindOf(x); return v == null || v === VAL.ARRAY }
|
|
884
|
+
const scan = (n) => {
|
|
885
|
+
if (dirty || !Array.isArray(n)) return
|
|
886
|
+
const op = n[0]
|
|
887
|
+
if (op === '()') {
|
|
888
|
+
const c = n[1]
|
|
889
|
+
// function-valued ARGUMENT to any call: a builtin may invoke it with
|
|
890
|
+
// receiver state we can't see; a bare func ref gives no edge to walk.
|
|
891
|
+
// (Arrow LITERAL args are fine — their bodies are scanned right here.)
|
|
892
|
+
const argRoot = n[2]
|
|
893
|
+
const args = Array.isArray(argRoot) && argRoot[0] === ',' ? argRoot.slice(1) : argRoot === undefined ? [] : [argRoot]
|
|
894
|
+
for (const a of args) if (typeof a === 'string' && ctx.func.map?.has(a)) { dirty = true; return }
|
|
895
|
+
if (typeof c === 'string') {
|
|
896
|
+
if (ctx.func.map?.has(c)) out.add(c)
|
|
897
|
+
else if (!(c.startsWith('math.') || c.startsWith('new.') || _NG_SAFE_CALLEES.has(c))) { dirty = true; return }
|
|
898
|
+
} else if (Array.isArray(c) && (c[0] === '.' || c[0] === '?.') && typeof c[2] === 'string') {
|
|
899
|
+
// A method name WRITTEN anywhere program-wide could be a user
|
|
900
|
+
// closure shadowing the builtin (the sidecar method fork) — its
|
|
901
|
+
// body is invisible here, so it poisons like any unknown call.
|
|
902
|
+
if (ctx.types.writtenProps?.has(c[2])) { dirty = true; return }
|
|
903
|
+
if (ARR_RESIZE_METHODS.has(c[2])) {
|
|
904
|
+
if (maybeArray(c[1])) { dirty = true; return }
|
|
905
|
+
} else if (!_NG_SAFE_METHODS.has(c[2])) { dirty = true; return }
|
|
906
|
+
} else { dirty = true; return } // computed callee — could be any user closure
|
|
907
|
+
} else if (PROP_WRITE_OPS.has(op) && Array.isArray(n[1])) {
|
|
908
|
+
const lhs = n[1]
|
|
909
|
+
if ((lhs[0] === '.' || lhs[0] === '?.') && lhs[2] === 'length') { dirty = true; return }
|
|
910
|
+
if (lhs[0] === '[]' && !isLiteralStr(lhs[2]) && maybeArray(lhs[1])) { dirty = true; return }
|
|
911
|
+
}
|
|
912
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
913
|
+
}
|
|
914
|
+
scan(func.body)
|
|
915
|
+
ctx.func.localValTypesOverlay = null
|
|
916
|
+
if (dirty) poisoned.add(func.name)
|
|
917
|
+
else edges.set(func.name, out)
|
|
918
|
+
}
|
|
919
|
+
ctx.func.localValTypesOverlay = prevOverlay
|
|
920
|
+
// Poison propagation: a caller of a poisoned/unknown callee is poisoned.
|
|
921
|
+
let changed = true
|
|
922
|
+
while (changed) {
|
|
923
|
+
changed = false
|
|
924
|
+
for (const [name, out] of edges) {
|
|
925
|
+
if (poisoned.has(name)) continue
|
|
926
|
+
for (const callee of out)
|
|
927
|
+
if (poisoned.has(callee) || !edges.has(callee)) { poisoned.add(name); changed = true; break }
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const func of ctx.func.list) {
|
|
931
|
+
if (!func.body || func.raw || poisoned.has(func.name) || !edges.has(func.name)) continue
|
|
932
|
+
const params = func.sig?.params || []
|
|
933
|
+
for (let k = 0; k < params.length; k++) {
|
|
934
|
+
if (func.rest && k === params.length - 1) continue
|
|
935
|
+
if (!safeReads(func.body, params[k].name)) continue
|
|
936
|
+
let reps = paramReps.get(func.name)
|
|
937
|
+
if (!reps) paramReps.set(func.name, reps = new Map())
|
|
938
|
+
const r = reps.get(k)
|
|
939
|
+
if (r) r.neverGrown = true
|
|
940
|
+
else reps.set(k, { neverGrown: true })
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
340
945
|
/** Whole-program slot intCertain observation.
|
|
341
946
|
*
|
|
342
947
|
* A schema slot `(sid, idx)` is `intCertain` iff every write to it across the
|
|
@@ -347,34 +952,89 @@ export function observeProgramSlots(ast) {
|
|
|
347
952
|
*
|
|
348
953
|
* Global poison semantics: any non-int write to a slot — in any body —
|
|
349
954
|
* permanently flips it false. Slots never observed stay `undefined`.
|
|
955
|
+
* Writes the census can't SEE (unresolvable receivers, computed keys, extern
|
|
956
|
+
* constructors) are poisoned via collectSlotWriteHazards at every (re)build.
|
|
350
957
|
*
|
|
351
958
|
* Cross-function flow (slot written from a call's return value) is **not**
|
|
352
959
|
* tracked — those writes count as non-int and poison the slot. Conservative:
|
|
353
960
|
* produces only false negatives, never false positives. */
|
|
354
|
-
|
|
961
|
+
/** @param opts.paramReps LATE-mode (plan's post-narrowSignatures block): re-derive
|
|
962
|
+
* the census FRESH with BODY-LOCAL receiver resolution — `const p = ps[i]`
|
|
963
|
+
* binds p's sid from the array's element schema (analyzeBody.arrElemSchemas
|
|
964
|
+
* for locals, paramReps.arrayElemSchema for params), which only exists after
|
|
965
|
+
* narrowing. Sound to REBUILD (not merely widen): every census consumer
|
|
966
|
+
* (toNumF64 / floor elision / intIndexIR) reads at EMIT time, after this. */
|
|
967
|
+
export function analyzeSchemaSlotIntCertain(ast, opts) {
|
|
355
968
|
if (!ctx.schema?.register) return
|
|
356
|
-
|
|
969
|
+
// Working state is the LEVEL map (0 | 1 integral | 2 strict-int32 — see
|
|
970
|
+
// type.js's lattice); the boolean projections slotIntCertain (≥1) and
|
|
971
|
+
// slotI32Certain (≥2) are published for consumers after the rounds settle.
|
|
972
|
+
const slotIntLevels = ctx.schema.slotIntLevels
|
|
973
|
+
if (opts?.paramReps) slotIntLevels.clear()
|
|
974
|
+
const hazards = collectSlotWriteHazards(ast, opts)
|
|
975
|
+
let flipped = false
|
|
357
976
|
const poisonSlot = (sid, idx) => {
|
|
358
|
-
let arr =
|
|
359
|
-
if (!arr) { arr = [];
|
|
977
|
+
let arr = slotIntLevels.get(sid)
|
|
978
|
+
if (!arr) { arr = []; slotIntLevels.set(sid, arr) }
|
|
360
979
|
while (arr.length <= idx) arr.push(undefined)
|
|
361
|
-
arr[idx] =
|
|
980
|
+
if (arr[idx] !== 0) flipped = true
|
|
981
|
+
arr[idx] = 0
|
|
362
982
|
}
|
|
363
|
-
const observeSlot = (sid, idx,
|
|
364
|
-
let arr =
|
|
365
|
-
if (!arr) { arr = [];
|
|
983
|
+
const observeSlot = (sid, idx, level) => {
|
|
984
|
+
let arr = slotIntLevels.get(sid)
|
|
985
|
+
if (!arr) { arr = []; slotIntLevels.set(sid, arr) }
|
|
366
986
|
while (arr.length <= idx) arr.push(undefined)
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
987
|
+
const cur = arr[idx]
|
|
988
|
+
if (cur === 0) return
|
|
989
|
+
const next = cur === undefined ? level : Math.min(cur, level)
|
|
990
|
+
if (next !== cur) {
|
|
991
|
+
arr[idx] = next
|
|
992
|
+
// Any drop below the optimistic top contradicts reads already resolved
|
|
993
|
+
// through it this round — re-derive (mirrors the old true→false flip).
|
|
994
|
+
if (next < (cur ?? 2)) flipped = true
|
|
995
|
+
}
|
|
370
996
|
}
|
|
371
997
|
|
|
372
|
-
|
|
998
|
+
// OPTIMISTIC slot-read resolver — the self-referential immutable-update
|
|
999
|
+
// idiom (`ps[i] = { x: hitX ? p.x : nx, … }`) rebuilds a slot FROM a read
|
|
1000
|
+
// of the same slot, so a single pessimistic pass poisons every such field.
|
|
1001
|
+
// Greatest fixpoint instead: a censused slot read counts int until some
|
|
1002
|
+
// write proves otherwise; each round re-derives every observation and any
|
|
1003
|
+
// true→false flip triggers another round (monotone-down, so it terminates
|
|
1004
|
+
// in ≤ slots+1 rounds and re-runs can only widen poisoning, never unpoison
|
|
1005
|
+
// — the documented re-entrancy contract holds). Same precise-path receiver
|
|
1006
|
+
// resolution as the write side; a censused FALSE answers definitively.
|
|
1007
|
+
// Receiver → sid. `curSids` is the CURRENT body's local element-alias map
|
|
1008
|
+
// (late mode only): `const p = ps[i]` resolves p through ps's element
|
|
1009
|
+
// schema. Precise-path rep/vars resolution is the fallback either way.
|
|
1010
|
+
let curSids = null
|
|
1011
|
+
const sidOfName = (obj) => {
|
|
1012
|
+
if (ctx.schema.poisoned?.has(obj)) return undefined
|
|
1013
|
+
return curSids?.get(obj) ?? repOf(obj)?.schemaId ?? ctx.schema.vars.get(obj)
|
|
1014
|
+
}
|
|
1015
|
+
const slotLevelOf = (obj, prop) => {
|
|
1016
|
+
const sid = sidOfName(obj)
|
|
1017
|
+
if (sid == null) return null
|
|
1018
|
+
const idx = ctx.schema.list[sid]?.indexOf(prop)
|
|
1019
|
+
if (idx == null || idx < 0) return null
|
|
1020
|
+
return slotIntLevels.get(sid)?.[idx] ?? 2 // unobserved = optimistic top
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// LATE mode: body-local element-alias sids (collectBodyElemSids — shared
|
|
1024
|
+
// with the hazard scan so receiver resolution stays in lockstep).
|
|
1025
|
+
const paramReps = opts?.paramReps
|
|
1026
|
+
const bodySidsOf = (func) => collectBodyElemSids(func, paramReps)
|
|
1027
|
+
|
|
1028
|
+
// Round 1 may reuse gen-cached checkers (they close over the LIVE census, so
|
|
1029
|
+
// later poisoning flows through); after any flip the LOCAL binding fixpoints
|
|
1030
|
+
// baked into those checkers may be stale-optimistic, so rebuild fresh.
|
|
1031
|
+
const bodyIntCertainOf = (body, fresh) => {
|
|
1032
|
+
if (fresh) return intLevelChecker(body, slotLevelOf)
|
|
373
1033
|
if (body != null && typeof body === 'object') {
|
|
374
1034
|
const hit = _bodyIntCertainCache.get(body)
|
|
375
1035
|
if (hit?.gen === _programFactsGen) return hit.isInt
|
|
376
1036
|
}
|
|
377
|
-
const isInt =
|
|
1037
|
+
const isInt = intLevelChecker(body, slotLevelOf)
|
|
378
1038
|
if (body != null && typeof body === 'object')
|
|
379
1039
|
_bodyIntCertainCache.set(body, { gen: _programFactsGen, isInt })
|
|
380
1040
|
return isInt
|
|
@@ -393,31 +1053,65 @@ export function analyzeSchemaSlotIntCertain(ast) {
|
|
|
393
1053
|
const sid = ctx.schema.register(parsed.names)
|
|
394
1054
|
for (let i = 0; i < parsed.values.length; i++) observeSlot(sid, i, isInt(parsed.values[i]))
|
|
395
1055
|
}
|
|
396
|
-
} else if (op
|
|
1056
|
+
} else if (PROP_WRITE_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '.') {
|
|
397
1057
|
const [, obj, prop] = node[1]
|
|
398
1058
|
if (typeof obj === 'string') {
|
|
399
1059
|
// Same precise-path resolution as ctx.schema.slotVT — no structural
|
|
400
1060
|
// fallback (slot index could differ across schemas with the same prop).
|
|
401
1061
|
// Poisoned names carry no schema (shape-disagreeing assignments).
|
|
402
|
-
|
|
403
|
-
|
|
1062
|
+
// Late mode adds the current body's element-alias sids (sidOfName).
|
|
1063
|
+
// Compound assigns / inc-dec observe their EFFECTIVE value (`o.n++` →
|
|
1064
|
+
// `o.n + 1` — self-referential, the optimistic fixpoint resolves it).
|
|
1065
|
+
const sid = sidOfName(obj)
|
|
404
1066
|
if (sid != null) {
|
|
405
1067
|
const idx = ctx.schema.list[sid]?.indexOf(prop)
|
|
406
|
-
if (idx >= 0) observeSlot(sid, idx, isInt(node[2]))
|
|
1068
|
+
if (idx >= 0) observeSlot(sid, idx, isInt(effectiveWriteValue(op, node[1], node[2])))
|
|
407
1069
|
else if (idx < 0) {/* off-schema write — irrelevant to existing slots */}
|
|
408
1070
|
}
|
|
1071
|
+
// Unresolvable receivers are hazard-poisoned (collectSlotWriteHazards).
|
|
409
1072
|
}
|
|
410
1073
|
}
|
|
411
1074
|
for (let i = 1; i < node.length; i++) visit(node[i], isInt)
|
|
412
1075
|
}
|
|
413
1076
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1077
|
+
const sweep = (fresh) => {
|
|
1078
|
+
// Hazard poison FIRST: the optimistic slotIntOf resolver must never count a
|
|
1079
|
+
// hazarded slot int mid-fixpoint (it would infect other slots' certainty).
|
|
1080
|
+
applySlotWriteHazards(hazards, poisonSlot)
|
|
1081
|
+
flipped = false
|
|
1082
|
+
curSids = null
|
|
1083
|
+
if (ast) visit(ast, bodyIntCertainOf(ast, fresh))
|
|
1084
|
+
for (const func of ctx.func.list) {
|
|
1085
|
+
if (!func.body || func.raw) continue
|
|
1086
|
+
curSids = bodySidsOf(func)
|
|
1087
|
+
visit(func.body, bodyIntCertainOf(func.body, fresh))
|
|
1088
|
+
curSids = null
|
|
1089
|
+
}
|
|
1090
|
+
if (ctx.module.initFacts?.hasSchemaLiterals && ctx.module.moduleInits) {
|
|
1091
|
+
for (const mi of ctx.module.moduleInits) visit(mi, bodyIntCertainOf(mi, fresh))
|
|
1092
|
+
}
|
|
418
1093
|
}
|
|
419
|
-
|
|
420
|
-
|
|
1094
|
+
sweep(!!paramReps)
|
|
1095
|
+
// Any flip invalidates the LOCAL binding fixpoints baked into round-1
|
|
1096
|
+
// checkers (both the rounds below and any same-gen cache reuse later), so
|
|
1097
|
+
// drop the cache and re-derive until the census is stable.
|
|
1098
|
+
let rounds = 0
|
|
1099
|
+
while (flipped && ++rounds <= 64) {
|
|
1100
|
+
_bodyIntCertainCache = new WeakMap()
|
|
1101
|
+
flipped = false
|
|
1102
|
+
sweep(true)
|
|
1103
|
+
}
|
|
1104
|
+
// Cap exhaustion (never expected — each slot descends ≤2 levels): the state
|
|
1105
|
+
// may still carry stale optimism, so fail closed for the whole program.
|
|
1106
|
+
if (flipped) for (const arr of slotIntLevels.values()) arr.fill(0)
|
|
1107
|
+
// Publish the consumer projections: intCertain = integral (≥1) for the
|
|
1108
|
+
// ToNumber-skip / floor-elision family, i32Certain = strict (=2) for raw
|
|
1109
|
+
// i32 slot loads and i32 local typing.
|
|
1110
|
+
const slotIntCertain = ctx.schema.slotIntCertain, slotI32Certain = ctx.schema.slotI32Certain
|
|
1111
|
+
slotIntCertain.clear(); slotI32Certain.clear()
|
|
1112
|
+
for (const [sid, arr] of slotIntLevels) {
|
|
1113
|
+
slotIntCertain.set(sid, arr.map(l => l === undefined ? undefined : l >= 1))
|
|
1114
|
+
slotI32Certain.set(sid, arr.map(l => l === 2))
|
|
421
1115
|
}
|
|
422
1116
|
}
|
|
423
1117
|
|