🕳️

Cycloneのソースリポジトリを蘇生してみる (中編)

2022/07/25に公開

前回の続きです。

Bisonのデバッグ

パーサーのエラーハンドリングがおかしいのは困るので調査を進めてみます。まずは最小再現ケースを作ります。こんな感じのヘッダを用意して、

// test.h
int x;
foo;

こんな感じのcysファイルを用意して、

// test.cys
test.h:
;

buildlibを実行してみます。

bin/buildlib -Bbin/lib/cyc-lib -d buildlib-test fixtures/test.cys -I$(realpath ./fixtures)

これで再現しました。

まず、エラー発生行がよくわからないのでcycbisonを改造してもう少し親切にします。

diff --git a/tools/bison/reader.cyc b/tools/bison/reader.cyc
index 93fffb0b..9cb93e56 100644
--- a/tools/bison/reader.cyc
+++ b/tools/bison/reader.cyc
@@ -782,14 +782,13 @@ parse_type_decl ()
     if(union_typename != NULL)
       vars = rprintf(pr,"<%s>",union_typename);
     fprintf((FILE @)fattrs,
-            "\nstatic %s yyget_%s(union YYSTYPE %s@yy1 ) {\n"
-            "  static string_t s = \"%s\";\n"
+            "\nstatic %s yyget_%s(union YYSTYPE %s@yy1, int line ) {\n"
             "  switch(yy1) {\n"
             "  case &{.%s = yy}: return yy;\n"
-            "  default: yythrowfail(s);\n"
+            "  default: yythrowfail(aprintf(\"%s (at %%d)\", line));\n"
             "  } \n"
             "}",
-            name,deconstructor,vars,name,deconstructor);
+            name,deconstructor,vars,deconstructor,name);
     fprintf((FILE @)fattrs, "\nstatic union YYSTYPE %s %s(%s yy1) { return YYSTYPE{.%s = yy1}; }",vars,deconstructor,name,deconstructor);
   }

@@ -1467,7 +1466,7 @@ copy_guard(symbol_list *rule, int stack_offset)
                type_name = strdup("BOGUS");
              }

-             fprintf((FILE @)fguard, "yyget_%s(&yyvsp[%d].v)", type2nth(type_name), n-stack_offset);
+             fprintf((FILE @)fguard, "yyget_%s(&yyvsp[%d].v, __LINE__)", type2nth(type_name), n-stack_offset);
              continue;
            }
          else if (c == '(') {
@@ -1758,15 +1757,15 @@ copy_action(symbol_list *rule, int stack_offset)
                    /* DAN: calling functions makes for smaller files. */
                    fprintf((FILE @)faction," yyget_%s",type2nth(type_name));
                    if (m==0)
-                     fprintf((FILE @)faction, "(&yyyvsp[%d].v)",n-1);
+                     fprintf((FILE @)faction, "(&yyyvsp[%d].v, __LINE__)",n-1);
                    else if (m > 0)
                       /* JGM: we'll get a compile-time type error if we
                        * try to convert this to a use of yyyvsp, because
                        * the value needs to be negative...
                        */
-                     fprintf((FILE @)faction, "(&yyvs[yyvsp_offset+%d].v)", m);
+                     fprintf((FILE @)faction, "(&yyvs[yyvsp_offset+%d].v, __LINE__)", m);
                    else
-                     fprintf((FILE @)faction, "(&yyyvsp[%d].v)",n-1);
+                     fprintf((FILE @)faction, "(&yyyvsp[%d].v, __LINE__)",n-1);

                    /*
                    fprintf(faction,"; yyinternal;})");

make cyclone_src && make update && make をやるとブートストラップが一周して変更が反映されます。 (これ毎回やるのめんどい……)

試してみると1340行目でエラーになっていることがわかります。これだけだと $1$2 のどちらが原因かわからないので、間に改行を入れて実行してみると、 $1 のほうで問題が起きていることがわかってきます。

つまり、 external_declarationのsemantic valueがうまく入っていないということが考えられます。奇しくもexternal_declarationはparse.yの中で唯一エラーハンドリング (errorトークンの処理) を行っている箇所です。なのでだいたい仮説として

  • (A) errorトークンを処理するルールのsemantic actionがおかしい。
  • (B) errorトークンを処理するときにparse.yの外で変なことが起こっている。

のどちらかだろうと見当がつきます。

というわけでerrorルールのところで yythrowfail("error in external_declaration"); と書いて強制終了させてみます。 ……何も起きません。なので上の仮説 (B) のほうなんじゃないかなという予感がしてきます。

生成されたコードを眺めていると、YYDEBUGという定数をオンにするともう少し詳しいことがわかりそうです。なのでつけてもう一度回してみます。デバッグ出力のせいでlibc.cysに対するbuildlib実行はほぼ止まらなくなってしまいますが、その辺りが動く段階でbuildlib自体は完成しているのでそっちは強制的に止めてテストコードに対してbuildlibを実行します。以下のログが出てきます。

Starting parse
Entering state 0
Reading a token: Next token is 266 (INT?)
Shifting token 266 (INT), Entering state 10
Reducing via rule 93 (line 1690), INT  -> type_specifier_notypedef
state stack now 0
Entering state 72
Reducing via rule 88 (line 1679), type_specifier_notypedef  -> type_specifier
state stack now 0
Entering state 71
Reading a token: Next token is 388 (IDENTIFIER"x")
Reducing via rule 64 (line 1602), type_specifier  -> declaration_specifiers
state stack now 0
Entering state 68
Next token is 388 (IDENTIFIER"x")
Shifting token 388 (IDENTIFIER), Entering state 45
Reducing via rule 595 (line 3291), IDENTIFIER  -> qual_opt_identifier
state stack now 0 68
Entering state 86
Reducing via rule 186 (line 1995), qual_opt_identifier  -> direct_declarator
state stack now 0 68
Entering state 81
Reading a token: Next token is 59 (';'"x")
Reducing via rule 182 (line 1978), direct_declarator  -> declarator
state stack now 0 68
Entering state 153
Next token is 59 (';'"x")
Reducing via rule 143 (line 1844), declarator  -> init_declarator
state stack now 0 68
Entering state 152
Reducing via rule 141 (line 1837), init_declarator  -> init_declarator_list_rev
state stack now 0 68
Entering state 151
Next token is 59 (';'"x")
Shifting token 59 (';'), Entering state 317
Reducing via rule 52 (line 1541), declaration_specifiers init_declarator_list_rev ';'  -> declaration
state stack now 0
Entering state 67
Reducing via rule 35 (line 1452), declaration  -> external_declaration
state stack now 0
Entering state 62
Reading a token: Next token is 388 (IDENTIFIER"foo")
Shifting token 388 (IDENTIFIER), Entering state 45
Reducing via rule 595 (line 3291), IDENTIFIER  -> qual_opt_identifier
state stack now 0 62
Entering state 86
Reducing via rule 186 (line 1995), qual_opt_identifier  -> direct_declarator
state stack now 0 62
Entering state 81
Reading a token: Next token is 59 (';'"foo")
Reducing via rule 182 (line 1978), direct_declarator  -> declarator
state stack now 0 62
Entering state 80
Next token is 59 (';'"foo")
Error: state stack now 0 62
Shifting error token, Entering state 62
Next token is 59 (';'"foo")
Discarding token 59 (';').
Shifting error token, Entering state 62
Reading a token: Now at end of input.
Reducing via rule 15 (line 1388),  -> translation_unit
state stack now 0 62 62 62
Entering state 144
Reducing via rule 4 (line 1338), external_declaration translation_unit  -> translation_unit

途中で例のエラーが出て強制退場を食らっているのでログは尻切れトンボです。

たまたま最近CRubyのparse.yと格闘していてBisonの気持ちがわかるので、追加情報としてエラーリカバリの説明だけ読めばだいたい見当がつきます。このログの最後のほうのスタック遷移は

0 [external_declaration] 62 [declarator] 80 | ';'
↓ error (pop the stack)
0 [external_declaration] 62 | ';'
↓ push the error token
0 [external_declaration] 62 [error] 62 | ';'

となっていますが、push error後の状態がおかしいです。このスタックなら次に error → external_declaration のreductionが起きないといけないので前と同じstateのはずがありません。

生成されたコード (parsetab.cyc) をさらに読み進めてみるとyyerrhandleでyystateの更新処理が抜けていることがわかります。

そこでこの辺りのコードからこんな感じの行が抜けていることがわかるので、これを足せばいい感じになりそうです。

→ ちゃんと直りました。最高。buildlibに -v を渡すとエラーも表示してくれます。

https://github.com/qnighy/Cyclone-Language/commit/6e4c010c4628885fb47d36fcd3e15e2c20b6aa8e

https://github.com/qnighy/Cyclone-Language/commit/e84892335419a1efecafae682b72508a373de050

パースエラーを直す

パースエラーが正しくハンドルされるようになったことで、パースできない定義があってもスキップされるだけになりました。とはいえ失敗した定義があるのはよろしくありません (その定義に対応するCycloneのスタブが生成されなくなってしまうため)

今後のために、最新のGCCをそのまま使ってもパースエラーなしに動くようにしておきます。

といっても手順自体は簡単です。前回追加したオプションを元に戻して、 -v をつけて実行するとパースエラーが出力されます。これを

  • パースエラーに書かれている行番号 (プリプロセッサによる展開前の位置)
  • 展開後の出力

と比較して、どの構文が悪さをしているかを調べて、それをパーサーに実装します。 (とりあえずパースが通ればいいのでそれなりに手を抜く)

https://github.com/qnighy/Cyclone-Language/commit/176587c88c1def232472e4b8a6136e867d300a74

テストを直す

あとで気が向いたらもう少しちゃんと改造したいので、既存のテストが動いていることを確認します。

make test はインストール済みのCycloneがないと動かない雰囲気があります。色々調べてみるとこれとは別に make test_boot があり、こちらを直せば良さそうでした。

結局、

  • LDFLAGSが無いせいでビルドできない一例
  • CCがハードコードされているせいで意図したアーキテクチャとは異なるアーキテクチャで実行されていた一例

の2例を直すだけでOKでした。

https://github.com/qnighy/Cyclone-Language/commit/4266984b2d9ae807f22c5aa1aa2c3a71752d41cd

gitignoreの整備

元々Subversionで管理されていたこともあり、gitignoreが全く整備されていませんでした。これもメンテナンスする上で困るので、おおよそいい感じのgitignoreを用意しました。

https://github.com/qnighy/Cyclone-Language/commit/94580d6bb8bbf351e88e4fa246ccf12983ed9d08

まとめ

最低限のメンテナンスができるようになりました。

しかし、現代では32bitな開発環境 (正確には sizeof(int) == sizeof(void*) な環境が必要) はなかなかありません。今回のようにmultiarch環境を作ることは可能ですが、やはり再現性の観点からはこのままではよろしくないような気がします。

なので、次回があれば64bit環境でも動くようにする作業をすることになるでしょう。その時が来るかは、わかりません。

Discussion