jz 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
|
@@ -29,6 +29,7 @@ import { collectProgramFacts, refreshProgramFacts } from '../program-facts.js'
|
|
|
29
29
|
import narrowSignatures, {
|
|
30
30
|
specializeBimorphicTyped, refineDynKeys,
|
|
31
31
|
applyJsstringBoundaryCarrierStandalone, narrowBoolResults,
|
|
32
|
+
strictBoundaryTypeCheck,
|
|
32
33
|
} from '../narrow.js'
|
|
33
34
|
|
|
34
35
|
import { optimizing } from './common.js'
|
|
@@ -94,6 +95,7 @@ export default function plan(ast, profiler) {
|
|
|
94
95
|
sweep('scalarizeTypedArrays', () => scalarizeFunctionTypedArrays(programFacts))
|
|
95
96
|
}
|
|
96
97
|
ctx.types.dynKeyVars = programFacts.dynVars
|
|
98
|
+
ctx.types.dynWriteVars = programFacts.dynWriteVars
|
|
97
99
|
ctx.types.anyDynKey = programFacts.anyDyn
|
|
98
100
|
|
|
99
101
|
t('materializeAutoBoxSchemas', () => materializeAutoBoxSchemas(programFacts))
|
|
@@ -105,6 +107,7 @@ export default function plan(ast, profiler) {
|
|
|
105
107
|
// fact, so `export let f = (a) => a > 2` boxes its boundary atom.
|
|
106
108
|
applyJsstringBoundaryCarrierStandalone(programFacts)
|
|
107
109
|
narrowBoolResults()
|
|
110
|
+
strictBoundaryTypeCheck(programFacts)
|
|
108
111
|
adviseProgram(programFacts)
|
|
109
112
|
return programFacts
|
|
110
113
|
}
|
|
@@ -112,6 +115,7 @@ export default function plan(ast, profiler) {
|
|
|
112
115
|
t('narrowSignatures', () => narrowSignatures(programFacts, ast))
|
|
113
116
|
t('specializeBimorphicTyped', () => specializeBimorphicTyped(programFacts))
|
|
114
117
|
t('refineDynKeys', () => refineDynKeys(programFacts))
|
|
118
|
+
strictBoundaryTypeCheck(programFacts)
|
|
115
119
|
|
|
116
120
|
adviseProgram(programFacts)
|
|
117
121
|
return programFacts
|
|
@@ -359,12 +359,15 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
359
359
|
if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
|
|
360
360
|
// Per-iteration call overhead dwarfs body-size bloat when EVERY site sits
|
|
361
361
|
// inside a caller's loop (game-of-life's rot: ~40 nodes × 2 sites, fired
|
|
362
|
-
// for most of 260k cells/frame
|
|
362
|
+
// for most of 260k cells/frame; cloth's relax: ~160 nodes × 2 sites, fired
|
|
363
|
+
// per link every relaxation pass). V8's pre-Turboshaft wasm tiers never
|
|
363
364
|
// inline cross-function, so an out-of-line leaf in a hot loop is a hard
|
|
364
365
|
// per-cell tax on Node ≤ 22 — and still saves call setup on newer tiers.
|
|
366
|
+
// The in-loop cap is generous because the gate above bounds non-tiny leaves
|
|
367
|
+
// to ≤2 sites, so the spliced duplication is at most ~2× a bounded body.
|
|
365
368
|
const allSitesInLoop = sites.every(site =>
|
|
366
369
|
site.callerFunc?.body && containsNode(site.callerFunc.body, site.node, false))
|
|
367
|
-
if (nodeSize(func.body) > (allSitesInLoop ?
|
|
370
|
+
if (nodeSize(func.body) > (allSitesInLoop ? 200 : 30)) continue
|
|
368
371
|
}
|
|
369
372
|
if (some(func.body, n => n[0] === '()' && n[1] === func.name)) continue
|
|
370
373
|
// Kernels with nested loops (depth ≥ 2) are typically large and the inner
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { ctx } from '../../ctx.js'
|
|
27
27
|
import {
|
|
28
|
-
some, T, stmtList, refsName, ASSIGN_OPS, isReassigned, hasControlTransfer,
|
|
28
|
+
some, T, stmtList, refsName, REFS_IN_EXPR, ASSIGN_OPS, isReassigned, hasControlTransfer,
|
|
29
29
|
} from '../../ast.js'
|
|
30
30
|
import {
|
|
31
31
|
intLiteralValue, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
@@ -90,7 +90,7 @@ const safeScalarArrayUse = (node, name, len, parentOp = null) => {
|
|
|
90
90
|
const op = node[0]
|
|
91
91
|
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
92
92
|
if (isMemberWriteTarget(op, node, name)) return false
|
|
93
|
-
if ((op
|
|
93
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
94
94
|
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
95
95
|
// Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
|
|
96
96
|
// grows the array (sparse-array semantics), which the fixed scalar slot set
|
|
@@ -141,7 +141,7 @@ const safeScalarObjectUse = (node, name, keys) => {
|
|
|
141
141
|
if (!Array.isArray(node)) return true
|
|
142
142
|
const op = node[0]
|
|
143
143
|
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
144
|
-
if ((op
|
|
144
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
145
145
|
if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2])
|
|
146
146
|
if (op === '[]' && node[1] === name) {
|
|
147
147
|
const key = staticPropertyKey(node[2])
|
|
@@ -181,7 +181,7 @@ const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
|
|
|
181
181
|
if (typeof node === 'string') return node !== name
|
|
182
182
|
if (!Array.isArray(node)) return true
|
|
183
183
|
const op = node[0]
|
|
184
|
-
if ((op
|
|
184
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
185
185
|
if (isMemberWriteTarget(op, node, name)) return false
|
|
186
186
|
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
187
187
|
if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
|
|
@@ -200,6 +200,34 @@ const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
|
|
|
200
200
|
for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
|
|
201
201
|
return true
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
// `name`'s reference used as a bare VALUE — anywhere except as the base of `name[i]`,
|
|
205
|
+
// `name.prop`, or `name.method(...)`. That captures a second handle to its backing memory.
|
|
206
|
+
const refsAsValue = (node, name) => {
|
|
207
|
+
if (node === name) return true
|
|
208
|
+
if (!Array.isArray(node)) return false
|
|
209
|
+
if (node[0] === '[]' && node[1] === name) return refsAsValue(node[2], name) // name[i]: vet the index only
|
|
210
|
+
if ((node[0] === '.' || node[0] === '?.') && node[1] === name) return false // name.prop / name.method()
|
|
211
|
+
if (node[0] === '()') return false // a call RESULT, not name itself
|
|
212
|
+
return node.slice(1).some(e => refsAsValue(e, name))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// True when `name`'s backing memory ESCAPES into a persistent alias: bound to another
|
|
216
|
+
// variable (`let b = name`), stored into a field or literal (`o.x = name`, `[name]`), or
|
|
217
|
+
// captured as a `.subarray(...)` view. Mirrored scalarization syncs scalars↔memory only
|
|
218
|
+
// AROUND each unsafe statement, so a write through the captured alias in a LATER statement
|
|
219
|
+
// never reaches `name`'s scalar slots (and vice-versa) — the array must stay memory-backed.
|
|
220
|
+
// A bare `name` passed only as a call ARGUMENT is transient (the callee touches it during
|
|
221
|
+
// the call, already covered by the surrounding sync), so it is NOT a capture.
|
|
222
|
+
const createsTypedArrayAlias = (node, name) => {
|
|
223
|
+
if (!Array.isArray(node)) return false
|
|
224
|
+
if (node[0] === '()' && Array.isArray(node[1]) && node[1][0] === '.'
|
|
225
|
+
&& node[1][1] === name && node[1][2] === 'subarray') return true // zero-copy view
|
|
226
|
+
if (node[0] === '=' && refsAsValue(node[2], name)) return true // let b = name / x = name
|
|
227
|
+
if ((node[0] === '[' || node[0] === '{}') && node.slice(1).some(e => refsAsValue(e, name))) return true // [name] / {k:name}
|
|
228
|
+
for (let i = 1; i < node.length; i++) if (createsTypedArrayAlias(node[i], name)) return true
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
203
231
|
const rewriteScalarTypedArrayUses = (node, arrays) => {
|
|
204
232
|
if (!Array.isArray(node)) return node
|
|
205
233
|
const op = node[0]
|
|
@@ -286,14 +314,16 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
|
|
|
286
314
|
const fixed = fixedScalarTypedArray(decl[2])
|
|
287
315
|
if (fixed == null) continue
|
|
288
316
|
const { len, coerce } = fixed
|
|
289
|
-
let hasSafeUse = false, hasUnsafeUse = false
|
|
317
|
+
let hasSafeUse = false, hasUnsafeUse = false, hasAliasUse = false
|
|
290
318
|
for (let j = 0; j < stmts.length; j++) {
|
|
291
319
|
if (j === i) continue
|
|
292
320
|
if (!refsName(stmts[j], decl[1])) continue
|
|
293
321
|
const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
|
|
294
322
|
hasSafeUse ||= safe
|
|
295
323
|
hasUnsafeUse ||= !safe
|
|
324
|
+
hasAliasUse ||= createsTypedArrayAlias(stmts[j], decl[1])
|
|
296
325
|
}
|
|
326
|
+
if (hasAliasUse) continue // persistent aliasing view (subarray) — keep memory-backed
|
|
297
327
|
if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
|
|
298
328
|
if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
|
|
299
329
|
else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
|
|
@@ -621,6 +651,191 @@ function scalarizeObjectLiterals(node, escapes) {
|
|
|
621
651
|
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
622
652
|
}
|
|
623
653
|
|
|
654
|
+
// === Whole-program constant fold of module-scope aggregate literals ===
|
|
655
|
+
//
|
|
656
|
+
// `var x = [1,2,3]; export const y = x[0]` materializes a data-segment array and
|
|
657
|
+
// indexes it through __arr_idx_known (bounds + grow-forwarding) — all dead weight
|
|
658
|
+
// for a never-grown constant. When EVERY reference to a module-scope const aggregate
|
|
659
|
+
// is a static READ, replace each `x[k]` / `x.length` / `o.key` by its literal value
|
|
660
|
+
// program-wide; the now-unused decl is dropped, so no array is built (no data, no
|
|
661
|
+
// memory, no index helper) and the global is never declared. The per-function
|
|
662
|
+
// scalarizers can't do this: the decl lives at module scope and may be read from
|
|
663
|
+
// several function bodies, so the check must span the whole program.
|
|
664
|
+
|
|
665
|
+
const ASSIGN_OR_UPDATE = (op) => ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--'
|
|
666
|
+
// Module-scope binding ops. `var` survives to compile at module scope (jzify only
|
|
667
|
+
// lowers it to `let` inside functions), so fold it too — the reassignment guard
|
|
668
|
+
// below keeps a re-bound `var` heap-backed.
|
|
669
|
+
const isDeclOp = (op) => op === 'let' || op === 'const' || op === 'var'
|
|
670
|
+
|
|
671
|
+
// Reject `delete x`, `delete x.k`, `delete x[k]` — a deletion mutates the aggregate.
|
|
672
|
+
const isDeleteOf = (node, name) =>
|
|
673
|
+
node[0] === 'delete' && (node[1] === name || (Array.isArray(node[1]) && node[1][1] === name))
|
|
674
|
+
|
|
675
|
+
// A reference is fold-safe only if it READS `name` via a static index/key (or
|
|
676
|
+
// `.length` for arrays). Any write/update/delete, reassignment, second declaration,
|
|
677
|
+
// bare value use (escape), spread, dynamic key, or non-own member (method / proto
|
|
678
|
+
// chain) escapes the literal model and disqualifies the binding.
|
|
679
|
+
const foldSafeArrayUse = (node, name, len) => {
|
|
680
|
+
if (typeof node === 'string') return node !== name
|
|
681
|
+
if (!Array.isArray(node)) return true
|
|
682
|
+
const op = node[0]
|
|
683
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
684
|
+
if (isDeleteOf(node, name)) return false
|
|
685
|
+
if (ASSIGN_OR_UPDATE(op)) {
|
|
686
|
+
const t = node[1]
|
|
687
|
+
if (t === name) return false
|
|
688
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
689
|
+
}
|
|
690
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
691
|
+
if (op === '[]' && node[1] === name) return constIntExpr(node[2]) != null
|
|
692
|
+
if (op === '...' && node[1] === name) return false
|
|
693
|
+
for (let i = 1; i < node.length; i++) if (!foldSafeArrayUse(node[i], name, len)) return false
|
|
694
|
+
return true
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const foldSafeObjectUse = (node, name, keys) => {
|
|
698
|
+
if (typeof node === 'string') return node !== name
|
|
699
|
+
if (!Array.isArray(node)) return true
|
|
700
|
+
const op = node[0]
|
|
701
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
702
|
+
if (isDeleteOf(node, name)) return false
|
|
703
|
+
if (ASSIGN_OR_UPDATE(op)) {
|
|
704
|
+
const t = node[1]
|
|
705
|
+
if (t === name) return false
|
|
706
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
707
|
+
}
|
|
708
|
+
if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2]) // own key only — proto-safe
|
|
709
|
+
if (op === '[]' && node[1] === name) { const k = staticPropertyKey(node[2]); return k != null && keys.has(k) }
|
|
710
|
+
if (op === '...' && node[1] === name) return false
|
|
711
|
+
for (let i = 1; i < node.length; i++) if (!foldSafeObjectUse(node[i], name, keys)) return false
|
|
712
|
+
return true
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const moduleStmtsOf = (seq) =>
|
|
716
|
+
Array.isArray(seq) && seq[0] === ';' ? seq.slice(1)
|
|
717
|
+
: Array.isArray(seq) && isDeclOp(seq[0]) ? [seq]
|
|
718
|
+
: []
|
|
719
|
+
|
|
720
|
+
export function foldStaticConstAggregates(ast) {
|
|
721
|
+
// Span the main module AST and every bundled sub-module init — a const declared in
|
|
722
|
+
// one can be read from another or from a function body (mirrors the constInts fold).
|
|
723
|
+
const seqs = [ast, ...(ctx.module.moduleInits || [])]
|
|
724
|
+
const moduleStmts = seqs.flatMap(moduleStmtsOf)
|
|
725
|
+
const funcs = ctx.func.list.filter(f => f.body && !f.raw)
|
|
726
|
+
// A function parameter named `x` rebinds `x` for the whole body — its `x[…]` reads
|
|
727
|
+
// the param, not the module binding (params live on `f.sig`, separate from `.body`,
|
|
728
|
+
// so the body scan can't see them). Such a function is skipped (scan) / excluded
|
|
729
|
+
// (rewrite) for that name.
|
|
730
|
+
const paramNames = (f) => (f.sig?.params || []).map(p => p.name)
|
|
731
|
+
// Every AST a function can reference an outer binding from: its body PLUS each
|
|
732
|
+
// default-parameter expression (`(v = x[0]) => …`), which prepare extracts to
|
|
733
|
+
// `f.defaults` — separate from the body, so the body scan/rewrite would miss it.
|
|
734
|
+
const funcNodes = (f) => f.defaults ? [f.body, ...Object.values(f.defaults)] : [f.body]
|
|
735
|
+
|
|
736
|
+
// Classify module statements. A binding's value comes from an inline decl
|
|
737
|
+
// (`const x = […]`) OR — for `var`, which jzify lowers to `let x; x = […]` — a
|
|
738
|
+
// lone module-scope assignment after an uninitialized decl. The init statement(s)
|
|
739
|
+
// are excluded from the read-only scan and dropped on fold.
|
|
740
|
+
const inlineInit = new Map() // name -> {value, stmt} | null (poisoned: >1 decl)
|
|
741
|
+
const assigns = new Map() // name -> [assignStmt…]
|
|
742
|
+
const uninitDecl = new Map() // name -> declStmt (`let x` / `var x`, string declarator)
|
|
743
|
+
for (const stmt of moduleStmts) {
|
|
744
|
+
if (!Array.isArray(stmt)) continue
|
|
745
|
+
const op = stmt[0]
|
|
746
|
+
if (isDeclOp(op) && stmt.length === 2 && Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string') {
|
|
747
|
+
const name = stmt[1][1]
|
|
748
|
+
inlineInit.set(name, inlineInit.has(name) ? null : { value: stmt[1][2], stmt })
|
|
749
|
+
} else if (isDeclOp(op) && stmt.length === 2 && typeof stmt[1] === 'string') {
|
|
750
|
+
uninitDecl.set(stmt[1], stmt)
|
|
751
|
+
} else if (op === '=' && typeof stmt[1] === 'string') {
|
|
752
|
+
(assigns.get(stmt[1]) ?? assigns.set(stmt[1], []).get(stmt[1])).push(stmt)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// index of each statement in module-execution order — used to prove a `var`'s
|
|
757
|
+
// assignment dominates its reads.
|
|
758
|
+
const pos = new Map()
|
|
759
|
+
moduleStmts.forEach((s, i) => pos.set(s, i))
|
|
760
|
+
|
|
761
|
+
const arr = new Map(), obj = new Map(), initStmts = new Map()
|
|
762
|
+
const consider = (name, value, init) => {
|
|
763
|
+
if (ctx.func.exports?.[name]) return // exported → escapes to JS
|
|
764
|
+
const elems = scalarArrayElems(value)
|
|
765
|
+
if (elems) { arr.set(name, elems); initStmts.set(name, init); return }
|
|
766
|
+
const props = scalarObjectProps(value)
|
|
767
|
+
if (props) { obj.set(name, props); initStmts.set(name, init) }
|
|
768
|
+
}
|
|
769
|
+
for (const [name, info] of inlineInit) {
|
|
770
|
+
// a decl-initialized binding that is ALSO assigned is reassigned → not constant.
|
|
771
|
+
if (info && !assigns.has(name)) consider(name, info.value, new Set([info.stmt]))
|
|
772
|
+
}
|
|
773
|
+
for (const [name, list] of assigns) {
|
|
774
|
+
// `var` lowering: exactly one assignment, a matching uninit decl, and no
|
|
775
|
+
// competing inline decl. The assignment must dominate every read — conservatively,
|
|
776
|
+
// it precedes all other module references and the name is unused in any function
|
|
777
|
+
// body (a function could run before the assignment). `let`/`const` need no such
|
|
778
|
+
// guard (TDZ forbids use-before-init).
|
|
779
|
+
if (inlineInit.has(name) || list.length !== 1 || !uninitDecl.has(name)) continue
|
|
780
|
+
const assign = list[0], at = pos.get(assign)
|
|
781
|
+
const refsBefore = moduleStmts.some((s, i) => i < at && s !== uninitDecl.get(name) && refsName(s, name, REFS_IN_EXPR))
|
|
782
|
+
const refsInFn = funcs.some(f => !paramNames(f).includes(name) && funcNodes(f).some(n => refsName(n, name, REFS_IN_EXPR)))
|
|
783
|
+
if (refsBefore || refsInFn) continue
|
|
784
|
+
consider(name, assign[2], new Set([assign, uninitDecl.get(name)]))
|
|
785
|
+
}
|
|
786
|
+
if (!arr.size && !obj.size) return false
|
|
787
|
+
|
|
788
|
+
// Every mention outside the binding's init statement(s) — across all module
|
|
789
|
+
// statements and all function bodies — must be a fold-safe static read.
|
|
790
|
+
const checkAll = (name, pred) => {
|
|
791
|
+
const skip = initStmts.get(name)
|
|
792
|
+
return moduleStmts.every(s => skip.has(s) || pred(s))
|
|
793
|
+
&& funcs.every(f => paramNames(f).includes(name) || funcNodes(f).every(pred))
|
|
794
|
+
}
|
|
795
|
+
for (const [name, elems] of [...arr]) if (!checkAll(name, n => foldSafeArrayUse(n, name, elems.length))) arr.delete(name)
|
|
796
|
+
for (const [name, props] of [...obj]) {
|
|
797
|
+
const keys = new Set(props.names)
|
|
798
|
+
if (!checkAll(name, n => foldSafeObjectUse(n, name, keys))) obj.delete(name)
|
|
799
|
+
}
|
|
800
|
+
if (!arr.size && !obj.size) return false
|
|
801
|
+
|
|
802
|
+
const objects = new Map()
|
|
803
|
+
for (const [name, props] of obj) {
|
|
804
|
+
const fields = new Map()
|
|
805
|
+
props.names.forEach((k, i) => fields.set(k, props.values[i]))
|
|
806
|
+
objects.set(name, fields)
|
|
807
|
+
}
|
|
808
|
+
const rewrite = (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, arr), objects)
|
|
809
|
+
// Every init statement (the decl and, for `var`, its lone assignment) is dropped —
|
|
810
|
+
// nothing reads the binding anymore, so no aggregate is built.
|
|
811
|
+
const dropped = new Set()
|
|
812
|
+
for (const name of [...arr.keys(), ...obj.keys()]) for (const s of initStmts.get(name)) dropped.add(s)
|
|
813
|
+
|
|
814
|
+
// Rewrite + drop init statements in each module sequence (mutate in place so
|
|
815
|
+
// callers holding `ast`/moduleInits references see the result).
|
|
816
|
+
for (const seq of seqs) {
|
|
817
|
+
if (!Array.isArray(seq) || seq[0] !== ';') continue
|
|
818
|
+
const kept = []
|
|
819
|
+
for (const stmt of seq.slice(1)) {
|
|
820
|
+
if (dropped.has(stmt)) continue
|
|
821
|
+
kept.push(rewrite(stmt))
|
|
822
|
+
}
|
|
823
|
+
seq.splice(1, seq.length - 1, ...kept)
|
|
824
|
+
}
|
|
825
|
+
// Rewrite each function body AND its default-parameter expressions, excluding the
|
|
826
|
+
// folded names its params shadow.
|
|
827
|
+
for (const f of funcs) {
|
|
828
|
+
const pn = paramNames(f)
|
|
829
|
+
const shadows = pn.some(p => arr.has(p) || objects.has(p))
|
|
830
|
+
const rw = shadows
|
|
831
|
+
? (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, new Map([...arr].filter(([k]) => !pn.includes(k)))), new Map([...objects].filter(([k]) => !pn.includes(k))))
|
|
832
|
+
: rewrite
|
|
833
|
+
f.body = rw(f.body)
|
|
834
|
+
if (f.defaults) for (const k of Object.keys(f.defaults)) f.defaults[k] = rw(f.defaults[k])
|
|
835
|
+
}
|
|
836
|
+
return true
|
|
837
|
+
}
|
|
838
|
+
|
|
624
839
|
function scalarizeArrayLiterals(node) {
|
|
625
840
|
if (!Array.isArray(node)) return { node, changed: false }
|
|
626
841
|
if (node[0] === '=>') {
|
|
@@ -44,62 +44,138 @@ import { invalidateProgramFactsCache } from '../program-facts.js'
|
|
|
44
44
|
// mixed ctors) clears the candidacy, keeping the read site polymorphic.
|
|
45
45
|
export const inferModuleLetTypes = (ast) => {
|
|
46
46
|
if (!ctx.scope.userGlobals) return
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// `
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
// Build an assignment/alias graph over EVERY `=`/`let`/`const` binding in the
|
|
48
|
+
// program (globals and locals alike), then resolve each global's typed-array
|
|
49
|
+
// ctor by least-fixed-point. A single forward pass can't see the double-buffer
|
|
50
|
+
// swap idiom — `let tmp = a; a = b; b = tmp` assigns `a` from `b` and `b` from
|
|
51
|
+
// a local `tmp` that aliases `a`, so neither ref resolves until its sibling is
|
|
52
|
+
// already known. The fixpoint closes that cycle: `a`/`b` each anchor on their
|
|
53
|
+
// `new Float64Array(...)` decl, the alias edges carry the ctor around the loop,
|
|
54
|
+
// and they promote to VAL.TYPED. Without it the swap poisoned both globals and
|
|
55
|
+
// every `a[i]` read forked __str_idx/__typed_idx, every `+` forked __str_concat.
|
|
56
|
+
//
|
|
57
|
+
// Lattice (per name): null (no evidence) < ctor < MIXED. `bad` evidence (a non-
|
|
58
|
+
// typed, non-alias RHS — number, string, call, arithmetic, compound-assign) jumps
|
|
59
|
+
// straight to MIXED; conflicting ctors join to MIXED. We promote a global only
|
|
60
|
+
// when its fixed point is a single concrete ctor — sound: every assignment then
|
|
61
|
+
// provably yields that typed-array kind or nullish.
|
|
62
|
+
const MIXED = MIXED_CTORS
|
|
63
|
+
const defs = new Map() // name → { ctors:Set<string>, refs:Set<string>, bad:bool }
|
|
64
|
+
const getDef = (name) => {
|
|
65
|
+
let d = defs.get(name)
|
|
66
|
+
if (!d) defs.set(name, d = { ctors: new Set(), refs: new Set(), bad: false })
|
|
67
|
+
return d
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
62
71
|
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
// User-function names — a call to one is an alias edge to its return value
|
|
74
|
+
// (virtual node `@ret:<fn>`, populated from each `return`). Lets a global
|
|
75
|
+
// assigned `a = makeBuffer(n)` inherit makeBuffer's typed-array ctor without
|
|
76
|
+
// relying on the call being inlined (locals get it via inlining; globals,
|
|
77
|
+
// typed before inlining runs, did not). `@`/`:` can't occur in a JS identifier,
|
|
78
|
+
// so the virtual key never collides with a real binding.
|
|
79
|
+
const fnames = new Set()
|
|
80
|
+
for (const f of ctx.func.list) if (f.body && !f.raw && typeof f.name === 'string') fnames.add(f.name)
|
|
81
|
+
// Typed-array methods that preserve the receiver's element ctor: `.subarray`
|
|
82
|
+
// and `.slice` (same-kind view/copy), `.map` (same-kind, per propagateTyped).
|
|
83
|
+
const CTOR_PRESERVING = new Set(['subarray', 'slice', 'map'])
|
|
84
|
+
|
|
85
|
+
// Record one assignment `name = rhs` as evidence. Nullish contributes nothing
|
|
86
|
+
// (consistent with any typed-array value); a bare identifier, a ctor-preserving
|
|
87
|
+
// method on a name, or a call to a user function are alias edges; anything else
|
|
88
|
+
// that isn't a typed ctor poisons the name.
|
|
89
|
+
// Scope-qualified binding key. A module global is ONE node program-wide (bare
|
|
90
|
+
// name); a function-local is unique to its scope `sid`. Keying locals by bare
|
|
91
|
+
// name made a numeric counter `let s = 0` in one function poison a typed
|
|
92
|
+
// swap-temp `let s = a` in another — cascading MIXED into the double-buffer
|
|
93
|
+
// globals so every `a[i]` fell back to runtime __str_idx/__typed_idx dispatch
|
|
94
|
+
// (lbm: 3.9× slower than JS). `@ret:` virtual nodes stay bare (module-wide
|
|
95
|
+
// return-value anchors).
|
|
96
|
+
const key = (name, sid) => name[0] === '@' || ctx.scope.userGlobals.has(name) ? name : sid + '\x00' + name
|
|
97
|
+
|
|
98
|
+
const observe = (name, rhs, sid) => {
|
|
99
|
+
const d = getDef(key(name, sid))
|
|
66
100
|
if (isNullishLit(rhs)) return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
const ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
|
|
102
|
+
if (ctor === MIXED) { d.bad = true; return }
|
|
103
|
+
if (ctor) { d.ctors.add(ctor); return }
|
|
104
|
+
if (typeof rhs === 'string') { d.refs.add(key(rhs, sid)); return }
|
|
105
|
+
if (Array.isArray(rhs) && rhs[0] === '()') {
|
|
106
|
+
const callee = rhs[1]
|
|
107
|
+
// `recv.subarray(...)` / `recv.slice(...)` / `recv.map(...)` → inherit recv's ctor.
|
|
108
|
+
if (Array.isArray(callee) && callee[0] === '.' && typeof callee[1] === 'string'
|
|
109
|
+
&& CTOR_PRESERVING.has(callee[2])) { d.refs.add(key(callee[1], sid)); return }
|
|
110
|
+
// `fn(...)` to a user function → inherit its return ctor.
|
|
111
|
+
if (typeof callee === 'string' && fnames.has(callee)) { d.refs.add('@ret:' + callee); return }
|
|
74
112
|
}
|
|
75
|
-
|
|
76
|
-
const prev = ctorOf.get(name)
|
|
77
|
-
if (prev && prev !== ctor) { invalid.add(name); return }
|
|
78
|
-
ctorOf.set(name, ctor)
|
|
113
|
+
d.bad = true
|
|
79
114
|
}
|
|
80
115
|
|
|
81
|
-
|
|
116
|
+
// Scope-aware walk. Every `=>` opens a fresh scope so same-named locals across
|
|
117
|
+
// functions (and sibling closures) stay distinct. A function bound to a name
|
|
118
|
+
// descends in a name-stable scope (`fn\0name`) so the ast descent and the
|
|
119
|
+
// func.list sweep below visit it identically (idempotent), and so `return`
|
|
120
|
+
// exprs anchor on `@ret:name`.
|
|
121
|
+
let sidc = 0
|
|
122
|
+
const walk = (node, sid, retFn) => {
|
|
82
123
|
if (!Array.isArray(node)) return
|
|
83
124
|
const op = node[0]
|
|
84
|
-
if (op === '
|
|
125
|
+
if (op === '=>') { walk(node[2], 's' + (++sidc), null); return }
|
|
126
|
+
if (op === 'return' && retFn != null) observe('@ret:' + retFn, node[1], sid)
|
|
127
|
+
const assign = (name, rhs) => {
|
|
128
|
+
if (Array.isArray(rhs) && rhs[0] === '=>') { observe(name, rhs, sid); enterFn(rhs[2], name); return }
|
|
129
|
+
observe(name, rhs, sid); walk(rhs, sid, retFn)
|
|
130
|
+
}
|
|
131
|
+
if (op === '=' && typeof node[1] === 'string') return assign(node[1], node[2])
|
|
85
132
|
if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
86
133
|
for (let i = 1; i < node.length; i++) {
|
|
87
134
|
const d = node[i]
|
|
88
|
-
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string'
|
|
89
|
-
|
|
135
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') assign(d[1], d[2])
|
|
136
|
+
else walk(d, sid, retFn)
|
|
90
137
|
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
// Compound-assigns (`+=`, `++`, …) can't preserve a typed-array kind — poison.
|
|
141
|
+
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string') { getDef(key(node[1], sid)).bad = true; walk(node[2], sid, retFn); return }
|
|
142
|
+
for (let i = 1; i < node.length; i++) walk(node[i], sid, retFn)
|
|
143
|
+
}
|
|
144
|
+
// Descend into a function body anchored on `@ret:fn`. An arrow expr-body IS the
|
|
145
|
+
// implicit return (`(n) => new Float64Array(n)`); a `{}` block uses explicit
|
|
146
|
+
// `return` nodes (captured in walk). Without the implicit-return capture, a
|
|
147
|
+
// global assigned `a = mk(n)` from an expr-body fn never inherited mk's ctor.
|
|
148
|
+
const enterFn = (body, fn) => {
|
|
149
|
+
if (Array.isArray(body) && body[0] !== '{}') observe('@ret:' + fn, body, 'fn\x00' + fn)
|
|
150
|
+
walk(body, 'fn\x00' + fn, fn)
|
|
151
|
+
}
|
|
152
|
+
walk(ast, 'mod', null)
|
|
153
|
+
// Defensive sweep: cover any func.list body not reachable by descent from `ast`
|
|
154
|
+
// (hoisted / submodule). Name-stable scope keeps it idempotent with the descent.
|
|
155
|
+
for (const f of ctx.func.list) if (f.body && !f.raw) enterFn(f.body, f.name)
|
|
156
|
+
|
|
157
|
+
// Least-fixed-point over the alias graph. join: null is bottom, MIXED is top.
|
|
158
|
+
const join = (a, b) => a === MIXED || b === MIXED ? MIXED : a == null ? b : b == null ? a : a === b ? a : MIXED
|
|
159
|
+
const state = new Map() // name → null | ctor | MIXED
|
|
160
|
+
// A ref to a name with no tracked defs resolves via an already-known typed
|
|
161
|
+
// global (const typed array / earlier-recorded rep); otherwise it's opaque → MIXED.
|
|
162
|
+
const refState = (r) => defs.has(r) ? (state.get(r) ?? null)
|
|
163
|
+
: ctx.scope.globalValTypes?.get(r) === VAL.TYPED ? (ctx.scope.globalTypedElem?.get(r) ?? MIXED)
|
|
164
|
+
: MIXED
|
|
165
|
+
let changed = true
|
|
166
|
+
while (changed) {
|
|
167
|
+
changed = false
|
|
168
|
+
for (const [name, d] of defs) {
|
|
169
|
+
let cur = d.bad ? MIXED : null
|
|
170
|
+
if (cur !== MIXED) for (const c of d.ctors) cur = join(cur, c)
|
|
171
|
+
if (cur !== MIXED) for (const r of d.refs) cur = join(cur, refState(r))
|
|
172
|
+
if (cur !== (state.get(name) ?? null)) { state.set(name, cur); changed = true }
|
|
91
173
|
}
|
|
92
|
-
// Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
|
|
93
|
-
// the typed-array kind — invalidate.
|
|
94
|
-
if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && ctorOf.has(node[1]))
|
|
95
|
-
invalid.add(node[1])
|
|
96
|
-
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
97
174
|
}
|
|
98
|
-
walk(ast)
|
|
99
|
-
for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
|
|
100
175
|
|
|
101
|
-
for (const
|
|
102
|
-
|
|
176
|
+
for (const name of ctx.scope.userGlobals) {
|
|
177
|
+
const ctor = state.get(name)
|
|
178
|
+
if (!ctor || ctor === MIXED) continue
|
|
103
179
|
if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
|
|
104
180
|
;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
|
|
105
181
|
;(ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
@@ -23,6 +23,24 @@ export function observeNodeFacts(node, f) {
|
|
|
23
23
|
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) &&
|
|
24
24
|
(args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string')
|
|
25
25
|
f.writtenProps.add(args[0][2])
|
|
26
|
+
// Computed-key WRITES (`o[k]=v`, `o[k]+=v`, `o[k]++`) are the ONLY operations
|
|
27
|
+
// that add ENUMERABLE keys beyond the static schema — computed reads and dot-adds
|
|
28
|
+
// (`o.b=2`) do not enumerate in jz. Tracked separately from `dynVars` (which also
|
|
29
|
+
// counts reads) so for-in / Object.keys key pooling can trust the static schema
|
|
30
|
+
// for a receiver that is only computed-READ. (`isLiteralStr` excludes literal
|
|
31
|
+
// string keys, which place in fixed schema slots.)
|
|
32
|
+
if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) && args[0][0] === '[]') {
|
|
33
|
+
const [, wobj, widx] = args[0]
|
|
34
|
+
// Flag the ROOT array var. `o[k]=v` → o; a NESTED write `o[i][j]=v` mutates an
|
|
35
|
+
// element of o, so walk the receiver chain to its root identifier and flag that
|
|
36
|
+
// too — else o's recorded (nested) element types would be wrongly trusted at a
|
|
37
|
+
// later `o[i][j]` read. Strictly more conservative for every dynWriteVars consumer.
|
|
38
|
+
if (!isLiteralStr(widx)) {
|
|
39
|
+
let root = wobj
|
|
40
|
+
while (Array.isArray(root) && root[0] === '[]') root = root[1]
|
|
41
|
+
if (typeof root === 'string') f.dynWriteVars?.add(root)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
26
44
|
if (op === '[]') {
|
|
27
45
|
const [obj, idx] = args
|
|
28
46
|
if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
|
|
@@ -68,7 +86,7 @@ export function invalidateProgramFactsCache(...roots) {
|
|
|
68
86
|
|
|
69
87
|
function emptyWalkFacts() {
|
|
70
88
|
return {
|
|
71
|
-
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
89
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
72
90
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
73
91
|
propMap: new Map(), valueUsed: new Set(), callSites: [],
|
|
74
92
|
writtenProps: new Set(),
|
|
@@ -78,6 +96,7 @@ function emptyWalkFacts() {
|
|
|
78
96
|
function mergeWalkFacts(into, from) {
|
|
79
97
|
if (from.anyDyn) into.anyDyn = true
|
|
80
98
|
for (const v of from.dynVars) into.dynVars.add(v)
|
|
99
|
+
for (const v of from.dynWriteVars) into.dynWriteVars.add(v)
|
|
81
100
|
if (from.hasSchemaLiterals) into.hasSchemaLiterals = true
|
|
82
101
|
if (from.maxDef > into.maxDef) into.maxDef = from.maxDef
|
|
83
102
|
if (from.maxCall > into.maxCall) into.maxCall = from.maxCall
|
|
@@ -234,7 +253,7 @@ export function collectProgramFacts(ast) {
|
|
|
234
253
|
// ctx — a mutated prop name anywhere disqualifies sharing a static instance.
|
|
235
254
|
ctx.module.writtenProps = f.writtenProps
|
|
236
255
|
return {
|
|
237
|
-
dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
256
|
+
dynVars: f.dynVars, dynWriteVars: f.dynWriteVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
|
|
238
257
|
maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
|
|
239
258
|
paramReps, hasSchemaLiterals: f.hasSchemaLiterals, writtenProps: f.writtenProps,
|
|
240
259
|
}
|