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,672 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeWholeProgramFields = analyzeWholeProgramFields;
|
|
4
|
+
exports.applyWholeProgramFieldRewrites = applyWholeProgramFieldRewrites;
|
|
5
|
+
const astWalk_1 = require("./astWalk");
|
|
6
|
+
const callGraph_1 = require("./callGraph");
|
|
7
|
+
const luaString_1 = require("./luaString");
|
|
8
|
+
function analyzeWholeProgramFields(objects, options = {}) {
|
|
9
|
+
const facts = new Map();
|
|
10
|
+
const annotationFacts = [];
|
|
11
|
+
const constructorWrites = new WeakSet();
|
|
12
|
+
const constructorsByTarget = new Map();
|
|
13
|
+
objects.resolvedConstructors.forEach((constructor) => {
|
|
14
|
+
const calls = constructorsByTarget.get(constructor.target) ?? [];
|
|
15
|
+
calls.push(constructor);
|
|
16
|
+
constructorsByTarget.set(constructor.target, calls);
|
|
17
|
+
});
|
|
18
|
+
const factFor = (object, field) => {
|
|
19
|
+
const key = `${object.id}\0${field}`;
|
|
20
|
+
const existing = facts.get(key);
|
|
21
|
+
if (existing)
|
|
22
|
+
return existing;
|
|
23
|
+
const fact = {
|
|
24
|
+
object,
|
|
25
|
+
field,
|
|
26
|
+
evidence: [],
|
|
27
|
+
invalidationReasons: new Set(),
|
|
28
|
+
};
|
|
29
|
+
facts.set(key, fact);
|
|
30
|
+
return fact;
|
|
31
|
+
};
|
|
32
|
+
objects.modules.forEach((module) => {
|
|
33
|
+
const metadata = options.metadataOf?.(module.name);
|
|
34
|
+
if (!metadata)
|
|
35
|
+
return;
|
|
36
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
37
|
+
onStatement: (statement) => {
|
|
38
|
+
const directives = metadata.emmyLuaOf(statement);
|
|
39
|
+
if (directives.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
if (statement.type !== "LocalStatement") {
|
|
42
|
+
directives.forEach((directive) => annotationFacts.push({
|
|
43
|
+
moduleName: module.name,
|
|
44
|
+
directive,
|
|
45
|
+
authorized: options.trustAnnotations === true,
|
|
46
|
+
}));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
statement.variables.forEach((identifier, index) => {
|
|
50
|
+
const symbol = module.resolved.symbolOf(identifier);
|
|
51
|
+
const expression = statement.init.at(index);
|
|
52
|
+
const object = expression ? objects.objectOf(expression) : undefined;
|
|
53
|
+
directives.forEach((directive) => {
|
|
54
|
+
const field = directive.kind === "field" ? directive.name : undefined;
|
|
55
|
+
annotationFacts.push({
|
|
56
|
+
moduleName: module.name,
|
|
57
|
+
directive,
|
|
58
|
+
...(symbol ? { symbol } : {}),
|
|
59
|
+
...(object ? { object } : {}),
|
|
60
|
+
...(field ? { field } : {}),
|
|
61
|
+
authorized: options.trustAnnotations === true,
|
|
62
|
+
});
|
|
63
|
+
if (object &&
|
|
64
|
+
directive.kind === "field" &&
|
|
65
|
+
options.trustAnnotations === true) {
|
|
66
|
+
const value = singletonAnnotationValue(directive.valueType);
|
|
67
|
+
if (!value)
|
|
68
|
+
return;
|
|
69
|
+
const fact = factFor(object, directive.name);
|
|
70
|
+
fact.value = value;
|
|
71
|
+
fact.evidence.push({
|
|
72
|
+
kind: "annotation",
|
|
73
|
+
moduleName: module.name,
|
|
74
|
+
...rangeOf(directive.comment),
|
|
75
|
+
assumption: "annotations",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
constructorsByTarget.forEach((calls, target) => {
|
|
84
|
+
const module = objects.modules.find((candidate) => candidate.analysis.callGraph.functions.includes(target));
|
|
85
|
+
if (!module)
|
|
86
|
+
return;
|
|
87
|
+
const writes = [];
|
|
88
|
+
target.declaration.body.forEach((node) => {
|
|
89
|
+
if (node.type !== "AssignmentStatement")
|
|
90
|
+
return;
|
|
91
|
+
node.variables.forEach((variable, index) => {
|
|
92
|
+
const value = node.init.at(index);
|
|
93
|
+
if (value &&
|
|
94
|
+
variable.type === "MemberExpression" &&
|
|
95
|
+
variable.base.type === "Identifier" &&
|
|
96
|
+
module.resolved.symbolOf(variable.base) === calls[0].returnedSymbol) {
|
|
97
|
+
writes.push({
|
|
98
|
+
statement: node,
|
|
99
|
+
field: variable.identifier.name,
|
|
100
|
+
value,
|
|
101
|
+
});
|
|
102
|
+
constructorWrites.add(node);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
const countByField = new Map();
|
|
107
|
+
writes.forEach((write) => countByField.set(write.field, (countByField.get(write.field) ?? 0) + 1));
|
|
108
|
+
calls.forEach((constructor) => {
|
|
109
|
+
writes.forEach((write) => {
|
|
110
|
+
const fact = factFor(constructor.object, write.field);
|
|
111
|
+
fact.initializer = write.statement;
|
|
112
|
+
if ((countByField.get(write.field) ?? 0) > 1) {
|
|
113
|
+
fact.invalidationReasons.add("field-reassignment");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const value = valueAtCall(write.value, target, constructor, module, objects.modules.find((candidate) => candidate.name === constructor.moduleName) ?? module, options);
|
|
117
|
+
if (!value)
|
|
118
|
+
return;
|
|
119
|
+
fact.value = value.value;
|
|
120
|
+
fact.evidence.push(value.evidence);
|
|
121
|
+
if (value.annotationEvidence)
|
|
122
|
+
fact.evidence.push(value.annotationEvidence);
|
|
123
|
+
if (value.contradiction)
|
|
124
|
+
fact.invalidationReasons.add("contradictory-annotation");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// A derived factory allocation receives the base instance fields through the
|
|
129
|
+
// same provenance edge #83 uses for inherited methods.
|
|
130
|
+
let inherited = true;
|
|
131
|
+
while (inherited) {
|
|
132
|
+
inherited = false;
|
|
133
|
+
objects.objects.forEach((object) => {
|
|
134
|
+
object.sources.forEach((source) => {
|
|
135
|
+
[...facts.values()]
|
|
136
|
+
.filter((fact) => fact.object === source)
|
|
137
|
+
.forEach((sourceFact) => {
|
|
138
|
+
const key = `${object.id}\0${sourceFact.field}`;
|
|
139
|
+
if (facts.has(key))
|
|
140
|
+
return;
|
|
141
|
+
facts.set(key, {
|
|
142
|
+
object,
|
|
143
|
+
field: sourceFact.field,
|
|
144
|
+
value: sourceFact.value,
|
|
145
|
+
evidence: [...sourceFact.evidence],
|
|
146
|
+
invalidationReasons: new Set(sourceFact.invalidationReasons),
|
|
147
|
+
initializer: sourceFact.initializer,
|
|
148
|
+
});
|
|
149
|
+
inherited = true;
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// #83 already proves the object boundary. Project its reasons onto only the
|
|
155
|
+
// affected allocation facts instead of inventing a second escape analysis.
|
|
156
|
+
facts.forEach((fact) => {
|
|
157
|
+
fact.object.invalidationReasons.forEach((reason) => {
|
|
158
|
+
if (reason === "dynamic-key")
|
|
159
|
+
fact.invalidationReasons.add("dynamic-key");
|
|
160
|
+
else if (reason === "metatable-mutation")
|
|
161
|
+
fact.invalidationReasons.add("metatable-mutation");
|
|
162
|
+
else if (reason === "instance-escape" || reason === "prototype-escape")
|
|
163
|
+
fact.invalidationReasons.add("alias-escape");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
objects.modules.forEach((module) => {
|
|
167
|
+
module.analysis.callGraph.calls.forEach((call) => {
|
|
168
|
+
if (call.call.type !== "CallExpression")
|
|
169
|
+
return;
|
|
170
|
+
const callExpression = call.call;
|
|
171
|
+
const invalidateArgument = (argument, reason) => {
|
|
172
|
+
if (!argument)
|
|
173
|
+
return;
|
|
174
|
+
const object = objects.objectOf(argument);
|
|
175
|
+
if (!object)
|
|
176
|
+
return;
|
|
177
|
+
facts.forEach((fact) => {
|
|
178
|
+
if (fact.object === object)
|
|
179
|
+
fact.invalidationReasons.add(reason);
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
if (call.hasUnknownTarget) {
|
|
183
|
+
callExpression.arguments.forEach((argument) => {
|
|
184
|
+
invalidateArgument(argument, "unknown-call");
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
call.targets.forEach((target) => {
|
|
189
|
+
const targetModule = objects.modules.find((candidate) => candidate.analysis.callGraph.functions.includes(target));
|
|
190
|
+
if (!targetModule)
|
|
191
|
+
return;
|
|
192
|
+
targetModule.analysis.interprocedural
|
|
193
|
+
.summaryOf(target)
|
|
194
|
+
.escapes.forEach((escape) => {
|
|
195
|
+
const argumentIndex = callExpression.base.type === "MemberExpression" &&
|
|
196
|
+
callExpression.base.indexer === ":"
|
|
197
|
+
? escape.parameterIndex - 1
|
|
198
|
+
: escape.parameterIndex;
|
|
199
|
+
invalidateArgument(callExpression.arguments[argumentIndex], "alias-escape");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
204
|
+
onStatement: (statement) => {
|
|
205
|
+
if (statement.type === "AssignmentStatement") {
|
|
206
|
+
statement.init.forEach((value, index) => {
|
|
207
|
+
const object = objects.objectOf(value);
|
|
208
|
+
const target = statement.variables[index];
|
|
209
|
+
if (!object ||
|
|
210
|
+
(target.type === "Identifier" &&
|
|
211
|
+
!module.resolved.isGlobalReference(target)))
|
|
212
|
+
return;
|
|
213
|
+
facts.forEach((fact) => {
|
|
214
|
+
if (fact.object === object)
|
|
215
|
+
fact.invalidationReasons.add("alias-escape");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
objects.resolvedMethods.forEach((method) => {
|
|
223
|
+
const summary = objects.summaryOfMethodCall(method.call);
|
|
224
|
+
summary?.effects
|
|
225
|
+
.filter((effect) => effect.parameterIndex === 0 && effect.access === "write")
|
|
226
|
+
.forEach((effect) => {
|
|
227
|
+
facts.forEach((fact) => {
|
|
228
|
+
if (fact.object !== method.object)
|
|
229
|
+
return;
|
|
230
|
+
if (effect.staticKey === undefined ||
|
|
231
|
+
effect.staticKey ===
|
|
232
|
+
(0, luaString_1.luaByteStringKey)((0, luaString_1.luaByteStringOfText)(fact.field)))
|
|
233
|
+
fact.invalidationReasons.add("field-reassignment");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// Direct writes after construction invalidate the precise field only.
|
|
238
|
+
objects.modules.forEach((module) => {
|
|
239
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
240
|
+
onStatement: (node) => {
|
|
241
|
+
if (node.type !== "AssignmentStatement" || constructorWrites.has(node))
|
|
242
|
+
return;
|
|
243
|
+
node.variables.forEach((variable) => {
|
|
244
|
+
if (variable.type === "IndexExpression") {
|
|
245
|
+
const object = objects.objectOf(variable.base);
|
|
246
|
+
if (object)
|
|
247
|
+
facts.forEach((fact) => {
|
|
248
|
+
if (fact.object === object)
|
|
249
|
+
fact.invalidationReasons.add("dynamic-key");
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else if (variable.type === "MemberExpression") {
|
|
253
|
+
const object = objects.objectOf(variable.base);
|
|
254
|
+
const fact = object
|
|
255
|
+
? facts.get(`${object.id}\0${variable.identifier.name}`)
|
|
256
|
+
: undefined;
|
|
257
|
+
fact?.invalidationReasons.add("field-reassignment");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
const methodObjects = new Map();
|
|
264
|
+
objects.resolvedMethods.forEach((method) => {
|
|
265
|
+
const receivers = methodObjects.get(method.target) ?? new Set();
|
|
266
|
+
receivers.add(method.object);
|
|
267
|
+
methodObjects.set(method.target, receivers);
|
|
268
|
+
});
|
|
269
|
+
const readFacts = new WeakMap();
|
|
270
|
+
objects.modules.forEach((module) => {
|
|
271
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
272
|
+
onExpression: (expression) => {
|
|
273
|
+
if (expression.type !== "MemberExpression" ||
|
|
274
|
+
expression.indexer !== ".")
|
|
275
|
+
return;
|
|
276
|
+
const directObject = objects.objectOf(expression.base);
|
|
277
|
+
if (directObject) {
|
|
278
|
+
const fact = facts.get(`${directObject.id}\0${expression.identifier.name}`);
|
|
279
|
+
if (fact && usable(fact))
|
|
280
|
+
readFacts.set(expression, fact);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (expression.base.type !== "Identifier")
|
|
284
|
+
return;
|
|
285
|
+
const symbol = module.resolved.symbolOf(expression.base);
|
|
286
|
+
const callable = module.analysis.callGraph.functions.find((candidate) => candidate.parameters[0] === symbol);
|
|
287
|
+
if (!callable)
|
|
288
|
+
return;
|
|
289
|
+
const receiverFacts = [...(methodObjects.get(callable) ?? [])]
|
|
290
|
+
.map((object) => facts.get(`${object.id}\0${expression.identifier.name}`))
|
|
291
|
+
.filter((fact) => fact !== undefined && usable(fact));
|
|
292
|
+
if (receiverFacts.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
const first = receiverFacts[0];
|
|
295
|
+
if (receiverFacts.length === (methodObjects.get(callable)?.size ?? 0) &&
|
|
296
|
+
receiverFacts.every((fact) => sameValue(fact.value, first.value)))
|
|
297
|
+
readFacts.set(expression, first);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
// A resolved method edge already supplies the exact receiver allocation.
|
|
302
|
+
// Index its parameter-zero field reads directly so callback calls do not
|
|
303
|
+
// depend on recovering the owning callable from a module-local scan.
|
|
304
|
+
objects.resolvedMethods.forEach((method) => {
|
|
305
|
+
const module = objects.modules.find((candidate) => candidate.analysis.callGraph.functions.includes(method.target));
|
|
306
|
+
const receiver = method.target.parameters[0];
|
|
307
|
+
if (!module)
|
|
308
|
+
return;
|
|
309
|
+
method.target.declaration.body.forEach((statement) => {
|
|
310
|
+
(0, astWalk_1.walkBlockDeep)([statement], {
|
|
311
|
+
onExpression: (expression) => {
|
|
312
|
+
if (expression.type !== "MemberExpression" ||
|
|
313
|
+
expression.indexer !== "." ||
|
|
314
|
+
expression.base.type !== "Identifier" ||
|
|
315
|
+
module.resolved.symbolOf(expression.base) !== receiver)
|
|
316
|
+
return;
|
|
317
|
+
const fact = facts.get(`${method.object.id}\0${expression.identifier.name}`);
|
|
318
|
+
if (fact && usable(fact))
|
|
319
|
+
readFacts.set(expression, fact);
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const diagnostics = [];
|
|
325
|
+
facts.forEach((fact) => {
|
|
326
|
+
if (usable(fact))
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
moduleName: fact.object.moduleName,
|
|
329
|
+
reason: "field-fact",
|
|
330
|
+
...rangeOf(fact.initializer),
|
|
331
|
+
});
|
|
332
|
+
fact.invalidationReasons.forEach((reason) => diagnostics.push({
|
|
333
|
+
moduleName: fact.object.moduleName,
|
|
334
|
+
reason,
|
|
335
|
+
...rangeOf(fact.initializer),
|
|
336
|
+
}));
|
|
337
|
+
});
|
|
338
|
+
const publicFacts = [...facts.values()];
|
|
339
|
+
const resolvedCallbacks = [];
|
|
340
|
+
const callbackTargets = new Map();
|
|
341
|
+
objects.callGraph.calls.forEach((call) => {
|
|
342
|
+
if (call.call.type !== "CallExpression" ||
|
|
343
|
+
call.call.base.type !== "MemberExpression" ||
|
|
344
|
+
call.call.base.indexer !== ".")
|
|
345
|
+
return;
|
|
346
|
+
const field = readFacts.get(call.call.base);
|
|
347
|
+
if (!field || field.value?.kind !== "function")
|
|
348
|
+
return;
|
|
349
|
+
callbackTargets.set(call, field.value.callable);
|
|
350
|
+
resolvedCallbacks.push({
|
|
351
|
+
call,
|
|
352
|
+
field,
|
|
353
|
+
target: field.value.callable,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
const callGraph = (0, callGraph_1.combineCallGraphs)([objects.callGraph], callbackTargets, objects.generation);
|
|
357
|
+
return {
|
|
358
|
+
generation: objects.generation,
|
|
359
|
+
callGraph,
|
|
360
|
+
resolvedCallbacks,
|
|
361
|
+
facts: publicFacts,
|
|
362
|
+
diagnostics,
|
|
363
|
+
annotationFacts,
|
|
364
|
+
factOf: (object, field) => facts.get(`${object.id}\0${field}`),
|
|
365
|
+
factOfRead: (read) => readFacts.get(read),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function applyWholeProgramFieldRewrites(objects, analysis, metadataOf, options = {}) {
|
|
369
|
+
const assignmentTargets = new WeakSet();
|
|
370
|
+
if (options.replaceReads !== false)
|
|
371
|
+
objects.modules.forEach((module) => {
|
|
372
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
373
|
+
onStatement: (statement) => {
|
|
374
|
+
if (statement.type !== "AssignmentStatement")
|
|
375
|
+
return;
|
|
376
|
+
statement.variables.forEach((variable) => {
|
|
377
|
+
if (variable.type === "MemberExpression")
|
|
378
|
+
assignmentTargets.add(variable);
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
const readCount = new Map();
|
|
384
|
+
let replacedReads = 0;
|
|
385
|
+
if (options.removeInitializers !== false)
|
|
386
|
+
objects.modules.forEach((module) => {
|
|
387
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
388
|
+
onExpression: (expression) => {
|
|
389
|
+
if (expression.type !== "MemberExpression" ||
|
|
390
|
+
assignmentTargets.has(expression))
|
|
391
|
+
return;
|
|
392
|
+
const fact = analysis.factOfRead(expression);
|
|
393
|
+
if (!fact?.value)
|
|
394
|
+
return;
|
|
395
|
+
const replacement = literalFor(fact.value, expression);
|
|
396
|
+
if (!replacement) {
|
|
397
|
+
if (fact.initializer)
|
|
398
|
+
readCount.set(fact.initializer, (readCount.get(fact.initializer) ?? 0) + 1);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
replaceExpression(expression, replacement);
|
|
402
|
+
replacedReads++;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
const factsByInitializer = new Map();
|
|
407
|
+
analysis.facts.forEach((fact) => {
|
|
408
|
+
if (!fact.initializer)
|
|
409
|
+
return;
|
|
410
|
+
const related = factsByInitializer.get(fact.initializer) ?? [];
|
|
411
|
+
related.push(fact);
|
|
412
|
+
factsByInitializer.set(fact.initializer, related);
|
|
413
|
+
});
|
|
414
|
+
let removedInitializers = 0;
|
|
415
|
+
let preservedEffects = 0;
|
|
416
|
+
objects.modules.forEach((module) => {
|
|
417
|
+
const metadata = metadataOf(module.name);
|
|
418
|
+
rewriteBlocks(module.chunk.body, (body, index, statement) => {
|
|
419
|
+
if (statement.type !== "AssignmentStatement")
|
|
420
|
+
return;
|
|
421
|
+
const related = factsByInitializer.get(statement);
|
|
422
|
+
if (!related ||
|
|
423
|
+
related.length === 0 ||
|
|
424
|
+
related.some((fact) => fact.invalidationReasons.size > 0 ||
|
|
425
|
+
(fact.initializer && (readCount.get(fact.initializer) ?? 0) > 0)) ||
|
|
426
|
+
metadata.annotationsOf(statement).keep)
|
|
427
|
+
return;
|
|
428
|
+
if (statement.variables.length !== 1 || statement.init.length !== 1)
|
|
429
|
+
return;
|
|
430
|
+
const value = statement.init[0];
|
|
431
|
+
const discardable = module.analysis.facts.discardabilityOf(value).discardable ||
|
|
432
|
+
(value.type === "Identifier" &&
|
|
433
|
+
module.resolved.symbolOf(value) !== undefined);
|
|
434
|
+
if (discardable) {
|
|
435
|
+
metadata.removeStatement(statement, body[index + 1]);
|
|
436
|
+
body.splice(index, 1);
|
|
437
|
+
}
|
|
438
|
+
else if (value.type === "CallExpression" ||
|
|
439
|
+
value.type === "TableCallExpression" ||
|
|
440
|
+
value.type === "StringCallExpression") {
|
|
441
|
+
const statementRange = statement
|
|
442
|
+
.range;
|
|
443
|
+
const replacement = {
|
|
444
|
+
type: "CallStatement",
|
|
445
|
+
expression: value,
|
|
446
|
+
...(statement.loc ? { loc: statement.loc } : {}),
|
|
447
|
+
...(statementRange ? { range: statementRange } : {}),
|
|
448
|
+
};
|
|
449
|
+
metadata.replaceStatement(statement, [replacement]);
|
|
450
|
+
body[index] = replacement;
|
|
451
|
+
preservedEffects++;
|
|
452
|
+
}
|
|
453
|
+
else
|
|
454
|
+
return;
|
|
455
|
+
removedInitializers++;
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
changed: replacedReads > 0 || removedInitializers > 0,
|
|
460
|
+
replacedReads,
|
|
461
|
+
removedInitializers,
|
|
462
|
+
preservedEffects,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function valueAtCall(expression, target, constructor, module, callerModule, options) {
|
|
466
|
+
let source = expression;
|
|
467
|
+
let valueModule = module;
|
|
468
|
+
if (expression.type === "Identifier") {
|
|
469
|
+
const symbol = module.resolved.symbolOf(expression);
|
|
470
|
+
const index = symbol ? target.parameters.indexOf(symbol) : -1;
|
|
471
|
+
const metadata = options.metadataOf?.(module.name);
|
|
472
|
+
const annotation = metadata
|
|
473
|
+
?.emmyLuaOf(target.declaration)
|
|
474
|
+
.find((directive) => directive.kind === "param" &&
|
|
475
|
+
index >= 0 &&
|
|
476
|
+
directive.name === target.parameters[index].name);
|
|
477
|
+
const annotationValue = options.trustAnnotations && annotation?.kind === "param"
|
|
478
|
+
? singletonAnnotationValue(annotation.valueType)
|
|
479
|
+
: undefined;
|
|
480
|
+
const annotationEvidence = annotationValue && annotation
|
|
481
|
+
? {
|
|
482
|
+
kind: "annotation",
|
|
483
|
+
moduleName: module.name,
|
|
484
|
+
...rangeOf(annotation.comment),
|
|
485
|
+
assumption: "annotations",
|
|
486
|
+
}
|
|
487
|
+
: undefined;
|
|
488
|
+
const argumentIndex = constructor.call.call.type === "CallExpression" &&
|
|
489
|
+
constructor.call.call.base.type === "MemberExpression" &&
|
|
490
|
+
constructor.call.call.base.indexer === ":"
|
|
491
|
+
? index - 1
|
|
492
|
+
: index;
|
|
493
|
+
if (argumentIndex >= 0 && constructor.arguments[argumentIndex]) {
|
|
494
|
+
source = constructor.arguments[argumentIndex];
|
|
495
|
+
valueModule = callerModule;
|
|
496
|
+
const actualValue = syntaxValue(source, valueModule);
|
|
497
|
+
if (actualValue &&
|
|
498
|
+
annotationValue &&
|
|
499
|
+
!sameValue(actualValue, annotationValue))
|
|
500
|
+
return {
|
|
501
|
+
value: actualValue,
|
|
502
|
+
evidence: {
|
|
503
|
+
kind: "code",
|
|
504
|
+
moduleName: constructor.moduleName,
|
|
505
|
+
...rangeOf(source),
|
|
506
|
+
},
|
|
507
|
+
annotationEvidence,
|
|
508
|
+
contradiction: true,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
else if (index >= 0 && annotationValue && annotationEvidence) {
|
|
512
|
+
return { value: annotationValue, evidence: annotationEvidence };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const value = syntaxValue(source, valueModule);
|
|
516
|
+
return value
|
|
517
|
+
? {
|
|
518
|
+
value,
|
|
519
|
+
evidence: {
|
|
520
|
+
kind: "code",
|
|
521
|
+
moduleName: source === expression ? module.name : constructor.moduleName,
|
|
522
|
+
...rangeOf(source),
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
: undefined;
|
|
526
|
+
}
|
|
527
|
+
function syntaxValue(expression, module) {
|
|
528
|
+
switch (expression.type) {
|
|
529
|
+
case "NilLiteral":
|
|
530
|
+
return { kind: "nil" };
|
|
531
|
+
case "BooleanLiteral":
|
|
532
|
+
return { kind: "boolean", value: expression.value };
|
|
533
|
+
case "NumericLiteral":
|
|
534
|
+
return { kind: "number", raw: expression.raw };
|
|
535
|
+
case "StringLiteral":
|
|
536
|
+
return { kind: "string", raw: expression.raw };
|
|
537
|
+
case "TableConstructorExpression":
|
|
538
|
+
return expression.fields.length === 0
|
|
539
|
+
? { kind: "empty-table", origin: expression }
|
|
540
|
+
: undefined;
|
|
541
|
+
case "FunctionDeclaration": {
|
|
542
|
+
const callable = module.analysis.callGraph.functionOf(expression);
|
|
543
|
+
return callable ? { kind: "function", callable } : undefined;
|
|
544
|
+
}
|
|
545
|
+
case "Identifier": {
|
|
546
|
+
const symbol = module.resolved.symbolOf(expression);
|
|
547
|
+
const callable = symbol
|
|
548
|
+
? module.analysis.callGraph.functionOfSymbol(symbol)
|
|
549
|
+
: undefined;
|
|
550
|
+
if (callable)
|
|
551
|
+
return { kind: "function", callable };
|
|
552
|
+
const value = module.analysis.facts.expressionFact(expression)?.value;
|
|
553
|
+
if (value?.kind === "allocation" &&
|
|
554
|
+
value.allocationKind === "table" &&
|
|
555
|
+
value.origin.type === "TableConstructorExpression" &&
|
|
556
|
+
value.origin.fields.length === 0)
|
|
557
|
+
return { kind: "empty-table", origin: value.origin };
|
|
558
|
+
if (!symbol)
|
|
559
|
+
return undefined;
|
|
560
|
+
let initializer;
|
|
561
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
562
|
+
onStatement: (statement) => {
|
|
563
|
+
if (statement.type !== "LocalStatement")
|
|
564
|
+
return;
|
|
565
|
+
statement.variables.forEach((variable, index) => {
|
|
566
|
+
if (variable === symbol.declaration)
|
|
567
|
+
initializer = statement.init[index];
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
return initializer?.type === "TableConstructorExpression" &&
|
|
572
|
+
initializer.fields.length === 0
|
|
573
|
+
? { kind: "empty-table", origin: initializer }
|
|
574
|
+
: undefined;
|
|
575
|
+
}
|
|
576
|
+
default:
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function singletonAnnotationValue(valueType) {
|
|
581
|
+
const value = valueType.trim();
|
|
582
|
+
if (value === "true" || value === "false")
|
|
583
|
+
return { kind: "boolean", value: value === "true" };
|
|
584
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value))
|
|
585
|
+
return { kind: "number", raw: value };
|
|
586
|
+
if (/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(value))
|
|
587
|
+
return { kind: "string", raw: value };
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
function literalFor(value, origin) {
|
|
591
|
+
const originRange = origin.range;
|
|
592
|
+
const source = {
|
|
593
|
+
...(origin.loc ? { loc: origin.loc } : {}),
|
|
594
|
+
...(originRange ? { range: originRange } : {}),
|
|
595
|
+
};
|
|
596
|
+
switch (value.kind) {
|
|
597
|
+
case "nil":
|
|
598
|
+
return { type: "NilLiteral", value: null, raw: "nil", ...source };
|
|
599
|
+
case "boolean":
|
|
600
|
+
return {
|
|
601
|
+
type: "BooleanLiteral",
|
|
602
|
+
value: value.value,
|
|
603
|
+
raw: value.value ? "true" : "false",
|
|
604
|
+
...source,
|
|
605
|
+
};
|
|
606
|
+
case "number":
|
|
607
|
+
return {
|
|
608
|
+
type: "NumericLiteral",
|
|
609
|
+
value: Number(value.raw),
|
|
610
|
+
raw: value.raw,
|
|
611
|
+
...source,
|
|
612
|
+
};
|
|
613
|
+
case "string":
|
|
614
|
+
return {
|
|
615
|
+
type: "StringLiteral",
|
|
616
|
+
value: value.raw.slice(1, -1),
|
|
617
|
+
raw: value.raw,
|
|
618
|
+
...source,
|
|
619
|
+
};
|
|
620
|
+
case "function":
|
|
621
|
+
case "empty-table":
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function replaceExpression(target, replacement) {
|
|
626
|
+
// Every AST consumer dispatches on `type`; source location and superseded
|
|
627
|
+
// member fields are inert provenance once the discriminant is replaced.
|
|
628
|
+
Object.assign(target, replacement);
|
|
629
|
+
}
|
|
630
|
+
function rewriteBlocks(body, visit) {
|
|
631
|
+
for (let index = body.length - 1; index >= 0; index--) {
|
|
632
|
+
const statement = body[index];
|
|
633
|
+
switch (statement.type) {
|
|
634
|
+
case "DoStatement":
|
|
635
|
+
case "WhileStatement":
|
|
636
|
+
case "RepeatStatement":
|
|
637
|
+
case "FunctionDeclaration":
|
|
638
|
+
case "ForNumericStatement":
|
|
639
|
+
case "ForGenericStatement":
|
|
640
|
+
rewriteBlocks(statement.body, visit);
|
|
641
|
+
break;
|
|
642
|
+
case "IfStatement":
|
|
643
|
+
statement.clauses.forEach((clause) => {
|
|
644
|
+
rewriteBlocks(clause.body, visit);
|
|
645
|
+
});
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
visit(body, index, statement);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function usable(fact) {
|
|
652
|
+
return fact.value !== undefined && fact.invalidationReasons.size === 0;
|
|
653
|
+
}
|
|
654
|
+
function sameValue(left, right) {
|
|
655
|
+
if (!left || !right || left.kind !== right.kind)
|
|
656
|
+
return false;
|
|
657
|
+
if (left.kind === "boolean" && right.kind === "boolean")
|
|
658
|
+
return left.value === right.value;
|
|
659
|
+
if (left.kind === "number" && right.kind === "number")
|
|
660
|
+
return left.raw === right.raw;
|
|
661
|
+
if (left.kind === "string" && right.kind === "string")
|
|
662
|
+
return left.raw === right.raw;
|
|
663
|
+
if (left.kind === "function" && right.kind === "function")
|
|
664
|
+
return left.callable === right.callable;
|
|
665
|
+
if (left.kind === "empty-table" && right.kind === "empty-table")
|
|
666
|
+
return left.origin === right.origin;
|
|
667
|
+
return left.kind === "nil" && right.kind === "nil";
|
|
668
|
+
}
|
|
669
|
+
function rangeOf(node) {
|
|
670
|
+
const range = node ? node.range : undefined;
|
|
671
|
+
return range ? { sourceRange: range } : {};
|
|
672
|
+
}
|