Rustでコンパイラ作り part2 (parser見直し)
1日経ってみてpart1の内容が後半薄すぎ、parserの実装詳細に触れることがなかったので、part2は補足ということでparserに軽く焦点を当てたいと思う。lexerはあまり書くことがないので省略。
もちろん早くコンパイラを実装したい気持ちはあるが、そんな一日二日で自分が望むような手書きコンパイラは完成したりしない。
parser及びASTについて書き忘れたこと
BNFっぽいものを見てピーンときた人はいるかもしれない。作ろうとしている言語は基本的には関数ブロックの中で定義するものは式であり、ifもstatementではなくif文となる予定である。
そして、プログラムのソースコードというのは近代のものはだいたいヘッダー(importやグローバル変数、定数宣言)があって、関数定義があって、、、というような要素から構成されている。
part1で紹介したFlatten ASTの記事ではExprの式しかフラットにしていないので、例えばImportなりなんなりをプログラムの構造で入れるとしたらpart1で書いたBNFの最初で入力できるよう拡張し、また、ast.rs のここにも拡張していくことになる。意味解析器や、バックエンドのインタプリタ・コンパイラは今はまだないので、この pub enum Inst
の Vec<Inst>
を今後構築、走査していくことになる。
parserで気を付けること
注意が必要なのは、返ってくるのは列挙型のトークンの参照であり、列挙子(値そのもの)ではないということ。このあたりはRust特有だと私は思っていて、C++なんかで実装する時は int
で返したりしそうである。Rustでは配列(Vec
)などに関数でアクセスするときOptionで返るため、効率化のために値ではなく参照にしていると私は理解している。
Parser structで定義した関数を再度見てみよう
fn peek(&mut self) -> Option<&Token> {}
&Token
である。
気を付けるとしたら、 Token::Identifier(String)
のようなトークンがあって、参照で渡される時は中の文字列も &String
である。文字列の場合は Some(Token::Identifier(s)) => { let s = s.to_string(); ...}
としてしまえば参照はなくなる(もちろんコピーしているためコストはかかる)。
などといった点に気を付けて実装あるいはコードリーディングをしてもらいたい。
parserでのエラー処理
今度はRustらしいエラー処理について見ていこう。慣れればコード行数も短く簡潔で、実装もしやすくなるはず。
少し長いが if
のパーサについて見てみる。エラー処理には std::Result
を使い、成功時には ExprRef
を返し、エラー時には String
を返すようにしている。
Rustに慣れた人ならわかるように、次のコードの parse_logical_expr()
や parse_block()
の呼び出しの後ろに ?
記号の演算子がある。 ?
演算子はエラーがあるとき、エラーをそのままreturnさせる。詳細は docs.rust-jp.rs/book-ja に譲るが、ifを一回分自分で書かずに省略できるのはコーディングのとき非常に楽である。
pub fn parse_if(&mut self) -> Result<ExprRef, String> {
let cond = self.parse_logical_expr()?;
let if_block = self.parse_block()?;
let else_block: ExprRef = match self.peek() {
Some(Token::Else) => {
self.next();
self.parse_block()?
}
_ => self.add(Expr::Block(vec![])), // through
};
return Ok(self.add(Expr::IfElse(cond, if_block, else_block)));
}
まとめ
Rustは所有権が難しいとか聞いたりするが、実際にコードを見るとどうだろうか。 多少の癖はあるにしても、近代のプログラミング言語としてけっこう楽ができる仕組みができていると思う。
次回part3の内容はASTと意味解析をどう実装するかがポイントになりそう。 ただし、実装がすぐにできあがるかは未定である。