storm-lua-minify 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -22
- package/dist/cliOptions.js +1 -1
- package/dist/globalRename.js +17 -4
- package/dist/minifier.js +227 -191
- package/dist/optimizerValueDomain.js +26 -6
- package/dist/renamer.js +79 -22
- package/dist/resolver.js +7 -0
- package/dist/statementScheduler.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,28 +86,28 @@ storm-lua-minify --config storm-lua-minify.json --no-function-inlining script.lu
|
|
|
86
86
|
|
|
87
87
|
### 個別スイッチ
|
|
88
88
|
|
|
89
|
-
| スイッチ | 既定 | 説明
|
|
90
|
-
| -------------------------------------- | ------- |
|
|
91
|
-
| `local-renaming` | ON | local名を短くします
|
|
92
|
-
| `local-name-reuse` | ON | 生存期間が重ならないlocalで同じ名前を再利用します
|
|
93
|
-
| `global-renaming` | **OFF** | 内部で使うglobal名を短くします(※1)
|
|
94
|
-
| `field-renaming` | ON | 安全に変更できるfield名を短くします
|
|
95
|
-
| `global-aliasing` | ON |
|
|
96
|
-
| `local-declaration-merging` | ON | 連続するlocal宣言をまとめます
|
|
97
|
-
| `local-declaration-hoisting` | ON | local宣言を移動してまとめやすくします
|
|
98
|
-
| `table-read-merging` | ON | tableを読み取るlocal宣言をまとめます
|
|
99
|
-
| `field-sensitive-table-effects` | ON | tableへの書き込みの影響をfield単位で判定します
|
|
100
|
-
| `constant-expression-evaluation` | **OFF** | 定数だけからなる式を事前に計算します
|
|
101
|
-
| `local-constant-propagation` | **OFF** | 再代入されないlocalの定数を参照先へ伝えます
|
|
102
|
-
| `interprocedural-constant-propagation` | **OFF** | 関数の呼び出しを越えて定数を伝えます
|
|
103
|
-
| `parameter-pruning` | ON | 使われない関数parameterを取り除きます
|
|
104
|
-
| `function-inlining` | ON | 関数呼び出しを関数本体で置き換えます
|
|
105
|
-
| `function-specialization` | ON | 呼び出し方に合わせて関数を特殊化します
|
|
106
|
-
| `field-value-propagation` | ON | 安定したfieldの値を参照先へ伝えます
|
|
107
|
-
| `unused-local-removal` | ON | 未使用のlocalを取り除きます
|
|
108
|
-
| `unused-function-removal` | ON | 未使用のlocal関数を取り除きます
|
|
109
|
-
| `unused-field-initializer-removal` | ON | 読み取られないfieldの初期化を取り除きます
|
|
110
|
-
| `unused-export-removal` | ON | entryから到達できないmodule exportを取り除きます
|
|
89
|
+
| スイッチ | 既定 | 説明 |
|
|
90
|
+
| -------------------------------------- | ------- | --------------------------------------------------------------------------- |
|
|
91
|
+
| `local-renaming` | ON | local名を短くします |
|
|
92
|
+
| `local-name-reuse` | ON | 生存期間が重ならないlocalで同じ名前を再利用します |
|
|
93
|
+
| `global-renaming` | **OFF** | 内部で使うglobal名を短くします(※1) |
|
|
94
|
+
| `field-renaming` | ON | 安全に変更できるfield名を短くします |
|
|
95
|
+
| `global-aliasing` | ON | プログラム内で代入されず、繰り返し参照するglobalへ短いlocal名を割り当てます |
|
|
96
|
+
| `local-declaration-merging` | ON | 連続するlocal宣言をまとめます |
|
|
97
|
+
| `local-declaration-hoisting` | ON | local宣言を移動してまとめやすくします |
|
|
98
|
+
| `table-read-merging` | ON | tableを読み取るlocal宣言をまとめます |
|
|
99
|
+
| `field-sensitive-table-effects` | ON | tableへの書き込みの影響をfield単位で判定します |
|
|
100
|
+
| `constant-expression-evaluation` | **OFF** | 定数だけからなる式を事前に計算します |
|
|
101
|
+
| `local-constant-propagation` | **OFF** | 再代入されないlocalの定数を参照先へ伝えます |
|
|
102
|
+
| `interprocedural-constant-propagation` | **OFF** | 関数の呼び出しを越えて定数を伝えます |
|
|
103
|
+
| `parameter-pruning` | ON | 使われない関数parameterを取り除きます |
|
|
104
|
+
| `function-inlining` | ON | 関数呼び出しを関数本体で置き換えます |
|
|
105
|
+
| `function-specialization` | ON | 呼び出し方に合わせて関数を特殊化します |
|
|
106
|
+
| `field-value-propagation` | ON | 安定したfieldの値を参照先へ伝えます |
|
|
107
|
+
| `unused-local-removal` | ON | 未使用のlocalを取り除きます |
|
|
108
|
+
| `unused-function-removal` | ON | 未使用のlocal関数を取り除きます |
|
|
109
|
+
| `unused-field-initializer-removal` | ON | 読み取られないfieldの初期化を取り除きます |
|
|
110
|
+
| `unused-export-removal` | ON | entryから到達できないmodule exportを取り除きます |
|
|
111
111
|
|
|
112
112
|
安全性の条件を満たさない候補は、スイッチが有効でも実行されません。
|
|
113
113
|
|
package/dist/cliOptions.js
CHANGED
|
@@ -10,7 +10,7 @@ function addBooleanSwitch(command, name, description, shortName) {
|
|
|
10
10
|
/** Build the v1 CLI surface without materializing inherited defaults. */
|
|
11
11
|
function createCliProgram() {
|
|
12
12
|
const command = new commander_1.Command()
|
|
13
|
-
.version("0.9.
|
|
13
|
+
.version("0.9.1")
|
|
14
14
|
.description("A Lua minifier also outputs source map")
|
|
15
15
|
.option("--config <path>", "JSON configuration file")
|
|
16
16
|
.addOption(new commander_1.Option("--runtime-profile <profile>", "Target runtime semantics")
|
package/dist/globalRename.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectProgramWrittenGlobals = collectProgramWrittenGlobals;
|
|
3
4
|
exports.classifyAndRenameGlobals = classifyAndRenameGlobals;
|
|
4
5
|
const renamer_1 = require("./renamer");
|
|
5
6
|
const linker_1 = require("./linker");
|
|
7
|
+
/**
|
|
8
|
+
* Returns names whose shared global binding is written by any linked module.
|
|
9
|
+
* A module that only reads one of these names must not mistake it for a
|
|
10
|
+
* read-only value supplied by the runtime environment.
|
|
11
|
+
*/
|
|
12
|
+
function collectProgramWrittenGlobals(moduleResolve) {
|
|
13
|
+
const written = new Set();
|
|
14
|
+
moduleResolve.forEach((resolved) => {
|
|
15
|
+
resolved.globals.forEach((binding) => {
|
|
16
|
+
if (binding.writes.length > 0)
|
|
17
|
+
written.add(binding.name);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
return written;
|
|
21
|
+
}
|
|
6
22
|
function classifyAndRenameGlobals(moduleResolve, neverRename, reserved) {
|
|
7
|
-
const everWritten =
|
|
23
|
+
const everWritten = collectProgramWrittenGlobals(moduleResolve);
|
|
8
24
|
const totalReferenceCount = new Map();
|
|
9
25
|
moduleResolve.forEach((resolved) => {
|
|
10
26
|
resolved.globals.forEach((binding) => {
|
|
11
|
-
if (binding.writes.length > 0) {
|
|
12
|
-
everWritten.add(binding.name);
|
|
13
|
-
}
|
|
14
27
|
totalReferenceCount.set(binding.name, (totalReferenceCount.get(binding.name) ?? 0) +
|
|
15
28
|
binding.references.length);
|
|
16
29
|
});
|
package/dist/minifier.js
CHANGED
|
@@ -35,6 +35,41 @@ const NO_RENAME = {
|
|
|
35
35
|
nameOf: () => undefined,
|
|
36
36
|
usedNames: new Set(),
|
|
37
37
|
};
|
|
38
|
+
class CostVariantProgress {
|
|
39
|
+
target;
|
|
40
|
+
pendingSteps = 0;
|
|
41
|
+
constructor(target) {
|
|
42
|
+
this.target = target;
|
|
43
|
+
}
|
|
44
|
+
addSteps(count) {
|
|
45
|
+
this.pendingSteps += count;
|
|
46
|
+
this.target.addSteps(count);
|
|
47
|
+
}
|
|
48
|
+
startStep(label) {
|
|
49
|
+
if (this.pendingSteps > 0)
|
|
50
|
+
this.pendingSteps--;
|
|
51
|
+
this.target.startStep(label);
|
|
52
|
+
}
|
|
53
|
+
tick() {
|
|
54
|
+
this.target.tick();
|
|
55
|
+
}
|
|
56
|
+
finishFailedCandidate() {
|
|
57
|
+
while (this.pendingSteps > 0) {
|
|
58
|
+
this.pendingSteps--;
|
|
59
|
+
this.target.startStep("Skip remaining failed cost-gate work");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Remove consumers before producers. A producer is therefore measured while
|
|
64
|
+
// every downstream transformation that may expose its savings is still active.
|
|
65
|
+
const COST_GATE_ABLATION_ORDER = [
|
|
66
|
+
"fieldRename",
|
|
67
|
+
"scheduler",
|
|
68
|
+
"exportDce",
|
|
69
|
+
"fieldFact",
|
|
70
|
+
"aggregateSpecialization",
|
|
71
|
+
"functionRewrite",
|
|
72
|
+
];
|
|
38
73
|
function sourceRangeOf(node) {
|
|
39
74
|
return node.range;
|
|
40
75
|
}
|
|
@@ -69,13 +104,16 @@ class Minifier {
|
|
|
69
104
|
exportDceVariant;
|
|
70
105
|
fieldRenameVariant;
|
|
71
106
|
progress;
|
|
107
|
+
// Transactional variants mutate independent AST copies, but their source
|
|
108
|
+
// bytes and initial parse are identical. Keep one untouched parse template
|
|
109
|
+
// for the whole variant tree and clone it at each link boundary.
|
|
110
|
+
inputCache;
|
|
72
111
|
linkedAstGeneration = 0;
|
|
73
112
|
wholeProgramObjectsValue;
|
|
74
113
|
wholeProgramFieldsValue;
|
|
75
114
|
wholeProgramExportsValue;
|
|
76
115
|
wholeProgramFieldRenamesValue;
|
|
77
|
-
|
|
78
|
-
constructor(entryFilePath, luaParseSettings, mode, schedulerVariantOrProgress, functionRewriteVariant, fieldFactVariant, aggregateSpecializationVariant, exportDceVariant, fieldRenameVariant, progress) {
|
|
116
|
+
constructor(entryFilePath, luaParseSettings, mode, schedulerVariantOrProgress, functionRewriteVariant, fieldFactVariant, aggregateSpecializationVariant, exportDceVariant, fieldRenameVariant, progress, inputCache) {
|
|
79
117
|
this.schedulerVariant =
|
|
80
118
|
typeof schedulerVariantOrProgress === "string"
|
|
81
119
|
? schedulerVariantOrProgress
|
|
@@ -89,6 +127,7 @@ class Minifier {
|
|
|
89
127
|
typeof schedulerVariantOrProgress === "object"
|
|
90
128
|
? schedulerVariantOrProgress
|
|
91
129
|
: progress;
|
|
130
|
+
this.inputCache = inputCache ?? new Map();
|
|
92
131
|
this.entryFilePath = entryFilePath;
|
|
93
132
|
this.identifiersInUse = new Set();
|
|
94
133
|
this.moduleSourceText = new Map();
|
|
@@ -126,206 +165,176 @@ class Minifier {
|
|
|
126
165
|
return this.wholeProgramFieldRenamesValue;
|
|
127
166
|
}
|
|
128
167
|
parse() {
|
|
129
|
-
if (this.
|
|
130
|
-
return this.
|
|
131
|
-
}
|
|
132
|
-
if (this.exportDceVariant === undefined && this.exportDceEnabled()) {
|
|
133
|
-
return this.parseWithExportDceSelection();
|
|
134
|
-
}
|
|
135
|
-
if (this.fieldFactVariant === undefined && this.fieldFactsEnabled()) {
|
|
136
|
-
return this.parseWithFieldFactSelection();
|
|
137
|
-
}
|
|
138
|
-
if (this.aggregateSpecializationVariant === undefined &&
|
|
139
|
-
this.functionSpecializationEnabled()) {
|
|
140
|
-
return this.parseWithAggregateSpecializationSelection();
|
|
141
|
-
}
|
|
142
|
-
if (this.functionRewriteVariant === undefined &&
|
|
143
|
-
this.functionRewritesEnabled()) {
|
|
144
|
-
return this.parseWithFunctionRewriteSelection();
|
|
145
|
-
}
|
|
146
|
-
if (this.schedulerVariant === undefined &&
|
|
147
|
-
this.requiresSchedulerSelection()) {
|
|
148
|
-
return this.parseWithSchedulerSelection();
|
|
149
|
-
}
|
|
168
|
+
if (this.costGatesToSelect().length > 0)
|
|
169
|
+
return this.parseWithCostGateSelection();
|
|
150
170
|
return this.parseOnce();
|
|
151
171
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Evaluate cost-gated transforms as one coalition, then remove a gate only
|
|
174
|
+
* when its absence makes the complete final output strictly shorter.
|
|
175
|
+
*
|
|
176
|
+
* An arbitrary final-byte cost function still requires 2^N evaluations to
|
|
177
|
+
* find its global minimum. This deterministic backward sweep deliberately
|
|
178
|
+
* chooses a narrower contract: it keeps interactions present in the full
|
|
179
|
+
* trial and finds the best point on one monotonic deletion path in at most
|
|
180
|
+
* N+2 evaluations. The all-off baseline remains the final upper bound.
|
|
181
|
+
*/
|
|
182
|
+
parseWithCostGateSelection() {
|
|
183
|
+
const gates = this.costGatesToSelect();
|
|
184
|
+
const evaluated = new Map();
|
|
185
|
+
const baseline = this.evaluateCostVariant(this.costGateVariants("baseline"), "Evaluate cost-gated baseline", evaluated);
|
|
186
|
+
let fullTrial;
|
|
160
187
|
try {
|
|
161
|
-
this.
|
|
162
|
-
trialMinifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, undefined, undefined, undefined, undefined, undefined, "trial", this.progress);
|
|
163
|
-
trial = trialMinifier.parse();
|
|
188
|
+
fullTrial = this.evaluateCostVariant(this.costGateVariants("trial"), "Evaluate joint cost-gated trial", evaluated);
|
|
164
189
|
}
|
|
165
190
|
catch {
|
|
166
|
-
this.copyDiagnosticsFrom(
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
this.copyDiagnosticsFrom(baseline.minifier);
|
|
192
|
+
gates.forEach((gate) => {
|
|
193
|
+
this.recordFinalGateDecision(gate, "rejected", "trial-failed");
|
|
194
|
+
});
|
|
195
|
+
this.recordFinalCostDecision("rejected", "trial-failed");
|
|
196
|
+
return this.adoptVariant(baseline.minifier, baseline.output);
|
|
169
197
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
let current = fullTrial;
|
|
199
|
+
for (const gate of COST_GATE_ABLATION_ORDER) {
|
|
200
|
+
if (!gates.includes(gate) || current.variants[gate] === "baseline")
|
|
201
|
+
continue;
|
|
202
|
+
const variants = {
|
|
203
|
+
...current.variants,
|
|
204
|
+
[gate]: "baseline",
|
|
205
|
+
// Specialization is executed inside the function-rewrite pipeline;
|
|
206
|
+
// keep the mask and final diagnostics closed over that dependency.
|
|
207
|
+
...(gate === "functionRewrite"
|
|
208
|
+
? { aggregateSpecialization: "baseline" }
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
let withoutGate;
|
|
212
|
+
try {
|
|
213
|
+
withoutGate = this.evaluateCostVariant(variants, `Evaluate joint trial without ${this.costGateLabel(gate)}`, evaluated);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Keep ties in the joint trial: a gate whose isolated contribution is
|
|
219
|
+
// hidden can still participate in a later transformation's savings.
|
|
220
|
+
if (withoutGate.byteLength < current.byteLength)
|
|
221
|
+
current = withoutGate;
|
|
175
222
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
223
|
+
const accepted = current.byteLength < baseline.byteLength;
|
|
224
|
+
const selected = accepted ? current : baseline;
|
|
225
|
+
// Successful trial diagnostics describe attempted opportunities even when
|
|
226
|
+
// the corresponding AST is not selected, matching the former gate policy.
|
|
227
|
+
this.copyDiagnosticsFrom(fullTrial.minifier);
|
|
228
|
+
gates.forEach((gate) => {
|
|
229
|
+
this.recordFinalGateDecision(gate, accepted && current.variants[gate] === "trial"
|
|
230
|
+
? "accepted"
|
|
231
|
+
: "rejected", accepted && current.variants[gate] === "trial"
|
|
232
|
+
? "final-output-shorter"
|
|
233
|
+
: "final-output-not-shorter");
|
|
234
|
+
});
|
|
235
|
+
this.recordFinalCostDecision(accepted ? "accepted" : "rejected", accepted ? "final-output-shorter" : "final-output-not-shorter", accepted ? baseline.byteLength - current.byteLength : undefined);
|
|
236
|
+
return this.adoptVariant(selected.minifier, selected.output);
|
|
179
237
|
}
|
|
180
|
-
|
|
238
|
+
evaluateCostVariant(variants, label, evaluated) {
|
|
239
|
+
const key = COST_GATE_ABLATION_ORDER.map((gate) => variants[gate] === "trial" ? "1" : "0").join("");
|
|
240
|
+
const cached = evaluated.get(key);
|
|
241
|
+
if (cached)
|
|
242
|
+
return cached;
|
|
181
243
|
this.progress?.addSteps(1);
|
|
182
|
-
|
|
183
|
-
|
|
244
|
+
this.progress?.startStep(label);
|
|
245
|
+
const candidateProgress = this.progress
|
|
246
|
+
? new CostVariantProgress(this.progress)
|
|
247
|
+
: undefined;
|
|
248
|
+
const minifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, variants.scheduler, variants.functionRewrite, variants.fieldFact, variants.aggregateSpecialization, variants.exportDce, variants.fieldRename, candidateProgress, this.inputCache);
|
|
249
|
+
let output;
|
|
184
250
|
try {
|
|
185
|
-
|
|
186
|
-
trialMinifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, undefined, undefined, undefined, undefined, "trial", this.fieldRenameVariant, this.progress);
|
|
187
|
-
trial = trialMinifier.parse();
|
|
251
|
+
output = minifier.parseOnce();
|
|
188
252
|
}
|
|
189
|
-
catch {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const baselineMinifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, undefined, undefined, undefined, undefined, "baseline", this.fieldRenameVariant, this.progress);
|
|
193
|
-
const baseline = baselineMinifier.parse();
|
|
194
|
-
this.copyDiagnosticsFrom(baselineMinifier);
|
|
195
|
-
this.recordFinalExportDceDecision("rejected", "trial-failed");
|
|
196
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
197
|
-
}
|
|
198
|
-
if (!trialMinifier.exportDceChanged) {
|
|
199
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
200
|
-
this.recordFinalExportDceDecision("rejected", "final-output-not-shorter");
|
|
201
|
-
return this.adoptVariant(trialMinifier, trial);
|
|
202
|
-
}
|
|
203
|
-
this.progress?.addSteps(1);
|
|
204
|
-
this.progress?.startStep("Evaluate unused export removal baseline");
|
|
205
|
-
const baselineMinifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, undefined, undefined, undefined, undefined, "baseline", this.fieldRenameVariant, this.progress);
|
|
206
|
-
const baseline = baselineMinifier.parse();
|
|
207
|
-
const baselineBytes = new TextEncoder().encode(baseline.toString()).length;
|
|
208
|
-
const trialBytes = new TextEncoder().encode(trial.toString()).length;
|
|
209
|
-
if (trialBytes >= baselineBytes) {
|
|
210
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
211
|
-
this.recordFinalExportDceDecision("rejected", "final-output-not-shorter");
|
|
212
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
253
|
+
catch (error) {
|
|
254
|
+
candidateProgress?.finishFailedCandidate();
|
|
255
|
+
throw error;
|
|
213
256
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
const result = {
|
|
258
|
+
minifier,
|
|
259
|
+
output,
|
|
260
|
+
byteLength: new TextEncoder().encode(output.toString()).length,
|
|
261
|
+
variants,
|
|
262
|
+
};
|
|
263
|
+
evaluated.set(key, result);
|
|
264
|
+
return result;
|
|
217
265
|
}
|
|
218
|
-
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.copyDiagnosticsFrom(baselineMinifier);
|
|
233
|
-
this.recordFinalFieldFactDecision("rejected", "trial-failed");
|
|
234
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
235
|
-
}
|
|
236
|
-
const trialBytes = new TextEncoder().encode(trial.toString()).length;
|
|
237
|
-
if (trialBytes >= baselineBytes) {
|
|
238
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
239
|
-
this.recordFinalFieldFactDecision("rejected", "final-output-not-shorter");
|
|
240
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
241
|
-
}
|
|
242
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
243
|
-
this.recordFinalFieldFactDecision("accepted", "final-output-shorter", baselineBytes - trialBytes);
|
|
244
|
-
return this.adoptVariant(trialMinifier, trial);
|
|
266
|
+
costGateVariants(fallback) {
|
|
267
|
+
const selected = new Set(this.costGatesToSelect());
|
|
268
|
+
const variant = (gate, explicit) => explicit ?? (selected.has(gate) ? fallback : "baseline");
|
|
269
|
+
const functionRewrite = variant("functionRewrite", this.functionRewriteVariant);
|
|
270
|
+
return {
|
|
271
|
+
fieldRename: variant("fieldRename", this.fieldRenameVariant),
|
|
272
|
+
scheduler: variant("scheduler", this.schedulerVariant),
|
|
273
|
+
exportDce: variant("exportDce", this.exportDceVariant),
|
|
274
|
+
fieldFact: variant("fieldFact", this.fieldFactVariant),
|
|
275
|
+
aggregateSpecialization: functionRewrite === "baseline"
|
|
276
|
+
? "baseline"
|
|
277
|
+
: variant("aggregateSpecialization", this.aggregateSpecializationVariant),
|
|
278
|
+
functionRewrite,
|
|
279
|
+
};
|
|
245
280
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
267
|
-
this.recordFinalAggregateSpecializationDecision("rejected", "final-output-not-shorter");
|
|
268
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
269
|
-
}
|
|
270
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
271
|
-
this.recordFinalAggregateSpecializationDecision("accepted", "final-output-shorter", baselineBytes - trialBytes);
|
|
272
|
-
return this.adoptVariant(trialMinifier, trial);
|
|
281
|
+
costGatesToSelect() {
|
|
282
|
+
return COST_GATE_ABLATION_ORDER.filter((gate) => {
|
|
283
|
+
switch (gate) {
|
|
284
|
+
case "fieldRename":
|
|
285
|
+
return (this.fieldRenameVariant === undefined && this.fieldRenamesEnabled());
|
|
286
|
+
case "scheduler":
|
|
287
|
+
return (this.schedulerVariant === undefined &&
|
|
288
|
+
this.requiresSchedulerSelection());
|
|
289
|
+
case "exportDce":
|
|
290
|
+
return this.exportDceVariant === undefined && this.exportDceEnabled();
|
|
291
|
+
case "fieldFact":
|
|
292
|
+
return (this.fieldFactVariant === undefined && this.fieldFactsEnabled());
|
|
293
|
+
case "aggregateSpecialization":
|
|
294
|
+
return (this.aggregateSpecializationVariant === undefined &&
|
|
295
|
+
this.functionSpecializationEnabled());
|
|
296
|
+
case "functionRewrite":
|
|
297
|
+
return (this.functionRewriteVariant === undefined &&
|
|
298
|
+
this.functionRewritesEnabled());
|
|
299
|
+
}
|
|
300
|
+
});
|
|
273
301
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
302
|
+
costGateLabel(gate) {
|
|
303
|
+
switch (gate) {
|
|
304
|
+
case "fieldRename":
|
|
305
|
+
return "field renaming";
|
|
306
|
+
case "scheduler":
|
|
307
|
+
return "statement scheduling";
|
|
308
|
+
case "exportDce":
|
|
309
|
+
return "unused export removal";
|
|
310
|
+
case "fieldFact":
|
|
311
|
+
return "field optimization";
|
|
312
|
+
case "aggregateSpecialization":
|
|
313
|
+
return "function specialization";
|
|
314
|
+
case "functionRewrite":
|
|
315
|
+
return "function rewrites";
|
|
286
316
|
}
|
|
287
|
-
catch {
|
|
288
|
-
this.copyDiagnosticsFrom(baselineMinifier);
|
|
289
|
-
this.recordFinalFunctionRewriteDecision("rejected", "trial-failed");
|
|
290
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
291
|
-
}
|
|
292
|
-
const trialBytes = new TextEncoder().encode(trial.toString()).length;
|
|
293
|
-
if (trialBytes >= baselineBytes) {
|
|
294
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
295
|
-
this.recordFinalFunctionRewriteDecision("rejected", "final-output-not-shorter");
|
|
296
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
297
|
-
}
|
|
298
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
299
|
-
this.recordFinalFunctionRewriteDecision("accepted", "final-output-shorter", baselineBytes - trialBytes);
|
|
300
|
-
return this.adoptVariant(trialMinifier, trial);
|
|
301
317
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (trialBytes >= baselineBytes) {
|
|
322
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
323
|
-
this.recordFinalSchedulerDecision("rejected", "final-output-not-shorter");
|
|
324
|
-
return this.adoptVariant(baselineMinifier, baseline);
|
|
318
|
+
recordFinalGateDecision(gate, decision, reason) {
|
|
319
|
+
switch (gate) {
|
|
320
|
+
case "fieldRename":
|
|
321
|
+
this.recordFinalFieldRenameDecision(decision, reason);
|
|
322
|
+
return;
|
|
323
|
+
case "scheduler":
|
|
324
|
+
this.recordFinalSchedulerDecision(decision, reason);
|
|
325
|
+
return;
|
|
326
|
+
case "exportDce":
|
|
327
|
+
this.recordFinalExportDceDecision(decision, reason);
|
|
328
|
+
return;
|
|
329
|
+
case "fieldFact":
|
|
330
|
+
this.recordFinalFieldFactDecision(decision, reason);
|
|
331
|
+
return;
|
|
332
|
+
case "aggregateSpecialization":
|
|
333
|
+
this.recordFinalAggregateSpecializationDecision(decision, reason);
|
|
334
|
+
return;
|
|
335
|
+
case "functionRewrite":
|
|
336
|
+
this.recordFinalFunctionRewriteDecision(decision, reason);
|
|
325
337
|
}
|
|
326
|
-
this.copyDiagnosticsFrom(trialMinifier);
|
|
327
|
-
this.recordFinalSchedulerDecision("accepted", "final-output-shorter", baselineBytes - trialBytes);
|
|
328
|
-
return this.adoptVariant(trialMinifier, trial);
|
|
329
338
|
}
|
|
330
339
|
parseOnce() {
|
|
331
340
|
this.progress?.addSteps(11);
|
|
@@ -427,7 +436,6 @@ class Minifier {
|
|
|
427
436
|
}));
|
|
428
437
|
if (!result.changed)
|
|
429
438
|
return;
|
|
430
|
-
this.exportDceChanged = true;
|
|
431
439
|
this.linkOrder.forEach((moduleName) => {
|
|
432
440
|
const ast = this.moduleAST.get(moduleName);
|
|
433
441
|
if (!ast)
|
|
@@ -898,7 +906,6 @@ class Minifier {
|
|
|
898
906
|
this.wholeProgramFieldsValue = minifier.wholeProgramFieldsValue;
|
|
899
907
|
this.wholeProgramExportsValue = minifier.wholeProgramExportsValue;
|
|
900
908
|
this.wholeProgramFieldRenamesValue = minifier.wholeProgramFieldRenamesValue;
|
|
901
|
-
this.exportDceChanged = minifier.exportDceChanged;
|
|
902
909
|
return output;
|
|
903
910
|
}
|
|
904
911
|
recordFinalSchedulerDecision(decision, reason, byteSavings) {
|
|
@@ -913,6 +920,18 @@ class Minifier {
|
|
|
913
920
|
sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
|
|
914
921
|
});
|
|
915
922
|
}
|
|
923
|
+
recordFinalCostDecision(decision, reason, byteSavings) {
|
|
924
|
+
this.diagnosticCollector?.record({
|
|
925
|
+
pass: "optimizer-final-cost",
|
|
926
|
+
decision,
|
|
927
|
+
reason,
|
|
928
|
+
candidateSize: 1,
|
|
929
|
+
estimatedByteSavings: byteSavings,
|
|
930
|
+
runtimeProfile: this.mode.runtimeProfile,
|
|
931
|
+
moduleName: this.entryModule,
|
|
932
|
+
sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
|
|
933
|
+
});
|
|
934
|
+
}
|
|
916
935
|
recordFinalFunctionRewriteDecision(decision, reason, byteSavings) {
|
|
917
936
|
this.diagnosticCollector?.record({
|
|
918
937
|
pass: "function-rewrite-final-cost",
|
|
@@ -1084,10 +1103,16 @@ class Minifier {
|
|
|
1084
1103
|
* なり)、意味が壊れる(要修正が発覚した実例)。
|
|
1085
1104
|
*/
|
|
1086
1105
|
transformAll() {
|
|
1106
|
+
// Global bindings are shared across linked modules. A name written in one
|
|
1107
|
+
// module is therefore program-owned even when another module only reads it;
|
|
1108
|
+
// treating that reader as an external-global alias candidate can capture
|
|
1109
|
+
// the value before the defining module runs (#102).
|
|
1110
|
+
const programWrittenGlobals = (0, globalRename_1.collectProgramWrittenGlobals)(this.moduleResolve);
|
|
1087
1111
|
// globalRenames.keys()は8aが実際にリネームした(=代入もされていた)名前のみ。
|
|
1088
1112
|
// neverRenameGlobalsは代入されていない名前にも及ぶ保護指定なので、8bのエイリアス化
|
|
1089
1113
|
// が誤ってそれらを書き換えてしまわないよう、必ず両方をあわせてexcludeNamesに渡す。
|
|
1090
1114
|
const excludeGlobalNames = new Set([
|
|
1115
|
+
...programWrittenGlobals,
|
|
1091
1116
|
...this.globalRenames.keys(),
|
|
1092
1117
|
...(this.mode.neverRenameGlobals ?? []),
|
|
1093
1118
|
...this.annotationProtectedGlobals(),
|
|
@@ -1363,11 +1388,22 @@ class Minifier {
|
|
|
1363
1388
|
visiting.add(moduleName);
|
|
1364
1389
|
stack.push(moduleName);
|
|
1365
1390
|
const fullResolvePath = path_1.default.join(this.dir, ...moduleName.split(".")) + ".lua";
|
|
1366
|
-
|
|
1367
|
-
|
|
1391
|
+
let input = this.inputCache.get(fullResolvePath);
|
|
1392
|
+
if (!input) {
|
|
1393
|
+
if (!fs_1.default.existsSync(fullResolvePath)) {
|
|
1394
|
+
throw new Error(moduleName + " is not found");
|
|
1395
|
+
}
|
|
1396
|
+
const sourceText = fs_1.default.readFileSync(fullResolvePath).toString();
|
|
1397
|
+
const ast = luaparse_1.default.parse(sourceText, this.luaParseSettings);
|
|
1398
|
+
input = {
|
|
1399
|
+
sourceText,
|
|
1400
|
+
ast,
|
|
1401
|
+
references: (0, linker_1.findModuleReferences)(ast),
|
|
1402
|
+
};
|
|
1403
|
+
this.inputCache.set(fullResolvePath, input);
|
|
1368
1404
|
}
|
|
1369
|
-
const code =
|
|
1370
|
-
const ast =
|
|
1405
|
+
const code = input.sourceText;
|
|
1406
|
+
const ast = structuredClone(input.ast);
|
|
1371
1407
|
this.moduleMetadata.set(moduleName, new sourceMetadata_1.SourceMetadata(ast, code));
|
|
1372
1408
|
// Resolveパス(#19): このモジュールのスコープ/シンボルを解析し、Renameパスの
|
|
1373
1409
|
// 入力として使い回せるようキャッシュする。グローバル参照はプログラム全体で
|
|
@@ -1379,7 +1415,7 @@ class Minifier {
|
|
|
1379
1415
|
// Source Mapの`sources`はURLとして解釈されるため、OS依存のpath.sepではなく
|
|
1380
1416
|
// 常に"/"区切りで保持する(Windows上でのビルドでも壊れないように)。
|
|
1381
1417
|
this.moduleNameAndFileName.set(moduleName, moduleName.replaceAll(".", "/") + ".lua");
|
|
1382
|
-
|
|
1418
|
+
input.references.forEach((ref) => {
|
|
1383
1419
|
visit(ref.moduleName);
|
|
1384
1420
|
});
|
|
1385
1421
|
visiting.delete(moduleName);
|
|
@@ -22,16 +22,16 @@ exports.EMPTY_OPTIMIZER_TUPLE = Object.freeze({
|
|
|
22
22
|
tail: Object.freeze({ kind: "none" }),
|
|
23
23
|
});
|
|
24
24
|
function finiteOptimizerValue(atoms = [], unknownReasons = [], limits = exports.DEFAULT_OPTIMIZER_VALUE_DOMAIN_LIMITS) {
|
|
25
|
-
|
|
25
|
+
validateCustomLimits(limits);
|
|
26
26
|
const normalizedAtoms = uniqueSorted(atoms, atomKey);
|
|
27
27
|
const normalizedReasons = uniqueSorted(unknownReasons, (reason) => reason);
|
|
28
28
|
const atomOverflow = normalizedAtoms.length > limits.maxAtoms;
|
|
29
29
|
const reasons = atomOverflow
|
|
30
|
-
? [...normalizedReasons, "atom-cap-exceeded"]
|
|
31
|
-
: normalizedReasons;
|
|
30
|
+
? capReasons([...normalizedReasons, "atom-cap-exceeded"], limits.maxUnknownReasons)
|
|
31
|
+
: capNormalizedReasons(normalizedReasons, limits.maxUnknownReasons);
|
|
32
32
|
return Object.freeze({
|
|
33
33
|
atoms: Object.freeze(normalizedAtoms.slice(0, limits.maxAtoms)),
|
|
34
|
-
unknownReasons: Object.freeze(
|
|
34
|
+
unknownReasons: Object.freeze(reasons),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
function unknownOptimizerValue(reason, limits = exports.DEFAULT_OPTIMIZER_VALUE_DOMAIN_LIMITS) {
|
|
@@ -41,7 +41,7 @@ function joinOptimizerValues(values, limits = exports.DEFAULT_OPTIMIZER_VALUE_DO
|
|
|
41
41
|
return finiteOptimizerValue(values.flatMap((value) => value.atoms), values.flatMap((value) => value.unknownReasons), limits);
|
|
42
42
|
}
|
|
43
43
|
function finiteOptimizerTuple(prefix, tail = { kind: "none" }, limits = exports.DEFAULT_OPTIMIZER_VALUE_DOMAIN_LIMITS) {
|
|
44
|
-
|
|
44
|
+
validateCustomLimits(limits);
|
|
45
45
|
const normalizedPrefix = prefix
|
|
46
46
|
.slice(0, limits.maxTuplePrefix)
|
|
47
47
|
.map((value) => finiteOptimizerValue(value.atoms, value.unknownReasons, limits));
|
|
@@ -75,7 +75,7 @@ function valueAtOptimizerTupleSlot(tuple, index, limits = exports.DEFAULT_OPTIMI
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
function joinOptimizerTuples(tuples, limits = exports.DEFAULT_OPTIMIZER_VALUE_DOMAIN_LIMITS) {
|
|
78
|
-
|
|
78
|
+
validateCustomLimits(limits);
|
|
79
79
|
if (tuples.length === 0)
|
|
80
80
|
return exports.EMPTY_OPTIMIZER_TUPLE;
|
|
81
81
|
const prefixLength = Math.min(Math.max(...tuples.map((tuple) => tuple.prefix.length)), limits.maxTuplePrefix);
|
|
@@ -151,6 +151,17 @@ function atomKey(atom) {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
function uniqueSorted(values, keyOf) {
|
|
154
|
+
if (values.length === 0)
|
|
155
|
+
return [];
|
|
156
|
+
if (values.length === 1)
|
|
157
|
+
return [values[0]];
|
|
158
|
+
if (values.length === 2) {
|
|
159
|
+
const firstKey = keyOf(values[0]);
|
|
160
|
+
const lastKey = keyOf(values[1]);
|
|
161
|
+
if (firstKey === lastKey)
|
|
162
|
+
return [values[1]];
|
|
163
|
+
return firstKey < lastKey ? [values[0], values[1]] : [values[1], values[0]];
|
|
164
|
+
}
|
|
154
165
|
const byKey = new Map();
|
|
155
166
|
values.forEach((value) => byKey.set(keyOf(value), value));
|
|
156
167
|
return [...byKey.entries()]
|
|
@@ -159,6 +170,9 @@ function uniqueSorted(values, keyOf) {
|
|
|
159
170
|
}
|
|
160
171
|
function capReasons(reasons, maximum) {
|
|
161
172
|
const normalized = uniqueSorted(reasons, (reason) => reason);
|
|
173
|
+
return capNormalizedReasons(normalized, maximum);
|
|
174
|
+
}
|
|
175
|
+
function capNormalizedReasons(normalized, maximum) {
|
|
162
176
|
if (normalized.length <= maximum)
|
|
163
177
|
return normalized;
|
|
164
178
|
if (maximum === 0)
|
|
@@ -168,6 +182,12 @@ function capReasons(reasons, maximum) {
|
|
|
168
182
|
"reason-cap-exceeded",
|
|
169
183
|
];
|
|
170
184
|
}
|
|
185
|
+
function validateCustomLimits(limits) {
|
|
186
|
+
// The default is a module-owned frozen literal that satisfies the invariants.
|
|
187
|
+
// Nearly every optimizer value uses it, so do not enumerate its fields again.
|
|
188
|
+
if (limits !== exports.DEFAULT_OPTIMIZER_VALUE_DOMAIN_LIMITS)
|
|
189
|
+
validateLimits(limits);
|
|
190
|
+
}
|
|
171
191
|
function validateLimits(limits) {
|
|
172
192
|
Object.entries(limits).forEach(([name, value]) => {
|
|
173
193
|
if (!Number.isInteger(value) || value < 0)
|
package/dist/renamer.js
CHANGED
|
@@ -124,9 +124,44 @@ function buildVariableInterference(chunk, resolved, symbols, allowLocalNameReuse
|
|
|
124
124
|
addEdge(graph, symbol, other);
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
// Liveness alone does not model Lua's lexical shadowing. An earlier local
|
|
128
|
+
// may be dead at a later declaration and then be assigned again; if both
|
|
129
|
+
// declarations receive one spelling, the later declaration captures that
|
|
130
|
+
// assignment and every following reference. Preserve reuse when all uses of
|
|
131
|
+
// the earlier binding precede the later declaration, but add an edge when
|
|
132
|
+
// any use follows it.
|
|
133
|
+
const declarationOrder = new Map(symbols.map((symbol) => [
|
|
134
|
+
symbol,
|
|
135
|
+
requireResolutionOrder(resolved, symbol.declaration),
|
|
136
|
+
]));
|
|
137
|
+
const lastReferenceOrder = new Map(symbols.map((symbol) => [
|
|
138
|
+
symbol,
|
|
139
|
+
symbol.references.reduce((last, reference) => Math.max(last, requireResolutionOrder(resolved, reference)), -1),
|
|
140
|
+
]));
|
|
141
|
+
for (let left = 0; left < symbols.length; left++) {
|
|
142
|
+
for (let right = left + 1; right < symbols.length; right++) {
|
|
143
|
+
const first = symbols[left];
|
|
144
|
+
const last = symbols[right];
|
|
145
|
+
if (first.scope !== last.scope)
|
|
146
|
+
continue;
|
|
147
|
+
const firstDeclarationOrder = declarationOrder.get(first) ?? -1;
|
|
148
|
+
const lastDeclarationOrder = declarationOrder.get(last) ?? -1;
|
|
149
|
+
const [earlier, laterDeclarationOrder] = firstDeclarationOrder < lastDeclarationOrder
|
|
150
|
+
? [first, lastDeclarationOrder]
|
|
151
|
+
: [last, firstDeclarationOrder];
|
|
152
|
+
if ((lastReferenceOrder.get(earlier) ?? -1) > laterDeclarationOrder)
|
|
153
|
+
addEdge(graph, first, last);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
127
156
|
addLexicalEdges(graph, symbols, allowLocalNameReuse);
|
|
128
157
|
return graph;
|
|
129
158
|
}
|
|
159
|
+
function requireResolutionOrder(resolved, identifier) {
|
|
160
|
+
const order = resolved.resolutionOrderOf(identifier);
|
|
161
|
+
if (order === undefined)
|
|
162
|
+
throw new Error("Resolved identifier has no resolution order");
|
|
163
|
+
return order;
|
|
164
|
+
}
|
|
130
165
|
function buildLexicalGraph(symbols, allowSameScopeReuse) {
|
|
131
166
|
const graph = mutableGraph(symbols);
|
|
132
167
|
addLexicalEdges(graph, symbols, allowSameScopeReuse);
|
|
@@ -170,35 +205,42 @@ function addEdge(graph, first, last) {
|
|
|
170
205
|
/** Deterministic weighted DSATUR coloring. */
|
|
171
206
|
function colorGraph(graph) {
|
|
172
207
|
const colors = new Map();
|
|
208
|
+
// Keep the colored-neighbor set incrementally. Recomputing it inside the
|
|
209
|
+
// sort comparator makes dense module graphs dominate the whole pipeline.
|
|
210
|
+
const saturationColors = new Map([...graph.keys()].map((symbol) => [symbol, new Set()]));
|
|
173
211
|
while (colors.size < graph.size) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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;
|
|
212
|
+
let symbol;
|
|
213
|
+
graph.forEach((_neighbors, candidate) => {
|
|
214
|
+
if (colors.has(candidate))
|
|
215
|
+
return;
|
|
216
|
+
if (symbol === undefined ||
|
|
217
|
+
coloringPriority(candidate, symbol, graph, saturationColors) < 0)
|
|
218
|
+
symbol = candidate;
|
|
184
219
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return color === undefined ? [] : [color];
|
|
189
|
-
}));
|
|
220
|
+
if (symbol === undefined)
|
|
221
|
+
throw new Error("Uncolored symbol not found");
|
|
222
|
+
const unavailable = saturationColors.get(symbol) ?? new Set();
|
|
190
223
|
let color = 0;
|
|
191
224
|
while (unavailable.has(color))
|
|
192
225
|
color++;
|
|
193
226
|
colors.set(symbol, color);
|
|
227
|
+
graph.get(symbol)?.forEach((neighbor) => {
|
|
228
|
+
if (!colors.has(neighbor))
|
|
229
|
+
saturationColors.get(neighbor)?.add(color);
|
|
230
|
+
});
|
|
194
231
|
}
|
|
195
232
|
return colors;
|
|
196
233
|
}
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
234
|
+
function coloringPriority(left, right, graph, saturationColors) {
|
|
235
|
+
const saturationDifference = (saturationColors.get(right)?.size ?? 0) -
|
|
236
|
+
(saturationColors.get(left)?.size ?? 0);
|
|
237
|
+
if (saturationDifference !== 0)
|
|
238
|
+
return saturationDifference;
|
|
239
|
+
const weightDifference = weightOf(right) - weightOf(left);
|
|
240
|
+
if (weightDifference !== 0)
|
|
241
|
+
return weightDifference;
|
|
242
|
+
const degreeDifference = (graph.get(right)?.size ?? 0) - (graph.get(left)?.size ?? 0);
|
|
243
|
+
return degreeDifference !== 0 ? degreeDifference : left.id - right.id;
|
|
202
244
|
}
|
|
203
245
|
function weightOf(symbol) {
|
|
204
246
|
return symbol.references.length + 1;
|
|
@@ -257,8 +299,17 @@ function validateBindings(chunk, original, names, globalRenames) {
|
|
|
257
299
|
return;
|
|
258
300
|
}
|
|
259
301
|
[symbol.declaration, ...symbol.references].forEach((identifier) => {
|
|
260
|
-
|
|
261
|
-
|
|
302
|
+
const rebound = recolored.symbolOf(identifier);
|
|
303
|
+
if (rebound?.declaration !== symbol.declaration) {
|
|
304
|
+
const reboundOriginal = rebound
|
|
305
|
+
? original.symbolOf(rebound.declaration)
|
|
306
|
+
: undefined;
|
|
307
|
+
throw new Error(`Identifier coloring changed binding for symbol ${String(symbol.id)} ` +
|
|
308
|
+
`(${symbol.name} -> ${names.get(symbol) ?? symbol.name}, ` +
|
|
309
|
+
`declaration ${formatIdentifierLocation(symbol.declaration)}, ` +
|
|
310
|
+
`reference ${formatIdentifierLocation(identifier)}, ` +
|
|
311
|
+
`rebound to ${reboundOriginal ? `symbol ${String(reboundOriginal.id)} (${reboundOriginal.name} -> ${names.get(reboundOriginal) ?? reboundOriginal.name}) at ${formatIdentifierLocation(reboundOriginal.declaration)}` : "global"})`);
|
|
312
|
+
}
|
|
262
313
|
});
|
|
263
314
|
});
|
|
264
315
|
original.globals.forEach((binding) => {
|
|
@@ -268,3 +319,9 @@ function validateBindings(chunk, original, names, globalRenames) {
|
|
|
268
319
|
});
|
|
269
320
|
});
|
|
270
321
|
}
|
|
322
|
+
function formatIdentifierLocation(identifier) {
|
|
323
|
+
const location = identifier.loc?.start;
|
|
324
|
+
return location
|
|
325
|
+
? `${String(location.line)}:${String(location.column)}`
|
|
326
|
+
: "unknown";
|
|
327
|
+
}
|
package/dist/resolver.js
CHANGED
|
@@ -6,6 +6,8 @@ function resolveScopes(chunk, options = {}) {
|
|
|
6
6
|
const allSymbols = [];
|
|
7
7
|
const globals = new Map();
|
|
8
8
|
const identifierSymbols = new WeakMap();
|
|
9
|
+
const identifierResolutionOrders = new WeakMap();
|
|
10
|
+
let nextResolutionOrder = 0;
|
|
9
11
|
const globalReferenceNodes = new WeakSet();
|
|
10
12
|
const functionScopes = new WeakMap();
|
|
11
13
|
const identifierName = options.identifierName ??
|
|
@@ -23,6 +25,7 @@ function resolveScopes(chunk, options = {}) {
|
|
|
23
25
|
return scope;
|
|
24
26
|
}
|
|
25
27
|
function declare(scope, node, kind, implicit = false) {
|
|
28
|
+
identifierResolutionOrders.set(node, nextResolutionOrder++);
|
|
26
29
|
const name = identifierName(node);
|
|
27
30
|
const symbol = {
|
|
28
31
|
id: nextSymbolId++,
|
|
@@ -41,6 +44,7 @@ function resolveScopes(chunk, options = {}) {
|
|
|
41
44
|
return symbol;
|
|
42
45
|
}
|
|
43
46
|
function declareLabel(scope, node) {
|
|
47
|
+
identifierResolutionOrders.set(node, nextResolutionOrder++);
|
|
44
48
|
const name = identifierName(node);
|
|
45
49
|
const symbol = {
|
|
46
50
|
id: nextSymbolId++,
|
|
@@ -75,6 +79,7 @@ function resolveScopes(chunk, options = {}) {
|
|
|
75
79
|
return undefined;
|
|
76
80
|
}
|
|
77
81
|
function reference(scope, node, isWrite = false) {
|
|
82
|
+
identifierResolutionOrders.set(node, nextResolutionOrder++);
|
|
78
83
|
const name = identifierName(node);
|
|
79
84
|
const symbol = lookupBinding(scope, name);
|
|
80
85
|
if (symbol) {
|
|
@@ -195,6 +200,7 @@ function resolveScopes(chunk, options = {}) {
|
|
|
195
200
|
// hoistLabelsで宣言済みのため、ここでは何もしない
|
|
196
201
|
return;
|
|
197
202
|
case "GotoStatement": {
|
|
203
|
+
identifierResolutionOrders.set(statement.label, nextResolutionOrder++);
|
|
198
204
|
const symbol = lookupLabel(scope, identifierName(statement.label));
|
|
199
205
|
if (symbol) {
|
|
200
206
|
symbol.references.push(statement.label);
|
|
@@ -319,6 +325,7 @@ function resolveScopes(chunk, options = {}) {
|
|
|
319
325
|
chunkScope,
|
|
320
326
|
symbols: allSymbols,
|
|
321
327
|
globals,
|
|
328
|
+
resolutionOrderOf: (identifier) => identifierResolutionOrders.get(identifier),
|
|
322
329
|
symbolOf: (identifier) => identifierSymbols.get(identifier),
|
|
323
330
|
isGlobalReference: (identifier) => globalReferenceNodes.has(identifier),
|
|
324
331
|
scopeOfFunction: (fn) => functionScopes.get(fn),
|
|
@@ -140,12 +140,15 @@ function applyStatementSchedule(schedule, metadata) {
|
|
|
140
140
|
const actions = [
|
|
141
141
|
...schedule.localGroups.map((group) => ({ kind: "local", group })),
|
|
142
142
|
...schedule.tableGroups.map((group) => ({ kind: "table", group })),
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
return 0;
|
|
146
|
-
return right.group.indexes[0] - left.group.indexes[0];
|
|
147
|
-
});
|
|
143
|
+
];
|
|
144
|
+
const actionsByBody = new Map();
|
|
148
145
|
actions.forEach((action) => {
|
|
146
|
+
const bodyActions = actionsByBody.get(action.group.body) ?? [];
|
|
147
|
+
bodyActions.push(action);
|
|
148
|
+
actionsByBody.set(action.group.body, bodyActions);
|
|
149
|
+
});
|
|
150
|
+
const orderedActions = [...actionsByBody.values()].flatMap((bodyActions) => bodyActions.sort((left, right) => right.group.indexes[0] - left.group.indexes[0]));
|
|
151
|
+
orderedActions.forEach((action) => {
|
|
149
152
|
if (action.kind === "table") {
|
|
150
153
|
const group = action.group;
|
|
151
154
|
const combined = {
|