jz 0.4.0 → 0.5.1
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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/plan.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* narrowed signatures, finalized global reps, and per-call decisions.
|
|
9
9
|
*
|
|
10
10
|
* # Pipeline (top-level `plan(ast)`)
|
|
11
|
-
* 1.
|
|
11
|
+
* 1. unboxConstTypedGlobals — finalize global storage. (Global value facts
|
|
12
|
+
* themselves are seeded by prepare via `infer.recordGlobalRep`.)
|
|
12
13
|
* 2. collectProgramFacts — sweep arrow bodies for typed-elem usage, key sets,
|
|
13
14
|
* loop depth, control-transfer shapes; rerun if hot inlining changes the AST.
|
|
14
15
|
* 3. materializeAutoBoxSchemas / resolveClosureWidth — settle layout decisions.
|
|
@@ -24,9 +25,10 @@
|
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
import { ctx } from './ctx.js'
|
|
27
|
-
import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey,
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
28
|
+
import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts, analyzeFuncNamespaces, extractParams, intLiteralValue } from './analyze.js'
|
|
29
|
+
import { includeModule } from './autoload.js'
|
|
30
|
+
import { MAX_CLOSURE_ARITY, UNDEF_WAT } from './ir.js'
|
|
31
|
+
import narrowSignatures, { specializeBimorphicTyped, refineDynKeys, applyJsstringBoundaryCarrierStandalone, narrowBoolResults } from './narrow.js'
|
|
30
32
|
|
|
31
33
|
const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
|
|
32
34
|
const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
|
|
@@ -199,17 +201,27 @@ const fixedScalarTypedArray = (expr) => {
|
|
|
199
201
|
|
|
200
202
|
const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
201
203
|
|
|
202
|
-
const safeScalarArrayUse = (node, name, parentOp = null) => {
|
|
204
|
+
const safeScalarArrayUse = (node, name, len, parentOp = null) => {
|
|
203
205
|
if (typeof node === 'string') return node !== name
|
|
204
206
|
if (!Array.isArray(node)) return true
|
|
205
207
|
const op = node[0]
|
|
206
208
|
if (ASSIGN_TARGET_OPS.has(op) && node[1] === name) return false
|
|
207
209
|
if ((op === 'let' || op === 'const') && node.slice(1).some(d => d === name || (Array.isArray(d) && d[1] === name))) return false
|
|
208
210
|
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
211
|
+
// Element write `name[idx] (op)= v` / `name[idx]++`: an out-of-bounds index
|
|
212
|
+
// grows the array (sparse-array semantics), which the fixed scalar slot set
|
|
213
|
+
// can't model — reject unless idx is a literal within the literal's bounds.
|
|
214
|
+
if ((ASSIGN_TARGET_OPS.has(op) || op === '++' || op === '--')
|
|
215
|
+
&& Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
|
|
216
|
+
const idx = intLit(node[1][2])
|
|
217
|
+
if (idx == null || idx < 0 || idx >= len) return false
|
|
218
|
+
for (let i = 2; i < node.length; i++) if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
209
221
|
if (op === '[]' && node[1] === name) return intLit(node[2]) != null
|
|
210
222
|
if (op === '...' && node[1] === name) return parentOp === '['
|
|
211
223
|
for (let i = 1; i < node.length; i++) {
|
|
212
|
-
if (!safeScalarArrayUse(node[i], name, op)) return false
|
|
224
|
+
if (!safeScalarArrayUse(node[i], name, len, op)) return false
|
|
213
225
|
}
|
|
214
226
|
return true
|
|
215
227
|
}
|
|
@@ -674,7 +686,7 @@ const scalarizeArrayLiteralSeq = (seq) => {
|
|
|
674
686
|
let ok = true
|
|
675
687
|
for (let j = 0; j < stmts.length && ok; j++) {
|
|
676
688
|
if (j === i) continue
|
|
677
|
-
ok = safeScalarArrayUse(stmts[j], decl[1])
|
|
689
|
+
ok = safeScalarArrayUse(stmts[j], decl[1], elems.length)
|
|
678
690
|
}
|
|
679
691
|
if (!ok) continue
|
|
680
692
|
candidates.set(decl[1], { index: i, op: stmt[0], elems })
|
|
@@ -804,6 +816,316 @@ const scalarizeFunctionArrayLiterals = () => {
|
|
|
804
816
|
return changed
|
|
805
817
|
}
|
|
806
818
|
|
|
819
|
+
// === Int-array → Int32Array auto-promotion =================================
|
|
820
|
+
//
|
|
821
|
+
// A `let xs = [intLit, intLit, …]` binding whose every use is TYPED-compatible
|
|
822
|
+
// is rewritten to `let xs = new Int32Array([intLit, …])`. Downstream analysis
|
|
823
|
+
// then takes over: valTypeOf → VAL.TYPED, methods dispatch through `.typed:`,
|
|
824
|
+
// loops get auto-vectorized via i32x4 (SIMD pass), and the carrier shrinks
|
|
825
|
+
// from 8-byte f64 slots to packed i32. The promotion runs after literal
|
|
826
|
+
// scalarization (so arrays fully scalarized away are already gone) and before
|
|
827
|
+
// typed-array param scalarization (so a freshly-promoted array can still
|
|
828
|
+
// participate in subsequent loop unrolling).
|
|
829
|
+
//
|
|
830
|
+
// Safety: the binding must never appear in a pattern that TYPED can't honor.
|
|
831
|
+
// Disqualifiers (each fires per binding name):
|
|
832
|
+
// 1. reassignment `xs = …` / compound `xs +=` / `++xs` / `--xs` — TYPED has
|
|
833
|
+
// no value-replacement op; `xs = new TypedArray(…)` would also drop the
|
|
834
|
+
// promoted view's identity.
|
|
835
|
+
// 2. element write `xs[k] = v` — TYPED's i32-trunc store would lose
|
|
836
|
+
// fractional/NaN bits that VAL.ARRAY would have preserved.
|
|
837
|
+
// 3. method calls outside the read-safe whitelist (push, pop, shift, …) —
|
|
838
|
+
// TYPED arrays are fixed-length; mutators don't exist on the carrier.
|
|
839
|
+
// 4. `Array.isArray(xs)` — semantics flip true→false on promotion.
|
|
840
|
+
// 5. `…xs` spread / `xs` as call arg / `xs` as return / bare reference in
|
|
841
|
+
// any other position — escape; callee may rely on ARRAY layout.
|
|
842
|
+
// 6. captured by a closure / shadowed by an inner decl — same escape
|
|
843
|
+
// reasoning, plus the inner decl could rebind to a non-array.
|
|
844
|
+
//
|
|
845
|
+
// Elements at init must all be i32-range integer literals. A negative literal
|
|
846
|
+
// arrives as `[null, -n]` after prepare's constant folding; intLiteralValue
|
|
847
|
+
// recognizes that form. Float literals (`[1, 2.5]`) and out-of-range ints
|
|
848
|
+
// (`0x80000000` on the +ve side, etc.) disqualify the array as a whole.
|
|
849
|
+
|
|
850
|
+
// Methods we promote across. The bar: every entry must have a real `.typed:*`
|
|
851
|
+
// emitter in module/typedarray.js. Receiver-flow into chained methods is also
|
|
852
|
+
// typed-aware now — `.typed:map`/`.typed:filter`/`.typed:slice` all return
|
|
853
|
+
// TYPED carriers, and downstream `.filter`/`.slice`/`.map`/etc. on those
|
|
854
|
+
// re-dispatch via emit.js:2211's `.typed:<m>` lookup (VAL.TYPED ⇒ typed
|
|
855
|
+
// emitter). Methods missing here (.join, .sort, .reverse, .subarray, .fill,
|
|
856
|
+
// .toString, .copyWithin, …) lack a typed emitter — disqualify the candidate.
|
|
857
|
+
const _TYPED_SAFE_METHODS = new Set([
|
|
858
|
+
'set',
|
|
859
|
+
'map', 'filter', 'slice',
|
|
860
|
+
'forEach', 'reduce',
|
|
861
|
+
'indexOf', 'includes', 'find', 'findIndex', 'some', 'every',
|
|
862
|
+
])
|
|
863
|
+
|
|
864
|
+
// `.length` is TYPED-aware via core.js:__len (shifts the byte header by
|
|
865
|
+
// __typed_shift on TAG=3). `.byteLength`/`.byteOffset`/`.buffer` are
|
|
866
|
+
// TYPED-only — a user reading those already expects a typed array, so a
|
|
867
|
+
// promotion candidate that hits them is a coincidence we'd rather not
|
|
868
|
+
// rely on; disqualify and let them write the TypedArray construction
|
|
869
|
+
// themselves.
|
|
870
|
+
const _TYPED_SAFE_PROPS = new Set(['length'])
|
|
871
|
+
|
|
872
|
+
// Returns the i32-range integer payload of an array-literal element, or null
|
|
873
|
+
// if the element isn't a literal integer that fits in i32. Mirrors the shape
|
|
874
|
+
// check used by `intLiteralValue` but without the rep-lookup (we're pre-
|
|
875
|
+
// analysis here, and want a pure syntactic gate).
|
|
876
|
+
const _intArrayLitElems = (expr) => {
|
|
877
|
+
if (!Array.isArray(expr) || expr[0] !== '[') return null
|
|
878
|
+
if (expr.length < 2) return null // empty literal — low value, skip
|
|
879
|
+
const out = []
|
|
880
|
+
for (let i = 1; i < expr.length; i++) {
|
|
881
|
+
const v = intLiteralValue(expr[i])
|
|
882
|
+
if (v == null) return null
|
|
883
|
+
out.push(v)
|
|
884
|
+
}
|
|
885
|
+
return out
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// True iff `name` appears anywhere within `node` as a bare identifier or
|
|
889
|
+
// inside any expression position. Used to detect escape across a closure
|
|
890
|
+
// boundary (where we can't trace the use sites locally).
|
|
891
|
+
const _refsName = (node, name) => {
|
|
892
|
+
if (typeof node === 'string') return node === name
|
|
893
|
+
if (!Array.isArray(node)) return false
|
|
894
|
+
for (let i = 1; i < node.length; i++) if (_refsName(node[i], name)) return true
|
|
895
|
+
return false
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Walks `node` and disqualifies every candidate name that appears in an
|
|
899
|
+
// unsafe context. `initSet` holds the candidate's own init-decl AST nodes
|
|
900
|
+
// (their LHS reference is the binding being defined, not an escape).
|
|
901
|
+
const _disqualifyPromotion = (node, candidates, disqualified, initSet) => {
|
|
902
|
+
if (initSet.has(node)) {
|
|
903
|
+
// The init decl itself: only walk the RHS (skip the LHS `name`).
|
|
904
|
+
return _disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
905
|
+
}
|
|
906
|
+
if (typeof node === 'string') {
|
|
907
|
+
// Bare identifier outside any handled parent context — escape.
|
|
908
|
+
if (candidates.has(node)) disqualified.add(node)
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
if (!Array.isArray(node)) return
|
|
912
|
+
const op = node[0]
|
|
913
|
+
|
|
914
|
+
// Closure body — any candidate referenced inside is captured. Bail without
|
|
915
|
+
// recursing further (we'd otherwise hit the bare-name leaf and disqualify
|
|
916
|
+
// anyway, but this is explicit and avoids walking the inner closure).
|
|
917
|
+
if (op === '=>') {
|
|
918
|
+
for (const n of candidates.keys()) {
|
|
919
|
+
if (!disqualified.has(n) && _refsName(node, n)) disqualified.add(n)
|
|
920
|
+
}
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Property access `name.prop` / `name?.prop`. Bare property reads only —
|
|
925
|
+
// method calls reach here via the `()` handler below (which intercepts
|
|
926
|
+
// before recursing into its callee).
|
|
927
|
+
if ((op === '.' || op === '?.') && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
928
|
+
if (!_TYPED_SAFE_PROPS.has(node[2])) disqualified.add(node[1])
|
|
929
|
+
return // node[2] is the property name (string), not an expression — done
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Method or function call. Two shapes carry the candidate:
|
|
933
|
+
// `name.method(args)` — receiver at node[1][1]; method whitelist gates.
|
|
934
|
+
// `f(…, name, …)` / `Array.isArray(name)` — name appears as a plain arg.
|
|
935
|
+
if (op === '()') {
|
|
936
|
+
const callee = node[1]
|
|
937
|
+
// Method call on a candidate receiver.
|
|
938
|
+
if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
|
|
939
|
+
typeof callee[1] === 'string' && candidates.has(callee[1])) {
|
|
940
|
+
if (!_TYPED_SAFE_METHODS.has(callee[2])) disqualified.add(callee[1])
|
|
941
|
+
// Walk method args (skip the receiver — already validated above).
|
|
942
|
+
for (let i = 2; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
943
|
+
return
|
|
944
|
+
}
|
|
945
|
+
// Array.isArray flips true→false under promotion.
|
|
946
|
+
if (callee === 'Array.isArray') {
|
|
947
|
+
const raw = node[2]
|
|
948
|
+
const list = raw == null ? [] : (Array.isArray(raw) && raw[0] === ',') ? raw.slice(1) : [raw]
|
|
949
|
+
for (const a of list) {
|
|
950
|
+
if (typeof a === 'string' && candidates.has(a)) disqualified.add(a)
|
|
951
|
+
else _disqualifyPromotion(a, candidates, disqualified, initSet)
|
|
952
|
+
}
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
// Fall through to generic recursion — `name` as a plain arg will hit the
|
|
956
|
+
// bare-name leaf above and disqualify.
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Index read `name[k]` — read access is TYPED-safe. Walk the key in case
|
|
960
|
+
// it contains references to other candidate names.
|
|
961
|
+
if (op === '[]' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
962
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Element write: `['=', ['[]', name, k], v]` (and compound forms).
|
|
967
|
+
// V1 stays read-only after init — element writes would silently truncate.
|
|
968
|
+
if (ASSIGN_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' &&
|
|
969
|
+
typeof node[1][1] === 'string' && candidates.has(node[1][1])) {
|
|
970
|
+
disqualified.add(node[1][1])
|
|
971
|
+
_disqualifyPromotion(node[1][2], candidates, disqualified, initSet)
|
|
972
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Whole-binding reassign: `name = …` / `name += …` / etc.
|
|
977
|
+
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
978
|
+
disqualified.add(node[1])
|
|
979
|
+
_disqualifyPromotion(node[2], candidates, disqualified, initSet)
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Pre/post-increment on var or element.
|
|
984
|
+
if (op === '++' || op === '--') {
|
|
985
|
+
const t = node[1]
|
|
986
|
+
if (typeof t === 'string' && candidates.has(t)) { disqualified.add(t); return }
|
|
987
|
+
if (Array.isArray(t) && t[0] === '[]' && typeof t[1] === 'string' && candidates.has(t[1])) {
|
|
988
|
+
disqualified.add(t[1])
|
|
989
|
+
_disqualifyPromotion(t[2], candidates, disqualified, initSet)
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// `let`/`const` declaration — the candidate's own init lands here via the
|
|
995
|
+
// initSet branch at the top. Any *other* decl that names a candidate is a
|
|
996
|
+
// shadow (impossible after jz hoists, but defensive) and disqualifies.
|
|
997
|
+
if (op === 'let' || op === 'const') {
|
|
998
|
+
for (let i = 1; i < node.length; i++) {
|
|
999
|
+
const d = node[i]
|
|
1000
|
+
if (typeof d === 'string' && candidates.has(d)) disqualified.add(d)
|
|
1001
|
+
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' &&
|
|
1002
|
+
candidates.has(d[1]) && !initSet.has(d)) {
|
|
1003
|
+
disqualified.add(d[1])
|
|
1004
|
+
_disqualifyPromotion(d[2], candidates, disqualified, initSet)
|
|
1005
|
+
} else {
|
|
1006
|
+
_disqualifyPromotion(d, candidates, disqualified, initSet)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Spread `…name` — could be in a call, a `[`, or a destructure target.
|
|
1013
|
+
if (op === '...' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
1014
|
+
disqualified.add(node[1])
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// for-of / for-in iteration: receiver position is `node[2]` (a bare name
|
|
1019
|
+
// there would otherwise trigger escape). TYPED supports iteration, so
|
|
1020
|
+
// allow the receiver but walk the body for other refs.
|
|
1021
|
+
if (op === 'for-of' || op === 'for-in') {
|
|
1022
|
+
// Walk decl (node[1]), iter (node[2]), body (node[3]); receiver as bare
|
|
1023
|
+
// name is fine — only the body matters for further refs to the same name
|
|
1024
|
+
// (but body refs would shadow or escape, which other rules catch).
|
|
1025
|
+
for (let i = 1; i < node.length; i++) {
|
|
1026
|
+
const child = node[i]
|
|
1027
|
+
if (i === 2 && typeof child === 'string' && candidates.has(child)) continue
|
|
1028
|
+
_disqualifyPromotion(child, candidates, disqualified, initSet)
|
|
1029
|
+
}
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Generic — recurse into children. Bare-name refs at unhandled positions
|
|
1034
|
+
// hit the string-leaf branch above and disqualify on contact.
|
|
1035
|
+
for (let i = 1; i < node.length; i++) _disqualifyPromotion(node[i], candidates, disqualified, initSet)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Walk `body` to collect every `let X = [intLit, …]` candidate. Each entry
|
|
1039
|
+
// carries the exact init-decl AST node so the disqualifier can skip the
|
|
1040
|
+
// binding's own LHS reference (which would otherwise look like a reassign).
|
|
1041
|
+
const _collectIntArrayCandidates = (node, candidates) => {
|
|
1042
|
+
if (!Array.isArray(node)) return
|
|
1043
|
+
const op = node[0]
|
|
1044
|
+
if (op === '=>') return
|
|
1045
|
+
if (op === 'let' || op === 'const') {
|
|
1046
|
+
for (let i = 1; i < node.length; i++) {
|
|
1047
|
+
const d = node[i]
|
|
1048
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1049
|
+
const elems = _intArrayLitElems(d[2])
|
|
1050
|
+
if (elems == null) continue
|
|
1051
|
+
// jz hoists `let` to function scope — duplicate candidate-name collisions
|
|
1052
|
+
// shouldn't happen, but if they do the second wins (disqualifyPromotion
|
|
1053
|
+
// will mark both via the shadow rule).
|
|
1054
|
+
candidates.set(d[1], { initDecl: d, elems })
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
for (let i = 1; i < node.length; i++) _collectIntArrayCandidates(node[i], candidates)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Rewrite `let name = [...]` → `let name = new Int32Array([...])` for every
|
|
1061
|
+
// validated candidate. Preserves the original element AST nodes so the
|
|
1062
|
+
// downstream `Int32Array.from` lowering picks up its existing static-data
|
|
1063
|
+
// segment / per-element-store fast paths.
|
|
1064
|
+
const _rewritePromoted = (node, validated, initSet) => {
|
|
1065
|
+
if (!Array.isArray(node)) return { node, changed: false }
|
|
1066
|
+
const op = node[0]
|
|
1067
|
+
if (op === '=>') {
|
|
1068
|
+
// Closures don't reach validated bindings (we disqualified on capture).
|
|
1069
|
+
// Still recurse — a nested closure may itself hold a promotable.
|
|
1070
|
+
let changed = false
|
|
1071
|
+
const out = [op]
|
|
1072
|
+
for (let i = 1; i < node.length; i++) {
|
|
1073
|
+
const r = _rewritePromoted(node[i], validated, initSet)
|
|
1074
|
+
if (r.changed) changed = true
|
|
1075
|
+
out.push(r.node)
|
|
1076
|
+
}
|
|
1077
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
1078
|
+
}
|
|
1079
|
+
let changed = false
|
|
1080
|
+
const out = [op]
|
|
1081
|
+
for (let i = 1; i < node.length; i++) {
|
|
1082
|
+
const child = node[i]
|
|
1083
|
+
if ((op === 'let' || op === 'const') && Array.isArray(child) && child[0] === '=' &&
|
|
1084
|
+
typeof child[1] === 'string' && validated.has(child[1]) && initSet.has(child)) {
|
|
1085
|
+
const newRhs = ['()', 'new.Int32Array', child[2]]
|
|
1086
|
+
out.push(['=', child[1], newRhs])
|
|
1087
|
+
changed = true
|
|
1088
|
+
continue
|
|
1089
|
+
}
|
|
1090
|
+
const r = _rewritePromoted(child, validated, initSet)
|
|
1091
|
+
if (r.changed) changed = true
|
|
1092
|
+
out.push(r.node)
|
|
1093
|
+
}
|
|
1094
|
+
return changed ? { node: out, changed: true } : { node, changed: false }
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const promoteIntArrayLiteralsInBody = (body) => {
|
|
1098
|
+
const candidates = new Map()
|
|
1099
|
+
_collectIntArrayCandidates(body, candidates)
|
|
1100
|
+
if (!candidates.size) return { node: body, changed: false }
|
|
1101
|
+
const initSet = new Set()
|
|
1102
|
+
for (const { initDecl } of candidates.values()) initSet.add(initDecl)
|
|
1103
|
+
const disqualified = new Set()
|
|
1104
|
+
_disqualifyPromotion(body, candidates, disqualified, initSet)
|
|
1105
|
+
const validated = new Set()
|
|
1106
|
+
for (const name of candidates.keys()) if (!disqualified.has(name)) validated.add(name)
|
|
1107
|
+
if (!validated.size) return { node: body, changed: false }
|
|
1108
|
+
return _rewritePromoted(body, validated, initSet)
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// On the first successful promotion, pull in the typedarray module so the
|
|
1112
|
+
// emitted `new.Int32Array` callee has a registered emitter. Calling
|
|
1113
|
+
// `includeModule('typedarray')` on a no-op (already loaded) is cheap.
|
|
1114
|
+
const promoteIntArrayLiterals = () => {
|
|
1115
|
+
let changed = false
|
|
1116
|
+
for (const func of ctx.func.list) {
|
|
1117
|
+
if (!func.body || func.raw) continue
|
|
1118
|
+
const r = promoteIntArrayLiteralsInBody(func.body)
|
|
1119
|
+
if (r.changed) {
|
|
1120
|
+
func.body = r.node
|
|
1121
|
+
invalidateLocalsCache(func.body)
|
|
1122
|
+
if (!changed) includeModule('typedarray')
|
|
1123
|
+
changed = true
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return changed
|
|
1127
|
+
}
|
|
1128
|
+
|
|
807
1129
|
const scalarizeFunctionObjectLiterals = () => {
|
|
808
1130
|
let changed = false
|
|
809
1131
|
for (const func of ctx.func.list) {
|
|
@@ -1018,13 +1340,20 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1018
1340
|
}
|
|
1019
1341
|
|
|
1020
1342
|
const candidates = new Map()
|
|
1343
|
+
// Forwarders — a candidate whose body calls one of its own parameters.
|
|
1344
|
+
// Inlining one replaces that parameter with the call-site argument; when the
|
|
1345
|
+
// argument is a known function name the resulting indirect call collapses to
|
|
1346
|
+
// a direct `call` (devirtualization).
|
|
1347
|
+
const forwarders = new Set()
|
|
1021
1348
|
for (const func of ctx.func.list) {
|
|
1022
1349
|
if (func.exported || func.raw || !func.body || func.rest || programFacts.valueUsed.has(func.name)) continue
|
|
1023
1350
|
if (func.defaults && Object.keys(func.defaults).length) continue
|
|
1024
1351
|
const sites = sitesByCallee.get(func.name)
|
|
1025
1352
|
const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
|
|
1026
1353
|
const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
|
|
1027
|
-
|
|
1354
|
+
const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
|
|
1355
|
+
const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
|
|
1356
|
+
if (!sites || sites.length < 1 || (!isTinyLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
|
|
1028
1357
|
const stmts = blockStmts(func.body)
|
|
1029
1358
|
// Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
|
|
1030
1359
|
// return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
|
|
@@ -1043,9 +1372,8 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1043
1372
|
// The leaf branch catches helpers like `isAlpha(c) => (c>=65 && c<=90) || …`
|
|
1044
1373
|
// that get hammered from a hot caller's loop — replacing the call with its
|
|
1045
1374
|
// body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
|
|
1046
|
-
const hasLoop = scanBody(func.body, n => LOOP_OPS.has(n[0]))
|
|
1047
1375
|
if (!hasLoop) {
|
|
1048
|
-
if (scanBody(func.body, n => n[0] === '()')) continue
|
|
1376
|
+
if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
|
|
1049
1377
|
if (nodeSize(func.body) > 30) continue
|
|
1050
1378
|
}
|
|
1051
1379
|
if (scanBody(func.body, n => n[0] === '()' && n[1] === func.name)) continue
|
|
@@ -1061,6 +1389,9 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1061
1389
|
// stays at generic f64 ABI with __typed_idx dispatch instead of i32 + f64.load.
|
|
1062
1390
|
// Keeping the factory as a callable function preserves the call-site type fact.
|
|
1063
1391
|
if (scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('new.'))) continue
|
|
1392
|
+
const paramNames = new Set((func.sig?.params || []).map(p => p.name))
|
|
1393
|
+
if (paramNames.size && scanBody(func.body, n => n[0] === '()' && typeof n[1] === 'string' && paramNames.has(n[1])))
|
|
1394
|
+
forwarders.add(func.name)
|
|
1064
1395
|
candidates.set(func.name, func)
|
|
1065
1396
|
}
|
|
1066
1397
|
if (!candidates.size) return false
|
|
@@ -1077,10 +1408,12 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1077
1408
|
const exportedCandidates = new Map()
|
|
1078
1409
|
for (const [name, func] of candidates) {
|
|
1079
1410
|
const sites = sitesByCallee.get(name)
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1411
|
+
const fixedSiteExported = hasFixedTypedArraySites(func, sites) &&
|
|
1412
|
+
!sites.some(site => site.callerFunc?.exported && site.callerFunc.body && containsNode(site.callerFunc.body, site.node))
|
|
1413
|
+
// Forwarders cross into an exported caller too: the tier-up rationale that
|
|
1414
|
+
// keeps candidates out of exports concerns relocated loop kernels, not
|
|
1415
|
+
// these tiny leaves — and inlining one devirtualizes a closure dispatch.
|
|
1416
|
+
if (fixedSiteExported || forwarders.has(name)) exportedCandidates.set(name, func)
|
|
1084
1417
|
}
|
|
1085
1418
|
for (const func of ctx.func.list) {
|
|
1086
1419
|
if (!func.body || func.raw) continue
|
|
@@ -1093,7 +1426,16 @@ const inlineHotInternalCalls = (programFacts, ast) => {
|
|
|
1093
1426
|
// caller's heap arrays.
|
|
1094
1427
|
const activeCandidates = func.exported ? exportedCandidates : candidates
|
|
1095
1428
|
if (func.exported && !activeCandidates.size) continue
|
|
1096
|
-
|
|
1429
|
+
// Expression-bodied arrows (`() => expr`) have func.body as the return
|
|
1430
|
+
// value itself — never a `{}` block. inlineInStmt treats its argument as a
|
|
1431
|
+
// statement (discards the return value of any top-level candidate call),
|
|
1432
|
+
// which would turn `() => x()` into an empty block and lose the result.
|
|
1433
|
+
// Route those through inlineInExpr so the call is replaced by the inlined
|
|
1434
|
+
// value expression instead.
|
|
1435
|
+
const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
|
|
1436
|
+
const r = isExprBody
|
|
1437
|
+
? inlineInExpr(func.body, activeCandidates)
|
|
1438
|
+
: inlineInStmt(func.body, activeCandidates)
|
|
1097
1439
|
let body = r.changed ? r.node : func.body
|
|
1098
1440
|
let bodyChanged = r.changed
|
|
1099
1441
|
if (!func.exported && exprOnlyCandidates.size) {
|
|
@@ -1341,28 +1683,86 @@ const specializeFixedRestCalls = (programFacts) => {
|
|
|
1341
1683
|
return changed
|
|
1342
1684
|
}
|
|
1343
1685
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1686
|
+
// `scanGlobalValueFacts` was deleted — prepare's depth-0 catch (calling
|
|
1687
|
+
// `recordGlobalRep` from src/infer.js) is the authoritative pass and a
|
|
1688
|
+
// strict superset of what this top-level walker observed.
|
|
1689
|
+
|
|
1690
|
+
// Flow-insensitive type inference for module-level `let` bindings whose
|
|
1691
|
+
// initial RHS doesn't pin a type (most often `let mem;` followed later by
|
|
1692
|
+
// `mem = new TypedArray(...)` inside an init function). Without this the
|
|
1693
|
+
// read site has to runtime-check the NaN-box tag on every access — game-of-life's
|
|
1694
|
+
// inner step does that 9× per cell, blowing up the hot loop. We union RHS types
|
|
1695
|
+
// across every assignment (initial decl + every `name = …` in any function);
|
|
1696
|
+
// if every observed RHS is either a typed-array ctor of the same kind, a known
|
|
1697
|
+
// VAL.TYPED binding of the same ctor, or null/undefined, the binding is
|
|
1698
|
+
// monomorphically VAL.TYPED. Anything else (literal number, non-typed call,
|
|
1699
|
+
// mixed ctors) clears the candidacy, keeping the read site polymorphic.
|
|
1700
|
+
const inferModuleLetTypes = (ast) => {
|
|
1701
|
+
if (!ctx.scope.userGlobals) return
|
|
1702
|
+
// candidates: name → { ctor: string|null, valid: true } | { valid: false }
|
|
1703
|
+
// valid=true with ctor=null means "still no positive evidence"; we promote
|
|
1704
|
+
// only when ctor is non-null at the end. Assignments to nullish (undef/null)
|
|
1705
|
+
// don't change ctor — they're consistent with any typed-array value.
|
|
1706
|
+
const seen = new Map()
|
|
1707
|
+
for (const name of ctx.scope.userGlobals) seen.set(name, { ctor: null, valid: true })
|
|
1708
|
+
|
|
1709
|
+
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
1710
|
+
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
1711
|
+
|
|
1712
|
+
const observe = (name, rhs) => {
|
|
1713
|
+
const c = seen.get(name)
|
|
1714
|
+
if (!c || !c.valid) return
|
|
1715
|
+
if (isNullishLit(rhs)) return
|
|
1716
|
+
// Resolve typed-array ctor from `new TypedArrayCtor(...)`, ternary of typed,
|
|
1717
|
+
// or a reference to a name we already know is typed.
|
|
1718
|
+
let ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
|
|
1719
|
+
if (ctor === MIXED_CTORS) { c.valid = false; return }
|
|
1720
|
+
if (!ctor && typeof rhs === 'string') {
|
|
1721
|
+
if (ctx.scope.globalValTypes?.get(rhs) === VAL.TYPED)
|
|
1722
|
+
ctor = ctx.scope.globalTypedElem?.get(rhs) ?? null
|
|
1723
|
+
}
|
|
1724
|
+
if (!ctor) { c.valid = false; return }
|
|
1725
|
+
if (c.ctor && c.ctor !== ctor) { c.valid = false; return }
|
|
1726
|
+
c.ctor = ctor
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const walk = (node) => {
|
|
1730
|
+
if (!Array.isArray(node)) return
|
|
1731
|
+
const op = node[0]
|
|
1732
|
+
if (op === '=' && typeof node[1] === 'string' && seen.has(node[1])) observe(node[1], node[2])
|
|
1733
|
+
if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
1734
|
+
for (let i = 1; i < node.length; i++) {
|
|
1735
|
+
const d = node[i]
|
|
1736
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && seen.has(d[1]))
|
|
1737
|
+
observe(d[1], d[2])
|
|
1361
1738
|
}
|
|
1362
1739
|
}
|
|
1740
|
+
// Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
|
|
1741
|
+
// the typed-array kind — invalidate.
|
|
1742
|
+
if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && seen.has(node[1])) {
|
|
1743
|
+
const c = seen.get(node[1])
|
|
1744
|
+
if (c) c.valid = false
|
|
1745
|
+
}
|
|
1746
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
1747
|
+
}
|
|
1748
|
+
walk(ast)
|
|
1749
|
+
for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
|
|
1750
|
+
|
|
1751
|
+
for (const [name, c] of seen) {
|
|
1752
|
+
if (!c.valid || !c.ctor) continue
|
|
1753
|
+
if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
|
|
1754
|
+
;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
|
|
1755
|
+
;(ctx.scope.globalTypedElem ||= new Map()).set(name, c.ctor)
|
|
1363
1756
|
}
|
|
1364
1757
|
}
|
|
1365
1758
|
|
|
1759
|
+
// `MIXED_CTORS` and `ternaryCtorOfRhs` are not exported from analyze — re-derive
|
|
1760
|
+
// the sentinel locally and look up the ctor through `typedElemCtor` alone.
|
|
1761
|
+
// `ternaryCtorOfRhs` is purely for ternary RHSs (e.g. `cond ? new Int32Array(N) : new Int32Array(M)`),
|
|
1762
|
+
// which game-of-life-style sources don't use; falling back to typedElemCtor is fine.
|
|
1763
|
+
const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
1764
|
+
const ternaryCtorOfRhs = () => null
|
|
1765
|
+
|
|
1366
1766
|
const unboxConstTypedGlobals = () => {
|
|
1367
1767
|
if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
|
|
1368
1768
|
for (const [name, ctor] of ctx.scope.globalTypedElem) {
|
|
@@ -1378,6 +1778,254 @@ const unboxConstTypedGlobals = () => {
|
|
|
1378
1778
|
}
|
|
1379
1779
|
}
|
|
1380
1780
|
|
|
1781
|
+
/**
|
|
1782
|
+
* Function-namespace scalar replacement + devirtualization.
|
|
1783
|
+
*
|
|
1784
|
+
* A property of a user function compiles, by default, as a dynamic object: each
|
|
1785
|
+
* `f.prop` write is a `__dyn_set` into a closure-keyed hash side-table, each
|
|
1786
|
+
* read a `__dyn_get`. But a function's property table can never be observed by
|
|
1787
|
+
* the host (the host receives only the callable; the table lives in jz linear
|
|
1788
|
+
* memory), so jz sees every `f.prop` site — the slot is a closed, fully-known
|
|
1789
|
+
* cell. Per property of a non-escaping namespace:
|
|
1790
|
+
*
|
|
1791
|
+
* - reassigned (`multiProp`) slot → dissolve into a plain f64 module global:
|
|
1792
|
+
* `__dyn_get/__dyn_set` → `global.get/global.set`. The indirect call stays
|
|
1793
|
+
* (a genuinely reassigned function pointer needs `call_indirect`). Pure
|
|
1794
|
+
* storage relocation: the global inits to `UNDEF_WAT`, exactly mirroring
|
|
1795
|
+
* "key never set → __dyn_get yields undefined".
|
|
1796
|
+
* - written once to its lifted `$f$prop` function and only ever *called*
|
|
1797
|
+
* (never read as a value) → the `__dyn_set` is dead: emit already lowers
|
|
1798
|
+
* `f.prop()` to a direct `call $f$prop`. Drop the write entirely.
|
|
1799
|
+
*
|
|
1800
|
+
* Disqualified namespaces (`f` escapes as a bare value / is computed-indexed —
|
|
1801
|
+
* an alias could reach the table) keep the dynamic path. Together these can
|
|
1802
|
+
* eliminate the `__dyn_*` machinery from a namespace-only program outright.
|
|
1803
|
+
*/
|
|
1804
|
+
const flattenFuncNamespaces = (ast) => {
|
|
1805
|
+
const names = ctx.func.names
|
|
1806
|
+
if (!names?.size) return false
|
|
1807
|
+
// Cheap structural gate: a flattenable namespace exists only if some lifted
|
|
1808
|
+
// `f$prop` name's `f` is itself a function (prepare lifts every `f.prop =
|
|
1809
|
+
// arrow` — multiProp slots included). The base `f` may itself carry a module
|
|
1810
|
+
// prefix (`mod$f`), so scan every `$` boundary, not just the first; a
|
|
1811
|
+
// populated `multiProp` registry is itself a direct namespace witness.
|
|
1812
|
+
let hasNs = ctx.func.multiProp.size > 0
|
|
1813
|
+
if (!hasNs) outer: for (const n of names) {
|
|
1814
|
+
for (let i = n.indexOf('$'); i > 0; i = n.indexOf('$', i + 1))
|
|
1815
|
+
if (names.has(n.slice(0, i))) { hasNs = true; break outer }
|
|
1816
|
+
}
|
|
1817
|
+
if (!hasNs) return false
|
|
1818
|
+
const ns = analyzeFuncNamespaces(ast)
|
|
1819
|
+
if (!ns.size) return false
|
|
1820
|
+
// f → Map<prop, decision>; decision is { global } (SROA) or { drop } (dead
|
|
1821
|
+
// write to an only-called single-write slot).
|
|
1822
|
+
const flat = new Map()
|
|
1823
|
+
for (const [f, info] of ns) {
|
|
1824
|
+
if (info.disq) continue
|
|
1825
|
+
let decide
|
|
1826
|
+
const plan = (prop, d) => { if (!decide) flat.set(f, decide = new Map()); decide.set(prop, d) }
|
|
1827
|
+
for (const prop of info.props) {
|
|
1828
|
+
if (ctx.func.multiProp.has(`${f}.${prop}`)) { plan(prop, { global: `${f}${T}${prop}` }); continue }
|
|
1829
|
+
const w = info.writes.get(prop)
|
|
1830
|
+
// Single write of the lifted `$f$prop`, never read as a value → drop it.
|
|
1831
|
+
if (w && w.length === 1 && w[0].atInit && w[0].rhs === `${f}$${prop}` && !info.valRead.has(prop))
|
|
1832
|
+
plan(prop, { drop: true })
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (!flat.size) return false
|
|
1836
|
+
for (const decide of flat.values())
|
|
1837
|
+
for (const d of decide.values())
|
|
1838
|
+
if (d.global && !ctx.scope.globals.has(d.global)) {
|
|
1839
|
+
ctx.scope.globals.set(d.global, `(global $${d.global} (mut f64) ${UNDEF_WAT})`)
|
|
1840
|
+
ctx.scope.globalTypes.set(d.global, 'f64')
|
|
1841
|
+
}
|
|
1842
|
+
const decisionFor = (obj, prop) =>
|
|
1843
|
+
typeof obj === 'string' && typeof prop === 'string' && flat.has(obj)
|
|
1844
|
+
? flat.get(obj).get(prop) : undefined
|
|
1845
|
+
const isEmptySeq = (n) => Array.isArray(n) && n.length === 1 && n[0] === ';'
|
|
1846
|
+
const rewrite = (node) => {
|
|
1847
|
+
if (!Array.isArray(node)) return node
|
|
1848
|
+
const op = node[0]
|
|
1849
|
+
if (op === '.' || op === '?.') {
|
|
1850
|
+
const d = decisionFor(node[1], node[2])
|
|
1851
|
+
if (d?.global) return d.global // drop-decisions leave reads/calls alone
|
|
1852
|
+
}
|
|
1853
|
+
if (op === '=' && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.')) {
|
|
1854
|
+
const d = decisionFor(node[1][1], node[1][2])
|
|
1855
|
+
if (d?.global) return ['=', d.global, rewrite(node[2])]
|
|
1856
|
+
if (d?.drop) return [';'] // dead write — emit nothing
|
|
1857
|
+
}
|
|
1858
|
+
const out = [op]
|
|
1859
|
+
// Filter dropped writes out of statement sequences (an empty `[';']` left in
|
|
1860
|
+
// a body would lower to an unrenderable node).
|
|
1861
|
+
for (let i = 1; i < node.length; i++) {
|
|
1862
|
+
const c = rewrite(node[i])
|
|
1863
|
+
if (op === ';' && isEmptySeq(c)) continue
|
|
1864
|
+
out.push(c)
|
|
1865
|
+
}
|
|
1866
|
+
return out
|
|
1867
|
+
}
|
|
1868
|
+
const newAst = rewrite(ast)
|
|
1869
|
+
ast.length = 0
|
|
1870
|
+
for (let i = 0; i < newAst.length; i++) ast.push(newAst[i])
|
|
1871
|
+
for (const fn of ctx.func.list)
|
|
1872
|
+
if (fn.body && !fn.raw) fn.body = rewrite(fn.body)
|
|
1873
|
+
// The defining `f.prop = …` writes live in moduleInits for bundled programs —
|
|
1874
|
+
// rewrite them too, or reads would resolve to an unwritten global.
|
|
1875
|
+
if (ctx.module.moduleInits)
|
|
1876
|
+
for (let i = 0; i < ctx.module.moduleInits.length; i++)
|
|
1877
|
+
ctx.module.moduleInits[i] = rewrite(ctx.module.moduleInits[i])
|
|
1878
|
+
return true
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Closure devirtualization.
|
|
1883
|
+
*
|
|
1884
|
+
* `flattenFuncNamespaces` dissolves a reassigned `f.prop` function slot into a
|
|
1885
|
+
* module global, but the call through it stays a `call_indirect` on a
|
|
1886
|
+
* `global.get`, dispatched via an ABI-adapting trampoline. When that global is
|
|
1887
|
+
* written *only* by unconditional module-init assignments it holds, for the
|
|
1888
|
+
* entire post-init program, one statically-known function — so every call
|
|
1889
|
+
* through it collapses to a direct `call`: no table lookup, no trampoline, no
|
|
1890
|
+
* 8-wide padding ABI, no closure type guard.
|
|
1891
|
+
*
|
|
1892
|
+
* A global G qualifies iff:
|
|
1893
|
+
* 1. every assignment to G is an unconditional module-init statement — none in
|
|
1894
|
+
* a function body, none nested inside init control flow;
|
|
1895
|
+
* 2. G's final init value resolves (through global aliases) to a top-level
|
|
1896
|
+
* function F;
|
|
1897
|
+
* 3. G is never *called* by module-init code, nor by any function reachable
|
|
1898
|
+
* from it — so every call site runs strictly post-init, where G ≡ F.
|
|
1899
|
+
* Devirt then only swaps an indirect call for a direct call to the very same
|
|
1900
|
+
* callee: it cannot change behavior, only drop dispatch overhead. The result is
|
|
1901
|
+
* recorded in `ctx.func.globalDevirt` (`Map<global, fn>`) and consumed by emit.
|
|
1902
|
+
*/
|
|
1903
|
+
const devirtGlobalCalls = (ast) => {
|
|
1904
|
+
const fnNames = ctx.func.names
|
|
1905
|
+
if (!fnNames?.size || !ctx.scope.globals?.size) return
|
|
1906
|
+
|
|
1907
|
+
// Module-init statement stream, in execution order: moduleInits run first in
|
|
1908
|
+
// `$__start`, then the main module's top-level.
|
|
1909
|
+
const initStmts = []
|
|
1910
|
+
const flatten = (n) => {
|
|
1911
|
+
if (Array.isArray(n) && n[0] === ';') for (let i = 1; i < n.length; i++) flatten(n[i])
|
|
1912
|
+
else if (n != null) initStmts.push(n)
|
|
1913
|
+
}
|
|
1914
|
+
for (const mi of ctx.module.moduleInits || []) flatten(mi)
|
|
1915
|
+
flatten(ast)
|
|
1916
|
+
|
|
1917
|
+
const isGlobal = (s) => typeof s === 'string' && ctx.scope.globals.has(s)
|
|
1918
|
+
// `[target, rhs]` pairs for a `=` / `let` / `const` node assigning a global.
|
|
1919
|
+
const writesOf = (node) => {
|
|
1920
|
+
if (!Array.isArray(node)) return []
|
|
1921
|
+
if (node[0] === '=' && isGlobal(node[1])) return [[node[1], node[2]]]
|
|
1922
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
1923
|
+
const out = []
|
|
1924
|
+
for (let i = 1; i < node.length; i++) {
|
|
1925
|
+
const d = node[i]
|
|
1926
|
+
if (Array.isArray(d) && d[0] === '=' && isGlobal(d[1])) out.push([d[1], d[2]])
|
|
1927
|
+
}
|
|
1928
|
+
return out
|
|
1929
|
+
}
|
|
1930
|
+
return []
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// Poison a global assigned anywhere but an unconditional init statement — in a
|
|
1934
|
+
// function body, or nested in init control flow. Its value is then not a
|
|
1935
|
+
// fixed post-init constant.
|
|
1936
|
+
const poison = new Set()
|
|
1937
|
+
const scanWrites = (node, topInit) => {
|
|
1938
|
+
if (!Array.isArray(node)) return
|
|
1939
|
+
const op = node[0]
|
|
1940
|
+
if (op === 'let' || op === 'const') {
|
|
1941
|
+
// A declarator `=` is part of the declaration, not a nested assignment —
|
|
1942
|
+
// poison only when the declaration itself is non-top-level.
|
|
1943
|
+
for (let i = 1; i < node.length; i++) {
|
|
1944
|
+
const d = node[i]
|
|
1945
|
+
if (Array.isArray(d) && d[0] === '=') {
|
|
1946
|
+
if (!topInit && isGlobal(d[1])) poison.add(d[1])
|
|
1947
|
+
scanWrites(d[2], false)
|
|
1948
|
+
} else scanWrites(d, false)
|
|
1949
|
+
}
|
|
1950
|
+
return
|
|
1951
|
+
}
|
|
1952
|
+
if (op === '=') {
|
|
1953
|
+
if (!topInit && isGlobal(node[1])) poison.add(node[1])
|
|
1954
|
+
scanWrites(node[1], false)
|
|
1955
|
+
scanWrites(node[2], false)
|
|
1956
|
+
return
|
|
1957
|
+
}
|
|
1958
|
+
for (let i = 1; i < node.length; i++) scanWrites(node[i], false)
|
|
1959
|
+
}
|
|
1960
|
+
for (const stmt of initStmts) scanWrites(stmt, true)
|
|
1961
|
+
for (const fn of ctx.func.map.values())
|
|
1962
|
+
if (fn.body && !fn.raw) scanWrites(fn.body, false)
|
|
1963
|
+
|
|
1964
|
+
// Resolve each global's value by a linear pass over init in execution order.
|
|
1965
|
+
const env = new Map()
|
|
1966
|
+
const evalFn = (rhs) =>
|
|
1967
|
+
typeof rhs !== 'string' ? null
|
|
1968
|
+
: fnNames.has(rhs) ? rhs
|
|
1969
|
+
: env.has(rhs) ? env.get(rhs)
|
|
1970
|
+
: null
|
|
1971
|
+
for (const stmt of initStmts)
|
|
1972
|
+
for (const [g, rhs] of writesOf(stmt)) env.set(g, evalFn(rhs))
|
|
1973
|
+
|
|
1974
|
+
const devirt = new Map()
|
|
1975
|
+
for (const [g, fn] of env)
|
|
1976
|
+
if (fn && fnNames.has(fn) && !poison.has(g)) devirt.set(g, fn)
|
|
1977
|
+
if (!devirt.size) return
|
|
1978
|
+
|
|
1979
|
+
// Condition 3: a call through G that runs *during* init would see an
|
|
1980
|
+
// intermediate value. Drop any candidate G called by init code, or by a
|
|
1981
|
+
// function reachable from it.
|
|
1982
|
+
//
|
|
1983
|
+
// `walkStraightLine` follows only straight-line execution: a nested `=>`
|
|
1984
|
+
// literal is a closure *constructed* here, not run here, so its body is
|
|
1985
|
+
// skipped — an IIFE callee `(=> …)()` is the one exception, its body does
|
|
1986
|
+
// run. This is what keeps operator-registration init (`binary('+', 11)`
|
|
1987
|
+
// builds, but does not invoke, a parselet closure) from dragging the parser
|
|
1988
|
+
// into the init-reachable set, and keeps a wrapper body's `space()` call —
|
|
1989
|
+
// which fires at parse time — from counting as an init call. (Soundness
|
|
1990
|
+
// rests on a closure constructed during init not also being invoked during
|
|
1991
|
+
// init: true of function-slot wrappers, which are registered then called at
|
|
1992
|
+
// use time.)
|
|
1993
|
+
const walkStraightLine = (node, onCall) => {
|
|
1994
|
+
if (!Array.isArray(node)) return
|
|
1995
|
+
const op = node[0]
|
|
1996
|
+
if (op === '()') {
|
|
1997
|
+
onCall(node[1])
|
|
1998
|
+
if (Array.isArray(node[1]) && node[1][0] === '=>') walkStraightLine(node[1][2], onCall)
|
|
1999
|
+
for (let i = 2; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
2000
|
+
return
|
|
2001
|
+
}
|
|
2002
|
+
if (op === '=>' || op === 'function') return
|
|
2003
|
+
for (let i = 1; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
2004
|
+
}
|
|
2005
|
+
const reachable = new Set()
|
|
2006
|
+
const queue = []
|
|
2007
|
+
const seedCalls = (node) => walkStraightLine(node, (c) => {
|
|
2008
|
+
if (typeof c === 'string' && fnNames.has(c)) queue.push(c)
|
|
2009
|
+
})
|
|
2010
|
+
for (const s of initStmts) seedCalls(s)
|
|
2011
|
+
while (queue.length) {
|
|
2012
|
+
const f = queue.pop()
|
|
2013
|
+
if (reachable.has(f)) continue
|
|
2014
|
+
reachable.add(f)
|
|
2015
|
+
const fn = ctx.func.map.get(f)
|
|
2016
|
+
if (fn?.body && !fn.raw) seedCalls(fn.body)
|
|
2017
|
+
}
|
|
2018
|
+
const calledInInit = new Set()
|
|
2019
|
+
const collectCalled = (node) => walkStraightLine(node, (c) => {
|
|
2020
|
+
if (devirt.has(c)) calledInInit.add(c)
|
|
2021
|
+
})
|
|
2022
|
+
for (const s of initStmts) collectCalled(s)
|
|
2023
|
+
for (const f of reachable) { const fn = ctx.func.map.get(f); if (fn?.body) collectCalled(fn.body) }
|
|
2024
|
+
for (const g of calledInInit) devirt.delete(g)
|
|
2025
|
+
|
|
2026
|
+
if (devirt.size) ctx.func.globalDevirt = devirt
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1381
2029
|
const materializeAutoBoxSchemas = (programFacts) => {
|
|
1382
2030
|
if (!ctx.schema.register) return
|
|
1383
2031
|
for (const [name, props] of programFacts.propMap) {
|
|
@@ -1434,10 +2082,17 @@ const canSkipWholeProgramNarrowing = (programFacts) =>
|
|
|
1434
2082
|
!ctx.closure.make
|
|
1435
2083
|
|
|
1436
2084
|
export default function plan(ast) {
|
|
1437
|
-
|
|
2085
|
+
inferModuleLetTypes(ast)
|
|
1438
2086
|
unboxConstTypedGlobals()
|
|
1439
2087
|
|
|
1440
2088
|
let programFacts = collectProgramFacts(ast)
|
|
2089
|
+
// Function-namespace SROA — dissolve reassigned `f.prop` slots into module
|
|
2090
|
+
// globals before inlining/narrowing, so all downstream passes see plain
|
|
2091
|
+
// globals instead of the dynamic property machinery.
|
|
2092
|
+
if (flattenFuncNamespaces(ast)) programFacts = collectProgramFacts(ast)
|
|
2093
|
+
// Devirtualize calls through init-constant function globals (closure
|
|
2094
|
+
// devirtualization) — must follow the SROA above, which creates the globals.
|
|
2095
|
+
devirtGlobalCalls(ast)
|
|
1441
2096
|
// The call-inlining family (`inlineHotInternalCalls` self-gates on `sourceInline`)
|
|
1442
2097
|
// is a pure speed optimization — the un-inlined calls emit correctly. Scalar
|
|
1443
2098
|
// replacement (`scalarize*`) is *not* gated on `sourceInline`: callers turn it on
|
|
@@ -1447,13 +2102,27 @@ export default function plan(ast) {
|
|
|
1447
2102
|
if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
|
|
1448
2103
|
if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
|
|
1449
2104
|
if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
|
|
2105
|
+
// Promotion runs AFTER literal scalarization (those that fully reduce to scalars
|
|
2106
|
+
// are gone) and BEFORE typed-array scalarization (so a freshly-promoted array's
|
|
2107
|
+
// fixed-length-typed-of-known-size variant could still participate in loop
|
|
2108
|
+
// unrolling — currently it can't, since promotion produces the `[...]`-arg
|
|
2109
|
+
// form rather than `new Int32Array(N)`, but the ordering keeps the door open).
|
|
2110
|
+
if (promoteIntArrayLiterals()) programFacts = collectProgramFacts(ast)
|
|
1450
2111
|
if (scalarizeFunctionTypedArrays(programFacts)) programFacts = collectProgramFacts(ast)
|
|
1451
2112
|
ctx.types.dynKeyVars = programFacts.dynVars
|
|
1452
2113
|
ctx.types.anyDynKey = programFacts.anyDyn
|
|
1453
2114
|
|
|
1454
2115
|
materializeAutoBoxSchemas(programFacts)
|
|
1455
2116
|
resolveClosureWidth(programFacts)
|
|
1456
|
-
if (canSkipWholeProgramNarrowing(programFacts))
|
|
2117
|
+
if (canSkipWholeProgramNarrowing(programFacts)) {
|
|
2118
|
+
// Phase J (jsstring boundary opt-in) is body-local and call-site-independent;
|
|
2119
|
+
// run it even when the rest of narrowing is skipped so simple `export let
|
|
2120
|
+
// f = (s) => s.length` still flips to externref. Likewise the boolean-result
|
|
2121
|
+
// fact, so `export let f = (a) => a > 2` boxes its boundary atom.
|
|
2122
|
+
applyJsstringBoundaryCarrierStandalone(programFacts)
|
|
2123
|
+
narrowBoolResults()
|
|
2124
|
+
return programFacts
|
|
2125
|
+
}
|
|
1457
2126
|
|
|
1458
2127
|
narrowSignatures(programFacts, ast)
|
|
1459
2128
|
specializeBimorphicTyped(programFacts)
|