Cycloneのソースリポジトリを蘇生してみる (前編)
Cycloneとは
CycloneはRustのリージョン推論の原型のひとつになった実験的なプログラミング言語です。
現在はメンテナンスされていませんが、歴史的な意義があることからCycloneのビルド環境を整備してみました。
(完結するかは未定)
ソースの取得
CycloneのWebサイトは生きているので、ソースはCycloneのDownloadページから取得できます。しかしここに不穏な文言があります。
If you use gcc 4, you must get the latest version of Cyclone from SVN (see below).
最新安定版よりも新しい版があること、またgccのバージョンに依存して壊れることが読み取れます。そしてSubversionと書いてあることから嫌な予感がした人もいると思いますが、このリポジトリは既に動いていません。
というわけで野良クローンを探すと、gitでクローンしたリポジトリがGitHub上に存在しました。
それなりに新しそうな雰囲気もあります (新しいといっても2009年とかですが)。 今回はこれをありがたく使うことにします🙏🙏🙏🙏。知らん奴のリポジトリを信頼するなという忠告は黙殺します。
こういうのはいつ無くなるかわからないのでとりあえず複製を作っておきます。
64bitで動かない
とりあえずソースを取得して ./configure
するとコケます (コンソールログ略)。ポインタ長のチェックがありそこで弾かれています。
config/configure.ac
が大元っぽいので、怪しい箇所をコメントアウトしてconfigureを通してみます。
autoreconf config/configure.ac && mv config/configure configure
./configure
これでconfigureは通りますが、案の定makeするとむちゃくちゃwarningが出てやがてsegfaultします (コンソールログ略)。
64bitで動くようにしたいところですが、ここで問題があります。Cycloneコンパイラはセルフホストされているので、まずはコンパイラを作らないと修正もままならないのです。というわけで一旦64bitで動かすのはあきらめて32bitの環境を作ります。手元環境はUbuntuなので、DebianのMultiarch/HOWTOを参考に環境を整えます。
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6-dev:i386 libc6-dbg:i386 ligcc-9-dev:i386
-m32
が渡った状態でconfigureすると通ります。なぜかarchはx86_64-unknown-linux-gnuということになっていますがとりあえずこのまま強行します。
CC="gcc -m32 -g" ./configure
セルフホストについて
セルフホストについて説明してなかったので少し書いておきます。要するにCycloneコンパイラ自体はCycloneコンパイラで書かれているので鶏卵問題/ブートストラップ問題が起きています。
ではこの問題をCycloneではどう解決しているかというと、CycloneはCへのトランスパイラでバックエンドはgccに任せているので、トランスパイル済みのCycloneコンパイラのCコードを同梱することで入口を作っています。Cycloneが作られた当時はLLVMのような使いやすいコンパイラバックエンドは無かった (orメジャーではなかった) はずなので、割と自然な判断ではないかと思います。
ただ、現代でいうところのBabelなどのように入力と出力の対応関係が比較的明確なトランスパイラとは違い、Cycloneの場合はかなり色々と展開したものが出てくるので、手で修正するのはかなり厳しいです。
Boehm GC
-m32
をつけると途中まではいい感じで進みますが、やはり生成したツールの実行でsegfaultが出てしまいます (コンソールログ略)。
仕方ないのでvalgrindをつけて再実行してみると (というかx86_64のvalgrindがi386のコードをどうやって実行しているんだ??) 、実はCyclone本体じゃなくてBoehm GCの初期化処理で失敗していることがわかります (コンソールログ略)。
Boehm GCのことはよくわかりませんが、まあどうせ新しくすれば直るだろうということで最新リリースのtarballを引っぱってきて置き換えます。7.1から8.0.6にしました。
これで問題の箇所は通過できました。更新されているソフトウェアは神。そして、更新を続けている方々も神です。
Error reading spec file
./configure結果の一部は bin/lib/cyc-lib/x86_64-unknown-linux-gnu/cycspecs
というファイルに吐かれるのですが、これの読み取り中になんかエラーが出ます。ここで出てるみたいです。
CPPのデフォルト定義の一覧が4096文字からはみ出てるのが原因っぽいです。こういうのはだいたい積み増しされていく運命にあるから仕方ないね。というわけで定数を書き換えて65536とかにしてしまいます。
お気付きのことかと思いますが、「CycloneコンパイラをビルドできないとCycloneコンパイラは修正できない」と言ったそばから生成されたCソースを書き換えています。幸運にも定数4096はファイル内で1箇所しかなかったので、生成されたコードの4096を全部65536に置き換えればOKというタネです。
<header> is not supported on this platform
さらに進めていくと "stdlib.h is not supported on this platform" みたいなエラーが出てきます。「stdlib.hがサポートされていないプラットフォーム」……って、そんなことあるかい。
これは結論から言うと最近のCの構文とか最近のGCCの独自構文をCycloneのパーサーが認識できないことが問題です。ログが build/x86_64-unknown-linux-gnu/include/BUILDLIB.LOG
に出ているので確認しています。いやーロギングをちゃんと行っているソフトウェアは神。
stdlib.h:
(中略)
Got Core::Failure(list_t<decl_t,`H>)
Not supported on this platform
??????????
??????????
前言撤回です。何もわかりませんでした。
後知恵で説明すると、これはCycloneが実装している独自Bison内のエラーで、非終端記号のsemantic valueに代入したときの型と取り出そうとしたときの型が違うことで発生しています。多分構文エラーからの復帰にバグがあるんじゃないかな。さすが安全性を重視する言語の先駆けだけあってちゃんとエラーになって偉い。でもこのログじゃわからん。
CPPのオプションをいじる
↑の処理はそもそも何をしているかというと、CのヘッダーからCycloneのヘッダーを作っています。OSでどんな関数が使えるかとか各種定数の値とかは環境固有なのでCヘッダーから取得したいです。一方でCのヘッダーにはCycloneの型チェックに必要な情報が不足しているので、 libc.cys
というファイルに書いておきます。この2つの情報源を合わせてCycloneのヘッダーを作るのがbuildlibというツールの仕事で、その中でパーサーが失敗しているせいでヘッダーがうまく作れてないというわけです。
Cのヘッダーをどうパースしているのかというと、プリプロセスまではGCCに任せてそこから先は自前のパーサーでパースしています。すごい。プリプロセッサだけを実行する方法はStackOverflowとかに書いてある方法と同じです。
# プリプロセッサ定義を取り出す
echo "#include <stdlib.h>" | $(CC) -dM -E -
# Cの定義を取り出す
echo "#include <stdlib.h>" | $(CC) -E -
なので、まずこのプリプロセッサのオプションをいじる方法を探します。先ほどいじっていた cycspecs
というファイルがconfigureの出力なので、ここをいじればどうやら反映されることはわかりました。
*cyclone_cc:
gcc -m32 -g
# ↑ここをいじるとbuildlibの挙動もいい感じに変わる
ただ(後からわかったことですが)これをいじるとbuildlibより後のフェーズでのgccのオプションも変わってしまいます。これは色々と困るんですが、特に -I
でインクルードパスを挿入したいときに困ります。OSのstdlib.hから生成されるCycloneヘッダの名前もstdlib.hなのですが、
- buildlibを使うときは、stdlib.hを独自版に差し替えてデバッグしたい
- 以降ではCycloneヘッダのほうが優先されてほしい
というのがうまく反映されなかったりします。
そこでbuildlibを呼んだときだけオプションを足す方法を考えます。buildlib内でgccを呼び出している箇所を見ると、cycspecs由来のオプション以外に cppargs
というのを渡していて、これはコマンドラインオプション由来であることがわかりました。
ブートストラップ時のbuildlibの呼び出しはこの行で行われているので、ここにオプションを足せばOKです。 (この正解を引き当てるのも結構大変だったけど面白くないので省略)
他にもbuildlibの呼び出し箇所はあるんですが、まあ最初に作ったCycloneヘッダを使い回せば当面困らないので放置しておきます。
さてここで困ったのが、コマンドラインオプションはどうやって渡すのが正解なのか?という点です。
最初はbuildlibの中をブラックボックスと思って試せばうまくいくかなーと思ったのですがなかなか正解を引き当てられなかったので、もう少し情報を見ながら探索することにしました。そこでstraceの登場です。
strace -f -e execve -o strace.txt 以下コマンド
とやると、中で起動されたコマンドが全部記録されるので、所望のgccコマンドが起動されているかどうかを確認しました。すると以下のことがわかりました。
- buildlibは
-
で始まる引数のうち自身が理解したなかったものをgccにフォワードする - 特に
-I /path/to/dir
と書くと-I
はgccにフォワードされるが/path/to/dir
はbuildlibに食われて生き別れになってしまうのが罠だった-
-I/path/to/dir
ならOK
-
これで無事にテストできるようになりました
(なお後でわかったことですが、buildlibに -v
をつけると内部で実行しているgccコマンドが表示されるので、これが先にわかっていればstraceを使う必要はありませんでした)
二分探索
これでプリプロセッサに干渉する手段を得ました (実際の手順は少し前後していて、先に二分探索をしてからbuildlibのオプションを調べています)。これにより本来OSが持っているファイルのかわりに自分たちのファイルをCycloneに食わせることができます。
なのでデバッグをしていきます。プリプロセッサはだいたい羃等なので、先にプリプロセッサを適用したものを作っておきます。 (これだとマクロ定義が消えてしまいますが、デバッグしたいのはパーサーのほうなので結果としてはOKでした)
echo "#include <stdlib.h>" | gcc -m32 -E - > overrides/stdlib.h
このoverridesディレクトリを -I
で指定しておきます。
あとは、 overrides/stdlib.h
のある行から下を消してbuildlibを実行して成否を見れば、残した範囲内に問題があるかどうかがわかるので、二分探索で問題箇所を絞り込めます。結果としてはだいたい以下のような問題があることがわかりました。
-
_Float128
,float128
などの新しめの型定義がパースできない。 - 特定の条件下で vfscanf, vscanf, vsscanf を
__asm__
関数属性でリネームしようとするが、リネームがうまく認識されず(?)に重複定義が発生する。
ここまでわかったら、展開前のヘッダを確認しながらなぜなに問答をはじめます。「このマクロ定義を無理矢理置き換えればどうにかなるはずだ」というものを見つける作業です。
結論としては以下の2つを入れれば動くようだということがわかりました。
-
-std=gnu99
...__STDC__
の値が小さくなり、新しいCの構文が使われなくなる -
-D__GNUC__=0
... GNU特有の構文が使われなくなる
まとめ
ここまでで32bitのCycloneコンパイラがビルドされるところまではできました。
しかし、ヘッダが変わるとすぐ対処不能になる問題や、64bit対応ができていないのはかなり厳しいです。何とかしたいですね。
……何とかしたら誰が得するんだ????
Discussion