storm-lua-minify 0.3.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +119 -41
  2. package/dist/aggregateSpecialization.js +406 -0
  3. package/dist/ast2lua.js +156 -68
  4. package/dist/astWalk.js +162 -0
  5. package/dist/callGraph.js +372 -0
  6. package/dist/cli.js +53 -58
  7. package/dist/cliOptions.js +36 -0
  8. package/dist/cliProgress.js +87 -0
  9. package/dist/config.js +73 -0
  10. package/dist/constantFold.js +798 -0
  11. package/dist/controlFlow.js +266 -0
  12. package/dist/functionRewrites.js +580 -0
  13. package/dist/generatedAst.js +108 -0
  14. package/dist/generatedNode.js +23 -0
  15. package/dist/globalRename.js +17 -4
  16. package/dist/interproceduralAnalysis.js +842 -0
  17. package/dist/interproceduralConstants.js +120 -0
  18. package/dist/luaString.js +157 -0
  19. package/dist/minifier.js +1219 -49
  20. package/dist/optimizerAnalysis.js +43 -0
  21. package/dist/optimizerDiagnostics.js +65 -0
  22. package/dist/optimizerFacts.js +529 -0
  23. package/dist/optimizerPass.js +96 -0
  24. package/dist/optimizerTransaction.js +56 -0
  25. package/dist/optimizerValueDomain.js +200 -0
  26. package/dist/options.js +233 -0
  27. package/dist/progress.js +2 -0
  28. package/dist/removeUnused.js +145 -0
  29. package/dist/renamer.js +280 -54
  30. package/dist/resolver.js +35 -11
  31. package/dist/runtimeEnvironment.js +105 -0
  32. package/dist/sourceMetadata.js +314 -0
  33. package/dist/statementDataflow.js +259 -0
  34. package/dist/statementScheduler.js +598 -0
  35. package/dist/symbolLiveness.js +92 -0
  36. package/dist/tableEffects.js +356 -0
  37. package/dist/transform.js +10 -371
  38. package/dist/valueFlow.js +409 -0
  39. package/dist/wholeProgramExports.js +646 -0
  40. package/dist/wholeProgramFieldRenames.js +583 -0
  41. package/dist/wholeProgramFields.js +672 -0
  42. package/dist/wholeProgramObjects.js +783 -0
  43. package/package.json +11 -2
  44. package/dist/index.js +0 -27
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeUnusedLocals = removeUnusedLocals;
4
+ function rangeOf(node) {
5
+ return node.range;
6
+ }
7
+ function nestedBodies(statement) {
8
+ switch (statement.type) {
9
+ case "DoStatement":
10
+ case "WhileStatement":
11
+ case "RepeatStatement":
12
+ case "FunctionDeclaration":
13
+ case "ForNumericStatement":
14
+ case "ForGenericStatement":
15
+ return [statement.body];
16
+ case "IfStatement":
17
+ return statement.clauses.map((clause) => clause.body);
18
+ default:
19
+ return [];
20
+ }
21
+ }
22
+ function isStatementCall(expression) {
23
+ return (expression.type === "CallExpression" ||
24
+ expression.type === "TableCallExpression" ||
25
+ expression.type === "StringCallExpression");
26
+ }
27
+ function callStatement(expression) {
28
+ return {
29
+ type: "CallStatement",
30
+ expression,
31
+ loc: expression.loc,
32
+ range: rangeOf(expression),
33
+ };
34
+ }
35
+ function hasExternalReference(statement, references) {
36
+ const range = rangeOf(statement);
37
+ if (!range)
38
+ return references.length > 0;
39
+ return references.some((reference) => {
40
+ const position = rangeOf(reference)?.[0];
41
+ return (position === undefined || position < range[0] || position >= range[1]);
42
+ });
43
+ }
44
+ function removeFromBlock(body, resolved, metadata, facts, onRemoveLocalFunction, options = { removeLocals: true, removeFunctions: true }) {
45
+ let changed = false;
46
+ body.forEach((statement) => {
47
+ nestedBodies(statement).forEach((nested) => {
48
+ changed =
49
+ removeFromBlock(nested, resolved, metadata, facts, onRemoveLocalFunction, options) || changed;
50
+ });
51
+ });
52
+ const replacements = new Map();
53
+ body.forEach((statement) => {
54
+ if (metadata.annotationsOf(statement).keep)
55
+ return;
56
+ if (options.removeFunctions &&
57
+ statement.type === "FunctionDeclaration" &&
58
+ statement.isLocal &&
59
+ statement.identifier?.type === "Identifier") {
60
+ const symbol = resolved.symbolOf(statement.identifier);
61
+ if (symbol && !hasExternalReference(statement, symbol.references)) {
62
+ onRemoveLocalFunction?.(statement);
63
+ changed = true;
64
+ replacements.set(statement, []);
65
+ return;
66
+ }
67
+ }
68
+ if (!options.removeLocals || statement.type !== "LocalStatement")
69
+ return;
70
+ const unused = statement.variables.map((variable) => {
71
+ const symbol = resolved.symbolOf(variable);
72
+ return Boolean(symbol && symbol.references.length === 0);
73
+ });
74
+ if (!unused.some(Boolean))
75
+ return;
76
+ if (unused.every(Boolean)) {
77
+ if (statement.init.every((expression) => facts.discardabilityOf(expression).discardable ||
78
+ isStatementCall(expression))) {
79
+ const calls = statement.init.filter(isStatementCall).map(callStatement);
80
+ changed = true;
81
+ replacements.set(statement, calls);
82
+ }
83
+ return;
84
+ }
85
+ let variables = [...statement.variables];
86
+ let init = [...statement.init];
87
+ let unusedAfterPairRemoval = [...unused];
88
+ // 初期値なしなら全変数の値はnilで固定され、位置対応を維持する必要がない。
89
+ if (init.length === 0) {
90
+ variables = variables.filter((_, index) => !unused[index]);
91
+ unusedAfterPairRemoval = unused.filter((value) => !value);
92
+ }
93
+ // 要素数が一致するときだけ、変数とRHSの対応を崩さず安全なペアを抜ける。
94
+ if (init.length > 0 && variables.length === init.length) {
95
+ const retainedIndexes = variables
96
+ .map((_, index) => index)
97
+ .filter((index) => !(unused[index] && facts.discardabilityOf(init[index]).discardable));
98
+ variables = retainedIndexes.map((index) => variables[index]);
99
+ init = retainedIndexes.map((index) => init[index]);
100
+ unusedAfterPairRemoval = retainedIndexes.map((index) => unused[index]);
101
+ }
102
+ // 末尾の未使用名は、対応RHSを余剰式として評価させたまま名前だけ除ける。
103
+ let retainedVariableCount = variables.length;
104
+ while (retainedVariableCount > 0 &&
105
+ unusedAfterPairRemoval[retainedVariableCount - 1]) {
106
+ retainedVariableCount--;
107
+ }
108
+ variables = variables.slice(0, retainedVariableCount);
109
+ if (variables.length === statement.variables.length)
110
+ return;
111
+ changed = true;
112
+ const replacement = {
113
+ ...statement,
114
+ variables,
115
+ init,
116
+ };
117
+ replacements.set(statement, [replacement]);
118
+ });
119
+ if (replacements.size > 0) {
120
+ const nextBody = [];
121
+ body.forEach((statement, index) => {
122
+ const replacement = replacements.get(statement);
123
+ if (!replacement) {
124
+ nextBody.push(statement);
125
+ return;
126
+ }
127
+ if (replacement.length > 0) {
128
+ metadata.replaceStatement(statement, replacement);
129
+ nextBody.push(...replacement);
130
+ return;
131
+ }
132
+ const next = body.slice(index + 1).find((candidate) => {
133
+ const candidateReplacement = replacements.get(candidate);
134
+ return !candidateReplacement || candidateReplacement.length > 0;
135
+ });
136
+ const nextReplacement = next ? replacements.get(next) : undefined;
137
+ metadata.removeStatement(statement, nextReplacement?.[0] ?? next);
138
+ });
139
+ body.splice(0, body.length, ...nextBody);
140
+ }
141
+ return changed;
142
+ }
143
+ function removeUnusedLocals(chunk, resolved, metadata, facts, onRemoveLocalFunction, options) {
144
+ return removeFromBlock(chunk.body, resolved, metadata, facts, onRemoveLocalFunction, options);
145
+ }
package/dist/renamer.js CHANGED
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isAvailable = isAvailable;
4
4
  exports.generateCandidate = generateCandidate;
5
5
  exports.assignRenames = assignRenames;
6
+ const resolver_1 = require("./resolver");
6
7
  const ast2lua_1 = require("./ast2lua");
8
+ const controlFlow_1 = require("./controlFlow");
9
+ const optimizerFacts_1 = require("./optimizerFacts");
10
+ const symbolLiveness_1 = require("./symbolLiveness");
7
11
  function isAvailable(id, reserved) {
8
12
  return id !== "self" && !(0, ast2lua_1.isKeyword)(id) && !reserved.has(id);
9
13
  }
@@ -21,62 +25,33 @@ function generateCandidate(counter) {
21
25
  }
22
26
  return id;
23
27
  }
24
- /**
25
- * スコープ木のDFSでシンボルごとにスロット番号を割り当てる。
26
- * `active`は祖先スコープ(自分を含む)で既に使われているスロットの集合。
27
- * 兄弟スコープには同じ`active`のコピーが渡されるため、互いの割当は影響しない。
28
- */
29
- function assignSlots(scope, active) {
30
- const slotOf = new Map();
31
- const used = new Set(active);
32
- scope.symbols.forEach((symbol) => {
33
- let slot = 0;
34
- while (used.has(slot)) {
35
- slot++;
36
- }
37
- slotOf.set(symbol, slot);
38
- used.add(slot);
39
- });
40
- scope.children.forEach((child) => {
41
- assignSlots(child, used).forEach((slot, symbol) => {
42
- slotOf.set(symbol, slot);
43
- });
44
- });
45
- return slotOf;
46
- }
47
- function assignRenames(resolveResult, reserved,
28
+ function assignRenames(chunk, resolveResult, reserved,
48
29
  // #8a: プログラム全体を横断して決定されたグローバル識別子の短縮名。
49
30
  // 全モジュールに対して同じマップを渡すことで、モジュールをまたいで
50
31
  // 共有される1つのランタイム束縛に一貫した短縮名を割り当てられる。
51
- globalRenames) {
52
- const slotOf = assignSlots(resolveResult.chunkScope, new Set());
53
- // スロットの通算参照回数(宣言自体も1回として数える)を集計する。
54
- const weightOfSlot = new Map();
55
- resolveResult.symbols.forEach((symbol) => {
56
- const slot = slotOf.get(symbol);
57
- if (slot === undefined) {
58
- return;
59
- }
60
- const weight = symbol.references.length + 1;
61
- weightOfSlot.set(slot, (weightOfSlot.get(slot) ?? 0) + weight);
62
- });
63
- const orderedSlots = [...weightOfSlot.keys()].sort((a, b) => (weightOfSlot.get(b) ?? 0) - (weightOfSlot.get(a) ?? 0));
64
- const nameOfSlot = new Map();
65
- let counter = 0;
66
- orderedSlots.forEach((slot) => {
67
- let candidate;
68
- do {
69
- candidate = generateCandidate(counter++);
70
- } while (!isAvailable(candidate, reserved));
71
- nameOfSlot.set(slot, candidate);
72
- });
73
- const nameOfSymbol = new Map();
74
- slotOf.forEach((slot, symbol) => {
75
- const name = nameOfSlot.get(slot);
76
- if (name !== undefined) {
77
- nameOfSymbol.set(symbol, name);
78
- }
79
- });
32
+ globalRenames, keepNames = new Set(), options = {}) {
33
+ const unavailableNames = new Set(reserved);
34
+ keepNames.forEach((symbol) => unavailableNames.add(symbol.name));
35
+ const variables = options.renameLocals === false
36
+ ? []
37
+ : resolveResult.symbols.filter((symbol) => symbol.kind !== "label" &&
38
+ !symbol.implicit &&
39
+ !keepNames.has(symbol));
40
+ const variableGraph = buildVariableInterference(chunk, resolveResult, variables, options.allowLocalNameReuse === true, options.analysis);
41
+ const variableColors = colorGraph(variableGraph);
42
+ const variableNames = assignColorNames(variableColors, unavailableNames);
43
+ // Labels have a separate Lua namespace. Color them independently so a label
44
+ // and a local may share a short spelling, while usedNames still reserves the
45
+ // spelling against labels from subsequently spliced modules.
46
+ const labels = options.renameLocals === false
47
+ ? []
48
+ : resolveResult.symbols.filter((symbol) => symbol.kind === "label" &&
49
+ symbol.name !== "self" &&
50
+ !keepNames.has(symbol));
51
+ const labelNames = assignColorNames(colorGraph(buildLexicalGraph(labels, false)), unavailableNames);
52
+ const nameOfSymbol = new Map([...variableNames, ...labelNames]);
53
+ validateBindings(chunk, resolveResult, nameOfSymbol, globalRenames);
54
+ const usedNames = new Set(nameOfSymbol.values());
80
55
  return {
81
56
  nameOf: (identifier) => {
82
57
  // メソッド定義の暗黙のselfパラメータは慣習的な名前のため常に維持する
@@ -96,6 +71,257 @@ globalRenames) {
96
71
  }
97
72
  return undefined;
98
73
  },
99
- usedNames: new Set(nameOfSlot.values()),
74
+ usedNames,
100
75
  };
101
76
  }
77
+ function buildVariableInterference(chunk, resolved, symbols, allowLocalNameReuse, analysis) {
78
+ const graph = mutableGraph(symbols);
79
+ const facts = analysis?.facts ?? (0, optimizerFacts_1.analyzeOptimizerFacts)(chunk, resolved);
80
+ const liveness = analysis?.liveness ??
81
+ (0, symbolLiveness_1.analyzeSymbolLiveness)((0, controlFlow_1.analyzeControlFlow)(chunk, resolved), facts);
82
+ if (facts.generation !== liveness.controlFlow.version)
83
+ throw new Error("Identifier coloring requires one AST generation");
84
+ liveness.controlFlow.nodes.forEach((node) => {
85
+ addClique(graph, liveness.liveIn(node));
86
+ addClique(graph, liveness.liveOut(node));
87
+ liveness.defs(node).forEach((definition) => {
88
+ liveness.liveOut(node).forEach((live) => {
89
+ addEdge(graph, definition, live);
90
+ });
91
+ });
92
+ });
93
+ // Parameters, generic-for variables, and multi-local declarations bind as a
94
+ // group. Giving two members one spelling would make only the last binding
95
+ // addressable after reparsing even if one member happens to be unused.
96
+ const declarationsByOwner = new Map();
97
+ symbols.forEach((symbol) => {
98
+ const declaration = facts
99
+ .operationsOfSymbol(symbol)
100
+ .find((operation) => operation.kind === "declare" &&
101
+ operation.origin === symbol.declaration);
102
+ if (!declaration)
103
+ return;
104
+ const group = declarationsByOwner.get(declaration.owner) ?? [];
105
+ group.push(symbol);
106
+ declarationsByOwner.set(declaration.owner, group);
107
+ });
108
+ declarationsByOwner.forEach((group) => {
109
+ addClique(graph, group);
110
+ });
111
+ // A captured binding can be shadowed inside the closure by any same-scope
112
+ // declaration whose emitted spelling matches it (notably `local function`).
113
+ // Extending this lexical interference across the whole declaring scope is
114
+ // conservative, deterministic, and leaves ordinary non-captured locals free
115
+ // to reuse names according to liveness.
116
+ symbols.forEach((symbol) => {
117
+ const captured = facts
118
+ .operationsOfSymbol(symbol)
119
+ .some((operation) => "location" in operation && operation.location.kind === "upvalue");
120
+ if (!captured)
121
+ return;
122
+ symbols.forEach((other) => {
123
+ if (other.scope === symbol.scope)
124
+ addEdge(graph, symbol, other);
125
+ });
126
+ });
127
+ // Liveness alone does not model Lua's lexical shadowing. An earlier local
128
+ // may be dead at a later declaration and then be assigned again; if both
129
+ // declarations receive one spelling, the later declaration captures that
130
+ // assignment and every following reference. Preserve reuse when all uses of
131
+ // the earlier binding precede the later declaration, but add an edge when
132
+ // any use follows it.
133
+ const declarationOrder = new Map(symbols.map((symbol) => [
134
+ symbol,
135
+ requireResolutionOrder(resolved, symbol.declaration),
136
+ ]));
137
+ const lastReferenceOrder = new Map(symbols.map((symbol) => [
138
+ symbol,
139
+ symbol.references.reduce((last, reference) => Math.max(last, requireResolutionOrder(resolved, reference)), -1),
140
+ ]));
141
+ for (let left = 0; left < symbols.length; left++) {
142
+ for (let right = left + 1; right < symbols.length; right++) {
143
+ const first = symbols[left];
144
+ const last = symbols[right];
145
+ if (first.scope !== last.scope)
146
+ continue;
147
+ const firstDeclarationOrder = declarationOrder.get(first) ?? -1;
148
+ const lastDeclarationOrder = declarationOrder.get(last) ?? -1;
149
+ const [earlier, laterDeclarationOrder] = firstDeclarationOrder < lastDeclarationOrder
150
+ ? [first, lastDeclarationOrder]
151
+ : [last, firstDeclarationOrder];
152
+ if ((lastReferenceOrder.get(earlier) ?? -1) > laterDeclarationOrder)
153
+ addEdge(graph, first, last);
154
+ }
155
+ }
156
+ addLexicalEdges(graph, symbols, allowLocalNameReuse);
157
+ return graph;
158
+ }
159
+ function requireResolutionOrder(resolved, identifier) {
160
+ const order = resolved.resolutionOrderOf(identifier);
161
+ if (order === undefined)
162
+ throw new Error("Resolved identifier has no resolution order");
163
+ return order;
164
+ }
165
+ function buildLexicalGraph(symbols, allowSameScopeReuse) {
166
+ const graph = mutableGraph(symbols);
167
+ addLexicalEdges(graph, symbols, allowSameScopeReuse);
168
+ return graph;
169
+ }
170
+ function mutableGraph(symbols) {
171
+ return new Map(symbols.map((symbol) => [symbol, new Set()]));
172
+ }
173
+ function addLexicalEdges(graph, symbols, allowSameScopeReuse) {
174
+ for (let left = 0; left < symbols.length; left++) {
175
+ for (let right = left + 1; right < symbols.length; right++) {
176
+ const first = symbols[left];
177
+ const last = symbols[right];
178
+ if (isAncestor(first.scope, last.scope) ||
179
+ isAncestor(last.scope, first.scope) ||
180
+ (!allowSameScopeReuse && first.scope === last.scope))
181
+ addEdge(graph, first, last);
182
+ }
183
+ }
184
+ }
185
+ function isAncestor(ancestor, descendant) {
186
+ if (ancestor === descendant)
187
+ return false;
188
+ for (let current = descendant.parent; current; current = current.parent)
189
+ if (current === ancestor)
190
+ return true;
191
+ return false;
192
+ }
193
+ function addClique(graph, symbols) {
194
+ const present = [...symbols].filter((symbol) => graph.has(symbol));
195
+ for (let left = 0; left < present.length; left++)
196
+ for (let right = left + 1; right < present.length; right++)
197
+ addEdge(graph, present[left], present[right]);
198
+ }
199
+ function addEdge(graph, first, last) {
200
+ if (first === last || !graph.has(first) || !graph.has(last))
201
+ return;
202
+ graph.get(first)?.add(last);
203
+ graph.get(last)?.add(first);
204
+ }
205
+ /** Deterministic weighted DSATUR coloring. */
206
+ function colorGraph(graph) {
207
+ const colors = new Map();
208
+ // Keep the colored-neighbor set incrementally. Recomputing it inside the
209
+ // sort comparator makes dense module graphs dominate the whole pipeline.
210
+ const saturationColors = new Map([...graph.keys()].map((symbol) => [symbol, new Set()]));
211
+ while (colors.size < graph.size) {
212
+ let symbol;
213
+ graph.forEach((_neighbors, candidate) => {
214
+ if (colors.has(candidate))
215
+ return;
216
+ if (symbol === undefined ||
217
+ coloringPriority(candidate, symbol, graph, saturationColors) < 0)
218
+ symbol = candidate;
219
+ });
220
+ if (symbol === undefined)
221
+ throw new Error("Uncolored symbol not found");
222
+ const unavailable = saturationColors.get(symbol) ?? new Set();
223
+ let color = 0;
224
+ while (unavailable.has(color))
225
+ color++;
226
+ colors.set(symbol, color);
227
+ graph.get(symbol)?.forEach((neighbor) => {
228
+ if (!colors.has(neighbor))
229
+ saturationColors.get(neighbor)?.add(color);
230
+ });
231
+ }
232
+ return colors;
233
+ }
234
+ function coloringPriority(left, right, graph, saturationColors) {
235
+ const saturationDifference = (saturationColors.get(right)?.size ?? 0) -
236
+ (saturationColors.get(left)?.size ?? 0);
237
+ if (saturationDifference !== 0)
238
+ return saturationDifference;
239
+ const weightDifference = weightOf(right) - weightOf(left);
240
+ if (weightDifference !== 0)
241
+ return weightDifference;
242
+ const degreeDifference = (graph.get(right)?.size ?? 0) - (graph.get(left)?.size ?? 0);
243
+ return degreeDifference !== 0 ? degreeDifference : left.id - right.id;
244
+ }
245
+ function weightOf(symbol) {
246
+ return symbol.references.length + 1;
247
+ }
248
+ function assignColorNames(colors, reserved) {
249
+ const unavailable = new Set(reserved);
250
+ const weightByColor = new Map();
251
+ const firstSymbolByColor = new Map();
252
+ colors.forEach((color, symbol) => {
253
+ weightByColor.set(color, (weightByColor.get(color) ?? 0) + weightOf(symbol));
254
+ firstSymbolByColor.set(color, Math.min(firstSymbolByColor.get(color) ?? symbol.id, symbol.id));
255
+ });
256
+ const orderedColors = [...weightByColor.keys()].sort((left, right) => (weightByColor.get(right) ?? 0) - (weightByColor.get(left) ?? 0) ||
257
+ (firstSymbolByColor.get(left) ?? 0) -
258
+ (firstSymbolByColor.get(right) ?? 0));
259
+ const nameByColor = new Map();
260
+ let counter = 0;
261
+ orderedColors.forEach((color) => {
262
+ let candidate;
263
+ do
264
+ candidate = generateCandidate(counter++);
265
+ while (!isAvailable(candidate, unavailable));
266
+ unavailable.add(candidate);
267
+ nameByColor.set(color, candidate);
268
+ });
269
+ // Every candidate is an identifier token and keywords are excluded, so the
270
+ // separator cost around each occurrence is invariant across candidates.
271
+ // Occurrence count therefore gives the exact candidate-dependent byte cost.
272
+ const result = new Map();
273
+ colors.forEach((color, symbol) => {
274
+ const name = nameByColor.get(color);
275
+ if (name === undefined)
276
+ throw new Error("Colored symbol has no name");
277
+ result.set(symbol, name);
278
+ });
279
+ return result;
280
+ }
281
+ function validateBindings(chunk, original, names, globalRenames) {
282
+ const recolored = (0, resolver_1.resolveScopes)(chunk, {
283
+ identifierName: (identifier) => {
284
+ const symbol = original.symbolOf(identifier);
285
+ if (symbol)
286
+ return names.get(symbol) ?? identifier.name;
287
+ return original.isGlobalReference(identifier)
288
+ ? (globalRenames?.get(identifier.name) ?? identifier.name)
289
+ : identifier.name;
290
+ },
291
+ });
292
+ original.symbols.forEach((symbol) => {
293
+ if (symbol.implicit) {
294
+ symbol.references.forEach((identifier) => {
295
+ const rebound = recolored.symbolOf(identifier);
296
+ if (!rebound?.implicit || rebound.name !== symbol.name)
297
+ throw new Error(`Identifier coloring changed implicit binding for symbol ${String(symbol.id)}`);
298
+ });
299
+ return;
300
+ }
301
+ [symbol.declaration, ...symbol.references].forEach((identifier) => {
302
+ const rebound = recolored.symbolOf(identifier);
303
+ if (rebound?.declaration !== symbol.declaration) {
304
+ const reboundOriginal = rebound
305
+ ? original.symbolOf(rebound.declaration)
306
+ : undefined;
307
+ throw new Error(`Identifier coloring changed binding for symbol ${String(symbol.id)} ` +
308
+ `(${symbol.name} -> ${names.get(symbol) ?? symbol.name}, ` +
309
+ `declaration ${formatIdentifierLocation(symbol.declaration)}, ` +
310
+ `reference ${formatIdentifierLocation(identifier)}, ` +
311
+ `rebound to ${reboundOriginal ? `symbol ${String(reboundOriginal.id)} (${reboundOriginal.name} -> ${names.get(reboundOriginal) ?? reboundOriginal.name}) at ${formatIdentifierLocation(reboundOriginal.declaration)}` : "global"})`);
312
+ }
313
+ });
314
+ });
315
+ original.globals.forEach((binding) => {
316
+ binding.references.forEach((identifier) => {
317
+ if (!recolored.isGlobalReference(identifier))
318
+ throw new Error(`Identifier coloring captured global ${binding.name}`);
319
+ });
320
+ });
321
+ }
322
+ function formatIdentifierLocation(identifier) {
323
+ const location = identifier.loc?.start;
324
+ return location
325
+ ? `${String(location.line)}:${String(location.column)}`
326
+ : "unknown";
327
+ }
package/dist/resolver.js CHANGED
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveScopes = resolveScopes;
4
- function resolveScopes(chunk) {
4
+ function resolveScopes(chunk, options = {}) {
5
5
  let nextSymbolId = 0;
6
6
  const allSymbols = [];
7
7
  const globals = new Map();
8
8
  const identifierSymbols = new WeakMap();
9
+ const identifierResolutionOrders = new WeakMap();
10
+ let nextResolutionOrder = 0;
9
11
  const globalReferenceNodes = new WeakSet();
12
+ const functionScopes = new WeakMap();
13
+ const identifierName = options.identifierName ??
14
+ ((identifier) => identifier.name);
10
15
  function createScope(kind, parent) {
11
16
  const scope = {
12
17
  kind,
@@ -19,33 +24,38 @@ function resolveScopes(chunk) {
19
24
  parent?.children.push(scope);
20
25
  return scope;
21
26
  }
22
- function declare(scope, node, kind) {
27
+ function declare(scope, node, kind, implicit = false) {
28
+ identifierResolutionOrders.set(node, nextResolutionOrder++);
29
+ const name = identifierName(node);
23
30
  const symbol = {
24
31
  id: nextSymbolId++,
25
- name: node.name,
32
+ name,
26
33
  kind,
27
34
  scope,
28
35
  declaration: node,
29
36
  references: [],
37
+ ...(implicit ? { implicit: true } : {}),
30
38
  };
31
39
  // 同名の再宣言はスコープ内の以後の参照から見た束縛を上書きする(Luaの通常のシャドーイング)
32
40
  scope.symbols.push(symbol);
33
- scope.bindings.set(node.name, symbol);
41
+ scope.bindings.set(name, symbol);
34
42
  allSymbols.push(symbol);
35
43
  identifierSymbols.set(node, symbol);
36
44
  return symbol;
37
45
  }
38
46
  function declareLabel(scope, node) {
47
+ identifierResolutionOrders.set(node, nextResolutionOrder++);
48
+ const name = identifierName(node);
39
49
  const symbol = {
40
50
  id: nextSymbolId++,
41
- name: node.name,
51
+ name,
42
52
  kind: "label",
43
53
  scope,
44
54
  declaration: node,
45
55
  references: [],
46
56
  };
47
57
  scope.symbols.push(symbol);
48
- scope.labels.set(node.name, symbol);
58
+ scope.labels.set(name, symbol);
49
59
  allSymbols.push(symbol);
50
60
  identifierSymbols.set(node, symbol);
51
61
  return symbol;
@@ -69,16 +79,18 @@ function resolveScopes(chunk) {
69
79
  return undefined;
70
80
  }
71
81
  function reference(scope, node, isWrite = false) {
72
- const symbol = lookupBinding(scope, node.name);
82
+ identifierResolutionOrders.set(node, nextResolutionOrder++);
83
+ const name = identifierName(node);
84
+ const symbol = lookupBinding(scope, name);
73
85
  if (symbol) {
74
86
  symbol.references.push(node);
75
87
  identifierSymbols.set(node, symbol);
76
88
  return;
77
89
  }
78
- let binding = globals.get(node.name);
90
+ let binding = globals.get(name);
79
91
  if (!binding) {
80
- binding = { name: node.name, references: [], writes: [] };
81
- globals.set(node.name, binding);
92
+ binding = { name, references: [], writes: [] };
93
+ globals.set(name, binding);
82
94
  }
83
95
  binding.references.push(node);
84
96
  if (isWrite) {
@@ -188,7 +200,8 @@ function resolveScopes(chunk) {
188
200
  // hoistLabelsで宣言済みのため、ここでは何もしない
189
201
  return;
190
202
  case "GotoStatement": {
191
- const symbol = lookupLabel(scope, statement.label.name);
203
+ identifierResolutionOrders.set(statement.label, nextResolutionOrder++);
204
+ const symbol = lookupLabel(scope, identifierName(statement.label));
192
205
  if (symbol) {
193
206
  symbol.references.push(statement.label);
194
207
  identifierSymbols.set(statement.label, symbol);
@@ -218,6 +231,14 @@ function resolveScopes(chunk) {
218
231
  }
219
232
  }
220
233
  const inner = createScope("function", scope);
234
+ functionScopes.set(fn, inner);
235
+ if (fn.identifier?.type === "MemberExpression" &&
236
+ fn.identifier.indexer === ":") {
237
+ // `function object:method(...)` declares an implicit first parameter. It has no
238
+ // declaration token in the AST, but it must still own every `self` reference so
239
+ // global analysis, aliases, summaries, and binding validation share Lua's binding.
240
+ declare(inner, { type: "Identifier", name: "self" }, "param", true);
241
+ }
221
242
  fn.parameters.forEach((parameter) => {
222
243
  if (parameter.type === "Identifier") {
223
244
  declare(inner, parameter, "param");
@@ -268,6 +289,7 @@ function resolveScopes(chunk) {
268
289
  return;
269
290
  case "FunctionDeclaration": {
270
291
  const inner = createScope("function", scope);
292
+ functionScopes.set(expr, inner);
271
293
  expr.parameters.forEach((parameter) => {
272
294
  if (parameter.type === "Identifier") {
273
295
  declare(inner, parameter, "param");
@@ -303,7 +325,9 @@ function resolveScopes(chunk) {
303
325
  chunkScope,
304
326
  symbols: allSymbols,
305
327
  globals,
328
+ resolutionOrderOf: (identifier) => identifierResolutionOrders.get(identifier),
306
329
  symbolOf: (identifier) => identifierSymbols.get(identifier),
307
330
  isGlobalReference: (identifier) => globalReferenceNodes.has(identifier),
331
+ scopeOfFunction: (fn) => functionScopes.get(fn),
308
332
  };
309
333
  }