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.
- package/README.md +119 -41
- package/dist/aggregateSpecialization.js +406 -0
- package/dist/ast2lua.js +156 -68
- package/dist/astWalk.js +162 -0
- package/dist/callGraph.js +372 -0
- package/dist/cli.js +53 -58
- package/dist/cliOptions.js +36 -0
- package/dist/cliProgress.js +87 -0
- package/dist/config.js +73 -0
- package/dist/constantFold.js +798 -0
- package/dist/controlFlow.js +266 -0
- package/dist/functionRewrites.js +580 -0
- package/dist/generatedAst.js +108 -0
- package/dist/generatedNode.js +23 -0
- package/dist/interproceduralAnalysis.js +842 -0
- package/dist/interproceduralConstants.js +120 -0
- package/dist/luaString.js +157 -0
- package/dist/minifier.js +1178 -44
- package/dist/optimizerAnalysis.js +43 -0
- package/dist/optimizerDiagnostics.js +65 -0
- package/dist/optimizerFacts.js +529 -0
- package/dist/optimizerPass.js +96 -0
- package/dist/optimizerTransaction.js +56 -0
- package/dist/optimizerValueDomain.js +180 -0
- package/dist/options.js +233 -0
- package/dist/progress.js +2 -0
- package/dist/removeUnused.js +145 -0
- package/dist/renamer.js +223 -54
- package/dist/resolver.js +28 -11
- package/dist/runtimeEnvironment.js +105 -0
- package/dist/sourceMetadata.js +314 -0
- package/dist/statementDataflow.js +259 -0
- package/dist/statementScheduler.js +595 -0
- package/dist/symbolLiveness.js +92 -0
- package/dist/tableEffects.js +356 -0
- package/dist/transform.js +10 -371
- package/dist/valueFlow.js +409 -0
- package/dist/wholeProgramExports.js +646 -0
- package/dist/wholeProgramFieldRenames.js +583 -0
- package/dist/wholeProgramFields.js +672 -0
- package/dist/wholeProgramObjects.js +783 -0
- package/package.json +11 -2
- package/dist/index.js +0 -27
package/README.md
CHANGED
|
@@ -12,68 +12,146 @@ npm i storm-lua-minify
|
|
|
12
12
|
npx storm-lua-minify script.lua
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## `require`の展開モード
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
- オプションを設定しないと `require` / `dofile` 先を呼び出し位置へ直接展開する、Stormworks向けLuaを外部で作成する場合に馴染みのある方式となります。
|
|
18
|
+
- ただし、実装上の制約により、文中で `require` を使用するとfunctionで包んで展開されることがあるので注意してください。
|
|
19
|
+
- `-m` / `--require-wrapper`は、`require`の挙動を通常のLuaに近づけます。
|
|
20
|
+
- 冒頭に`require`関数を定義し、モジュールはそこに展開されます。
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
## コンパイル進捗
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
対話環境で起動すると、現在のファイル、ステップ番号、処理段階を示す表示が出ます。内部処理の分岐によってステップ数の最大値が途中で増減することがあります。
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
| ---------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
-
| (既定) | 複数行の `--[[`〜`]]` ブロックコメント | 過去のバージョンと同じ出力。既存ツールとの互換性を優先する場合はこのまま使う |
|
|
28
|
-
| `--single-line-source-mapping-url` | 単一行の `--` ラインコメント | 有効なLuaのまま、Source Map仕様が定める「アノテーションは生成コードの最終行に置く」という規則を満たす |
|
|
29
|
-
| `--strict-source-mapping-url` | Luaコメントで包まないマーカー文字列そのまま | 規則を厳密に満たすが、出力ファイルの最終行は有効なLua文ではなくなる(Lua文法上、`//`から始まる行をコメント化なしに構文解析可能にする方法は存在しないため) |
|
|
26
|
+
- `--no-progress`で進捗表示を無効化できます。
|
|
27
|
+
- パイプやCIなどの非対話環境では既定で無効です。
|
|
28
|
+
- 非対話環境でも`--progress`を指定するとステップの開始と完了を通常のログ行として標準エラーへ出力します。
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
## 最適化アノテーション
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
### EmmyLuaアノテーション
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
EmmyLuaの`class`/継承、`field`、`param`/`return`/`type`、`alias`/`enum`を最適化解析のヒントとして使用します。
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
### 保護アノテーション(独自)
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
宣言の直前(空行を挟まない位置)に、次のアノテーションを指定できます。
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
- `--@storm export`: Luaコード外から名前で参照される宣言として、未使用削除と名前短縮の双方から保護します
|
|
41
|
+
- `--@storm keep`: 未使用削除から保護しますが、名前短縮は許可します
|
|
42
|
+
- `--@storm keep-name`: 名前短縮から保護しますが、未使用なら削除を許可します
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
未知の`--@storm`指示や引数付きの指示は、エラーになります。
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
- `--no-merge-locals`: 連続する`local`変数宣言のまとめ上げを無効にします
|
|
47
|
-
- `--no-global-rename`: 代入されている(=スクリプト内部でのみ使われている)グローバル識別子の短縮を無効にします
|
|
48
|
-
- `--no-global-alias`: 短縮できない外部グローバル識別子(`screen`など、代入されず参照のみされるもの)を、頻出する場合にローカル変数へ代入して短縮する最適化を無効にします
|
|
49
|
-
- `--reserved-globals-config <path>`: `{"neverRenameGlobals": ["name", ...]}`形式のJSONファイルを指定し、**代入されていても常にリネームしないグローバル名**を列挙します
|
|
46
|
+
## 最適化オプション
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
最適化オプションは、**個別スイッチ**と**機能グループ**の階層構造を持ち、すべての最適化オプションのスイッチに肯定形と否定形があります。
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
また、最適化オプションとは別に、Luaの動作に仮定を置くことで圧縮強度を高める設定(**仮定モード**)があります。仮定モードは最適化オプションとは別のレイヤーの設定です。
|
|
54
51
|
|
|
55
|
-
|
|
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
56
|
|
|
57
|
-
|
|
57
|
+
設定ファイルはコマンドと同じキーを使い、平坦に書きます。設定ファイルを使用する際は明示的に`--config`オプションで指定してください。
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
各オプションの優先度は、コマンド指定値(個別スイッチ) > コマンド指定値(機能グループ) > 設定ファイル指定値(個別スイッチ) > 設定ファイル指定値(機能グループ) > 既定値の順です。
|
|
60
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
|
+
}
|
|
61
69
|
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
storm-lua-minify --config storm-lua-minify.json --no-function-inlining script.lua
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
### 仮定モード
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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文ではなくなる |
|
|
74
154
|
|
|
75
|
-
|
|
76
|
-
既知バグ(#11, #12 など)の再現ケースは `test.todo` として登録されており、`npm test` は成功しますが、
|
|
77
|
-
修正が入るまではそのテスト自体は失敗した状態のまま todo 扱いになります。
|
|
155
|
+
# 開発者向けガイド
|
|
78
156
|
|
|
79
|
-
|
|
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
|
+
}
|