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,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
+ }