jz 0.8.0 → 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 -6178
- 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 +318 -43
- 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 +1032 -145
- 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 +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- 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 +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- 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 +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- 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
package/src/abi/string.js
CHANGED
|
@@ -59,9 +59,14 @@ const ssoI64 = (sF64) => ['i64.reinterpret_f64', sF64]
|
|
|
59
59
|
import { isReassigned, isLeaf } from '../ast.js'
|
|
60
60
|
import { LAYOUT, oobNanIR, ssoBitI64Hex } from '../../layout.js'
|
|
61
61
|
|
|
62
|
-
/** Pre-shifted SSO discriminator — layout.js is cycle-free;
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/** Pre-shifted SSO discriminator — layout.js is cycle-free; the thunk exists for
|
|
63
|
+
* load-order laziness ONLY. Deliberately NOT memoized: a module-level memo of a
|
|
64
|
+
* runtime-BUILT string dangles across the self-host kernel's `_clear()` arena
|
|
65
|
+
* rewind (warm compile #2 interpolated the stale pointer's garbage bytes into
|
|
66
|
+
* `(i64.const …)` → watr "Bad int") — the same dangling-cache class as DOLLAR /
|
|
67
|
+
* stdlibParseCache (see scripts/self.js setupSelf). Recomputing is a few ops at
|
|
68
|
+
* emit time; correctness over a micro-memo. */
|
|
69
|
+
const ssoBitI64 = () => ssoBitI64Hex()
|
|
65
70
|
|
|
66
71
|
/** Allocate a fresh i64 local in the current function. Replicated here (not
|
|
67
72
|
* imported from `src/ir.js`) to keep this module loadable during ctx.js
|
|
@@ -127,8 +132,12 @@ function emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds = false) {
|
|
|
127
132
|
* reaching here via type error) gets `len=0` so every bounds check trips and
|
|
128
133
|
* every `charCodeAt` returns 0 — same shape as the legacy `__char_at`. */
|
|
129
134
|
export function emitCharDecompPrologue(dec) {
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
// Receiver expression: a param's local slot (shape-1 classic) or a stable
|
|
136
|
+
// module global (dec.recvGlobal — the parser-state shape: `cur.charCodeAt(idx)`
|
|
137
|
+
// against a global assigned only outside the scanning function). Built fresh
|
|
138
|
+
// (IR nodes must not be structurally shared; also the self-host kernel
|
|
139
|
+
// compiles this file — stick to constructs jz itself supports).
|
|
140
|
+
const ptr = ['i64.reinterpret_f64', dec.recvGlobal ? ['global.get', dec.recvGlobal] : ['local.get', `$${dec.param}`]]
|
|
132
141
|
const ssoTest = ['i64.ne',
|
|
133
142
|
['i64.and', ptr, ['i64.const', ssoBitI64()]],
|
|
134
143
|
['i64.const', 0]]
|
|
@@ -162,6 +171,24 @@ export function emitCharDecompPrologue(dec) {
|
|
|
162
171
|
]
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
// True iff every call expression in `body` is a `.charCodeAt` member call and
|
|
175
|
+
// no suspension point (yield/await) or `new` appears — the stability proof for
|
|
176
|
+
// shape-1b global decomposition: nothing that runs during this function can
|
|
177
|
+
// reassign a module global. Escaped arrows are safe to ignore beyond their
|
|
178
|
+
// visible call nodes: they only run when called, and every call here is
|
|
179
|
+
// charCodeAt (single-threaded).
|
|
180
|
+
export function bodyOnlyCharCodeAtCalls(body) {
|
|
181
|
+
if (!Array.isArray(body)) return true
|
|
182
|
+
const op = body[0]
|
|
183
|
+
if (op === 'yield' || op === 'await' || op === 'new') return false
|
|
184
|
+
if (op === '()' || op === '?.()') {
|
|
185
|
+
const callee = body[1]
|
|
186
|
+
if (!(Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') && callee[2] === 'charCodeAt')) return false
|
|
187
|
+
}
|
|
188
|
+
for (let i = 1; i < body.length; i++) if (!bodyOnlyCharCodeAtCalls(body[i])) return false
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
export const sso = {
|
|
166
193
|
// Wasm slot type a string value occupies under this carrier: the f64
|
|
167
194
|
// NaN-boxed slot (PTR.STRING tag in the high bits, SSO inline data or
|
|
@@ -272,6 +299,45 @@ export const sso = {
|
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
// Shape 1b: receiver is a `global.get` of a module global that is STABLE
|
|
303
|
+
// within this function — the layered-parser hot shape (`cur.charCodeAt(idx)`
|
|
304
|
+
// in subscript's space/peek/next loops, where `cur` is module state written
|
|
305
|
+
// only by parse() entry, never by the scanning function). Same entry
|
|
306
|
+
// decomposition as the param path. Soundness gates:
|
|
307
|
+
// - the global is never assigned in this function's body, AND
|
|
308
|
+
// - the body's only call expressions are `.charCodeAt` member calls (so
|
|
309
|
+
// no user call can transitively reassign the global mid-function), AND
|
|
310
|
+
// - no yield/await (a suspension point lets foreign code write it).
|
|
311
|
+
// Gated on charDecompGlobals — only emitFunc's named-function path drains
|
|
312
|
+
// the prologue (closure bodies have no collectParamInits; an undrained
|
|
313
|
+
// decomposition would read len=0 and misreport every char as OOB).
|
|
314
|
+
if (Array.isArray(sF64) && sF64[0] === 'global.get' && ctx.func.charDecompGlobals) {
|
|
315
|
+
const raw = typeof sF64[1] === 'string' ? sF64[1] : ''
|
|
316
|
+
const name = raw.startsWith('$') ? raw.slice(1) : raw
|
|
317
|
+
if (name && ctx.func.body
|
|
318
|
+
&& !isReassigned(ctx.func.body, name)
|
|
319
|
+
&& bodyOnlyCharCodeAtCalls(ctx.func.body)) {
|
|
320
|
+
if (!ctx.func.charDecomp) ctx.func.charDecomp = new Map()
|
|
321
|
+
const key = `#g:${name}`
|
|
322
|
+
let dec = ctx.func.charDecomp.get(key)
|
|
323
|
+
if (!dec) {
|
|
324
|
+
const base = `${name}$ccbase`
|
|
325
|
+
const len = `${name}$cclen`
|
|
326
|
+
const sso = `${name}$ccsso`
|
|
327
|
+
const loadbase = `${name}$ccldb`
|
|
328
|
+
const ptr64 = `${name}$ccp64`
|
|
329
|
+
ctx.func.locals.set(base, 'i32')
|
|
330
|
+
ctx.func.locals.set(len, 'i32')
|
|
331
|
+
ctx.func.locals.set(sso, 'i32')
|
|
332
|
+
ctx.func.locals.set(loadbase, 'i32')
|
|
333
|
+
ctx.func.locals.set(ptr64, 'i64')
|
|
334
|
+
dec = { base, len, sso, loadbase, ptr64, param: name, recvGlobal: `$${name}`, global: true }
|
|
335
|
+
ctx.func.charDecomp.set(key, dec)
|
|
336
|
+
}
|
|
337
|
+
return emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
275
341
|
// Shape 2: generic inline form for non-param receivers.
|
|
276
342
|
//
|
|
277
343
|
// Both the receiver `sF64` and the index `iI32` are duplicated across the
|
|
@@ -373,15 +439,20 @@ export const sso = {
|
|
|
373
439
|
* dispatcher's string/array runtime guess (emit.js) would hijack it into a
|
|
374
440
|
* bogus array concat. A non-builtin name routes through dynamic property
|
|
375
441
|
* dispatch (load the closure slot, call it) correctly. */
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
442
|
+
// `ext` (default false) opts into the bump-EXTEND fast path — sound only when emit
|
|
443
|
+
// proves `a` is dead-after (a self-accumulation `x = x + …`). Otherwise the _fresh twin
|
|
444
|
+
// alloc+copies, never mutating the live `a` operand. (See __str_concat in module/string.js.)
|
|
445
|
+
cat: (aF64, bF64, ctx, ext = false) => {
|
|
446
|
+
const fn = ext ? '__str_concat' : '__str_concat_fresh'
|
|
447
|
+
ctx.core.includes.add(fn)
|
|
448
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
379
449
|
},
|
|
380
450
|
|
|
381
451
|
/** Concat assuming both sides are already strings (skip ToString). */
|
|
382
|
-
concatRaw: (aF64, bF64, ctx) => {
|
|
383
|
-
|
|
384
|
-
|
|
452
|
+
concatRaw: (aF64, bF64, ctx, ext = false) => {
|
|
453
|
+
const fn = ext ? '__str_concat_raw' : '__str_concat_raw_fresh'
|
|
454
|
+
ctx.core.includes.add(fn)
|
|
455
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
385
456
|
},
|
|
386
457
|
},
|
|
387
458
|
}
|
package/src/ast.js
CHANGED
|
@@ -39,7 +39,7 @@ export const isI32 = (v) => Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX
|
|
|
39
39
|
|
|
40
40
|
/** Statement operators — distinguish block bodies from object literals. */
|
|
41
41
|
export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'for-in', 'while', 'break', 'continue', 'switch',
|
|
42
|
-
'=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
|
|
42
|
+
'=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
|
|
43
43
|
'throw', 'try', 'catch', 'finally', '++', '--', '()'])
|
|
44
44
|
|
|
45
45
|
/** jzify superset: pre-lowered JS shapes before prepare strips them. */
|
|
@@ -74,7 +74,7 @@ export const isLeaf = n => Array.isArray(n) && (n[0] === 'local.get' || n[0] ===
|
|
|
74
74
|
// === Assignment / reassignment ===
|
|
75
75
|
|
|
76
76
|
/** Assignment operators — shared across analyze, plan, emit, abi. */
|
|
77
|
-
export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
77
|
+
export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
78
78
|
|
|
79
79
|
/** Detect whether `name` is written to (=, +=, ++, --, etc.) anywhere within `body`. */
|
|
80
80
|
export function isReassigned(body, name) {
|
|
@@ -397,14 +397,20 @@ export function cloneNode(node) {
|
|
|
397
397
|
// identically and compare equal — an unsound merge (SLP packs `[Inf,-Inf]` as one
|
|
398
398
|
// splat lane; CSE/LICM dedups distinct invariants). Tag each so it round-trips with
|
|
399
399
|
// Object.is semantics.
|
|
400
|
-
|
|
400
|
+
// Recursive keyer, NOT JSON.stringify with a replacer: the kernel's stringify
|
|
401
|
+
// silently dropped the replacer, so in-kernel nodeEqual collapsed the very
|
|
402
|
+
// constants the tagging exists to distinguish (a latent unsound SLP merge,
|
|
403
|
+
// host≠kernel). A plain walk behaves identically on both sides.
|
|
404
|
+
export function stableNodeKey(v) {
|
|
405
|
+
if (Array.isArray(v)) { let s = '['; for (let i = 0; i < v.length; i++) s += (i ? ',' : '') + stableNodeKey(v[i]); return s + ']' }
|
|
401
406
|
if (typeof v === 'bigint') return `${v}n`
|
|
402
407
|
if (typeof v === 'number' && (v !== v || v === Infinity || v === -Infinity || Object.is(v, -0)))
|
|
403
408
|
return v !== v ? '#NaN' : v === Infinity ? '#Inf' : v === -Infinity ? '#-Inf' : '#-0'
|
|
404
|
-
return v
|
|
409
|
+
if (typeof v === 'string') return JSON.stringify(v)
|
|
410
|
+
return String(v) // numbers, booleans, null, undefined — all distinct spellings
|
|
405
411
|
}
|
|
406
412
|
export function nodeEqual(a, b) {
|
|
407
|
-
return
|
|
413
|
+
return stableNodeKey(a) === stableNodeKey(b)
|
|
408
414
|
}
|
|
409
415
|
|
|
410
416
|
/** Property entries of an object-literal AST node (`['{}', …]`). */
|
package/src/autoload.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as mods from '../module/index.js'
|
|
|
5
5
|
|
|
6
6
|
const dict = obj => Object.assign(Object.create(null), obj)
|
|
7
7
|
|
|
8
|
-
export const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', Date: 'date', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string',
|
|
8
|
+
export const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', Date: 'date', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string', Atomics: 'atomics',
|
|
9
9
|
// SIMD intrinsic namespaces (f32x4/i32x4/f64x2/v128) all live in the `simd` module.
|
|
10
10
|
f32x4: 'simd', i32x4: 'simd', f64x2: 'simd', v128: 'simd' }
|
|
11
11
|
|
|
@@ -17,6 +17,7 @@ export const PROP_MODULES = Object.assign(Object.create(null), {
|
|
|
17
17
|
findLast: ['core', 'array'], findLastIndex: ['core', 'array'],
|
|
18
18
|
every: ['core', 'array'], some: ['core', 'array'], flat: ['core', 'array'], flatMap: ['core', 'array'],
|
|
19
19
|
join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'string', 'array'],
|
|
20
|
+
toSorted: ['core', 'array'], toReversed: ['core', 'array'], with: ['core', 'array'],
|
|
20
21
|
charAt: ['core', 'string'], charCodeAt: ['core', 'string'], codePointAt: ['core', 'string'],
|
|
21
22
|
toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], toLocaleLowerCase: ['core', 'string'], trim: ['core', 'string'],
|
|
22
23
|
trimStart: ['core', 'string'], trimEnd: ['core', 'string'],
|
|
@@ -26,6 +27,10 @@ export const PROP_MODULES = Object.assign(Object.create(null), {
|
|
|
26
27
|
matchAll: ['core', 'string'], match: ['core', 'string'],
|
|
27
28
|
substring: ['core', 'string'], substr: ['core', 'string'],
|
|
28
29
|
add: ['core', 'collection'], clear: ['core', 'collection'],
|
|
30
|
+
union: ['core', 'collection'], intersection: ['core', 'collection'],
|
|
31
|
+
difference: ['core', 'collection'], symmetricDifference: ['core', 'collection'],
|
|
32
|
+
isSubsetOf: ['core', 'collection'], isSupersetOf: ['core', 'collection'],
|
|
33
|
+
isDisjointFrom: ['core', 'collection'],
|
|
29
34
|
slice: ['core', 'string', 'array'], concat: ['core', 'string', 'array'],
|
|
30
35
|
indexOf: ['core', 'string', 'array'], lastIndexOf: ['core', 'string', 'array'],
|
|
31
36
|
includes: ['core', 'string', 'array'],
|
|
@@ -48,6 +53,7 @@ export const OP_MODULES = {
|
|
|
48
53
|
'delete': ['core', 'collection', 'string'],
|
|
49
54
|
'//': ['core', 'string', 'regex'],
|
|
50
55
|
'**': ['math'],
|
|
56
|
+
'**=': ['math'], // desugars to `name = name ** val` at emit — needs the same module
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export const TYPED_CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
|
|
@@ -77,6 +83,22 @@ export const CALL_MODULES = dict({
|
|
|
77
83
|
'Object.values': ['core', 'object', 'string'],
|
|
78
84
|
'Object.entries': ['core', 'object', 'string'],
|
|
79
85
|
'Object.hasOwn': ['core', 'object', 'string', 'collection'],
|
|
86
|
+
'Object.groupBy': ['core', 'collection', 'object', 'string', 'array', 'fn'],
|
|
87
|
+
'Map.groupBy': ['core', 'collection', 'array', 'fn'],
|
|
88
|
+
'RegExp.escape': ['core', 'string', 'regex'],
|
|
89
|
+
structuredClone: ['core', 'collection', 'array'],
|
|
90
|
+
'Atomics.load': ['core', 'typedarray', 'atomics'],
|
|
91
|
+
'Atomics.store': ['core', 'typedarray', 'atomics'],
|
|
92
|
+
'Atomics.add': ['core', 'typedarray', 'atomics'],
|
|
93
|
+
'Atomics.sub': ['core', 'typedarray', 'atomics'],
|
|
94
|
+
'Atomics.and': ['core', 'typedarray', 'atomics'],
|
|
95
|
+
'Atomics.or': ['core', 'typedarray', 'atomics'],
|
|
96
|
+
'Atomics.xor': ['core', 'typedarray', 'atomics'],
|
|
97
|
+
'Atomics.exchange': ['core', 'typedarray', 'atomics'],
|
|
98
|
+
'Atomics.compareExchange': ['core', 'typedarray', 'atomics'],
|
|
99
|
+
'Atomics.notify': ['core', 'typedarray', 'atomics'],
|
|
100
|
+
'Atomics.isLockFree': ['core', 'typedarray', 'atomics'],
|
|
101
|
+
'Atomics.wait': ['core', 'typedarray', 'atomics', 'number', 'string'],
|
|
80
102
|
'Object.freeze': ['core', 'object'],
|
|
81
103
|
'Object.assign': ['core', 'object'],
|
|
82
104
|
'Object.create': ['core', 'object'],
|
|
@@ -87,11 +109,15 @@ export const CALL_MODULES = dict({
|
|
|
87
109
|
'Date.now': ['core', 'console'],
|
|
88
110
|
'performance.now': ['core', 'console'],
|
|
89
111
|
'readStdin': ['core', 'console'],
|
|
112
|
+
'fs.read': ['core', 'string', 'fs'],
|
|
113
|
+
'fetch': ['core', 'web'],
|
|
114
|
+
'fs.write': ['core', 'string', 'fs'],
|
|
90
115
|
'String.fromCharCode': ['core', 'string'],
|
|
91
116
|
'String.fromCodePoint': ['core', 'string'],
|
|
92
117
|
'BigInt.asIntN': ['number'],
|
|
93
118
|
'BigInt.asUintN': ['number'],
|
|
94
119
|
...Object.fromEntries(TYPED_CTORS.filter(n => n.endsWith('Array')).map(n => [`${n}.from`, ['core', 'typedarray', 'array']])),
|
|
120
|
+
'Array.of': ['core', 'array'],
|
|
95
121
|
'ArrayBuffer.isView': ['core', 'typedarray'],
|
|
96
122
|
// instanceof Map / Set / TypedArray predicates (synthesized by jzify).
|
|
97
123
|
'__is_map': ['core', 'collection'],
|
|
@@ -118,6 +144,7 @@ export const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval',
|
|
|
118
144
|
|
|
119
145
|
const MOD_DEPS = {
|
|
120
146
|
number: ['core', 'string'],
|
|
147
|
+
atomics: ['core', 'typedarray'],
|
|
121
148
|
string: ['core', 'number'],
|
|
122
149
|
array: ['core'],
|
|
123
150
|
object: ['core'],
|
package/src/bridge.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { ctx, emitter } from './ctx.js'
|
|
12
|
-
import { typed, asF64, asI32, asI64 } from './ir.js'
|
|
12
|
+
import { typed, asF64, asI32, asI64, carrierF64 } from './ir.js'
|
|
13
13
|
|
|
14
14
|
export { emitter } from './ctx.js'
|
|
15
15
|
|
|
@@ -83,8 +83,13 @@ export const dual = (wrap, core, fast) => {
|
|
|
83
83
|
|
|
84
84
|
const cast = { I: asI64, F: asF64, i: asI32 }
|
|
85
85
|
|
|
86
|
+
// 'I' is the boxed-value slot (receivers, collection keys/values) — a boolean
|
|
87
|
+
// crosses it as its TRUE/FALSE atom so identity survives the container round-trip
|
|
88
|
+
// (typeof / String / strict-eq); 'F'/'i' are numeric positions and stay raw.
|
|
86
89
|
const coerce = (sig, nodes) =>
|
|
87
|
-
sig.split('').map((c, i) =>
|
|
90
|
+
sig.split('').map((c, i) => c === 'I'
|
|
91
|
+
? asI64(carrierF64(nodes[i], emit(nodes[i])))
|
|
92
|
+
: cast[c](emit(nodes[i])))
|
|
88
93
|
|
|
89
94
|
const wrap = (fmt, call) => {
|
|
90
95
|
if (fmt === 'i64') return typed(['f64.reinterpret_i64', call], 'f64')
|
|
@@ -149,15 +149,27 @@ export const USE = {
|
|
|
149
149
|
DELETE_MEMBER: 11, // `delete name.member`
|
|
150
150
|
BARE: 12, // any other value position — the conservative catch-all
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
let _bindingUsesCache = new WeakMap()
|
|
153
|
+
// Self-host-only: see resetProgramFactsCache (program-facts.js) — swap in a fresh
|
|
154
|
+
// WeakMap so a warm-instance compile-clear-compile loop never reads a dangling
|
|
155
|
+
// arena pointer out of the old backing storage.
|
|
156
|
+
export function resetBindingUsesCache() { _bindingUsesCache = new WeakMap() }
|
|
153
157
|
const _CMP_OPS = new Set(['==', '!=', '===', '!==', '<', '>', '<=', '>='])
|
|
154
158
|
const _isNullishLit = (e) =>
|
|
155
159
|
e === 'null' || e === 'undefined' ||
|
|
156
160
|
(Array.isArray(e) && e[0] == null && (e[1] === null || e[1] === undefined))
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
// `trackNames` (optional): also report uses of these names even though they're
|
|
163
|
+
// never `let`/`const`-declared IN THIS body — the program-wide dyn-fn-table scan
|
|
164
|
+
// (compile/dyn-closure-tables.js) uses this to see a GLOBAL's uses inside every
|
|
165
|
+
// function body, not just its one module-scope declaration site. Bypasses the
|
|
166
|
+
// cache (a different `trackNames` on the same body would otherwise read a stale
|
|
167
|
+
// entry keyed only by `body`) — fine, this path is a one-shot pre-pass, not hot.
|
|
168
|
+
export function scanBindingUses(body, trackNames) {
|
|
169
|
+
if (!trackNames) {
|
|
170
|
+
const hit = _bindingUsesCache.get(body)
|
|
171
|
+
if (hit) return hit
|
|
172
|
+
}
|
|
161
173
|
|
|
162
174
|
const summary = new Map() // name → { decls, initRhs, uses }
|
|
163
175
|
const slot = (name) => {
|
|
@@ -323,8 +335,11 @@ export function scanBindingUses(body) {
|
|
|
323
335
|
|
|
324
336
|
walk(body, false)
|
|
325
337
|
|
|
326
|
-
for (const [name, s] of summary) if (s.decls === 0) summary.delete(name)
|
|
327
|
-
|
|
338
|
+
for (const [name, s] of summary) if (s.decls === 0 && !trackNames?.has(name)) summary.delete(name)
|
|
339
|
+
// `body` can be null (a module whose every top-level statement got lifted
|
|
340
|
+
// into ctx.func.list, e.g. a single `export const f = () => …` leaves
|
|
341
|
+
// nothing at module scope) — WeakMap keys must be objects.
|
|
342
|
+
if (!trackNames && body != null && typeof body === 'object') _bindingUsesCache.set(body, summary)
|
|
328
343
|
return summary
|
|
329
344
|
}
|
|
330
345
|
|
|
@@ -481,7 +496,7 @@ export function scanSliceViews(body) {
|
|
|
481
496
|
* `a[0].push(x)`, never relocates `a` itself, so `a` stays eligible — see safeReads.)
|
|
482
497
|
*/
|
|
483
498
|
const grownOrEscapes = (op) => ASSIGN_OPS.has(op) || op === '++' || op === '--' || op === 'delete'
|
|
484
|
-
function safeReads(node, name) {
|
|
499
|
+
export function safeReads(node, name) {
|
|
485
500
|
if (typeof node === 'string') return node !== name // bare value use → escape
|
|
486
501
|
if (!Array.isArray(node)) return true
|
|
487
502
|
const op = node[0]
|
package/src/compile/analyze.js
CHANGED
|
@@ -23,7 +23,7 @@ import { ctx, err } from '../ctx.js'
|
|
|
23
23
|
import { VAL, repOf, repOfGlobal, updateRep, updateGlobalRep, lookupValType, lookupNotString } from '../reps.js'
|
|
24
24
|
import { valTypeOf, jsonConstString, shapeOf, shapeOfObjectLiteralAst } from '../kind.js'
|
|
25
25
|
import { intLiteralValue, nonNegIntLiteral, constIntExpr, NO_VALUE, staticPropertyKey, staticValue, staticObjectProps, staticArrayElems, objLiteralSchemaId, exprSchemaId, inlineArraySid } from '../static.js'
|
|
26
|
-
import { typedElemCtor, MIXED_CTORS, isCondExpr, ternaryCtorOfRhs, scanBoundedLoops, inBoundsCharCodeAt, exprType, intCertainMap } from '../type.js'
|
|
26
|
+
import { typedElemCtor, typedStaticLen, MIXED_CTORS, isCondExpr, ternaryCtorOfRhs, scanBoundedLoops, inBoundsCharCodeAt, exprType, intCertainMap } from '../type.js'
|
|
27
27
|
import { TYPED_ELEM_CODE, TYPED_ELEM_VIEW_FLAG, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux, typedElemAux, TYPED_ELEM_NAMES, ctorFromElemAux } from '../../layout.js'
|
|
28
28
|
|
|
29
29
|
// ValueRep field docs + ParamReps lattice helpers — storage lives in src/reps.js.
|
|
@@ -81,7 +81,13 @@ import {
|
|
|
81
81
|
|
|
82
82
|
export { findFreeVars, findMutations, boxedCaptures } from './analyze-scans.js'
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
let _bodyFactsCache = new WeakMap()
|
|
85
|
+
// Self-host-only: see resetProgramFactsCache (program-facts.js) — the WeakMap's own
|
|
86
|
+
// backing storage is an arena allocation that a warm-instance `_clear` rewinds, so
|
|
87
|
+
// a compile-clear-compile loop must swap in a fresh instance, not just rely on
|
|
88
|
+
// per-body identity misses (which natively is enough since AST nodes are fresh
|
|
89
|
+
// each compile and the old WeakMap contents just become GC-unreachable).
|
|
90
|
+
export function resetBodyFactsCache() { _bodyFactsCache = new WeakMap() }
|
|
85
91
|
|
|
86
92
|
// Per-name monotone fact trackers over a pluggable {get,set,delete} store. First
|
|
87
93
|
// observation wins; a conflicting later one poisons the name (and clears the store
|
|
@@ -104,9 +110,9 @@ const makeValTracker = (get, set, del) => {
|
|
|
104
110
|
set(name, vt)
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
|
-
const makeTypedTracker = (get, set, del) => {
|
|
113
|
+
const makeTypedTracker = (get, set, del, getLen, setLen, delLen) => {
|
|
108
114
|
const poison = new Set()
|
|
109
|
-
const invalidate = (name) => { poison.add(name); del(name) }
|
|
115
|
+
const invalidate = (name) => { poison.add(name); del(name); if (delLen) delLen(name) }
|
|
110
116
|
// Resolve a variable-name ternary branch to its known typed-array ctor: a
|
|
111
117
|
// local typed binding (`get`), or a module global promoted typed by plan
|
|
112
118
|
// (`inferModuleLetTypes` populates `globalTypedElem`, copied into
|
|
@@ -125,7 +131,21 @@ const makeTypedTracker = (get, set, del) => {
|
|
|
125
131
|
if (typeof c === 'string' && c.endsWith('.view')) ctx.features.typedView = true
|
|
126
132
|
const prev = get(name)
|
|
127
133
|
if (prev && prev !== c) invalidate(name)
|
|
128
|
-
else
|
|
134
|
+
else {
|
|
135
|
+
set(name, c)
|
|
136
|
+
// Static length rides the ctor's stability (fixed-length arrays): a redef
|
|
137
|
+
// with an unknown or conflicting length drops the entry — typedStaticLen is
|
|
138
|
+
// null for subarray/copy/ternary/computed rhs, so those invalidate for free.
|
|
139
|
+
// Same live-closure style as get/set/del (call-time ctx deref, per the
|
|
140
|
+
// makeValTracker comment above — a captured Map would orphan on the
|
|
141
|
+
// per-function ctx.types reset).
|
|
142
|
+
if (setLen) {
|
|
143
|
+
const len = typedStaticLen(rhs)
|
|
144
|
+
const prevLen = getLen(name)
|
|
145
|
+
if (len == null || (prevLen !== undefined && prevLen !== len)) delLen(name)
|
|
146
|
+
else setLen(name, len)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
129
149
|
}
|
|
130
150
|
const ctor = typedElemCtor(rhs)
|
|
131
151
|
if (ctor) return setOrInvalidate(ctor)
|
|
@@ -148,6 +168,15 @@ const makeTypedTracker = (get, set, del) => {
|
|
|
148
168
|
}
|
|
149
169
|
return
|
|
150
170
|
}
|
|
171
|
+
// Field provenance: `const tw = plan.twRe` where the receiver's schema slot
|
|
172
|
+
// holds one typed-array kind program-wide and the prop is never written
|
|
173
|
+
// (gate inside slotTypedCtorAt) — the binding keeps the concrete kind, so
|
|
174
|
+
// hot-loop reads stay on the typed path (bench: provenance, fftplan).
|
|
175
|
+
if (Array.isArray(rhs) && (rhs[0] === '.' || rhs[0] === '?.') &&
|
|
176
|
+
typeof rhs[1] === 'string' && typeof rhs[2] === 'string' && ctx.schema?.slotTypedCtorAt) {
|
|
177
|
+
const fc = ctx.schema.slotTypedCtorAt(rhs[1], rhs[2])
|
|
178
|
+
if (fc) return setOrInvalidate(fc)
|
|
179
|
+
}
|
|
151
180
|
// Heterogeneous ternary (`n===16 ? new Uint8Array(16) : new Uint16Array(8)`):
|
|
152
181
|
// ctors that don't unify must invalidate so a sibling-scope decl can't lock in
|
|
153
182
|
// the wrong store width.
|
|
@@ -194,6 +223,7 @@ export function analyzeBody(body) {
|
|
|
194
223
|
// resolve as that typed array so `arr[i][j]` / `let o = arr[i]; o[j]` inline.
|
|
195
224
|
const arrElemTypedCtors = new Map()
|
|
196
225
|
const typedElems = new Map()
|
|
226
|
+
const typedLens = new Map()
|
|
197
227
|
const escapes = new Map() // name → bool: local holds allocation, true if it escapes
|
|
198
228
|
|
|
199
229
|
const doSchemas = !!ctx.schema?.register
|
|
@@ -274,7 +304,8 @@ export function analyzeBody(body) {
|
|
|
274
304
|
|
|
275
305
|
// Local-Map slices: bind the Map's get/set/delete as the tracker's three ops.
|
|
276
306
|
const trackVal = makeValTracker(n => valTypes.get(n), (n, vt) => valTypes.set(n, vt), n => valTypes.delete(n))
|
|
277
|
-
const trackTyped = makeTypedTracker(n => typedElems.get(n), (n, c) => typedElems.set(n, c), n => typedElems.delete(n)
|
|
307
|
+
const trackTyped = makeTypedTracker(n => typedElems.get(n), (n, c) => typedElems.set(n, c), n => typedElems.delete(n),
|
|
308
|
+
n => typedLens.get(n), (n, l) => typedLens.set(n, l), n => typedLens.delete(n))
|
|
278
309
|
|
|
279
310
|
// === Per-decl observation (called for each `let`/`const` `name = rhs`) ===
|
|
280
311
|
const processDecl = (name, rhs) => {
|
|
@@ -630,7 +661,7 @@ export function analyzeBody(body) {
|
|
|
630
661
|
// Never-relocated array bindings — reads may skip the realloc-forwarding follow.
|
|
631
662
|
const neverGrown = doSchemas ? scanNeverGrown(body) : new Set()
|
|
632
663
|
|
|
633
|
-
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, arrElemTypedCtors, typedElems, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
|
|
664
|
+
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, arrElemTypedCtors, typedElems, typedLens, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
|
|
634
665
|
_bodyFactsCache.set(body, result)
|
|
635
666
|
return result
|
|
636
667
|
}
|
|
@@ -659,7 +690,21 @@ export function analyzeBody(body) {
|
|
|
659
690
|
*/
|
|
660
691
|
function widenLocalTypes(body, locals) {
|
|
661
692
|
const i32SafeIdx = collectI32SafeIndexVars(body, locals)
|
|
662
|
-
|
|
693
|
+
// Names this scope's own locals map might be reassigned FROM INSIDE A NESTED
|
|
694
|
+
// ARROW — a captured, mutated variable. analyzeBody runs before boxedCaptures
|
|
695
|
+
// populates ctx.func.boxed, so recompute the same "some arrow writes this
|
|
696
|
+
// name" fact locally via findMutations (which already doesn't skip `=>`).
|
|
697
|
+
// Threaded into intCertainMap and both widening walks below: none of them
|
|
698
|
+
// used to look past a `=>` boundary, so `let env = 0; let set = () => { env
|
|
699
|
+
// = 1.5 }` never saw the closure-body float write — `env` stayed provably-int
|
|
700
|
+
// (keepI32 exempted it from Pass A, Pass B never re-checked its only visible
|
|
701
|
+
// def, the never-a-def-of-1.5 one), and the ENCLOSING FUNCTION's own result
|
|
702
|
+
// then narrowed to i32 (narrowI32Results trusts this same `locals` map),
|
|
703
|
+
// silently truncating the return. Gated on nestedNames.size so the common
|
|
704
|
+
// case (no nested reassignment anywhere) keeps the original, cheaper walk.
|
|
705
|
+
const nestedNames = new Set()
|
|
706
|
+
findMutations(body, new Set(locals.keys()), nestedNames)
|
|
707
|
+
const intCounters = intCertainMap(body, nestedNames)
|
|
663
708
|
const f64IdxVars = collectF64StridedIndexVars(body, locals) // counters that trunc anyway — don't keep i32
|
|
664
709
|
const keepI32 = (name) => i32SafeIdx.has(name) || (intCounters.get(name) === true && !f64IdxVars.has(name))
|
|
665
710
|
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!='])
|
|
@@ -672,7 +717,8 @@ function widenLocalTypes(body, locals) {
|
|
|
672
717
|
if (ta === 'i32' && tb === 'f64' && typeof a === 'string' && locals.has(a) && !keepI32(a)) locals.set(a, 'f64')
|
|
673
718
|
if (tb === 'i32' && ta === 'f64' && typeof b === 'string' && locals.has(b) && !keepI32(b)) locals.set(b, 'f64')
|
|
674
719
|
}
|
|
675
|
-
if (op
|
|
720
|
+
if (op === '=>') { if (nestedNames.size) widenPass(args[1]) }
|
|
721
|
+
else for (const a of args) widenPass(a)
|
|
676
722
|
}
|
|
677
723
|
widenPass(body)
|
|
678
724
|
|
|
@@ -682,7 +728,7 @@ function widenLocalTypes(body, locals) {
|
|
|
682
728
|
const recheck = (node) => {
|
|
683
729
|
if (!Array.isArray(node)) return
|
|
684
730
|
const op = node[0]
|
|
685
|
-
if (op === '=>') return
|
|
731
|
+
if (op === '=>') { if (nestedNames.size) recheck(node[2]); return }
|
|
686
732
|
if (op === 'let' || op === 'const') {
|
|
687
733
|
for (let i = 1; i < node.length; i++) {
|
|
688
734
|
const a = node[i]
|
|
@@ -724,18 +770,46 @@ export function invalidateLocalsCache(body) {
|
|
|
724
770
|
if (body && typeof body === 'object') _bodyFactsCache.delete(body)
|
|
725
771
|
}
|
|
726
772
|
|
|
727
|
-
// Can this RHS expression produce null/undefined?
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
773
|
+
// Can this RHS expression produce null/undefined? FAIL-CLOSED: anything not
|
|
774
|
+
// STRUCTURALLY provable non-nullish counts nullable. The flag's only effect
|
|
775
|
+
// is suppressing emit.js's strictSentinel constant fold (the comparison pays
|
|
776
|
+
// a cheap runtime nullish check instead) plus capture propagation — while a
|
|
777
|
+
// wrong non-nullable verdict FOLDS AWAY a real miss guard. The old shape
|
|
778
|
+
// list (nullish literals + ternary arms only) was sound while opaque sources
|
|
779
|
+
// carried no value kind (no kind ⇒ no fold); the Map/element value-kind
|
|
780
|
+
// inference broke that assumption: the self-host kernel's own
|
|
781
|
+
// `autoCache.get(name) !== undefined` cache probe folded to TRUE (the get's
|
|
782
|
+
// rep carried the map's value kind, non-nullable) and every autoDepsOf call
|
|
783
|
+
// returned the miss sentinel unconditionally — the byte-parity root.
|
|
784
|
+
const NEVER_NULLISH_OPS = new Set([
|
|
785
|
+
'str', '//', '{}', '[', '=>', 'new', 'bool',
|
|
786
|
+
'+', '-', '*', '/', '%', '**', '|', '&', '^', '~', '<<', '>>', '>>>',
|
|
787
|
+
'==', '!=', '===', '!==', '<', '>', '<=', '>=', '!', 'u-', 'u+',
|
|
788
|
+
'typeof', 'in', 'instanceof', '++', '--',
|
|
789
|
+
])
|
|
790
|
+
// `nameNullable` resolves a bare-name read; the default reads the CURRENT
|
|
791
|
+
// function's rep (emit-time callers). narrow.js passes its own resolver — at
|
|
792
|
+
// plan time no caller's ctx.func is installed, so it re-derives nullability
|
|
793
|
+
// from the caller body's writes instead. Exported for exactly that consumer.
|
|
794
|
+
export function mayBeNullish(n, nameNullable = (name) => !!repOf(name)?.nullable) {
|
|
795
|
+
if (typeof n === 'number' || typeof n === 'boolean') return false
|
|
796
|
+
// name read: inherit the source binding's settled flag (best-effort — an
|
|
797
|
+
// unsettled rep reads false, matching the old behavior for plain aliases)
|
|
798
|
+
if (typeof n === 'string') return nameNullable(n)
|
|
799
|
+
if (!Array.isArray(n)) return true
|
|
800
|
+
const op = n[0]
|
|
801
|
+
if (op == null) return n[1] == null // [null, v] literal value
|
|
802
|
+
if (op === '?' || op === '?:') return mayBeNullish(n[2], nameNullable) || mayBeNullish(n[3], nameNullable)
|
|
803
|
+
// `a && b` yields a (when falsy — possibly nullish) or b; `a || b` / `a ?? b`
|
|
804
|
+
// yield a only when truthy/non-nullish, so only b's nullability matters.
|
|
805
|
+
if (op === '&&') return mayBeNullish(n[1], nameNullable) || mayBeNullish(n[2], nameNullable)
|
|
806
|
+
if (op === '||' || op === '??') return mayBeNullish(n[2], nameNullable)
|
|
807
|
+
if (op === '=') return mayBeNullish(n[2], nameNullable) // assignment expression yields its rhs
|
|
808
|
+
if (op === ',') return mayBeNullish(n[n.length - 1], nameNullable)
|
|
809
|
+
if (typeof op === 'string' && (NEVER_NULLISH_OPS.has(op) || op.startsWith('new.'))) return false
|
|
810
|
+
// calls (incl. `.get()` misses), member/element reads, optional chains,
|
|
811
|
+
// and anything unrecognized: missable — fail closed.
|
|
812
|
+
return true
|
|
739
813
|
}
|
|
740
814
|
|
|
741
815
|
/**
|
|
@@ -800,6 +874,9 @@ export function analyzeValTypes(body) {
|
|
|
800
874
|
(n) => ctx.types.typedElem?.get(n),
|
|
801
875
|
(n, c) => (ctx.types.typedElem ??= new Map()).set(n, c),
|
|
802
876
|
(n) => ctx.types.typedElem?.delete(n),
|
|
877
|
+
(n) => ctx.types.typedLen?.get(n),
|
|
878
|
+
(n, l) => (ctx.types.typedLen ??= new Map()).set(n, l),
|
|
879
|
+
(n) => ctx.types.typedLen?.delete(n),
|
|
803
880
|
)
|
|
804
881
|
// Total write count for `name` across the whole body, recursing into nested
|
|
805
882
|
// closures so a closure that reassigns the var is also counted. Capped at 2 —
|
|
@@ -955,9 +1032,21 @@ export function analyzeValTypes(body) {
|
|
|
955
1032
|
}
|
|
956
1033
|
}
|
|
957
1034
|
|
|
958
|
-
/** Forward-propagate `intCertain` on local bindings. Fixpoint lives in type.js.
|
|
1035
|
+
/** Forward-propagate `intCertain` on local bindings. Fixpoint lives in type.js.
|
|
1036
|
+
* Threads the settled slot census as the `.prop`-read resolver — without it a
|
|
1037
|
+
* binding built from an int-certain slot (`const x = hitX ? p.x : nx`) stayed
|
|
1038
|
+
* uncertain and every consumer re-paid the ToNumber guard. */
|
|
959
1039
|
export function analyzeIntCertain(body) {
|
|
960
|
-
|
|
1040
|
+
const slotIntOf = ctx.schema?.slotIntCertainAt
|
|
1041
|
+
? (obj, prop) => {
|
|
1042
|
+
const id = ctx.schema.idOf?.(obj)
|
|
1043
|
+
if (id == null) return null
|
|
1044
|
+
const idx = ctx.schema.list[id]?.indexOf(prop)
|
|
1045
|
+
if (idx == null || idx < 0) return null
|
|
1046
|
+
return ctx.schema.slotIntCertainAt(obj, prop)
|
|
1047
|
+
}
|
|
1048
|
+
: undefined
|
|
1049
|
+
for (const [name, intC] of intCertainMap(body, undefined, slotIntOf)) {
|
|
961
1050
|
if (intC) updateRep(name, { intCertain: true })
|
|
962
1051
|
}
|
|
963
1052
|
}
|
|
@@ -1550,6 +1639,16 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1550
1639
|
* (direct calls, no storage at all).
|
|
1551
1640
|
* - otherwise → a mutable f64 module global (`global.get` / `global.set`).
|
|
1552
1641
|
*
|
|
1642
|
+
* Escape-as-CALLEE vs escape-as-TABLE: `f`/`f.PROP` appearing as a call's
|
|
1643
|
+
* callee (`f(...)`, `f?.(...)`, `f.prop(...)`, `f.prop?.(...)`) or as the
|
|
1644
|
+
* exported value (`export`) hands out only the invoked/callable function
|
|
1645
|
+
* VALUE — never a handle onto f's property table — so neither disqualifies.
|
|
1646
|
+
* Every other bare mention of `f` (stored into a data structure, aliased
|
|
1647
|
+
* `let g = f`, compared, an argument to anything but treated as a plain call
|
|
1648
|
+
* position, `f[computed]`/`f?.[computed]`) COULD leak a reference the table
|
|
1649
|
+
* is reachable through, so it still disqualifies (`start strict`: this is
|
|
1650
|
+
* deliberately not trying to prove higher-order callback safety yet).
|
|
1651
|
+
*
|
|
1553
1652
|
* Returns `Map<funcName, { disq, props:Set, valRead:Set,
|
|
1554
1653
|
* writes:Map<prop,[{rhs,atInit}]> }>` — `valRead` is the subset of props read
|
|
1555
1654
|
* as a value (not merely called). `flattenFuncNamespaces` (plan.js) turns it
|
|
@@ -1620,22 +1719,29 @@ export function analyzeFuncNamespaces(ast) {
|
|
|
1620
1719
|
return
|
|
1621
1720
|
}
|
|
1622
1721
|
|
|
1623
|
-
|
|
1722
|
+
// `f(...)` / `f?.(...)` and `f.PROP(...)` / `f.PROP?.(...)` — escape-as-
|
|
1723
|
+
// CALLEE, not escape-as-table. Calling f (or a prop of f) hands the host/
|
|
1724
|
+
// callee only the invoked function VALUE, never f's property table, so
|
|
1725
|
+
// neither shape disqualifies. `prep()` keeps `?.()` a distinct op from
|
|
1726
|
+
// `()` (same flattened-args shape — see prepare/index.js's `'?.()'`
|
|
1727
|
+
// handler) — mirror both here, exactly as `boundSafeCalls` does.
|
|
1728
|
+
if (op === '()' || op === '?.()') {
|
|
1624
1729
|
const m = memberOf(node[1])
|
|
1625
1730
|
if (m) rec(m[1]).props.add(m[2])
|
|
1626
|
-
else if (!isFuncRef(node[1], funcNames)) visit(node[1], false) // bare f(...) ok
|
|
1731
|
+
else if (!isFuncRef(node[1], funcNames)) visit(node[1], false) // bare f(...)/f?.(...) ok
|
|
1627
1732
|
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
1628
1733
|
return
|
|
1629
1734
|
}
|
|
1630
1735
|
|
|
1631
1736
|
// `f.PROP` / `f?.PROP` as a plain value (read) — not the callee of a call
|
|
1632
|
-
// (those are handled by the `()` branch above). A value-read means
|
|
1633
|
-
// property's stored value must stay retrievable; devirt cannot drop it.
|
|
1737
|
+
// (those are handled by the `()`/`?.()` branch above). A value-read means
|
|
1738
|
+
// the property's stored value must stay retrievable; devirt cannot drop it.
|
|
1634
1739
|
const m = memberOf(node)
|
|
1635
1740
|
if (m) { const r = rec(m[1]); r.props.add(m[2]); r.valRead.add(m[2]); return }
|
|
1636
1741
|
|
|
1637
|
-
// Computed `f[k]` — the key set is no longer static
|
|
1638
|
-
|
|
1742
|
+
// Computed `f[k]` / `f?.[k]` — the key set is no longer static: a genuine
|
|
1743
|
+
// table escape (unlike computed CALL args, this reaches arbitrary props).
|
|
1744
|
+
if ((op === '[]' || op === '?.[]') && isFuncRef(node[1], funcNames)) {
|
|
1639
1745
|
rec(node[1]).disq = true
|
|
1640
1746
|
for (let i = 2; i < node.length; i++) visit(node[i], false)
|
|
1641
1747
|
return
|