storm-lua-minify 0.2.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 (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +157 -37
  3. package/dist/aggregateSpecialization.js +406 -0
  4. package/dist/ast2lua.js +223 -82
  5. package/dist/astWalk.js +162 -0
  6. package/dist/callGraph.js +372 -0
  7. package/dist/cli.js +53 -32
  8. package/dist/cliOptions.js +36 -0
  9. package/dist/cliProgress.js +87 -0
  10. package/dist/config.js +73 -0
  11. package/dist/constantFold.js +798 -0
  12. package/dist/controlFlow.js +266 -0
  13. package/dist/functionRewrites.js +580 -0
  14. package/dist/generatedAst.js +108 -0
  15. package/dist/generatedNode.js +23 -0
  16. package/dist/globalRename.js +35 -0
  17. package/dist/interproceduralAnalysis.js +842 -0
  18. package/dist/interproceduralConstants.js +120 -0
  19. package/dist/keywordLocator.js +68 -0
  20. package/dist/linker.js +13 -0
  21. package/dist/luaString.js +157 -0
  22. package/dist/minifier.js +1243 -35
  23. package/dist/optimizerAnalysis.js +43 -0
  24. package/dist/optimizerDiagnostics.js +65 -0
  25. package/dist/optimizerFacts.js +529 -0
  26. package/dist/optimizerPass.js +96 -0
  27. package/dist/optimizerTransaction.js +56 -0
  28. package/dist/optimizerValueDomain.js +180 -0
  29. package/dist/options.js +233 -0
  30. package/dist/progress.js +2 -0
  31. package/dist/removeUnused.js +145 -0
  32. package/dist/renamer.js +236 -53
  33. package/dist/resolver.js +37 -14
  34. package/dist/runtimeEnvironment.js +105 -0
  35. package/dist/sourceMetadata.js +314 -0
  36. package/dist/statementDataflow.js +259 -0
  37. package/dist/statementScheduler.js +595 -0
  38. package/dist/symbolLiveness.js +92 -0
  39. package/dist/tableEffects.js +356 -0
  40. package/dist/transform.js +40 -0
  41. package/dist/valueFlow.js +409 -0
  42. package/dist/wholeProgramExports.js +646 -0
  43. package/dist/wholeProgramFieldRenames.js +583 -0
  44. package/dist/wholeProgramFields.js +672 -0
  45. package/dist/wholeProgramObjects.js +783 -0
  46. package/package.json +50 -33
  47. package/.github/workflows/ci.yml +0 -39
  48. package/.github/workflows/publish.yml +0 -39
  49. package/.prettierignore +0 -3
  50. package/.prettierrc.json +0 -1
  51. package/dist/index.js +0 -27
  52. package/eslint.config.js +0 -29
  53. package/src/ast2lua.ts +0 -940
  54. package/src/cli.ts +0 -86
  55. package/src/linker.ts +0 -108
  56. package/src/minifier.ts +0 -284
  57. package/src/output.ts +0 -71
  58. package/src/renamer.ts +0 -134
  59. package/src/resolver.ts +0 -378
  60. package/test/circular-require.test.ts +0 -19
  61. package/test/fixtures/bare-require/main.lua +0 -2
  62. package/test/fixtures/bare-require/mod.lua +0 -1
  63. package/test/fixtures/bitwise-precedence/main.lua +0 -10
  64. package/test/fixtures/circular-require/a.lua +0 -2
  65. package/test/fixtures/circular-require/b.lua +0 -2
  66. package/test/fixtures/circular-require/main.lua +0 -2
  67. package/test/fixtures/dofile/greet.lua +0 -1
  68. package/test/fixtures/dofile/main.lua +0 -2
  69. package/test/fixtures/entry-scope-many-requires/dep_alpha.lua +0 -2
  70. package/test/fixtures/entry-scope-many-requires/dep_bravo.lua +0 -2
  71. package/test/fixtures/entry-scope-many-requires/dep_charlie.lua +0 -2
  72. package/test/fixtures/entry-scope-many-requires/dep_delta.lua +0 -2
  73. package/test/fixtures/entry-scope-many-requires/dep_echo.lua +0 -2
  74. package/test/fixtures/entry-scope-many-requires/dep_foxtrot.lua +0 -2
  75. package/test/fixtures/entry-scope-many-requires/dep_golf.lua +0 -2
  76. package/test/fixtures/entry-scope-many-requires/dep_hotel.lua +0 -2
  77. package/test/fixtures/entry-scope-many-requires/dep_india.lua +0 -2
  78. package/test/fixtures/entry-scope-many-requires/dep_juliet.lua +0 -2
  79. package/test/fixtures/entry-scope-many-requires/dep_kilo.lua +0 -2
  80. package/test/fixtures/entry-scope-many-requires/main.lua +0 -25
  81. package/test/fixtures/multi-require/common.lua +0 -1
  82. package/test/fixtures/multi-require/main.lua +0 -3
  83. package/test/fixtures/nested-module/main.lua +0 -2
  84. package/test/fixtures/nested-module/sub/deep.lua +0 -1
  85. package/test/fixtures/require-call/main.lua +0 -2
  86. package/test/fixtures/require-call/mod.lua +0 -5
  87. package/test/fixtures/require-in-expression/main.lua +0 -1
  88. package/test/fixtures/require-in-expression/mod.lua +0 -1
  89. package/test/fixtures/require-string-call/main.lua +0 -2
  90. package/test/fixtures/require-string-call/mod.lua +0 -5
  91. package/test/fixtures/single-file/main.lua +0 -15
  92. package/test/identifier-collision.test.ts +0 -28
  93. package/test/lib/collision.ts +0 -131
  94. package/test/lib/helpers.ts +0 -124
  95. package/test/no-rename.test.ts +0 -19
  96. package/test/output.test.ts +0 -95
  97. package/test/precedence.test.ts +0 -28
  98. package/test/renamer.test.ts +0 -130
  99. package/test/resolver.test.ts +0 -206
  100. package/test/roundtrip.test.ts +0 -26
  101. package/test/snapshot.test.ts +0 -27
  102. package/test/snapshots/bare-require.sl.lua +0 -1
  103. package/test/snapshots/bitwise-precedence.sl.lua +0 -2
  104. package/test/snapshots/dofile.sl.lua +0 -1
  105. package/test/snapshots/entry-scope-many-requires.m.lua +0 -14
  106. package/test/snapshots/multi-require.m.lua +0 -4
  107. package/test/snapshots/multi-require.sl.lua +0 -1
  108. package/test/snapshots/nested-module.m.lua +0 -4
  109. package/test/snapshots/require-call.m.lua +0 -5
  110. package/test/snapshots/require-call.sl.lua +0 -1
  111. package/test/snapshots/require-in-expression.sl.lua +0 -1
  112. package/test/snapshots/require-string-call.m.lua +0 -5
  113. package/test/snapshots/single-file.sl.lua +0 -6
  114. package/test/sourcemap.test.ts +0 -105
  115. package/tsconfig.eslint.json +0 -8
  116. package/tsconfig.json +0 -111
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Nona Takahara, Mathias Bynens
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nona Takahara, Mathias Bynens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,37 +1,157 @@
1
- # storm-lua-minify
2
-
3
- このプログラムは [mathiasbynens/luamin](https://github.com/mathiasbynens/luamin) をベースにした Stormworks: Build and Rescue 向けのLua minifierです。
4
-
5
- Stormworks: Build and Rescue 以外の用途にも使用可能です。
6
-
7
- # 使い方
8
-
9
- ```
10
- npm i storm-lua-minify
11
-
12
- npx storm-lua-minify script.lua
13
- ```
14
-
15
- - `-m`オプションを付加すると、モジュールの挙動をLuaの実際の挙動に近づけます
16
-
17
- # テスト
18
-
19
- ```
20
- npm ci
21
- npm run build
22
- npm test
23
- ```
24
-
25
- # Lint / Format
26
-
27
- ```
28
- npm run lint # ESLint
29
- npm run format:check # Prettierのフォーマットチェック
30
- npm run format # Prettierでフォーマット
31
- ```
32
-
33
- `test/` 以下にスナップショット・ラウンドトリップパース・識別子衝突検知のテストがあります。
34
- 既知バグ(#11, #12 など)の再現ケースは `test.todo` として登録されており、`npm test` は成功しますが、
35
- 修正が入るまではそのテスト自体は失敗した状態のまま todo 扱いになります。
36
-
37
- スナップショットを更新する場合は `UPDATE_SNAPSHOTS=1 npm test` を実行してください。
1
+ # storm-lua-minify
2
+
3
+ このプログラムは [mathiasbynens/luamin](https://github.com/mathiasbynens/luamin) をベースにした Stormworks: Build and Rescue 向けのLua minifierです。
4
+
5
+ Stormworks: Build and Rescue 以外の用途にも使用可能です。
6
+
7
+ # 使い方
8
+
9
+ ```
10
+ npm i storm-lua-minify
11
+
12
+ npx storm-lua-minify script.lua
13
+ ```
14
+
15
+ ## `require`の展開モード
16
+
17
+ - オプションを設定しないと `require` / `dofile` 先を呼び出し位置へ直接展開する、Stormworks向けLuaを外部で作成する場合に馴染みのある方式となります。
18
+ - ただし、実装上の制約により、文中で `require` を使用するとfunctionで包んで展開されることがあるので注意してください。
19
+ - `-m` / `--require-wrapper`は、`require`の挙動を通常のLuaに近づけます。
20
+ - 冒頭に`require`関数を定義し、モジュールはそこに展開されます。
21
+
22
+ ## コンパイル進捗
23
+
24
+ 対話環境で起動すると、現在のファイル、ステップ番号、処理段階を示す表示が出ます。内部処理の分岐によってステップ数の最大値が途中で増減することがあります。
25
+
26
+ - `--no-progress`で進捗表示を無効化できます。
27
+ - パイプやCIなどの非対話環境では既定で無効です。
28
+ - 非対話環境でも`--progress`を指定するとステップの開始と完了を通常のログ行として標準エラーへ出力します。
29
+
30
+ ## 最適化アノテーション
31
+
32
+ ### EmmyLuaアノテーション
33
+
34
+ EmmyLuaの`class`/継承、`field`、`param`/`return`/`type`、`alias`/`enum`を最適化解析のヒントとして使用します。
35
+
36
+ ### 保護アノテーション(独自)
37
+
38
+ 宣言の直前(空行を挟まない位置)に、次のアノテーションを指定できます。
39
+
40
+ - `--@storm export`: Luaコード外から名前で参照される宣言として、未使用削除と名前短縮の双方から保護します
41
+ - `--@storm keep`: 未使用削除から保護しますが、名前短縮は許可します
42
+ - `--@storm keep-name`: 名前短縮から保護しますが、未使用なら削除を許可します
43
+
44
+ 未知の`--@storm`指示や引数付きの指示は、エラーになります。
45
+
46
+ ## 最適化オプション
47
+
48
+ 最適化オプションは、**個別スイッチ**と**機能グループ**の階層構造を持ち、すべての最適化オプションのスイッチに肯定形と否定形があります。
49
+
50
+ また、最適化オプションとは別に、Luaの動作に仮定を置くことで圧縮強度を高める設定(**仮定モード**)があります。仮定モードは最適化オプションとは別のレイヤーの設定です。
51
+
52
+ ```console
53
+ storm-lua-minify --function-optimizations --no-function-inlining script.lua
54
+ storm-lua-minify --no-optimizations --local-renaming script.lua
55
+ ```
56
+
57
+ 設定ファイルはコマンドと同じキーを使い、平坦に書きます。設定ファイルを使用する際は明示的に`--config`オプションで指定してください。
58
+
59
+ 各オプションの優先度は、コマンド指定値(個別スイッチ) > コマンド指定値(機能グループ) > 設定ファイル指定値(個別スイッチ) > 設定ファイル指定値(機能グループ) > 既定値の順です。
60
+
61
+ ```json
62
+ {
63
+ "runtime-profile": "stormworks",
64
+ "function-optimizations": false,
65
+ "function-inlining": true,
66
+ "global-renaming": true,
67
+ "never-rename-globals": ["onTick", "onDraw"]
68
+ }
69
+ ```
70
+
71
+ ```console
72
+ storm-lua-minify --config storm-lua-minify.json --no-function-inlining script.lua
73
+ ```
74
+
75
+ ### 仮定モード
76
+
77
+ `runtime-profile`の影響を受けます。`runtime-profile`は、コマンドでは`stormworks`が既定です。ライブラリAPIで省略した場合は`lua53`として扱います。
78
+
79
+ これらの機能は実際のコードに対して、圧縮が強まる仮定を置く機能です。仮定が誤っているとコードの結果が変化することがあるため、注意してください。
80
+
81
+ | 仮定 | 既定 | 説明 |
82
+ | ------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | `allow-introspection-changes` | `lua53`: OFF<br>`stormworks`: ON | local名・生存期間、parameter、stack frameなどdebug APIから観測できる変更を許可します。<br>**現時点では`stormworks`プロファイルではOFFできません** |
84
+ | `assume-annotations` | OFF | EmmyLuaアノテーションを最適化の変換根拠として信頼します |
85
+ | `allow-observable-table-read-changes` | OFF | テーブルから読み取ってlocal変数を初期化する処理について、テーブルへの書き込み前へ移動する候補を許可します。local変数の値がテーブルへの書き込み前の値に変わる可能性があります |
86
+
87
+ ### 個別スイッチ
88
+
89
+ | スイッチ | 既定 | 説明 |
90
+ | -------------------------------------- | ------- | ------------------------------------------------- |
91
+ | `local-renaming` | ON | local名を短くします |
92
+ | `local-name-reuse` | ON | 生存期間が重ならないlocalで同じ名前を再利用します |
93
+ | `global-renaming` | **OFF** | 内部で使うglobal名を短くします(※1) |
94
+ | `field-renaming` | ON | 安全に変更できるfield名を短くします |
95
+ | `global-aliasing` | ON | 繰り返し参照するglobalへ短いlocal名を割り当てます |
96
+ | `local-declaration-merging` | ON | 連続するlocal宣言をまとめます |
97
+ | `local-declaration-hoisting` | ON | local宣言を移動してまとめやすくします |
98
+ | `table-read-merging` | ON | tableを読み取るlocal宣言をまとめます |
99
+ | `field-sensitive-table-effects` | ON | tableへの書き込みの影響をfield単位で判定します |
100
+ | `constant-expression-evaluation` | **OFF** | 定数だけからなる式を事前に計算します |
101
+ | `local-constant-propagation` | **OFF** | 再代入されないlocalの定数を参照先へ伝えます |
102
+ | `interprocedural-constant-propagation` | **OFF** | 関数の呼び出しを越えて定数を伝えます |
103
+ | `parameter-pruning` | ON | 使われない関数parameterを取り除きます |
104
+ | `function-inlining` | ON | 関数呼び出しを関数本体で置き換えます |
105
+ | `function-specialization` | ON | 呼び出し方に合わせて関数を特殊化します |
106
+ | `field-value-propagation` | ON | 安定したfieldの値を参照先へ伝えます |
107
+ | `unused-local-removal` | ON | 未使用のlocalを取り除きます |
108
+ | `unused-function-removal` | ON | 未使用のlocal関数を取り除きます |
109
+ | `unused-field-initializer-removal` | ON | 読み取られないfieldの初期化を取り除きます |
110
+ | `unused-export-removal` | ON | entryから到達できないmodule exportを取り除きます |
111
+
112
+ 安全性の条件を満たさない候補は、スイッチが有効でも実行されません。
113
+
114
+ #### `--global-renaming` から短縮を保護する(※1)
115
+
116
+ `--global-renaming`を有効にすると、このツールは「プログラム中のどこかで代入されているグローバル名」を内部専用とみなしてリネームします。
117
+ これは通常のスクリプト内部の状態変数には安全ですが、**実行環境が直接呼び出す関数名・変数名をうまく検出できません**。
118
+
119
+ 短縮から保護する必要がある名前を`never-rename-globals`へ列挙するか、前述の`--@storm export`によって保護してください。
120
+ コマンドでは`--name-rename-globals <保護名>`を繰り返し記述します。設定ファイルでは`never-rename-globals`に保護する名前の配列を与えます。
121
+
122
+ ### 機能グループ
123
+
124
+ 機能グループのスイッチを設定すると、個別スイッチをまとめてオンオフします。
125
+
126
+ | 機能グループ | 個別スイッチ |
127
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
128
+ | `optimizations` | 以下の全機能グループ(他の機能グループ設定の方が優先されます) |
129
+ | `identifier-optimizations` | `local-renaming`, `local-name-reuse`, `global-renaming`, `field-renaming`, `global-aliasing` |
130
+ | `statement-optimizations` | `local-declaration-merging`, `local-declaration-hoisting`, `table-read-merging`, `field-sensitive-table-effects` |
131
+ | `constant-optimizations` | `constant-expression-evaluation`, `local-constant-propagation`, `interprocedural-constant-propagation` |
132
+ | `function-optimizations` | `parameter-pruning`, `function-inlining`, `function-specialization` |
133
+ | `object-optimizations` | `field-value-propagation` |
134
+ | `dead-code-optimizations` | `unused-code-removal`, `unused-export-removal` |
135
+ | `unused-code-removal` | `unused-local-removal`, `unused-function-removal`, `unused-field-initializer-removal` |
136
+
137
+ ## Source Map
138
+
139
+ このツールは、圧縮後のコードと合わせて [Source Map](https://tc39.es/source-map/) (`.lua.map`) を出力します。
140
+
141
+ `.lua.map` の `sourcesContent` には、エントリファイルおよび `require`/`dofile` で参照される全モジュールの元テキストがそのまま埋め込まれますので、Source Mapファイル単体で元コードを参照できます。
142
+
143
+ ### sourceMappingURLアノテーションの出力形式
144
+
145
+ 出力される `.min.lua` の末尾に付く `sourceMappingURL` アノテーションの書式は、3種類から選べます。
146
+
147
+ [本ツール作者による紹介記事](https://nonasaba.net/blog/entry16.html)で紹介したツールでは、Luaコードとしての有効性を保ちながら認識させるには`legacy`を使う必要があるため、非標準動作を既定としています。
148
+
149
+ | `--source-mapping-url-style` | 書式 | 特徴 |
150
+ | ---------------------------- | -------------------------------------- | ------------------------------------------------- |
151
+ | `legacy`(既定) | 複数行の `--[[`〜`]]` ブロックコメント | 従来形式 |
152
+ | `line` | 単一行の `--` ラインコメント | 有効なLuaのままSource Map仕様の最終行規則を満たす |
153
+ | `strict` | Luaコメントで包まないマーカー文字列 | 最終行は有効なLua文ではなくなる |
154
+
155
+ # 開発者向けガイド
156
+
157
+ 開発環境の構築、テスト構成、最適化を追加するときの判断基準、`scripts/`の調査・検証コマンドについては[開発者向けガイド](DEVELOPMENT.md)を参照してください。
@@ -0,0 +1,406 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAggregateSpecialization = applyAggregateSpecialization;
4
+ const astWalk_1 = require("./astWalk");
5
+ const generatedNode_1 = require("./generatedNode");
6
+ const MAX_VARIANTS = 128;
7
+ const MAX_CANDIDATE_SITES = 2048;
8
+ /**
9
+ * Build one callable-oriented plan for direct, method, and stable-field calls.
10
+ * Every site is normalized to the callee parameter order; method receiver and
11
+ * callback base evaluation become explicit leading actuals before grouping.
12
+ */
13
+ function applyAggregateSpecialization(objects, fields, modules) {
14
+ if (objects.generation !== fields.generation)
15
+ throw new Error("Cannot specialize mixed whole-program generations");
16
+ const moduleOfCallable = new Map();
17
+ objects.modules.forEach((module) => {
18
+ const target = modules.find((candidate) => candidate.name === module.name);
19
+ if (!target)
20
+ return;
21
+ module.analysis.callGraph.functions.forEach((callable) => moduleOfCallable.set(callable, target));
22
+ });
23
+ const moduleOfNode = new WeakMap();
24
+ modules.forEach((module) => {
25
+ indexNodes(module.chunk.body, module, moduleOfNode);
26
+ });
27
+ const methods = new Map(objects.resolvedMethods.map((method) => [method.call.call, method]));
28
+ const callbacks = new Map(fields.resolvedCallbacks.map((callback) => [callback.call.call, callback]));
29
+ const recursive = new Set(fields.callGraph.sccs
30
+ .filter((scc) => scc.recursive)
31
+ .flatMap((scc) => scc.functions));
32
+ const sitesByCallable = new Map();
33
+ fields.callGraph.calls.forEach((site) => {
34
+ if (site.hasUnknownTarget || site.targets.size !== 1)
35
+ return;
36
+ const callable = [...site.targets][0];
37
+ if (site.call.type !== "CallExpression")
38
+ return;
39
+ const module = moduleOfNode.get(site.owner);
40
+ if (!module)
41
+ return;
42
+ const method = methods.get(site.call);
43
+ const callback = callbacks.get(site.call);
44
+ let actuals = site.call.arguments;
45
+ let parameterOffset = 0;
46
+ if (method)
47
+ actuals = [method.receiver, ...site.call.arguments];
48
+ else if (callback && site.call.base.type === "MemberExpression") {
49
+ // Preserve evaluation of the callback receiver before its arguments.
50
+ actuals = [site.call.base.base, ...site.call.arguments];
51
+ parameterOffset = 1;
52
+ }
53
+ const normalized = sitesByCallable.get(callable) ?? [];
54
+ normalized.push({ site, callable, module, actuals, parameterOffset });
55
+ sitesByCallable.set(callable, normalized);
56
+ });
57
+ const constantParameters = inferConstantParameters(sitesByCallable);
58
+ const diagnostics = [];
59
+ const unavailableNames = new Set(modules.flatMap((module) => [
60
+ ...module.resolved.symbols.map((symbol) => symbol.name),
61
+ ...module.resolved.globals.keys(),
62
+ ]));
63
+ const callbackFields = new Map();
64
+ fields.facts.forEach((fact) => {
65
+ if (fact.value?.kind !== "function")
66
+ return;
67
+ const stored = callbackFields.get(fact.value.callable) ?? new Set();
68
+ stored.add(fact.field);
69
+ callbackFields.set(fact.value.callable, stored);
70
+ if (fact.invalidationReasons.has("field-reassignment"))
71
+ diagnostics.push({
72
+ callable: fact.value.callable,
73
+ reason: "callback-reassignment",
74
+ count: 1,
75
+ });
76
+ else if (fact.invalidationReasons.size > 0)
77
+ diagnostics.push({
78
+ callable: fact.value.callable,
79
+ reason: "dynamic-dispatch",
80
+ count: 1,
81
+ });
82
+ });
83
+ callbackFields.forEach((stored, callable) => {
84
+ if (stored.size > 1)
85
+ diagnostics.push({
86
+ callable,
87
+ reason: "multiple-callback-storage",
88
+ count: stored.size,
89
+ });
90
+ });
91
+ let candidateCallables = 0;
92
+ let createdVariants = 0;
93
+ let replacedCalls = 0;
94
+ const introducedByBody = new Map();
95
+ const totalSites = [...sitesByCallable.values()].reduce((sum, sites) => sum + sites.length, 0);
96
+ if (totalSites > MAX_CANDIDATE_SITES) {
97
+ sitesByCallable.forEach((_, callable) => diagnostics.push({ callable, reason: "resource-budget", count: 1 }));
98
+ return {
99
+ changed: false,
100
+ candidateCallables: sitesByCallable.size,
101
+ createdVariants: 0,
102
+ replacedCalls: 0,
103
+ diagnostics,
104
+ };
105
+ }
106
+ sitesByCallable.forEach((sites, callable) => {
107
+ const ownerModule = moduleOfCallable.get(callable);
108
+ if (!ownerModule || sites.length === 0)
109
+ return;
110
+ candidateCallables++;
111
+ const refusal = refusalOf(callable, sites, ownerModule, recursive);
112
+ if (refusal) {
113
+ diagnostics.push({ callable, reason: refusal, count: 1 });
114
+ return;
115
+ }
116
+ const groups = variantGroups(callable, sites, constantParameters);
117
+ groups.forEach((group) => {
118
+ if (createdVariants >= MAX_VARIANTS) {
119
+ diagnostics.push({ callable, reason: "resource-budget", count: 1 });
120
+ return;
121
+ }
122
+ const insertionModule = group.sites[0].module;
123
+ if (group.sites.some((site) => site.module !== insertionModule))
124
+ return;
125
+ const firstOwner = group.sites[0].site.owner;
126
+ const insertionBody = bodyContaining(insertionModule.chunk.body, firstOwner);
127
+ if (!insertionBody ||
128
+ group.sites.some((site) => bodyContaining(insertionModule.chunk.body, site.site.owner) !==
129
+ insertionBody)) {
130
+ diagnostics.push({ callable, reason: "unknown-capture", count: 1 });
131
+ return;
132
+ }
133
+ const introduced = introducedByBody.get(insertionBody) ?? 0;
134
+ if (introduced >= insertionModule.maxIntroducedLocalsAt(firstOwner)) {
135
+ diagnostics.push({ callable, reason: "resource-budget", count: 1 });
136
+ return;
137
+ }
138
+ let variantIndex = createdVariants;
139
+ let variantName = `__stormVariant${String(variantIndex)}`;
140
+ while (unavailableNames.has(variantName)) {
141
+ variantIndex++;
142
+ variantName = `__stormVariant${String(variantIndex)}`;
143
+ }
144
+ unavailableNames.add(variantName);
145
+ const variant = makeVariant(callable, group, variantName, ownerModule);
146
+ if (!variant) {
147
+ diagnostics.push({ callable, reason: "unknown-capture", count: 1 });
148
+ return;
149
+ }
150
+ insertionBody.splice(insertionBody.indexOf(firstOwner), 0, variant);
151
+ introducedByBody.set(insertionBody, introduced + 1);
152
+ group.sites.forEach((normalized) => {
153
+ const call = normalized.site.call;
154
+ if (call.type !== "CallExpression")
155
+ return;
156
+ const retainedActuals = normalized.actuals.filter((_, index) => {
157
+ const parameterIndex = index - normalized.parameterOffset;
158
+ return parameterIndex < 0 || !group.literals.has(parameterIndex);
159
+ });
160
+ const replacement = {
161
+ type: "Identifier",
162
+ name: variantName,
163
+ };
164
+ (0, generatedNode_1.copyNodeOrigin)(replacement, call.base);
165
+ call.base = replacement;
166
+ call.arguments = retainedActuals.map((actual) => structuredClone(actual));
167
+ replacedCalls++;
168
+ });
169
+ createdVariants++;
170
+ diagnostics.push({
171
+ callable,
172
+ reason: "variant-created",
173
+ count: group.sites.length,
174
+ });
175
+ });
176
+ });
177
+ return {
178
+ changed: createdVariants > 0,
179
+ candidateCallables,
180
+ createdVariants,
181
+ replacedCalls,
182
+ diagnostics,
183
+ };
184
+ }
185
+ function refusalOf(callable, sites, ownerModule, recursive) {
186
+ if (recursive.has(callable))
187
+ return "recursive-function";
188
+ if (callable.declaration.parameters.some((parameter) => parameter.type === "VarargLiteral"))
189
+ return "vararg-function";
190
+ if (callable.declaration.parameters.length === 0)
191
+ return "unsupported-shape";
192
+ const annotations = ownerModule.metadata.annotationsOf(callable.declaration);
193
+ if (annotations.keep || annotations.exported)
194
+ return "unsupported-shape";
195
+ if (!captureSafe(callable, sites, ownerModule))
196
+ return "unknown-capture";
197
+ return undefined;
198
+ }
199
+ function captureSafe(callable, sites, ownerModule) {
200
+ const resolved = ownerModule.resolved;
201
+ const functionScope = resolved.scopeOfFunction(callable.declaration);
202
+ if (!functionScope)
203
+ return false;
204
+ const state = { safe: true, captures: false };
205
+ callable.declaration.body.forEach((statement) => {
206
+ (0, astWalk_1.walkStatement)(statement, {
207
+ onIdentifierReference: (identifier) => {
208
+ const symbol = resolved.symbolOf(identifier);
209
+ if (symbol && !insideScope(symbol, functionScope))
210
+ state.captures = true;
211
+ },
212
+ onFunction: () => {
213
+ state.safe = false;
214
+ },
215
+ });
216
+ });
217
+ return (state.safe &&
218
+ (!state.captures || sites.every((site) => site.module === ownerModule)));
219
+ }
220
+ function insideScope(symbol, scope) {
221
+ for (let current = symbol.scope; current; current = current.parent)
222
+ if (current === scope)
223
+ return true;
224
+ return false;
225
+ }
226
+ function variantGroups(callable, sites, constantParameters) {
227
+ const candidates = new Map();
228
+ sites.forEach((site) => {
229
+ callable.declaration.parameters.forEach((_, parameterIndex) => {
230
+ const actual = site.actuals.at(parameterIndex + site.parameterOffset);
231
+ const direct = primitiveLiteral(actual);
232
+ const actualSymbol = !direct && actual?.type === "Identifier"
233
+ ? site.module.resolved.symbolOf(actual)
234
+ : undefined;
235
+ const literal = direct ??
236
+ (actualSymbol ? constantParameters.get(actualSymbol) : undefined);
237
+ if (!literal)
238
+ return;
239
+ const key = `${String(parameterIndex)}:${literalKey(literal)}`;
240
+ const candidate = candidates.get(key) ?? {
241
+ parameterIndex,
242
+ literal,
243
+ sites: [],
244
+ derived: direct === undefined,
245
+ };
246
+ candidate.sites.push(site);
247
+ if (!direct)
248
+ candidate.derived = true;
249
+ candidates.set(key, candidate);
250
+ });
251
+ });
252
+ const assigned = new Set();
253
+ return [...candidates.entries()]
254
+ .filter(([, candidate]) => candidate.sites.length >= 2 || candidate.derived)
255
+ .sort(([leftKey, left], [rightKey, right]) => right.sites.length - left.sites.length ||
256
+ left.parameterIndex - right.parameterIndex ||
257
+ leftKey.localeCompare(rightKey))
258
+ .flatMap(([key, candidate]) => {
259
+ const grouped = candidate.sites.filter((site) => !assigned.has(site));
260
+ if (grouped.length < 2 && !candidate.derived)
261
+ return [];
262
+ grouped.forEach((site) => assigned.add(site));
263
+ return [
264
+ {
265
+ key,
266
+ sites: grouped,
267
+ literals: new Map([[candidate.parameterIndex, candidate.literal]]),
268
+ },
269
+ ];
270
+ });
271
+ }
272
+ function inferConstantParameters(sitesByCallable) {
273
+ const result = new Map();
274
+ let changed = true;
275
+ while (changed) {
276
+ changed = false;
277
+ sitesByCallable.forEach((sites, callable) => {
278
+ callable.parameters.forEach((parameter, parameterIndex) => {
279
+ if (result.has(parameter) || sites.length === 0)
280
+ return;
281
+ const values = sites.map((site) => {
282
+ const actual = site.actuals.at(parameterIndex + site.parameterOffset);
283
+ const direct = primitiveLiteral(actual);
284
+ if (direct)
285
+ return direct;
286
+ if (!actual || actual.type !== "Identifier")
287
+ return undefined;
288
+ const symbol = site.module.resolved.symbolOf(actual);
289
+ return symbol ? result.get(symbol) : undefined;
290
+ });
291
+ const first = values[0];
292
+ if (!first ||
293
+ values.some((value) => !value || literalKey(value) !== literalKey(first)))
294
+ return;
295
+ result.set(parameter, first);
296
+ changed = true;
297
+ });
298
+ });
299
+ }
300
+ return result;
301
+ }
302
+ function makeVariant(callable, group, name, ownerModule) {
303
+ const declaration = callable.declaration;
304
+ if (declaration.parameters.some((parameter) => parameter.type !== "Identifier"))
305
+ return undefined;
306
+ const copiedBody = declaration.body.map((statement) => structuredClone(statement));
307
+ replaceParameterReferences(declaration.body, copiedBody, ownerModule.resolved, new Map([...group.literals].map(([index, literal]) => [callable.parameters[index], literal])));
308
+ const parameters = declaration.parameters
309
+ .filter((_, index) => !group.literals.has(index))
310
+ .map((parameter) => structuredClone(parameter));
311
+ if (group.sites[0].parameterOffset > 0)
312
+ parameters.unshift({ type: "Identifier", name: "__stormReceiver" });
313
+ const identifier = { type: "Identifier", name };
314
+ (0, generatedNode_1.copyNodeOrigin)(identifier, declaration.identifier ?? declaration);
315
+ const variant = {
316
+ type: "FunctionDeclaration",
317
+ identifier,
318
+ isLocal: true,
319
+ parameters,
320
+ body: copiedBody,
321
+ };
322
+ (0, generatedNode_1.copyNodeOrigin)(variant, declaration);
323
+ return variant;
324
+ }
325
+ function replaceParameterReferences(originals, copies, resolved, literals) {
326
+ const visit = (original, copy) => {
327
+ if (!original ||
328
+ !copy ||
329
+ typeof original !== "object" ||
330
+ typeof copy !== "object")
331
+ return;
332
+ if (Array.isArray(original)) {
333
+ if (!Array.isArray(copy))
334
+ return;
335
+ original.forEach((item, index) => {
336
+ visit(item, copy[index]);
337
+ });
338
+ return;
339
+ }
340
+ if (original.type === "Identifier") {
341
+ const symbol = resolved.symbolOf(original);
342
+ const literal = symbol ? literals.get(symbol) : undefined;
343
+ if (literal)
344
+ Object.assign(copy, structuredClone(literal));
345
+ return;
346
+ }
347
+ Object.keys(original).forEach((key) => {
348
+ visit(original[key], copy[key]);
349
+ });
350
+ };
351
+ visit(originals, copies);
352
+ }
353
+ function primitiveLiteral(expression) {
354
+ if (!expression)
355
+ return { type: "NilLiteral", value: null, raw: "nil" };
356
+ switch (expression.type) {
357
+ case "StringLiteral":
358
+ case "NumericLiteral":
359
+ case "BooleanLiteral":
360
+ case "NilLiteral":
361
+ return expression;
362
+ default:
363
+ return undefined;
364
+ }
365
+ }
366
+ function literalKey(literal) {
367
+ return `${literal.type}:${literal.raw}`;
368
+ }
369
+ function indexNodes(body, module, index) {
370
+ const visit = (value) => {
371
+ if (!value || typeof value !== "object")
372
+ return;
373
+ index.set(value, module);
374
+ if (Array.isArray(value))
375
+ value.forEach(visit);
376
+ else
377
+ Object.values(value).forEach(visit);
378
+ };
379
+ visit(body);
380
+ }
381
+ function bodyContaining(body, owner) {
382
+ if (body.includes(owner))
383
+ return body;
384
+ for (const statement of body)
385
+ for (const child of childBlocks(statement)) {
386
+ const found = bodyContaining(child, owner);
387
+ if (found)
388
+ return found;
389
+ }
390
+ return undefined;
391
+ }
392
+ function childBlocks(statement) {
393
+ switch (statement.type) {
394
+ case "DoStatement":
395
+ case "WhileStatement":
396
+ case "RepeatStatement":
397
+ case "FunctionDeclaration":
398
+ case "ForNumericStatement":
399
+ case "ForGenericStatement":
400
+ return [statement.body];
401
+ case "IfStatement":
402
+ return statement.clauses.map((clause) => clause.body);
403
+ default:
404
+ return [];
405
+ }
406
+ }