Prismのソースコードリーティング
モチベ
RubyのパーサーPrismについて知りたい。特にエラートレラントについて理解を深めたい。
下記の3点について理解したい
- どのような構文エラーに対して機能するのか
- 構文の推論をどのように行っているのか
- 新しく対応したい構文エラーがあったらどこに手を入れたら良いのか
エラートレラントのエントリーポイントを探す
-
Prism.parse
(ここまでRuby) - parse関数 (こっからC)
- parse_input関数
- pm_parse
- parse_program
- parse_statements
- parser->recovering
詳細は省略するが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を作りながら「このケースはエラーですよね。あなたがやりたいことはこれですよね」を人為的に決定して実装されているようです。つまり構文情報というものがあり、そこから論理的に近しいトークンを推論するって訳じゃなさそう。
調査ログ
エラートレラントのエントリーポイントになってそうなところを探していく。
Prism.parse(source)
このメソッド起点にしてコードを追っていきます。
Prism
モジュールがC言語で定義されている
続いてPrism.parse
の定義
parse関数を呼んでいるだけ。parse関数ではparse_inputを呼んでる。
parse_inputではpm_parseを読んでる
現在実装されているエラー
エラーメッセージが定数定義されているのでこの一覧を見るとどのようなものがあるのか理解できる。
テストコード。ASTを入力としてエラーメッセージが期待する出力であることをテストしている。
こちらの方が定数一覧を見るより分かりやすい
defined?
キーワードのパースを読む
defined?
キーワードパースを読んで下記に答えてみる。
defined?
を選んだのは非常にシンプルだから。
構文の推論をどのように行っているのか
新しく対応したい構文エラーがあったらどこに手を入れたら良いのか
実装はここ
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
で埋めるのが基本方針っぽい。