optipack 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +231 -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 -6
- package/src/cli.js +180 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -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/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/harness/setup.js +198 -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 +260 -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/loader-hooks.mjs +38 -0
- package/test/mock/@minecraft/server.mjs +274 -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/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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B5 null-chain-flatten — balanced level.
|
|
3
|
+
*
|
|
4
|
+
* After S1/S2 prove a guard, `a?.b` becomes `a.b` when `a` is statically
|
|
5
|
+
* non-nullish in that region: a guard like `if (!a) return;` (or
|
|
6
|
+
* `if (a == null) throw ...`) dominates the following statements, and nothing
|
|
7
|
+
* can reassign `a` before the use. The guard window is barrier-aware: a
|
|
8
|
+
* statement containing calls flattens its own uses only when no call executes
|
|
9
|
+
* before them (evaluation-order corridor), and then ends the window.
|
|
10
|
+
*
|
|
11
|
+
* Only the FIRST optional hop rooted at the guarded identifier is flattened;
|
|
12
|
+
* deeper optional hops (`a?.b?.c`) remain — `a.b` itself may still be nullish.
|
|
13
|
+
*/
|
|
14
|
+
import traverse from '@babel/traverse';
|
|
15
|
+
import * as t from '@babel/types';
|
|
16
|
+
import { walkSameFunction, corridorSafe } from '../pass-utils.js';
|
|
17
|
+
|
|
18
|
+
const _traverse = traverse.default || traverse;
|
|
19
|
+
|
|
20
|
+
export const ID = 'null-chain-flatten';
|
|
21
|
+
export const CODE = 'B5';
|
|
22
|
+
|
|
23
|
+
function statementHasBarrier(stmt) {
|
|
24
|
+
let barrier = false;
|
|
25
|
+
walkSameFunction(stmt, (n) => {
|
|
26
|
+
if (t.isCallExpression(n) || t.isNewExpression(n) || t.isAssignmentExpression(n) ||
|
|
27
|
+
t.isUpdateExpression(n) || t.isAwaitExpression(n) || t.isYieldExpression(n) ||
|
|
28
|
+
t.isForOfStatement(n) || t.isForInStatement(n) ||
|
|
29
|
+
t.isForStatement(n) || t.isWhileStatement(n) || t.isDoWhileStatement(n) ||
|
|
30
|
+
t.isTaggedTemplateExpression(n)) {
|
|
31
|
+
barrier = true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
return barrier;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Does this statement (or block) always exit? */
|
|
40
|
+
function alwaysExits(stmt) {
|
|
41
|
+
if (!stmt) return false;
|
|
42
|
+
if (t.isReturnStatement(stmt) || t.isThrowStatement(stmt) ||
|
|
43
|
+
t.isBreakStatement(stmt) || t.isContinueStatement(stmt)) return true;
|
|
44
|
+
if (t.isBlockStatement(stmt)) {
|
|
45
|
+
if (stmt.body.length === 0) return false;
|
|
46
|
+
for (const s of stmt.body) {
|
|
47
|
+
if (t.isIfStatement(s) || t.isSwitchStatement(s)) return false;
|
|
48
|
+
}
|
|
49
|
+
return alwaysExits(stmt.body[stmt.body.length - 1]);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Matches `!a`, `a == null`, `a === null`, `a == undefined`, `a === undefined`,
|
|
55
|
+
* and `a === null || a === undefined` combinations. Returns the identifier or null. */
|
|
56
|
+
function guardIdent(test) {
|
|
57
|
+
const idOf = (n) => (t.isIdentifier(n) ? n : null);
|
|
58
|
+
|
|
59
|
+
if (t.isUnaryExpression(test) && test.operator === '!') return idOf(test.argument);
|
|
60
|
+
|
|
61
|
+
const eqNullish = (n) => {
|
|
62
|
+
if (!t.isBinaryExpression(n)) return false;
|
|
63
|
+
if (n.operator !== '==' && n.operator !== '===') return false;
|
|
64
|
+
const { left, right } = n;
|
|
65
|
+
const litNull = t.isNullLiteral(right) || (t.isIdentifier(right) && right.name === 'undefined');
|
|
66
|
+
const litNullL = t.isNullLiteral(left) || (t.isIdentifier(left) && left.name === 'undefined');
|
|
67
|
+
return (idOf(left) && litNull) || (idOf(right) && litNullL);
|
|
68
|
+
};
|
|
69
|
+
if (eqNullish(test)) {
|
|
70
|
+
if (t.isBinaryExpression(test)) {
|
|
71
|
+
return idOf(t.isIdentifier(test.left) ? test.left : test.right);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (t.isLogicalExpression(test) && test.operator === '||') {
|
|
75
|
+
const l = guardIdent(test.left);
|
|
76
|
+
const r = guardIdent(test.right);
|
|
77
|
+
if (l && r && l.name === r.name) return l;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function nullChainFlatten(programPath, ctx) {
|
|
83
|
+
programPath.traverse({
|
|
84
|
+
BlockStatement(path) {
|
|
85
|
+
const body = path.node.body;
|
|
86
|
+
for (let i = 0; i < body.length; i++) {
|
|
87
|
+
const stmt = body[i];
|
|
88
|
+
if (!t.isIfStatement(stmt)) continue;
|
|
89
|
+
if (!alwaysExits(stmt.consequent)) continue;
|
|
90
|
+
if (stmt.alternate && !alwaysExits(stmt.alternate)) continue;
|
|
91
|
+
const ident = guardIdent(stmt.test);
|
|
92
|
+
if (!ident) continue;
|
|
93
|
+
if (ctx.isDisabled(path, ID, CODE)) continue;
|
|
94
|
+
|
|
95
|
+
const binding = path.scope.getBinding(ident.name);
|
|
96
|
+
if (!binding) continue; // need a trackable local/param
|
|
97
|
+
if (binding.constantViolations.length > 0) {
|
|
98
|
+
ctx.bail(stmt.loc ? stmt.loc.start.line : 0, CODE,
|
|
99
|
+
`guarded identifier "${ident.name}" is reassigned in scope`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Barrier-aware window after the guard.
|
|
104
|
+
let applied = 0;
|
|
105
|
+
for (let j = i + 1; j < body.length; j++) {
|
|
106
|
+
const s = body[j];
|
|
107
|
+
// Collect optional chains rooted at ident in this statement.
|
|
108
|
+
const uses = [];
|
|
109
|
+
walkSameFunction(s, (n) => {
|
|
110
|
+
if (t.isOptionalMemberExpression(n) && n.optional &&
|
|
111
|
+
t.isIdentifier(n.object) && n.object.name === ident.name) {
|
|
112
|
+
uses.push(n);
|
|
113
|
+
} else if (t.isOptionalCallExpression(n) && n.optional &&
|
|
114
|
+
t.isIdentifier(n.callee) && n.callee.name === ident.name) {
|
|
115
|
+
uses.push(n);
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
if (uses.length === 0) {
|
|
120
|
+
if (statementHasBarrier(s)) break; // window ends at barriers
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (corridorSafe(s, uses)) {
|
|
124
|
+
for (const n of uses) {
|
|
125
|
+
n.optional = false;
|
|
126
|
+
applied++;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (statementHasBarrier(s)) break; // window ends at barriers
|
|
132
|
+
}
|
|
133
|
+
if (applied > 0) ctx.applied(ID);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered pass registry. Running level N enables all passes at level <= N.
|
|
3
|
+
*/
|
|
4
|
+
import * as s1 from './s1-const-fold.js';
|
|
5
|
+
import * as s2 from './s2-dead-code.js';
|
|
6
|
+
import * as s3 from './s3-bool-simplify.js';
|
|
7
|
+
import * as s4 from './s4-for-of-to-indexed.js';
|
|
8
|
+
import * as s5 from './s5-array-methods-to-loops.js';
|
|
9
|
+
import * as s6 from './s6-destructure-to-direct.js';
|
|
10
|
+
import * as s7 from './s7-hoist-invariant-members.js';
|
|
11
|
+
import * as b1 from './b1-cache-getter-chains.js';
|
|
12
|
+
import * as b2 from './b2-closure-lift.js';
|
|
13
|
+
import * as b3 from './b3-string-build.js';
|
|
14
|
+
import * as b4 from './b4-dynprop-hoist.js';
|
|
15
|
+
import * as b5 from './b5-null-chain-flatten.js';
|
|
16
|
+
import * as r1 from './r1-cross-tick-cache.js';
|
|
17
|
+
import * as r2 from './r2-json-fold.js';
|
|
18
|
+
import * as r3 from './r3-try-catch-strip.js';
|
|
19
|
+
import * as r4 from './r4-getter-cache-cross-barrier.js';
|
|
20
|
+
|
|
21
|
+
export const PASSES = [
|
|
22
|
+
{ code: 'S1', id: s1.ID, level: 'safe', run: s1.default },
|
|
23
|
+
{ code: 'S2', id: s2.ID, level: 'safe', run: s2.default },
|
|
24
|
+
{ code: 'S3', id: s3.ID, level: 'safe', run: s3.default },
|
|
25
|
+
{ code: 'S4', id: s4.ID, level: 'safe', run: s4.default },
|
|
26
|
+
{ code: 'S5', id: s5.ID, level: 'safe', run: s5.default },
|
|
27
|
+
{ code: 'S6', id: s6.ID, level: 'safe', run: s6.default },
|
|
28
|
+
{ code: 'S7', id: s7.ID, level: 'safe', run: s7.default },
|
|
29
|
+
{ code: 'B1', id: b1.ID, level: 'balanced', run: b1.default },
|
|
30
|
+
{ code: 'B2', id: b2.ID, level: 'balanced', run: b2.default },
|
|
31
|
+
{ code: 'B3', id: b3.ID, level: 'balanced', run: b3.default },
|
|
32
|
+
{ code: 'B4', id: b4.ID, level: 'balanced', run: b4.default },
|
|
33
|
+
{ code: 'B5', id: b5.ID, level: 'balanced', run: b5.default },
|
|
34
|
+
{ code: 'R1', id: r1.ID, level: 'risky', run: r1.default },
|
|
35
|
+
{ code: 'R2', id: r2.ID, level: 'risky', run: r2.default },
|
|
36
|
+
{ code: 'R3', id: r3.ID, level: 'risky', run: r3.default },
|
|
37
|
+
{ code: 'R4', id: r4.ID, level: 'risky', run: r4.default },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const PASS_BY_ID = new Map(PASSES.map((p) => [p.id, p]));
|
|
41
|
+
export const PASS_BY_CODE = new Map(PASSES.map((p) => [p.code, p]));
|
|
42
|
+
export const PASS_IDS = new Set(PASSES.map((p) => p.id));
|
|
43
|
+
export const PASS_CODES = new Map(PASSES.map((p) => [p.code, p.id]));
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R1 cross-tick-cache — RISKY.
|
|
3
|
+
*
|
|
4
|
+
* Module-level WeakMap caches keyed by entity/player for
|
|
5
|
+
* `getComponent("...")` results and for immutable-per-entity fields
|
|
6
|
+
* (`entity.id`, `entity.typeId`), reused across ticks with no invalidation.
|
|
7
|
+
*
|
|
8
|
+
* Failure mode: stale component views after death / dimension change. Even the
|
|
9
|
+
* immutable subset is reported as risky (a hostile addon could mutate id).
|
|
10
|
+
*
|
|
11
|
+
* Applied to reads inside hot functions only. Every application emits a
|
|
12
|
+
* warning in the report.
|
|
13
|
+
*/
|
|
14
|
+
import traverse from '@babel/traverse';
|
|
15
|
+
import * as t from '@babel/types';
|
|
16
|
+
|
|
17
|
+
const _traverse = traverse.default || traverse;
|
|
18
|
+
|
|
19
|
+
export const ID = 'cross-tick-cache';
|
|
20
|
+
export const CODE = 'R1';
|
|
21
|
+
|
|
22
|
+
function isWriteTargetPath(memberPath) {
|
|
23
|
+
const parent = memberPath.parent;
|
|
24
|
+
if (t.isAssignmentExpression(parent) && parent.left === memberPath.node) return true;
|
|
25
|
+
if (t.isUpdateExpression(parent) && parent.argument === memberPath.node) return true;
|
|
26
|
+
if (t.isUnaryExpression(parent) && parent.operator === 'delete' && parent.argument === memberPath.node) return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function crossTickCache(programPath, ctx) {
|
|
31
|
+
const edits = []; // { kind: 'component'|'field', line, node, stmt, recvNode, key }
|
|
32
|
+
let sawAny = false;
|
|
33
|
+
|
|
34
|
+
programPath.traverse({
|
|
35
|
+
'FunctionDeclaration|FunctionExpression|ArrowFunctionExpression'(path) {
|
|
36
|
+
const fn = path.node;
|
|
37
|
+
if (!ctx.isHot(fn)) return;
|
|
38
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
39
|
+
|
|
40
|
+
// ---- component reads: X.getComponent("lit")
|
|
41
|
+
path.traverse({
|
|
42
|
+
CallExpression(cp) {
|
|
43
|
+
const n = cp.node;
|
|
44
|
+
if (cp.parentPath && isWriteTargetCall(cp)) return;
|
|
45
|
+
if (!(t.isMemberExpression(n.callee) && !n.callee.computed &&
|
|
46
|
+
t.isIdentifier(n.callee.property) && n.callee.property.name === 'getComponent' &&
|
|
47
|
+
n.arguments.length === 1 && t.isStringLiteral(n.arguments[0]))) return;
|
|
48
|
+
const obj = n.callee.object;
|
|
49
|
+
if (!t.isIdentifier(obj) && !t.isThisExpression(obj)) return;
|
|
50
|
+
edits.push({
|
|
51
|
+
kind: 'component',
|
|
52
|
+
line: n.loc ? n.loc.start.line : 0,
|
|
53
|
+
node: n,
|
|
54
|
+
fnPath: path,
|
|
55
|
+
recvName: t.isIdentifier(obj) ? obj.name : null,
|
|
56
|
+
isThis: t.isThisExpression(obj),
|
|
57
|
+
key: n.arguments[0].value,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
MemberExpression(mp) {
|
|
61
|
+
const n = mp.node;
|
|
62
|
+
if (isWriteTargetPath(mp)) return;
|
|
63
|
+
if (mp.parentPath && t.isCallExpression(mp.parentPath.node) && mp.parentPath.node.callee === n) return;
|
|
64
|
+
if (!(!n.computed && t.isIdentifier(n.property) && (n.property.name === 'id' || n.property.name === 'typeId'))) return;
|
|
65
|
+
const obj = n.object;
|
|
66
|
+
if (!t.isIdentifier(obj)) return;
|
|
67
|
+
edits.push({
|
|
68
|
+
kind: 'field',
|
|
69
|
+
line: n.loc ? n.loc.start.line : 0,
|
|
70
|
+
node: n,
|
|
71
|
+
fnPath: path,
|
|
72
|
+
recvName: obj.name,
|
|
73
|
+
isThis: false,
|
|
74
|
+
key: n.property.name,
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (edits.length === 0) return;
|
|
82
|
+
|
|
83
|
+
// Group by (receiver identity, key) to share cache slots; rewrite within the
|
|
84
|
+
// owning statement.
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const e of edits) {
|
|
87
|
+
if (seen.has(e.node)) continue;
|
|
88
|
+
seen.add(e.node);
|
|
89
|
+
const helper = e.kind === 'component' ? '__opR1Component' : '__opR1Field';
|
|
90
|
+
const recv = e.isThis ? t.thisExpression() : t.identifier(e.recvName);
|
|
91
|
+
const call = t.callExpression(t.identifier(helper), [recv, t.stringLiteral(e.key)]);
|
|
92
|
+
// Replace within the enclosing hot function body subtree.
|
|
93
|
+
const root = e.fnPath.node.body;
|
|
94
|
+
if (replaceInTree(root, e.node, call)) {
|
|
95
|
+
sawAny = true;
|
|
96
|
+
ctx.applied(ID);
|
|
97
|
+
ctx.warn(e.line, CODE,
|
|
98
|
+
e.kind === 'component'
|
|
99
|
+
? `getComponent("${e.key}") cached across ticks in a WeakMap — stale after death/dimension change`
|
|
100
|
+
: `.${e.key} cached across ticks in a WeakMap — stale if mutated`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (sawAny) {
|
|
105
|
+
injectHelpers(programPath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isWriteTargetCall(cp) {
|
|
110
|
+
const parent = cp.parentPath && cp.parentPath.node;
|
|
111
|
+
return t.isAssignmentExpression(parent) && parent.left === cp.node;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function replaceInTree(rootNode, oldNode, newNode) {
|
|
115
|
+
const stack = [rootNode];
|
|
116
|
+
while (stack.length > 0) {
|
|
117
|
+
const cur = stack.pop();
|
|
118
|
+
if (!cur || typeof cur !== 'object') continue;
|
|
119
|
+
for (const key of Object.keys(cur)) {
|
|
120
|
+
if (key === 'loc' || key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments' || key === 'extra') continue;
|
|
121
|
+
const v = cur[key];
|
|
122
|
+
if (v === oldNode) { cur[key] = newNode; return true; }
|
|
123
|
+
if (Array.isArray(v)) {
|
|
124
|
+
const idx = v.indexOf(oldNode);
|
|
125
|
+
if (idx >= 0) { v[idx] = newNode; return true; }
|
|
126
|
+
for (const item of v) if (item && typeof item === 'object' && typeof item.type === 'string') stack.push(item);
|
|
127
|
+
} else if (v && typeof v === 'object' && typeof v.type === 'string') {
|
|
128
|
+
stack.push(v);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function injectHelpers(programPath) {
|
|
136
|
+
const body = programPath.node.body;
|
|
137
|
+
let lastImport = -1;
|
|
138
|
+
for (let i = 0; i < body.length; i++) {
|
|
139
|
+
if (t.isImportDeclaration(body[i])) lastImport = i;
|
|
140
|
+
}
|
|
141
|
+
body.splice(lastImport + 1, 0, ...buildHelperStatements());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildHelperStatements() {
|
|
145
|
+
const cacheMap = t.identifier('__opR1Cache');
|
|
146
|
+
|
|
147
|
+
const cacheGetFn = (name, readExpr) => {
|
|
148
|
+
// function name(obj, key) { ... }
|
|
149
|
+
const obj = t.identifier('obj');
|
|
150
|
+
const key = t.identifier('key');
|
|
151
|
+
const m = t.identifier('m');
|
|
152
|
+
const v = t.identifier('v');
|
|
153
|
+
return t.functionDeclaration(
|
|
154
|
+
t.identifier(name),
|
|
155
|
+
[obj, key],
|
|
156
|
+
t.blockStatement([
|
|
157
|
+
t.variableDeclaration('let', [t.variableDeclarator(m,
|
|
158
|
+
t.callExpression(t.memberExpression(cacheMap, t.identifier('get')), [obj]))]),
|
|
159
|
+
t.ifStatement(
|
|
160
|
+
t.binaryExpression('===', m, t.identifier('undefined')),
|
|
161
|
+
t.blockStatement([
|
|
162
|
+
t.expressionStatement(t.assignmentExpression('=', m,
|
|
163
|
+
t.newExpression(t.identifier('Map'), []))),
|
|
164
|
+
t.expressionStatement(t.callExpression(
|
|
165
|
+
t.memberExpression(cacheMap, t.identifier('set')), [obj, m])),
|
|
166
|
+
])
|
|
167
|
+
),
|
|
168
|
+
t.variableDeclaration('let', [t.variableDeclarator(v,
|
|
169
|
+
t.callExpression(t.memberExpression(m, t.identifier('get')), [key]))]),
|
|
170
|
+
t.ifStatement(
|
|
171
|
+
t.binaryExpression('===', v, t.identifier('undefined')),
|
|
172
|
+
t.blockStatement([
|
|
173
|
+
t.expressionStatement(t.assignmentExpression('=', v, readExpr(obj, key))),
|
|
174
|
+
t.expressionStatement(t.callExpression(
|
|
175
|
+
t.memberExpression(m, t.identifier('set')), [key, v])),
|
|
176
|
+
])
|
|
177
|
+
),
|
|
178
|
+
t.returnStatement(v),
|
|
179
|
+
])
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return [
|
|
184
|
+
t.variableDeclaration('const', [
|
|
185
|
+
t.variableDeclarator(cacheMap, t.newExpression(t.identifier('WeakMap'), [])),
|
|
186
|
+
]),
|
|
187
|
+
cacheGetFn('__opR1Component', (obj, key) =>
|
|
188
|
+
t.callExpression(t.memberExpression(obj, t.identifier('getComponent')), [key])),
|
|
189
|
+
cacheGetFn('__opR1Field', (obj, key) =>
|
|
190
|
+
t.memberExpression(obj, key, true)),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 json-fold  RISKY.
|
|
3
|
+
*
|
|
4
|
+
* `const x = JSON.parse('{"a":1}')` on a constant string is hoisted to a
|
|
5
|
+
* module-level `const` shared by every call site, when the tool can prove the
|
|
6
|
+
* parsed object is never mutated and never escapes to unknown code.
|
|
7
|
+
*
|
|
8
|
+
* Failure mode: shared mutable state if any code mutates the parsed object.
|
|
9
|
+
*
|
|
10
|
+
* Proof obligations (else bail):
|
|
11
|
+
* - the string literal must parse as JSON at build time
|
|
12
|
+
* - binding is `const`, never reassigned
|
|
13
|
+
* - every use is a pure READ: member-access object position, arithmetic /
|
|
14
|
+
* comparison / logical / unary (non-delete) operand, template substitution,
|
|
15
|
+
* conditional test  nothing else
|
|
16
|
+
* - never passed to any function (even known ones), never returned, thrown,
|
|
17
|
+
* spread, aliased, iterated (for-of), or stored in another object/array
|
|
18
|
+
* - no method call on it (x.reset() could mutate)
|
|
19
|
+
*
|
|
20
|
+
* Every application emits a warning in the report.
|
|
21
|
+
*/
|
|
22
|
+
import traverse from '@babel/traverse';
|
|
23
|
+
import * as t from '@babel/types';
|
|
24
|
+
|
|
25
|
+
const _traverse = traverse.default || traverse;
|
|
26
|
+
|
|
27
|
+
export const ID = 'json-fold';
|
|
28
|
+
export const CODE = 'R2';
|
|
29
|
+
|
|
30
|
+
export default function jsonFold(programPath, ctx) {
|
|
31
|
+
const folds = [];
|
|
32
|
+
|
|
33
|
+
programPath.traverse({
|
|
34
|
+
VariableDeclarator(path) {
|
|
35
|
+
const node = path.node;
|
|
36
|
+
const decl = path.parentPath.node;
|
|
37
|
+
if (!t.isVariableDeclaration(path.parentPath.node) || path.parentPath.node.kind !== 'const') return;
|
|
38
|
+
if (path.parentPath.node.declarations.length !== 1) return;
|
|
39
|
+
if (!t.isIdentifier(node.id)) return;
|
|
40
|
+
const init = node.init;
|
|
41
|
+
if (!init || !t.isCallExpression(init)) return;
|
|
42
|
+
const callee = init.callee;
|
|
43
|
+
if (!(t.isMemberExpression(callee) && t.isIdentifier(callee.object) &&
|
|
44
|
+
callee.object.name === 'JSON' && t.isIdentifier(callee.property) &&
|
|
45
|
+
callee.property.name === 'parse' && init.arguments.length === 1)) return;
|
|
46
|
+
const arg = init.arguments[0];
|
|
47
|
+
if (!t.isStringLiteral(arg)) return;
|
|
48
|
+
const line = node.loc ? node.loc.start.line : 0;
|
|
49
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
50
|
+
|
|
51
|
+
// The literal must actually parse.
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(arg.value);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
ctx.bail(line, CODE, `literal is not valid JSON: ${e.message}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void parsed;
|
|
60
|
+
|
|
61
|
+
const name = node.id.name;
|
|
62
|
+
const binding = path.scope.getBinding(name);
|
|
63
|
+
if (!binding) return;
|
|
64
|
+
if (binding.constantViolations.length > 0) {
|
|
65
|
+
ctx.bail(line, CODE, 'binding is reassigned');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Exported bindings never fold.
|
|
70
|
+
let up = path.parentPath;
|
|
71
|
+
let exported = false;
|
|
72
|
+
while (up) {
|
|
73
|
+
if (up.isExportNamedDeclaration() || up.isExportDefaultDeclaration()) { exported = true; break; }
|
|
74
|
+
up = up.parentPath;
|
|
75
|
+
}
|
|
76
|
+
if (exported) {
|
|
77
|
+
ctx.bail(line, CODE, 'binding is exported');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Verify every use is a pure read. The parsed value is known at build
|
|
82
|
+
// time, so property reads of primitive JSON values are safe to pass
|
|
83
|
+
// anywhere (a copy flows); the object itself never escapes.
|
|
84
|
+
let ok = true;
|
|
85
|
+
let failReason = null;
|
|
86
|
+
for (const ref of binding.referencePaths) {
|
|
87
|
+
const verdict = isPureReadUse(ref, name, parsed);
|
|
88
|
+
if (!verdict.ok) { ok = false; failReason = verdict.reason; break; }
|
|
89
|
+
}
|
|
90
|
+
if (!ok) {
|
|
91
|
+
ctx.bail(line, CODE, failReason || 'use is not a pure read');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
folds.push({ path, name, literal: t.cloneNode(arg, true), line, refs: [...binding.referencePaths] });
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (folds.length === 0) return;
|
|
100
|
+
|
|
101
|
+
const body = programPath.node.body;
|
|
102
|
+
let lastImport = -1;
|
|
103
|
+
for (let i = 0; i < body.length; i++) {
|
|
104
|
+
if (t.isImportDeclaration(body[i])) lastImport = i;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const newStmts = [];
|
|
108
|
+
let idx = 1;
|
|
109
|
+
for (const f of folds) {
|
|
110
|
+
const hoistName = programPath.scope.generateUid('opJson');
|
|
111
|
+
newStmts.push(t.variableDeclaration('const', [
|
|
112
|
+
t.variableDeclarator(t.identifier(hoistName),
|
|
113
|
+
t.callExpression(t.memberExpression(t.identifier('JSON'), t.identifier('parse')), [f.literal])),
|
|
114
|
+
]));
|
|
115
|
+
for (const ref of f.refs) {
|
|
116
|
+
if (ref.node) ref.replaceWith(t.identifier(hoistName));
|
|
117
|
+
}
|
|
118
|
+
f.path.remove();
|
|
119
|
+
ctx.applied(ID);
|
|
120
|
+
ctx.warn(f.line, CODE,
|
|
121
|
+
`JSON.parse(${JSON.stringify(truncate(f.literal.value))}) hoisted to module scope  shared mutable object`);
|
|
122
|
+
idx++;
|
|
123
|
+
}
|
|
124
|
+
void idx;
|
|
125
|
+
|
|
126
|
+
body.splice(lastImport + 1, 0, ...newStmts);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function truncate(s) {
|
|
130
|
+
return s.length > 40 ? s.slice(0, 37) + '...' : s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Walk from a reference up to the nearest statement; every intermediate node
|
|
135
|
+
* must be a read-safe context. When the value escapes into a call argument,
|
|
136
|
+
* return, throw, etc., the escape is safe only if a member dereference happened
|
|
137
|
+
* on the way AND the JSON value at that path is a primitive.
|
|
138
|
+
*/
|
|
139
|
+
function isPureReadUse(ref, name, parsed) {
|
|
140
|
+
let p = ref.parentPath;
|
|
141
|
+
let prev = ref.node;
|
|
142
|
+
const propPath = []; // property names dereferenced so far
|
|
143
|
+
const escapeSafe = () => {
|
|
144
|
+
if (propPath.length === 0) return false; // the raw object escapes
|
|
145
|
+
let v = parsed;
|
|
146
|
+
for (const k of propPath) {
|
|
147
|
+
if (v === null || typeof v !== 'object') return true; // dereferenced past a primitive
|
|
148
|
+
v = v[k];
|
|
149
|
+
}
|
|
150
|
+
return v === null || typeof v !== 'object'; // primitive value flows: a copy
|
|
151
|
+
};
|
|
152
|
+
while (p && !p.isStatement()) {
|
|
153
|
+
const n = p.node;
|
|
154
|
+
if (t.isMemberExpression(n) || t.isOptionalMemberExpression(n)) {
|
|
155
|
+
if (n.object !== prev) return { ok: false, reason: 'used as a property key' };
|
|
156
|
+
if (t.isIdentifier(n.property)) propPath.push(n.property.name);
|
|
157
|
+
else if (t.isStringLiteral(n.property)) propPath.push(n.property.value);
|
|
158
|
+
// A method call on the parsed object could mutate it.
|
|
159
|
+
const pp = p.parentPath;
|
|
160
|
+
if (pp && t.isCallExpression(pp.node) && pp.node.callee === n) {
|
|
161
|
+
return { ok: false, reason: 'method called on the parsed object' };
|
|
162
|
+
}
|
|
163
|
+
if (pp && (t.isAssignmentExpression(pp.node) && pp.node.left === n)) {
|
|
164
|
+
return { ok: false, reason: 'property of the parsed object is written' };
|
|
165
|
+
}
|
|
166
|
+
if (pp && (t.isUpdateExpression(pp.node) && pp.node.argument === n)) {
|
|
167
|
+
return { ok: false, reason: 'property of the parsed object is updated' };
|
|
168
|
+
}
|
|
169
|
+
if (pp && t.isUnaryExpression(pp.node) && pp.node.operator === 'delete' && pp.node.argument === n) {
|
|
170
|
+
return { ok: false, reason: 'property of the parsed object is deleted' };
|
|
171
|
+
}
|
|
172
|
+
prev = n;
|
|
173
|
+
p = p.parentPath;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (t.isBinaryExpression(n) || t.isLogicalExpression(n) || t.isUnaryExpression(n) ||
|
|
177
|
+
t.isConditionalExpression(n) || t.isTemplateLiteral(n)) {
|
|
178
|
+
if (t.isUnaryExpression(n) && n.operator === 'delete') {
|
|
179
|
+
return { ok: false, reason: 'delete of parsed object property' };
|
|
180
|
+
}
|
|
181
|
+
prev = n;
|
|
182
|
+
p = p.parentPath;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (t.isExpressionStatement(n) && n.expression === prev) break;
|
|
186
|
+
if (t.isVariableDeclarator(n) && n.init === prev) {
|
|
187
|
+
return { ok: false, reason: 'aliased to another binding' };
|
|
188
|
+
}
|
|
189
|
+
if (t.isCallExpression(n) || t.isNewExpression(n)) {
|
|
190
|
+
return escapeSafe() ? { ok: true } : { ok: false, reason: 'parsed object (or an object-valued property) passed to a call' };
|
|
191
|
+
}
|
|
192
|
+
if (t.isReturnStatement(n) || t.isThrowStatement(n) || t.isSpreadElement(n) ||
|
|
193
|
+
t.isForOfStatement(n) || t.isForInStatement(n) || t.isArrayExpression(n) ||
|
|
194
|
+
t.isObjectProperty(n)) {
|
|
195
|
+
return escapeSafe() ? { ok: true } : { ok: false, reason: 'parsed object escapes by reference' };
|
|
196
|
+
}
|
|
197
|
+
return { ok: false, reason: `used in unsupported context (${n.type})` };
|
|
198
|
+
}
|
|
199
|
+
if (!p) return { ok: false, reason: 'no statement context' };
|
|
200
|
+
return { ok: true };
|
|
201
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R3 try-catch-strip — RISKY, hot functions only.
|
|
3
|
+
*
|
|
4
|
+
* Removes `try { ... } catch {}` when the try body contains no calls (and the
|
|
5
|
+
* catch body is empty). The catch becomes dead weight: with no calls in the
|
|
6
|
+
* try body, the only throws left are engine-level pathological ones.
|
|
7
|
+
*
|
|
8
|
+
* Failure mode: a pathological engine-level throw now crashes the pack script
|
|
9
|
+
* instead of being swallowed.
|
|
10
|
+
*
|
|
11
|
+
* Bail conditions: finally present, catch body non-empty, try body contains
|
|
12
|
+
* CallExpression / NewExpression / AwaitExpression / YieldExpression /
|
|
13
|
+
* tagged templates / optional calls. Every application warns.
|
|
14
|
+
*/
|
|
15
|
+
import traverse from '@babel/traverse';
|
|
16
|
+
import * as t from '@babel/types';
|
|
17
|
+
import { walkSameFunction } from '../pass-utils.js';
|
|
18
|
+
|
|
19
|
+
const _traverse = traverse.default || traverse;
|
|
20
|
+
|
|
21
|
+
export const ID = 'try-catch-strip';
|
|
22
|
+
export const CODE = 'R3';
|
|
23
|
+
|
|
24
|
+
export default function tryCatchStrip(programPath, ctx) {
|
|
25
|
+
programPath.traverse({
|
|
26
|
+
TryStatement(path) {
|
|
27
|
+
const node = path.node;
|
|
28
|
+
const line = node.loc ? node.loc.start.line : 0;
|
|
29
|
+
const fnParent = path.getFunctionParent();
|
|
30
|
+
if (!fnParent || !ctx.isHot(fnParent.node)) return;
|
|
31
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
32
|
+
|
|
33
|
+
if (node.finalizer) {
|
|
34
|
+
ctx.bail(line, CODE, 'finally present');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!node.handler) return; // try/finally handled above
|
|
38
|
+
if (node.handler.body.body.length > 0) {
|
|
39
|
+
ctx.bail(line, CODE, 'catch body is not empty');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let hasCall = false;
|
|
43
|
+
walkSameFunction(node.block, (n) => {
|
|
44
|
+
if (t.isCallExpression(n) || t.isNewExpression(n) || t.isTaggedTemplateExpression(n) ||
|
|
45
|
+
t.isOptionalCallExpression(n) || t.isAwaitExpression(n) || t.isYieldExpression(n)) {
|
|
46
|
+
hasCall = true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
if (hasCall) {
|
|
52
|
+
ctx.bail(line, CODE, 'try body contains calls');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
path.replaceWithMultiple(node.block.body);
|
|
56
|
+
ctx.applied(ID);
|
|
57
|
+
ctx.warn(line, CODE,
|
|
58
|
+
'try/catch removed in hot function — engine-level throws now propagate');
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R4 getter-cache-cross-barrier — RISKY.
|
|
3
|
+
*
|
|
4
|
+
* B1 (cache-getter-chains), but allowed to cross call and assignment barriers
|
|
5
|
+
* within one synchronous callback invocation. Await/yield remain barriers.
|
|
6
|
+
*
|
|
7
|
+
* Failure mode: if an intervening call moves or modifies the entity (teleport,
|
|
8
|
+
* damage, dimension change), cached values are stale — e.g. teleport then read
|
|
9
|
+
* cached `location`. Every hoisted chain emits a warning.
|
|
10
|
+
*/
|
|
11
|
+
import traverse from '@babel/traverse';
|
|
12
|
+
import b1Impl from './b1-cache-getter-chains.js';
|
|
13
|
+
|
|
14
|
+
const _traverse = traverse.default || traverse;
|
|
15
|
+
|
|
16
|
+
export const ID = 'getter-cache-cross-barrier';
|
|
17
|
+
export const CODE = 'R4';
|
|
18
|
+
|
|
19
|
+
export default function getterCacheCrossBarrier(programPath, ctx) {
|
|
20
|
+
b1Impl(programPath, ctx, {
|
|
21
|
+
id: ID,
|
|
22
|
+
code: CODE,
|
|
23
|
+
crossBarriers: true,
|
|
24
|
+
onApply: (line, links) => {
|
|
25
|
+
ctx.warn(line, CODE,
|
|
26
|
+
`chain .${links} cached across call barriers — stale if an intervening call moves/modifies the entity`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|