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.
Files changed (44) hide show
  1. package/README.md +119 -41
  2. package/dist/aggregateSpecialization.js +406 -0
  3. package/dist/ast2lua.js +156 -68
  4. package/dist/astWalk.js +162 -0
  5. package/dist/callGraph.js +372 -0
  6. package/dist/cli.js +53 -58
  7. package/dist/cliOptions.js +36 -0
  8. package/dist/cliProgress.js +87 -0
  9. package/dist/config.js +73 -0
  10. package/dist/constantFold.js +798 -0
  11. package/dist/controlFlow.js +266 -0
  12. package/dist/functionRewrites.js +580 -0
  13. package/dist/generatedAst.js +108 -0
  14. package/dist/generatedNode.js +23 -0
  15. package/dist/globalRename.js +17 -4
  16. package/dist/interproceduralAnalysis.js +842 -0
  17. package/dist/interproceduralConstants.js +120 -0
  18. package/dist/luaString.js +157 -0
  19. package/dist/minifier.js +1219 -49
  20. package/dist/optimizerAnalysis.js +43 -0
  21. package/dist/optimizerDiagnostics.js +65 -0
  22. package/dist/optimizerFacts.js +529 -0
  23. package/dist/optimizerPass.js +96 -0
  24. package/dist/optimizerTransaction.js +56 -0
  25. package/dist/optimizerValueDomain.js +200 -0
  26. package/dist/options.js +233 -0
  27. package/dist/progress.js +2 -0
  28. package/dist/removeUnused.js +145 -0
  29. package/dist/renamer.js +280 -54
  30. package/dist/resolver.js +35 -11
  31. package/dist/runtimeEnvironment.js +105 -0
  32. package/dist/sourceMetadata.js +314 -0
  33. package/dist/statementDataflow.js +259 -0
  34. package/dist/statementScheduler.js +598 -0
  35. package/dist/symbolLiveness.js +92 -0
  36. package/dist/tableEffects.js +356 -0
  37. package/dist/transform.js +10 -371
  38. package/dist/valueFlow.js +409 -0
  39. package/dist/wholeProgramExports.js +646 -0
  40. package/dist/wholeProgramFieldRenames.js +583 -0
  41. package/dist/wholeProgramFields.js +672 -0
  42. package/dist/wholeProgramObjects.js +783 -0
  43. package/package.json +11 -2
  44. package/dist/index.js +0 -27
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeValueFlow = analyzeValueFlow;
4
+ const controlFlow_1 = require("./controlFlow");
5
+ const UNKNOWN_ENTRY = {
6
+ kind: "unknown",
7
+ reason: "function-entry",
8
+ };
9
+ /** CFG fixed-point value and reaching-definition analysis for intraprocedural optimizer facts. */
10
+ function analyzeValueFlow(chunk, resolved, facts, version = facts.generation, interprocedural) {
11
+ if (facts.generation !== version) {
12
+ throw new Error("Value flow requires facts from the same AST generation");
13
+ }
14
+ const controlFlow = (0, controlFlow_1.analyzeControlFlow)(chunk, resolved, version);
15
+ const allocations = [];
16
+ const definitions = [];
17
+ const allocationByOrigin = new WeakMap();
18
+ const callAllocations = new WeakMap();
19
+ const writesByNode = new Map();
20
+ const beforeByNode = new Map();
21
+ const afterByNode = new Map();
22
+ const reachingBeforeByNode = new Map();
23
+ const reachingAfterByNode = new Map();
24
+ const reachableFrom = computeReachability(controlFlow);
25
+ facts.operations.forEach((operation) => {
26
+ if (operation.kind !== "allocate" || operation.allocationKind !== "table")
27
+ return;
28
+ if (operation.origin.type !== "TableConstructorExpression")
29
+ return;
30
+ const point = controlFlow.pointOf(operation.owner);
31
+ if (!point || allocationByOrigin.has(operation.origin))
32
+ return;
33
+ const allocation = {
34
+ id: allocations.length,
35
+ kind: "table",
36
+ origin: operation.origin,
37
+ owner: operation.owner,
38
+ unit: point.unit,
39
+ };
40
+ allocations.push(allocation);
41
+ allocationByOrigin.set(operation.origin, allocation);
42
+ });
43
+ interprocedural?.callGraph.calls.forEach((call) => {
44
+ const point = controlFlow.pointOf(call.owner);
45
+ if (!point)
46
+ return;
47
+ const byTemplate = new Map();
48
+ interprocedural.returnsOf(call).prefix.forEach((value) => {
49
+ const templates = value.atoms.filter((atom) => atom.kind === "allocation" && atom.allocationKind === "table");
50
+ if (value.unknownReasons.length > 0 ||
51
+ templates.length === 0 ||
52
+ templates.length !== value.atoms.length)
53
+ return;
54
+ const existing = templates
55
+ .map((template) => byTemplate.get(template.id))
56
+ .find((allocation) => allocation !== undefined);
57
+ const allocation = existing ??
58
+ {
59
+ id: allocations.length,
60
+ kind: "table",
61
+ origin: call.call,
62
+ templateId: templates.map((template) => template.id).join("|"),
63
+ owner: call.owner,
64
+ unit: point.unit,
65
+ };
66
+ if (!existing)
67
+ allocations.push(allocation);
68
+ templates.forEach((template) => byTemplate.set(template.id, allocation));
69
+ });
70
+ if (byTemplate.size > 0)
71
+ callAllocations.set(call.call, byTemplate);
72
+ });
73
+ controlFlow.nodes.forEach((node) => {
74
+ const statement = node.statement;
75
+ if (!statement || node !== controlFlow.nodeOf(statement))
76
+ return;
77
+ if (statement.type !== "LocalStatement" &&
78
+ statement.type !== "AssignmentStatement")
79
+ return;
80
+ const slots = facts.valueSlotsOf(statement);
81
+ const writes = [];
82
+ statement.variables.forEach((target, index) => {
83
+ if (target.type !== "Identifier")
84
+ return;
85
+ const symbol = resolved.symbolOf(target);
86
+ if (!symbol)
87
+ return;
88
+ const definition = {
89
+ id: definitions.length,
90
+ symbol,
91
+ owner: statement,
92
+ value: UNKNOWN_ENTRY,
93
+ };
94
+ definitions.push(definition);
95
+ writes.push({ definition, slot: slots[index] });
96
+ });
97
+ if (writes.length > 0)
98
+ writesByNode.set(node, writes);
99
+ });
100
+ controlFlow.units.forEach((unit) => {
101
+ const unitNodes = controlFlow.nodes.filter((node) => node.unit === unit);
102
+ const entry = unitNodes.find((node) => node.kind === "entry");
103
+ if (!entry)
104
+ return;
105
+ beforeByNode.set(entry, new Map());
106
+ afterByNode.set(entry, new Map());
107
+ reachingBeforeByNode.set(entry, new Map());
108
+ reachingAfterByNode.set(entry, new Map());
109
+ let changed = true;
110
+ let iterations = 0;
111
+ const iterationLimit = Math.max(1, unitNodes.length * (definitions.length + 1));
112
+ while (changed) {
113
+ if (iterations++ > iterationLimit) {
114
+ throw new Error("Value-flow finite lattice failed to converge");
115
+ }
116
+ changed = false;
117
+ // CFG nodes are constructed from the continuation backwards. Reverse iteration is therefore
118
+ // source-forward and reaches a straight-line fixed point in one pass.
119
+ [...unitNodes].reverse().forEach((node) => {
120
+ if (node === entry)
121
+ return;
122
+ const predecessorValues = node.predecessors.flatMap((edge) => {
123
+ const values = afterByNode.get(edge.from);
124
+ return values ? [values] : [];
125
+ });
126
+ // An absent predecessor state is lattice bottom (not an unknown runtime value). Waiting until
127
+ // one reachable predecessor has propagated prevents a not-yet-visited loop back-edge from
128
+ // poisoning the first forward value with `function-entry`.
129
+ if (predecessorValues.length === 0)
130
+ return;
131
+ const before = joinValueMaps(predecessorValues);
132
+ const predecessorDefinitions = node.predecessors.flatMap((edge) => {
133
+ const definitions = reachingAfterByNode.get(edge.from);
134
+ return definitions ? [definitions] : [];
135
+ });
136
+ const reachingBefore = joinDefinitionMaps(predecessorDefinitions);
137
+ const after = new Map(before);
138
+ const reachingAfter = cloneDefinitionMap(reachingBefore);
139
+ (writesByNode.get(node) ?? []).forEach((write) => {
140
+ const value = abstractSlot(write.slot, before, resolved, allocationByOrigin, interprocedural, callAllocations);
141
+ write.definition.value = value;
142
+ after.set(write.definition.symbol, value);
143
+ reachingAfter.set(write.definition.symbol, new Set([write.definition]));
144
+ });
145
+ if (!valueMapsEqual(beforeByNode.get(node), before)) {
146
+ beforeByNode.set(node, before);
147
+ changed = true;
148
+ }
149
+ if (!valueMapsEqual(afterByNode.get(node), after)) {
150
+ afterByNode.set(node, after);
151
+ changed = true;
152
+ }
153
+ if (!definitionMapsEqual(reachingBeforeByNode.get(node), reachingBefore)) {
154
+ reachingBeforeByNode.set(node, reachingBefore);
155
+ changed = true;
156
+ }
157
+ if (!definitionMapsEqual(reachingAfterByNode.get(node), reachingAfter)) {
158
+ reachingAfterByNode.set(node, reachingAfter);
159
+ changed = true;
160
+ }
161
+ });
162
+ }
163
+ });
164
+ const valueAt = (store, point, symbol) => store.get(point.node)?.get(symbol) ?? UNKNOWN_ENTRY;
165
+ return {
166
+ version,
167
+ controlFlow,
168
+ allocations,
169
+ definitions,
170
+ valueBefore: (point, symbol) => valueAt(beforeByNode, point, symbol),
171
+ valueAfter: (point, symbol) => valueAt(afterByNode, point, symbol),
172
+ reachingDefinitionBefore: (point, symbol) => {
173
+ const reaching = reachingBeforeByNode.get(point.node)?.get(symbol);
174
+ return reaching?.size === 1 ? reaching.values().next().value : undefined;
175
+ },
176
+ aliasesBefore: (point, allocation) => {
177
+ const aliases = new Set();
178
+ beforeByNode.get(point.node)?.forEach((value, symbol) => {
179
+ if (value.kind === "allocations" &&
180
+ value.allocations.size === 1 &&
181
+ value.allocations.has(allocation))
182
+ aliases.add(symbol);
183
+ });
184
+ return aliases;
185
+ },
186
+ allocationOfBase: (expression, point) => {
187
+ if (expression.type !== "Identifier")
188
+ return undefined;
189
+ const symbol = resolved.symbolOf(expression);
190
+ if (!symbol)
191
+ return undefined;
192
+ const value = valueAt(beforeByNode, point, symbol);
193
+ return value.kind === "allocations" && value.allocations.size === 1
194
+ ? value.allocations.values().next().value
195
+ : undefined;
196
+ },
197
+ stableAllocationBetween: (first, last, symbol, expected) => {
198
+ const firstPoint = controlFlow.pointOf(first);
199
+ const lastPoint = controlFlow.pointOf(last);
200
+ if (!firstPoint || !lastPoint || firstPoint.unit !== lastPoint.unit)
201
+ return false;
202
+ if (!controlFlow.nodeDominates(firstPoint.node, lastPoint.node))
203
+ return false;
204
+ if (!isExpected(valueAt(afterByNode, firstPoint, symbol), expected))
205
+ return false;
206
+ if (!isExpected(valueAt(beforeByNode, lastPoint, symbol), expected))
207
+ return false;
208
+ return !controlFlow.nodes.some((node) => {
209
+ if (node.unit !== firstPoint.unit ||
210
+ node === firstPoint.node ||
211
+ node === lastPoint.node)
212
+ return false;
213
+ if (!reachableFrom.get(firstPoint.node)?.has(node) ||
214
+ !reachableFrom.get(node)?.has(lastPoint.node))
215
+ return false;
216
+ return (writesByNode.get(node) ?? []).some((write) => write.definition.symbol === symbol);
217
+ });
218
+ },
219
+ };
220
+ }
221
+ function abstractSlot(slot, values, resolved, allocations, interprocedural, callAllocations) {
222
+ if (!slot || slot.source.kind === "nil-padding")
223
+ return { kind: "nil" };
224
+ const expression = slot.source.expression;
225
+ if (expression.type === "TableConstructorExpression") {
226
+ const allocation = allocations.get(expression);
227
+ return allocation
228
+ ? { kind: "allocations", allocations: new Set([allocation]) }
229
+ : { kind: "unknown", reason: "allocation-unindexed" };
230
+ }
231
+ if (expression.type === "Identifier") {
232
+ const symbol = resolved.symbolOf(expression);
233
+ return symbol
234
+ ? (values.get(symbol) ?? UNKNOWN_ENTRY)
235
+ : { kind: "unknown", reason: "global-or-unresolved" };
236
+ }
237
+ if (expression.type === "CallExpression" ||
238
+ expression.type === "TableCallExpression" ||
239
+ expression.type === "StringCallExpression") {
240
+ const call = interprocedural?.callGraph.callSiteOf(expression);
241
+ if (call && interprocedural) {
242
+ const result = interprocedural.returnsOf(call);
243
+ const index = slot.source.kind === "tail-expansion" ? slot.source.offset : 0;
244
+ const value = result.prefix.at(index);
245
+ if (value) {
246
+ const abstracted = abstractInterproceduralValue(value, callAllocations?.get(expression));
247
+ if (abstracted.kind !== "unknown")
248
+ return abstracted;
249
+ const symbolic = interprocedural
250
+ .symbolicReturnsOf(call)
251
+ .prefix.at(index);
252
+ const argumentsValue = symbolic
253
+ ? abstractParameterAliases(symbolic, callArguments(expression), values, resolved, allocations)
254
+ : undefined;
255
+ if (argumentsValue)
256
+ return argumentsValue;
257
+ return abstracted;
258
+ }
259
+ }
260
+ }
261
+ if (slot.source.kind === "tail-expansion" && slot.source.offset > 0) {
262
+ return { kind: "unknown", reason: "multi-value-tail" };
263
+ }
264
+ if (expression.type === "NilLiteral")
265
+ return { kind: "nil" };
266
+ return { kind: "unknown", reason: "unsupported-expression" };
267
+ }
268
+ function callArguments(call) {
269
+ const explicit = call.type === "CallExpression"
270
+ ? call.arguments
271
+ : [call.type === "TableCallExpression" ? call.arguments : call.argument];
272
+ return call.base.type === "MemberExpression" && call.base.indexer === ":"
273
+ ? [call.base.base, ...explicit]
274
+ : explicit;
275
+ }
276
+ function abstractParameterAliases(value, actuals, values, resolved, allocations) {
277
+ if (value.unknownReasons.length > 0 ||
278
+ value.atoms.length === 0 ||
279
+ !value.atoms.every((atom) => atom.kind === "parameter"))
280
+ return undefined;
281
+ const alternatives = value.atoms.map((atom) => {
282
+ const actual = actuals.at(atom.index);
283
+ if (!actual)
284
+ return { kind: "nil" };
285
+ if (actual.type === "Identifier") {
286
+ const symbol = resolved.symbolOf(actual);
287
+ return symbol ? (values.get(symbol) ?? UNKNOWN_ENTRY) : UNKNOWN_ENTRY;
288
+ }
289
+ if (actual.type === "TableConstructorExpression") {
290
+ const allocation = allocations.get(actual);
291
+ return allocation
292
+ ? {
293
+ kind: "allocations",
294
+ allocations: new Set([allocation]),
295
+ }
296
+ : UNKNOWN_ENTRY;
297
+ }
298
+ if (actual.type === "NilLiteral")
299
+ return { kind: "nil" };
300
+ return UNKNOWN_ENTRY;
301
+ });
302
+ return alternatives.reduce(joinValue);
303
+ }
304
+ function abstractInterproceduralValue(value, allocations) {
305
+ if (value.unknownReasons.length > 0)
306
+ return { kind: "unknown", reason: value.unknownReasons.join(",") };
307
+ const mapped = value.atoms.flatMap((atom) => {
308
+ if (atom.kind !== "allocation" || atom.allocationKind !== "table")
309
+ return [];
310
+ const allocation = allocations?.get(atom.id);
311
+ return allocation ? [allocation] : [];
312
+ });
313
+ const resolved = new Set(mapped);
314
+ if (mapped.length === value.atoms.length && resolved.size > 0)
315
+ return { kind: "allocations", allocations: resolved };
316
+ if (value.atoms.length === 1 && value.atoms[0].kind === "nil")
317
+ return { kind: "nil" };
318
+ return { kind: "unknown", reason: "interprocedural-nonallocation-value" };
319
+ }
320
+ function joinValueMaps(inputs) {
321
+ if (inputs.length === 0)
322
+ return new Map();
323
+ const symbols = new Set(inputs.flatMap((input) => [...input.keys()]));
324
+ const joined = new Map();
325
+ symbols.forEach((symbol) => {
326
+ let value = inputs[0].get(symbol) ?? UNKNOWN_ENTRY;
327
+ for (let index = 1; index < inputs.length; index++) {
328
+ value = joinValue(value, inputs[index].get(symbol) ?? UNKNOWN_ENTRY);
329
+ }
330
+ joined.set(symbol, value);
331
+ });
332
+ return joined;
333
+ }
334
+ function joinValue(left, right) {
335
+ if (left.kind === "nil" && right.kind === "nil")
336
+ return left;
337
+ if (left.kind === "allocations" && right.kind === "allocations") {
338
+ return {
339
+ kind: "allocations",
340
+ allocations: new Set([...left.allocations, ...right.allocations]),
341
+ };
342
+ }
343
+ if (left.kind === "unknown" &&
344
+ right.kind === "unknown" &&
345
+ left.reason === right.reason)
346
+ return left;
347
+ return { kind: "unknown", reason: "control-flow-join" };
348
+ }
349
+ function joinDefinitionMaps(inputs) {
350
+ const joined = new Map();
351
+ inputs.forEach((input) => {
352
+ input.forEach((definitions, symbol) => {
353
+ const target = joined.get(symbol) ?? new Set();
354
+ definitions.forEach((definition) => target.add(definition));
355
+ joined.set(symbol, target);
356
+ });
357
+ });
358
+ return joined;
359
+ }
360
+ function cloneDefinitionMap(input) {
361
+ return new Map([...input].map(([symbol, definitions]) => [symbol, new Set(definitions)]));
362
+ }
363
+ function valueMapsEqual(left, right) {
364
+ if (!left || left.size !== right.size)
365
+ return false;
366
+ return [...left].every(([symbol, value]) => valuesEqual(value, right.get(symbol)));
367
+ }
368
+ function valuesEqual(left, right) {
369
+ if (!right || left.kind !== right.kind)
370
+ return false;
371
+ if (left.kind === "nil")
372
+ return true;
373
+ if (left.kind === "unknown")
374
+ return right.kind === "unknown" && left.reason === right.reason;
375
+ return (right.kind === "allocations" &&
376
+ left.allocations.size === right.allocations.size &&
377
+ [...left.allocations].every((allocation) => right.allocations.has(allocation)));
378
+ }
379
+ function definitionMapsEqual(left, right) {
380
+ if (!left || left.size !== right.size)
381
+ return false;
382
+ return [...left].every(([symbol, definitions]) => {
383
+ const other = right.get(symbol);
384
+ return (!!other &&
385
+ definitions.size === other.size &&
386
+ [...definitions].every((item) => other.has(item)));
387
+ });
388
+ }
389
+ function isExpected(value, expected) {
390
+ return (value.kind === "allocations" &&
391
+ value.allocations.size === 1 &&
392
+ value.allocations.has(expected));
393
+ }
394
+ function computeReachability(flow) {
395
+ const result = new Map();
396
+ flow.nodes.forEach((first) => {
397
+ const pending = [first];
398
+ const seen = new Set();
399
+ while (pending.length > 0) {
400
+ const node = pending.pop();
401
+ if (!node || seen.has(node) || node.unit !== first.unit)
402
+ continue;
403
+ seen.add(node);
404
+ node.successors.forEach((edge) => pending.push(edge.to));
405
+ }
406
+ result.set(first, seen);
407
+ });
408
+ return result;
409
+ }