storm-lua-minify 0.2.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +157 -37
  3. package/dist/aggregateSpecialization.js +406 -0
  4. package/dist/ast2lua.js +223 -82
  5. package/dist/astWalk.js +162 -0
  6. package/dist/callGraph.js +372 -0
  7. package/dist/cli.js +53 -32
  8. package/dist/cliOptions.js +36 -0
  9. package/dist/cliProgress.js +87 -0
  10. package/dist/config.js +73 -0
  11. package/dist/constantFold.js +798 -0
  12. package/dist/controlFlow.js +266 -0
  13. package/dist/functionRewrites.js +580 -0
  14. package/dist/generatedAst.js +108 -0
  15. package/dist/generatedNode.js +23 -0
  16. package/dist/globalRename.js +35 -0
  17. package/dist/interproceduralAnalysis.js +842 -0
  18. package/dist/interproceduralConstants.js +120 -0
  19. package/dist/keywordLocator.js +68 -0
  20. package/dist/linker.js +13 -0
  21. package/dist/luaString.js +157 -0
  22. package/dist/minifier.js +1243 -35
  23. package/dist/optimizerAnalysis.js +43 -0
  24. package/dist/optimizerDiagnostics.js +65 -0
  25. package/dist/optimizerFacts.js +529 -0
  26. package/dist/optimizerPass.js +96 -0
  27. package/dist/optimizerTransaction.js +56 -0
  28. package/dist/optimizerValueDomain.js +180 -0
  29. package/dist/options.js +233 -0
  30. package/dist/progress.js +2 -0
  31. package/dist/removeUnused.js +145 -0
  32. package/dist/renamer.js +236 -53
  33. package/dist/resolver.js +37 -14
  34. package/dist/runtimeEnvironment.js +105 -0
  35. package/dist/sourceMetadata.js +314 -0
  36. package/dist/statementDataflow.js +259 -0
  37. package/dist/statementScheduler.js +595 -0
  38. package/dist/symbolLiveness.js +92 -0
  39. package/dist/tableEffects.js +356 -0
  40. package/dist/transform.js +40 -0
  41. package/dist/valueFlow.js +409 -0
  42. package/dist/wholeProgramExports.js +646 -0
  43. package/dist/wholeProgramFieldRenames.js +583 -0
  44. package/dist/wholeProgramFields.js +672 -0
  45. package/dist/wholeProgramObjects.js +783 -0
  46. package/package.json +50 -33
  47. package/.github/workflows/ci.yml +0 -39
  48. package/.github/workflows/publish.yml +0 -39
  49. package/.prettierignore +0 -3
  50. package/.prettierrc.json +0 -1
  51. package/dist/index.js +0 -27
  52. package/eslint.config.js +0 -29
  53. package/src/ast2lua.ts +0 -940
  54. package/src/cli.ts +0 -86
  55. package/src/linker.ts +0 -108
  56. package/src/minifier.ts +0 -284
  57. package/src/output.ts +0 -71
  58. package/src/renamer.ts +0 -134
  59. package/src/resolver.ts +0 -378
  60. package/test/circular-require.test.ts +0 -19
  61. package/test/fixtures/bare-require/main.lua +0 -2
  62. package/test/fixtures/bare-require/mod.lua +0 -1
  63. package/test/fixtures/bitwise-precedence/main.lua +0 -10
  64. package/test/fixtures/circular-require/a.lua +0 -2
  65. package/test/fixtures/circular-require/b.lua +0 -2
  66. package/test/fixtures/circular-require/main.lua +0 -2
  67. package/test/fixtures/dofile/greet.lua +0 -1
  68. package/test/fixtures/dofile/main.lua +0 -2
  69. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +0 -2
  70. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +0 -2
  71. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +0 -2
  72. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +0 -2
  73. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +0 -2
  74. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +0 -2
  75. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +0 -2
  76. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +0 -2
  77. package/test/fixtures/entry-scope-many-requires/dep_india.lua +0 -2
  78. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +0 -2
  79. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +0 -2
  80. package/test/fixtures/entry-scope-many-requires/main.lua +0 -25
  81. package/test/fixtures/multi-require/common.lua +0 -1
  82. package/test/fixtures/multi-require/main.lua +0 -3
  83. package/test/fixtures/nested-module/main.lua +0 -2
  84. package/test/fixtures/nested-module/sub/deep.lua +0 -1
  85. package/test/fixtures/require-call/main.lua +0 -2
  86. package/test/fixtures/require-call/mod.lua +0 -5
  87. package/test/fixtures/require-in-expression/main.lua +0 -1
  88. package/test/fixtures/require-in-expression/mod.lua +0 -1
  89. package/test/fixtures/require-string-call/main.lua +0 -2
  90. package/test/fixtures/require-string-call/mod.lua +0 -5
  91. package/test/fixtures/single-file/main.lua +0 -15
  92. package/test/identifier-collision.test.ts +0 -28
  93. package/test/lib/collision.ts +0 -131
  94. package/test/lib/helpers.ts +0 -124
  95. package/test/no-rename.test.ts +0 -19
  96. package/test/output.test.ts +0 -95
  97. package/test/precedence.test.ts +0 -28
  98. package/test/renamer.test.ts +0 -130
  99. package/test/resolver.test.ts +0 -206
  100. package/test/roundtrip.test.ts +0 -26
  101. package/test/snapshot.test.ts +0 -27
  102. package/test/snapshots/bare-require.sl.lua +0 -1
  103. package/test/snapshots/bitwise-precedence.sl.lua +0 -2
  104. package/test/snapshots/dofile.sl.lua +0 -1
  105. package/test/snapshots/entry-scope-many-requires.m.lua +0 -14
  106. package/test/snapshots/multi-require.m.lua +0 -4
  107. package/test/snapshots/multi-require.sl.lua +0 -1
  108. package/test/snapshots/nested-module.m.lua +0 -4
  109. package/test/snapshots/require-call.m.lua +0 -5
  110. package/test/snapshots/require-call.sl.lua +0 -1
  111. package/test/snapshots/require-in-expression.sl.lua +0 -1
  112. package/test/snapshots/require-string-call.m.lua +0 -5
  113. package/test/snapshots/single-file.sl.lua +0 -6
  114. package/test/sourcemap.test.ts +0 -105
  115. package/tsconfig.eslint.json +0 -8
  116. package/tsconfig.json +0 -111
@@ -0,0 +1,842 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeInterprocedural = analyzeInterprocedural;
4
+ const optimizerValueDomain_1 = require("./optimizerValueDomain");
5
+ const luaString_1 = require("./luaString");
6
+ const luaString_2 = require("./luaString");
7
+ /**
8
+ * Function summaries are symbolic transfers over parameter atoms. Recursive SCCs start at
9
+ * lattice bottom and grow monotonically, so a recursive edge cannot erase a provable base case.
10
+ */
11
+ function analyzeInterprocedural(chunk, resolved, callGraph, options = {}) {
12
+ const mutable = new Map(callGraph.functions.map((callable) => [
13
+ callable,
14
+ {
15
+ returns: undefined,
16
+ effects: [],
17
+ escapes: [],
18
+ externalEffects: [],
19
+ allocationShapes: [],
20
+ escapedAllocations: [],
21
+ mayError: false,
22
+ mayInvokeMetamethod: false,
23
+ },
24
+ ]));
25
+ const callByExpression = new WeakMap();
26
+ callGraph.calls.forEach((call) => callByExpression.set(call.call, call));
27
+ const allocationTemplate = new WeakMap();
28
+ let nextAllocation = 0;
29
+ const contractOf = (call) => call.externalTargetName
30
+ ? options.externalContracts?.get(call.externalTargetName)
31
+ : undefined;
32
+ visitExpressions(chunk.body, (expression) => {
33
+ if (expression.type === "TableConstructorExpression")
34
+ allocationTemplate.set(expression, `table:${String(nextAllocation++)}`);
35
+ });
36
+ let changed = true;
37
+ let iterations = 0;
38
+ const iterationLimit = Math.max(1, callGraph.functions.length * 64);
39
+ while (changed) {
40
+ if (iterations++ > iterationLimit)
41
+ throw new Error("Interprocedural finite lattice failed to converge");
42
+ changed = false;
43
+ callGraph.functions.forEach((callable) => {
44
+ const evaluated = evaluateFunction(callable);
45
+ const previous = mutable.get(callable);
46
+ if (!previous)
47
+ throw new Error("Function summary is missing");
48
+ const next = {
49
+ returns: (0, optimizerValueDomain_1.joinOptimizerTuples)([
50
+ ...(previous.returns ? [previous.returns] : []),
51
+ ...evaluated.returns,
52
+ ]),
53
+ effects: uniqueEffects([...previous.effects, ...evaluated.effects]),
54
+ escapes: uniqueEscapes([...previous.escapes, ...evaluated.escapes]),
55
+ externalEffects: uniqueExternalEffects([
56
+ ...previous.externalEffects,
57
+ ...evaluated.externalEffects,
58
+ ]),
59
+ allocationShapes: joinAllocationShapes([
60
+ ...previous.allocationShapes,
61
+ ...evaluated.allocationShapes,
62
+ ]),
63
+ escapedAllocations: uniqueStrings([
64
+ ...previous.escapedAllocations,
65
+ ...evaluated.escapedAllocations,
66
+ ]),
67
+ mayError: previous.mayError || evaluated.mayError,
68
+ mayInvokeMetamethod: previous.mayInvokeMetamethod || evaluated.mayInvokeMetamethod,
69
+ };
70
+ if (summaryKey(previous) !== summaryKey(next)) {
71
+ mutable.set(callable, next);
72
+ changed = true;
73
+ }
74
+ });
75
+ }
76
+ const summaries = callGraph.functions.map((callable) => {
77
+ const summary = mutable.get(callable);
78
+ if (!summary)
79
+ throw new Error("Function summary is missing");
80
+ return {
81
+ callable,
82
+ ...summary,
83
+ returns: summary.returns ?? optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE,
84
+ converged: true,
85
+ };
86
+ });
87
+ const summaryByCallable = new Map(summaries.map((summary) => [summary.callable, summary]));
88
+ const diagnostics = [
89
+ ...callGraph.calls.map((call) => ({
90
+ reason: contractOf(call)
91
+ ? "external-contract-used"
92
+ : call.hasUnknownTarget
93
+ ? "unknown-call-target"
94
+ : "resolved-call-target",
95
+ callId: call.id,
96
+ ...sourceRangeOf(call.call),
97
+ })),
98
+ ...callGraph.sccs
99
+ .filter((scc) => scc.recursive)
100
+ .map((scc) => ({
101
+ reason: "recursive-scc-converged",
102
+ functionId: scc.functions[0].id,
103
+ ...sourceRangeOf(scc.functions[0].declaration),
104
+ })),
105
+ ...summaries.flatMap((summary) => [
106
+ ...summary.effects.map(() => ({
107
+ reason: "parameter-field-effect",
108
+ functionId: summary.callable.id,
109
+ ...sourceRangeOf(summary.callable.declaration),
110
+ })),
111
+ ...summary.escapes.map(() => ({
112
+ reason: "parameter-escape",
113
+ functionId: summary.callable.id,
114
+ ...sourceRangeOf(summary.callable.declaration),
115
+ })),
116
+ ]),
117
+ ];
118
+ const returnsOf = (call) => {
119
+ const actuals = actualValues(call.call, new Map());
120
+ const instantiated = [...call.targets].map((target) => instantiateTuple(safeSummaryReturns(summaryByCallable.get(target)), actuals, call));
121
+ const contract = contractOf(call);
122
+ if (contract)
123
+ instantiated.push(instantiateTuple(contract.returns, actuals, call));
124
+ else if (call.hasUnknownTarget)
125
+ instantiated.push((0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
126
+ kind: "unknown",
127
+ reasons: ["unknown-call-target"],
128
+ }));
129
+ return instantiated.length > 0
130
+ ? (0, optimizerValueDomain_1.joinOptimizerTuples)(instantiated)
131
+ : (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
132
+ kind: "unknown",
133
+ reasons: ["unresolved-call-target"],
134
+ });
135
+ };
136
+ const symbolicReturnsOf = (call) => {
137
+ const tuples = [...call.targets].map((target) => safeSummaryReturns(summaryByCallable.get(target)));
138
+ const contract = contractOf(call);
139
+ if (contract)
140
+ tuples.push(contract.returns);
141
+ else if (call.hasUnknownTarget)
142
+ tuples.push((0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
143
+ kind: "unknown",
144
+ reasons: ["unknown-call-target"],
145
+ }));
146
+ return tuples.length > 0
147
+ ? (0, optimizerValueDomain_1.joinOptimizerTuples)(tuples)
148
+ : optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE;
149
+ };
150
+ return {
151
+ generation: callGraph.generation,
152
+ callGraph,
153
+ summaries,
154
+ diagnostics,
155
+ summaryOf: (callable) => {
156
+ const summary = summaryByCallable.get(callable);
157
+ if (!summary)
158
+ throw new Error("Function summary is missing");
159
+ return summary;
160
+ },
161
+ symbolicReturnsOf,
162
+ returnsOf,
163
+ effectsOf: (call) => {
164
+ const effects = [...call.targets].flatMap((target) => summaryByCallable.get(target)?.effects ?? []);
165
+ return uniqueInstantiatedEffects([
166
+ ...effects.map((effect) => ({
167
+ argumentIndex: effect.parameterIndex,
168
+ access: effect.access,
169
+ ...(effect.staticKey === undefined
170
+ ? {}
171
+ : { staticKey: effect.staticKey }),
172
+ })),
173
+ ...(contractOf(call)?.effects ?? []),
174
+ ]);
175
+ },
176
+ escapesArgument: (call, argumentIndex) => (call.hasUnknownTarget &&
177
+ (contractOf(call)
178
+ ? (contractOf(call)?.escapingArguments?.has(argumentIndex) ?? false)
179
+ : true)) ||
180
+ [...call.targets].some((target) => (summaryByCallable.get(target)?.escapes ?? []).some((escape) => escape.parameterIndex === argumentIndex &&
181
+ escape.reason !== "return")),
182
+ shapeOfAllocation: (allocationId) => {
183
+ const templateId = allocationId.split("/").at(-1);
184
+ return summaries
185
+ .flatMap((summary) => summary.allocationShapes)
186
+ .find((shape) => shape.templateId === templateId);
187
+ },
188
+ };
189
+ function evaluateFunction(callable) {
190
+ const evaluation = {
191
+ returns: [],
192
+ effects: [],
193
+ escapes: [],
194
+ externalEffects: [],
195
+ allocationShapes: [],
196
+ escapedAllocations: [],
197
+ evaluatedCalls: new WeakMap(),
198
+ mayError: false,
199
+ mayInvokeMetamethod: false,
200
+ };
201
+ const environment = new Map();
202
+ callable.parameters.forEach((parameter, index) => environment.set(parameter, (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "parameter", index }])));
203
+ evaluateBlock(callable.declaration.body, environment, evaluation);
204
+ if (evaluation.returns.length === 0)
205
+ evaluation.returns.push(optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE);
206
+ return evaluation;
207
+ }
208
+ function evaluateBlock(body, environment, evaluation) {
209
+ body.forEach((statement) => {
210
+ switch (statement.type) {
211
+ case "LocalStatement":
212
+ case "AssignmentStatement": {
213
+ const values = valuesOfExpressionList(statement.init, environment, evaluation, statement.variables.length);
214
+ statement.variables.forEach((target, index) => {
215
+ if (target.type === "Identifier") {
216
+ const symbol = resolved.symbolOf(target);
217
+ if (symbol &&
218
+ (statement.type === "LocalStatement" || environment.has(symbol)))
219
+ environment.set(symbol, values[index] ?? (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "nil" }]));
220
+ else {
221
+ recordStoreEscapes(values[index] ?? (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "nil" }]), evaluation);
222
+ evaluation.externalEffects.push({
223
+ access: "write",
224
+ id: symbol ? `upvalue:${String(symbol.id)}` : target.name,
225
+ });
226
+ }
227
+ }
228
+ else {
229
+ recordStoreEscapes(values[index] ?? (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "nil" }]), evaluation);
230
+ recordTableEffect(target, "write", environment, evaluation);
231
+ }
232
+ });
233
+ return;
234
+ }
235
+ case "ReturnStatement": {
236
+ const tuple = tupleOfExpressionList(statement.arguments, environment, evaluation);
237
+ tuple.prefix.forEach((value) => {
238
+ parameterIndexes(value).forEach((parameterIndex) => evaluation.escapes.push({
239
+ parameterIndex,
240
+ reason: "return",
241
+ }));
242
+ });
243
+ evaluation.returns.push(tuple);
244
+ return;
245
+ }
246
+ case "CallStatement":
247
+ valueOf(statement.expression, environment, evaluation);
248
+ return;
249
+ case "IfStatement": {
250
+ const branches = statement.clauses.map((clause) => {
251
+ if (clause.type !== "ElseClause")
252
+ valueOf(clause.condition, environment, evaluation);
253
+ const branch = new Map(environment);
254
+ evaluateBlock(clause.body, branch, evaluation);
255
+ return branch;
256
+ });
257
+ if (!statement.clauses.some((clause) => clause.type === "ElseClause"))
258
+ branches.push(new Map(environment));
259
+ joinEnvironments(environment, branches);
260
+ return;
261
+ }
262
+ case "DoStatement": {
263
+ const block = new Map(environment);
264
+ evaluateBlock(statement.body, block, evaluation);
265
+ projectOuterEnvironment(environment, block);
266
+ return;
267
+ }
268
+ case "WhileStatement":
269
+ case "RepeatStatement": {
270
+ valueOf(statement.condition, environment, evaluation);
271
+ const loop = new Map(environment);
272
+ evaluateBlock(statement.body, loop, evaluation);
273
+ joinEnvironments(environment, [environment, loop]);
274
+ return;
275
+ }
276
+ case "ForNumericStatement": {
277
+ valueOf(statement.start, environment, evaluation);
278
+ valueOf(statement.end, environment, evaluation);
279
+ if (statement.step)
280
+ valueOf(statement.step, environment, evaluation);
281
+ const loop = new Map(environment);
282
+ evaluateBlock(statement.body, loop, evaluation);
283
+ joinEnvironments(environment, [new Map(environment), loop]);
284
+ return;
285
+ }
286
+ case "ForGenericStatement": {
287
+ valuesOfExpressionList(statement.iterators, environment, evaluation, statement.variables.length);
288
+ const loop = new Map(environment);
289
+ evaluateBlock(statement.body, loop, evaluation);
290
+ joinEnvironments(environment, [new Map(environment), loop]);
291
+ return;
292
+ }
293
+ case "FunctionDeclaration":
294
+ recordCaptures(statement, environment, evaluation);
295
+ return;
296
+ case "BreakStatement":
297
+ case "LabelStatement":
298
+ case "GotoStatement":
299
+ return;
300
+ }
301
+ });
302
+ }
303
+ function valueOf(expression, environment, evaluation) {
304
+ if (!expression)
305
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "nil" }]);
306
+ switch (expression.type) {
307
+ case "NilLiteral":
308
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "nil" }]);
309
+ case "BooleanLiteral":
310
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([
311
+ { kind: "boolean", value: expression.value },
312
+ ]);
313
+ case "NumericLiteral":
314
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "number", raw: expression.raw }]);
315
+ case "StringLiteral":
316
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([
317
+ { kind: "string", value: expression.value },
318
+ ]);
319
+ case "Identifier": {
320
+ const symbol = resolved.symbolOf(expression);
321
+ const knownFunction = symbol
322
+ ? callGraph.functionOfSymbol(symbol)
323
+ : undefined;
324
+ if (knownFunction)
325
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([
326
+ { kind: "function", id: String(knownFunction.id) },
327
+ ]);
328
+ return symbol
329
+ ? (environment.get(symbol) ??
330
+ (0, optimizerValueDomain_1.unknownOptimizerValue)("symbol-value-unavailable"))
331
+ : (evaluation.externalEffects.push({
332
+ access: "read",
333
+ id: expression.name,
334
+ }),
335
+ (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "external", id: expression.name }], ["external-value"]));
336
+ }
337
+ case "FunctionDeclaration": {
338
+ const target = callGraph.functionOf(expression);
339
+ recordCaptures(expression, environment, evaluation);
340
+ return target
341
+ ? (0, optimizerValueDomain_1.finiteOptimizerValue)([{ kind: "function", id: String(target.id) }])
342
+ : (0, optimizerValueDomain_1.unknownOptimizerValue)("function-unindexed");
343
+ }
344
+ case "TableConstructorExpression": {
345
+ const fields = [];
346
+ let unknownKeyWrite = false;
347
+ let arrayIndex = 1;
348
+ expression.fields.forEach((field) => {
349
+ let staticKey;
350
+ if (field.type === "TableKeyString") {
351
+ staticKey = (0, luaString_1.luaByteStringKey)((0, luaString_1.luaByteStringOfText)(field.key.name));
352
+ }
353
+ else if (field.type === "TableKey") {
354
+ if (field.key.type === "StringLiteral") {
355
+ const decoded = (0, luaString_2.decodeLuaStringLiteral)(field.key);
356
+ if (decoded.ok)
357
+ staticKey = (0, luaString_1.luaByteStringKey)(decoded.value);
358
+ }
359
+ valueOf(field.key, environment, evaluation);
360
+ }
361
+ else {
362
+ staticKey = `number:${String(arrayIndex++)}`;
363
+ }
364
+ const fieldValue = valueOf(field.value, environment, evaluation);
365
+ if (staticKey === undefined)
366
+ unknownKeyWrite = true;
367
+ else
368
+ fields.push({ staticKey, value: fieldValue });
369
+ });
370
+ const templateId = allocationTemplate.get(expression) ?? "table:unindexed";
371
+ evaluation.allocationShapes.push({
372
+ templateId,
373
+ fields,
374
+ unknownKeyWrite,
375
+ });
376
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)([
377
+ {
378
+ kind: "allocation",
379
+ allocationKind: "table",
380
+ id: templateId,
381
+ },
382
+ ]);
383
+ }
384
+ case "MemberExpression":
385
+ case "IndexExpression":
386
+ recordTableEffect(expression, "read", environment, evaluation);
387
+ evaluation.mayError = true;
388
+ evaluation.mayInvokeMetamethod = true;
389
+ return (0, optimizerValueDomain_1.unknownOptimizerValue)("table-read-value");
390
+ case "CallExpression":
391
+ case "TableCallExpression":
392
+ case "StringCallExpression": {
393
+ const cached = evaluation.evaluatedCalls.get(expression);
394
+ if (cached)
395
+ return (0, optimizerValueDomain_1.valueAtOptimizerTupleSlot)(cached.returns, 0);
396
+ const call = callByExpression.get(expression);
397
+ const actuals = actualValues(expression, environment, evaluation);
398
+ const contract = call ? contractOf(call) : undefined;
399
+ if (!call || (call.hasUnknownTarget && !contract)) {
400
+ evaluation.externalEffects.push({
401
+ access: "call",
402
+ id: expression.base.type === "Identifier"
403
+ ? expression.base.name
404
+ : "unknown-call",
405
+ });
406
+ actuals.forEach((actual) => {
407
+ recordStoreEscapes(actual, evaluation, "unknown-call");
408
+ parameterIndexes(actual).forEach((parameterIndex) => evaluation.escapes.push({
409
+ parameterIndex,
410
+ reason: "unknown-call",
411
+ }));
412
+ });
413
+ evaluation.mayError = true;
414
+ evaluation.mayInvokeMetamethod = true;
415
+ }
416
+ if (!call)
417
+ return (0, optimizerValueDomain_1.unknownOptimizerValue)("call-unindexed");
418
+ const tuples = [...call.targets].flatMap((target) => {
419
+ const targetSummary = mutable.get(target);
420
+ if (!targetSummary)
421
+ throw new Error("Function summary is missing");
422
+ targetSummary.effects.forEach((effect) => {
423
+ parameterIndexes(actuals[effect.parameterIndex]).forEach((parameterIndex) => evaluation.effects.push({
424
+ parameterIndex,
425
+ access: effect.access,
426
+ ...(effect.staticKey === undefined
427
+ ? {}
428
+ : { staticKey: effect.staticKey }),
429
+ }));
430
+ });
431
+ targetSummary.escapes.forEach((escape) => {
432
+ if (escape.reason !== "return")
433
+ allocationIds(actuals[escape.parameterIndex]).forEach((id) => evaluation.escapedAllocations.push(id));
434
+ parameterIndexes(actuals[escape.parameterIndex]).forEach((parameterIndex) => evaluation.escapes.push({
435
+ parameterIndex,
436
+ reason: escape.reason,
437
+ }));
438
+ });
439
+ evaluation.externalEffects.push(...targetSummary.externalEffects);
440
+ evaluation.mayError ||= targetSummary.mayError;
441
+ evaluation.mayInvokeMetamethod ||= targetSummary.mayInvokeMetamethod;
442
+ return targetSummary.returns
443
+ ? [
444
+ instantiateTuple(rejectEscapedAllocations(targetSummary.returns, targetSummary.escapedAllocations), actuals, call),
445
+ ]
446
+ : [];
447
+ });
448
+ if (contract) {
449
+ evaluation.mayError ||= contract.mayError ?? false;
450
+ evaluation.mayInvokeMetamethod ||=
451
+ contract.mayInvokeMetamethod ?? false;
452
+ (contract.effects ?? []).forEach((effect) => {
453
+ parameterIndexes(actuals[effect.argumentIndex]).forEach((parameterIndex) => evaluation.effects.push({
454
+ parameterIndex,
455
+ access: effect.access,
456
+ ...(effect.staticKey === undefined
457
+ ? {}
458
+ : { staticKey: effect.staticKey }),
459
+ }));
460
+ });
461
+ contract.escapingArguments?.forEach((argumentIndex) => {
462
+ const actual = actuals.at(argumentIndex);
463
+ if (actual)
464
+ recordStoreEscapes(actual, evaluation, "unknown-call");
465
+ parameterIndexes(actual).forEach((parameterIndex) => evaluation.escapes.push({
466
+ parameterIndex,
467
+ reason: "unknown-call",
468
+ }));
469
+ });
470
+ tuples.push(instantiateTuple(contract.returns, actuals, call));
471
+ }
472
+ else if (call.hasUnknownTarget)
473
+ tuples.push((0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
474
+ kind: "unknown",
475
+ reasons: ["unknown-call-target"],
476
+ }));
477
+ const returns = tuples.length > 0
478
+ ? (0, optimizerValueDomain_1.joinOptimizerTuples)(tuples)
479
+ : call.targets.size > 0
480
+ ? (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
481
+ kind: "unknown",
482
+ reasons: ["recursive-summary-bottom"],
483
+ })
484
+ : optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE;
485
+ evaluation.evaluatedCalls.set(expression, { actuals, returns });
486
+ return (0, optimizerValueDomain_1.valueAtOptimizerTupleSlot)(returns, 0);
487
+ }
488
+ case "BinaryExpression":
489
+ case "LogicalExpression":
490
+ valueOf(expression.left, environment, evaluation);
491
+ valueOf(expression.right, environment, evaluation);
492
+ evaluation.mayError = true;
493
+ evaluation.mayInvokeMetamethod = true;
494
+ return (0, optimizerValueDomain_1.unknownOptimizerValue)("computed-expression");
495
+ case "UnaryExpression":
496
+ valueOf(expression.argument, environment, evaluation);
497
+ evaluation.mayError = true;
498
+ evaluation.mayInvokeMetamethod = true;
499
+ return (0, optimizerValueDomain_1.unknownOptimizerValue)("computed-expression");
500
+ case "VarargLiteral":
501
+ return (0, optimizerValueDomain_1.unknownOptimizerValue)("vararg-value");
502
+ }
503
+ }
504
+ function actualValues(call, environment, evaluation) {
505
+ const sink = evaluation ??
506
+ {
507
+ returns: [],
508
+ effects: [],
509
+ escapes: [],
510
+ externalEffects: [],
511
+ allocationShapes: [],
512
+ escapedAllocations: [],
513
+ evaluatedCalls: new WeakMap(),
514
+ mayError: false,
515
+ mayInvokeMetamethod: false,
516
+ };
517
+ const explicit = call.type === "CallExpression"
518
+ ? call.arguments
519
+ : [
520
+ call.type === "TableCallExpression"
521
+ ? call.arguments
522
+ : call.argument,
523
+ ];
524
+ const receiver = call.base.type === "MemberExpression" && call.base.indexer === ":"
525
+ ? [call.base.base]
526
+ : [];
527
+ return valuesOfExpressionList([...receiver, ...explicit], environment, sink, Number.POSITIVE_INFINITY);
528
+ }
529
+ function valuesOfExpressionList(expressions, environment, evaluation, count) {
530
+ const tuple = tupleOfExpressionList(expressions, environment, evaluation);
531
+ const available = Number.isFinite(count)
532
+ ? count
533
+ : Math.max(expressions.length, tuple.prefix.length);
534
+ return Array.from({ length: available }, (_, index) => (0, optimizerValueDomain_1.valueAtOptimizerTupleSlot)(tuple, index));
535
+ }
536
+ function tupleOfExpressionList(expressions, environment, evaluation) {
537
+ if (expressions.length === 0)
538
+ return optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE;
539
+ const leading = expressions
540
+ .slice(0, -1)
541
+ .map((expression) => valueOf(expression, environment, evaluation));
542
+ const last = expressions.at(-1);
543
+ if (!last)
544
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)(leading);
545
+ const single = valueOf(last, environment, evaluation);
546
+ const tail = tupleOfLastExpression(last, environment, evaluation, single);
547
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)([...leading, ...tail.prefix], tail.tail);
548
+ }
549
+ function tupleOfLastExpression(expression, environment, evaluation, singleValue) {
550
+ if (expression.type === "VarargLiteral") {
551
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
552
+ kind: "unknown",
553
+ reasons: ["vararg-tail"],
554
+ });
555
+ }
556
+ if (expression.type !== "CallExpression" &&
557
+ expression.type !== "TableCallExpression" &&
558
+ expression.type !== "StringCallExpression")
559
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)([singleValue]);
560
+ const call = callByExpression.get(expression);
561
+ if (!call)
562
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
563
+ kind: "unknown",
564
+ reasons: ["call-unindexed"],
565
+ });
566
+ valueOf(expression, environment, evaluation);
567
+ const actuals = evaluation.evaluatedCalls.get(expression)?.actuals;
568
+ if (!actuals)
569
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
570
+ kind: "unknown",
571
+ reasons: ["call-evaluation-missing"],
572
+ });
573
+ const tuples = [...call.targets].flatMap((target) => {
574
+ const returns = mutable.get(target)?.returns;
575
+ return returns
576
+ ? [
577
+ instantiateTuple(rejectEscapedAllocations(returns, mutable.get(target)?.escapedAllocations ?? []), actuals, call),
578
+ ]
579
+ : [];
580
+ });
581
+ const contract = contractOf(call);
582
+ if (contract)
583
+ tuples.push(instantiateTuple(contract.returns, actuals, call));
584
+ else if (call.hasUnknownTarget)
585
+ tuples.push((0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
586
+ kind: "unknown",
587
+ reasons: ["unknown-call-target"],
588
+ }));
589
+ return tuples.length > 0
590
+ ? (0, optimizerValueDomain_1.joinOptimizerTuples)(tuples)
591
+ : call.targets.size > 0
592
+ ? (0, optimizerValueDomain_1.finiteOptimizerTuple)([], {
593
+ kind: "unknown",
594
+ reasons: ["recursive-summary-bottom"],
595
+ })
596
+ : optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE;
597
+ }
598
+ function recordTableEffect(expression, access, environment, evaluation) {
599
+ const base = valueOf(expression.base, environment, evaluation);
600
+ const staticKey = expression.type === "MemberExpression"
601
+ ? (0, luaString_1.luaByteStringKey)((0, luaString_1.luaByteStringOfText)(expression.identifier.name))
602
+ : expression.index.type === "StringLiteral"
603
+ ? (() => {
604
+ const decoded = (0, luaString_2.decodeLuaStringLiteral)(expression.index);
605
+ return decoded.ok ? (0, luaString_1.luaByteStringKey)(decoded.value) : undefined;
606
+ })()
607
+ : undefined;
608
+ parameterIndexes(base).forEach((parameterIndex) => evaluation.effects.push({
609
+ parameterIndex,
610
+ access,
611
+ ...(staticKey === undefined ? {} : { staticKey }),
612
+ }));
613
+ if (expression.type === "IndexExpression")
614
+ valueOf(expression.index, environment, evaluation);
615
+ }
616
+ function recordCaptures(declaration, environment, evaluation) {
617
+ visitExpressions(declaration.body, (expression) => {
618
+ if (expression.type !== "Identifier")
619
+ return;
620
+ const symbol = resolved.symbolOf(expression);
621
+ const value = symbol ? environment.get(symbol) : undefined;
622
+ parameterIndexes(value).forEach((parameterIndex) => evaluation.escapes.push({ parameterIndex, reason: "capture" }));
623
+ });
624
+ }
625
+ }
626
+ function instantiateTuple(tuple, actuals, call) {
627
+ const instantiate = (value) => (0, optimizerValueDomain_1.joinOptimizerValues)([
628
+ (0, optimizerValueDomain_1.finiteOptimizerValue)(value.atoms.flatMap((atom) => {
629
+ if (atom.kind === "parameter")
630
+ return [...(actuals[atom.index]?.atoms ?? [])];
631
+ if (atom.kind === "allocation")
632
+ return [{ ...atom, id: `call:${String(call.id)}/${atom.id}` }];
633
+ return [atom];
634
+ }), value.unknownReasons),
635
+ ...value.atoms
636
+ .filter((atom) => atom.kind === "parameter")
637
+ .map((atom) => actuals[atom.index] ?? (0, optimizerValueDomain_1.unknownOptimizerValue)("missing-argument")),
638
+ ]);
639
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)(tuple.prefix.map(instantiate), tuple.tail.kind === "vararg"
640
+ ? { kind: "vararg", value: instantiate(tuple.tail.value) }
641
+ : tuple.tail);
642
+ }
643
+ function safeSummaryReturns(summary) {
644
+ return summary
645
+ ? rejectEscapedAllocations(summary.returns, summary.escapedAllocations)
646
+ : optimizerValueDomain_1.EMPTY_OPTIMIZER_TUPLE;
647
+ }
648
+ function rejectEscapedAllocations(tuple, escapedAllocations) {
649
+ if (escapedAllocations.length === 0)
650
+ return tuple;
651
+ const escaped = new Set(escapedAllocations);
652
+ const reject = (value) => {
653
+ const retained = value.atoms.filter((atom) => atom.kind !== "allocation" || !escaped.has(atom.id));
654
+ return (0, optimizerValueDomain_1.finiteOptimizerValue)(retained, retained.length === value.atoms.length
655
+ ? value.unknownReasons
656
+ : [...value.unknownReasons, "escaped-allocation"]);
657
+ };
658
+ return (0, optimizerValueDomain_1.finiteOptimizerTuple)(tuple.prefix.map(reject), tuple.tail.kind === "vararg"
659
+ ? { kind: "vararg", value: reject(tuple.tail.value) }
660
+ : tuple.tail);
661
+ }
662
+ function parameterIndexes(value) {
663
+ return value
664
+ ? value.atoms.flatMap((atom) => atom.kind === "parameter" ? [atom.index] : [])
665
+ : [];
666
+ }
667
+ function recordStoreEscapes(value, evaluation, parameterReason = "store") {
668
+ parameterIndexes(value).forEach((parameterIndex) => evaluation.escapes.push({ parameterIndex, reason: parameterReason }));
669
+ allocationIds(value).forEach((id) => evaluation.escapedAllocations.push(id));
670
+ }
671
+ function allocationIds(value) {
672
+ return value
673
+ ? value.atoms.flatMap((atom) => atom.kind === "allocation" ? [atom.id] : [])
674
+ : [];
675
+ }
676
+ function projectOuterEnvironment(target, source) {
677
+ [...target.keys()].forEach((symbol) => {
678
+ const value = source.get(symbol);
679
+ if (value)
680
+ target.set(symbol, value);
681
+ });
682
+ }
683
+ function uniqueStrings(values) {
684
+ return [...new Set(values)].sort();
685
+ }
686
+ function joinEnvironments(target, branches) {
687
+ const symbols = new Set(branches.flatMap((branch) => [...branch.keys()]));
688
+ symbols.forEach((symbol) => target.set(symbol, (0, optimizerValueDomain_1.joinOptimizerValues)(branches.map((branch) => branch.get(symbol) ?? (0, optimizerValueDomain_1.unknownOptimizerValue)("branch-local")))));
689
+ }
690
+ function uniqueEffects(effects) {
691
+ const byKey = new Map();
692
+ effects.forEach((effect) => byKey.set(`${String(effect.parameterIndex)}:${effect.access}:${effect.staticKey ?? "*"}`, effect));
693
+ return [...byKey.entries()]
694
+ .sort(([left], [right]) => left.localeCompare(right))
695
+ .map(([, effect]) => effect);
696
+ }
697
+ function uniqueInstantiatedEffects(effects) {
698
+ const byKey = new Map();
699
+ effects.forEach((effect) => byKey.set(`${String(effect.argumentIndex)}:${effect.access}:${effect.staticKey ?? "*"}`, effect));
700
+ return [...byKey.entries()]
701
+ .sort(([left], [right]) => left.localeCompare(right))
702
+ .map(([, effect]) => effect);
703
+ }
704
+ function uniqueEscapes(escapes) {
705
+ const byKey = new Map();
706
+ escapes.forEach((escape) => byKey.set(`${String(escape.parameterIndex)}:${escape.reason}`, escape));
707
+ return [...byKey.entries()]
708
+ .sort(([left], [right]) => left.localeCompare(right))
709
+ .map(([, escape]) => escape);
710
+ }
711
+ function uniqueExternalEffects(effects) {
712
+ const byKey = new Map();
713
+ effects.forEach((effect) => byKey.set(`${effect.access}:${effect.id}`, effect));
714
+ return [...byKey.entries()]
715
+ .sort(([left], [right]) => left.localeCompare(right))
716
+ .map(([, effect]) => effect);
717
+ }
718
+ function joinAllocationShapes(shapes) {
719
+ const byTemplate = new Map();
720
+ shapes.forEach((shape) => {
721
+ const entries = byTemplate.get(shape.templateId) ?? [];
722
+ entries.push(shape);
723
+ byTemplate.set(shape.templateId, entries);
724
+ });
725
+ return [...byTemplate.entries()]
726
+ .sort(([left], [right]) => left.localeCompare(right))
727
+ .map(([templateId, alternatives]) => {
728
+ const keys = new Set(alternatives.flatMap((shape) => shape.fields.map((field) => field.staticKey)));
729
+ const fields = [...keys].sort().flatMap((staticKey) => {
730
+ const values = alternatives.flatMap((shape) => {
731
+ const field = shape.fields.find((candidate) => candidate.staticKey === staticKey);
732
+ return field ? [field.value] : [];
733
+ });
734
+ return values.length === alternatives.length
735
+ ? [{ staticKey, value: (0, optimizerValueDomain_1.joinOptimizerValues)(values) }]
736
+ : [];
737
+ });
738
+ return {
739
+ templateId,
740
+ fields,
741
+ unknownKeyWrite: alternatives.some((shape) => shape.unknownKeyWrite),
742
+ };
743
+ });
744
+ }
745
+ function summaryKey(summary) {
746
+ return JSON.stringify(summary);
747
+ }
748
+ function sourceRangeOf(node) {
749
+ const range = node.range;
750
+ return range ? { sourceRange: range } : {};
751
+ }
752
+ function visitExpressions(body, visit) {
753
+ const expression = (value) => {
754
+ visit(value);
755
+ switch (value.type) {
756
+ case "FunctionDeclaration":
757
+ visitExpressions(value.body, visit);
758
+ return;
759
+ case "CallExpression":
760
+ expression(value.base);
761
+ value.arguments.forEach(expression);
762
+ return;
763
+ case "TableCallExpression":
764
+ expression(value.base);
765
+ expression(value.arguments);
766
+ return;
767
+ case "StringCallExpression":
768
+ expression(value.base);
769
+ expression(value.argument);
770
+ return;
771
+ case "BinaryExpression":
772
+ case "LogicalExpression":
773
+ expression(value.left);
774
+ expression(value.right);
775
+ return;
776
+ case "UnaryExpression":
777
+ expression(value.argument);
778
+ return;
779
+ case "MemberExpression":
780
+ expression(value.base);
781
+ return;
782
+ case "IndexExpression":
783
+ expression(value.base);
784
+ expression(value.index);
785
+ return;
786
+ case "TableConstructorExpression":
787
+ value.fields.forEach((field) => {
788
+ if (field.type === "TableKey")
789
+ expression(field.key);
790
+ expression(field.value);
791
+ });
792
+ return;
793
+ default:
794
+ return;
795
+ }
796
+ };
797
+ body.forEach((statement) => {
798
+ switch (statement.type) {
799
+ case "LocalStatement":
800
+ case "AssignmentStatement":
801
+ statement.init.forEach(expression);
802
+ return;
803
+ case "CallStatement":
804
+ expression(statement.expression);
805
+ return;
806
+ case "ReturnStatement":
807
+ statement.arguments.forEach(expression);
808
+ return;
809
+ case "FunctionDeclaration":
810
+ visit(statement);
811
+ visitExpressions(statement.body, visit);
812
+ return;
813
+ case "DoStatement":
814
+ case "WhileStatement":
815
+ case "RepeatStatement":
816
+ if ("condition" in statement)
817
+ expression(statement.condition);
818
+ visitExpressions(statement.body, visit);
819
+ return;
820
+ case "IfStatement":
821
+ statement.clauses.forEach((clause) => {
822
+ if (clause.type !== "ElseClause")
823
+ expression(clause.condition);
824
+ visitExpressions(clause.body, visit);
825
+ });
826
+ return;
827
+ case "ForNumericStatement":
828
+ expression(statement.start);
829
+ expression(statement.end);
830
+ if (statement.step)
831
+ expression(statement.step);
832
+ visitExpressions(statement.body, visit);
833
+ return;
834
+ case "ForGenericStatement":
835
+ statement.iterators.forEach(expression);
836
+ visitExpressions(statement.body, visit);
837
+ return;
838
+ default:
839
+ return;
840
+ }
841
+ });
842
+ }