storm-lua-minify 0.2.0 → 0.9.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 -21
- package/README.md +157 -37
- package/dist/aggregateSpecialization.js +406 -0
- package/dist/ast2lua.js +223 -82
- package/dist/astWalk.js +162 -0
- package/dist/callGraph.js +372 -0
- package/dist/cli.js +53 -32
- package/dist/cliOptions.js +36 -0
- package/dist/cliProgress.js +87 -0
- package/dist/config.js +73 -0
- package/dist/constantFold.js +798 -0
- package/dist/controlFlow.js +266 -0
- package/dist/functionRewrites.js +580 -0
- package/dist/generatedAst.js +108 -0
- package/dist/generatedNode.js +23 -0
- package/dist/globalRename.js +35 -0
- package/dist/interproceduralAnalysis.js +842 -0
- package/dist/interproceduralConstants.js +120 -0
- package/dist/keywordLocator.js +68 -0
- package/dist/linker.js +13 -0
- package/dist/luaString.js +157 -0
- package/dist/minifier.js +1243 -35
- package/dist/optimizerAnalysis.js +43 -0
- package/dist/optimizerDiagnostics.js +65 -0
- package/dist/optimizerFacts.js +529 -0
- package/dist/optimizerPass.js +96 -0
- package/dist/optimizerTransaction.js +56 -0
- package/dist/optimizerValueDomain.js +180 -0
- package/dist/options.js +233 -0
- package/dist/progress.js +2 -0
- package/dist/removeUnused.js +145 -0
- package/dist/renamer.js +236 -53
- package/dist/resolver.js +37 -14
- package/dist/runtimeEnvironment.js +105 -0
- package/dist/sourceMetadata.js +314 -0
- package/dist/statementDataflow.js +259 -0
- package/dist/statementScheduler.js +595 -0
- package/dist/symbolLiveness.js +92 -0
- package/dist/tableEffects.js +356 -0
- package/dist/transform.js +40 -0
- package/dist/valueFlow.js +409 -0
- package/dist/wholeProgramExports.js +646 -0
- package/dist/wholeProgramFieldRenames.js +583 -0
- package/dist/wholeProgramFields.js +672 -0
- package/dist/wholeProgramObjects.js +783 -0
- package/package.json +50 -33
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/publish.yml +0 -39
- package/.prettierignore +0 -3
- package/.prettierrc.json +0 -1
- package/dist/index.js +0 -27
- package/eslint.config.js +0 -29
- package/src/ast2lua.ts +0 -940
- package/src/cli.ts +0 -86
- package/src/linker.ts +0 -108
- package/src/minifier.ts +0 -284
- package/src/output.ts +0 -71
- package/src/renamer.ts +0 -134
- package/src/resolver.ts +0 -378
- package/test/circular-require.test.ts +0 -19
- package/test/fixtures/bare-require/main.lua +0 -2
- package/test/fixtures/bare-require/mod.lua +0 -1
- package/test/fixtures/bitwise-precedence/main.lua +0 -10
- package/test/fixtures/circular-require/a.lua +0 -2
- package/test/fixtures/circular-require/b.lua +0 -2
- package/test/fixtures/circular-require/main.lua +0 -2
- package/test/fixtures/dofile/greet.lua +0 -1
- package/test/fixtures/dofile/main.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_delta.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_echo.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_golf.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_india.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +0 -2
- package/test/fixtures/entry-scope-many-requires/main.lua +0 -25
- package/test/fixtures/multi-require/common.lua +0 -1
- package/test/fixtures/multi-require/main.lua +0 -3
- package/test/fixtures/nested-module/main.lua +0 -2
- package/test/fixtures/nested-module/sub/deep.lua +0 -1
- package/test/fixtures/require-call/main.lua +0 -2
- package/test/fixtures/require-call/mod.lua +0 -5
- package/test/fixtures/require-in-expression/main.lua +0 -1
- package/test/fixtures/require-in-expression/mod.lua +0 -1
- package/test/fixtures/require-string-call/main.lua +0 -2
- package/test/fixtures/require-string-call/mod.lua +0 -5
- package/test/fixtures/single-file/main.lua +0 -15
- package/test/identifier-collision.test.ts +0 -28
- package/test/lib/collision.ts +0 -131
- package/test/lib/helpers.ts +0 -124
- package/test/no-rename.test.ts +0 -19
- package/test/output.test.ts +0 -95
- package/test/precedence.test.ts +0 -28
- package/test/renamer.test.ts +0 -130
- package/test/resolver.test.ts +0 -206
- package/test/roundtrip.test.ts +0 -26
- package/test/snapshot.test.ts +0 -27
- package/test/snapshots/bare-require.sl.lua +0 -1
- package/test/snapshots/bitwise-precedence.sl.lua +0 -2
- package/test/snapshots/dofile.sl.lua +0 -1
- package/test/snapshots/entry-scope-many-requires.m.lua +0 -14
- package/test/snapshots/multi-require.m.lua +0 -4
- package/test/snapshots/multi-require.sl.lua +0 -1
- package/test/snapshots/nested-module.m.lua +0 -4
- package/test/snapshots/require-call.m.lua +0 -5
- package/test/snapshots/require-call.sl.lua +0 -1
- package/test/snapshots/require-in-expression.sl.lua +0 -1
- package/test/snapshots/require-string-call.m.lua +0 -5
- package/test/snapshots/single-file.sl.lua +0 -6
- package/test/sourcemap.test.ts +0 -105
- package/tsconfig.eslint.json +0 -8
- package/tsconfig.json +0 -111
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeUnusedLocals = removeUnusedLocals;
|
|
4
|
+
function rangeOf(node) {
|
|
5
|
+
return node.range;
|
|
6
|
+
}
|
|
7
|
+
function nestedBodies(statement) {
|
|
8
|
+
switch (statement.type) {
|
|
9
|
+
case "DoStatement":
|
|
10
|
+
case "WhileStatement":
|
|
11
|
+
case "RepeatStatement":
|
|
12
|
+
case "FunctionDeclaration":
|
|
13
|
+
case "ForNumericStatement":
|
|
14
|
+
case "ForGenericStatement":
|
|
15
|
+
return [statement.body];
|
|
16
|
+
case "IfStatement":
|
|
17
|
+
return statement.clauses.map((clause) => clause.body);
|
|
18
|
+
default:
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isStatementCall(expression) {
|
|
23
|
+
return (expression.type === "CallExpression" ||
|
|
24
|
+
expression.type === "TableCallExpression" ||
|
|
25
|
+
expression.type === "StringCallExpression");
|
|
26
|
+
}
|
|
27
|
+
function callStatement(expression) {
|
|
28
|
+
return {
|
|
29
|
+
type: "CallStatement",
|
|
30
|
+
expression,
|
|
31
|
+
loc: expression.loc,
|
|
32
|
+
range: rangeOf(expression),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function hasExternalReference(statement, references) {
|
|
36
|
+
const range = rangeOf(statement);
|
|
37
|
+
if (!range)
|
|
38
|
+
return references.length > 0;
|
|
39
|
+
return references.some((reference) => {
|
|
40
|
+
const position = rangeOf(reference)?.[0];
|
|
41
|
+
return (position === undefined || position < range[0] || position >= range[1]);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function removeFromBlock(body, resolved, metadata, facts, onRemoveLocalFunction, options = { removeLocals: true, removeFunctions: true }) {
|
|
45
|
+
let changed = false;
|
|
46
|
+
body.forEach((statement) => {
|
|
47
|
+
nestedBodies(statement).forEach((nested) => {
|
|
48
|
+
changed =
|
|
49
|
+
removeFromBlock(nested, resolved, metadata, facts, onRemoveLocalFunction, options) || changed;
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
const replacements = new Map();
|
|
53
|
+
body.forEach((statement) => {
|
|
54
|
+
if (metadata.annotationsOf(statement).keep)
|
|
55
|
+
return;
|
|
56
|
+
if (options.removeFunctions &&
|
|
57
|
+
statement.type === "FunctionDeclaration" &&
|
|
58
|
+
statement.isLocal &&
|
|
59
|
+
statement.identifier?.type === "Identifier") {
|
|
60
|
+
const symbol = resolved.symbolOf(statement.identifier);
|
|
61
|
+
if (symbol && !hasExternalReference(statement, symbol.references)) {
|
|
62
|
+
onRemoveLocalFunction?.(statement);
|
|
63
|
+
changed = true;
|
|
64
|
+
replacements.set(statement, []);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!options.removeLocals || statement.type !== "LocalStatement")
|
|
69
|
+
return;
|
|
70
|
+
const unused = statement.variables.map((variable) => {
|
|
71
|
+
const symbol = resolved.symbolOf(variable);
|
|
72
|
+
return Boolean(symbol && symbol.references.length === 0);
|
|
73
|
+
});
|
|
74
|
+
if (!unused.some(Boolean))
|
|
75
|
+
return;
|
|
76
|
+
if (unused.every(Boolean)) {
|
|
77
|
+
if (statement.init.every((expression) => facts.discardabilityOf(expression).discardable ||
|
|
78
|
+
isStatementCall(expression))) {
|
|
79
|
+
const calls = statement.init.filter(isStatementCall).map(callStatement);
|
|
80
|
+
changed = true;
|
|
81
|
+
replacements.set(statement, calls);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let variables = [...statement.variables];
|
|
86
|
+
let init = [...statement.init];
|
|
87
|
+
let unusedAfterPairRemoval = [...unused];
|
|
88
|
+
// 初期値なしなら全変数の値はnilで固定され、位置対応を維持する必要がない。
|
|
89
|
+
if (init.length === 0) {
|
|
90
|
+
variables = variables.filter((_, index) => !unused[index]);
|
|
91
|
+
unusedAfterPairRemoval = unused.filter((value) => !value);
|
|
92
|
+
}
|
|
93
|
+
// 要素数が一致するときだけ、変数とRHSの対応を崩さず安全なペアを抜ける。
|
|
94
|
+
if (init.length > 0 && variables.length === init.length) {
|
|
95
|
+
const retainedIndexes = variables
|
|
96
|
+
.map((_, index) => index)
|
|
97
|
+
.filter((index) => !(unused[index] && facts.discardabilityOf(init[index]).discardable));
|
|
98
|
+
variables = retainedIndexes.map((index) => variables[index]);
|
|
99
|
+
init = retainedIndexes.map((index) => init[index]);
|
|
100
|
+
unusedAfterPairRemoval = retainedIndexes.map((index) => unused[index]);
|
|
101
|
+
}
|
|
102
|
+
// 末尾の未使用名は、対応RHSを余剰式として評価させたまま名前だけ除ける。
|
|
103
|
+
let retainedVariableCount = variables.length;
|
|
104
|
+
while (retainedVariableCount > 0 &&
|
|
105
|
+
unusedAfterPairRemoval[retainedVariableCount - 1]) {
|
|
106
|
+
retainedVariableCount--;
|
|
107
|
+
}
|
|
108
|
+
variables = variables.slice(0, retainedVariableCount);
|
|
109
|
+
if (variables.length === statement.variables.length)
|
|
110
|
+
return;
|
|
111
|
+
changed = true;
|
|
112
|
+
const replacement = {
|
|
113
|
+
...statement,
|
|
114
|
+
variables,
|
|
115
|
+
init,
|
|
116
|
+
};
|
|
117
|
+
replacements.set(statement, [replacement]);
|
|
118
|
+
});
|
|
119
|
+
if (replacements.size > 0) {
|
|
120
|
+
const nextBody = [];
|
|
121
|
+
body.forEach((statement, index) => {
|
|
122
|
+
const replacement = replacements.get(statement);
|
|
123
|
+
if (!replacement) {
|
|
124
|
+
nextBody.push(statement);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (replacement.length > 0) {
|
|
128
|
+
metadata.replaceStatement(statement, replacement);
|
|
129
|
+
nextBody.push(...replacement);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const next = body.slice(index + 1).find((candidate) => {
|
|
133
|
+
const candidateReplacement = replacements.get(candidate);
|
|
134
|
+
return !candidateReplacement || candidateReplacement.length > 0;
|
|
135
|
+
});
|
|
136
|
+
const nextReplacement = next ? replacements.get(next) : undefined;
|
|
137
|
+
metadata.removeStatement(statement, nextReplacement?.[0] ?? next);
|
|
138
|
+
});
|
|
139
|
+
body.splice(0, body.length, ...nextBody);
|
|
140
|
+
}
|
|
141
|
+
return changed;
|
|
142
|
+
}
|
|
143
|
+
function removeUnusedLocals(chunk, resolved, metadata, facts, onRemoveLocalFunction, options) {
|
|
144
|
+
return removeFromBlock(chunk.body, resolved, metadata, facts, onRemoveLocalFunction, options);
|
|
145
|
+
}
|
package/dist/renamer.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAvailable = isAvailable;
|
|
4
|
+
exports.generateCandidate = generateCandidate;
|
|
3
5
|
exports.assignRenames = assignRenames;
|
|
6
|
+
const resolver_1 = require("./resolver");
|
|
4
7
|
const ast2lua_1 = require("./ast2lua");
|
|
8
|
+
const controlFlow_1 = require("./controlFlow");
|
|
9
|
+
const optimizerFacts_1 = require("./optimizerFacts");
|
|
10
|
+
const symbolLiveness_1 = require("./symbolLiveness");
|
|
5
11
|
function isAvailable(id, reserved) {
|
|
6
12
|
return id !== "self" && !(0, ast2lua_1.isKeyword)(id) && !reserved.has(id);
|
|
7
13
|
}
|
|
@@ -19,69 +25,246 @@ function generateCandidate(counter) {
|
|
|
19
25
|
}
|
|
20
26
|
return id;
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
function assignRenames(chunk, resolveResult, reserved,
|
|
29
|
+
// #8a: プログラム全体を横断して決定されたグローバル識別子の短縮名。
|
|
30
|
+
// 全モジュールに対して同じマップを渡すことで、モジュールをまたいで
|
|
31
|
+
// 共有される1つのランタイム束縛に一貫した短縮名を割り当てられる。
|
|
32
|
+
globalRenames, keepNames = new Set(), options = {}) {
|
|
33
|
+
const unavailableNames = new Set(reserved);
|
|
34
|
+
keepNames.forEach((symbol) => unavailableNames.add(symbol.name));
|
|
35
|
+
const variables = options.renameLocals === false
|
|
36
|
+
? []
|
|
37
|
+
: resolveResult.symbols.filter((symbol) => symbol.kind !== "label" &&
|
|
38
|
+
!symbol.implicit &&
|
|
39
|
+
!keepNames.has(symbol));
|
|
40
|
+
const variableGraph = buildVariableInterference(chunk, resolveResult, variables, options.allowLocalNameReuse === true, options.analysis);
|
|
41
|
+
const variableColors = colorGraph(variableGraph);
|
|
42
|
+
const variableNames = assignColorNames(variableColors, unavailableNames);
|
|
43
|
+
// Labels have a separate Lua namespace. Color them independently so a label
|
|
44
|
+
// and a local may share a short spelling, while usedNames still reserves the
|
|
45
|
+
// spelling against labels from subsequently spliced modules.
|
|
46
|
+
const labels = options.renameLocals === false
|
|
47
|
+
? []
|
|
48
|
+
: resolveResult.symbols.filter((symbol) => symbol.kind === "label" &&
|
|
49
|
+
symbol.name !== "self" &&
|
|
50
|
+
!keepNames.has(symbol));
|
|
51
|
+
const labelNames = assignColorNames(colorGraph(buildLexicalGraph(labels, false)), unavailableNames);
|
|
52
|
+
const nameOfSymbol = new Map([...variableNames, ...labelNames]);
|
|
53
|
+
validateBindings(chunk, resolveResult, nameOfSymbol, globalRenames);
|
|
54
|
+
const usedNames = new Set(nameOfSymbol.values());
|
|
55
|
+
return {
|
|
56
|
+
nameOf: (identifier) => {
|
|
57
|
+
// メソッド定義の暗黙のselfパラメータは慣習的な名前のため常に維持する
|
|
58
|
+
// (呼び出し側から見える名前ではないため短縮しても安全ではあるが、
|
|
59
|
+
// 可読性のために元の名前のままにする)。
|
|
60
|
+
if (identifier.name === "self") {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const symbol = resolveResult.symbolOf(identifier);
|
|
64
|
+
if (symbol) {
|
|
65
|
+
return nameOfSymbol.get(symbol);
|
|
66
|
+
}
|
|
67
|
+
// フィールド名(`.foo`)はここに来るが、これらはisGlobalReferenceが
|
|
68
|
+
// falseになるため名前文字列がたまたま一致しても誤ってリネームしない。
|
|
69
|
+
if (globalRenames && resolveResult.isGlobalReference(identifier)) {
|
|
70
|
+
return globalRenames.get(identifier.name);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
usedNames,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function buildVariableInterference(chunk, resolved, symbols, allowLocalNameReuse, analysis) {
|
|
78
|
+
const graph = mutableGraph(symbols);
|
|
79
|
+
const facts = analysis?.facts ?? (0, optimizerFacts_1.analyzeOptimizerFacts)(chunk, resolved);
|
|
80
|
+
const liveness = analysis?.liveness ??
|
|
81
|
+
(0, symbolLiveness_1.analyzeSymbolLiveness)((0, controlFlow_1.analyzeControlFlow)(chunk, resolved), facts);
|
|
82
|
+
if (facts.generation !== liveness.controlFlow.version)
|
|
83
|
+
throw new Error("Identifier coloring requires one AST generation");
|
|
84
|
+
liveness.controlFlow.nodes.forEach((node) => {
|
|
85
|
+
addClique(graph, liveness.liveIn(node));
|
|
86
|
+
addClique(graph, liveness.liveOut(node));
|
|
87
|
+
liveness.defs(node).forEach((definition) => {
|
|
88
|
+
liveness.liveOut(node).forEach((live) => {
|
|
89
|
+
addEdge(graph, definition, live);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
37
92
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
93
|
+
// Parameters, generic-for variables, and multi-local declarations bind as a
|
|
94
|
+
// group. Giving two members one spelling would make only the last binding
|
|
95
|
+
// addressable after reparsing even if one member happens to be unused.
|
|
96
|
+
const declarationsByOwner = new Map();
|
|
97
|
+
symbols.forEach((symbol) => {
|
|
98
|
+
const declaration = facts
|
|
99
|
+
.operationsOfSymbol(symbol)
|
|
100
|
+
.find((operation) => operation.kind === "declare" &&
|
|
101
|
+
operation.origin === symbol.declaration);
|
|
102
|
+
if (!declaration)
|
|
103
|
+
return;
|
|
104
|
+
const group = declarationsByOwner.get(declaration.owner) ?? [];
|
|
105
|
+
group.push(symbol);
|
|
106
|
+
declarationsByOwner.set(declaration.owner, group);
|
|
107
|
+
});
|
|
108
|
+
declarationsByOwner.forEach((group) => {
|
|
109
|
+
addClique(graph, group);
|
|
110
|
+
});
|
|
111
|
+
// A captured binding can be shadowed inside the closure by any same-scope
|
|
112
|
+
// declaration whose emitted spelling matches it (notably `local function`).
|
|
113
|
+
// Extending this lexical interference across the whole declaring scope is
|
|
114
|
+
// conservative, deterministic, and leaves ordinary non-captured locals free
|
|
115
|
+
// to reuse names according to liveness.
|
|
116
|
+
symbols.forEach((symbol) => {
|
|
117
|
+
const captured = facts
|
|
118
|
+
.operationsOfSymbol(symbol)
|
|
119
|
+
.some((operation) => "location" in operation && operation.location.kind === "upvalue");
|
|
120
|
+
if (!captured)
|
|
121
|
+
return;
|
|
122
|
+
symbols.forEach((other) => {
|
|
123
|
+
if (other.scope === symbol.scope)
|
|
124
|
+
addEdge(graph, symbol, other);
|
|
41
125
|
});
|
|
42
126
|
});
|
|
43
|
-
|
|
127
|
+
addLexicalEdges(graph, symbols, allowLocalNameReuse);
|
|
128
|
+
return graph;
|
|
44
129
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
130
|
+
function buildLexicalGraph(symbols, allowSameScopeReuse) {
|
|
131
|
+
const graph = mutableGraph(symbols);
|
|
132
|
+
addLexicalEdges(graph, symbols, allowSameScopeReuse);
|
|
133
|
+
return graph;
|
|
134
|
+
}
|
|
135
|
+
function mutableGraph(symbols) {
|
|
136
|
+
return new Map(symbols.map((symbol) => [symbol, new Set()]));
|
|
137
|
+
}
|
|
138
|
+
function addLexicalEdges(graph, symbols, allowSameScopeReuse) {
|
|
139
|
+
for (let left = 0; left < symbols.length; left++) {
|
|
140
|
+
for (let right = left + 1; right < symbols.length; right++) {
|
|
141
|
+
const first = symbols[left];
|
|
142
|
+
const last = symbols[right];
|
|
143
|
+
if (isAncestor(first.scope, last.scope) ||
|
|
144
|
+
isAncestor(last.scope, first.scope) ||
|
|
145
|
+
(!allowSameScopeReuse && first.scope === last.scope))
|
|
146
|
+
addEdge(graph, first, last);
|
|
53
147
|
}
|
|
54
|
-
|
|
55
|
-
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function isAncestor(ancestor, descendant) {
|
|
151
|
+
if (ancestor === descendant)
|
|
152
|
+
return false;
|
|
153
|
+
for (let current = descendant.parent; current; current = current.parent)
|
|
154
|
+
if (current === ancestor)
|
|
155
|
+
return true;
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
function addClique(graph, symbols) {
|
|
159
|
+
const present = [...symbols].filter((symbol) => graph.has(symbol));
|
|
160
|
+
for (let left = 0; left < present.length; left++)
|
|
161
|
+
for (let right = left + 1; right < present.length; right++)
|
|
162
|
+
addEdge(graph, present[left], present[right]);
|
|
163
|
+
}
|
|
164
|
+
function addEdge(graph, first, last) {
|
|
165
|
+
if (first === last || !graph.has(first) || !graph.has(last))
|
|
166
|
+
return;
|
|
167
|
+
graph.get(first)?.add(last);
|
|
168
|
+
graph.get(last)?.add(first);
|
|
169
|
+
}
|
|
170
|
+
/** Deterministic weighted DSATUR coloring. */
|
|
171
|
+
function colorGraph(graph) {
|
|
172
|
+
const colors = new Map();
|
|
173
|
+
while (colors.size < graph.size) {
|
|
174
|
+
const remaining = [...graph.keys()].filter((symbol) => !colors.has(symbol));
|
|
175
|
+
remaining.sort((left, right) => {
|
|
176
|
+
const saturationDifference = saturation(graph, colors, right) - saturation(graph, colors, left);
|
|
177
|
+
if (saturationDifference !== 0)
|
|
178
|
+
return saturationDifference;
|
|
179
|
+
const weightDifference = weightOf(right) - weightOf(left);
|
|
180
|
+
if (weightDifference !== 0)
|
|
181
|
+
return weightDifference;
|
|
182
|
+
const degreeDifference = (graph.get(right)?.size ?? 0) - (graph.get(left)?.size ?? 0);
|
|
183
|
+
return degreeDifference !== 0 ? degreeDifference : left.id - right.id;
|
|
184
|
+
});
|
|
185
|
+
const symbol = remaining[0];
|
|
186
|
+
const unavailable = new Set([...(graph.get(symbol) ?? [])].flatMap((neighbor) => {
|
|
187
|
+
const color = colors.get(neighbor);
|
|
188
|
+
return color === undefined ? [] : [color];
|
|
189
|
+
}));
|
|
190
|
+
let color = 0;
|
|
191
|
+
while (unavailable.has(color))
|
|
192
|
+
color++;
|
|
193
|
+
colors.set(symbol, color);
|
|
194
|
+
}
|
|
195
|
+
return colors;
|
|
196
|
+
}
|
|
197
|
+
function saturation(graph, colors, symbol) {
|
|
198
|
+
return new Set([...(graph.get(symbol) ?? [])].flatMap((neighbor) => {
|
|
199
|
+
const color = colors.get(neighbor);
|
|
200
|
+
return color === undefined ? [] : [color];
|
|
201
|
+
})).size;
|
|
202
|
+
}
|
|
203
|
+
function weightOf(symbol) {
|
|
204
|
+
return symbol.references.length + 1;
|
|
205
|
+
}
|
|
206
|
+
function assignColorNames(colors, reserved) {
|
|
207
|
+
const unavailable = new Set(reserved);
|
|
208
|
+
const weightByColor = new Map();
|
|
209
|
+
const firstSymbolByColor = new Map();
|
|
210
|
+
colors.forEach((color, symbol) => {
|
|
211
|
+
weightByColor.set(color, (weightByColor.get(color) ?? 0) + weightOf(symbol));
|
|
212
|
+
firstSymbolByColor.set(color, Math.min(firstSymbolByColor.get(color) ?? symbol.id, symbol.id));
|
|
56
213
|
});
|
|
57
|
-
const
|
|
58
|
-
|
|
214
|
+
const orderedColors = [...weightByColor.keys()].sort((left, right) => (weightByColor.get(right) ?? 0) - (weightByColor.get(left) ?? 0) ||
|
|
215
|
+
(firstSymbolByColor.get(left) ?? 0) -
|
|
216
|
+
(firstSymbolByColor.get(right) ?? 0));
|
|
217
|
+
const nameByColor = new Map();
|
|
59
218
|
let counter = 0;
|
|
60
|
-
|
|
219
|
+
orderedColors.forEach((color) => {
|
|
61
220
|
let candidate;
|
|
62
|
-
do
|
|
221
|
+
do
|
|
63
222
|
candidate = generateCandidate(counter++);
|
|
64
|
-
|
|
65
|
-
|
|
223
|
+
while (!isAvailable(candidate, unavailable));
|
|
224
|
+
unavailable.add(candidate);
|
|
225
|
+
nameByColor.set(color, candidate);
|
|
66
226
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
227
|
+
// Every candidate is an identifier token and keywords are excluded, so the
|
|
228
|
+
// separator cost around each occurrence is invariant across candidates.
|
|
229
|
+
// Occurrence count therefore gives the exact candidate-dependent byte cost.
|
|
230
|
+
const result = new Map();
|
|
231
|
+
colors.forEach((color, symbol) => {
|
|
232
|
+
const name = nameByColor.get(color);
|
|
233
|
+
if (name === undefined)
|
|
234
|
+
throw new Error("Colored symbol has no name");
|
|
235
|
+
result.set(symbol, name);
|
|
73
236
|
});
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
function validateBindings(chunk, original, names, globalRenames) {
|
|
240
|
+
const recolored = (0, resolver_1.resolveScopes)(chunk, {
|
|
241
|
+
identifierName: (identifier) => {
|
|
242
|
+
const symbol = original.symbolOf(identifier);
|
|
243
|
+
if (symbol)
|
|
244
|
+
return names.get(symbol) ?? identifier.name;
|
|
245
|
+
return original.isGlobalReference(identifier)
|
|
246
|
+
? (globalRenames?.get(identifier.name) ?? identifier.name)
|
|
247
|
+
: identifier.name;
|
|
84
248
|
},
|
|
85
|
-
|
|
86
|
-
|
|
249
|
+
});
|
|
250
|
+
original.symbols.forEach((symbol) => {
|
|
251
|
+
if (symbol.implicit) {
|
|
252
|
+
symbol.references.forEach((identifier) => {
|
|
253
|
+
const rebound = recolored.symbolOf(identifier);
|
|
254
|
+
if (!rebound?.implicit || rebound.name !== symbol.name)
|
|
255
|
+
throw new Error(`Identifier coloring changed implicit binding for symbol ${String(symbol.id)}`);
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
[symbol.declaration, ...symbol.references].forEach((identifier) => {
|
|
260
|
+
if (recolored.symbolOf(identifier)?.declaration !== symbol.declaration)
|
|
261
|
+
throw new Error(`Identifier coloring changed binding for symbol ${String(symbol.id)}`);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
original.globals.forEach((binding) => {
|
|
265
|
+
binding.references.forEach((identifier) => {
|
|
266
|
+
if (!recolored.isGlobalReference(identifier))
|
|
267
|
+
throw new Error(`Identifier coloring captured global ${binding.name}`);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
87
270
|
}
|
package/dist/resolver.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveScopes = resolveScopes;
|
|
4
|
-
function resolveScopes(chunk) {
|
|
4
|
+
function resolveScopes(chunk, options = {}) {
|
|
5
5
|
let nextSymbolId = 0;
|
|
6
6
|
const allSymbols = [];
|
|
7
7
|
const globals = new Map();
|
|
8
8
|
const identifierSymbols = new WeakMap();
|
|
9
|
+
const globalReferenceNodes = new WeakSet();
|
|
10
|
+
const functionScopes = new WeakMap();
|
|
11
|
+
const identifierName = options.identifierName ??
|
|
12
|
+
((identifier) => identifier.name);
|
|
9
13
|
function createScope(kind, parent) {
|
|
10
14
|
const scope = {
|
|
11
15
|
kind,
|
|
@@ -18,33 +22,36 @@ function resolveScopes(chunk) {
|
|
|
18
22
|
parent?.children.push(scope);
|
|
19
23
|
return scope;
|
|
20
24
|
}
|
|
21
|
-
function declare(scope, node, kind) {
|
|
25
|
+
function declare(scope, node, kind, implicit = false) {
|
|
26
|
+
const name = identifierName(node);
|
|
22
27
|
const symbol = {
|
|
23
28
|
id: nextSymbolId++,
|
|
24
|
-
name
|
|
29
|
+
name,
|
|
25
30
|
kind,
|
|
26
31
|
scope,
|
|
27
32
|
declaration: node,
|
|
28
33
|
references: [],
|
|
34
|
+
...(implicit ? { implicit: true } : {}),
|
|
29
35
|
};
|
|
30
36
|
// 同名の再宣言はスコープ内の以後の参照から見た束縛を上書きする(Luaの通常のシャドーイング)
|
|
31
37
|
scope.symbols.push(symbol);
|
|
32
|
-
scope.bindings.set(
|
|
38
|
+
scope.bindings.set(name, symbol);
|
|
33
39
|
allSymbols.push(symbol);
|
|
34
40
|
identifierSymbols.set(node, symbol);
|
|
35
41
|
return symbol;
|
|
36
42
|
}
|
|
37
43
|
function declareLabel(scope, node) {
|
|
44
|
+
const name = identifierName(node);
|
|
38
45
|
const symbol = {
|
|
39
46
|
id: nextSymbolId++,
|
|
40
|
-
name
|
|
47
|
+
name,
|
|
41
48
|
kind: "label",
|
|
42
49
|
scope,
|
|
43
50
|
declaration: node,
|
|
44
51
|
references: [],
|
|
45
52
|
};
|
|
46
53
|
scope.symbols.push(symbol);
|
|
47
|
-
scope.labels.set(
|
|
54
|
+
scope.labels.set(name, symbol);
|
|
48
55
|
allSymbols.push(symbol);
|
|
49
56
|
identifierSymbols.set(node, symbol);
|
|
50
57
|
return symbol;
|
|
@@ -67,19 +74,24 @@ function resolveScopes(chunk) {
|
|
|
67
74
|
}
|
|
68
75
|
return undefined;
|
|
69
76
|
}
|
|
70
|
-
function reference(scope, node) {
|
|
71
|
-
const
|
|
77
|
+
function reference(scope, node, isWrite = false) {
|
|
78
|
+
const name = identifierName(node);
|
|
79
|
+
const symbol = lookupBinding(scope, name);
|
|
72
80
|
if (symbol) {
|
|
73
81
|
symbol.references.push(node);
|
|
74
82
|
identifierSymbols.set(node, symbol);
|
|
75
83
|
return;
|
|
76
84
|
}
|
|
77
|
-
let binding = globals.get(
|
|
85
|
+
let binding = globals.get(name);
|
|
78
86
|
if (!binding) {
|
|
79
|
-
binding = { name:
|
|
80
|
-
globals.set(
|
|
87
|
+
binding = { name, references: [], writes: [] };
|
|
88
|
+
globals.set(name, binding);
|
|
81
89
|
}
|
|
82
90
|
binding.references.push(node);
|
|
91
|
+
if (isWrite) {
|
|
92
|
+
binding.writes.push(node);
|
|
93
|
+
}
|
|
94
|
+
globalReferenceNodes.add(node);
|
|
83
95
|
}
|
|
84
96
|
// ラベルはブロック内での宣言位置に関わらずブロック全体から参照できる
|
|
85
97
|
// (前方goto)ため、通常の宣言文と違い先読みで登録する。
|
|
@@ -112,7 +124,7 @@ function resolveScopes(chunk) {
|
|
|
112
124
|
});
|
|
113
125
|
statement.variables.forEach((v) => {
|
|
114
126
|
if (v.type === "Identifier") {
|
|
115
|
-
reference(scope, v);
|
|
127
|
+
reference(scope, v, true);
|
|
116
128
|
}
|
|
117
129
|
else {
|
|
118
130
|
resolveExpression(v, scope);
|
|
@@ -183,7 +195,7 @@ function resolveScopes(chunk) {
|
|
|
183
195
|
// hoistLabelsで宣言済みのため、ここでは何もしない
|
|
184
196
|
return;
|
|
185
197
|
case "GotoStatement": {
|
|
186
|
-
const symbol = lookupLabel(scope, statement.label
|
|
198
|
+
const symbol = lookupLabel(scope, identifierName(statement.label));
|
|
187
199
|
if (symbol) {
|
|
188
200
|
symbol.references.push(statement.label);
|
|
189
201
|
identifierSymbols.set(statement.label, symbol);
|
|
@@ -205,7 +217,7 @@ function resolveScopes(chunk) {
|
|
|
205
217
|
}
|
|
206
218
|
else {
|
|
207
219
|
// 非local: 既存のローカル/グローバルへの代入として扱う(新規宣言ではない)
|
|
208
|
-
reference(scope, fn.identifier);
|
|
220
|
+
reference(scope, fn.identifier, true);
|
|
209
221
|
}
|
|
210
222
|
}
|
|
211
223
|
else {
|
|
@@ -213,6 +225,14 @@ function resolveScopes(chunk) {
|
|
|
213
225
|
}
|
|
214
226
|
}
|
|
215
227
|
const inner = createScope("function", scope);
|
|
228
|
+
functionScopes.set(fn, inner);
|
|
229
|
+
if (fn.identifier?.type === "MemberExpression" &&
|
|
230
|
+
fn.identifier.indexer === ":") {
|
|
231
|
+
// `function object:method(...)` declares an implicit first parameter. It has no
|
|
232
|
+
// declaration token in the AST, but it must still own every `self` reference so
|
|
233
|
+
// global analysis, aliases, summaries, and binding validation share Lua's binding.
|
|
234
|
+
declare(inner, { type: "Identifier", name: "self" }, "param", true);
|
|
235
|
+
}
|
|
216
236
|
fn.parameters.forEach((parameter) => {
|
|
217
237
|
if (parameter.type === "Identifier") {
|
|
218
238
|
declare(inner, parameter, "param");
|
|
@@ -263,6 +283,7 @@ function resolveScopes(chunk) {
|
|
|
263
283
|
return;
|
|
264
284
|
case "FunctionDeclaration": {
|
|
265
285
|
const inner = createScope("function", scope);
|
|
286
|
+
functionScopes.set(expr, inner);
|
|
266
287
|
expr.parameters.forEach((parameter) => {
|
|
267
288
|
if (parameter.type === "Identifier") {
|
|
268
289
|
declare(inner, parameter, "param");
|
|
@@ -299,5 +320,7 @@ function resolveScopes(chunk) {
|
|
|
299
320
|
symbols: allSymbols,
|
|
300
321
|
globals,
|
|
301
322
|
symbolOf: (identifier) => identifierSymbols.get(identifier),
|
|
323
|
+
isGlobalReference: (identifier) => globalReferenceNodes.has(identifier),
|
|
324
|
+
scopeOfFunction: (fn) => functionScopes.get(fn),
|
|
302
325
|
};
|
|
303
326
|
}
|