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