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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S1 const-fold — fold constant expressions.
|
|
3
|
+
*
|
|
4
|
+
* Folds: literal arithmetic / comparisons / bitwise ops, string concatenation,
|
|
5
|
+
* typeof on literals, `!` on literals, template literals whose interpolations
|
|
6
|
+
* are all literals, and logical expressions whose left operand is a literal.
|
|
7
|
+
*
|
|
8
|
+
* Visitors run in the exit phase so children fold before their parents and
|
|
9
|
+
* nested literal expressions cascade in a single traversal.
|
|
10
|
+
*
|
|
11
|
+
* Never folds across identifiers or function calls. Never produces -0, NaN or
|
|
12
|
+
* non-finite numbers (kept in original form to avoid serialization surprises).
|
|
13
|
+
*/
|
|
14
|
+
import traverse from '@babel/traverse';
|
|
15
|
+
import * as t from '@babel/types';
|
|
16
|
+
import { literalValue, evalBinary, valueToLiteralNode, isNumericResultSafe } from '../pass-utils.js';
|
|
17
|
+
|
|
18
|
+
const _traverse = traverse.default || traverse;
|
|
19
|
+
|
|
20
|
+
export const ID = 'const-fold';
|
|
21
|
+
export const CODE = 'S1';
|
|
22
|
+
|
|
23
|
+
export default function constFold(programPath, ctx) {
|
|
24
|
+
programPath.traverse({
|
|
25
|
+
BinaryExpression: {
|
|
26
|
+
exit(path) {
|
|
27
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
28
|
+
const { left, right, operator } = path.node;
|
|
29
|
+
const lv = literalValue(left);
|
|
30
|
+
const rv = literalValue(right);
|
|
31
|
+
if (!lv || !rv) return;
|
|
32
|
+
// No cross-type `in`/`instanceof` (need objects); shifts need integers.
|
|
33
|
+
if (operator === 'in' || operator === 'instanceof') return;
|
|
34
|
+
if (['<<', '>>', '>>>'].includes(operator) && (!Number.isInteger(lv.value) || !Number.isInteger(rv.value))) return;
|
|
35
|
+
const v = evalBinary(operator, lv, rv);
|
|
36
|
+
if (v === undefined) return;
|
|
37
|
+
if (typeof v === 'number' && !isNumericResultSafe(v)) return;
|
|
38
|
+
const node = valueToLiteralNode(v);
|
|
39
|
+
if (!node) return;
|
|
40
|
+
path.replaceWith(node);
|
|
41
|
+
ctx.applied(ID);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
UnaryExpression: {
|
|
46
|
+
exit(path) {
|
|
47
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
48
|
+
const { operator, argument } = path.node;
|
|
49
|
+
if (operator === 'typeof') {
|
|
50
|
+
if (t.isStringLiteral(argument)) {
|
|
51
|
+
path.replaceWith(t.stringLiteral('string'));
|
|
52
|
+
ctx.applied(ID);
|
|
53
|
+
} else if (t.isNumericLiteral(argument)) {
|
|
54
|
+
path.replaceWith(t.stringLiteral('number'));
|
|
55
|
+
ctx.applied(ID);
|
|
56
|
+
} else if (t.isBooleanLiteral(argument)) {
|
|
57
|
+
path.replaceWith(t.stringLiteral('boolean'));
|
|
58
|
+
ctx.applied(ID);
|
|
59
|
+
} else if (t.isNullLiteral(argument)) {
|
|
60
|
+
path.replaceWith(t.stringLiteral('object'));
|
|
61
|
+
ctx.applied(ID);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (operator === '!') {
|
|
66
|
+
const lv = literalValue(argument);
|
|
67
|
+
if (lv && typeof lv.value !== 'object') {
|
|
68
|
+
path.replaceWith(t.booleanLiteral(!lv.value));
|
|
69
|
+
ctx.applied(ID);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (operator === '~') {
|
|
74
|
+
const lv = literalValue(argument);
|
|
75
|
+
if (lv && typeof lv.value === 'number' && Number.isInteger(lv.value)) {
|
|
76
|
+
const v = ~lv.value;
|
|
77
|
+
if (isNumericResultSafe(v)) {
|
|
78
|
+
path.replaceWith(t.numericLiteral(v));
|
|
79
|
+
ctx.applied(ID);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// `-x` / `+x` left as-is: literals are canonical already.
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
TemplateLiteral: {
|
|
89
|
+
exit(path) {
|
|
90
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
91
|
+
const parent = path.parent;
|
|
92
|
+
if (t.isTaggedTemplateExpression(parent) && parent.tag === path.node) return;
|
|
93
|
+
const { quasis, expressions } = path.node;
|
|
94
|
+
if (expressions.length === 0) return; // nothing to fold
|
|
95
|
+
for (const e of expressions) {
|
|
96
|
+
const lv = literalValue(e);
|
|
97
|
+
if (!lv) return;
|
|
98
|
+
}
|
|
99
|
+
let s = '';
|
|
100
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
101
|
+
s += quasis[i].value.raw;
|
|
102
|
+
if (i < expressions.length) {
|
|
103
|
+
const lv = literalValue(expressions[i]);
|
|
104
|
+
s += String(lv.value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
path.replaceWith(t.stringLiteral(s));
|
|
108
|
+
ctx.applied(ID);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
LogicalExpression: {
|
|
113
|
+
exit(path) {
|
|
114
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
115
|
+
const { left, operator } = path.node;
|
|
116
|
+
const lv = literalValue(left);
|
|
117
|
+
if (!lv) return;
|
|
118
|
+
const truthy = typeof lv.value === 'string' ? lv.value.length > 0 : !!lv.value;
|
|
119
|
+
const isNullish = lv.kind === 'null';
|
|
120
|
+
let repl = null;
|
|
121
|
+
if (operator === '&&') repl = truthy ? path.node.right : path.node.left;
|
|
122
|
+
else if (operator === '||') repl = truthy ? path.node.left : path.node.right;
|
|
123
|
+
else if (operator === '??') repl = isNullish ? path.node.right : path.node.left;
|
|
124
|
+
if (!repl) return;
|
|
125
|
+
path.replaceWith(repl);
|
|
126
|
+
ctx.applied(ID);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S2 dead-code — remove provably unreachable code.
|
|
3
|
+
*
|
|
4
|
+
* 1. Statements after an unconditional return/throw/break/continue in a block.
|
|
5
|
+
* 2. Branches made constant by S1 (if / conditional / while-false / for-false).
|
|
6
|
+
* 3. Unused local declarations whose initializers are side-effect-free
|
|
7
|
+
* (no calls, no member writes, no await/yield, no delete).
|
|
8
|
+
*/
|
|
9
|
+
import traverse from '@babel/traverse';
|
|
10
|
+
import * as t from '@babel/types';
|
|
11
|
+
import { isPureExpression, isStaticLiteral, literalValue } from '../pass-utils.js';
|
|
12
|
+
|
|
13
|
+
const _traverse = traverse.default || traverse;
|
|
14
|
+
|
|
15
|
+
export const ID = 'dead-code';
|
|
16
|
+
export const CODE = 'S2';
|
|
17
|
+
|
|
18
|
+
function isTerminal(stmt) {
|
|
19
|
+
return (
|
|
20
|
+
t.isReturnStatement(stmt) ||
|
|
21
|
+
t.isThrowStatement(stmt) ||
|
|
22
|
+
t.isBreakStatement(stmt) ||
|
|
23
|
+
t.isContinueStatement(stmt)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function blockBodyOf(stmt) {
|
|
28
|
+
if (t.isBlockStatement(stmt)) return stmt.body;
|
|
29
|
+
return [stmt];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function allExit(stmts) {
|
|
33
|
+
if (stmts.length === 0) return false;
|
|
34
|
+
const last = stmts[stmts.length - 1];
|
|
35
|
+
if (!isTerminal(last)) return false;
|
|
36
|
+
for (const s of stmts) {
|
|
37
|
+
if (t.isIfStatement(s) || t.isSwitchStatement(s)) return false; // may fall through around it
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function deadCode(programPath, ctx) {
|
|
43
|
+
// Step 1: code after a terminal statement.
|
|
44
|
+
programPath.traverse({
|
|
45
|
+
BlockStatement(path) {
|
|
46
|
+
const body = path.node.body;
|
|
47
|
+
let cut = -1;
|
|
48
|
+
for (let i = 0; i < body.length; i++) {
|
|
49
|
+
if (isTerminal(body[i])) { cut = i; break; }
|
|
50
|
+
}
|
|
51
|
+
if (cut >= 0 && cut < body.length - 1) {
|
|
52
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
53
|
+
const removed = body.length - 1 - cut;
|
|
54
|
+
path.node.body = body.slice(0, cut + 1);
|
|
55
|
+
ctx.applied(ID);
|
|
56
|
+
// record one bail-style note is unnecessary: this is an application.
|
|
57
|
+
void removed;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Step 2: constant branches.
|
|
63
|
+
programPath.traverse({
|
|
64
|
+
IfStatement(path) {
|
|
65
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
66
|
+
const test = path.node.test;
|
|
67
|
+
if (!isStaticLiteral(test)) return;
|
|
68
|
+
const lv = literalValue(test);
|
|
69
|
+
const truthy = lv ? (typeof lv.value === 'string' ? lv.value.length > 0 : !!lv.value) : null;
|
|
70
|
+
if (truthy === null) return;
|
|
71
|
+
const keep = truthy ? path.node.consequent : path.node.alternate;
|
|
72
|
+
if (!keep) {
|
|
73
|
+
path.remove();
|
|
74
|
+
} else {
|
|
75
|
+
path.replaceWithMultiple(blockBodyOf(keep));
|
|
76
|
+
}
|
|
77
|
+
ctx.applied(ID);
|
|
78
|
+
},
|
|
79
|
+
ConditionalExpression(path) {
|
|
80
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
81
|
+
const lv = literalValue(path.node.test);
|
|
82
|
+
if (!lv) return;
|
|
83
|
+
const truthy = typeof lv.value === 'string' ? lv.value.length > 0 : !!lv.value;
|
|
84
|
+
path.replaceWith(truthy ? path.node.consequent : path.node.alternate);
|
|
85
|
+
ctx.applied(ID);
|
|
86
|
+
},
|
|
87
|
+
WhileStatement(path) {
|
|
88
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
89
|
+
const lv = literalValue(path.node.test);
|
|
90
|
+
if (!lv) return;
|
|
91
|
+
const truthy = typeof lv.value === 'string' ? lv.value.length > 0 : !!lv.value;
|
|
92
|
+
if (!truthy) {
|
|
93
|
+
path.remove();
|
|
94
|
+
ctx.applied(ID);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
ForStatement(path) {
|
|
98
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
99
|
+
if (!path.node.test) return;
|
|
100
|
+
const lv = literalValue(path.node.test);
|
|
101
|
+
if (!lv) return;
|
|
102
|
+
const truthy = typeof lv.value === 'string' ? lv.value.length > 0 : !!lv.value;
|
|
103
|
+
if (!truthy) {
|
|
104
|
+
path.remove();
|
|
105
|
+
ctx.applied(ID);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Step 3: unused local declarations with pure initializers.
|
|
111
|
+
programPath.scope.crawl();
|
|
112
|
+
const removals = [];
|
|
113
|
+
const scopeSet = new Set();
|
|
114
|
+
programPath.traverse({
|
|
115
|
+
enter(p) {
|
|
116
|
+
if (p.scope) scopeSet.add(p.scope);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
scopeSet.add(programPath.scope);
|
|
120
|
+
const scopes = [...scopeSet];
|
|
121
|
+
|
|
122
|
+
for (const scope of scopes) {
|
|
123
|
+
for (const name of Object.keys(scope.bindings)) {
|
|
124
|
+
const binding = scope.bindings[name];
|
|
125
|
+
if (binding.references > 0) continue;
|
|
126
|
+
const bp = binding.path;
|
|
127
|
+
let declPath = null;
|
|
128
|
+
if (bp.isVariableDeclarator()) declPath = bp;
|
|
129
|
+
else if (bp.isFunctionDeclaration()) declPath = bp;
|
|
130
|
+
if (!declPath) continue;
|
|
131
|
+
// Never touch anything exported.
|
|
132
|
+
let exported = false;
|
|
133
|
+
let up = declPath.parentPath;
|
|
134
|
+
while (up) {
|
|
135
|
+
if (up.isExportNamedDeclaration() || up.isExportDefaultDeclaration()) { exported = true; break; }
|
|
136
|
+
up = up.parentPath;
|
|
137
|
+
}
|
|
138
|
+
if (exported) continue;
|
|
139
|
+
if (declPath.isVariableDeclarator()) {
|
|
140
|
+
const init = declPath.node.init;
|
|
141
|
+
if (!init) {
|
|
142
|
+
// `var x;` unused — remove the declarator.
|
|
143
|
+
removals.push({ kind: 'declarator', path: declPath });
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!isPureExpression(init)) continue;
|
|
147
|
+
removals.push({ kind: 'declarator', path: declPath });
|
|
148
|
+
} else {
|
|
149
|
+
// Respect explicit user intent: a pragma-hot function is never dead.
|
|
150
|
+
const fnLoc = declPath.node.loc;
|
|
151
|
+
if (fnLoc && ctx.pragmas && ctx.pragmas.isHotLine(fnLoc.start.line)) continue;
|
|
152
|
+
removals.push({ kind: 'function', path: declPath });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply deterministically by source position.
|
|
158
|
+
removals.sort((a, b) => {
|
|
159
|
+
const al = a.path.node.loc ? a.path.node.loc.start.line : 0;
|
|
160
|
+
const bl = b.path.node.loc ? b.path.node.loc.start.line : 0;
|
|
161
|
+
return al - bl || a.path.node.start - b.path.node.start;
|
|
162
|
+
});
|
|
163
|
+
for (const r of removals) {
|
|
164
|
+
if (ctx.isDisabled(r.path, ID, CODE)) continue;
|
|
165
|
+
if (!r.path.node) continue; // parent may already be gone
|
|
166
|
+
try {
|
|
167
|
+
if (r.kind === 'declarator') {
|
|
168
|
+
const decl = r.path.parentPath; // VariableDeclaration
|
|
169
|
+
if (!decl || !decl.node) continue;
|
|
170
|
+
if (decl.node.declarations.length === 1) decl.remove();
|
|
171
|
+
else {
|
|
172
|
+
const idx = decl.node.declarations.indexOf(r.path.node);
|
|
173
|
+
if (idx >= 0) decl.node.declarations.splice(idx, 1);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
r.path.remove();
|
|
177
|
+
}
|
|
178
|
+
ctx.applied(ID);
|
|
179
|
+
} catch {
|
|
180
|
+
// Node already removed by an overlapping removal; ignore.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 bool-simplify — boolean-tautology simplification.
|
|
3
|
+
*
|
|
4
|
+
* - `x ? true : false` -> `!!x`
|
|
5
|
+
* - `if (x) return true; else return false;` -> `return !!x;`
|
|
6
|
+
* - `x === true` / `x !== false` -> `x` (x statically boolean-typed)
|
|
7
|
+
* - `x === false` / `x !== true` -> `!x` (duals, same precondition)
|
|
8
|
+
*
|
|
9
|
+
* Bails whenever `x` is not statically boolean-typed (result of a comparison,
|
|
10
|
+
* a `!` expression, a Boolean() call, or an and/or of such).
|
|
11
|
+
*/
|
|
12
|
+
import traverse from '@babel/traverse';
|
|
13
|
+
import * as t from '@babel/types';
|
|
14
|
+
import { isBooleanTyped } from '../pass-utils.js';
|
|
15
|
+
|
|
16
|
+
const _traverse = traverse.default || traverse;
|
|
17
|
+
|
|
18
|
+
export const ID = 'bool-simplify';
|
|
19
|
+
export const CODE = 'S3';
|
|
20
|
+
|
|
21
|
+
function notNot(expr) {
|
|
22
|
+
return t.unaryExpression('!', t.unaryExpression('!', expr));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function singleReturn(stmt, value) {
|
|
26
|
+
if (t.isReturnStatement(stmt)) {
|
|
27
|
+
return t.isBooleanLiteral(stmt.argument) && stmt.argument.value === value;
|
|
28
|
+
}
|
|
29
|
+
if (t.isBlockStatement(stmt) && stmt.body.length === 1) {
|
|
30
|
+
const s = stmt.body[0];
|
|
31
|
+
return t.isReturnStatement(s) && t.isBooleanLiteral(s.argument) && s.argument.value === value;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function boolSimplify(programPath, ctx) {
|
|
37
|
+
programPath.traverse({
|
|
38
|
+
ConditionalExpression(path) {
|
|
39
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
40
|
+
const { test, consequent, alternate } = path.node;
|
|
41
|
+
if (t.isBooleanLiteral(consequent) && consequent.value === true &&
|
|
42
|
+
t.isBooleanLiteral(alternate) && alternate.value === false) {
|
|
43
|
+
path.replaceWith(notNot(test));
|
|
44
|
+
ctx.applied(ID);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
IfStatement(path) {
|
|
48
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
49
|
+
const { test, consequent, alternate } = path.node;
|
|
50
|
+
if (!alternate) return;
|
|
51
|
+
if (singleReturn(consequent, true) && singleReturn(alternate, false)) {
|
|
52
|
+
path.replaceWith(t.returnStatement(notNot(test)));
|
|
53
|
+
ctx.applied(ID);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
BinaryExpression(path) {
|
|
57
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
58
|
+
const { left, right, operator } = path.node;
|
|
59
|
+
// Normalize operand order: find (expr, literal) pair.
|
|
60
|
+
let expr = null;
|
|
61
|
+
if (t.isBooleanLiteral(right)) expr = left;
|
|
62
|
+
else if (t.isBooleanLiteral(left)) expr = right;
|
|
63
|
+
if (!expr) return;
|
|
64
|
+
const lit = t.isBooleanLiteral(right) ? right.value : left.value;
|
|
65
|
+
if (!isBooleanTyped(expr)) {
|
|
66
|
+
ctx.bail(path.node.loc ? path.node.loc.start.line : 0, CODE,
|
|
67
|
+
'operand not statically boolean-typed');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// x === true -> x ; x !== false -> x ; x === false -> !x ; x !== true -> !x
|
|
71
|
+
const replacement =
|
|
72
|
+
(operator === '===' && lit === true) || (operator === '!==' && lit === false)
|
|
73
|
+
? expr
|
|
74
|
+
: (operator === '===' && lit === false) || (operator === '!==' && lit === true)
|
|
75
|
+
? t.unaryExpression('!', expr)
|
|
76
|
+
: null;
|
|
77
|
+
if (!replacement) return;
|
|
78
|
+
path.replaceWith(replacement);
|
|
79
|
+
ctx.applied(ID);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S4 for-of-to-indexed — convert `for (const v of it)` to an indexed loop,
|
|
3
|
+
* only when the iterable is statically provably an array.
|
|
4
|
+
*
|
|
5
|
+
* Bail conditions:
|
|
6
|
+
* - `for await`
|
|
7
|
+
* - iterable not provably an array (host returns, Map/Set, strings, generators)
|
|
8
|
+
* - destructuring binding with rest elements
|
|
9
|
+
* - a scope binding named `Symbol` (iterator protocol could be shadowed)
|
|
10
|
+
* - non-identifier / non-array-pattern bindings
|
|
11
|
+
*
|
|
12
|
+
* Semantics preserved:
|
|
13
|
+
* - per-iteration binding (const inside the loop body)
|
|
14
|
+
* - live `.length` checks (pushes during iteration extend the loop, exactly
|
|
15
|
+
* like the array iterator)
|
|
16
|
+
* - break/continue/return/labels untouched
|
|
17
|
+
*/
|
|
18
|
+
import traverse from '@babel/traverse';
|
|
19
|
+
import * as t from '@babel/types';
|
|
20
|
+
import { provablyArray } from '../pass-utils.js';
|
|
21
|
+
|
|
22
|
+
const _traverse = traverse.default || traverse;
|
|
23
|
+
|
|
24
|
+
export const ID = 'for-of-to-indexed';
|
|
25
|
+
export const CODE = 'S4';
|
|
26
|
+
|
|
27
|
+
function indexExpr(arrName, idxName, offset) {
|
|
28
|
+
const idx = offset === 0 ? t.identifier(idxName) : t.binaryExpression('+', t.identifier(idxName), t.numericLiteral(offset));
|
|
29
|
+
return t.memberExpression(t.identifier(arrName), idx, true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function forOfToIndexed(programPath, ctx) {
|
|
33
|
+
programPath.traverse({
|
|
34
|
+
ForOfStatement(path) {
|
|
35
|
+
const node = path.node;
|
|
36
|
+
const line = node.loc ? node.loc.start.line : 0;
|
|
37
|
+
if (ctx.isDisabled(path, ID, CODE)) return;
|
|
38
|
+
|
|
39
|
+
const fail = (reason) => {
|
|
40
|
+
ctx.bail(line, CODE, reason);
|
|
41
|
+
return;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (node.await) return fail('for await loop');
|
|
45
|
+
if (path.scope.getBinding('Symbol')) return fail('Symbol shadowed in scope');
|
|
46
|
+
|
|
47
|
+
if (!t.isVariableDeclaration(node.left) || node.left.declarations.length !== 1) {
|
|
48
|
+
return fail('loop head is not a single declarator');
|
|
49
|
+
}
|
|
50
|
+
const declarator = node.left.declarations[0];
|
|
51
|
+
const id = declarator.id;
|
|
52
|
+
const kind = node.left.kind;
|
|
53
|
+
|
|
54
|
+
if (t.isIdentifier(id)) {
|
|
55
|
+
if (id.name === 'arguments' || id.name === 'eval') return fail('loop variable is arguments/eval');
|
|
56
|
+
// Reassignment of a let/var loop variable is fine (translate to let).
|
|
57
|
+
} else if (t.isArrayPattern(id)) {
|
|
58
|
+
for (const el of id.elements) {
|
|
59
|
+
if (el && t.isRestElement(el)) return fail('destructuring with rest element');
|
|
60
|
+
if (el && !t.isIdentifier(el) && !t.isAssignmentPattern(el)) {
|
|
61
|
+
return fail('unsupported destructuring element');
|
|
62
|
+
}
|
|
63
|
+
if (el && t.isAssignmentPattern(el) && !t.isIdentifier(el.left)) {
|
|
64
|
+
return fail('unsupported destructuring default target');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
return fail('loop variable is not an identifier or array pattern');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const prov = provablyArray(node.right, path);
|
|
72
|
+
if (!prov.array) return fail('iterable not provably an array');
|
|
73
|
+
|
|
74
|
+
// Choose the array reference: direct identifier when immutable, else a hoisted const.
|
|
75
|
+
let arrName;
|
|
76
|
+
const pre = [];
|
|
77
|
+
if (t.isIdentifier(node.right)) {
|
|
78
|
+
const binding = path.scope.getBinding(node.right.name);
|
|
79
|
+
if (binding && binding.constantViolations.length === 0) {
|
|
80
|
+
arrName = node.right.name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!arrName) {
|
|
84
|
+
arrName = path.scope.generateUid('opArr');
|
|
85
|
+
pre.push(t.variableDeclaration('const', [t.variableDeclarator(t.identifier(arrName), node.right)]));
|
|
86
|
+
}
|
|
87
|
+
const idxName = path.scope.generateUid('opI');
|
|
88
|
+
|
|
89
|
+
// Binding declarations inside the loop body.
|
|
90
|
+
const bindDecls = [];
|
|
91
|
+
const declKind = kind === 'const' ? 'const' : 'let';
|
|
92
|
+
const pushDecl = (name, readExpr, deflt) => {
|
|
93
|
+
if (deflt) {
|
|
94
|
+
bindDecls.push(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(name), readExpr)]));
|
|
95
|
+
bindDecls.push(
|
|
96
|
+
t.ifStatement(
|
|
97
|
+
t.binaryExpression('===', t.identifier(name), t.identifier('undefined')),
|
|
98
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier(name), deflt))
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
bindDecls.push(t.variableDeclaration(declKind, [t.variableDeclarator(t.identifier(name), readExpr)]));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (t.isIdentifier(id)) {
|
|
106
|
+
pushDecl(id.name, indexExpr(arrName, idxName, 0), null);
|
|
107
|
+
} else {
|
|
108
|
+
// Array pattern: read the element once, then index into it.
|
|
109
|
+
const elemUid = path.scope.generateUid('opEl');
|
|
110
|
+
bindDecls.push(t.variableDeclaration('const', [
|
|
111
|
+
t.variableDeclarator(t.identifier(elemUid), indexExpr(arrName, idxName, 0)),
|
|
112
|
+
]));
|
|
113
|
+
id.elements.forEach((el, k) => {
|
|
114
|
+
if (!el) return; // hole
|
|
115
|
+
const elemRead = t.memberExpression(t.identifier(elemUid), t.numericLiteral(k), true);
|
|
116
|
+
if (t.isIdentifier(el)) pushDecl(el.name, elemRead, null);
|
|
117
|
+
else pushDecl(el.left.name, elemRead, el.right);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Loop body: binding decls + original body (wrapped when it was a block).
|
|
122
|
+
const bodyStmts = [...bindDecls];
|
|
123
|
+
if (t.isBlockStatement(node.body)) {
|
|
124
|
+
bodyStmts.push(node.body); // nested block preserves shadowing
|
|
125
|
+
} else {
|
|
126
|
+
bodyStmts.push(node.body);
|
|
127
|
+
}
|
|
128
|
+
const forNode = t.forStatement(
|
|
129
|
+
t.variableDeclaration('let', [t.variableDeclarator(t.identifier(idxName), t.numericLiteral(0))]),
|
|
130
|
+
t.binaryExpression('<', t.identifier(idxName), t.memberExpression(t.identifier(arrName), t.identifier('length'))),
|
|
131
|
+
t.updateExpression('++', t.identifier(idxName)),
|
|
132
|
+
t.blockStatement(bodyStmts)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const replacement = pre.length > 0 ? t.blockStatement([...pre, forNode]) : forNode;
|
|
136
|
+
path.replaceWith(replacement);
|
|
137
|
+
ctx.applied(ID);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|