Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Lrama へのコントリビューションを通して学ぶ Ruby のパーサジェネレータ事情

Lrama へのコントリビューションを通して学ぶ Ruby のパーサジェネレータ事情

Junichi Kobayashi

September 09, 2023
Tweet

More Decks by Junichi Kobayashi

Other Decks in Programming

Transcript

  1. 自己紹介 • 小林純一 • X / GitHub: @junk0612 • 株式会社永和システムマネジメント

    アジャイル事業部 ◦ RubyxAgile グループ ◦ 構文解析器研究部員 • 音ゲーマー・ボードゲーマー
  2. 構文解析器の構成要素 • レキサー ◦ テキストを読み込んで、トークンに分割するプログラム ◦ トークンに分割することをトークナイズという • パーサー ◦

    トークン列を読み込んで、内部構造を取り出すプログラム ◦ コンパイラ系では「ソースコード → AST」 ▪ AST: Abstract Syntax Tree (抽象構文木) ◦ JSON や CSV のパーサーでは「テキスト → データ構造」 • パーサージェネレーター ◦ 文法ファイルを読み込んで、パーサーを生成するプログラム
  3. CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード

    パーサー 生成器 パーサージェネレーター 文法ファイル パーサー 構文解析器の構成要素
  4. CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード

    パーサー 生成器 パーサージェネレーター 文法ファイル レキサー トークン列 AST パーサー 生成器 パーサー 構文解析器の構成要素
  5. 言語処理系の用語 • 形式言語 ◦ 「言語」を数学的・集合論的に取り扱う言語学の分野 ◦ 言語がどのようなテキストとして表されるかを考える ▪ 人間に意味のある言葉として認識されるかは気にしない ▪

    英語なら「アルファベットの羅列で、たまにスペースや 記号が入る」など ◦ 言語を構成する記号と、それらを結びつける文法からなる
  6. 言語処理系の用語 • 文脈自由言語 (Context Free Grammar: CFG) ◦ 形式言語の1つで、以下の形式で表されるもの ▪

    rule: A B C ... | D E F ... ▪ このような記法を BNF(Backus-Naur Form) という ◦ 世の中のほとんどのプログラミング言語が属する ◦ パーサージェネレーターの入力となる文法ファイルに使われる ◦ 他の文法によって展開できる記号を非終端記号という ◦ それ以上展開できない記号を終端記号という
  7. 文脈自由言語と BNF number: digit digit digit: '0' | '1' |

    '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  8. 文脈自由言語と BNF number: digit digit digit: '0' | '1' |

    '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 00 ~ 99 の2桁の数字全体を表す言語
  9. 文脈自由言語と BNF expression: digit '+' digit | digit '-' digit

    | digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  10. 文脈自由言語と BNF expression: digit '+' digit | digit '-' digit

    | digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁の数の2項四則演算式を表す言語
  11. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  12. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression
  13. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression / digit
  14. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression ) / digit
  15. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression + digit ) / digit
  16. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / digit
  17. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + digit ) / digit
  18. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / digit
  19. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / 3
  20. 文脈自由言語と BNF expression: digit | expression '+' digit | expression

    '-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁のかっこ付き四則演算式を表す言語
  21. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( 2 + 7 ) / 3
  22. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + 7 ) / 3
  23. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / 3
  24. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( digit + digit ) / digit
  25. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression + digit ) / digit
  26. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression ) / digit
  27. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression / digit
  28. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression
  29. 実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'

    digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' Accepted!
  30. Lrama とは • Bison を代替するために作られた Ruby 製パーサージェネレーター • RubyKaigi 2023

    で kaneko.y さんが発表した ◦ 詳しくは https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL • CRuby 3.3 から導入される ◦ preview1 には入っているので、今この場でお試しいただけます ◦ ※ Ruby のふるまいは変わりません
  31. Lrama を導入するメリット • Bison のバージョンに引きずられずに済む ◦ Bison はユーザーごとにバージョンが異なるので、古いものがインス トールされていることを想定しなければいけない ◦

    新しい機能が導入されても使えない • Ruby 独自の実装ができる ◦ LSP 向けに書きかけのコードをいい感じにパースする ◦ 複雑になっている文法のパースをうまくやる
  32. Lrama が目指すところ • Usability • Maintainability • Universal Parser •

    詳しくは RubyKaigi 2023 での kaneko.y さんの発表をどうぞ ◦ https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL
  33. Universal Parser • 世の中にはさまざまな Ruby 実装があり、 それぞれ独自のパーサーを利用している ◦ mruby や

    JRuby といった別実装 ◦ sorbet や typeprof のようなツール • 現在の CRuby のパーサーは CRuby 独自の機能に依存しており 移植が難しくなっている • 文法ファイルを整理することで、他実装にも利用できるパーサーを 作りたい
  34. Lrama 周辺のパーサー事情 • YARP ◦ Bison が作ったパーサーを置き換えるべく作られている CRuby 向けの手書き (!)

    パーサー ◦ JRuby / TruffleRuby での実績もあるらしい • Bison ◦ GNU が開発した Yacc の後継のパーサージェネレーター ◦ CRuby においては、parse.y を読み込んで Ruby のパーサーを 出力するために使われていた • Racc ◦ 青木峰郎さんの開発したパーサージェネレーター ◦ parser gem (RuboCop の依存 gem) などで使われている
  35. Named References とは • Bison の機能の 1 つ • Action

    内の References として非終端記号名を利用できる
  36. Named References とは • Bison の機能の 1 つ • Action

    内の References として非終端記号名を利用できる 🤔
  37. %{ Prologue (約 1500 行) %} Bison declarations (約 200

    行) %% Grammar rules (約 4500 行) %% Epilogue (約 8300 行) Bison 文法ファイルの構造 かっこ内は CRuby の parse.y における行数
  38. %{ Prologue (約 1500 行) %} Bison declarations (約 200

    行) %% Grammar rules (約 4500 行) ← 今回はここの話 %% Epilogue (約 8300 行) Bison 文法ファイルの構造
  39. Grammar rules の構造 rule_name: rule rule .. rule { action

    } | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 }
  40. Grammar rules の構造 rule_name: rule rule .. rule { action

    } | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } ← | NUMBER '-' expression { $$ = $1 - $3 } ← 今回はここの話 | '(' expression ')' { $$ = $2 } ←
  41. Action とは • Bison が生成するパーサーは、なにもしなければ 文法に則った入力になっているかどうかしか教えてくれない ◦ AST を作ったり、後の処理に必要な情報を保存したりはしない •

    各文法の後ろに {} でくくってプログラムを書くことができる • $n や @n を使うと、文法中の記号の値を使って処理が行える ◦ この機能を (Numbered) References という
  42. Action の具体例 expression: NUMBER '+' expression { $$ = $1

    + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
  43. Action の具体例 expression: NUMBER '+' expression { $$ = $1

    + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
  44. Action の具体例 expression: NUMBER '+' expression { $$ = $1

    + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
  45. Action の具体例 expression: NUMBER '+' expression { $$ = $1

    + $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
  46. Named References とは • Numbered References の問題点 ◦ 記述的でなくわかりにくい ◦

    場所の番号で指定するため、 文法が変更になると書き直さなければならない • 上記の問題を解消するため、非終端記号名を参照して 値を使えるようにした機能が Named References
  47. Named References とは expression: NUMBER '+' expression { $$ =

    $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] }
  48. Named References とは expression: NUMBER '+' expression { $$ =

    $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } 前に $ をつけてルール名で値を参照できる
  49. Named References とは expression: NUMBER '+' expression { $$ =

    $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } ルール記述側で [] でくくると別名をつけられる
  50. Named References とは expression: NUMBER '+' expression { $$ =

    $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] } ルール名や別名に記号を含む場合は 呼び出し側も [] でくくれば OK
  51. Named References とは expression: NUMBER '+' expression { $$ =

    $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 } expression[result]: NUMBER '+' expression[rest] { $result = $NUMBER + $rest } | NUMBER '-' expression[rest] { $result = $NUMBER - $rest } | '(' expression[inside-exp] ')' { $result = $[inside-exp] }
  52. ここまでのまとめ • やったことの説明 ◦ Lrama に Named References を実装した ▪

    Lrama は Bison を置き換えるために作られた パーサージェネレーター ▪ Named References は Bison が持つ機能で、Action 内の References として非終端記号名を利用できる
  53. 今後やりたいこと • 内部パーサーの Racc による自動生成化 ◦ WIP の PR →

    https://github.com/ruby/lrama/pull/62 • Bison の機能の取り込み • kaneko.y さんによるやりたいことリスト → https://docs.google.com/document/d/1EAZzYMXBOdzK-6mMIj 2YNJxZZRVcpJxE7-4zXbHn8JA/edit?usp=sharing