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
package/dist/minifier.js CHANGED
@@ -7,18 +7,78 @@ exports.Minifier = void 0;
7
7
  const luaparse_1 = __importDefault(require("luaparse"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const source_map_1 = require("source-map");
11
10
  const ast2lua_1 = require("./ast2lua");
12
11
  const linker_1 = require("./linker");
13
12
  const resolver_1 = require("./resolver");
14
13
  const renamer_1 = require("./renamer");
15
14
  const globalRename_1 = require("./globalRename");
16
15
  const transform_1 = require("./transform");
16
+ const sourceMetadata_1 = require("./sourceMetadata");
17
+ const removeUnused_1 = require("./removeUnused");
18
+ const constantFold_1 = require("./constantFold");
19
+ const generatedAst_1 = require("./generatedAst");
20
+ const statementScheduler_1 = require("./statementScheduler");
21
+ const optimizerPass_1 = require("./optimizerPass");
22
+ const runtimeEnvironment_1 = require("./runtimeEnvironment");
23
+ const optimizerDiagnostics_1 = require("./optimizerDiagnostics");
24
+ const optimizerFacts_1 = require("./optimizerFacts");
25
+ const optimizerAnalysis_1 = require("./optimizerAnalysis");
26
+ const interproceduralConstants_1 = require("./interproceduralConstants");
27
+ const functionRewrites_1 = require("./functionRewrites");
28
+ const wholeProgramObjects_1 = require("./wholeProgramObjects");
29
+ const wholeProgramFields_1 = require("./wholeProgramFields");
30
+ const aggregateSpecialization_1 = require("./aggregateSpecialization");
31
+ const wholeProgramExports_1 = require("./wholeProgramExports");
32
+ const wholeProgramFieldRenames_1 = require("./wholeProgramFieldRenames");
33
+ const options_1 = require("./options");
17
34
  const NO_RENAME = {
18
35
  nameOf: () => undefined,
19
36
  usedNames: new Set(),
20
37
  };
38
+ class CostVariantProgress {
39
+ target;
40
+ pendingSteps = 0;
41
+ constructor(target) {
42
+ this.target = target;
43
+ }
44
+ addSteps(count) {
45
+ this.pendingSteps += count;
46
+ this.target.addSteps(count);
47
+ }
48
+ startStep(label) {
49
+ if (this.pendingSteps > 0)
50
+ this.pendingSteps--;
51
+ this.target.startStep(label);
52
+ }
53
+ tick() {
54
+ this.target.tick();
55
+ }
56
+ finishFailedCandidate() {
57
+ while (this.pendingSteps > 0) {
58
+ this.pendingSteps--;
59
+ this.target.startStep("Skip remaining failed cost-gate work");
60
+ }
61
+ }
62
+ }
63
+ // Remove consumers before producers. A producer is therefore measured while
64
+ // every downstream transformation that may expose its savings is still active.
65
+ const COST_GATE_ABLATION_ORDER = [
66
+ "fieldRename",
67
+ "scheduler",
68
+ "exportDce",
69
+ "fieldFact",
70
+ "aggregateSpecialization",
71
+ "functionRewrite",
72
+ ];
73
+ function sourceRangeOf(node) {
74
+ return node.range;
75
+ }
76
+ function copyMap(source, target) {
77
+ target.clear();
78
+ source.forEach((value, key) => target.set(key, value));
79
+ }
21
80
  class Minifier {
81
+ entryFilePath;
22
82
  identifiersInUse;
23
83
  moduleSourceText;
24
84
  moduleAST;
@@ -35,44 +95,915 @@ class Minifier {
35
95
  renameCache = new Map();
36
96
  // #8a: プログラム全体を横断して決定された「内部グローバル名 -> 短縮名」の対応
37
97
  globalRenames = new Map();
38
- constructor(entryFilePath, luaParseSettings, mode) {
98
+ moduleMetadata = new Map();
99
+ diagnosticCollector;
100
+ schedulerVariant;
101
+ functionRewriteVariant;
102
+ fieldFactVariant;
103
+ aggregateSpecializationVariant;
104
+ exportDceVariant;
105
+ fieldRenameVariant;
106
+ progress;
107
+ // Transactional variants mutate independent AST copies, but their source
108
+ // bytes and initial parse are identical. Keep one untouched parse template
109
+ // for the whole variant tree and clone it at each link boundary.
110
+ inputCache;
111
+ linkedAstGeneration = 0;
112
+ wholeProgramObjectsValue;
113
+ wholeProgramFieldsValue;
114
+ wholeProgramExportsValue;
115
+ wholeProgramFieldRenamesValue;
116
+ constructor(entryFilePath, luaParseSettings, mode, schedulerVariantOrProgress, functionRewriteVariant, fieldFactVariant, aggregateSpecializationVariant, exportDceVariant, fieldRenameVariant, progress, inputCache) {
117
+ this.schedulerVariant =
118
+ typeof schedulerVariantOrProgress === "string"
119
+ ? schedulerVariantOrProgress
120
+ : undefined;
121
+ this.functionRewriteVariant = functionRewriteVariant;
122
+ this.fieldFactVariant = fieldFactVariant;
123
+ this.aggregateSpecializationVariant = aggregateSpecializationVariant;
124
+ this.exportDceVariant = exportDceVariant;
125
+ this.fieldRenameVariant = fieldRenameVariant;
126
+ this.progress =
127
+ typeof schedulerVariantOrProgress === "object"
128
+ ? schedulerVariantOrProgress
129
+ : progress;
130
+ this.inputCache = inputCache ?? new Map();
131
+ this.entryFilePath = entryFilePath;
39
132
  this.identifiersInUse = new Set();
40
133
  this.moduleSourceText = new Map();
41
134
  this.moduleAST = new Map();
42
135
  this.moduleNameAndFileName = new Map();
43
- this.luaParseSettings = luaParseSettings;
44
- this.mode = mode;
136
+ // コメントの所有関係と自己再帰参照の判定は位置情報を不変条件とする。
137
+ // 呼び出し側が省略・無効化しても、内部パスに必要な情報は常に収集する。
138
+ this.luaParseSettings = {
139
+ ...luaParseSettings,
140
+ comments: true,
141
+ locations: true,
142
+ ranges: true,
143
+ };
144
+ this.mode = (0, options_1.resolveMinifierMode)(mode);
145
+ if (mode.collectOptimizationDiagnostics) {
146
+ this.diagnosticCollector = new optimizerDiagnostics_1.OptimizationDiagnosticCollector();
147
+ }
45
148
  const pn = path_1.default.parse(entryFilePath);
46
149
  this.dir = pn.dir;
47
150
  this.entryModule = pn.name;
48
151
  }
152
+ get optimizationDiagnostics() {
153
+ return this.diagnosticCollector?.diagnostics ?? [];
154
+ }
155
+ get wholeProgramObjects() {
156
+ return this.wholeProgramObjectsValue;
157
+ }
158
+ get wholeProgramFields() {
159
+ return this.wholeProgramFieldsValue;
160
+ }
161
+ get wholeProgramExports() {
162
+ return this.wholeProgramExportsValue;
163
+ }
164
+ get wholeProgramFieldRenames() {
165
+ return this.wholeProgramFieldRenamesValue;
166
+ }
49
167
  parse() {
168
+ if (this.costGatesToSelect().length > 0)
169
+ return this.parseWithCostGateSelection();
170
+ return this.parseOnce();
171
+ }
172
+ /**
173
+ * Evaluate cost-gated transforms as one coalition, then remove a gate only
174
+ * when its absence makes the complete final output strictly shorter.
175
+ *
176
+ * An arbitrary final-byte cost function still requires 2^N evaluations to
177
+ * find its global minimum. This deterministic backward sweep deliberately
178
+ * chooses a narrower contract: it keeps interactions present in the full
179
+ * trial and finds the best point on one monotonic deletion path in at most
180
+ * N+2 evaluations. The all-off baseline remains the final upper bound.
181
+ */
182
+ parseWithCostGateSelection() {
183
+ const gates = this.costGatesToSelect();
184
+ const evaluated = new Map();
185
+ const baseline = this.evaluateCostVariant(this.costGateVariants("baseline"), "Evaluate cost-gated baseline", evaluated);
186
+ let fullTrial;
187
+ try {
188
+ fullTrial = this.evaluateCostVariant(this.costGateVariants("trial"), "Evaluate joint cost-gated trial", evaluated);
189
+ }
190
+ catch {
191
+ this.copyDiagnosticsFrom(baseline.minifier);
192
+ gates.forEach((gate) => {
193
+ this.recordFinalGateDecision(gate, "rejected", "trial-failed");
194
+ });
195
+ this.recordFinalCostDecision("rejected", "trial-failed");
196
+ return this.adoptVariant(baseline.minifier, baseline.output);
197
+ }
198
+ let current = fullTrial;
199
+ for (const gate of COST_GATE_ABLATION_ORDER) {
200
+ if (!gates.includes(gate) || current.variants[gate] === "baseline")
201
+ continue;
202
+ const variants = {
203
+ ...current.variants,
204
+ [gate]: "baseline",
205
+ // Specialization is executed inside the function-rewrite pipeline;
206
+ // keep the mask and final diagnostics closed over that dependency.
207
+ ...(gate === "functionRewrite"
208
+ ? { aggregateSpecialization: "baseline" }
209
+ : {}),
210
+ };
211
+ let withoutGate;
212
+ try {
213
+ withoutGate = this.evaluateCostVariant(variants, `Evaluate joint trial without ${this.costGateLabel(gate)}`, evaluated);
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ // Keep ties in the joint trial: a gate whose isolated contribution is
219
+ // hidden can still participate in a later transformation's savings.
220
+ if (withoutGate.byteLength < current.byteLength)
221
+ current = withoutGate;
222
+ }
223
+ const accepted = current.byteLength < baseline.byteLength;
224
+ const selected = accepted ? current : baseline;
225
+ // Successful trial diagnostics describe attempted opportunities even when
226
+ // the corresponding AST is not selected, matching the former gate policy.
227
+ this.copyDiagnosticsFrom(fullTrial.minifier);
228
+ gates.forEach((gate) => {
229
+ this.recordFinalGateDecision(gate, accepted && current.variants[gate] === "trial"
230
+ ? "accepted"
231
+ : "rejected", accepted && current.variants[gate] === "trial"
232
+ ? "final-output-shorter"
233
+ : "final-output-not-shorter");
234
+ });
235
+ this.recordFinalCostDecision(accepted ? "accepted" : "rejected", accepted ? "final-output-shorter" : "final-output-not-shorter", accepted ? baseline.byteLength - current.byteLength : undefined);
236
+ return this.adoptVariant(selected.minifier, selected.output);
237
+ }
238
+ evaluateCostVariant(variants, label, evaluated) {
239
+ const key = COST_GATE_ABLATION_ORDER.map((gate) => variants[gate] === "trial" ? "1" : "0").join("");
240
+ const cached = evaluated.get(key);
241
+ if (cached)
242
+ return cached;
243
+ this.progress?.addSteps(1);
244
+ this.progress?.startStep(label);
245
+ const candidateProgress = this.progress
246
+ ? new CostVariantProgress(this.progress)
247
+ : undefined;
248
+ const minifier = new Minifier(this.entryFilePath, this.luaParseSettings, this.mode, variants.scheduler, variants.functionRewrite, variants.fieldFact, variants.aggregateSpecialization, variants.exportDce, variants.fieldRename, candidateProgress, this.inputCache);
249
+ let output;
250
+ try {
251
+ output = minifier.parseOnce();
252
+ }
253
+ catch (error) {
254
+ candidateProgress?.finishFailedCandidate();
255
+ throw error;
256
+ }
257
+ const result = {
258
+ minifier,
259
+ output,
260
+ byteLength: new TextEncoder().encode(output.toString()).length,
261
+ variants,
262
+ };
263
+ evaluated.set(key, result);
264
+ return result;
265
+ }
266
+ costGateVariants(fallback) {
267
+ const selected = new Set(this.costGatesToSelect());
268
+ const variant = (gate, explicit) => explicit ?? (selected.has(gate) ? fallback : "baseline");
269
+ const functionRewrite = variant("functionRewrite", this.functionRewriteVariant);
270
+ return {
271
+ fieldRename: variant("fieldRename", this.fieldRenameVariant),
272
+ scheduler: variant("scheduler", this.schedulerVariant),
273
+ exportDce: variant("exportDce", this.exportDceVariant),
274
+ fieldFact: variant("fieldFact", this.fieldFactVariant),
275
+ aggregateSpecialization: functionRewrite === "baseline"
276
+ ? "baseline"
277
+ : variant("aggregateSpecialization", this.aggregateSpecializationVariant),
278
+ functionRewrite,
279
+ };
280
+ }
281
+ costGatesToSelect() {
282
+ return COST_GATE_ABLATION_ORDER.filter((gate) => {
283
+ switch (gate) {
284
+ case "fieldRename":
285
+ return (this.fieldRenameVariant === undefined && this.fieldRenamesEnabled());
286
+ case "scheduler":
287
+ return (this.schedulerVariant === undefined &&
288
+ this.requiresSchedulerSelection());
289
+ case "exportDce":
290
+ return this.exportDceVariant === undefined && this.exportDceEnabled();
291
+ case "fieldFact":
292
+ return (this.fieldFactVariant === undefined && this.fieldFactsEnabled());
293
+ case "aggregateSpecialization":
294
+ return (this.aggregateSpecializationVariant === undefined &&
295
+ this.functionSpecializationEnabled());
296
+ case "functionRewrite":
297
+ return (this.functionRewriteVariant === undefined &&
298
+ this.functionRewritesEnabled());
299
+ }
300
+ });
301
+ }
302
+ costGateLabel(gate) {
303
+ switch (gate) {
304
+ case "fieldRename":
305
+ return "field renaming";
306
+ case "scheduler":
307
+ return "statement scheduling";
308
+ case "exportDce":
309
+ return "unused export removal";
310
+ case "fieldFact":
311
+ return "field optimization";
312
+ case "aggregateSpecialization":
313
+ return "function specialization";
314
+ case "functionRewrite":
315
+ return "function rewrites";
316
+ }
317
+ }
318
+ recordFinalGateDecision(gate, decision, reason) {
319
+ switch (gate) {
320
+ case "fieldRename":
321
+ this.recordFinalFieldRenameDecision(decision, reason);
322
+ return;
323
+ case "scheduler":
324
+ this.recordFinalSchedulerDecision(decision, reason);
325
+ return;
326
+ case "exportDce":
327
+ this.recordFinalExportDceDecision(decision, reason);
328
+ return;
329
+ case "fieldFact":
330
+ this.recordFinalFieldFactDecision(decision, reason);
331
+ return;
332
+ case "aggregateSpecialization":
333
+ this.recordFinalAggregateSpecializationDecision(decision, reason);
334
+ return;
335
+ case "functionRewrite":
336
+ this.recordFinalFunctionRewriteDecision(decision, reason);
337
+ }
338
+ }
339
+ parseOnce() {
340
+ this.progress?.addSteps(11);
341
+ this.progress?.startStep("Load and parse modules");
50
342
  this.link();
343
+ this.progress?.tick();
344
+ // #85 consumes #84 function-valued field facts before field DCE can remove
345
+ // callback storage. The field pass then sees specialized calls and includes
346
+ // storage/wrapper cleanup in the same final-output trial.
347
+ this.progress?.startStep("Rewrite functions");
348
+ this.rewriteFunctionsAll();
349
+ this.progress?.tick();
350
+ this.progress?.startStep("Analyze and rewrite fields");
351
+ this.rewriteWholeProgramFieldsAll();
352
+ this.progress?.tick();
353
+ this.progress?.startStep("Fold constants");
354
+ this.foldConstantsAll();
355
+ this.progress?.tick();
356
+ this.progress?.startStep("Remove unused exports");
357
+ this.rewriteWholeProgramExportsAll();
358
+ this.progress?.tick();
359
+ this.progress?.startStep("Remove unused code");
360
+ this.removeUnusedAll();
361
+ this.progress?.tick();
362
+ this.progress?.startStep("Plan global names");
363
+ this.rebuildIdentifiersInUse();
51
364
  this.computeGlobalRenames();
365
+ this.progress?.tick();
366
+ this.progress?.startStep("Transform statements");
52
367
  this.transformAll();
53
- this.renameAll();
54
- const parts = [];
55
- const entryComments = this.moduleAST.get(this.entryModule)?.comments;
56
- if (entryComments) {
57
- entryComments
58
- .filter((v) => v.raw.includes("--#") || v.raw.includes("[[#"))
59
- .forEach((comment) => {
60
- parts.push(new source_map_1.SourceNode(comment.loc?.start.line ?? null, comment.loc?.start.column ?? null, this.moduleNameAndFileName.get(this.entryModule) ?? null, comment.raw), "\n");
368
+ this.progress?.tick();
369
+ this.progress?.startStep("Finalize whole-program analyses");
370
+ if (this.functionRewritesEnabled() ||
371
+ this.fieldFactsEnabled() ||
372
+ this.fieldRenamesEnabled()) {
373
+ // fold/remove/schedule may have changed any module after method rewrites. Publish only a
374
+ // snapshot rebuilt from the complete linked AST generation consumed by final Rename/Print.
375
+ this.linkedAstGeneration++;
376
+ this.wholeProgramObjectsValue = this.analyzeWholeProgramObjects();
377
+ this.wholeProgramFieldsValue = (0, wholeProgramFields_1.analyzeWholeProgramFields)(this.wholeProgramObjectsValue, {
378
+ trustAnnotations: this.mode.assumeAnnotations === true,
379
+ metadataOf: (moduleName) => this.getSourceMetadata(moduleName),
61
380
  });
381
+ this.wholeProgramExportsValue = (0, wholeProgramExports_1.analyzeWholeProgramExports)(this.wholeProgramObjectsValue, this.entryModule, (moduleName) => this.getSourceMetadata(moduleName));
382
+ if (this.fieldRenameVariant === "trial") {
383
+ this.wholeProgramFieldRenamesValue = (0, wholeProgramFieldRenames_1.planWholeProgramFieldRenames)(this.wholeProgramObjectsValue, this.wholeProgramFieldsValue, this.wholeProgramExportsValue, this.entryModule, (moduleName) => this.getSourceMetadata(moduleName));
384
+ this.recordWholeProgramFieldRenameDiagnostics(this.wholeProgramFieldRenamesValue);
385
+ }
62
386
  }
63
- if (this.mode.moduleLikeLua) {
64
- parts.push(this.buildRequireWrapper());
65
- }
66
- parts.push(this.printModule(this.entryModule));
67
- const result = new source_map_1.SourceNode(null, null, null, parts);
387
+ this.progress?.tick();
388
+ this.progress?.startStep("Rename identifiers");
389
+ this.renameAll();
390
+ this.progress?.tick();
391
+ this.progress?.startStep("Generate Lua and source map");
392
+ const result = this.mode.requireWrapper
393
+ ? this.printModuleWithRequireWrapper()
394
+ : this.printModule(this.entryModule);
68
395
  this.moduleSourceText.forEach((v, k) => {
69
396
  const fileName = this.moduleNameAndFileName.get(k);
70
397
  if (fileName) {
71
398
  result.setSourceContent(fileName, v);
72
399
  }
400
+ this.progress?.tick();
73
401
  });
74
402
  return result;
75
403
  }
404
+ rewriteWholeProgramExportsAll() {
405
+ if (this.exportDceVariant !== "trial" || !this.exportDceEnabled())
406
+ return;
407
+ const objectAnalysis = this.analyzeWholeProgramObjects();
408
+ const exportAnalysis = (0, wholeProgramExports_1.analyzeWholeProgramExports)(objectAnalysis, this.entryModule, (moduleName) => this.getSourceMetadata(moduleName));
409
+ this.wholeProgramObjectsValue = objectAnalysis;
410
+ this.wholeProgramExportsValue = exportAnalysis;
411
+ exportAnalysis.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
412
+ pass: "whole-program-export-reachability",
413
+ moduleName: diagnostic.moduleName,
414
+ fieldName: diagnostic.field,
415
+ runtimeProfile: this.mode.runtimeProfile,
416
+ decision: diagnostic.reason === "export-field-candidate" ||
417
+ diagnostic.reason === "field-live" ||
418
+ diagnostic.reason === "field-unreachable"
419
+ ? "accepted"
420
+ : "rejected",
421
+ reason: diagnostic.reason,
422
+ candidateSize: 1,
423
+ sourceRange: diagnostic.sourceRange,
424
+ }));
425
+ const analysisByModule = new Map(objectAnalysis.modules.map((module) => [module.name, module.analysis]));
426
+ const result = (0, wholeProgramExports_1.applyWholeProgramExportDce)(exportAnalysis, (moduleName) => this.getSourceMetadata(moduleName), (moduleName, expression) => analysisByModule.get(moduleName)?.facts.discardabilityOf(expression)
427
+ .discardable === true);
428
+ result.refusedEffectfulInitializerFields.forEach((field) => this.diagnosticCollector?.record({
429
+ pass: "whole-program-export-dce",
430
+ moduleName: field.moduleName,
431
+ fieldName: field.key,
432
+ runtimeProfile: this.mode.runtimeProfile,
433
+ decision: "rejected",
434
+ reason: "effectful-initializer",
435
+ candidateSize: 1,
436
+ }));
437
+ if (!result.changed)
438
+ return;
439
+ this.linkOrder.forEach((moduleName) => {
440
+ const ast = this.moduleAST.get(moduleName);
441
+ if (!ast)
442
+ throw new Error(moduleName + " is not found");
443
+ this.moduleResolve.set(moduleName, (0, resolver_1.resolveScopes)(ast));
444
+ });
445
+ this.linkedAstGeneration++;
446
+ this.wholeProgramObjectsValue = undefined;
447
+ this.wholeProgramFieldsValue = undefined;
448
+ this.wholeProgramExportsValue = undefined;
449
+ result.removedFields.forEach((field) => this.diagnosticCollector?.record({
450
+ pass: "whole-program-export-dce",
451
+ moduleName: field.moduleName,
452
+ fieldName: field.key,
453
+ runtimeProfile: this.mode.runtimeProfile,
454
+ decision: "accepted",
455
+ reason: "field-removed",
456
+ candidateSize: 1,
457
+ }));
458
+ result.preservedEffectFields.forEach((field) => this.diagnosticCollector?.record({
459
+ pass: "whole-program-export-dce",
460
+ moduleName: field.moduleName,
461
+ fieldName: field.key,
462
+ runtimeProfile: this.mode.runtimeProfile,
463
+ decision: "accepted",
464
+ reason: "field-effect-preserved",
465
+ candidateSize: 1,
466
+ }));
467
+ }
468
+ rewriteWholeProgramFieldsAll() {
469
+ if (this.fieldFactVariant === "baseline" || !this.fieldFactsEnabled())
470
+ return;
471
+ const objectAnalysis = this.analyzeWholeProgramObjects();
472
+ const fieldAnalysis = (0, wholeProgramFields_1.analyzeWholeProgramFields)(objectAnalysis, {
473
+ trustAnnotations: this.mode.assumeAnnotations === true,
474
+ metadataOf: (moduleName) => this.getSourceMetadata(moduleName),
475
+ });
476
+ this.wholeProgramObjectsValue = objectAnalysis;
477
+ this.wholeProgramFieldsValue = fieldAnalysis;
478
+ fieldAnalysis.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
479
+ pass: "whole-program-constructor-fields",
480
+ moduleName: diagnostic.moduleName,
481
+ runtimeProfile: this.mode.runtimeProfile,
482
+ decision: diagnostic.reason === "field-fact" ? "accepted" : "rejected",
483
+ reason: diagnostic.reason,
484
+ candidateSize: 1,
485
+ sourceRange: diagnostic.sourceRange,
486
+ }));
487
+ const result = (0, wholeProgramFields_1.applyWholeProgramFieldRewrites)(objectAnalysis, fieldAnalysis, (moduleName) => this.getSourceMetadata(moduleName), {
488
+ replaceReads: this.mode.fieldValuePropagation,
489
+ removeInitializers: this.mode.unusedFieldInitializerRemoval,
490
+ });
491
+ if (!result.changed)
492
+ return;
493
+ this.linkOrder.forEach((moduleName) => {
494
+ const ast = this.moduleAST.get(moduleName);
495
+ if (!ast)
496
+ throw new Error(moduleName + " is not found");
497
+ const passes = new optimizerPass_1.PassOrchestrator(ast, (0, resolver_1.resolveScopes)(ast));
498
+ passes.runUntilStable("fold-constructor-field-constants", (resolved) => {
499
+ const facts = passes.analysis(optimizerFacts_1.OPTIMIZER_FACTS_CACHE_KEY, optimizerFacts_1.analyzeOptimizerFactsAtGeneration);
500
+ const changed = (0, constantFold_1.foldConstants)(ast, resolved, this.getSourceMetadata(moduleName), facts, {
501
+ evaluateExpressions: this.mode.constantExpressionEvaluation,
502
+ propagateLocals: this.mode.localConstantPropagation,
503
+ });
504
+ return { changed, invalidatesResolve: changed };
505
+ });
506
+ this.moduleResolve.set(moduleName, passes.resolved);
507
+ });
508
+ this.linkedAstGeneration++;
509
+ this.wholeProgramObjectsValue = undefined;
510
+ this.wholeProgramFieldsValue = undefined;
511
+ this.wholeProgramExportsValue = undefined;
512
+ this.diagnosticCollector?.record({
513
+ pass: "whole-program-constructor-field-rewrite",
514
+ moduleName: this.entryModule,
515
+ runtimeProfile: this.mode.runtimeProfile,
516
+ decision: "accepted",
517
+ reason: "field-rewrite-applied",
518
+ candidateSize: result.replacedReads + result.removedInitializers,
519
+ });
520
+ const recordRewrite = (reason, count) => {
521
+ if (count === 0)
522
+ return;
523
+ this.diagnosticCollector?.record({
524
+ pass: "whole-program-constructor-field-rewrite",
525
+ moduleName: this.entryModule,
526
+ runtimeProfile: this.mode.runtimeProfile,
527
+ decision: "accepted",
528
+ reason,
529
+ candidateSize: count,
530
+ });
531
+ };
532
+ recordRewrite("field-read-replaced", result.replacedReads);
533
+ recordRewrite("dead-field-write", result.removedInitializers);
534
+ recordRewrite("field-write-effect-preserved", result.preservedEffects);
535
+ }
536
+ /** Function-summary consumers run before scheduling and final rename/print. */
537
+ rewriteFunctionsAll() {
538
+ if (this.functionRewriteVariant === "baseline" ||
539
+ !this.functionRewritesEnabled())
540
+ return;
541
+ let initialWholeProgram = this.analyzeWholeProgramObjects();
542
+ this.recordWholeProgramObjectDiagnostics(initialWholeProgram);
543
+ const initiallyResolvedMethodDeclarations = new Set(initialWholeProgram.resolvedMethods.map((method) => method.target.declaration));
544
+ if (this.mode.functionSpecialization &&
545
+ this.aggregateSpecializationVariant !== "baseline") {
546
+ const initialFieldFacts = (0, wholeProgramFields_1.analyzeWholeProgramFields)(initialWholeProgram, {
547
+ trustAnnotations: this.mode.assumeAnnotations === true,
548
+ metadataOf: (moduleName) => this.getSourceMetadata(moduleName),
549
+ });
550
+ const specialization = (0, aggregateSpecialization_1.applyAggregateSpecialization)(initialWholeProgram, initialFieldFacts, this.linkOrder.map((name) => {
551
+ const chunk = this.moduleAST.get(name);
552
+ const resolved = this.moduleResolve.get(name);
553
+ if (!chunk || !resolved)
554
+ throw new Error(name + " is not found");
555
+ const resources = (0, runtimeEnvironment_1.analyzeLocalResourceUsage)(chunk);
556
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
557
+ return {
558
+ name,
559
+ chunk,
560
+ resolved,
561
+ metadata: this.getSourceMetadata(name),
562
+ maxIntroducedLocalsAt: (statement) => {
563
+ const active = resources.activeLocalsBefore(statement);
564
+ if (active === undefined)
565
+ return 0;
566
+ return Math.max(0, Math.min(runtime.resources.maxActiveLocalsPerFunction - active, runtime.resources.maxRegistersPerFunction - active));
567
+ },
568
+ };
569
+ }));
570
+ specialization.diagnostics.forEach((diagnostic) => {
571
+ const module = initialWholeProgram.modules.find((candidate) => candidate.analysis.callGraph.functions.includes(diagnostic.callable));
572
+ this.diagnosticCollector?.record({
573
+ pass: "aggregate-function-specialization",
574
+ moduleName: module?.name,
575
+ runtimeProfile: this.mode.runtimeProfile,
576
+ decision: diagnostic.reason === "variant-created" ? "accepted" : "rejected",
577
+ reason: diagnostic.reason,
578
+ candidateSize: diagnostic.count,
579
+ sourceRange: sourceRangeOf(diagnostic.callable.declaration),
580
+ });
581
+ });
582
+ if (specialization.changed) {
583
+ this.linkOrder.forEach((moduleName) => {
584
+ const ast = this.moduleAST.get(moduleName);
585
+ if (!ast)
586
+ throw new Error(moduleName + " is not found");
587
+ this.moduleResolve.set(moduleName, (0, resolver_1.resolveScopes)(ast));
588
+ });
589
+ this.linkedAstGeneration++;
590
+ this.wholeProgramObjectsValue = undefined;
591
+ this.wholeProgramFieldsValue = undefined;
592
+ this.wholeProgramExportsValue = undefined;
593
+ initialWholeProgram = this.analyzeWholeProgramObjects();
594
+ const specializedFields = (0, wholeProgramFields_1.analyzeWholeProgramFields)(initialWholeProgram, {
595
+ trustAnnotations: this.mode.assumeAnnotations === true,
596
+ metadataOf: (moduleName) => this.getSourceMetadata(moduleName),
597
+ });
598
+ const downstream = (0, wholeProgramFields_1.applyWholeProgramFieldRewrites)(initialWholeProgram, specializedFields, (moduleName) => this.getSourceMetadata(moduleName), {
599
+ replaceReads: this.mode.fieldValuePropagation,
600
+ removeInitializers: this.mode.unusedFieldInitializerRemoval,
601
+ });
602
+ if (downstream.removedInitializers > 0)
603
+ this.diagnosticCollector?.record({
604
+ pass: "aggregate-function-specialization",
605
+ moduleName: this.entryModule,
606
+ runtimeProfile: this.mode.runtimeProfile,
607
+ decision: "accepted",
608
+ reason: "dead-field-write",
609
+ candidateSize: downstream.removedInitializers,
610
+ });
611
+ if (downstream.changed) {
612
+ this.linkOrder.forEach((moduleName) => {
613
+ const ast = this.moduleAST.get(moduleName);
614
+ if (!ast)
615
+ throw new Error(moduleName + " is not found");
616
+ this.moduleResolve.set(moduleName, (0, resolver_1.resolveScopes)(ast));
617
+ });
618
+ this.linkedAstGeneration++;
619
+ this.wholeProgramObjectsValue = undefined;
620
+ this.wholeProgramFieldsValue = undefined;
621
+ this.wholeProgramExportsValue = undefined;
622
+ initialWholeProgram = this.analyzeWholeProgramObjects();
623
+ }
624
+ }
625
+ }
626
+ const resolvedMethodDeclarations = new Set([
627
+ ...initiallyResolvedMethodDeclarations,
628
+ ...initialWholeProgram.resolvedMethods.map((method) => method.target.declaration),
629
+ ]);
630
+ if (this.mode.parameterPruning) {
631
+ initialWholeProgram = this.pruneWholeProgramParameters(initialWholeProgram, resolvedMethodDeclarations);
632
+ }
633
+ this.linkOrder.forEach((moduleName) => {
634
+ const ast = this.moduleAST.get(moduleName);
635
+ const resolved = this.moduleResolve.get(moduleName);
636
+ if (!ast || !resolved)
637
+ throw new Error(moduleName + " is not found");
638
+ const passes = new optimizerPass_1.PassOrchestrator(ast, resolved);
639
+ const runtimeProfile = this.mode.runtimeProfile;
640
+ const moduleRange = sourceRangeOf(ast) ??
641
+ [0, this.moduleSourceText.get(moduleName)?.length ?? 0];
642
+ const recordAccepted = (pass, count) => {
643
+ if (count === 0)
644
+ return;
645
+ this.diagnosticCollector?.record({
646
+ pass,
647
+ moduleName,
648
+ runtimeProfile,
649
+ decision: "accepted",
650
+ reason: "function-rewrite-applied",
651
+ candidateSize: count,
652
+ sourceRange: moduleRange,
653
+ });
654
+ };
655
+ const initialAnalysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
656
+ const recursive = new Set(initialAnalysis.callGraph.sccs
657
+ .filter((scc) => scc.recursive)
658
+ .flatMap((scc) => scc.functions));
659
+ initialAnalysis.interprocedural.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
660
+ pass: "interprocedural-summary",
661
+ moduleName,
662
+ runtimeProfile,
663
+ decision: diagnostic.reason === "unknown-call-target"
664
+ ? "rejected"
665
+ : "accepted",
666
+ reason: diagnostic.reason,
667
+ candidateSize: 1,
668
+ sourceRange: diagnostic.sourceRange ?? moduleRange,
669
+ }));
670
+ initialAnalysis.callGraph.functions.forEach((callable) => {
671
+ if (!callable.symbol || !callable.declaration.isLocal)
672
+ return;
673
+ const calls = initialAnalysis.callGraph.calls.filter((call) => call.targets.has(callable)).length;
674
+ const reason = recursive.has(callable)
675
+ ? "recursive-function"
676
+ : callable.declaration.parameters.some((parameter) => parameter.type === "VarargLiteral")
677
+ ? "vararg-function"
678
+ : callable.symbol.references.length > calls
679
+ ? "function-escape"
680
+ : undefined;
681
+ if (!reason)
682
+ return;
683
+ this.diagnosticCollector?.record({
684
+ pass: "function-rewrite",
685
+ moduleName,
686
+ runtimeProfile,
687
+ decision: "rejected",
688
+ reason,
689
+ candidateSize: 1,
690
+ sourceRange: sourceRangeOf(callable.declaration),
691
+ });
692
+ });
693
+ if (this.mode.functionInlining)
694
+ passes.run("inline-closed-single-use-functions", (currentResolve) => {
695
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
696
+ const result = (0, functionRewrites_1.inlineClosedSingleUseFunctions)(analysis.interprocedural, currentResolve, this.getSourceMetadata(moduleName));
697
+ recordAccepted("inline-closed-single-use-functions", result.inlinedFunctions);
698
+ return {
699
+ changed: result.changed,
700
+ invalidatesResolve: result.changed,
701
+ };
702
+ });
703
+ if (this.mode.functionInlining)
704
+ passes.run("inline-literal-argument-functions", (currentResolve) => {
705
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
706
+ const result = (0, functionRewrites_1.inlineLiteralArgumentFunctions)(analysis.interprocedural, currentResolve, this.getSourceMetadata(moduleName));
707
+ recordAccepted("inline-literal-argument-functions", result.inlinedFunctions);
708
+ return {
709
+ changed: result.changed,
710
+ invalidatesResolve: result.changed,
711
+ };
712
+ });
713
+ if (this.mode.functionInlining)
714
+ passes.run("inline-tail-call-functions", (currentResolve) => {
715
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
716
+ const localResources = (0, runtimeEnvironment_1.analyzeLocalResourceUsage)(ast);
717
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
718
+ const result = (0, functionRewrites_1.inlineTailCallFunctions)(ast, analysis.interprocedural, currentResolve, this.getSourceMetadata(moduleName), {
719
+ maxIntroducedLocalsAt: (statement) => {
720
+ const active = localResources.activeLocalsBefore(statement);
721
+ if (active === undefined)
722
+ return 0;
723
+ return Math.max(0, Math.min(runtime.resources.maxActiveLocalsPerFunction - active, runtime.resources.maxRegistersPerFunction - active));
724
+ },
725
+ });
726
+ recordAccepted("inline-tail-call-functions", result.inlinedFunctions);
727
+ return {
728
+ changed: result.changed,
729
+ invalidatesResolve: result.changed,
730
+ };
731
+ });
732
+ if (this.mode.functionInlining)
733
+ passes.run("inline-closed-statement-functions", (currentResolve) => {
734
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
735
+ const result = (0, functionRewrites_1.inlineClosedStatementFunctions)(ast, analysis.interprocedural, currentResolve, this.getSourceMetadata(moduleName));
736
+ recordAccepted("inline-closed-statement-functions", result.inlinedFunctions);
737
+ return {
738
+ changed: result.changed,
739
+ invalidatesResolve: result.changed,
740
+ };
741
+ });
742
+ if (this.mode.functionInlining)
743
+ passes.run("inline-bound-statement-functions", (currentResolve) => {
744
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
745
+ const localResources = (0, runtimeEnvironment_1.analyzeLocalResourceUsage)(ast);
746
+ const result = (0, functionRewrites_1.inlineBoundStatementFunctions)(ast, analysis.interprocedural, currentResolve, this.getSourceMetadata(moduleName), {
747
+ maxIntroducedLocalsAt: (statement) => {
748
+ const active = localResources.activeLocalsBefore(statement);
749
+ if (active === undefined)
750
+ return 0;
751
+ return Math.max(0, Math.min((0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile).resources
752
+ .maxActiveLocalsPerFunction - active, (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile).resources
753
+ .maxRegistersPerFunction - active));
754
+ },
755
+ });
756
+ recordAccepted("inline-bound-statement-functions", result.inlinedFunctions);
757
+ return {
758
+ changed: result.changed,
759
+ invalidatesResolve: result.changed,
760
+ };
761
+ });
762
+ this.moduleResolve.set(moduleName, passes.resolved);
763
+ if (passes.astGeneration > 0) {
764
+ this.linkedAstGeneration += passes.astGeneration;
765
+ this.wholeProgramObjectsValue = undefined;
766
+ this.wholeProgramFieldsValue = undefined;
767
+ this.wholeProgramExportsValue = undefined;
768
+ }
769
+ this.progress?.tick();
770
+ });
771
+ this.wholeProgramObjectsValue = this.analyzeWholeProgramObjects();
772
+ this.recordWholeProgramObjectDiagnostics(this.wholeProgramObjectsValue);
773
+ }
774
+ pruneWholeProgramParameters(analysis, resolvedMethodDeclarations) {
775
+ const functionsByModule = new Map(analysis.modules.map((module) => [
776
+ module.name,
777
+ new Set(module.analysis.callGraph.functions),
778
+ ]));
779
+ const changedModules = new Set();
780
+ this.linkOrder.forEach((moduleName) => {
781
+ const functions = functionsByModule.get(moduleName);
782
+ if (!functions)
783
+ return;
784
+ const result = (0, functionRewrites_1.pruneTrailingUnusedParameters)(analysis.callGraph, this.getSourceMetadata(moduleName), (callable) => functions.has(callable) &&
785
+ (callable.declaration.identifier?.type !== "MemberExpression" ||
786
+ callable.declaration.identifier.indexer !== ":" ||
787
+ resolvedMethodDeclarations.has(callable.declaration)));
788
+ if (!result.changed)
789
+ return;
790
+ changedModules.add(moduleName);
791
+ this.diagnosticCollector?.record({
792
+ pass: "prune-trailing-unused-parameters",
793
+ moduleName,
794
+ runtimeProfile: this.mode.runtimeProfile,
795
+ decision: "accepted",
796
+ reason: "function-rewrite-applied",
797
+ candidateSize: result.prunedParameters,
798
+ sourceRange: sourceRangeOf(this.moduleAST.get(moduleName) ?? {}),
799
+ });
800
+ if (result.prunedMethodParameters > 0)
801
+ this.diagnosticCollector?.record({
802
+ pass: "whole-program-method-parameter-pruning",
803
+ moduleName,
804
+ runtimeProfile: this.mode.runtimeProfile,
805
+ decision: "accepted",
806
+ reason: "function-rewrite-applied",
807
+ candidateSize: result.prunedMethodParameters,
808
+ sourceRange: sourceRangeOf(this.moduleAST.get(moduleName) ?? {}),
809
+ });
810
+ });
811
+ if (changedModules.size === 0)
812
+ return analysis;
813
+ changedModules.forEach((moduleName) => {
814
+ const ast = this.moduleAST.get(moduleName);
815
+ if (!ast)
816
+ throw new Error(moduleName + " is not found");
817
+ this.moduleResolve.set(moduleName, (0, resolver_1.resolveScopes)(ast));
818
+ });
819
+ this.linkedAstGeneration++;
820
+ this.wholeProgramObjectsValue = undefined;
821
+ this.wholeProgramFieldsValue = undefined;
822
+ this.wholeProgramExportsValue = undefined;
823
+ return this.analyzeWholeProgramObjects();
824
+ }
825
+ analyzeWholeProgramObjects() {
826
+ const modules = this.linkOrder.map((name) => {
827
+ const chunk = this.moduleAST.get(name);
828
+ const resolved = this.moduleResolve.get(name);
829
+ if (!chunk || !resolved)
830
+ throw new Error(name + " is not found");
831
+ const module = {
832
+ name,
833
+ chunk,
834
+ resolved,
835
+ analysis: (0, optimizerAnalysis_1.analyzeOptimizer)(chunk, resolved, {
836
+ generation: this.linkedAstGeneration,
837
+ }),
838
+ };
839
+ this.progress?.tick();
840
+ return module;
841
+ });
842
+ return (0, wholeProgramObjects_1.analyzeWholeProgramObjects)(modules, this.linkedAstGeneration);
843
+ }
844
+ recordWholeProgramObjectDiagnostics(analysis) {
845
+ analysis.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
846
+ pass: "whole-program-method-resolution",
847
+ moduleName: diagnostic.moduleName,
848
+ runtimeProfile: this.mode.runtimeProfile,
849
+ decision: diagnostic.reason === "resolved-method-target"
850
+ ? "accepted"
851
+ : "rejected",
852
+ reason: diagnostic.reason,
853
+ candidateSize: 1,
854
+ sourceRange: diagnostic.sourceRange,
855
+ }));
856
+ }
857
+ requiresSchedulerSelection() {
858
+ if (this.mode.localDeclarationMerging)
859
+ return true;
860
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
861
+ const lifetimeAllowed = !runtime.semantics.debugLocalIntrospection ||
862
+ this.mode.allowIntrospectionChanges === true;
863
+ return (lifetimeAllowed &&
864
+ (this.mode.localDeclarationHoisting || this.mode.tableReadMerging));
865
+ }
866
+ functionRewritesEnabled() {
867
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
868
+ return ((this.mode.parameterPruning ||
869
+ this.mode.functionInlining ||
870
+ this.mode.functionSpecialization) &&
871
+ (!runtime.semantics.debugLocalIntrospection ||
872
+ this.mode.allowIntrospectionChanges === true));
873
+ }
874
+ functionSpecializationEnabled() {
875
+ if (!this.mode.functionSpecialization)
876
+ return false;
877
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
878
+ return (!runtime.semantics.debugLocalIntrospection ||
879
+ this.mode.allowIntrospectionChanges === true);
880
+ }
881
+ fieldFactsEnabled() {
882
+ return (this.mode.fieldValuePropagation || this.mode.unusedFieldInitializerRemoval);
883
+ }
884
+ fieldRenamesEnabled() {
885
+ return this.mode.fieldRenaming;
886
+ }
887
+ exportDceEnabled() {
888
+ return this.mode.unusedExportRemoval;
889
+ }
890
+ copyDiagnosticsFrom(minifier) {
891
+ minifier.optimizationDiagnostics.forEach((diagnostic) => this.diagnosticCollector?.record(diagnostic));
892
+ }
893
+ adoptVariant(minifier, output) {
894
+ this.identifiersInUse.clear();
895
+ minifier.identifiersInUse.forEach((name) => this.identifiersInUse.add(name));
896
+ copyMap(minifier.moduleSourceText, this.moduleSourceText);
897
+ copyMap(minifier.moduleAST, this.moduleAST);
898
+ copyMap(minifier.moduleNameAndFileName, this.moduleNameAndFileName);
899
+ this.linkOrder.splice(0, this.linkOrder.length, ...minifier.linkOrder);
900
+ copyMap(minifier.moduleResolve, this.moduleResolve);
901
+ copyMap(minifier.renameCache, this.renameCache);
902
+ this.globalRenames = new Map(minifier.globalRenames);
903
+ copyMap(minifier.moduleMetadata, this.moduleMetadata);
904
+ this.linkedAstGeneration = minifier.linkedAstGeneration;
905
+ this.wholeProgramObjectsValue = minifier.wholeProgramObjectsValue;
906
+ this.wholeProgramFieldsValue = minifier.wholeProgramFieldsValue;
907
+ this.wholeProgramExportsValue = minifier.wholeProgramExportsValue;
908
+ this.wholeProgramFieldRenamesValue = minifier.wholeProgramFieldRenamesValue;
909
+ return output;
910
+ }
911
+ recordFinalSchedulerDecision(decision, reason, byteSavings) {
912
+ this.diagnosticCollector?.record({
913
+ pass: "statement-scheduler-final-cost",
914
+ decision,
915
+ reason,
916
+ candidateSize: 1,
917
+ estimatedByteSavings: byteSavings,
918
+ runtimeProfile: this.mode.runtimeProfile,
919
+ moduleName: this.entryModule,
920
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
921
+ });
922
+ }
923
+ recordFinalCostDecision(decision, reason, byteSavings) {
924
+ this.diagnosticCollector?.record({
925
+ pass: "optimizer-final-cost",
926
+ decision,
927
+ reason,
928
+ candidateSize: 1,
929
+ estimatedByteSavings: byteSavings,
930
+ runtimeProfile: this.mode.runtimeProfile,
931
+ moduleName: this.entryModule,
932
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
933
+ });
934
+ }
935
+ recordFinalFunctionRewriteDecision(decision, reason, byteSavings) {
936
+ this.diagnosticCollector?.record({
937
+ pass: "function-rewrite-final-cost",
938
+ decision,
939
+ reason,
940
+ candidateSize: 1,
941
+ estimatedByteSavings: byteSavings,
942
+ runtimeProfile: this.mode.runtimeProfile,
943
+ moduleName: this.entryModule,
944
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
945
+ });
946
+ }
947
+ recordFinalAggregateSpecializationDecision(decision, reason, byteSavings) {
948
+ this.diagnosticCollector?.record({
949
+ pass: "aggregate-specialization-final-cost",
950
+ decision,
951
+ reason,
952
+ candidateSize: 1,
953
+ estimatedByteSavings: byteSavings,
954
+ runtimeProfile: this.mode.runtimeProfile,
955
+ moduleName: this.entryModule,
956
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
957
+ });
958
+ }
959
+ recordFinalFieldFactDecision(decision, reason, byteSavings) {
960
+ this.diagnosticCollector?.record({
961
+ pass: "constructor-field-final-cost",
962
+ decision,
963
+ reason,
964
+ candidateSize: 1,
965
+ estimatedByteSavings: byteSavings,
966
+ runtimeProfile: this.mode.runtimeProfile,
967
+ moduleName: this.entryModule,
968
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
969
+ });
970
+ }
971
+ recordFinalExportDceDecision(decision, reason, byteSavings) {
972
+ this.diagnosticCollector?.record({
973
+ pass: "module-export-dce-final-cost",
974
+ decision,
975
+ reason,
976
+ candidateSize: 1,
977
+ estimatedByteSavings: byteSavings,
978
+ runtimeProfile: this.mode.runtimeProfile,
979
+ moduleName: this.entryModule,
980
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
981
+ });
982
+ }
983
+ recordFinalFieldRenameDecision(decision, reason, byteSavings) {
984
+ this.diagnosticCollector?.record({
985
+ pass: "whole-program-field-rename-final-cost",
986
+ decision,
987
+ reason,
988
+ candidateSize: 1,
989
+ estimatedByteSavings: byteSavings,
990
+ runtimeProfile: this.mode.runtimeProfile,
991
+ moduleName: this.entryModule,
992
+ sourceRange: [0, fs_1.default.readFileSync(this.entryFilePath, "utf8").length],
993
+ });
994
+ }
995
+ recordWholeProgramFieldRenameDiagnostics(plan) {
996
+ plan.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
997
+ pass: "whole-program-field-rename",
998
+ moduleName: diagnostic.moduleName,
999
+ fieldName: diagnostic.field,
1000
+ runtimeProfile: this.mode.runtimeProfile,
1001
+ decision: diagnostic.accepted ? "accepted" : "rejected",
1002
+ reason: diagnostic.reason,
1003
+ candidateSize: 1,
1004
+ sourceRange: diagnostic.sourceRange,
1005
+ }));
1006
+ }
76
1007
  /**
77
1008
  * dofileの呼び出し箇所ごとに、キャッシュ済みASTから新規にSourceNodeを作り直す。
78
1009
  * 同じSourceNodeインスタンスを複数箇所へ挿入すると壊れるため、常に作り直す(#18)。
@@ -91,14 +1022,14 @@ class Minifier {
91
1022
  if (!ast || !fileName) {
92
1023
  throw new Error(moduleName + " is not found");
93
1024
  }
94
- return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parseAsStatementsAndFinalExpression(moduleName === this.entryModule);
1025
+ return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parseAsStatementsAndFinalExpression();
95
1026
  }
96
1027
  /**
97
1028
  * 指定モジュールのRenameパス結果を返す。`renameAll`で事前に計算済みの
98
1029
  * ものをそのまま返すだけの参照用アクセサ。
99
1030
  */
100
1031
  getRenameResult(moduleName) {
101
- if (this.mode.rename === false) {
1032
+ if (!this.mode.localRenaming && !this.mode.globalRenaming) {
102
1033
  return NO_RENAME;
103
1034
  }
104
1035
  const cached = this.renameCache.get(moduleName);
@@ -107,6 +1038,18 @@ class Minifier {
107
1038
  }
108
1039
  return cached;
109
1040
  }
1041
+ getFieldRename(node) {
1042
+ const name = this.wholeProgramFieldRenamesValue?.nameOf(node);
1043
+ const originalName = this.wholeProgramFieldRenamesValue?.originalNameOf(node);
1044
+ return name && originalName ? { name, originalName } : undefined;
1045
+ }
1046
+ getSourceMetadata(moduleName) {
1047
+ const metadata = this.moduleMetadata.get(moduleName);
1048
+ if (!metadata) {
1049
+ throw new Error(moduleName + " is not found");
1050
+ }
1051
+ return metadata;
1052
+ }
110
1053
  /**
111
1054
  * Renameパス(#20): linkOrder(依存されている側が先)の順にモジュールごとの
112
1055
  * 短縮名を割り当てる。
@@ -120,17 +1063,25 @@ class Minifier {
120
1063
  * 犠牲になるが、モジュール内でのスコープに基づく再利用は維持される)。
121
1064
  */
122
1065
  renameAll() {
123
- if (this.mode.rename === false) {
1066
+ if (!this.mode.localRenaming && !this.mode.globalRenaming) {
124
1067
  return;
125
1068
  }
126
1069
  this.linkOrder.forEach((moduleName) => {
1070
+ const ast = this.moduleAST.get(moduleName);
127
1071
  const resolved = this.moduleResolve.get(moduleName);
128
- if (!resolved) {
1072
+ if (!ast || !resolved) {
129
1073
  throw new Error(moduleName + " is not found");
130
1074
  }
131
- const result = (0, renamer_1.assignRenames)(resolved, this.identifiersInUse, this.globalRenames);
1075
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
1076
+ const result = this.renameCache.get(moduleName) ??
1077
+ (0, renamer_1.assignRenames)(ast, resolved, this.identifiersInUse, this.globalRenames, new Set(resolved.symbols.filter((symbol) => this.getSourceMetadata(moduleName).annotationsOfIdentifier(symbol.declaration).keepName)), {
1078
+ allowLocalNameReuse: !runtime.semantics.debugLocalIntrospection ||
1079
+ this.mode.allowIntrospectionChanges === true,
1080
+ renameLocals: this.mode.localRenaming,
1081
+ });
132
1082
  this.renameCache.set(moduleName, result);
133
1083
  result.usedNames.forEach((name) => this.identifiersInUse.add(name));
1084
+ this.progress?.tick();
134
1085
  });
135
1086
  }
136
1087
  /**
@@ -152,31 +1103,149 @@ class Minifier {
152
1103
  * なり)、意味が壊れる(要修正が発覚した実例)。
153
1104
  */
154
1105
  transformAll() {
1106
+ // Global bindings are shared across linked modules. A name written in one
1107
+ // module is therefore program-owned even when another module only reads it;
1108
+ // treating that reader as an external-global alias candidate can capture
1109
+ // the value before the defining module runs (#102).
1110
+ const programWrittenGlobals = (0, globalRename_1.collectProgramWrittenGlobals)(this.moduleResolve);
155
1111
  // globalRenames.keys()は8aが実際にリネームした(=代入もされていた)名前のみ。
156
1112
  // neverRenameGlobalsは代入されていない名前にも及ぶ保護指定なので、8bのエイリアス化
157
1113
  // が誤ってそれらを書き換えてしまわないよう、必ず両方をあわせてexcludeNamesに渡す。
158
1114
  const excludeGlobalNames = new Set([
1115
+ ...programWrittenGlobals,
159
1116
  ...this.globalRenames.keys(),
160
1117
  ...(this.mode.neverRenameGlobals ?? []),
1118
+ ...this.annotationProtectedGlobals(),
161
1119
  ]);
1120
+ // renameAllと同じmodule順・予約名更新で仮Renameを行い、plannerのbyte costを
1121
+ // 実際の出力名長に対する保守的な見積りにする。
1122
+ const plannedIdentifiersInUse = new Set(this.identifiersInUse);
162
1123
  this.linkOrder.forEach((moduleName) => {
163
1124
  const ast = this.moduleAST.get(moduleName);
164
1125
  let resolved = this.moduleResolve.get(moduleName);
165
1126
  if (!ast || !resolved) {
166
1127
  throw new Error(moduleName + " is not found");
167
1128
  }
168
- if (this.mode.rename !== false && this.mode.globalAlias !== false) {
169
- (0, transform_1.insertGlobalAliases)(ast, resolved, {
170
- excludeNames: excludeGlobalNames,
1129
+ const passes = new optimizerPass_1.PassOrchestrator(ast, resolved);
1130
+ if (this.mode.globalAliasing) {
1131
+ passes.run("global-alias", (currentResolve) => {
1132
+ const changed = (0, transform_1.insertGlobalAliases)(ast, currentResolve, {
1133
+ excludeNames: excludeGlobalNames,
1134
+ });
1135
+ return { changed, invalidatesResolve: changed };
171
1136
  });
172
- resolved = (0, resolver_1.resolveScopes)(ast);
173
1137
  }
174
- if (this.mode.mergeLocals !== false) {
175
- (0, transform_1.mergeLocalDeclarations)(ast, resolved, {
176
- preserveRequireSplice: !this.mode.moduleLikeLua,
1138
+ resolved = passes.resolved;
1139
+ const runtime = (0, runtimeEnvironment_1.runtimeEnvironmentOf)(this.mode.runtimeProfile);
1140
+ const optimizerAnalysis = () => passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, (chunk, currentResolve, generation) => (0, optimizerAnalysis_1.analyzeOptimizer)(chunk, currentResolve, {
1141
+ generation,
1142
+ runtime,
1143
+ assumptions: this.mode.allowObservableTableReadChanges === true
1144
+ ? new Map([
1145
+ [
1146
+ "allow-observable-table-read-changes",
1147
+ "explicit allowObservableTableReadChanges opt-in",
1148
+ ],
1149
+ ])
1150
+ : undefined,
1151
+ }));
1152
+ const localResources = (0, runtimeEnvironment_1.analyzeLocalResourceUsage)(ast);
1153
+ const lifetimeChangesAllowed = !runtime.semantics.debugLocalIntrospection ||
1154
+ this.mode.allowIntrospectionChanges === true;
1155
+ const localNameReuseEnabled = this.mode.localNameReuse && lifetimeChangesAllowed;
1156
+ const keepNames = new Set(resolved.symbols.filter((symbol) => this.getSourceMetadata(moduleName).annotationsOfIdentifier(symbol.declaration).keepName));
1157
+ if (this.schedulerVariant !== "baseline" &&
1158
+ (this.mode.localDeclarationMerging ||
1159
+ (lifetimeChangesAllowed &&
1160
+ (this.mode.localDeclarationHoisting || this.mode.tableReadMerging)))) {
1161
+ const provisionalAnalysis = !this.mode.localRenaming && !this.mode.globalRenaming
1162
+ ? undefined
1163
+ : optimizerAnalysis();
1164
+ const provisionalRenames = !this.mode.localRenaming && !this.mode.globalRenaming
1165
+ ? NO_RENAME
1166
+ : (0, renamer_1.assignRenames)(ast, resolved, plannedIdentifiersInUse, this.globalRenames, keepNames, {
1167
+ allowLocalNameReuse: localNameReuseEnabled,
1168
+ renameLocals: this.mode.localRenaming,
1169
+ analysis: provisionalAnalysis
1170
+ ? {
1171
+ facts: provisionalAnalysis.facts,
1172
+ liveness: provisionalAnalysis.statementDataflow.symbolLiveness,
1173
+ }
1174
+ : undefined,
1175
+ });
1176
+ passes.run("statement-scheduler", (currentResolve) => {
1177
+ const analysis = optimizerAnalysis();
1178
+ analysis.interprocedural.diagnostics.forEach((diagnostic) => this.diagnosticCollector?.record({
1179
+ pass: "interprocedural-summary",
1180
+ moduleName,
1181
+ runtimeProfile: runtime.profile,
1182
+ decision: diagnostic.reason === "unknown-call-target"
1183
+ ? "rejected"
1184
+ : "accepted",
1185
+ reason: diagnostic.reason,
1186
+ candidateSize: 1,
1187
+ sourceRange: diagnostic.sourceRange,
1188
+ }));
1189
+ const metadata = this.getSourceMetadata(moduleName);
1190
+ const canMoveAnnotatedStatement = (statement) => {
1191
+ const annotations = metadata.annotationsOf(statement);
1192
+ return (metadata.beforeOf(statement).length === 0 &&
1193
+ metadata.trailingOf(statement).length === 0 &&
1194
+ !annotations.keep &&
1195
+ !annotations.keepName &&
1196
+ !annotations.exported);
1197
+ };
1198
+ const plan = (0, statementScheduler_1.planStatementSchedule)(ast, currentResolve, {
1199
+ facts: analysis.facts,
1200
+ dataflow: analysis.statementDataflow,
1201
+ outputNameLengthOf: (symbol) => (provisionalRenames.nameOf(symbol.declaration) ?? symbol.name)
1202
+ .length,
1203
+ preserveRequireSplice: !this.mode.requireWrapper,
1204
+ enableLocalPacking: lifetimeChangesAllowed && this.mode.localDeclarationHoisting,
1205
+ enableLexicalLocalMerge: this.mode.localDeclarationMerging,
1206
+ tableEffects: lifetimeChangesAllowed && this.mode.tableReadMerging
1207
+ ? analysis.tableEffects
1208
+ : undefined,
1209
+ dirtyGranularity: !this.mode.fieldSensitiveTableEffects
1210
+ ? "table"
1211
+ : "static-key",
1212
+ allowObservableTableValueChanges: this.mode.allowObservableTableReadChanges === true,
1213
+ maxTableMergeArity: runtime.resources.conservativeParallelValueLimit,
1214
+ maxTableMergeArityAt: (statement) => (0, runtimeEnvironment_1.checkParallelEvaluation)(runtime, {
1215
+ activeLocalsBefore: localResources.activeLocalsBefore(statement) ??
1216
+ runtime.resources.maxActiveLocalsPerFunction,
1217
+ parallelValueCount: runtime.resources.conservativeParallelValueLimit,
1218
+ }).limit,
1219
+ canMoveTableRead: canMoveAnnotatedStatement,
1220
+ maxHoistedLocalsAt: (statement) => {
1221
+ const active = localResources.activeLocalsBefore(statement);
1222
+ if (active === undefined)
1223
+ return 0;
1224
+ return Math.max(0, Math.min(runtime.resources.maxActiveLocalsPerFunction - active, runtime.resources.maxRegistersPerFunction - active));
1225
+ },
1226
+ canChangeLocalLifetime: canMoveAnnotatedStatement,
1227
+ diagnostics: this.diagnosticCollector,
1228
+ moduleName,
1229
+ runtimeProfile: runtime.profile,
1230
+ });
1231
+ return (0, statementScheduler_1.applyStatementSchedule)(plan, this.getSourceMetadata(moduleName));
177
1232
  });
178
1233
  }
1234
+ resolved = passes.resolved;
179
1235
  this.moduleResolve.set(moduleName, resolved);
1236
+ if (this.mode.localRenaming || this.mode.globalRenaming) {
1237
+ const finalKeepNames = new Set(resolved.symbols.filter((symbol) => this.getSourceMetadata(moduleName).annotationsOfIdentifier(symbol.declaration).keepName));
1238
+ // plannedIdentifiersInUse advances in the same link order as renameAll.
1239
+ // Cache this final-generation result so Print does not rebuild the same
1240
+ // facts, CFG, liveness, graph, and binding proof a second time.
1241
+ const finalRename = (0, renamer_1.assignRenames)(ast, resolved, plannedIdentifiersInUse, this.globalRenames, finalKeepNames, {
1242
+ allowLocalNameReuse: localNameReuseEnabled,
1243
+ renameLocals: this.mode.localRenaming,
1244
+ });
1245
+ this.renameCache.set(moduleName, finalRename);
1246
+ finalRename.usedNames.forEach((name) => plannedIdentifiersInUse.add(name));
1247
+ }
1248
+ this.progress?.tick();
180
1249
  });
181
1250
  }
182
1251
  /**
@@ -191,10 +1260,13 @@ class Minifier {
191
1260
  * ローカルの短縮名がグローバルの新しい短縮名と衝突しうる。
192
1261
  */
193
1262
  computeGlobalRenames() {
194
- if (this.mode.rename === false || this.mode.globalRename === false) {
1263
+ if (!this.mode.globalRenaming) {
195
1264
  return;
196
1265
  }
197
- const neverRename = this.mode.neverRenameGlobals ?? new Set();
1266
+ const neverRename = new Set([
1267
+ ...(this.mode.neverRenameGlobals ?? []),
1268
+ ...this.annotationProtectedGlobals(),
1269
+ ]);
198
1270
  this.globalRenames = (0, globalRename_1.classifyAndRenameGlobals)(this.moduleResolve, neverRename, this.identifiersInUse);
199
1271
  this.globalRenames.forEach((shortName, originalName) => {
200
1272
  this.identifiersInUse.add(shortName);
@@ -207,7 +1279,92 @@ class Minifier {
207
1279
  if (!ast || !fileName) {
208
1280
  throw new Error(moduleName + " is not found");
209
1281
  }
210
- return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parse(moduleName === this.entryModule);
1282
+ return new ast2lua_1.MinifyFile(fileName, moduleName, ast, this, this.mode).parse();
1283
+ }
1284
+ annotationProtectedGlobals() {
1285
+ const protectedNames = new Set();
1286
+ this.moduleResolve.forEach((resolved, moduleName) => {
1287
+ const metadata = this.getSourceMetadata(moduleName);
1288
+ resolved.globals.forEach((binding) => {
1289
+ if (binding.writes.some((write) => metadata.annotationsOfIdentifier(write).keepName)) {
1290
+ protectedNames.add(binding.name);
1291
+ }
1292
+ });
1293
+ });
1294
+ return protectedNames;
1295
+ }
1296
+ /**
1297
+ * 定数畳み込みパス(#44): opt-inオプション。既定では無効。
1298
+ * link()の直後、removeUnusedAll()の前に実行する。伝搬で参照が消えたローカル
1299
+ * 宣言はこのパス自身では消さず、直後に実行される(既定で有効な)未使用ローカル
1300
+ * 削除に任せる。
1301
+ */
1302
+ foldConstantsAll() {
1303
+ if (!this.mode.constantExpressionEvaluation &&
1304
+ !this.mode.localConstantPropagation &&
1305
+ !this.mode.interproceduralConstantPropagation)
1306
+ return;
1307
+ this.linkOrder.forEach((moduleName) => {
1308
+ const ast = this.moduleAST.get(moduleName);
1309
+ const resolved = this.moduleResolve.get(moduleName);
1310
+ if (!ast || !resolved)
1311
+ throw new Error(moduleName + " is not found");
1312
+ const passes = new optimizerPass_1.PassOrchestrator(ast, resolved);
1313
+ if (this.mode.interproceduralConstantPropagation)
1314
+ passes.run("interprocedural-constants", () => {
1315
+ const analysis = passes.analysis(optimizerAnalysis_1.OPTIMIZER_ANALYSIS_CACHE_KEY, optimizerAnalysis_1.analyzeOptimizerAtGeneration);
1316
+ const changed = (0, interproceduralConstants_1.propagateInterproceduralConstants)(ast, analysis.interprocedural);
1317
+ return { changed, invalidatesResolve: changed };
1318
+ });
1319
+ if (this.mode.constantExpressionEvaluation ||
1320
+ this.mode.localConstantPropagation)
1321
+ passes.runUntilStable("fold-constants", (currentResolve) => {
1322
+ const facts = passes.analysis(optimizerFacts_1.OPTIMIZER_FACTS_CACHE_KEY, optimizerFacts_1.analyzeOptimizerFactsAtGeneration);
1323
+ const changed = (0, constantFold_1.foldConstants)(ast, currentResolve, this.getSourceMetadata(moduleName), facts, {
1324
+ evaluateExpressions: this.mode.constantExpressionEvaluation,
1325
+ propagateLocals: this.mode.localConstantPropagation,
1326
+ });
1327
+ return { changed, invalidatesResolve: changed };
1328
+ });
1329
+ this.moduleResolve.set(moduleName, passes.resolved);
1330
+ this.progress?.tick();
1331
+ });
1332
+ }
1333
+ removeUnusedAll() {
1334
+ if (!this.mode.unusedLocalRemoval && !this.mode.unusedFunctionRemoval)
1335
+ return;
1336
+ this.linkOrder.forEach((moduleName) => {
1337
+ const ast = this.moduleAST.get(moduleName);
1338
+ const metadata = this.getSourceMetadata(moduleName);
1339
+ const resolved = this.moduleResolve.get(moduleName);
1340
+ if (!ast || !resolved)
1341
+ throw new Error(moduleName + " is not found");
1342
+ const passes = new optimizerPass_1.PassOrchestrator(ast, resolved);
1343
+ passes.runUntilStable("remove-unused", (currentResolve) => {
1344
+ const facts = passes.analysis(optimizerFacts_1.OPTIMIZER_FACTS_CACHE_KEY, optimizerFacts_1.analyzeOptimizerFactsAtGeneration);
1345
+ const changed = (0, removeUnused_1.removeUnusedLocals)(ast, currentResolve, metadata, facts, (statement) => this.diagnosticCollector?.record({
1346
+ pass: "function-dce",
1347
+ moduleName,
1348
+ runtimeProfile: this.mode.runtimeProfile,
1349
+ decision: "accepted",
1350
+ reason: "unused-function",
1351
+ candidateSize: 1,
1352
+ sourceRange: sourceRangeOf(statement),
1353
+ }), {
1354
+ removeLocals: this.mode.unusedLocalRemoval,
1355
+ removeFunctions: this.mode.unusedFunctionRemoval,
1356
+ });
1357
+ return { changed, invalidatesResolve: changed };
1358
+ });
1359
+ this.moduleResolve.set(moduleName, passes.resolved);
1360
+ this.progress?.tick();
1361
+ });
1362
+ }
1363
+ rebuildIdentifiersInUse() {
1364
+ this.identifiersInUse.clear();
1365
+ this.moduleResolve.forEach((resolved) => {
1366
+ resolved.globals.forEach((binding) => this.identifiersInUse.add(binding.name));
1367
+ });
211
1368
  }
212
1369
  /**
213
1370
  * エントリファイルから到達可能な全モジュールをASTレベルで解決するLinkパス(#18)。
@@ -231,28 +1388,40 @@ class Minifier {
231
1388
  visiting.add(moduleName);
232
1389
  stack.push(moduleName);
233
1390
  const fullResolvePath = path_1.default.join(this.dir, ...moduleName.split(".")) + ".lua";
234
- if (!fs_1.default.existsSync(fullResolvePath)) {
235
- throw new Error(moduleName + " is not found");
1391
+ let input = this.inputCache.get(fullResolvePath);
1392
+ if (!input) {
1393
+ if (!fs_1.default.existsSync(fullResolvePath)) {
1394
+ throw new Error(moduleName + " is not found");
1395
+ }
1396
+ const sourceText = fs_1.default.readFileSync(fullResolvePath).toString();
1397
+ const ast = luaparse_1.default.parse(sourceText, this.luaParseSettings);
1398
+ input = {
1399
+ sourceText,
1400
+ ast,
1401
+ references: (0, linker_1.findModuleReferences)(ast),
1402
+ };
1403
+ this.inputCache.set(fullResolvePath, input);
236
1404
  }
237
- const code = fs_1.default.readFileSync(fullResolvePath).toString();
238
- const ast = luaparse_1.default.parse(code, this.luaParseSettings);
1405
+ const code = input.sourceText;
1406
+ const ast = structuredClone(input.ast);
1407
+ this.moduleMetadata.set(moduleName, new sourceMetadata_1.SourceMetadata(ast, code));
239
1408
  // Resolveパス(#19): このモジュールのスコープ/シンボルを解析し、Renameパスの
240
1409
  // 入力として使い回せるようキャッシュする。グローバル参照はプログラム全体で
241
1410
  // 予約すべき名前(identifiersInUse)としてここで集計する。
242
1411
  const resolved = (0, resolver_1.resolveScopes)(ast);
243
1412
  this.moduleResolve.set(moduleName, resolved);
244
- resolved.globals.forEach((binding) => this.identifiersInUse.add(binding.name));
245
1413
  this.moduleSourceText.set(moduleName, code);
246
1414
  this.moduleAST.set(moduleName, ast);
247
1415
  // Source Mapの`sources`はURLとして解釈されるため、OS依存のpath.sepではなく
248
1416
  // 常に"/"区切りで保持する(Windows上でのビルドでも壊れないように)。
249
1417
  this.moduleNameAndFileName.set(moduleName, moduleName.replaceAll(".", "/") + ".lua");
250
- (0, linker_1.findModuleReferences)(ast).forEach((ref) => {
1418
+ input.references.forEach((ref) => {
251
1419
  visit(ref.moduleName);
252
1420
  });
253
1421
  visiting.delete(moduleName);
254
1422
  stack.pop();
255
1423
  this.linkOrder.push(moduleName);
1424
+ this.progress?.tick();
256
1425
  };
257
1426
  visit(this.entryModule);
258
1427
  }
@@ -275,19 +1444,20 @@ class Minifier {
275
1444
  });
276
1445
  return targets;
277
1446
  }
278
- buildRequireWrapper() {
1447
+ printModuleWithRequireWrapper() {
279
1448
  const targets = this.collectRequireTargets();
280
- const parts = [
281
- "function require(m,r)package=package or{loaded={}};if package.loaded[m]then return package.loaded[m]end\n",
1449
+ const moduleNames = this.linkOrder.filter((moduleName) => moduleName !== this.entryModule && targets.has(moduleName));
1450
+ const wrapperAst = (0, generatedAst_1.buildRequireWrapperAst)(moduleNames);
1451
+ const entryAst = this.moduleAST.get(this.entryModule);
1452
+ const entryFileName = this.moduleNameAndFileName.get(this.entryModule);
1453
+ if (!entryAst || !entryFileName) {
1454
+ throw new Error(this.entryModule + " is not found");
1455
+ }
1456
+ const statements = [
1457
+ wrapperAst,
1458
+ { type: "ModuleSplice", moduleName: this.entryModule },
282
1459
  ];
283
- this.linkOrder.forEach((moduleName) => {
284
- if (moduleName === this.entryModule || !targets.has(moduleName)) {
285
- return;
286
- }
287
- parts.push('if m=="', moduleName, '"then r=(function() ', this.printModule(moduleName), " end)()end\n");
288
- });
289
- parts.push("package.loaded[m]=package.loaded[m]or r or true;return package.loaded[m]end\n");
290
- return new source_map_1.SourceNode(null, null, null, parts);
1460
+ return new ast2lua_1.MinifyFile(entryFileName, this.entryModule, entryAst, this, this.mode).printGeneratedStatements(statements);
291
1461
  }
292
1462
  }
293
1463
  exports.Minifier = Minifier;