optipack 0.0.0-stage → 1.0.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/BENCHMARKING.md +127 -0
- package/LICENSE +21 -0
- package/README.md +136 -2
- package/bin/optipack.js +12 -0
- package/example/manifest.json +27 -0
- package/example/scripts/drops.js +24 -0
- package/example/scripts/main.js +67 -0
- package/example/scripts/util.js +30 -0
- package/package.json +34 -4
- package/src/bench.js +231 -0
- package/src/cli.js +245 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -0
- package/src/harness/driver.js +216 -0
- package/src/harness/loader-hooks.mjs +63 -0
- package/src/harness/mock/@minecraft/common.mjs +17 -0
- package/src/harness/mock/@minecraft/debug-utilities.mjs +10 -0
- package/src/harness/mock/@minecraft/diagnostics.mjs +14 -0
- package/src/harness/mock/@minecraft/server-admin.mjs +14 -0
- package/src/harness/mock/@minecraft/server-editor.mjs +39 -0
- package/src/harness/mock/@minecraft/server-extras.mjs +538 -0
- package/src/harness/mock/@minecraft/server-gametest.mjs +22 -0
- package/src/harness/mock/@minecraft/server-graphics.mjs +21 -0
- package/src/harness/mock/@minecraft/server-net.mjs +15 -0
- package/src/harness/mock/@minecraft/server-ui.mjs +125 -0
- package/src/harness/mock/@minecraft/server.mjs +394 -0
- package/src/harness/mock/permissive.mjs +71 -0
- package/src/hotpath.js +158 -0
- package/src/lint.js +150 -0
- package/src/pack-walker.js +50 -0
- package/src/pass-utils.js +419 -0
- package/src/passes/b1-cache-getter-chains.js +406 -0
- package/src/passes/b2-closure-lift.js +316 -0
- package/src/passes/b3-string-build.js +167 -0
- package/src/passes/b4-dynprop-hoist.js +177 -0
- package/src/passes/b5-null-chain-flatten.js +137 -0
- package/src/passes/index.js +43 -0
- package/src/passes/r1-cross-tick-cache.js +192 -0
- package/src/passes/r2-json-fold.js +201 -0
- package/src/passes/r3-try-catch-strip.js +61 -0
- package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
- package/src/passes/s1-const-fold.js +130 -0
- package/src/passes/s2-dead-code.js +183 -0
- package/src/passes/s3-bool-simplify.js +82 -0
- package/src/passes/s4-for-of-to-indexed.js +140 -0
- package/src/passes/s5-array-methods-to-loops.js +357 -0
- package/src/passes/s6-destructure-to-direct.js +186 -0
- package/src/passes/s7-hoist-invariant-members.js +327 -0
- package/src/pipeline.js +186 -0
- package/src/pragma.js +115 -0
- package/src/report.js +98 -0
- package/test/array-methods-to-loops/expected.js +58 -0
- package/test/array-methods-to-loops/expects.json +6 -0
- package/test/array-methods-to-loops/input.js +23 -0
- package/test/array-methods-to-loops/scenario.json +22 -0
- package/test/array-methods-to-loops-bail/expected.js +10 -0
- package/test/array-methods-to-loops-bail/input.js +24 -0
- package/test/bool-simplify/expected.js +14 -0
- package/test/bool-simplify/expects.json +11 -0
- package/test/bool-simplify/input.js +18 -0
- package/test/cache-getter-chains/expected.js +20 -0
- package/test/cache-getter-chains/expects.json +11 -0
- package/test/cache-getter-chains/input.js +20 -0
- package/test/cache-getter-chains/scenario.json +35 -0
- package/test/closure-lift/expected.js +58 -0
- package/test/closure-lift/expects.json +1 -0
- package/test/closure-lift/input.js +50 -0
- package/test/closure-lift/scenario.json +9 -0
- package/test/const-fold/expected.js +12 -0
- package/test/const-fold/expects.json +1 -0
- package/test/const-fold/input.js +12 -0
- package/test/const-fold-bail/expected.js +7 -0
- package/test/const-fold-bail/expects.json +3 -0
- package/test/const-fold-bail/input.js +7 -0
- package/test/cross-tick-cache/config.json +1 -0
- package/test/cross-tick-cache/expected.js +38 -0
- package/test/cross-tick-cache/expects.json +6 -0
- package/test/cross-tick-cache/input.js +13 -0
- package/test/cross-tick-cache/scenario.json +33 -0
- package/test/dead-code/expected.js +14 -0
- package/test/dead-code/expects.json +5 -0
- package/test/dead-code/input.js +23 -0
- package/test/destructure-to-direct/expected.js +37 -0
- package/test/destructure-to-direct/expects.json +11 -0
- package/test/destructure-to-direct/input.js +28 -0
- package/test/destructure-to-direct/scenario.json +22 -0
- package/test/destructure-to-direct-bail/expected.js +36 -0
- package/test/destructure-to-direct-bail/expects.json +11 -0
- package/test/destructure-to-direct-bail/input.js +25 -0
- package/test/differential.test.mjs +267 -0
- package/test/dynprop-hoist/expected.js +24 -0
- package/test/dynprop-hoist/expects.json +11 -0
- package/test/dynprop-hoist/input.js +25 -0
- package/test/dynprop-hoist/scenario.json +24 -0
- package/test/example-report-snapshot.json +107 -0
- package/test/example.test.mjs +115 -0
- package/test/fetch-corpus.mjs +92 -0
- package/test/for-of-to-indexed/expected.js +59 -0
- package/test/for-of-to-indexed/expects.json +11 -0
- package/test/for-of-to-indexed/input.js +43 -0
- package/test/for-of-to-indexed-bail/expected.js +25 -0
- package/test/for-of-to-indexed-bail/expects.json +11 -0
- package/test/for-of-to-indexed-bail/input.js +29 -0
- package/test/gen-expected.mjs +33 -0
- package/test/getter-cache-cross-barrier/config.json +1 -0
- package/test/getter-cache-cross-barrier/expected.js +10 -0
- package/test/getter-cache-cross-barrier/expects.json +6 -0
- package/test/getter-cache-cross-barrier/input.js +11 -0
- package/test/getter-cache-cross-barrier/scenario.json +28 -0
- package/test/harness/fixture.js +117 -0
- package/test/hoist-invariant-members/expected.js +37 -0
- package/test/hoist-invariant-members/expects.json +11 -0
- package/test/hoist-invariant-members/input.js +34 -0
- package/test/hoist-invariant-members/scenario.json +35 -0
- package/test/hoist-invariant-members-bail/expected.js +19 -0
- package/test/hoist-invariant-members-bail/expects.json +5 -0
- package/test/hoist-invariant-members-bail/input.js +22 -0
- package/test/infra.test.mjs +270 -0
- package/test/json-fold/config.json +1 -0
- package/test/json-fold/expected.js +14 -0
- package/test/json-fold/expects.json +12 -0
- package/test/json-fold/input.js +17 -0
- package/test/json-fold-bail/config.json +1 -0
- package/test/json-fold-bail/expected.js +10 -0
- package/test/json-fold-bail/expects.json +5 -0
- package/test/json-fold-bail/input.js +14 -0
- package/test/null-chain-flatten/expected.js +22 -0
- package/test/null-chain-flatten/expects.json +5 -0
- package/test/null-chain-flatten/input.js +22 -0
- package/test/null-chain-flatten/scenario.json +24 -0
- package/test/server-ui-forms/expected.js +15 -0
- package/test/server-ui-forms/expects.json +1 -0
- package/test/server-ui-forms/input.js +25 -0
- package/test/server-ui-forms/scenario.json +1 -0
- package/test/string-build/expected.js +29 -0
- package/test/string-build/expects.json +5 -0
- package/test/string-build/input.js +29 -0
- package/test/string-build/scenario.json +27 -0
- package/test/string-build-bail/expected.js +10 -0
- package/test/string-build-bail/input.js +21 -0
- package/test/transforms.test.mjs +43 -0
- package/test/try-catch-strip/config.json +1 -0
- package/test/try-catch-strip/expected.js +57 -0
- package/test/try-catch-strip/expects.json +1 -0
- package/test/try-catch-strip/input.js +34 -0
- package/test/try-catch-strip/scenario.json +28 -0
- package/test/update-example-snapshot.mjs +19 -0
- package/test/update-mock-stubs.mjs +91 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B2 closure-lift — balanced level.
|
|
3
|
+
*
|
|
4
|
+
* Arrow / function expressions created lexically inside hot functions are
|
|
5
|
+
* re-allocated on every tick. When they capture nothing, only module-level
|
|
6
|
+
* identifiers/globals, or only `const`-bound primitives (inlined as module
|
|
7
|
+
* consts), they are lifted to module scope and their creation site becomes a
|
|
8
|
+
* plain identifier reference.
|
|
9
|
+
*
|
|
10
|
+
* Cell capture: when an arrow captures a per-invocation local that is never
|
|
11
|
+
* reassigned, it can still be lifted by routing the capture through a
|
|
12
|
+
* module-level cell (`let __opCellN;`) assigned at the arrow's creation point.
|
|
13
|
+
* This is sound only when the arrow can never outlive the cell assignment:
|
|
14
|
+
*
|
|
15
|
+
* - every reference of the arrow binding is a DIRECT synchronous call in the
|
|
16
|
+
* enclosing function (callee position; never passed as a value, registered,
|
|
17
|
+
* stored, or returned) — so no closure survives into a later invocation
|
|
18
|
+
* where the cell holds a different value;
|
|
19
|
+
* - the arrow's creation point is not lexically inside a loop (a shared cell
|
|
20
|
+
* would be clobbered across iterations that each register/keep a closure);
|
|
21
|
+
* - the enclosing function is sync and contains no await/yield (no
|
|
22
|
+
* interleaving between the cell assignment and the call);
|
|
23
|
+
* - the captured local is declared (and its declaration ends) before the
|
|
24
|
+
* arrow's creation statement (no TDZ / `var` hoisting reads);
|
|
25
|
+
* - the captured binding is never reassigned (its value at the assignment
|
|
26
|
+
* point equals its value at every call point).
|
|
27
|
+
*
|
|
28
|
+
* Arrows that ESCAPE with live captures (e.g. `system.runInterval(() =>
|
|
29
|
+
* fn(player))` inside a handler that may run more than once) still bail: each
|
|
30
|
+
* registration needs its own capture, which a single cell cannot represent,
|
|
31
|
+
* and the enclosing function's invocation count is statically unprovable.
|
|
32
|
+
*
|
|
33
|
+
* Also bails: references to `this`, `arguments`, `super`, `new.target`.
|
|
34
|
+
* Hotness is preserved: a lifted function that was hot stays hot.
|
|
35
|
+
*/
|
|
36
|
+
import traverse from '@babel/traverse';
|
|
37
|
+
import * as t from '@babel/types';
|
|
38
|
+
import { walk, walkSameFunction } from '../pass-utils.js';
|
|
39
|
+
|
|
40
|
+
const _traverse = traverse.default || traverse;
|
|
41
|
+
|
|
42
|
+
export const ID = 'closure-lift';
|
|
43
|
+
export const CODE = 'B2';
|
|
44
|
+
|
|
45
|
+
function primitiveInitNode(node) {
|
|
46
|
+
if (!node) return null;
|
|
47
|
+
if (t.isStringLiteral(node) || t.isNumericLiteral(node) || t.isBooleanLiteral(node) || t.isNullLiteral(node)) return node;
|
|
48
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) return node;
|
|
49
|
+
if (t.isUnaryExpression(node) && (node.operator === '-' || node.operator === '+') && t.isNumericLiteral(node.argument)) return node;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isProgramScope(binding) {
|
|
54
|
+
return !!(binding.scope && binding.scope.path && binding.scope.path.isProgram());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Is the node lexically inside the body (or per-iteration parts) of a loop? */
|
|
58
|
+
function insideLoop(nodePath) {
|
|
59
|
+
for (let p = nodePath.parentPath; p; p = p.parentPath) {
|
|
60
|
+
const n = p.node;
|
|
61
|
+
if (t.isFunction(n) || t.isProgram(n)) return false;
|
|
62
|
+
if (t.isForStatement(n) || t.isWhileStatement(n) || t.isDoWhileStatement(n) ||
|
|
63
|
+
t.isForOfStatement(n) || t.isForInStatement(n)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Does the enclosing function itself contain await or yield (same function only)? */
|
|
71
|
+
function containsSuspension(fnPath) {
|
|
72
|
+
let found = false;
|
|
73
|
+
walkSameFunction(fnPath.node.body, (n) => {
|
|
74
|
+
if (t.isAwaitExpression(n) || t.isYieldExpression(n)) {
|
|
75
|
+
found = true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (t.isForOfStatement(n) && n.await) { found = true; return false; }
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* No-escape check for cell capture: every reference of the arrow's binding is
|
|
86
|
+
* the CALLEE of a call whose enclosing function is `ownerFnNode`. Returns
|
|
87
|
+
* { ok, reason }.
|
|
88
|
+
*/
|
|
89
|
+
function allUsesAreSyncCalls(binding, ownerFnNode) {
|
|
90
|
+
if (!binding) return { ok: false, reason: 'anonymous arrow' };
|
|
91
|
+
if (binding.constantViolations.length > 0) {
|
|
92
|
+
return { ok: false, reason: 'arrow binding is reassigned' };
|
|
93
|
+
}
|
|
94
|
+
const refs = binding.referencePaths;
|
|
95
|
+
if (refs.length === 0) return { ok: false, reason: 'arrow is never used' };
|
|
96
|
+
for (const ref of refs) {
|
|
97
|
+
const parent = ref.parent;
|
|
98
|
+
if (!(t.isCallExpression(parent) && parent.callee === ref.node)) {
|
|
99
|
+
return { ok: false, reason: 'arrow escapes as a value (registered, stored, or returned)' };
|
|
100
|
+
}
|
|
101
|
+
const callParent = ref.getFunctionParent();
|
|
102
|
+
if (!callParent || callParent.node !== ownerFnNode) {
|
|
103
|
+
return { ok: false, reason: 'arrow is called from a nested function' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function closureLift(programPath, ctx) {
|
|
110
|
+
// Compute the insertion index after imports (or 0 when uses may precede it).
|
|
111
|
+
const body = programPath.node.body;
|
|
112
|
+
let lastImport = -1;
|
|
113
|
+
for (let i = 0; i < body.length; i++) {
|
|
114
|
+
if (t.isImportDeclaration(body[i])) lastImport = i;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lifts = []; // { path, fnNode, liftName, cloned, capDecls, cellDecls, cellAssignments }
|
|
118
|
+
|
|
119
|
+
programPath.traverse({
|
|
120
|
+
'ArrowFunctionExpression|FunctionExpression'(path) {
|
|
121
|
+
const fn = path.node;
|
|
122
|
+
const fnParent = path.getFunctionParent();
|
|
123
|
+
if (!fnParent || !ctx.isHot(fnParent.node)) return;
|
|
124
|
+
if (fnParent.node === fn) return;
|
|
125
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
126
|
+
if (lifts.some((l) => l.fnNode === fn)) return;
|
|
127
|
+
|
|
128
|
+
const fail = (reason) => {
|
|
129
|
+
ctx.bail(fn.loc ? fn.loc.start.line : 0, CODE, reason);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let bad = null;
|
|
133
|
+
walk(fn.body, (n) => {
|
|
134
|
+
if (bad) return false;
|
|
135
|
+
if (t.isThisExpression(n)) { bad = 'references this'; return false; }
|
|
136
|
+
if (t.isIdentifier(n) && n.name === 'arguments') { bad = 'references arguments'; return false; }
|
|
137
|
+
if (t.isSuper(n)) { bad = 'references super'; return false; }
|
|
138
|
+
if (t.isMetaProperty(n)) { bad = 'references new.target'; return false; }
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
if (bad) return fail(bad);
|
|
142
|
+
|
|
143
|
+
// Classify captures.
|
|
144
|
+
const captures = new Map(); // binding -> { mode: 'inline'|'cell', init?, refs: [] }
|
|
145
|
+
let bailReason = null;
|
|
146
|
+
path.traverse({
|
|
147
|
+
ReferencedIdentifier(refPath) {
|
|
148
|
+
if (bailReason) return;
|
|
149
|
+
const name = refPath.node.name;
|
|
150
|
+
const binding = refPath.scope.getBinding(name);
|
|
151
|
+
if (!binding) return; // global
|
|
152
|
+
if (binding.path === path || isDescendantPath(binding.path, path)) return;
|
|
153
|
+
if (isProgramScope(binding)) return; // module-level
|
|
154
|
+
if (binding.constantViolations.length > 0) {
|
|
155
|
+
bailReason = `captured local "${name}" is reassigned`;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const decl = binding.path;
|
|
159
|
+
if (decl.isVariableDeclarator()) {
|
|
160
|
+
const prim = primitiveInitNode(decl.node.init);
|
|
161
|
+
if (prim) {
|
|
162
|
+
if (!captures.has(binding)) captures.set(binding, { mode: 'inline', init: prim, refs: [] });
|
|
163
|
+
captures.get(binding).refs.push(refPath);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Not an inline-able const primitive: candidate for cell capture.
|
|
168
|
+
if (!captures.has(binding)) captures.set(binding, { mode: 'cell', name, refs: [] });
|
|
169
|
+
captures.get(binding).refs.push(refPath);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
if (bailReason) return fail(bailReason);
|
|
173
|
+
|
|
174
|
+
const needsCell = [...captures.values()].some((c) => c.mode === 'cell');
|
|
175
|
+
const stmtPath = nearestStatement(path);
|
|
176
|
+
if (needsCell) {
|
|
177
|
+
// Cell capture soundness preconditions.
|
|
178
|
+
if (insideLoop(path)) {
|
|
179
|
+
return fail('captures a live local and is created inside a loop (a shared cell would be clobbered)');
|
|
180
|
+
}
|
|
181
|
+
if (fnParent.node.async || fnParent.node.generator || containsSuspension(fnParent)) {
|
|
182
|
+
return fail('captures a live local inside an async/generator function (interleaving could change the cell)');
|
|
183
|
+
}
|
|
184
|
+
// The arrow binding: VariableDeclarator id, or AssignmentExpression lhs.
|
|
185
|
+
let binding = null;
|
|
186
|
+
if (path.parentPath.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id)) {
|
|
187
|
+
binding = path.parentPath.scope.getBinding(path.parentPath.node.id.name);
|
|
188
|
+
} else if (path.parentPath.isAssignmentExpression() && t.isIdentifier(path.parentPath.node.left)) {
|
|
189
|
+
binding = path.parentPath.scope.getBinding(path.parentPath.node.left.name);
|
|
190
|
+
}
|
|
191
|
+
const escape = allUsesAreSyncCalls(binding, fnParent.node);
|
|
192
|
+
if (!escape.ok) {
|
|
193
|
+
return fail(`captures a live local and ${escape.reason} — a lifted function could observe a newer cell value`);
|
|
194
|
+
}
|
|
195
|
+
// Captured locals must be declared, and their declarations finished,
|
|
196
|
+
// before the STATEMENT containing the arrow's creation (the cell
|
|
197
|
+
// assignment is inserted there — reading a not-yet-declared local
|
|
198
|
+
// would be a new TDZ/hoisting hazard).
|
|
199
|
+
if (!stmtPath) {
|
|
200
|
+
return fail('captures a live local but has no statement context');
|
|
201
|
+
}
|
|
202
|
+
const creationStmtStart = stmtPath.node.start ?? 0;
|
|
203
|
+
for (const [binding2, cap] of captures) {
|
|
204
|
+
if (cap.mode !== 'cell') continue;
|
|
205
|
+
const declEnd = binding2.path.node.end ?? 0;
|
|
206
|
+
if (declEnd >= creationStmtStart) {
|
|
207
|
+
return fail(`captured local "${cap.name}" is declared at or after the arrow's creation statement (TDZ/hoisting hazard)`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Plan the lift.
|
|
213
|
+
const capDecls = [];
|
|
214
|
+
const cellDecls = [];
|
|
215
|
+
for (const [, cap] of captures) {
|
|
216
|
+
if (cap.mode === 'inline') {
|
|
217
|
+
const capName = programPath.scope.generateUid('opCap');
|
|
218
|
+
capDecls.push({ name: capName, init: t.cloneNode(cap.init, true) });
|
|
219
|
+
for (const ref of cap.refs) {
|
|
220
|
+
if (ref.node && ref.node.type) ref.replaceWith(t.identifier(capName));
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
const cellName = programPath.scope.generateUid('opCell');
|
|
224
|
+
cap.cellName = cellName;
|
|
225
|
+
cellDecls.push(cellName);
|
|
226
|
+
for (const ref of cap.refs) {
|
|
227
|
+
if (ref.node && ref.node.type) ref.replaceWith(t.identifier(cellName));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const liftName = programPath.scope.generateUid('opLift');
|
|
232
|
+
const cloned = t.cloneNode(path.node, true);
|
|
233
|
+
|
|
234
|
+
// Cell assignments go immediately before the statement containing the
|
|
235
|
+
// arrow's creation, in the order the captures were classified. The
|
|
236
|
+
// statement path was already computed during the bail checks.
|
|
237
|
+
const cellAssignments = [];
|
|
238
|
+
if (needsCell) {
|
|
239
|
+
for (const [, cap] of captures) {
|
|
240
|
+
if (cap.mode !== 'cell') continue;
|
|
241
|
+
cellAssignments.push(t.expressionStatement(
|
|
242
|
+
t.assignmentExpression('=', t.identifier(cap.cellName), t.identifier(cap.name))
|
|
243
|
+
));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
lifts.push({ path, fnNode: fn, liftName, cloned, capDecls, cellDecls, cellAssignments, stmtPath });
|
|
247
|
+
if (ctx.hot) ctx.hot.copyHotness(fn, cloned);
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (lifts.length === 0) return;
|
|
252
|
+
|
|
253
|
+
// Sort by source position for deterministic insertion.
|
|
254
|
+
lifts.sort((a, b) => (a.fnNode.start || 0) - (b.fnNode.start || 0));
|
|
255
|
+
|
|
256
|
+
// Do any lift uses precede the end of the import block?
|
|
257
|
+
const importBoundary = lastImport >= 0 ? (body[lastImport].end || 0) : -1;
|
|
258
|
+
const anyBefore = lifts.some((l) => (l.fnNode.start || Infinity) < importBoundary);
|
|
259
|
+
|
|
260
|
+
const newStmts = [];
|
|
261
|
+
for (const l of lifts) {
|
|
262
|
+
for (const cap of l.capDecls) {
|
|
263
|
+
newStmts.push(t.variableDeclaration('const', [
|
|
264
|
+
t.variableDeclarator(t.identifier(cap.name), cap.init),
|
|
265
|
+
]));
|
|
266
|
+
}
|
|
267
|
+
for (const cellName of l.cellDecls) {
|
|
268
|
+
newStmts.push(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(cellName))]));
|
|
269
|
+
}
|
|
270
|
+
newStmts.push(t.variableDeclaration('const', [
|
|
271
|
+
t.variableDeclarator(t.identifier(l.liftName), l.cloned),
|
|
272
|
+
]));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const insertIdx = anyBefore ? 0 : lastImport + 1;
|
|
276
|
+
body.splice(insertIdx, 0, ...newStmts);
|
|
277
|
+
|
|
278
|
+
// Insert cell assignments grouped per statement (preserving lift order).
|
|
279
|
+
const byStatement = new Map();
|
|
280
|
+
for (const l of lifts) {
|
|
281
|
+
if (l.cellAssignments.length === 0 || !l.stmtPath) continue;
|
|
282
|
+
const key = l.stmtPath.node;
|
|
283
|
+
if (!byStatement.has(key)) byStatement.set(key, { stmtPath: l.stmtPath, stmts: [] });
|
|
284
|
+
byStatement.get(key).stmts.push(...l.cellAssignments);
|
|
285
|
+
}
|
|
286
|
+
for (const [, group] of byStatement) {
|
|
287
|
+
try {
|
|
288
|
+
group.stmtPath.insertBefore(group.stmts);
|
|
289
|
+
} catch {
|
|
290
|
+
// Statement not in a list position (e.g. an if consequent): wrap.
|
|
291
|
+
const node = group.stmtPath.node;
|
|
292
|
+
group.stmtPath.replaceWith(t.blockStatement([...group.stmts, node]));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const l of lifts) {
|
|
297
|
+
if (ctx.isDisabled(l.path, ID, CODE)) continue;
|
|
298
|
+
l.path.replaceWith(t.identifier(l.liftName));
|
|
299
|
+
ctx.applied(ID);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nearestStatement(path) {
|
|
304
|
+
let p = path;
|
|
305
|
+
while (p && !p.isStatement()) p = p.parentPath;
|
|
306
|
+
return p;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isDescendantPath(bindingPath, fnPath) {
|
|
310
|
+
let p = bindingPath;
|
|
311
|
+
while (p) {
|
|
312
|
+
if (p === fnPath) return true;
|
|
313
|
+
p = p.parentPath;
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B3 string-build — balanced level.
|
|
3
|
+
*
|
|
4
|
+
* `s += x` accumulated inside a loop in a hot function becomes
|
|
5
|
+
* `parts.push("" + x)` with a single `s = parts.join("")` after the loop.
|
|
6
|
+
*
|
|
7
|
+
* Why: QuickJS strings are immutable flat buffers, so `+=` over long loops is
|
|
8
|
+
* O(n^2) copying. Array parts + one join is O(n).
|
|
9
|
+
*
|
|
10
|
+
* Preconditions (else bail):
|
|
11
|
+
* - `s` is a `let` string initialized to "" and only ever written by the
|
|
12
|
+
* `+=`/`s = s + x` statements inside ONE loop (or the initializer)
|
|
13
|
+
* - `s` is never read inside the loop (including from nested closures)
|
|
14
|
+
* - writes live in the hot function itself (not a nested closure)
|
|
15
|
+
* - `s = x + s` (prepend) is not used
|
|
16
|
+
*
|
|
17
|
+
* `"" + x` preserves exact ToString semantics (including null -> "null",
|
|
18
|
+
* undefined -> "undefined", symbol throws) at the same evaluation point.
|
|
19
|
+
*/
|
|
20
|
+
import traverse from '@babel/traverse';
|
|
21
|
+
import * as t from '@babel/types';
|
|
22
|
+
import { walk } from '../pass-utils.js';
|
|
23
|
+
|
|
24
|
+
const _traverse = traverse.default || traverse;
|
|
25
|
+
|
|
26
|
+
export const ID = 'string-build';
|
|
27
|
+
export const CODE = 'B3';
|
|
28
|
+
|
|
29
|
+
export default function stringBuild(programPath, ctx) {
|
|
30
|
+
programPath.traverse({
|
|
31
|
+
VariableDeclaration(path) {
|
|
32
|
+
const node = path.node;
|
|
33
|
+
const line = node.loc ? node.loc.start.line : 0;
|
|
34
|
+
const fnParent = path.getFunctionParent();
|
|
35
|
+
if (!fnParent || !ctx.isHot(fnParent.node)) return;
|
|
36
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
37
|
+
|
|
38
|
+
const fail = (reason) => ctx.bail(line, CODE, reason);
|
|
39
|
+
|
|
40
|
+
if (node.kind !== 'let' || node.declarations.length !== 1) return;
|
|
41
|
+
const declarator = node.declarations[0];
|
|
42
|
+
if (!t.isIdentifier(declarator.id)) return;
|
|
43
|
+
if (!declarator.init || !t.isStringLiteral(declarator.init) || declarator.init.value !== '') return;
|
|
44
|
+
|
|
45
|
+
const name = declarator.id.name;
|
|
46
|
+
const binding = path.scope.getBinding(name);
|
|
47
|
+
if (!binding) return;
|
|
48
|
+
|
|
49
|
+
// All constant violations must be += or `s = s + x` inside ONE loop.
|
|
50
|
+
if (binding.constantViolations.length === 0) return;
|
|
51
|
+
let loopNode = null;
|
|
52
|
+
const writes = [];
|
|
53
|
+
for (const viol of binding.constantViolations) {
|
|
54
|
+
if (!viol.isAssignmentExpression()) return fail('non-assignment write to accumulator');
|
|
55
|
+
const vn = viol.node;
|
|
56
|
+
if (vn.operator === '+=') {
|
|
57
|
+
if (!t.isIdentifier(vn.left) || vn.left.name !== name) return fail('unexpected assignment shape');
|
|
58
|
+
writes.push({ node: vn, value: vn.right, violPath: viol });
|
|
59
|
+
} else if (vn.operator === '=') {
|
|
60
|
+
if (!t.isIdentifier(vn.left) || vn.left.name !== name) return fail('unexpected assignment shape');
|
|
61
|
+
const r = vn.right;
|
|
62
|
+
if (t.isBinaryExpression(r) && r.operator === '+' && t.isIdentifier(r.left) && r.left.name === name) {
|
|
63
|
+
writes.push({ node: vn, value: r.right, violPath: viol });
|
|
64
|
+
} else {
|
|
65
|
+
return fail('accumulator reassigned (not an append)');
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
return fail('unexpected assignment operator');
|
|
69
|
+
}
|
|
70
|
+
// Must live inside one loop, in the hot function itself.
|
|
71
|
+
const violParent = viol.parentPath ? viol.parentPath : null;
|
|
72
|
+
let l = viol;
|
|
73
|
+
while (l && !l.isProgram()) {
|
|
74
|
+
if (l.isForStatement() || l.isWhileStatement() || l.isDoWhileStatement() ||
|
|
75
|
+
l.isForOfStatement() || l.isForInStatement()) {
|
|
76
|
+
if (!loopNode) loopNode = l.node;
|
|
77
|
+
else if (loopNode !== l.node) return fail('writes span multiple loops');
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (l.isFunctionExpression() || l.isArrowFunctionExpression()) {
|
|
81
|
+
return fail('accumulator written inside a nested closure');
|
|
82
|
+
}
|
|
83
|
+
l = l.parentPath;
|
|
84
|
+
}
|
|
85
|
+
if (!loopNode) return fail('accumulator written outside a loop');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// No reads of `s` inside the loop other than write targets and the
|
|
89
|
+
// `s` operand of a `s = s + x` append.
|
|
90
|
+
const appendOperands = new Set();
|
|
91
|
+
for (const w of writes) {
|
|
92
|
+
if (t.isBinaryExpression(w.value) && w.value.operator === '+' && t.isIdentifier(w.value.left) && w.value.left.name === name) {
|
|
93
|
+
appendOperands.add(w.value.left);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const inLoop = (n) => loopNode.start <= n.start && n.end <= loopNode.end;
|
|
97
|
+
for (const ref of binding.referencePaths) {
|
|
98
|
+
const n = ref.node;
|
|
99
|
+
if (!n || !inLoop(n)) continue;
|
|
100
|
+
const parent = ref.parent;
|
|
101
|
+
const isWriteTarget = t.isAssignmentExpression(parent) && parent.left === n;
|
|
102
|
+
if (!isWriteTarget && !appendOperands.has(n)) return fail('accumulator read inside the loop');
|
|
103
|
+
}
|
|
104
|
+
// `s += s` shape: right side reading s inside the loop.
|
|
105
|
+
for (const w of writes) {
|
|
106
|
+
let readsSelf = false;
|
|
107
|
+
walk(w.value, (n) => {
|
|
108
|
+
if (t.isIdentifier(n) && n.name === name) readsSelf = true;
|
|
109
|
+
return true;
|
|
110
|
+
});
|
|
111
|
+
if (readsSelf) return fail('accumulator reads itself in the append expression');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The loop must be in the same function as the declaration; the
|
|
115
|
+
// violation scan above already established containment.
|
|
116
|
+
|
|
117
|
+
// ---- transform
|
|
118
|
+
const partsName = path.scope.generateUid('opParts');
|
|
119
|
+
const partsDecl = t.variableDeclaration('const', [
|
|
120
|
+
t.variableDeclarator(t.identifier(partsName), t.arrayExpression([])),
|
|
121
|
+
]);
|
|
122
|
+
const joinStmt = t.expressionStatement(
|
|
123
|
+
t.assignmentExpression('=', t.identifier(name),
|
|
124
|
+
t.callExpression(t.memberExpression(t.identifier(partsName), t.identifier('join')), [t.stringLiteral('')]))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
for (const w of writes) {
|
|
128
|
+
if (!w.violPath.parentPath || !w.violPath.parentPath.isExpressionStatement()) {
|
|
129
|
+
return fail('accumulator append is not a standalone statement');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const w of writes) {
|
|
133
|
+
const push = t.expressionStatement(
|
|
134
|
+
t.callExpression(
|
|
135
|
+
t.memberExpression(t.identifier(partsName), t.identifier('push')),
|
|
136
|
+
[t.binaryExpression('+', t.stringLiteral(''), w.value)]
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
w.violPath.replaceWith(push);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Insert decl before the loop and join after it.
|
|
143
|
+
const loopPath = findLoopPath(programPath, loopNode);
|
|
144
|
+
if (!loopPath) return fail('internal: loop path not found');
|
|
145
|
+
const parent = loopPath.parentPath;
|
|
146
|
+
const inList = parent && (parent.isArrayExpression() || parent.isBlockStatement() ||
|
|
147
|
+
parent.isSwitchCase() || parent.isProgram());
|
|
148
|
+
if (inList) {
|
|
149
|
+
loopPath.insertBefore(partsDecl);
|
|
150
|
+
loopPath.insertAfter(joinStmt);
|
|
151
|
+
} else {
|
|
152
|
+
loopPath.replaceWith(t.blockStatement([partsDecl, loopPath.node, joinStmt]));
|
|
153
|
+
}
|
|
154
|
+
ctx.applied(ID);
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findLoopPath(programPath, loopNode) {
|
|
160
|
+
let found = null;
|
|
161
|
+
programPath.traverse({
|
|
162
|
+
'ForStatement|WhileStatement|DoWhileStatement|ForOfStatement|ForInStatement'(p) {
|
|
163
|
+
if (p.node === loopNode && !found) { found = p; p.stop(); }
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B4 dynprop-hoist — balanced level.
|
|
3
|
+
*
|
|
4
|
+
* `X.getDynamicProperty("k")` with a constant key, read inside a loop in a hot
|
|
5
|
+
* function, is hoisted above the loop when the loop is barrier-free apart from
|
|
6
|
+
* the reads themselves (no other calls, no property writes, no await/yield,
|
|
7
|
+
* no delete) and the receiver is never reassigned inside the loop.
|
|
8
|
+
*
|
|
9
|
+
* Reads are collected from unconditional top-level body statements only.
|
|
10
|
+
* Never cached across tick invocations at this level — that is R1 territory.
|
|
11
|
+
*/
|
|
12
|
+
import traverse from '@babel/traverse';
|
|
13
|
+
import * as t from '@babel/types';
|
|
14
|
+
import { walkSameFunction } from '../pass-utils.js';
|
|
15
|
+
|
|
16
|
+
const _traverse = traverse.default || traverse;
|
|
17
|
+
|
|
18
|
+
export const ID = 'dynprop-hoist';
|
|
19
|
+
export const CODE = 'B4';
|
|
20
|
+
|
|
21
|
+
const LOOP_TYPES = ['ForStatement', 'WhileStatement', 'DoWhileStatement'];
|
|
22
|
+
|
|
23
|
+
/** Replace oldNode with newNode anywhere in the tree rooted at rootNode. */
|
|
24
|
+
function replaceIn(rootNode, oldNode, newNode) {
|
|
25
|
+
const stack = [rootNode];
|
|
26
|
+
while (stack.length > 0) {
|
|
27
|
+
const cur = stack.pop();
|
|
28
|
+
if (!cur || typeof cur !== 'object') continue;
|
|
29
|
+
for (const key of Object.keys(cur)) {
|
|
30
|
+
if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments' || key === 'extra' || key === '__opParent') continue;
|
|
31
|
+
const v = cur[key];
|
|
32
|
+
if (v === oldNode) { cur[key] = newNode; return true; }
|
|
33
|
+
if (Array.isArray(v)) {
|
|
34
|
+
const idx = v.indexOf(oldNode);
|
|
35
|
+
if (idx >= 0) { v[idx] = newNode; return true; }
|
|
36
|
+
for (const item of v) if (item && typeof item === 'object' && typeof item.type === 'string') stack.push(item);
|
|
37
|
+
} else if (v && typeof v === 'object' && typeof v.type === 'string') {
|
|
38
|
+
stack.push(v);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function dynpropHoist(programPath, ctx) {
|
|
46
|
+
programPath.traverse({
|
|
47
|
+
[LOOP_TYPES.join('|')](path) {
|
|
48
|
+
const node = path.node;
|
|
49
|
+
const line = node.loc ? node.loc.start.line : 0;
|
|
50
|
+
const fnParent = path.getFunctionParent();
|
|
51
|
+
if (!fnParent || !ctx.isHot(fnParent.node)) return;
|
|
52
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
53
|
+
|
|
54
|
+
const fail = (reason) => ctx.bail(line, CODE, reason);
|
|
55
|
+
|
|
56
|
+
// ---- barrier scan: no calls other than getDynamicProperty reads,
|
|
57
|
+
// no await/yield, no property writes, no delete.
|
|
58
|
+
let hasOtherCall = false;
|
|
59
|
+
let hasAwaitYield = false;
|
|
60
|
+
let hasPropWrite = false;
|
|
61
|
+
const scan = (n) => {
|
|
62
|
+
walkSameFunction(n, (m) => {
|
|
63
|
+
if (t.isFunctionExpression(m) || t.isArrowFunctionExpression(m)) return false;
|
|
64
|
+
if (t.isCallExpression(m)) {
|
|
65
|
+
const isDynPropRead = t.isMemberExpression(m.callee) && !m.callee.computed &&
|
|
66
|
+
t.isIdentifier(m.callee.property) && m.callee.property.name === 'getDynamicProperty' &&
|
|
67
|
+
m.arguments.length === 1 && t.isStringLiteral(m.arguments[0]) &&
|
|
68
|
+
(t.isIdentifier(m.callee.object) || t.isThisExpression(m.callee.object));
|
|
69
|
+
if (!isDynPropRead) { hasOtherCall = true; return false; }
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (t.isNewExpression(m) || t.isTaggedTemplateExpression(m) || t.isOptionalCallExpression(m)) {
|
|
73
|
+
hasOtherCall = true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (t.isAwaitExpression(m) || t.isYieldExpression(m)) { hasAwaitYield = true; return false; }
|
|
77
|
+
if (t.isAssignmentExpression(m)) {
|
|
78
|
+
if (t.isMemberExpression(m.left)) { hasPropWrite = true; return false; }
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (t.isUpdateExpression(m)) {
|
|
82
|
+
if (t.isMemberExpression(m.argument)) { hasPropWrite = true; return false; }
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (t.isUnaryExpression(m) && m.operator === 'delete') { hasPropWrite = true; return false; }
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
walkSameFunction(node.body, scan);
|
|
90
|
+
if (node.type === 'ForStatement') {
|
|
91
|
+
if (node.test) scan(node.test);
|
|
92
|
+
if (node.update) scan(node.update);
|
|
93
|
+
}
|
|
94
|
+
if (hasPropWrite) return fail('loop contains property writes');
|
|
95
|
+
if (hasAwaitYield) return fail('loop contains await/yield');
|
|
96
|
+
if (hasOtherCall) return fail('loop contains calls other than getDynamicProperty reads');
|
|
97
|
+
|
|
98
|
+
// ---- collect reads from unconditional top-level body statements
|
|
99
|
+
const reads = []; // { node, stmt, receiverName, isThis, key }
|
|
100
|
+
const collect = (stmt) => {
|
|
101
|
+
walkSameFunction(stmt, (m) => {
|
|
102
|
+
if (t.isFunctionExpression(m) || t.isArrowFunctionExpression(m)) return false;
|
|
103
|
+
if (t.isCallExpression(m) && t.isMemberExpression(m.callee) && !m.callee.computed &&
|
|
104
|
+
t.isIdentifier(m.callee.property) && m.callee.property.name === 'getDynamicProperty' &&
|
|
105
|
+
m.arguments.length === 1 && t.isStringLiteral(m.arguments[0])) {
|
|
106
|
+
const obj = m.callee.object;
|
|
107
|
+
if (t.isIdentifier(obj)) {
|
|
108
|
+
reads.push({ node: m, stmt, receiverName: obj.name, isThis: false, key: m.arguments[0].value });
|
|
109
|
+
} else if (t.isThisExpression(obj)) {
|
|
110
|
+
reads.push({ node: m, stmt, receiverName: null, isThis: true, key: m.arguments[0].value });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const bodyIsBlock = t.isBlockStatement(node.body);
|
|
117
|
+
const topLevel = bodyIsBlock ? node.body.body : [node.body];
|
|
118
|
+
for (const stmt of topLevel) collect(stmt);
|
|
119
|
+
if (reads.length === 0) return;
|
|
120
|
+
|
|
121
|
+
// ---- receiver invariance inside the loop
|
|
122
|
+
const loopRange = { start: node.start, end: node.end };
|
|
123
|
+
for (const r of reads) {
|
|
124
|
+
if (r.isThis) continue;
|
|
125
|
+
const binding = path.scope.getBinding(r.receiverName);
|
|
126
|
+
if (!binding) continue;
|
|
127
|
+
for (const viol of binding.constantViolations) {
|
|
128
|
+
const vn = viol.node;
|
|
129
|
+
if (vn && vn.start !== undefined && vn.start >= loopRange.start && vn.end <= loopRange.end) {
|
|
130
|
+
return fail(`receiver "${r.receiverName}" reassigned inside the loop`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- group by receiver identity + key
|
|
136
|
+
const bindingId = (name) => {
|
|
137
|
+
const binding = path.scope.getBinding(name);
|
|
138
|
+
return binding ? `b:${bindingScopeId(binding)}` : 'global:' + name;
|
|
139
|
+
};
|
|
140
|
+
const groups = new Map();
|
|
141
|
+
for (const r of reads) {
|
|
142
|
+
const key = (r.isThis ? '@this' : bindingId(r.receiverName)) + '#' + r.key;
|
|
143
|
+
if (!groups.has(key)) groups.set(key, { receiverName: r.receiverName, isThis: r.isThis, key: r.key, uses: [] });
|
|
144
|
+
groups.get(key).uses.push(r);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---- hoist + rewrite
|
|
148
|
+
const decls = [];
|
|
149
|
+
for (const [, g] of groups) {
|
|
150
|
+
const name = path.scope.generateUid('opDP');
|
|
151
|
+
const recv = g.isThis ? t.thisExpression() : t.identifier(g.receiverName);
|
|
152
|
+
const call = t.callExpression(
|
|
153
|
+
t.memberExpression(recv, t.identifier('getDynamicProperty')),
|
|
154
|
+
[t.stringLiteral(g.key)]
|
|
155
|
+
);
|
|
156
|
+
decls.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(name), call)]));
|
|
157
|
+
for (const u of g.uses) {
|
|
158
|
+
replaceIn(u.stmt, u.node, t.identifier(name));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const insertTarget = path.parentPath && path.parentPath.isLabeledStatement() ? path.parentPath : path;
|
|
162
|
+
insertTarget.insertBefore(decls);
|
|
163
|
+
ctx.applied(ID);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let scopeCounter = 0;
|
|
169
|
+
const scopeIds = new WeakMap();
|
|
170
|
+
function bindingScopeId(binding) {
|
|
171
|
+
let id = scopeIds.get(binding);
|
|
172
|
+
if (id === undefined) {
|
|
173
|
+
id = ++scopeCounter;
|
|
174
|
+
scopeIds.set(binding, id);
|
|
175
|
+
}
|
|
176
|
+
return id;
|
|
177
|
+
}
|