Closed4

Prismのソースコードリーティング

nasanasa

モチベ

RubyのパーサーPrismについて知りたい。特にエラートレラントについて理解を深めたい。

下記の3点について理解したい

  • どのような構文エラーに対して機能するのか
  • 構文の推論をどのように行っているのか
  • 新しく対応したい構文エラーがあったらどこに手を入れたら良いのか
nasanasa

エラートレラントのエントリーポイントを探す

詳細は省略するがparser構造体のrecoveringがtrueのときにエラーからの復帰が試行されることが分かった。

recoveringのチェックは12箇所でおこなわれている。これらのどこかがエラートレラントのエントリーポイントになっているかもしれない。

~/lab/oss/prism / - main
:) % rg "parser->recovering"
src/prism.c
11175:        if (parser->recovering) {
11178:            if (context_terminator(context, &parser->current)) parser->recovering = false;
11513:        if (PM_NODE_TYPE_P(argument, PM_MISSING_NODE) || parser->recovering) break;
11818:                    if (parser->recovering) {
11901:                        if (parser->recovering) {
14294:                if (parser->recovering) {
14297:                    if (match1(parser, PM_TOKEN_PARENTHESIS_RIGHT)) parser->recovering = false;
15423:                if (parser->recovering) {
15574:                    if (!parser->recovering) {
16414:                parser->recovering = true;
17187:            if (parser->recovering) {
17375: * Consumers of this function should always check parser->recovering to

と思ったがrecoveringは単にパーサーの状態を示すだけでエントリーポイントらしきものは見つからない。。。

下記のように(defキーワードのパース時にendキーワードが見つからなかった場合など)ASTを作りながら「このケースはエラーですよね。あなたがやりたいことはこれですよね」を人為的に決定して実装されているようです。つまり構文情報というものがあり、そこから論理的に近しいトークンを推論するって訳じゃなさそう。

https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/src/prism.c#L15354-L15357

調査ログ

エラートレラントのエントリーポイントになってそうなところを探していく。

Prism.parse(source)

このメソッド起点にしてコードを追っていきます。

PrismモジュールがC言語で定義されている
https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/ext/prism/extension.c#L1040-L1096

続いてPrism.parseの定義
https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/ext/prism/extension.c#L1080

parse関数を呼んでいるだけ。parse関数ではparse_inputを呼んでる。
https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/ext/prism/extension.c#L632-L679

parse_inputではpm_parseを読んでる

https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/ext/prism/extension.c#L605-L610

nasanasa

現在実装されているエラー

エラーメッセージが定数定義されているのでこの一覧を見るとどのようなものがあるのか理解できる。
https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/src/diagnostic.c#L53-L273

テストコード。ASTを入力としてエラーメッセージが期待する出力であることをテストしている。
こちらの方が定数一覧を見るより分かりやすい

https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/test/prism/errors_test.rb#L9

nasanasa

defined?キーワードのパースを読む

defined?キーワードパースを読んで下記に答えてみる。
defined?を選んだのは非常にシンプルだから。

構文の推論をどのように行っているのか
新しく対応したい構文エラーがあったらどこに手を入れたら良いのか

実装はここ

https://github.com/ruby/prism/blob/3f00d9f0743c948f2c1768dce4716ff499b927ce/src/prism.c#L15388-L15420

case PM_TOKEN_KEYWORD_DEFINED: {
    // 略...

    // 括弧が省略されていない時
    // ex. defined?(expression)
    if (accept1(parser, PM_TOKEN_PARENTHESIS_LEFT)) {
        lparen = parser->previous;

        // 式のパース。今回は本題じゃない
        expression = parse_expression(parser, PM_BINDING_POWER_COMPOSITION, true, PM_ERR_DEFINED_EXPRESSION);

        // `parser->recovering`はエラーから復帰中のときにtrueになる
        // どのトークンで埋めるか自明じゃないときにcontextにそれを残しておいてどっかでリカバリーするっぽい?
        if (parser->recovering) {
            rparen = not_provided(parser);
        } else {
            // 指定したトークンであれば読み飛ばす。ここでは改行であれば読み飛ばす
            // 下記の書き方に対応するため
            // ex.
            // ```ruby
            // defined?(
            //  expression
            //            ^ ここの改行に対応
            // )
            // ```
            accept1(parser, PM_TOKEN_NEWLINE);

            // ** ここが本題 **
            // 開始括弧(`lparen`)が存在するか確認している。
            // 存在しなければ第三引数`PM_ERR_EXPECT_RPAREN`エラーを`parser`構造体のerrorsに追加する。
            // その後`PM_TOKEN_MISSING`で埋める
            expect1(parser, PM_TOKEN_PARENTHESIS_RIGHT, PM_ERR_EXPECT_RPAREN);
            rparen = parser->previous;
        }
    // 括弧が省略時
    // ex. defined? expression
    } else {
        // not_providedは括弧が省略されているこのを示すトークンを返す関数
        lparen = not_provided(parser);
        rparen = not_provided(parser);

        // 式のパース。今回は本題じゃない
        expression = parse_expression(parser, PM_BINDING_POWER_DEFINED, false, PM_ERR_DEFINED_EXPRESSION);
    }

    return (pm_node_t *) pm_defined_node_create(
        parser,
        &lparen,
        expression,
        &rparen,
        &PM_LOCATION_TOKEN_VALUE(&keyword)
    );
}

「閉じ括弧がない」っていう自明なケースを例に上げて説明した。
基本的な方針としては、必要なトークンがないときはエラーを配列に詰めて、足りないトークンをPM_TOKEN_MISSINGで埋めるのが基本方針っぽい。

このスクラップは2024/01/26にクローズされました