プログラミングではたまにエスパー力が必要になることがある

プログラミングをやっていると、たまにエスパー力が必要になることがあります。つまり、不可解な現象に遭遇し、少ない手がかりで問題を解決しなければならない状況です。

私はLunarMLという言語処理系を趣味で作っているのですが、今回はそれの開発中に遭遇した出来事を取り上げます。

LunarMLは、Standard MLという言語のプログラムをLuaやJavaScriptのプログラムに変換するコンパイラーです。開発中にバグを埋め込まないように、多くのテストプログラムを用意して、それが期待通りに動作することを確かめています。これはGitHub Actionsでpushのたびに行っています。

それが、ある日のpushから突然失敗するようになりました。失敗したテストはこれで、ターゲットはLua 5.3です:

https://github.com/minoki/LunarML/blob/8585853f03ed4a60e84b158b90eff31099f4be6b/test/mlbasis/should_run/word.sml

val true = 0w1 + 0w2 = 0w3;
val 0w2 = 0w3 - 0w1;
val 0w2 = 0w3 + Word.~ 0w1;
val 0w2 = 0w3 + (~ 0w1);

これの3行目で「右辺の値が左辺にマッチしない」という例外が飛びました。一応説明しておくと、Standard MLでは符号なし整数(word)は 0w から始め、単項マイナスはチルダ ~ を使います。

該当部分は次のようなLuaコードにコンパイルされていました:

-- ...9700行くらい...
LOCAL_3839260[218] = -0x1
LOCAL_3839260[219] = 0x3 + LOCAL_3839260[218]
LOCAL_3839260[220] = LOCAL_3839260[219] == 0x2
if LOCAL_3839260[220] then
else
  _raise(_Bind, "word.sml:3:5")
end

コンパイル後のコードは問題なさそうです(最適化が不完全なのでコード量は膨大になっていますが)。実際、手元でテストを実行しても成功します。

直近のコミットは入出力関係の機能追加で、符号なし整数の実装はいじっていません。影響としては、せいぜい出力されるコード量が増えたくらいです。

偶発的な問題(宇宙線?)かと思ってGitHub Actionsの失敗したやつをrerunしても、相変わらず失敗します。キャッシュは使っていません。

お手上げなので、この日は問題解決しないまま寝ました。


皆さんはこの状態で原因を推測できたでしょうか?できたらエスパー検定のかなり上位のやつに合格できると思います。


翌日は真面目に原因究明に取り組むことにしました。まず、もう一回rerunしてみます。失敗します。以前成功した時点のやつをrerunします。成功しました。

そうすると、考えにくいのですが「入出力のコードを増やしたら不可思議な力で符号なし整数のテストが失敗するようになった」と考えるしかなさそうです。偶発的なエラーではないのなら、引き金を引いたコミットがどこかにあるはずです。

というわけで調査した結果、問題が発生するようになったのはこのコミットでした:

https://github.com/minoki/LunarML/commit/daeb653b39638ee25a15382c92990bb45ea38cf1

しかし、これは正に「バイナリ入出力のコードを増やしているだけ」で、符号なし整数には影響しなさそうです。実行されないであろうコードが増えただけで不可解なエラーが起こるようになるものでしょうか?


調査は終わりました。あとはエスパーするだけです。あなたは原因を推測できたでしょうか?


まあこの記事のここまでに書いた情報だけでわかったらすごいですね。我々がエスパーと呼んでいるのは、普段意識しない些細な違和感、記事にわざわざ記述しないようなことに注目する能力かもしれません。

調査の過程でGitHub Actionsのログを丁寧に読むと、実行に使用しているLuaのバージョンが5.3.3なことに気づきました。Lua 5.3は5.3.6が最新かつ最終版です。手元では5.3.6を使っていました。Lua処理系のバージョンの違いによって、古いバージョンに存在したバグを踏んでしまったのかもしれません。

Lua 5.3.3の既知のバグ一覧(後のバージョンで修正されたバグ)を見てみると、

「定数を大量に含む関数で間違ったコード生成が行われる」というものがあります。今回踏んだバグがこれかはわかりませんが、可能性はあります。「無関係なコードが増えたらバグるようになった」という状況にも一致します。

果たして、手元でLua 5.3.3を使って問題のテストプログラムを動かしてみると、GitHub Actions上と同じエラーが出るようになりました。


あとは解決方法を考えるだけです。なぜGitHub ActionsでLua 5.3.3という古いバージョンを使っていたかというと、Ubuntu 20.04のaptで入るのがそれだからです。

なぜUbuntu 20.04を使っていたかというと、より新しいUbuntu 22.04のaptではLunarMLをビルドするのに使っているMLton(Standard MLコンパイラー)が入らないからです。

というわけで、解決方法は

  1. Ubuntu 20.04にLua 5.3.6を入れる
  2. Ubuntu 22.04にMLtonを入れる
  3. Ubuntu 24.04を使う

のいずれかです。できれば3.で行きたいところですが、執筆時点ではUbuntu 24.04のrunnerはGitHub Actionsには用意されていないようです。2.のMLtonを自前で入れるのはだるそうなので、ひとまずは1.で行ってみようと思います。


プログラミングをやっていると不可解な現象は日常茶飯事です。その中でも今回の件は「既存のプログラミング言語処理系のバグを踏んだ」ということで、かなり不可解度が高かったのではないでしょうか。エスパー検定の何級に相当しますかね。

この記事を面白いと感じていただけたら、GitHubのLunarMLにスターを頂けると嬉しいです。現在241で、そろそろ250に届くかなあとそわそわしています。