storm-lua-minify 0.3.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +119 -41
  2. package/dist/aggregateSpecialization.js +406 -0
  3. package/dist/ast2lua.js +156 -68
  4. package/dist/astWalk.js +162 -0
  5. package/dist/callGraph.js +372 -0
  6. package/dist/cli.js +53 -58
  7. package/dist/cliOptions.js +36 -0
  8. package/dist/cliProgress.js +87 -0
  9. package/dist/config.js +73 -0
  10. package/dist/constantFold.js +798 -0
  11. package/dist/controlFlow.js +266 -0
  12. package/dist/functionRewrites.js +580 -0
  13. package/dist/generatedAst.js +108 -0
  14. package/dist/generatedNode.js +23 -0
  15. package/dist/interproceduralAnalysis.js +842 -0
  16. package/dist/interproceduralConstants.js +120 -0
  17. package/dist/luaString.js +157 -0
  18. package/dist/minifier.js +1178 -44
  19. package/dist/optimizerAnalysis.js +43 -0
  20. package/dist/optimizerDiagnostics.js +65 -0
  21. package/dist/optimizerFacts.js +529 -0
  22. package/dist/optimizerPass.js +96 -0
  23. package/dist/optimizerTransaction.js +56 -0
  24. package/dist/optimizerValueDomain.js +180 -0
  25. package/dist/options.js +233 -0
  26. package/dist/progress.js +2 -0
  27. package/dist/removeUnused.js +145 -0
  28. package/dist/renamer.js +223 -54
  29. package/dist/resolver.js +28 -11
  30. package/dist/runtimeEnvironment.js +105 -0
  31. package/dist/sourceMetadata.js +314 -0
  32. package/dist/statementDataflow.js +259 -0
  33. package/dist/statementScheduler.js +595 -0
  34. package/dist/symbolLiveness.js +92 -0
  35. package/dist/tableEffects.js +356 -0
  36. package/dist/transform.js +10 -371
  37. package/dist/valueFlow.js +409 -0
  38. package/dist/wholeProgramExports.js +646 -0
  39. package/dist/wholeProgramFieldRenames.js +583 -0
  40. package/dist/wholeProgramFields.js +672 -0
  41. package/dist/wholeProgramObjects.js +783 -0
  42. package/package.json +11 -2
  43. package/dist/index.js +0 -27
@@ -0,0 +1,798 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.foldConstants = foldConstants;
4
+ exports.constantValueOf = constantValueOf;
5
+ const luaString_1 = require("./luaString");
6
+ const INT64_MIN = -(2n ** 63n);
7
+ function rangeOf(node) {
8
+ return node.range;
9
+ }
10
+ // 生成・複製したノードに、元ノードの位置情報(loc/range)をコピーする(Source Mapのため)。
11
+ function withOriginPosition(node, origin) {
12
+ node.loc = origin.loc;
13
+ const range = rangeOf(origin);
14
+ if (range) {
15
+ node.range = range;
16
+ }
17
+ }
18
+ const DEC_INT_RAW = /^[0-9]+$/;
19
+ const HEX_INT_RAW = /^0[xX][0-9a-fA-F]+$/;
20
+ function numericConstantOf(node) {
21
+ const raw = node.raw;
22
+ if (DEC_INT_RAW.test(raw)) {
23
+ const parsed = BigInt(raw);
24
+ const wrapped = BigInt.asIntN(64, parsed);
25
+ // 10進の整数リテラルがint64に収まらない場合、Luaはfloatとして扱う。
26
+ // 扱いを分けず、単に見送る。
27
+ if (wrapped !== parsed)
28
+ return undefined;
29
+ return { kind: "int", value: wrapped };
30
+ }
31
+ if (HEX_INT_RAW.test(raw)) {
32
+ // 16進整数リテラルは大きさに関わらず常にint64として2の補数でラップされる
33
+ // (Luaの規定)。
34
+ return { kind: "int", value: BigInt.asIntN(64, BigInt(raw)) };
35
+ }
36
+ // それ以外(`.`やe/Eを含む10進float、16進float)。16進floatのp指数はNumber()
37
+ // では解釈できないため、luaparseが既に解いた.valueを使う。整数の桁と違い、
38
+ // floatはどちらの経路でもJavaScriptのdoubleと同じ精度なので、ここでvalueを
39
+ // 使っても精度は失われない。
40
+ const value = node.value;
41
+ if (!Number.isFinite(value))
42
+ return undefined;
43
+ return { kind: "float", value };
44
+ }
45
+ function stringConstantOf(node) {
46
+ const decoded = (0, luaString_1.decodeLuaStringLiteral)(node);
47
+ return decoded.ok
48
+ ? { kind: "string", value: decoded.value, raw: node.raw }
49
+ : undefined;
50
+ }
51
+ // 式が「今すぐ書き出せる定数」かどうかを判定する。畳み込み・伝搬どちらの判断でも
52
+ // 使う共通の読み取り関数で、ASTを変更しない。
53
+ function constantValueOf(expr) {
54
+ switch (expr.type) {
55
+ case "NilLiteral":
56
+ return { kind: "nil" };
57
+ case "BooleanLiteral":
58
+ return { kind: "boolean", value: expr.value };
59
+ case "NumericLiteral":
60
+ return numericConstantOf(expr);
61
+ case "StringLiteral":
62
+ return stringConstantOf(expr);
63
+ case "UnaryExpression":
64
+ if (expr.operator === "-" && expr.argument.type === "NumericLiteral") {
65
+ const inner = numericConstantOf(expr.argument);
66
+ if (!inner)
67
+ return undefined;
68
+ if (inner.kind === "int") {
69
+ return { kind: "int", value: BigInt.asIntN(64, -inner.value) };
70
+ }
71
+ if (inner.kind === "float") {
72
+ const negated = -inner.value;
73
+ if (Object.is(negated, -0))
74
+ return undefined; // -0は生成しない/認めない
75
+ return { kind: "float", value: negated };
76
+ }
77
+ return undefined;
78
+ }
79
+ return undefined;
80
+ default:
81
+ return undefined;
82
+ }
83
+ }
84
+ // ============================================================
85
+ // literalNodeFor: 定数値からASTノードを作る
86
+ // ============================================================
87
+ function intLiteralNode(v, origin) {
88
+ if (v === INT64_MIN) {
89
+ // math.mininteger(-9223372036854775808)は、絶対値(2^63)が10進の整数
90
+ // リテラルとしてint64の範囲外になり、Luaの数値リテラル規則上floatとして
91
+ // 読み直されてしまう(constantValueOfのnumericConstantOfが自分自身の
92
+ // asIntN判定で弾く値と同じ)。この値を正しく再現する10進表記が無いため、
93
+ // 畳み込みを見送る。
94
+ return undefined;
95
+ }
96
+ if (v >= 0n) {
97
+ const raw = v.toString(10);
98
+ const node = {
99
+ type: "NumericLiteral",
100
+ value: Number(v),
101
+ raw,
102
+ };
103
+ withOriginPosition(node, origin);
104
+ return node;
105
+ }
106
+ // 負のintは単項式として持つ。印字はrawをそのまま出すため、raw="-2"のような
107
+ // 数値リテラルが`a - -2`の右辺に来ると`a--2`と出力され、以降が行コメントに
108
+ // なってしまう。`-`を単項式にすれば、既存の印字経路(insertSeparator)が
109
+ // 区切りを入れてくれる。
110
+ const abs = -v;
111
+ const raw = abs.toString(10);
112
+ const inner = {
113
+ type: "NumericLiteral",
114
+ value: Number(abs),
115
+ raw,
116
+ };
117
+ withOriginPosition(inner, origin);
118
+ const node = {
119
+ type: "UnaryExpression",
120
+ operator: "-",
121
+ argument: inner,
122
+ };
123
+ withOriginPosition(node, origin);
124
+ return node;
125
+ }
126
+ function floatLiteralNode(v, origin) {
127
+ if (!Number.isFinite(v))
128
+ return undefined;
129
+ if (Object.is(v, -0))
130
+ return undefined; // String(-0)は"0"になり-0.0と食い違う
131
+ if (v < 0) {
132
+ const positive = floatLiteralNode(-v, origin);
133
+ if (!positive)
134
+ return undefined;
135
+ const node = {
136
+ type: "UnaryExpression",
137
+ operator: "-",
138
+ argument: positive,
139
+ };
140
+ withOriginPosition(node, origin);
141
+ return node;
142
+ }
143
+ let raw = String(v);
144
+ if (!/[.eE]/.test(raw)) {
145
+ // 3を3.0にする。付けないとfloatが整数に化ける。
146
+ raw += ".0";
147
+ }
148
+ const node = { type: "NumericLiteral", value: v, raw };
149
+ withOriginPosition(node, origin);
150
+ return node;
151
+ }
152
+ function literalNodeFor(value, origin) {
153
+ switch (value.kind) {
154
+ case "nil": {
155
+ const node = {
156
+ type: "NilLiteral",
157
+ value: null,
158
+ raw: "nil",
159
+ };
160
+ withOriginPosition(node, origin);
161
+ return node;
162
+ }
163
+ case "boolean": {
164
+ const node = {
165
+ type: "BooleanLiteral",
166
+ value: value.value,
167
+ raw: value.value ? "true" : "false",
168
+ };
169
+ withOriginPosition(node, origin);
170
+ return node;
171
+ }
172
+ case "string": {
173
+ const node = {
174
+ type: "StringLiteral",
175
+ // Printerと再解析はrawを使う。luaparse型ではstring必須だが、既定の
176
+ // discardStrings環境では意味値を保持しないため空文字を入れる。
177
+ value: "",
178
+ raw: value.raw,
179
+ };
180
+ withOriginPosition(node, origin);
181
+ return node;
182
+ }
183
+ case "int":
184
+ return intLiteralNode(value.value, origin);
185
+ case "float":
186
+ return floatLiteralNode(value.value, origin);
187
+ }
188
+ }
189
+ // ============================================================
190
+ // 畳み込む規則
191
+ // ============================================================
192
+ function isNumeric(v) {
193
+ return v.kind === "int" || v.kind === "float";
194
+ }
195
+ function numAsFloat(v) {
196
+ return v.kind === "int" ? Number(v.value) : v.value;
197
+ }
198
+ // Luaの `//` は床除算。BigIntの`/`は0方向への切り捨てなので、符号が異なり
199
+ // 余りがあるときに1引いて床に合わせる。
200
+ function bigIntFloorDiv(l, r) {
201
+ const q = l / r;
202
+ const rem = l % r;
203
+ if (rem !== 0n && rem < 0n !== r < 0n) {
204
+ return q - 1n;
205
+ }
206
+ return q;
207
+ }
208
+ function evalArithmetic(op, l, r) {
209
+ if (!isNumeric(l) || !isNumeric(r))
210
+ return undefined;
211
+ if (op === "/") {
212
+ // `/`は常にfloat
213
+ return { kind: "float", value: numAsFloat(l) / numAsFloat(r) };
214
+ }
215
+ if (op === "^") {
216
+ // `^`は常にfloat
217
+ return { kind: "float", value: Math.pow(numAsFloat(l), numAsFloat(r)) };
218
+ }
219
+ const bothInt = l.kind === "int" && r.kind === "int";
220
+ if (op === "//") {
221
+ if (bothInt) {
222
+ if (r.value === 0n)
223
+ return undefined; // Luaは実行時エラー。エラーを消してはならない
224
+ return {
225
+ kind: "int",
226
+ value: BigInt.asIntN(64, bigIntFloorDiv(l.value, r.value)),
227
+ };
228
+ }
229
+ const lf = numAsFloat(l);
230
+ const rf = numAsFloat(r);
231
+ return { kind: "float", value: Math.floor(lf / rf) };
232
+ }
233
+ if (op === "%") {
234
+ // Luaの剰余は床方向で、JavaScriptの`%`とは負数で結果が異なる。
235
+ //
236
+ // 浮動小数点では、Luaのマニュアルにある `a - floor(a/b)*b` という等式を
237
+ // そのまま計算してはいけない。両辺の大きさが極端に違うとき、掛け戻しの桁で
238
+ // 情報が落ちる(`10 % 1e-300`が0になる)。Lua自身はC言語のfmodを使って
239
+ // 余りを直接求め、符号が除数と食い違うときだけ除数を足している。
240
+ // JavaScriptの`%`はfmodと同じ切り捨て方向の余りなので、同じ手順を踏む。
241
+ if (bothInt) {
242
+ if (r.value === 0n)
243
+ return undefined;
244
+ return {
245
+ kind: "int",
246
+ value: BigInt.asIntN(64, l.value - bigIntFloorDiv(l.value, r.value) * r.value),
247
+ };
248
+ }
249
+ const lf = numAsFloat(l);
250
+ const rf = numAsFloat(r);
251
+ let remainder = lf % rf;
252
+ if (remainder !== 0 && remainder < 0 !== rf < 0) {
253
+ remainder += rf;
254
+ }
255
+ return { kind: "float", value: remainder };
256
+ }
257
+ // + - *
258
+ if (bothInt) {
259
+ const raw = op === "+"
260
+ ? l.value + r.value
261
+ : op === "-"
262
+ ? l.value - r.value
263
+ : l.value * r.value;
264
+ return { kind: "int", value: BigInt.asIntN(64, raw) };
265
+ }
266
+ const lf = numAsFloat(l);
267
+ const rf = numAsFloat(r);
268
+ const raw = op === "+" ? lf + rf : op === "-" ? lf - rf : lf * rf;
269
+ return { kind: "float", value: raw };
270
+ }
271
+ // Luaのシフトは論理シフト(符号なし)。符号無し64bitに直してシフトし、int64に戻す。
272
+ // シフト量が負なら逆方向のシフトとして扱い、絶対値が64以上なら結果は0。
273
+ function shiftLogical(valueUnsigned, amount) {
274
+ if (amount >= 0n) {
275
+ if (amount >= 64n)
276
+ return 0n;
277
+ return BigInt.asUintN(64, valueUnsigned << amount);
278
+ }
279
+ const shiftRightBy = -amount;
280
+ if (shiftRightBy >= 64n)
281
+ return 0n;
282
+ return valueUnsigned >> shiftRightBy;
283
+ }
284
+ function evalBitwise(op, l, r) {
285
+ // 両辺がintのときだけ扱う(floatは整数値でも対象外)
286
+ if (l.kind !== "int" || r.kind !== "int")
287
+ return undefined;
288
+ const lu = BigInt.asUintN(64, l.value);
289
+ const ru = BigInt.asUintN(64, r.value);
290
+ switch (op) {
291
+ case "&":
292
+ return { kind: "int", value: BigInt.asIntN(64, lu & ru) };
293
+ case "|":
294
+ return { kind: "int", value: BigInt.asIntN(64, lu | ru) };
295
+ case "~":
296
+ return { kind: "int", value: BigInt.asIntN(64, lu ^ ru) };
297
+ case "<<":
298
+ return {
299
+ kind: "int",
300
+ value: BigInt.asIntN(64, shiftLogical(lu, r.value)),
301
+ };
302
+ case ">>":
303
+ return {
304
+ kind: "int",
305
+ value: BigInt.asIntN(64, shiftLogical(lu, -r.value)),
306
+ };
307
+ }
308
+ }
309
+ function isSafeBigInt(v) {
310
+ return (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER));
311
+ }
312
+ function evalOrderComparison(op, l, r) {
313
+ if (l.kind === "string" && r.kind === "string") {
314
+ const comparison = (0, luaString_1.compareLuaByteStrings)(l.value, r.value);
315
+ return { kind: "boolean", value: compareValues(op, comparison, 0) };
316
+ }
317
+ // 数値と文字列の比較はLuaでは実行時エラーなので、畳み込んでエラーを消さない。
318
+ if (!isNumeric(l) || !isNumeric(r))
319
+ return undefined;
320
+ let result;
321
+ if (l.kind === "int" && r.kind === "int") {
322
+ result = compareValues(op, l.value, r.value);
323
+ }
324
+ else {
325
+ // 型混在(int/float)でintが安全整数の範囲外なら、Number化で誤差が出るため畳み込まない。
326
+ if (l.kind === "int" && !isSafeBigInt(l.value))
327
+ return undefined;
328
+ if (r.kind === "int" && !isSafeBigInt(r.value))
329
+ return undefined;
330
+ result = compareValues(op, numAsFloat(l), numAsFloat(r));
331
+ }
332
+ return { kind: "boolean", value: result };
333
+ }
334
+ function compareValues(op, l, r) {
335
+ switch (op) {
336
+ case "<":
337
+ return l < r;
338
+ case "<=":
339
+ return l <= r;
340
+ case ">":
341
+ return l > r;
342
+ case ">=":
343
+ return l >= r;
344
+ }
345
+ }
346
+ // 両辺が定数のときに== ~=を判定する。型が違えばfalse(強制変換しない)。
347
+ // intとfloatは同じ数値型として値で比較する(1 == 1.0はtrue)。
348
+ // intとfloatが混在する比較では、大小比較と同様に安全整数の範囲外のintを避ける
349
+ // (精度を落として誤った真偽値を畳み込まないための防御)。
350
+ function valuesEqual(l, r) {
351
+ if (l.kind === "nil" || r.kind === "nil") {
352
+ return l.kind === "nil" && r.kind === "nil";
353
+ }
354
+ if (l.kind === "boolean" || r.kind === "boolean") {
355
+ return l.kind === "boolean" && r.kind === "boolean" && l.value === r.value;
356
+ }
357
+ if (l.kind === "string" || r.kind === "string") {
358
+ return (l.kind === "string" &&
359
+ r.kind === "string" &&
360
+ (0, luaString_1.luaByteStringKey)(l.value) === (0, luaString_1.luaByteStringKey)(r.value));
361
+ }
362
+ if (l.kind === "int" && r.kind === "int")
363
+ return l.value === r.value;
364
+ if (l.kind === "float" && r.kind === "float")
365
+ return l.value === r.value;
366
+ const intSide = l.kind === "int" ? l : r;
367
+ if (!isSafeBigInt(intSide.value))
368
+ return undefined;
369
+ return numAsFloat(l) === numAsFloat(r);
370
+ }
371
+ function evalConcat(l, r) {
372
+ // 両辺が文字列定数のときだけ。数値との連結は対象外(1と1.0で表記が変わるため)。
373
+ if (l.kind !== "string" || r.kind !== "string")
374
+ return undefined;
375
+ const combined = (0, luaString_1.concatLuaByteStrings)(l.value, r.value);
376
+ return {
377
+ kind: "string",
378
+ value: combined,
379
+ raw: (0, luaString_1.encodeLuaByteString)(combined),
380
+ };
381
+ }
382
+ function isTruthy(v) {
383
+ return !(v.kind === "nil" || (v.kind === "boolean" && !v.value));
384
+ }
385
+ function tryFoldBinary(expr) {
386
+ const l = constantValueOf(expr.left);
387
+ const r = constantValueOf(expr.right);
388
+ if (!l || !r)
389
+ return undefined;
390
+ const op = expr.operator;
391
+ let result;
392
+ switch (op) {
393
+ case "+":
394
+ case "-":
395
+ case "*":
396
+ case "/":
397
+ case "//":
398
+ case "%":
399
+ case "^":
400
+ result = evalArithmetic(op, l, r);
401
+ break;
402
+ case "&":
403
+ case "|":
404
+ case "~":
405
+ case "<<":
406
+ case ">>":
407
+ result = evalBitwise(op, l, r);
408
+ break;
409
+ case "<":
410
+ case "<=":
411
+ case ">":
412
+ case ">=":
413
+ result = evalOrderComparison(op, l, r);
414
+ break;
415
+ case "==":
416
+ case "~=": {
417
+ const eq = valuesEqual(l, r);
418
+ result =
419
+ eq === undefined
420
+ ? undefined
421
+ : { kind: "boolean", value: op === "==" ? eq : !eq };
422
+ break;
423
+ }
424
+ case "..":
425
+ result = evalConcat(l, r);
426
+ break;
427
+ default: {
428
+ const exhaustive = op;
429
+ throw new TypeError("Unknown binary operator: `" + JSON.stringify(exhaustive) + "`");
430
+ }
431
+ }
432
+ if (!result)
433
+ return undefined;
434
+ return literalNodeFor(result, expr);
435
+ }
436
+ function tryFoldLogical(expr) {
437
+ const l = constantValueOf(expr.left);
438
+ if (!l)
439
+ return undefined;
440
+ const leftTruthy = isTruthy(l);
441
+ if (expr.operator === "and") {
442
+ if (!leftTruthy) {
443
+ // 左が偽なら右は評価されない。左を残す(右が定数でなくてよい)。
444
+ return literalNodeFor(l, expr);
445
+ }
446
+ const r = constantValueOf(expr.right);
447
+ if (!r)
448
+ return undefined; // 右も定数のときだけ置き換える(関数呼び出し等の多値展開を守る)
449
+ return literalNodeFor(r, expr);
450
+ }
451
+ // or
452
+ if (leftTruthy) {
453
+ return literalNodeFor(l, expr);
454
+ }
455
+ const r = constantValueOf(expr.right);
456
+ if (!r)
457
+ return undefined;
458
+ return literalNodeFor(r, expr);
459
+ }
460
+ function tryFoldUnary(expr) {
461
+ if (expr.operator === "-" && expr.argument.type === "NumericLiteral") {
462
+ // constantValueOfはUnaryExpression("-", NumericLiteral)をそのまま定数として
463
+ // 認める(終端の形)。ここで同じ形へ畳み込み直すと、`changed`が真になり続け
464
+ // 前進しないまま無限ループになる。この形は既に確定した終端として扱い、
465
+ // 何もしない。
466
+ return undefined;
467
+ }
468
+ const v = constantValueOf(expr.argument);
469
+ if (!v)
470
+ return undefined;
471
+ if (expr.operator === "not") {
472
+ return literalNodeFor({ kind: "boolean", value: !isTruthy(v) }, expr);
473
+ }
474
+ if (expr.operator === "-") {
475
+ if (v.kind === "int") {
476
+ return literalNodeFor({ kind: "int", value: BigInt.asIntN(64, -v.value) }, expr);
477
+ }
478
+ if (v.kind === "float") {
479
+ const negated = -v.value;
480
+ if (Object.is(negated, -0))
481
+ return undefined;
482
+ return literalNodeFor({ kind: "float", value: negated }, expr);
483
+ }
484
+ return undefined;
485
+ }
486
+ if (expr.operator === "~") {
487
+ if (v.kind !== "int")
488
+ return undefined;
489
+ return literalNodeFor({ kind: "int", value: BigInt.asIntN(64, ~v.value) }, expr);
490
+ }
491
+ // 残るのは`#`。Lua stringは共有decoderが返すbyte列なので、JavaScriptの
492
+ // code unit数ではなくbyte数を使う。tableへの`#`は実行時に決まるため対象外。
493
+ if (v.kind !== "string")
494
+ return undefined;
495
+ return literalNodeFor({ kind: "int", value: BigInt(v.value.bytes.length) }, expr);
496
+ }
497
+ // ============================================================
498
+ // 定数伝搬 — 対象の収集
499
+ // ============================================================
500
+ function childBlocksOf(statement) {
501
+ switch (statement.type) {
502
+ case "DoStatement":
503
+ case "WhileStatement":
504
+ case "RepeatStatement":
505
+ case "FunctionDeclaration":
506
+ case "ForNumericStatement":
507
+ case "ForGenericStatement":
508
+ return [statement.body];
509
+ case "IfStatement":
510
+ return statement.clauses.map((clause) => clause.body);
511
+ default:
512
+ return [];
513
+ }
514
+ }
515
+ // 共通operationのwriteから再代入を求める。Symbol.referencesはread/writeを区別しない
516
+ // ため参照数では代用できない。これを落とすと`local x=5 x=7`が`5=7`になる。
517
+ function collectReassignedSymbols(facts) {
518
+ const out = new Set();
519
+ facts.operations.forEach((operation) => {
520
+ if (operation.kind === "write" &&
521
+ (operation.location.kind === "local" ||
522
+ operation.location.kind === "parameter" ||
523
+ operation.location.kind === "upvalue")) {
524
+ out.add(operation.location.symbol);
525
+ }
526
+ });
527
+ return out;
528
+ }
529
+ // 伝搬した値が印字時に何バイトになるかを見積もる。畳み込みの出力経路
530
+ // (intLiteralNode/floatLiteralNode/literalNodeFor)と同じ表記規則で数える。
531
+ function printedLengthOf(value) {
532
+ switch (value.kind) {
533
+ case "nil":
534
+ return 3; // "nil"
535
+ case "boolean":
536
+ return value.value ? 4 : 5; // "true" / "false"
537
+ case "int": {
538
+ const abs = value.value < 0n ? -value.value : value.value;
539
+ return (value.value < 0n ? 1 : 0) + abs.toString(10).length;
540
+ }
541
+ case "float": {
542
+ const abs = Math.abs(value.value);
543
+ let raw = String(abs);
544
+ if (!/[.eE]/.test(raw))
545
+ raw += ".0";
546
+ return (value.value < 0 ? 1 : 0) + raw.length;
547
+ }
548
+ case "string":
549
+ return new TextEncoder().encode(value.raw).length;
550
+ }
551
+ }
552
+ // 参照が複数ある定数ローカルを配ってよいか。
553
+ //
554
+ // 配ると「宣言(`local <名前>=<値>` のおよそ7+名前の長さ+値の長さバイト)」が
555
+ // 丸ごと消える代わりに、参照のたびに識別子(名前の長さ)ではなく値の長さが
556
+ // 出力される。名前の長さは、この畳み込みパスがrenameパスより前に走るため
557
+ // ここではまだ決まっていない(minifier.tsのfoldConstantsAllはrenameAllより前)。
558
+ // 決め方を誤って出力を伸ばすくらいなら伝搬しない方に倒したいので、renameが
559
+ // 名前をこれ以上削れない最短の1文字にできた場合(配らない側にとって最も有利な
560
+ // 場合)を仮定して、その上でなお配る方が短くなることだけを条件にする。
561
+ // 名前が実際には1文字より長くなった場合、配る側はこの見積りより得をする
562
+ // だけなので、この判定が縮まない伝搬を通すことはない。
563
+ //
564
+ // 1文字(`printedLength<=1`)は、名前が最短の1文字であっても宣言の分だけ
565
+ // 必ず得なので、参照回数に関わらず常に配ってよい(isShortLiteralが対象に
566
+ // していた「1文字の数値リテラル」を含む、より一般化した条件になっている)。
567
+ function worthPropagatingWhenShared(printedLength, refCount) {
568
+ if (printedLength <= 1)
569
+ return true;
570
+ // 名前1文字・宣言の定数オーバーヘッド(`local `+`=`)7バイトを仮定したときの
571
+ // 収支: refCount*(printedLength-1) <= printedLength+8
572
+ return refCount * (printedLength - 1) <= printedLength + 8;
573
+ }
574
+ function collectPropagationCandidates(body, resolved, metadata, reassigned) {
575
+ const out = new Map();
576
+ function visit(block) {
577
+ block.forEach((statement) => {
578
+ childBlocksOf(statement).forEach(visit);
579
+ if (statement.type !== "LocalStatement")
580
+ return;
581
+ if (statement.variables.length !== 1 || statement.init.length !== 1)
582
+ return;
583
+ const annotations = metadata.annotationsOf(statement);
584
+ if (annotations.keep || annotations.keepName || annotations.exported)
585
+ return;
586
+ const value = constantValueOf(statement.init[0]);
587
+ if (!value)
588
+ return;
589
+ const symbol = resolved.symbolOf(statement.variables[0]);
590
+ if (!symbol)
591
+ return;
592
+ if (reassigned.has(symbol))
593
+ return;
594
+ const refCount = symbol.references.length;
595
+ if (refCount === 1 ||
596
+ worthPropagatingWhenShared(printedLengthOf(value), refCount)) {
597
+ out.set(symbol, value);
598
+ }
599
+ });
600
+ }
601
+ visit(body);
602
+ return out;
603
+ }
604
+ function rewriteCallLike(expr, ctx) {
605
+ expr.base = rewriteExpression(expr.base, ctx);
606
+ if (expr.type === "CallExpression") {
607
+ expr.arguments = expr.arguments.map((a) => rewriteExpression(a, ctx));
608
+ }
609
+ else if (expr.type === "TableCallExpression") {
610
+ expr.arguments = rewriteExpression(expr.arguments, ctx);
611
+ }
612
+ else {
613
+ expr.argument = rewriteExpression(expr.argument, ctx);
614
+ }
615
+ }
616
+ function rewriteExpression(expr, ctx) {
617
+ switch (expr.type) {
618
+ case "Identifier": {
619
+ const symbol = ctx.resolved.symbolOf(expr);
620
+ const value = symbol ? ctx.propagate.get(symbol) : undefined;
621
+ if (value) {
622
+ // 伝搬の結果は、置き換えられた参照側のIdentifierの位置を使う(宣言側ではない)。
623
+ const literal = literalNodeFor(value, expr);
624
+ if (literal) {
625
+ ctx.changed = true;
626
+ return literal;
627
+ }
628
+ }
629
+ return expr;
630
+ }
631
+ case "StringLiteral":
632
+ case "NumericLiteral":
633
+ case "BooleanLiteral":
634
+ case "NilLiteral":
635
+ case "VarargLiteral":
636
+ return expr;
637
+ case "BinaryExpression": {
638
+ expr.left = rewriteExpression(expr.left, ctx);
639
+ expr.right = rewriteExpression(expr.right, ctx);
640
+ const folded = ctx.evaluateExpressions ? tryFoldBinary(expr) : undefined;
641
+ if (folded) {
642
+ ctx.changed = true;
643
+ return folded;
644
+ }
645
+ return expr;
646
+ }
647
+ case "LogicalExpression": {
648
+ expr.left = rewriteExpression(expr.left, ctx);
649
+ expr.right = rewriteExpression(expr.right, ctx);
650
+ const folded = ctx.evaluateExpressions ? tryFoldLogical(expr) : undefined;
651
+ if (folded) {
652
+ ctx.changed = true;
653
+ return folded;
654
+ }
655
+ return expr;
656
+ }
657
+ case "UnaryExpression": {
658
+ expr.argument = rewriteExpression(expr.argument, ctx);
659
+ const folded = ctx.evaluateExpressions ? tryFoldUnary(expr) : undefined;
660
+ if (folded) {
661
+ ctx.changed = true;
662
+ return folded;
663
+ }
664
+ return expr;
665
+ }
666
+ case "CallExpression":
667
+ case "TableCallExpression":
668
+ case "StringCallExpression":
669
+ rewriteCallLike(expr, ctx);
670
+ return expr;
671
+ case "IndexExpression":
672
+ expr.base = rewriteExpression(expr.base, ctx);
673
+ expr.index = rewriteExpression(expr.index, ctx);
674
+ return expr;
675
+ case "MemberExpression":
676
+ expr.base = rewriteExpression(expr.base, ctx);
677
+ return expr;
678
+ case "FunctionDeclaration":
679
+ rewriteBlock(expr.body, ctx);
680
+ return expr;
681
+ case "TableConstructorExpression":
682
+ expr.fields = expr.fields.map((field) => {
683
+ if (field.type === "TableKey") {
684
+ field.key = rewriteExpression(field.key, ctx);
685
+ field.value = rewriteExpression(field.value, ctx);
686
+ return field;
687
+ }
688
+ // TableValueとTableKeyString(キー名は書き換えない)
689
+ field.value = rewriteExpression(field.value, ctx);
690
+ return field;
691
+ });
692
+ return expr;
693
+ default: {
694
+ const exhaustive = expr;
695
+ throw new TypeError("Unknown expression type: `" + JSON.stringify(exhaustive) + "`");
696
+ }
697
+ }
698
+ }
699
+ function rewriteAssignmentTarget(v, ctx) {
700
+ if (v.type === "Identifier")
701
+ return v; // 代入の左辺は決して書き換えない
702
+ if (v.type === "IndexExpression") {
703
+ v.base = rewriteExpression(v.base, ctx);
704
+ v.index = rewriteExpression(v.index, ctx);
705
+ return v;
706
+ }
707
+ v.base = rewriteExpression(v.base, ctx);
708
+ return v;
709
+ }
710
+ function rewriteStatement(statement, ctx) {
711
+ switch (statement.type) {
712
+ case "LocalStatement":
713
+ statement.init = statement.init.map((e) => rewriteExpression(e, ctx));
714
+ return;
715
+ case "AssignmentStatement":
716
+ statement.variables = statement.variables.map((v) => rewriteAssignmentTarget(v, ctx));
717
+ statement.init = statement.init.map((e) => rewriteExpression(e, ctx));
718
+ return;
719
+ case "CallStatement":
720
+ rewriteCallLike(statement.expression, ctx);
721
+ return;
722
+ case "DoStatement":
723
+ rewriteBlock(statement.body, ctx);
724
+ return;
725
+ case "WhileStatement":
726
+ statement.condition = rewriteExpression(statement.condition, ctx);
727
+ rewriteBlock(statement.body, ctx);
728
+ return;
729
+ case "RepeatStatement":
730
+ rewriteBlock(statement.body, ctx);
731
+ statement.condition = rewriteExpression(statement.condition, ctx);
732
+ return;
733
+ case "IfStatement":
734
+ statement.clauses.forEach((clause) => {
735
+ if (clause.type !== "ElseClause") {
736
+ clause.condition = rewriteExpression(clause.condition, ctx);
737
+ }
738
+ rewriteBlock(clause.body, ctx);
739
+ });
740
+ return;
741
+ case "ForNumericStatement":
742
+ statement.start = rewriteExpression(statement.start, ctx);
743
+ statement.end = rewriteExpression(statement.end, ctx);
744
+ if (statement.step) {
745
+ statement.step = rewriteExpression(statement.step, ctx);
746
+ }
747
+ rewriteBlock(statement.body, ctx);
748
+ return;
749
+ case "ForGenericStatement":
750
+ statement.iterators = statement.iterators.map((it) => rewriteExpression(it, ctx));
751
+ rewriteBlock(statement.body, ctx);
752
+ return;
753
+ case "FunctionDeclaration":
754
+ // identifier(宣言名)とparametersは決して書き換えない
755
+ rewriteBlock(statement.body, ctx);
756
+ return;
757
+ case "ReturnStatement":
758
+ statement.arguments = statement.arguments.map((a) => rewriteExpression(a, ctx));
759
+ return;
760
+ case "BreakStatement":
761
+ case "LabelStatement":
762
+ case "GotoStatement":
763
+ return;
764
+ default: {
765
+ const exhaustive = statement;
766
+ throw new TypeError("Unknown statement type: `" + JSON.stringify(exhaustive) + "`");
767
+ }
768
+ }
769
+ }
770
+ function rewriteBlock(body, ctx) {
771
+ body.forEach((statement) => {
772
+ rewriteStatement(statement, ctx);
773
+ });
774
+ }
775
+ // ============================================================
776
+ // 入口
777
+ // ============================================================
778
+ /**
779
+ * 定数式の事前計算(畳み込み)と、定数ローカル変数の伝搬を1回分行う。
780
+ * removeUnusedLocalsと同じ形で、変更があればtrueを返す。呼び出し側は変化が
781
+ * 無くなるまで繰り返す。各回の変換は必ずノード数か参照数を減らす
782
+ * (減らない変換を入れると無限ループになる。tryFoldUnaryの終端形保護や
783
+ * intLiteralNodeのINT64_MIN見送りはこの不変条件を守るためのもの)。
784
+ */
785
+ function foldConstants(chunk, resolved, metadata, facts, options = {}) {
786
+ const reassigned = collectReassignedSymbols(facts);
787
+ const propagate = options.propagateLocals === false
788
+ ? new Map()
789
+ : collectPropagationCandidates(chunk.body, resolved, metadata, reassigned);
790
+ const ctx = {
791
+ resolved,
792
+ propagate,
793
+ evaluateExpressions: options.evaluateExpressions !== false,
794
+ changed: false,
795
+ };
796
+ rewriteBlock(chunk.body, ctx);
797
+ return ctx.changed;
798
+ }