PHPerKaigi2020 Day1 PHPでつくるインタプリタ入門 の資料です。
https://fortee.jp/phperkaigi-2020/proposal/0f7ef74b-17ba-41b6-9ca6-abff2d30f9a7
参考文献などはブログでリンクをまとめています。 https://budougumi0617.github.io/2020/02/10/phperkaigi2020/
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.2020/02/10 PHPerKaigi 2020@budougumi0617PHPでつくるインタプリタ入門
View Slide
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.自己紹介● 清水 陽一郎 @budougumi0617● BASE BANK, inc. Dev Division○ PHP歴3ヶ月。PHP/Go/Python/AWS etc...● コミュニティ: golang.tokyo / Go Conference運営● 趣味: ボルダリング/毎週ブログを書くこと○ https://budougumi0617.github.io/Go Conferenece
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.自己紹介● 技術書典8に出典します○ 技術書典8 Day 1 あ33 golang.tokyo● コミュニティ有志による合同誌● Goの本ですが、立ち読みだけでもお越しください
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● インタプリタとは?● インタプリタを作ると何が得られるのか?● 字句解析、構文解析、評価とはなにか。それぞれの役割● PHPUnitと型を使ったPHP7.4での実装アプローチ● (時間が余ったら)デモ○ https://github.com/budougumi0617/php-go● まとめアジェンダ
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● インタプリタがどんな理論を使っているのか、実装方法がわかる○ インタプリタに関わるキーワードを知っている● tree-walking型インタプリタの処理の流れがわかる● 他の言語のインタプリタを作るときの参考になる○ TDDを使った漸進的なアプローチ● 「こういう流れでやればいいのね、意外と簡単」って気分になる今日のゴール
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.実装方針123PHPUnitを使ったTDD型を使った契約設計漸進的アプローチ
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 仕様が決まっている、かつコマンドラインツールなので練習として取り組みやすい● 再帰処理、オートマトンや構文木と仲良くなれる● いろいろな言語のパーサーも読めるようになる○ [Re:VIEW] textlintのlintエラーからttインライン命令で装飾された文字列を除外する○ https://budougumi0617.github.io/2020/01/19/textlint-ignore-tt-inlines● 構文解析ができるようになると自前の静的解析ができるようになる○ https://github.com/budougumi0617/layerインタプリタを作る理由
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● Go言語で作るインタプリタ(オライリー)○ 原著 https://interpreterbook.com/ (未訳だが続編のコンパイラ本もある)○ Goでオリジナル言語Monkeyのインタプリタを作る● コンパイラ | 情報系教科書シリーズ第9巻 (オーム社)○ 学生のころの教科書だったため。今はもっといい本があるのかもしれない?● 低レイヤを知りたい人のためのCコンパイラ作成入門(by Rui Ueyama)○ https://www.sigbus.info/compilerbook○ 「9ccの本」と言ったほうが通じるかもしれない参考文献
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.インタプリタとは
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● インタプリタ○ あるプログラミング言語を解釈する装置。○ REPL, LL言語● コンパイラ○ 生成物が存在する(*.o, *.exe, *.jar etc...)● パーサー○ 構文解析まで(実行可否・実行結果はわからない)そもそもインタプリタってなんだ?
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.インタプリタの処理の大きく3工程に分かれる123字句解析(Lexical analysis)構文解析(Syntax analysis)評価(Evaluate)
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.1. 字句解析(Lexical analysis)○ 文字列をトークンに変換していく2. 構文解析(Syntax analysis)○ トークン→抽象構文木(AST, Abstract Syntax Tree)○ パーサーはこのへんまでしかしない3. 評価○ AST→実際に結果を評価○ コンパイラの場合は意味解析(Semantic analysis)してコード生成に進むインタプリタの処理の流れ
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.字句解析
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.字句解析(Lexical analysis)ソースプログラムのテキストを、字句要素(Lexeme)と呼ばれる文字列に分ける。個々の字句要素はトークン(Token)と呼ばれるデータに変換させる。各トークンは、字句要素の種類を区別するための情報と、付加的な情報を蓄えている。
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.字句要素の例(Go)breakchandefercaseconstinterface=etc...+!=1
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 一番馴染みのある(?)文字列マッチング○ 正規表現● ここで使うのはオートマトンの原理どうやって文字列からトークンを生成していくのか?
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 有限個の状態(state)を持ち、文字を1文字ずつ読み込むことによって自分自身の状態を変化させる、仮想計算機の一種● 各状態を円で表し、受理状態を二重の円で表す● 状態遷移と対応する矢印に入力文字αを添えることで、状態遷移をを表す有限オートマトン(Finite Automaton)q q’α
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 任意の状態qと任意の文字αに対して、状態遷移が高々1つしか存在しないとき、有限オートマトンを決定性有限オートマトンと呼ぶ。○ 解釈が複数あると字句解析は成り立たない決定性有限オートマトン(DFA, Deterministic Finite Automaton)q q’α
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 1:キーワード if 2:識別子 <英字><英数字>* 3:空白 <スペース>*DFAの簡単な例i1英数字ff以外の英数字i以外の英数字英数字スペーススペース232
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.字句解析の実装
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.... x + y = 5 ...2(基本的には)素直に一文字ずつ読んでいく現在位置xを読み込んだ後の位置
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc. 2トークンのデータ構造
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.トークンの種類を定義
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc. 2テストランナーを作って少しずつ
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc. 2少しずつテストケースを追加していく
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.文字を読んで現在位置を進めていくだけ● キーワード文字があったら即トークンを生成● 文字列を見つけたらスペースまで読み込む○ その後、予約語か判断してトークン生成● 数字も同様
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.\n x = ...2ちょっと面倒なパターン「代入」トークン?
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.\n x = =2ちょっと面倒なパターン比較トークン
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.「現在位置」を動かず先読みしてしまう2
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● トークンを取得しただけではまだ何もわからない● プログラミング言語として、意味ある(解釈可能な)トークン文字列になっているかはわからない● プログラミング言語として文法を解釈していくには…○ 構文解析を行なう!トークンを取得したが…
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.構文解析
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.構文解析(Syntax analysis)構文解析では、トークン列を解析することによって、それが、言語の文法に従ったものであるかどうかをチェックし、木構造の構文木(Syntax tree)を生成する。
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.式(Expression)と文(Statement)● 式(Expression)○ 値を返す処理● 文(Statement)○ 値を返さない処理● 言語によって一概に言えない処理もある(式であるifもある)● Goの場合は宣言(Declation)という見方もしている
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 例: 1 + 2 + 3 のAST抽象構文木(AST, Abstract Syntax Tree)1BinayExpressionBinayExpression BasicLiteralBasicLiteral BasicLiteral23
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 単純に左辺から組み立てていくだけではできない○ 四則演算、言語固有の評価の優先順位● 再帰的に評価を行なっていくどうやってトークンからASTを導出していくのか?
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.再帰的下降構文解析(Recursive decent parsing)● 直感的な構文解析方法。○ ボトムアップ構文解析もある。● 左再帰とバックトラックが入ると複雑になってくる○ 左再帰…生成規則の左辺に自らが存在する場合の無限再帰○ バックトラック…複数候補があって解析失敗にしたとき、元の位置に戻って評価をやり直す
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.Pratt構文解析● 再帰的下降構文解析のひとつ○ 文法のトークンごとに解析関数を実装する○ トークンと解析関数が対になるので実装もわかりやすい○ 演算子に優先順位を付けて解析できる
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 例: 1 + 2 ✕ 3 のAST優先順位が必要な理由2BinayExpressionBinayExpressionBasicLiteral BasicLiteral3BasicLiteral1
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.構文解析の実装
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.その前にプログラミング言語ってどういう風に定義されている?何が正しくて何が間違った書き方?
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.BNF記法● 生成規則:文法を再帰的に定義する● BNF:生成規則をコンパクトで平易に記述するための一つの記法○ 現在はBNF記法を拡張したEBNFが多く使われている○ Goも言語仕様としてEBNFの記載がある■ https://golang.org/ref/spec#Notation
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.EBNFを読めばパースすべき正解が分かってくる● https://golang.org/ref/spec
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.構文解析の実装● Goの構文解析は結構難しい○ 終端を判断するのが難しい(PHPの「;」のような目印がない)○ 唐突に一つの式で複数の変数を宣言できる■ x, y, z := 10, 20, 30● 左辺が配列になるし”var”みたいな予告がない…○ 関数の引数も書き方にバリエーションがある■ func Myfunc(x, y int, z string)
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.ひたすらgo/parserを移植する● Goで実装されたGoのパーサー○ https://github.com/golang/go/tree/master/src/go/parser● ひたすらこれをTDDで移植していく○ テストが通る === 移殖成功● 型構造もほぼ同じように移殖○ Goの場合はExpression, Statementの他にDeclationもある● 例外と型アサーションで失敗に気づきやすくしておく
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.ASTのクラス構造NodeInterfaceStatementInterface DecrationInterfaceUnaryExprExpressionInterfaceBinaryExprAssignStatementReturnStatementGenDeclFuncDecl
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.パーサーもトークンがなくなるまでループするだけ4
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.トークンに対応する関数を実装する4
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.テストを書く4
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.ASTで使うクラス定義を書く4
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.テストが通るまで実装する5
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.キャストを使って誤操作を防ぐ& IDEを使いこなす5
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.未実装部分には例外を仕込んで誤再帰を防ぐ5
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 入力文字列がプログラミング言語として何を表現しようとしているのわかった!○ いよいよ実行結果を評価する!構文解析が終わると
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.評価
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.構文を評価するために必要なことあとは実行していくだけ!…では終わらない
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● スコープ(Scope)を考えないといけない式文を評価するために必要なことこれは何?この変数は定義済み?関数の中にはない。これは何?この関数は定義済み?コードの中にはない。
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 定義が関数内になかったら?● ファイルの中になかったら?● スコープの作り方は言語依存メモリ空間を表現したScopeクラスを用いる
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.評価の実装
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.Parserでパースした型に合わせて評価を実施5
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.評価関数も再帰を繰り返す6
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.評価の実装● Scopeから”add”関数の定義を探す● 引数の”x”, “10”をScope内で評価する○ “x”という名前の変数の定義を探す○ “10”はIntegerオブジェクトとして評価● 評価した引数を使って”add”関数を実行add(x, 10) というような関数呼び出しを評価する場合
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.ここまでするとやっと…
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.まとめ
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 思っていたより固く書けてよかった○ (私のプログラミング経歴がC/C++/Java/C#/Goらへんが通っているので)● 標準ライブラリだけで実装できている○ (テストではPHPUnitを用いている)● PhpStormでゴリゴリ書いていると楽しい● array使い始めると型情報が欠落してしまうのどうにかならないかな…PHPでインタプリタを実装してみて
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● インタプリタがどんな理論を使っているのか、実装方法がわかる● tree-walking型インタプリタの処理の流れがわかる● 他の言語のインタプリタを作るときの参考になる○ TDDを使った漸進的なアプローチ● 「こういう流れでやればいいのね、意外と簡単」って気分になる今日のゴール
© 2012-2019 BASE, Inc.© 2012-2020 BASE, Inc.● 足し算の結果を出力するところまではできました(やや無念)○ https://github.com/budougumi0617/php-go● インタプリタの基本的な処理とその役割を説明○ 字句解析、構文解析、評価● PHPUnitと型を使ったPHP7.4での実装アプローチ○ TDDでデグレを防ぎながら実装していく○ 型を使っていると契約設計的に異常検知ができる● 実際に作ってみてコンパイラのキモチが少し分かるようになったまとめ