storm-lua-minify 0.3.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 +119 -41
- package/dist/aggregateSpecialization.js +406 -0
- package/dist/ast2lua.js +156 -68
- package/dist/astWalk.js +162 -0
- package/dist/callGraph.js +372 -0
- package/dist/cli.js +53 -58
- 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 +17 -4
- package/dist/interproceduralAnalysis.js +842 -0
- package/dist/interproceduralConstants.js +120 -0
- package/dist/luaString.js +157 -0
- package/dist/minifier.js +1219 -49
- 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 +200 -0
- package/dist/options.js +233 -0
- package/dist/progress.js +2 -0
- package/dist/removeUnused.js +145 -0
- package/dist/renamer.js +280 -54
- package/dist/resolver.js +35 -11
- package/dist/runtimeEnvironment.js +105 -0
- package/dist/sourceMetadata.js +314 -0
- package/dist/statementDataflow.js +259 -0
- package/dist/statementScheduler.js +598 -0
- package/dist/symbolLiveness.js +92 -0
- package/dist/tableEffects.js +356 -0
- package/dist/transform.js +10 -371
- 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 +11 -2
- package/dist/index.js +0 -27
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planStatementSchedule = planStatementSchedule;
|
|
4
|
+
exports.applyStatementSchedule = applyStatementSchedule;
|
|
5
|
+
const astWalk_1 = require("./astWalk");
|
|
6
|
+
const generatedNode_1 = require("./generatedNode");
|
|
7
|
+
const linker_1 = require("./linker");
|
|
8
|
+
const luaString_1 = require("./luaString");
|
|
9
|
+
/**
|
|
10
|
+
* Plans source-order rewrites from shared CFG/facts. Initializers are split when a declaration crosses
|
|
11
|
+
* another statement; only the lexical binding point moves, never an evaluation.
|
|
12
|
+
*/
|
|
13
|
+
function planStatementSchedule(chunk, resolved, options) {
|
|
14
|
+
if (options.facts.generation !== options.dataflow.generation ||
|
|
15
|
+
options.facts.generation !== options.dataflow.controlFlow.version)
|
|
16
|
+
throw new Error("Statement scheduler requires one AST generation");
|
|
17
|
+
const localGroups = [];
|
|
18
|
+
const tableGroups = [];
|
|
19
|
+
const tableStatements = new WeakSet();
|
|
20
|
+
const scheduledLocals = new WeakSet();
|
|
21
|
+
if (options.tableEffects) {
|
|
22
|
+
planTableGroups(chunk.body, options, tableGroups, tableStatements);
|
|
23
|
+
}
|
|
24
|
+
const processBody = (body) => {
|
|
25
|
+
body.forEach((statement) => {
|
|
26
|
+
childBodies(statement).forEach(processBody);
|
|
27
|
+
});
|
|
28
|
+
let run = [];
|
|
29
|
+
const record = (decision, reason, candidateSize, byteSavings, sourceRange) => options.diagnostics?.record({
|
|
30
|
+
pass: "statement-scheduler",
|
|
31
|
+
moduleName: options.moduleName,
|
|
32
|
+
runtimeProfile: options.runtimeProfile,
|
|
33
|
+
decision,
|
|
34
|
+
reason,
|
|
35
|
+
candidateSize,
|
|
36
|
+
estimatedByteSavings: byteSavings,
|
|
37
|
+
estimatedOpportunityBytes: decision === "rejected"
|
|
38
|
+
? Math.max(0, 5 * (candidateSize - 1))
|
|
39
|
+
: undefined,
|
|
40
|
+
sourceRange,
|
|
41
|
+
});
|
|
42
|
+
const flush = (reason = "insufficient-group") => {
|
|
43
|
+
if (run.length < 2) {
|
|
44
|
+
if (run.length === 1)
|
|
45
|
+
record("rejected", reason, 1, undefined, rangeOf(run[0].statement));
|
|
46
|
+
run = [];
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const lengths = run.map((candidate) => options.outputNameLengthOf(candidate.symbol));
|
|
50
|
+
if (!lengths.every((length) => length !== undefined)) {
|
|
51
|
+
record("rejected", "output-name-unknown", run.length, undefined, rangeOf(run[0].statement));
|
|
52
|
+
run = [];
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const adjacent = run.every((candidate, index) => index === 0 || candidate.index === run[index - 1].index + 1);
|
|
56
|
+
const initializerDependency = run.some((candidate, index) => {
|
|
57
|
+
if (index === 0)
|
|
58
|
+
return false;
|
|
59
|
+
const prior = new Set(run.slice(0, index).map((item) => item.symbol));
|
|
60
|
+
return options.facts
|
|
61
|
+
.operationsOf(candidate.statement)
|
|
62
|
+
.some((operation) => operation.kind === "read" &&
|
|
63
|
+
symbolOf(operation) !== undefined &&
|
|
64
|
+
prior.has(symbolOf(operation)));
|
|
65
|
+
});
|
|
66
|
+
const mode = adjacent && !initializerDependency
|
|
67
|
+
? "merge-initializers"
|
|
68
|
+
: "split-initializers";
|
|
69
|
+
const byteSavings = mode === "merge-initializers"
|
|
70
|
+
? 5 * (run.length - 1)
|
|
71
|
+
: lengths.slice(1).reduce((sum, length) => sum + 5 - length, 0);
|
|
72
|
+
const maxHoisted = options.maxHoistedLocalsAt?.(run[0].statement) ?? Infinity;
|
|
73
|
+
if (mode === "split-initializers" && run.length - 1 > maxHoisted) {
|
|
74
|
+
record("rejected", "resource-budget", run.length, undefined, rangeOf(run[0].statement));
|
|
75
|
+
}
|
|
76
|
+
else if (byteSavings <= 0) {
|
|
77
|
+
record("rejected", "nonpositive-cost", run.length, undefined, rangeOf(run[0].statement));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
localGroups.push({
|
|
81
|
+
body,
|
|
82
|
+
statements: run.map((candidate) => candidate.statement),
|
|
83
|
+
indexes: run.map((candidate) => candidate.index),
|
|
84
|
+
symbols: run.map((candidate) => candidate.symbol),
|
|
85
|
+
mode,
|
|
86
|
+
byteSavings,
|
|
87
|
+
});
|
|
88
|
+
run.forEach((candidate) => scheduledLocals.add(candidate.statement));
|
|
89
|
+
record("accepted", "profitable-group", run.length, byteSavings, rangeOf(run[0].statement));
|
|
90
|
+
}
|
|
91
|
+
run = [];
|
|
92
|
+
};
|
|
93
|
+
body.forEach((statement, index) => {
|
|
94
|
+
if (isHardBoundary(statement)) {
|
|
95
|
+
flush("control-flow-barrier");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (statement.type !== "LocalStatement")
|
|
99
|
+
return;
|
|
100
|
+
if (tableStatements.has(statement) || scheduledLocals.has(statement)) {
|
|
101
|
+
flush();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const candidate = candidateOf(statement, index, resolved, options);
|
|
105
|
+
if ("reason" in candidate) {
|
|
106
|
+
flush(candidate.reason);
|
|
107
|
+
record("rejected", candidate.reason, 1, undefined, rangeOf(statement));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const start = run[0]?.index ?? index;
|
|
111
|
+
if (options.dataflow.controlFlow.unknownEdges.some((edge) => edge.from.unit ===
|
|
112
|
+
options.dataflow.controlFlow.pointOf(statement)?.unit)) {
|
|
113
|
+
flush("unknown-control-flow");
|
|
114
|
+
record("rejected", "unknown-control-flow", 1, undefined, rangeOf(statement));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (widensOverNameReference(body, start, index, candidate.symbol.name, options.facts) ||
|
|
118
|
+
run.some((prior) => prior.symbol.name === candidate.symbol.name)) {
|
|
119
|
+
flush("binding-shadow-hazard");
|
|
120
|
+
}
|
|
121
|
+
if (run.length > 0 &&
|
|
122
|
+
options.canChangeLocalLifetime?.(statement) === false) {
|
|
123
|
+
flush("metadata-preserved");
|
|
124
|
+
}
|
|
125
|
+
run.push(candidate);
|
|
126
|
+
});
|
|
127
|
+
flush();
|
|
128
|
+
};
|
|
129
|
+
if (options.enableLexicalLocalMerge !== false) {
|
|
130
|
+
planLexicalLocalGroups(chunk.body, resolved, options, localGroups, tableStatements, scheduledLocals);
|
|
131
|
+
}
|
|
132
|
+
// Preserve the cheapest structural rewrite first. Non-adjacent packing can
|
|
133
|
+
// otherwise claim an adjacent run and replace its five-byte `local` removal
|
|
134
|
+
// with initializer assignments that only become profitable in isolation.
|
|
135
|
+
if (options.enableLocalPacking !== false)
|
|
136
|
+
processBody(chunk.body);
|
|
137
|
+
return { generation: options.facts.generation, localGroups, tableGroups };
|
|
138
|
+
}
|
|
139
|
+
function applyStatementSchedule(schedule, metadata) {
|
|
140
|
+
const actions = [
|
|
141
|
+
...schedule.localGroups.map((group) => ({ kind: "local", group })),
|
|
142
|
+
...schedule.tableGroups.map((group) => ({ kind: "table", group })),
|
|
143
|
+
];
|
|
144
|
+
const actionsByBody = new Map();
|
|
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) => {
|
|
152
|
+
if (action.kind === "table") {
|
|
153
|
+
const group = action.group;
|
|
154
|
+
const combined = {
|
|
155
|
+
type: "LocalStatement",
|
|
156
|
+
variables: group.statements.map((statement) => statement.variables[0]),
|
|
157
|
+
init: group.statements.map((statement) => statement.init[0]),
|
|
158
|
+
};
|
|
159
|
+
(0, generatedNode_1.copyNodeOrigin)(combined, group.statements[0]);
|
|
160
|
+
metadata?.transferStatements(group.statements, combined);
|
|
161
|
+
for (let offset = group.indexes.length - 1; offset >= 0; offset--) {
|
|
162
|
+
const index = group.indexes[offset];
|
|
163
|
+
group.body.splice(index, 1, ...(offset === 0 ? [combined] : []));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const group = action.group;
|
|
168
|
+
const combined = {
|
|
169
|
+
type: "LocalStatement",
|
|
170
|
+
variables: group.statements.flatMap((statement) => statement.variables),
|
|
171
|
+
init: group.mode === "merge-initializers"
|
|
172
|
+
? combineInitializerValues(group.statements)
|
|
173
|
+
: [group.statements[0].init[0]],
|
|
174
|
+
};
|
|
175
|
+
(0, generatedNode_1.copyNodeOrigin)(combined, group.statements[0]);
|
|
176
|
+
if (group.mode === "merge-initializers") {
|
|
177
|
+
metadata?.transferStatements(group.statements, combined);
|
|
178
|
+
for (let offset = group.indexes.length - 1; offset >= 0; offset--) {
|
|
179
|
+
const index = group.indexes[offset];
|
|
180
|
+
group.body.splice(index, 1, ...(offset === 0 ? [combined] : []));
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const assignments = group.statements.slice(1).map((statement) => {
|
|
185
|
+
const assignment = {
|
|
186
|
+
type: "AssignmentStatement",
|
|
187
|
+
variables: [(0, generatedNode_1.identifierWithOrigin)(statement.variables[0])],
|
|
188
|
+
init: [statement.init[0]],
|
|
189
|
+
};
|
|
190
|
+
(0, generatedNode_1.copyNodeOrigin)(assignment, statement);
|
|
191
|
+
return assignment;
|
|
192
|
+
});
|
|
193
|
+
for (let offset = group.indexes.length - 1; offset >= 0; offset--) {
|
|
194
|
+
const index = group.indexes[offset];
|
|
195
|
+
const source = group.statements[offset];
|
|
196
|
+
const replacements = offset === 0 ? [combined] : [assignments[offset - 1]];
|
|
197
|
+
metadata?.replaceStatement(source, replacements);
|
|
198
|
+
group.body.splice(index, 1, ...replacements);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const changed = actions.length > 0;
|
|
202
|
+
return {
|
|
203
|
+
changed,
|
|
204
|
+
invalidatesResolve: changed,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function planLexicalLocalGroups(body, resolved, options, groups, tableStatements, scheduled) {
|
|
208
|
+
body.forEach((statement) => {
|
|
209
|
+
childBodies(statement).forEach((child) => {
|
|
210
|
+
planLexicalLocalGroups(child, resolved, options, groups, tableStatements, scheduled);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
let run = [];
|
|
214
|
+
let runStart = 0;
|
|
215
|
+
const flush = () => {
|
|
216
|
+
if (run.length >= 2) {
|
|
217
|
+
const symbols = run.flatMap((statement) => statement.variables.flatMap((variable) => {
|
|
218
|
+
const symbol = resolved.symbolOf(variable);
|
|
219
|
+
return symbol ? [symbol] : [];
|
|
220
|
+
}));
|
|
221
|
+
const padding = paddingCountOf(run);
|
|
222
|
+
const byteSavings = 5 * (run.length - 1) - 4 * padding;
|
|
223
|
+
if (symbols.length ===
|
|
224
|
+
run.reduce((sum, statement) => sum + statement.variables.length, 0) &&
|
|
225
|
+
byteSavings > 0) {
|
|
226
|
+
const indexes = run.map((_, offset) => runStart + offset);
|
|
227
|
+
groups.push({
|
|
228
|
+
body,
|
|
229
|
+
statements: run,
|
|
230
|
+
indexes,
|
|
231
|
+
symbols,
|
|
232
|
+
mode: "merge-initializers",
|
|
233
|
+
byteSavings,
|
|
234
|
+
});
|
|
235
|
+
run.forEach((statement) => scheduled.add(statement));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
run = [];
|
|
239
|
+
};
|
|
240
|
+
body.forEach((statement, index) => {
|
|
241
|
+
if (statement.type !== "LocalStatement" ||
|
|
242
|
+
tableStatements.has(statement) ||
|
|
243
|
+
scheduled.has(statement) ||
|
|
244
|
+
(options.preserveRequireSplice &&
|
|
245
|
+
statement.init.length === 1 &&
|
|
246
|
+
isRequireCall(statement.init[0]))) {
|
|
247
|
+
flush();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (run.length > 0) {
|
|
251
|
+
const previous = run[run.length - 1];
|
|
252
|
+
if (!classifyNonTerminal(previous).safe ||
|
|
253
|
+
initializerReferencesPrior(statement, run, resolved, options.facts))
|
|
254
|
+
flush();
|
|
255
|
+
}
|
|
256
|
+
if (run.length === 0)
|
|
257
|
+
runStart = index;
|
|
258
|
+
run.push(statement);
|
|
259
|
+
});
|
|
260
|
+
flush();
|
|
261
|
+
}
|
|
262
|
+
function initializerReferencesPrior(candidate, prior, resolved, facts) {
|
|
263
|
+
const declarations = new Set(prior.flatMap((statement) => statement.variables.flatMap((variable) => {
|
|
264
|
+
const symbol = resolved.symbolOf(variable);
|
|
265
|
+
return symbol ? [symbol] : [];
|
|
266
|
+
})));
|
|
267
|
+
return facts
|
|
268
|
+
.operationsWithin(candidate)
|
|
269
|
+
.some((operation) => operation.kind === "read" &&
|
|
270
|
+
symbolOf(operation) !== undefined &&
|
|
271
|
+
declarations.has(symbolOf(operation)));
|
|
272
|
+
}
|
|
273
|
+
function classifyNonTerminal(statement) {
|
|
274
|
+
if (statement.variables.length === statement.init.length) {
|
|
275
|
+
return { safe: true, needsPadding: false };
|
|
276
|
+
}
|
|
277
|
+
if (statement.variables.length > statement.init.length) {
|
|
278
|
+
const last = statement.init.at(-1);
|
|
279
|
+
return last && isExpandable(last)
|
|
280
|
+
? { safe: false, needsPadding: false }
|
|
281
|
+
: { safe: true, needsPadding: true };
|
|
282
|
+
}
|
|
283
|
+
return { safe: false, needsPadding: false };
|
|
284
|
+
}
|
|
285
|
+
function isExpandable(expression) {
|
|
286
|
+
return (expression.type === "CallExpression" ||
|
|
287
|
+
expression.type === "TableCallExpression" ||
|
|
288
|
+
expression.type === "StringCallExpression" ||
|
|
289
|
+
expression.type === "VarargLiteral");
|
|
290
|
+
}
|
|
291
|
+
function paddingCountOf(statements) {
|
|
292
|
+
return statements.slice(0, -1).reduce((sum, statement) => {
|
|
293
|
+
const classification = classifyNonTerminal(statement);
|
|
294
|
+
return (sum +
|
|
295
|
+
(classification.safe && classification.needsPadding
|
|
296
|
+
? statement.variables.length - statement.init.length
|
|
297
|
+
: 0));
|
|
298
|
+
}, 0);
|
|
299
|
+
}
|
|
300
|
+
function combineInitializerValues(statements) {
|
|
301
|
+
const init = [];
|
|
302
|
+
statements.forEach((statement, index) => {
|
|
303
|
+
init.push(...statement.init);
|
|
304
|
+
if (index < statements.length - 1) {
|
|
305
|
+
const classification = classifyNonTerminal(statement);
|
|
306
|
+
if (classification.safe && classification.needsPadding) {
|
|
307
|
+
const count = statement.variables.length - statement.init.length;
|
|
308
|
+
for (let padding = 0; padding < count; padding++) {
|
|
309
|
+
init.push({ type: "NilLiteral", value: null, raw: "nil" });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
while (init.at(-1)?.type === "NilLiteral")
|
|
315
|
+
init.pop();
|
|
316
|
+
return init;
|
|
317
|
+
}
|
|
318
|
+
function planTableGroups(body, options, groups, claimed) {
|
|
319
|
+
body.forEach((statement) => {
|
|
320
|
+
childBodies(statement).forEach((child) => {
|
|
321
|
+
planTableGroups(child, options, groups, claimed);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const analysis = options.tableEffects;
|
|
325
|
+
if (!analysis)
|
|
326
|
+
return;
|
|
327
|
+
const indexOf = new Map(body.map((statement, index) => [statement, index]));
|
|
328
|
+
let run = [];
|
|
329
|
+
const flush = (rejectionReason = "insufficient-group") => {
|
|
330
|
+
const policyLimit = options.maxTableMergeArity ?? 50;
|
|
331
|
+
let accepted = 0;
|
|
332
|
+
for (let start = 0; start < run.length;) {
|
|
333
|
+
const limit = Math.min(policyLimit, options.maxTableMergeArityAt?.(run[start].statement) ?? policyLimit);
|
|
334
|
+
const part = run.slice(start, start + limit);
|
|
335
|
+
start += Math.max(1, part.length);
|
|
336
|
+
if (part.length < 2)
|
|
337
|
+
continue;
|
|
338
|
+
const byteSavings = 5 * (part.length - 1);
|
|
339
|
+
groups.push({
|
|
340
|
+
body,
|
|
341
|
+
statements: part.map((candidate) => candidate.statement),
|
|
342
|
+
indexes: part.map((candidate) => candidate.index),
|
|
343
|
+
reads: part.map((candidate) => candidate.read),
|
|
344
|
+
byteSavings,
|
|
345
|
+
});
|
|
346
|
+
part.forEach((candidate) => claimed.add(candidate.statement));
|
|
347
|
+
accepted += part.length;
|
|
348
|
+
options.diagnostics?.record({
|
|
349
|
+
pass: "statement-scheduler",
|
|
350
|
+
moduleName: options.moduleName,
|
|
351
|
+
runtimeProfile: options.runtimeProfile,
|
|
352
|
+
decision: "accepted",
|
|
353
|
+
reason: "profitable-group",
|
|
354
|
+
candidateSize: part.length,
|
|
355
|
+
estimatedByteSavings: byteSavings,
|
|
356
|
+
sourceRange: rangeOf(part[0].statement),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (run.length > accepted)
|
|
360
|
+
options.diagnostics?.record({
|
|
361
|
+
pass: "statement-scheduler",
|
|
362
|
+
moduleName: options.moduleName,
|
|
363
|
+
runtimeProfile: options.runtimeProfile,
|
|
364
|
+
decision: "rejected",
|
|
365
|
+
reason: rejectionReason,
|
|
366
|
+
candidateSize: run.length - accepted,
|
|
367
|
+
estimatedOpportunityBytes: Math.max(0, 5 * (run.length - accepted - 1)),
|
|
368
|
+
sourceRange: rangeOf(run[accepted]?.statement ?? run[0].statement),
|
|
369
|
+
});
|
|
370
|
+
run = [];
|
|
371
|
+
};
|
|
372
|
+
body.forEach((statement, index) => {
|
|
373
|
+
if (statement.type === "IfStatement" ||
|
|
374
|
+
statement.type === "WhileStatement" ||
|
|
375
|
+
statement.type === "RepeatStatement" ||
|
|
376
|
+
statement.type === "ForNumericStatement" ||
|
|
377
|
+
statement.type === "ForGenericStatement" ||
|
|
378
|
+
isHardBoundary(statement)) {
|
|
379
|
+
flush();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (statement.type === "AssignmentStatement" ||
|
|
383
|
+
statement.type === "CallStatement")
|
|
384
|
+
return;
|
|
385
|
+
const decision = tableCandidateOf(statement, index, analysis, options);
|
|
386
|
+
if (!("candidate" in decision)) {
|
|
387
|
+
flush();
|
|
388
|
+
if (decision.reason)
|
|
389
|
+
options.diagnostics?.record({
|
|
390
|
+
pass: "statement-scheduler",
|
|
391
|
+
moduleName: options.moduleName,
|
|
392
|
+
runtimeProfile: options.runtimeProfile,
|
|
393
|
+
decision: "rejected",
|
|
394
|
+
reason: decision.reason,
|
|
395
|
+
candidateSize: 1,
|
|
396
|
+
estimatedOpportunityBytes: 0,
|
|
397
|
+
sourceRange: rangeOf(statement),
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const candidate = decision.candidate;
|
|
402
|
+
const point = options.dataflow.controlFlow.pointOf(statement);
|
|
403
|
+
if (point &&
|
|
404
|
+
options.dataflow.controlFlow.unknownEdges.some((edge) => edge.from.unit === point.unit)) {
|
|
405
|
+
flush("unknown-control-flow");
|
|
406
|
+
options.diagnostics?.record({
|
|
407
|
+
pass: "statement-scheduler",
|
|
408
|
+
moduleName: options.moduleName,
|
|
409
|
+
runtimeProfile: options.runtimeProfile,
|
|
410
|
+
decision: "rejected",
|
|
411
|
+
reason: "unknown-control-flow",
|
|
412
|
+
candidateSize: 1,
|
|
413
|
+
estimatedOpportunityBytes: 0,
|
|
414
|
+
sourceRange: rangeOf(statement),
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (run.length > 0) {
|
|
419
|
+
const first = run[0];
|
|
420
|
+
const stability = analysis.stabilityBetween(candidate.read.table, candidate.read.baseSymbol, first.statement, candidate.statement);
|
|
421
|
+
const dirty = tableDirtyReasonBetween(candidate.read, first.index, index, indexOf, analysis, options.dirtyGranularity ?? "static-key");
|
|
422
|
+
const shadow = shadowsInterveningReference(body, first.index, candidate, analysis);
|
|
423
|
+
const dependency = body
|
|
424
|
+
.slice(first.index + 1, index)
|
|
425
|
+
.flatMap((obstacle) => options.dataflow.dependenciesBetween(obstacle, statement))
|
|
426
|
+
.find((edge) => edge.kind !== "error-order" &&
|
|
427
|
+
edge.kind !== "metamethod-order" &&
|
|
428
|
+
edge.kind !== "scope-order" &&
|
|
429
|
+
!(options.allowObservableTableValueChanges &&
|
|
430
|
+
(edge.kind === "read-after-write" ||
|
|
431
|
+
edge.kind === "write-after-read")));
|
|
432
|
+
if (shadow)
|
|
433
|
+
flush("binding-shadow-hazard");
|
|
434
|
+
else if (dependency)
|
|
435
|
+
flush(dependencyReason(dependency.kind));
|
|
436
|
+
else if (!options.allowObservableTableValueChanges && !stability.stable) {
|
|
437
|
+
flush(stability.reason);
|
|
438
|
+
}
|
|
439
|
+
else if (!options.allowObservableTableValueChanges && dirty)
|
|
440
|
+
flush(dirty);
|
|
441
|
+
}
|
|
442
|
+
run.push(candidate);
|
|
443
|
+
});
|
|
444
|
+
flush();
|
|
445
|
+
}
|
|
446
|
+
function tableCandidateOf(statement, index, analysis, options) {
|
|
447
|
+
if (statement.type !== "LocalStatement")
|
|
448
|
+
return {};
|
|
449
|
+
if (statement.variables.length !== 1 || statement.init.length !== 1) {
|
|
450
|
+
return { reason: "unsupported-shape" };
|
|
451
|
+
}
|
|
452
|
+
const init = statement.init[0];
|
|
453
|
+
if (init.type !== "MemberExpression" && init.type !== "IndexExpression")
|
|
454
|
+
return {};
|
|
455
|
+
if (options.canMoveTableRead?.(statement) === false)
|
|
456
|
+
return { reason: "metadata-preserved" };
|
|
457
|
+
const read = analysis.effects.find((effect) => effect.access === "read" && effect.expression === init);
|
|
458
|
+
if (!read)
|
|
459
|
+
return { reason: "allocation-unknown" };
|
|
460
|
+
if (read.staticKey === undefined) {
|
|
461
|
+
return {
|
|
462
|
+
reason: init.type === "IndexExpression" &&
|
|
463
|
+
init.index.type === "StringLiteral" &&
|
|
464
|
+
!(0, luaString_1.decodeLuaStringLiteral)(init.index).ok
|
|
465
|
+
? "unsupported-string-key"
|
|
466
|
+
: "dynamic-key",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const escape = analysis.escapeReasonsOf(read.table).at(0);
|
|
470
|
+
if (escape)
|
|
471
|
+
return { reason: escapeReasonOf(escape) };
|
|
472
|
+
return { candidate: { statement, index, read } };
|
|
473
|
+
}
|
|
474
|
+
function tableDirtyReasonBetween(read, start, end, indexOf, analysis, granularity) {
|
|
475
|
+
const dirty = analysis.effectsOf(read.table).find((effect) => {
|
|
476
|
+
const index = indexOf.get(effect.owner);
|
|
477
|
+
if (effect.access !== "write" ||
|
|
478
|
+
index === undefined ||
|
|
479
|
+
index <= start ||
|
|
480
|
+
index >= end)
|
|
481
|
+
return false;
|
|
482
|
+
return (granularity === "table" ||
|
|
483
|
+
effect.staticKey === undefined ||
|
|
484
|
+
effect.staticKey === read.staticKey);
|
|
485
|
+
});
|
|
486
|
+
if (!dirty)
|
|
487
|
+
return undefined;
|
|
488
|
+
return granularity === "static-key" && dirty.staticKey !== undefined
|
|
489
|
+
? "dirty-static-key"
|
|
490
|
+
: "dirty-table";
|
|
491
|
+
}
|
|
492
|
+
function shadowsInterveningReference(body, start, candidate, analysis) {
|
|
493
|
+
const name = candidate.statement.variables[0].name;
|
|
494
|
+
for (let index = start; index <= candidate.index; index++) {
|
|
495
|
+
if (analysis.facts
|
|
496
|
+
.operationsWithin(body[index])
|
|
497
|
+
.some((operation) => (operation.kind === "read" || operation.kind === "write") &&
|
|
498
|
+
nameOf(operation) === name))
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
function escapeReasonOf(reason) {
|
|
504
|
+
return `${reason === "value-use" ? "value-use" : reason}-escape`;
|
|
505
|
+
}
|
|
506
|
+
function dependencyReason(kind) {
|
|
507
|
+
return `dependency-${kind}`;
|
|
508
|
+
}
|
|
509
|
+
function candidateOf(statement, index, resolved, options) {
|
|
510
|
+
if (statement.variables.length !== 1 || statement.init.length !== 1) {
|
|
511
|
+
return { reason: "unsupported-shape" };
|
|
512
|
+
}
|
|
513
|
+
if (options.preserveRequireSplice && isRequireCall(statement.init[0])) {
|
|
514
|
+
return { reason: "require-splice" };
|
|
515
|
+
}
|
|
516
|
+
const symbol = resolved.symbolOf(statement.variables[0]);
|
|
517
|
+
return symbol?.kind === "local"
|
|
518
|
+
? { statement, index, symbol }
|
|
519
|
+
: { reason: "unsupported-shape" };
|
|
520
|
+
}
|
|
521
|
+
function widensOverNameReference(body, start, end, name, facts) {
|
|
522
|
+
for (let index = start; index < end; index++) {
|
|
523
|
+
if (facts
|
|
524
|
+
.operationsWithin(body[index])
|
|
525
|
+
.some((operation) => nameOf(operation) === name)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
function symbolOf(operation) {
|
|
532
|
+
if (!("location" in operation))
|
|
533
|
+
return undefined;
|
|
534
|
+
const location = operation.location;
|
|
535
|
+
return location.kind === "local" ||
|
|
536
|
+
location.kind === "parameter" ||
|
|
537
|
+
location.kind === "upvalue"
|
|
538
|
+
? location.symbol
|
|
539
|
+
: undefined;
|
|
540
|
+
}
|
|
541
|
+
function nameOf(operation) {
|
|
542
|
+
if (!("location" in operation))
|
|
543
|
+
return undefined;
|
|
544
|
+
const location = operation.location;
|
|
545
|
+
if (location.kind === "local" ||
|
|
546
|
+
location.kind === "parameter" ||
|
|
547
|
+
location.kind === "upvalue") {
|
|
548
|
+
return location.symbol.name;
|
|
549
|
+
}
|
|
550
|
+
return location.kind === "global" ? location.binding.name : undefined;
|
|
551
|
+
}
|
|
552
|
+
function isHardBoundary(statement) {
|
|
553
|
+
return (statement.type === "ReturnStatement" ||
|
|
554
|
+
statement.type === "BreakStatement" ||
|
|
555
|
+
statement.type === "GotoStatement" ||
|
|
556
|
+
statement.type === "LabelStatement" ||
|
|
557
|
+
statement.type === "FunctionDeclaration");
|
|
558
|
+
}
|
|
559
|
+
function childBodies(statement) {
|
|
560
|
+
const bodies = [];
|
|
561
|
+
switch (statement.type) {
|
|
562
|
+
case "DoStatement":
|
|
563
|
+
case "WhileStatement":
|
|
564
|
+
case "RepeatStatement":
|
|
565
|
+
case "ForNumericStatement":
|
|
566
|
+
case "ForGenericStatement":
|
|
567
|
+
bodies.push(statement.body);
|
|
568
|
+
break;
|
|
569
|
+
case "FunctionDeclaration":
|
|
570
|
+
bodies.push(statement.body);
|
|
571
|
+
break;
|
|
572
|
+
case "IfStatement":
|
|
573
|
+
bodies.push(...statement.clauses.map((clause) => clause.body));
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
(0, astWalk_1.walkStatement)(statement, {
|
|
577
|
+
onFunction: (fn) => {
|
|
578
|
+
if (!bodies.includes(fn.body))
|
|
579
|
+
bodies.push(fn.body);
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
return bodies;
|
|
583
|
+
}
|
|
584
|
+
function isRequireCall(expression) {
|
|
585
|
+
if (expression.type === "CallExpression") {
|
|
586
|
+
return (expression.base.type === "Identifier" &&
|
|
587
|
+
expression.base.name === "require" &&
|
|
588
|
+
expression.arguments.length > 0 &&
|
|
589
|
+
(0, linker_1.staticStringArgument)(expression.arguments[0]) !== undefined);
|
|
590
|
+
}
|
|
591
|
+
return (expression.type === "StringCallExpression" &&
|
|
592
|
+
expression.base.type === "Identifier" &&
|
|
593
|
+
expression.base.name === "require" &&
|
|
594
|
+
(0, linker_1.staticStringArgument)(expression.argument) !== undefined);
|
|
595
|
+
}
|
|
596
|
+
function rangeOf(statement) {
|
|
597
|
+
return statement.range;
|
|
598
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeSymbolLiveness = analyzeSymbolLiveness;
|
|
4
|
+
/**
|
|
5
|
+
* Computes the symbol liveness shared by scheduling and identifier coloring.
|
|
6
|
+
* Nested functions are independent CFG units; upvalue reads remain uses in the
|
|
7
|
+
* nested unit, while parameter declarations owned by a function syntax node
|
|
8
|
+
* must not be mistaken for definitions in its parent's unit.
|
|
9
|
+
*/
|
|
10
|
+
function analyzeSymbolLiveness(controlFlow, facts) {
|
|
11
|
+
const directUses = new Map();
|
|
12
|
+
const directDefs = new Map();
|
|
13
|
+
const liveIn = new Map();
|
|
14
|
+
const liveOut = new Map();
|
|
15
|
+
controlFlow.nodes.forEach((node) => {
|
|
16
|
+
const operations = node.statement ? facts.operationsOf(node.statement) : [];
|
|
17
|
+
directUses.set(node, new Set(operations.flatMap((operation) => {
|
|
18
|
+
if (operation.kind !== "read")
|
|
19
|
+
return [];
|
|
20
|
+
const symbol = symbolOf(operation);
|
|
21
|
+
return symbol ? [symbol] : [];
|
|
22
|
+
})));
|
|
23
|
+
directDefs.set(node, new Set(operations.flatMap((operation) => {
|
|
24
|
+
if (operation.kind !== "write" && operation.kind !== "declare")
|
|
25
|
+
return [];
|
|
26
|
+
const symbol = symbolOf(operation);
|
|
27
|
+
if (!symbol)
|
|
28
|
+
return [];
|
|
29
|
+
// Parameter declaration operations are attached to the function
|
|
30
|
+
// syntax node in its parent unit. Their simultaneous binding is
|
|
31
|
+
// represented explicitly by the interference builder instead.
|
|
32
|
+
return operation.kind === "declare" && symbol.kind === "param"
|
|
33
|
+
? []
|
|
34
|
+
: [symbol];
|
|
35
|
+
})));
|
|
36
|
+
});
|
|
37
|
+
controlFlow.units.forEach((unit) => {
|
|
38
|
+
const unitNodes = controlFlow.nodes.filter((node) => node.unit === unit);
|
|
39
|
+
unitNodes.forEach((node) => {
|
|
40
|
+
liveIn.set(node, new Set());
|
|
41
|
+
liveOut.set(node, new Set());
|
|
42
|
+
});
|
|
43
|
+
let changed = true;
|
|
44
|
+
while (changed) {
|
|
45
|
+
changed = false;
|
|
46
|
+
// CFG nodes are built in reverse lexical order, which is a useful and
|
|
47
|
+
// deterministic work-list order for this backward fixed point.
|
|
48
|
+
unitNodes.forEach((node) => {
|
|
49
|
+
const nextOut = new Set();
|
|
50
|
+
node.successors.forEach((edge) => liveIn.get(edge.to)?.forEach((symbol) => nextOut.add(symbol)));
|
|
51
|
+
const nextIn = new Set(directUses.get(node) ?? []);
|
|
52
|
+
nextOut.forEach((symbol) => {
|
|
53
|
+
if (!(directDefs.get(node) ?? new Set()).has(symbol))
|
|
54
|
+
nextIn.add(symbol);
|
|
55
|
+
});
|
|
56
|
+
if (!setsEqual(liveOut.get(node), nextOut)) {
|
|
57
|
+
liveOut.set(node, nextOut);
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (!setsEqual(liveIn.get(node), nextIn)) {
|
|
61
|
+
liveIn.set(node, nextIn);
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
controlFlow,
|
|
69
|
+
uses: (node) => directUses.get(node) ?? new Set(),
|
|
70
|
+
defs: (node) => directDefs.get(node) ?? new Set(),
|
|
71
|
+
liveIn: (node) => liveIn.get(node) ?? new Set(),
|
|
72
|
+
liveOut: (node) => liveOut.get(node) ?? new Set(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function symbolOf(operation) {
|
|
76
|
+
if (!("location" in operation))
|
|
77
|
+
return undefined;
|
|
78
|
+
const location = operation.location;
|
|
79
|
+
return location.kind === "local" ||
|
|
80
|
+
location.kind === "parameter" ||
|
|
81
|
+
location.kind === "upvalue"
|
|
82
|
+
? location.symbol
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
85
|
+
function setsEqual(left, right) {
|
|
86
|
+
if (!left || left.size !== right.size)
|
|
87
|
+
return false;
|
|
88
|
+
for (const value of left)
|
|
89
|
+
if (!right.has(value))
|
|
90
|
+
return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|