Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Lrama へのコントリビューションを通して学ぶ Ruby のパーサジェネレータ事情
Search
Junichi Kobayashi
September 09, 2023
Programming
7.3k
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Lrama へのコントリビューションを通して学ぶ Ruby のパーサジェネレータ事情
Junichi Kobayashi
September 09, 2023
More Decks by Junichi Kobayashi
See All by Junichi Kobayashi
rage against annotate_predecessor
junk0612
0
260
The Implementations of Advanced LR Parser Algorithm
junk0612
3
2.7k
「今のプロジェクトいろいろ大変なんですよ、app/services とかもあって……」/After Kaigi on Rails 2024 LT Night
junk0612
6
2.9k
LR で JSON パーサーを作る / Coding LR JSON Parser
junk0612
2
1.9k
「ナントカLR」を整理する / Clarifying LR Algorithms
junk0612
1
700
From LALR to IELR: A Lrama's Next Step
junk0612
2
5k
RubyConf Taiwan / Understanding Parser Generators surrounding Ruby with Contributing Lrama
junk0612
2
7.2k
LL法とLR法の違いは?調べてみた!-完全版-/Comparing LL and LR parse algorithm -EX Edition-
junk0612
0
2k
ESM Super LT/Comparing LL and LR parse algorithm
junk0612
1
230
Other Decks in Programming
See All in Programming
「AIで開発し、AIを届ける」をEvalでつなぐ 〜AIネイティブに始めるプロダクト開発の実践〜 / Connecting "Develop with AI, deliver AI" with Eval
rkaga
4
5.1k
Vue × Nuxt × Oxc どこまで使える?実運用の現在地
andpad
0
250
ADKを使って簡単にAIエージェントを作ってみよう
k1mu21
0
260
技術記事、AIに書かせるか、自分で書くか? 〜それでも私が自分の手で書く理由〜 / #QiitaConference
jnchito
2
1.4k
PHPで使える日時の表現と、その知り方 #frontend_phpcon_do
o0h
PRO
0
240
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
540
例外の正しい扱い方 そのエラー try-catchして大丈夫?
jinwatanabe
0
230
Creating Composable Callables in Contemporary C++
rollbear
0
130
AIだと陥りがちなJakarta EE最新技術への移行時の落とし穴と解決策
tnagao7
0
110
エージェンティックRAGにAWSで入門しよう!
har1101
8
1.5k
IBM Bobを活用したレガシーアプリの最新化
oniak3ibm
PRO
1
200
Claspは野良GASの夢をみるか
takter00
0
190
Featured
See All Featured
The Language of Interfaces
destraynor
162
27k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
2
4k
Color Theory Basics | Prateek | Gurzu
gurzu
0
360
Navigating Weather and Climate Data
rabernat
0
220
The SEO Collaboration Effect
kristinabergwall1
1
480
Building Flexible Design Systems
yeseniaperezcruz
330
40k
The Limits of Empathy - UXLibs8
cassininazir
1
360
Visualization
eitanlees
152
17k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.2k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
17k
Transcript
Lrama へのコントリビューションを通して学ぶ Ruby のパーサージェネレーター事情 株式会社永和システムマネジメント アジャイル事業部 小林 純一 (@junk0612) 大阪
Ruby 会議 03 ハートンホテル心斎橋別館 松風ホール 2023/09/09(Sat.)
小林 純一 (@junk0612)
自己紹介 • 小林純一 • X / GitHub: @junk0612 • 株式会社永和システムマネジメント
アジャイル事業部 ◦ RubyxAgile グループ ◦ 構文解析器研究部員 • 音ゲーマー・ボードゲーマー
None
本日のおしながき • パーサー周りの基礎知識 • Lrama にコントリビューションした話 • 実装を通して知った Lrama の内部構造
• 今後やってみたいこと
パーサー周りの基礎知識
パーサー周りの基礎知識 • 構文解析器の構成要素 ◦ レキサー ◦ パーサー ◦ パーサージェネレーター •
言語処理系の用語 ◦ 形式言語 ◦ 文脈自由言語 ◦ BNF
構文解析器の構成要素 • レキサー ◦ テキストを読み込んで、トークンに分割するプログラム ◦ トークンに分割することをトークナイズという • パーサー ◦
トークン列を読み込んで、内部構造を取り出すプログラム ◦ コンパイラ系では「ソースコード → AST」 ▪ AST: Abstract Syntax Tree (抽象構文木) ◦ JSON や CSV のパーサーでは「テキスト → データ構造」 • パーサージェネレーター ◦ 文法ファイルを読み込んで、パーサーを生成するプログラム
CRuby 実行環境 .rb ファイル Ruby VM 構文解析器の構成要素
CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード
パーサー 生成器 構文解析器の構成要素
CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード
パーサー 生成器 パーサージェネレーター 文法ファイル パーサー 構文解析器の構成要素
CRuby 実行環境 .rb ファイル Ruby VM レキサー トークン列 AST バイトコード
パーサー 生成器 パーサージェネレーター 文法ファイル レキサー トークン列 AST パーサー 生成器 パーサー 構文解析器の構成要素
言語処理系の用語 • 形式言語 ◦ 「言語」を数学的・集合論的に取り扱う言語学の分野 ◦ 言語がどのようなテキストとして表されるかを考える ▪ 人間に意味のある言葉として認識されるかは気にしない ▪
英語なら「アルファベットの羅列で、たまにスペースや 記号が入る」など ◦ 言語を構成する記号と、それらを結びつける文法からなる
言語処理系の用語 • 文脈自由言語 (Context Free Grammar: CFG) ◦ 形式言語の1つで、以下の形式で表されるもの ▪
rule: A B C ... | D E F ... ▪ このような記法を BNF(Backus-Naur Form) という ◦ 世の中のほとんどのプログラミング言語が属する ◦ パーサージェネレーターの入力となる文法ファイルに使われる ◦ 他の文法によって展開できる記号を非終端記号という ◦ それ以上展開できない記号を終端記号という
文脈自由言語と BNF number: digit digit digit: '0' | '1' |
'2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
文脈自由言語と BNF number: digit digit digit: '0' | '1' |
'2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 00 ~ 99 の2桁の数字全体を表す言語
文脈自由言語と BNF expression: digit '+' digit | digit '-' digit
| digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
文脈自由言語と BNF expression: digit '+' digit | digit '-' digit
| digit '*' digit | digit '/' digit digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁の数の2項四則演算式を表す言語
文脈自由言語と BNF expression: digit | expression '+' digit | expression
'-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
文脈自由言語と BNF expression: digit | expression '+' digit | expression
'-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と 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
文脈自由言語と BNF expression: digit | expression '+' digit | expression
'-' digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 1桁のかっこ付き四則演算式を表す言語
実際のパーサーの処理 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
実際のパーサーの処理 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
実際のパーサーの処理 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
実際のパーサーの処理 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
実際のパーサーの処理 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
実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'
digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: ( expression ) / digit
実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'
digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression / digit
実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'
digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' 例: expression
実際のパーサーの処理 expression: digit | expression '+' digit | expression '-'
digit | expression '*' digit | expression '/' digit | '(' expression ')' digit: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' Accepted!
まとめ • ソースコードが実行・コンパイルされるとき、 レキサーとパーサーがプログラムを解析してデータ構造を作る • パーサーを文法ファイルから自動生成するプログラムを パーサージェネレーターという • パーサージェネレーターの入力に使われる文法ファイルは 文脈自由文法で書かれる
• 文脈自由文法の記法として BNF がある
Lrama に コントリビューションした話
やったこと • Lrama に Named References を実装した
やったこと • Lrama に Named References を実装した 🤔
Lrama とは • Bison を代替するために作られた Ruby 製パーサージェネレーター • RubyKaigi 2023
で kaneko.y さんが発表した ◦ 詳しくは https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL • CRuby 3.3 から導入される ◦ preview1 には入っているので、今この場でお試しいただけます ◦ ※ Ruby のふるまいは変わりません
Lrama を導入するメリット • Bison のバージョンに引きずられずに済む ◦ Bison はユーザーごとにバージョンが異なるので、古いものがインス トールされていることを想定しなければいけない ◦
新しい機能が導入されても使えない • Ruby 独自の実装ができる ◦ LSP 向けに書きかけのコードをいい感じにパースする ◦ 複雑になっている文法のパースをうまくやる
Lrama が目指すところ • Usability • Maintainability • Universal Parser •
詳しくは RubyKaigi 2023 での kaneko.y さんの発表をどうぞ ◦ https://youtu.be/IhfDsLx784g?si=kO1q6mLpTa1bIRYL
Usability • Error-tolerant なパーサーがほしい ◦ 旧来のパーサーは、入力されたコードが文法的に正しいか どうかを判断すればよかった ◦ LSP が存在している現代においては、書きかけの半端なコード
についてもなるべくパースしたいという要求がある ▪ Bison の panic モードでは文法エラーの場所を 読み飛ばしてしまうので困る
Maintainability • 歴史的経緯により、parse.y は非常に複雑になっている ◦ 特に、ここから生成される CRuby のパーサーとレキサーが 密結合しているのがメンテナンス性に悪影響を与えている •
パーサーの力をうまく使ってやるともっとメンテナンス しやすくなるので、それを使うための機能を入れたい
Universal Parser • 世の中にはさまざまな Ruby 実装があり、 それぞれ独自のパーサーを利用している ◦ mruby や
JRuby といった別実装 ◦ sorbet や typeprof のようなツール • 現在の CRuby のパーサーは CRuby 独自の機能に依存しており 移植が難しくなっている • 文法ファイルを整理することで、他実装にも利用できるパーサーを 作りたい
Lrama 周辺のパーサー事情 • YARP ◦ Bison が作ったパーサーを置き換えるべく作られている CRuby 向けの手書き (!)
パーサー ◦ JRuby / TruffleRuby での実績もあるらしい • Bison ◦ GNU が開発した Yacc の後継のパーサージェネレーター ◦ CRuby においては、parse.y を読み込んで Ruby のパーサーを 出力するために使われていた • Racc ◦ 青木峰郎さんの開発したパーサージェネレーター ◦ parser gem (RuboCop の依存 gem) などで使われている
Q: Racc で Bison の置き換えってできないの? 生成アルゴリズムは同じだが、下記の部分が異なっているために共通で利用 できる部分が少なく、イチから作ったほうが手間が少ない • 入力する文法ファイルそのものの文法 ◦
Bison は Yacc 由来、Racc は独自記法 • 出力するパーサーの言語 ◦ Bison は C、Racc は Ruby
Named References とは • Bison の機能の 1 つ • Action
内の References として非終端記号名を利用できる
Named References とは • Bison の機能の 1 つ • Action
内の References として非終端記号名を利用できる 🤔
%{ Prologue (約 1500 行) %} Bison declarations (約 200
行) %% Grammar rules (約 4500 行) %% Epilogue (約 8300 行) Bison 文法ファイルの構造 かっこ内は CRuby の parse.y における行数
%{ Prologue (約 1500 行) %} Bison declarations (約 200
行) %% Grammar rules (約 4500 行) ← 今回はここの話 %% Epilogue (約 8300 行) Bison 文法ファイルの構造
Grammar rules の構造 rule_name: rule rule .. rule { action
} | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } | NUMBER '-' expression { $$ = $1 - $3 } | '(' expression ')' { $$ = $2 }
Grammar rules の構造 rule_name: rule rule .. rule { action
} | rule rule .. rule { action } expression: NUMBER '+' expression { $$ = $1 + $3 } ← | NUMBER '-' expression { $$ = $1 - $3 } ← 今回はここの話 | '(' expression ')' { $$ = $2 } ←
Action とは • Bison が生成するパーサーは、なにもしなければ 文法に則った入力になっているかどうかしか教えてくれない ◦ AST を作ったり、後の処理に必要な情報を保存したりはしない •
各文法の後ろに {} でくくってプログラムを書くことができる • $n や @n を使うと、文法中の記号の値を使って処理が行える ◦ この機能を (Numbered) References という
Action の具体例 expression: NUMBER '+' expression { $$ = $1
+ $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
Action の具体例 expression: NUMBER '+' expression { $$ = $1
+ $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
Action の具体例 expression: NUMBER '+' expression { $$ = $1
+ $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
Action の具体例 expression: NUMBER '+' expression { $$ = $1
+ $3 } この文法を受理したときの返り値は 1つ目の要素と3つ目の要素の足し算
Named References とは • Numbered References の問題点 ◦ 記述的でなくわかりにくい ◦
場所の番号で指定するため、 文法が変更になると書き直さなければならない • 上記の問題を解消するため、非終端記号名を参照して 値を使えるようにした機能が Named References
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] }
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] } 前に $ をつけてルール名で値を参照できる
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] } ルール記述側で [] でくくると別名をつけられる
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
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] }
ここまでのまとめ • やったことの説明 ◦ Lrama に Named References を実装した ▪
Lrama は Bison を置き換えるために作られた パーサージェネレーター ▪ Named References は Bison が持つ機能で、Action 内の References として非終端記号名を利用できる
実装を通して知った Lrama の内部構造
パーサージェネレーターの内部構造 パーサージェネレーター 文法ファイル レキサー トークン列 AST パーサー 生成器 パーサー
Lrama の内部構造 Lrama parse.y Lexer Token Grammar Parser Output パーサー
まずはテストを書く
どこに手を入れるか? • Numbered References はすでに実装されている ◦ 非終端記号名と Action 内の呼び出しを関連づけられれば 関連からコードを生成する部分はすでにある
◦ Numbered References の実装を参考にして 非終端記号名と呼び出しを関連づけることにする
どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー
どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー
どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー
どこに手を入れるか? Lrama parse.y Lexer Token Grammar Parser Output パーサー
なぜレキサーに手を入れるのか • Lrama ではレキサーが文脈を持ってトークナイズしている ◦ 「いま何をパースしているか」という情報 • Action を読み込むとき、直前に読んだルールの場所を確認して 「どのルールを参照しているか」という関連づけまでやっている
• これをまるごとパーサーに持っていくのは非常に大変だし、 もともとの設計にも合わない ◦ トークナイズ → パースというフェーズ分けをしたかった
文脈をどこに持つか? • レキサーは文脈を持っていないと適切なトークナイズができない ◦ コメントは読み飛ばす ◦ HereDoc の途中で改行が含まれてもパースエラーにしない ◦ etc.
• これをパーサーに持つかレキサーに持つか
文脈をどこに持つか? • レキサーに持つと ◦ 入力から自身の状態を変えながら一気にトークナイズができ、 トークナイズ → パースというフェーズ分けができる • パーサーに持つと
◦ パーサーが状態を元に次のトークンをレキサーから受け取る ようになるので、レキサーはトークナイズに集中できる
https://github.com/ruby/lrama/pull/41 Pull Request
今後やりたいこと • 内部パーサーの 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
世はまさに 大パーサー時代 人々はパーサーの 海へと漕ぎ出す ――RubyKaigi 2023 LT kaneko.y