jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
|
@@ -25,10 +25,10 @@
|
|
|
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
|
-
intLiteralValue, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
31
|
+
intLiteralValue, nonNegIntLiteral, constIntExpr, staticObjectProps, staticPropertyKey,
|
|
32
32
|
} from '../../static.js'
|
|
33
33
|
import {
|
|
34
34
|
smallConstForTripCount, containsDeclOf, cloneWithSubst,
|
|
@@ -37,7 +37,7 @@ import { VAL } from '../../reps.js'
|
|
|
37
37
|
import { includeModule } from '../../autoload.js'
|
|
38
38
|
import { analyzeBody, invalidateLocalsCache } from '../analyze.js'
|
|
39
39
|
import {
|
|
40
|
-
isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen,
|
|
40
|
+
isSimpleArg, fixedScalarTypedArray, fixedTypedArraysInBody, maxScalarTypedArrayLen, freshTypedArrayLocals,
|
|
41
41
|
} from './common.js'
|
|
42
42
|
|
|
43
43
|
// === Loop unrolling & scalarization ===
|
|
@@ -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 })
|
|
@@ -496,6 +526,44 @@ export const scalarizeFunctionTypedArrays = (programFacts) => {
|
|
|
496
526
|
return changed
|
|
497
527
|
}
|
|
498
528
|
|
|
529
|
+
// Param-distinctness (alias analysis). Marks a function's typed-array params MUTUALLY DISTINCT
|
|
530
|
+
// (provably different buffers) when EVERY call site passes a distinct fresh `new TypedArray` local
|
|
531
|
+
// for each of them. This is what lets the optimizer's LICM hoist a load from one such param across
|
|
532
|
+
// a store to another (the load can't be clobbered) — the alias-analysis-enabled LICM that
|
|
533
|
+
// rust/clang get for free (raytrace's sphere loads vs the framebuffer store). Sound because:
|
|
534
|
+
// • a fresh `new TypedArray(N)` is a unique buffer (the allocator returns fresh memory);
|
|
535
|
+
// • requiring ALL typed-array-param args to be fresh-new excludes views/subarrays (not fresh)
|
|
536
|
+
// and forwarded params (not fresh), the only ways two args could alias;
|
|
537
|
+
// • pairwise-distinct arg names rule out the same buffer passed twice;
|
|
538
|
+
// • scalar (non-TYPED) params are ignored — they can't alias a buffer.
|
|
539
|
+
// Conservatively all-or-nothing per function: any non-fresh/duplicate typed arg ⇒ no fact.
|
|
540
|
+
export const analyzeParamDistinctness = (programFacts) => {
|
|
541
|
+
const freshByFunc = new Map(ctx.func.list.map(func => [func, freshTypedArrayLocals(func.body)]))
|
|
542
|
+
const sitesByCallee = new Map()
|
|
543
|
+
for (const site of programFacts.callSites) {
|
|
544
|
+
if (!site.callerFunc) continue
|
|
545
|
+
const l = sitesByCallee.get(site.callee); l ? l.push(site) : sitesByCallee.set(site.callee, [site])
|
|
546
|
+
}
|
|
547
|
+
for (const func of ctx.func.list) {
|
|
548
|
+
const params = func.sig?.params, sites = sitesByCallee.get(func.name)
|
|
549
|
+
if (!params || !sites?.length) continue
|
|
550
|
+
const typedIdx = []
|
|
551
|
+
for (let i = 0; i < params.length; i++) if (params[i].ptrKind === VAL.TYPED) typedIdx.push(i)
|
|
552
|
+
if (typedIdx.length < 2) continue // distinctness only matters with ≥2 typed-array params
|
|
553
|
+
let ok = true
|
|
554
|
+
for (const site of sites) {
|
|
555
|
+
const seen = new Set()
|
|
556
|
+
for (const i of typedIdx) {
|
|
557
|
+
const arg = site.argList?.[i]
|
|
558
|
+
if (typeof arg !== 'string' || !freshByFunc.get(site.callerFunc)?.has(arg) || seen.has(arg)) { ok = false; break }
|
|
559
|
+
seen.add(arg)
|
|
560
|
+
}
|
|
561
|
+
if (!ok) break
|
|
562
|
+
}
|
|
563
|
+
if (ok) func.distinctParams = new Set(typedIdx.map(i => params[i].name))
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
499
567
|
const scalarizeArrayLiteralSeq = (seq) => {
|
|
500
568
|
if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
|
|
501
569
|
let changed = false
|
|
@@ -621,6 +689,191 @@ function scalarizeObjectLiterals(node, escapes) {
|
|
|
621
689
|
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
622
690
|
}
|
|
623
691
|
|
|
692
|
+
// === Whole-program constant fold of module-scope aggregate literals ===
|
|
693
|
+
//
|
|
694
|
+
// `var x = [1,2,3]; export const y = x[0]` materializes a data-segment array and
|
|
695
|
+
// indexes it through __arr_idx_known (bounds + grow-forwarding) — all dead weight
|
|
696
|
+
// for a never-grown constant. When EVERY reference to a module-scope const aggregate
|
|
697
|
+
// is a static READ, replace each `x[k]` / `x.length` / `o.key` by its literal value
|
|
698
|
+
// program-wide; the now-unused decl is dropped, so no array is built (no data, no
|
|
699
|
+
// memory, no index helper) and the global is never declared. The per-function
|
|
700
|
+
// scalarizers can't do this: the decl lives at module scope and may be read from
|
|
701
|
+
// several function bodies, so the check must span the whole program.
|
|
702
|
+
|
|
703
|
+
const ASSIGN_OR_UPDATE = (op) => ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--'
|
|
704
|
+
// Module-scope binding ops. `var` survives to compile at module scope (jzify only
|
|
705
|
+
// lowers it to `let` inside functions), so fold it too — the reassignment guard
|
|
706
|
+
// below keeps a re-bound `var` heap-backed.
|
|
707
|
+
const isDeclOp = (op) => op === 'let' || op === 'const' || op === 'var'
|
|
708
|
+
|
|
709
|
+
// Reject `delete x`, `delete x.k`, `delete x[k]` — a deletion mutates the aggregate.
|
|
710
|
+
const isDeleteOf = (node, name) =>
|
|
711
|
+
node[0] === 'delete' && (node[1] === name || (Array.isArray(node[1]) && node[1][1] === name))
|
|
712
|
+
|
|
713
|
+
// A reference is fold-safe only if it READS `name` via a static index/key (or
|
|
714
|
+
// `.length` for arrays). Any write/update/delete, reassignment, second declaration,
|
|
715
|
+
// bare value use (escape), spread, dynamic key, or non-own member (method / proto
|
|
716
|
+
// chain) escapes the literal model and disqualifies the binding.
|
|
717
|
+
const foldSafeArrayUse = (node, name, len) => {
|
|
718
|
+
if (typeof node === 'string') return node !== name
|
|
719
|
+
if (!Array.isArray(node)) return true
|
|
720
|
+
const op = node[0]
|
|
721
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
722
|
+
if (isDeleteOf(node, name)) return false
|
|
723
|
+
if (ASSIGN_OR_UPDATE(op)) {
|
|
724
|
+
const t = node[1]
|
|
725
|
+
if (t === name) return false
|
|
726
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
727
|
+
}
|
|
728
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
729
|
+
if (op === '[]' && node[1] === name) return constIntExpr(node[2]) != null
|
|
730
|
+
if (op === '...' && node[1] === name) return false
|
|
731
|
+
for (let i = 1; i < node.length; i++) if (!foldSafeArrayUse(node[i], name, len)) return false
|
|
732
|
+
return true
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const foldSafeObjectUse = (node, name, keys) => {
|
|
736
|
+
if (typeof node === 'string') return node !== name
|
|
737
|
+
if (!Array.isArray(node)) return true
|
|
738
|
+
const op = node[0]
|
|
739
|
+
if (isDeclOp(op) && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
740
|
+
if (isDeleteOf(node, name)) return false
|
|
741
|
+
if (ASSIGN_OR_UPDATE(op)) {
|
|
742
|
+
const t = node[1]
|
|
743
|
+
if (t === name) return false
|
|
744
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
745
|
+
}
|
|
746
|
+
if ((op === '.' || op === '?.') && node[1] === name) return keys.has(node[2]) // own key only — proto-safe
|
|
747
|
+
if (op === '[]' && node[1] === name) { const k = staticPropertyKey(node[2]); return k != null && keys.has(k) }
|
|
748
|
+
if (op === '...' && node[1] === name) return false
|
|
749
|
+
for (let i = 1; i < node.length; i++) if (!foldSafeObjectUse(node[i], name, keys)) return false
|
|
750
|
+
return true
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const moduleStmtsOf = (seq) =>
|
|
754
|
+
Array.isArray(seq) && seq[0] === ';' ? seq.slice(1)
|
|
755
|
+
: Array.isArray(seq) && isDeclOp(seq[0]) ? [seq]
|
|
756
|
+
: []
|
|
757
|
+
|
|
758
|
+
export function foldStaticConstAggregates(ast) {
|
|
759
|
+
// Span the main module AST and every bundled sub-module init — a const declared in
|
|
760
|
+
// one can be read from another or from a function body (mirrors the constInts fold).
|
|
761
|
+
const seqs = [ast, ...(ctx.module.moduleInits || [])]
|
|
762
|
+
const moduleStmts = seqs.flatMap(moduleStmtsOf)
|
|
763
|
+
const funcs = ctx.func.list.filter(f => f.body && !f.raw)
|
|
764
|
+
// A function parameter named `x` rebinds `x` for the whole body — its `x[…]` reads
|
|
765
|
+
// the param, not the module binding (params live on `f.sig`, separate from `.body`,
|
|
766
|
+
// so the body scan can't see them). Such a function is skipped (scan) / excluded
|
|
767
|
+
// (rewrite) for that name.
|
|
768
|
+
const paramNames = (f) => (f.sig?.params || []).map(p => p.name)
|
|
769
|
+
// Every AST a function can reference an outer binding from: its body PLUS each
|
|
770
|
+
// default-parameter expression (`(v = x[0]) => …`), which prepare extracts to
|
|
771
|
+
// `f.defaults` — separate from the body, so the body scan/rewrite would miss it.
|
|
772
|
+
const funcNodes = (f) => f.defaults ? [f.body, ...Object.values(f.defaults)] : [f.body]
|
|
773
|
+
|
|
774
|
+
// Classify module statements. A binding's value comes from an inline decl
|
|
775
|
+
// (`const x = […]`) OR — for `var`, which jzify lowers to `let x; x = […]` — a
|
|
776
|
+
// lone module-scope assignment after an uninitialized decl. The init statement(s)
|
|
777
|
+
// are excluded from the read-only scan and dropped on fold.
|
|
778
|
+
const inlineInit = new Map() // name -> {value, stmt} | null (poisoned: >1 decl)
|
|
779
|
+
const assigns = new Map() // name -> [assignStmt…]
|
|
780
|
+
const uninitDecl = new Map() // name -> declStmt (`let x` / `var x`, string declarator)
|
|
781
|
+
for (const stmt of moduleStmts) {
|
|
782
|
+
if (!Array.isArray(stmt)) continue
|
|
783
|
+
const op = stmt[0]
|
|
784
|
+
if (isDeclOp(op) && stmt.length === 2 && Array.isArray(stmt[1]) && stmt[1][0] === '=' && typeof stmt[1][1] === 'string') {
|
|
785
|
+
const name = stmt[1][1]
|
|
786
|
+
inlineInit.set(name, inlineInit.has(name) ? null : { value: stmt[1][2], stmt })
|
|
787
|
+
} else if (isDeclOp(op) && stmt.length === 2 && typeof stmt[1] === 'string') {
|
|
788
|
+
uninitDecl.set(stmt[1], stmt)
|
|
789
|
+
} else if (op === '=' && typeof stmt[1] === 'string') {
|
|
790
|
+
(assigns.get(stmt[1]) ?? assigns.set(stmt[1], []).get(stmt[1])).push(stmt)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// index of each statement in module-execution order — used to prove a `var`'s
|
|
795
|
+
// assignment dominates its reads.
|
|
796
|
+
const pos = new Map()
|
|
797
|
+
moduleStmts.forEach((s, i) => pos.set(s, i))
|
|
798
|
+
|
|
799
|
+
const arr = new Map(), obj = new Map(), initStmts = new Map()
|
|
800
|
+
const consider = (name, value, init) => {
|
|
801
|
+
if (ctx.func.exports?.[name]) return // exported → escapes to JS
|
|
802
|
+
const elems = scalarArrayElems(value)
|
|
803
|
+
if (elems) { arr.set(name, elems); initStmts.set(name, init); return }
|
|
804
|
+
const props = scalarObjectProps(value)
|
|
805
|
+
if (props) { obj.set(name, props); initStmts.set(name, init) }
|
|
806
|
+
}
|
|
807
|
+
for (const [name, info] of inlineInit) {
|
|
808
|
+
// a decl-initialized binding that is ALSO assigned is reassigned → not constant.
|
|
809
|
+
if (info && !assigns.has(name)) consider(name, info.value, new Set([info.stmt]))
|
|
810
|
+
}
|
|
811
|
+
for (const [name, list] of assigns) {
|
|
812
|
+
// `var` lowering: exactly one assignment, a matching uninit decl, and no
|
|
813
|
+
// competing inline decl. The assignment must dominate every read — conservatively,
|
|
814
|
+
// it precedes all other module references and the name is unused in any function
|
|
815
|
+
// body (a function could run before the assignment). `let`/`const` need no such
|
|
816
|
+
// guard (TDZ forbids use-before-init).
|
|
817
|
+
if (inlineInit.has(name) || list.length !== 1 || !uninitDecl.has(name)) continue
|
|
818
|
+
const assign = list[0], at = pos.get(assign)
|
|
819
|
+
const refsBefore = moduleStmts.some((s, i) => i < at && s !== uninitDecl.get(name) && refsName(s, name, REFS_IN_EXPR))
|
|
820
|
+
const refsInFn = funcs.some(f => !paramNames(f).includes(name) && funcNodes(f).some(n => refsName(n, name, REFS_IN_EXPR)))
|
|
821
|
+
if (refsBefore || refsInFn) continue
|
|
822
|
+
consider(name, assign[2], new Set([assign, uninitDecl.get(name)]))
|
|
823
|
+
}
|
|
824
|
+
if (!arr.size && !obj.size) return false
|
|
825
|
+
|
|
826
|
+
// Every mention outside the binding's init statement(s) — across all module
|
|
827
|
+
// statements and all function bodies — must be a fold-safe static read.
|
|
828
|
+
const checkAll = (name, pred) => {
|
|
829
|
+
const skip = initStmts.get(name)
|
|
830
|
+
return moduleStmts.every(s => skip.has(s) || pred(s))
|
|
831
|
+
&& funcs.every(f => paramNames(f).includes(name) || funcNodes(f).every(pred))
|
|
832
|
+
}
|
|
833
|
+
for (const [name, elems] of [...arr]) if (!checkAll(name, n => foldSafeArrayUse(n, name, elems.length))) arr.delete(name)
|
|
834
|
+
for (const [name, props] of [...obj]) {
|
|
835
|
+
const keys = new Set(props.names)
|
|
836
|
+
if (!checkAll(name, n => foldSafeObjectUse(n, name, keys))) obj.delete(name)
|
|
837
|
+
}
|
|
838
|
+
if (!arr.size && !obj.size) return false
|
|
839
|
+
|
|
840
|
+
const objects = new Map()
|
|
841
|
+
for (const [name, props] of obj) {
|
|
842
|
+
const fields = new Map()
|
|
843
|
+
props.names.forEach((k, i) => fields.set(k, props.values[i]))
|
|
844
|
+
objects.set(name, fields)
|
|
845
|
+
}
|
|
846
|
+
const rewrite = (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, arr), objects)
|
|
847
|
+
// Every init statement (the decl and, for `var`, its lone assignment) is dropped —
|
|
848
|
+
// nothing reads the binding anymore, so no aggregate is built.
|
|
849
|
+
const dropped = new Set()
|
|
850
|
+
for (const name of [...arr.keys(), ...obj.keys()]) for (const s of initStmts.get(name)) dropped.add(s)
|
|
851
|
+
|
|
852
|
+
// Rewrite + drop init statements in each module sequence (mutate in place so
|
|
853
|
+
// callers holding `ast`/moduleInits references see the result).
|
|
854
|
+
for (const seq of seqs) {
|
|
855
|
+
if (!Array.isArray(seq) || seq[0] !== ';') continue
|
|
856
|
+
const kept = []
|
|
857
|
+
for (const stmt of seq.slice(1)) {
|
|
858
|
+
if (dropped.has(stmt)) continue
|
|
859
|
+
kept.push(rewrite(stmt))
|
|
860
|
+
}
|
|
861
|
+
seq.splice(1, seq.length - 1, ...kept)
|
|
862
|
+
}
|
|
863
|
+
// Rewrite each function body AND its default-parameter expressions, excluding the
|
|
864
|
+
// folded names its params shadow.
|
|
865
|
+
for (const f of funcs) {
|
|
866
|
+
const pn = paramNames(f)
|
|
867
|
+
const shadows = pn.some(p => arr.has(p) || objects.has(p))
|
|
868
|
+
const rw = shadows
|
|
869
|
+
? (n) => rewriteScalarObjectUses(rewriteScalarArrayUses(n, new Map([...arr].filter(([k]) => !pn.includes(k)))), new Map([...objects].filter(([k]) => !pn.includes(k))))
|
|
870
|
+
: rewrite
|
|
871
|
+
f.body = rw(f.body)
|
|
872
|
+
if (f.defaults) for (const k of Object.keys(f.defaults)) f.defaults[k] = rw(f.defaults[k])
|
|
873
|
+
}
|
|
874
|
+
return true
|
|
875
|
+
}
|
|
876
|
+
|
|
624
877
|
function scalarizeArrayLiterals(node) {
|
|
625
878
|
if (!Array.isArray(node)) return { node, changed: false }
|
|
626
879
|
if (node[0] === '=>') {
|
|
@@ -725,13 +978,41 @@ const _intArrayLitElems = (expr) => {
|
|
|
725
978
|
return out
|
|
726
979
|
}
|
|
727
980
|
|
|
981
|
+
// Arithmetic and bitwise operators that always produce a numeric result —
|
|
982
|
+
// regardless of operand types — so `name[expr]` is index-safe after promotion.
|
|
983
|
+
// Bitwise ops (`&`, `|`, etc.) ToInt32 their operands; pure-arithmetic ops with
|
|
984
|
+
// numeric leaves stay numeric. `+` is excluded: `"a" + "b"` is a string.
|
|
985
|
+
const _NUMERIC_INDEX_OPS = new Set(['-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>', 'u-', 'u+'])
|
|
986
|
+
|
|
987
|
+
// Returns true when `key` is provably a numeric index at plan time: an integer
|
|
988
|
+
// literal, a local name whose val-type is VAL.NUMBER in `valTypes`, or a
|
|
989
|
+
// compound expression that always produces a number (arithmetic/bitwise ops).
|
|
990
|
+
// Mirrors the `idxNumericName` / `intIndexIR` guard in emit so that the same
|
|
991
|
+
// index shapes that skip `__is_str_key` at emit-time also pass here. Used by
|
|
992
|
+
// `_disqualifyPromotion` to gate index reads: a non-numeric key on a promoted
|
|
993
|
+
// Int32Array NaN-coerces to 0 (trunc_sat_f64_s(NaN) = 0) instead of returning
|
|
994
|
+
// undefined — the correct JS behaviour for an out-of-range or string index.
|
|
995
|
+
const _isNumericKey = (key, valTypes) => {
|
|
996
|
+
if (key == null) return false
|
|
997
|
+
if (nonNegIntLiteral(key) != null) return true // literal integer
|
|
998
|
+
if (typeof key === 'string') return valTypes?.get(key) === VAL.NUMBER
|
|
999
|
+
if (!Array.isArray(key)) return false
|
|
1000
|
+
const op = key[0]
|
|
1001
|
+
if (op == null) return typeof key[1] === 'number' // [null, n] literal
|
|
1002
|
+
if (_NUMERIC_INDEX_OPS.has(op)) return true // always produces Number
|
|
1003
|
+
// `+` is numeric only when both operands are proven numeric.
|
|
1004
|
+
if (op === '+' && key.length === 3)
|
|
1005
|
+
return _isNumericKey(key[1], valTypes) && _isNumericKey(key[2], valTypes)
|
|
1006
|
+
return false
|
|
1007
|
+
}
|
|
1008
|
+
|
|
728
1009
|
// Walks `node` and disqualifies every candidate name that appears in an
|
|
729
1010
|
// unsafe context. `initSet` holds the candidate's own init-decl AST nodes
|
|
730
1011
|
// (their LHS reference is the binding being defined, not an escape).
|
|
731
|
-
const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
1012
|
+
const _disqualifyPromotion = (node, candidates, disqualified, initSet, valTypes) => {
|
|
732
1013
|
if (initSet.has(node)) {
|
|
733
1014
|
// The init decl itself: only walk the RHS (skip the LHS `name`).
|
|
734
|
-
return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1015
|
+
return _disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
735
1016
|
}
|
|
736
1017
|
if (typeof node === 'string') {
|
|
737
1018
|
// Bare identifier outside any handled parent context — escape.
|
|
@@ -757,7 +1038,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
757
1038
|
Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
|
|
758
1039
|
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
759
1040
|
disqualified.add(node[1][1])
|
|
760
|
-
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1041
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
761
1042
|
return
|
|
762
1043
|
}
|
|
763
1044
|
|
|
@@ -779,7 +1060,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
779
1060
|
typeof callee[1] === 'string' && candidates.has(callee[1])) {
|
|
780
1061
|
if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
|
|
781
1062
|
// Walk method args (skip the receiver — already validated above).
|
|
782
|
-
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1063
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
783
1064
|
return
|
|
784
1065
|
}
|
|
785
1066
|
// Array.isArray flips true→false under promotion.
|
|
@@ -788,7 +1069,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
788
1069
|
const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
789
1070
|
for (const a of list) {
|
|
790
1071
|
if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
|
|
791
|
-
else _disqualifyPromotion(a, candidates, disqualified, initSet)
|
|
1072
|
+
else _disqualifyPromotion(a, candidates, disqualified, initSet, valTypes)
|
|
792
1073
|
}
|
|
793
1074
|
return
|
|
794
1075
|
}
|
|
@@ -796,10 +1077,15 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
796
1077
|
// bare-name leaf above and disqualify.
|
|
797
1078
|
}
|
|
798
1079
|
|
|
799
|
-
// Index read `name[k]` —
|
|
800
|
-
//
|
|
1080
|
+
// Index read `name[k]` — TYPED-safe only when the key is provably numeric.
|
|
1081
|
+
// A string or unknown key on a promoted Int32Array would NaN-coerce to 0
|
|
1082
|
+
// (i32.trunc_sat_f64_s(NaN) = 0) instead of returning undefined — silently
|
|
1083
|
+
// wrong. Mirror the `idxNumericName` / `intIndexIR` guard in emit: disqualify
|
|
1084
|
+
// the candidate unless `k` is an integer literal, a VAL.NUMBER local, or an
|
|
1085
|
+
// expression that always produces a Number (bitwise/arithmetic ops).
|
|
801
1086
|
if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
802
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1087
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
1088
|
+
if (!_isNumericKey(node[2], valTypes)) disqualified.add(node[1])
|
|
803
1089
|
return
|
|
804
1090
|
}
|
|
805
1091
|
|
|
@@ -808,15 +1094,15 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
808
1094
|
if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
|
|
809
1095
|
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
810
1096
|
disqualified.add(node[1][1])
|
|
811
|
-
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
|
|
812
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1097
|
+
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet, valTypes)
|
|
1098
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
813
1099
|
return
|
|
814
1100
|
}
|
|
815
1101
|
|
|
816
1102
|
// Whole-binding reassign: `name = …` / `name += …` / etc.
|
|
817
1103
|
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
818
1104
|
disqualified.add(node[1])
|
|
819
|
-
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
1105
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet, valTypes)
|
|
820
1106
|
return
|
|
821
1107
|
}
|
|
822
1108
|
|
|
@@ -826,7 +1112,7 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
826
1112
|
if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
|
|
827
1113
|
if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
|
|
828
1114
|
disqualified.add(t[1])
|
|
829
|
-
_disqualifyPromotion(t[2], candidates, disqualified, initSet)
|
|
1115
|
+
_disqualifyPromotion(t[2], candidates, disqualified, initSet, valTypes)
|
|
830
1116
|
return
|
|
831
1117
|
}
|
|
832
1118
|
}
|
|
@@ -841,9 +1127,9 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
841
1127
|
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
842
1128
|
candidates.has(d[1]) && !initSet.has(d)) {
|
|
843
1129
|
disqualified.add(d[1])
|
|
844
|
-
_disqualifyPromotion(d[2], candidates, disqualified, initSet)
|
|
1130
|
+
_disqualifyPromotion(d[2], candidates, disqualified, initSet, valTypes)
|
|
845
1131
|
} else {
|
|
846
|
-
_disqualifyPromotion(d, candidates, disqualified, initSet)
|
|
1132
|
+
_disqualifyPromotion(d, candidates, disqualified, initSet, valTypes)
|
|
847
1133
|
}
|
|
848
1134
|
}
|
|
849
1135
|
return
|
|
@@ -865,14 +1151,14 @@ const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
|
865
1151
|
for (let i = 1; i < node.length; i++) {
|
|
866
1152
|
const child = node[i]
|
|
867
1153
|
if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
|
|
868
|
-
_disqualifyPromotion(child, candidates, disqualified, initSet)
|
|
1154
|
+
_disqualifyPromotion(child, candidates, disqualified, initSet, valTypes)
|
|
869
1155
|
}
|
|
870
1156
|
return
|
|
871
1157
|
}
|
|
872
1158
|
|
|
873
1159
|
// Generic — recurse into children. Bare-name refs at unhandled positions
|
|
874
1160
|
// hit the string-leaf branch above and disqualify on contact.
|
|
875
|
-
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1161
|
+
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet, valTypes)
|
|
876
1162
|
}
|
|
877
1163
|
|
|
878
1164
|
// Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
|
|
@@ -940,8 +1226,11 @@ const promoteIntArrayLiteralsInBody = (body) => {
|
|
|
940
1226
|
if (!candidates.size) return { node: body, changed: false }
|
|
941
1227
|
const initSet = new Set()
|
|
942
1228
|
for (const { initDecl } of candidates.values()) initSet.add(initDecl)
|
|
1229
|
+
// valTypes from analyzeBody gives per-local VAL.* kinds, used by
|
|
1230
|
+
// _disqualifyPromotion to prove index keys are numeric (see _isNumericKey).
|
|
1231
|
+
const { valTypes } = analyzeBody(body)
|
|
943
1232
|
const disqualified = new Set()
|
|
944
|
-
_disqualifyPromotion(body, candidates, disqualified, initSet)
|
|
1233
|
+
_disqualifyPromotion(body, candidates, disqualified, initSet, valTypes)
|
|
945
1234
|
const validated = new Set()
|
|
946
1235
|
for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
|
|
947
1236
|
if (!validated.size) return { node: body, changed: false }
|