<自作言語処理系の説明用テンプレ>
自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。
- リポジトリ: github.com/sonota88/vm2gol-v2
- 小規模: コンパイラ部分は 1,000 行程度
- pure Ruby / 標準ライブラリ以外への依存なし
- 独自VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
- 変数宣言、代入、反復、条件分岐、関数呼び出し
- 演算子:
+
,*
,==
,!=
のみ(優先順位なし) - 型なし(値は整数のみ)
- 作ったときに書いた備忘記事
- Ruby 以外の言語への移植(コンパイラ部分のみ)
- セルフホスト版(別リポジトリ)
<説明用テンプレおわり>
もともとパーサ部分は手書きの再帰下降パーサでしたが、PEGベースのパーサライブラリ Parslet 版を作ってみました。
できたもの
vgparser_parslet.rb
を追加したブランチです。
ここでは雰囲気程度ということで Parser クラスのみ貼ります。全体は GitHub の方で見てください。transform なども合わせると全体は 340 行くらいです。
Parslet を使うのは今回初めてで、まだ慣れてなくて、こなれていない感じがします。もっといい書き方ができそう。
class Parser < Parslet::Parser
rule(:comment) {
str("//") >>
(str("\n").absent? >> any).repeat >>
str("\n")
}
rule(:spaces) {
(
match('[ \n]') | comment
).repeat(1)
}
rule(:spaces?) { spaces.maybe }
rule(:lparen ) { str("(") >> spaces? }
rule(:rparen ) { str(")") >> spaces? }
rule(:lbrace ) { str("{") >> spaces? }
rule(:rbrace ) { str("}") >> spaces? }
rule(:comma ) { str(",") >> spaces? }
rule(:semicolon) { str(";") >> spaces? }
rule(:equal ) { str("=") >> spaces? }
rule(:ident) {
(
match('[_a-z]') >> match('[_a-z0-9]').repeat
).as(:ident_) >> spaces?
}
rule(:int) {
(
str("-").maybe >>
(
(match('[1-9]') >> match('[0-9]').repeat) |
str("0")
)
).as(:int_) >> spaces?
}
rule(:string) {
str('"') >>
((str('"').absent? >> any).repeat).as(:string_) >>
str('"') >> spaces?
}
rule(:arg) { ident | int }
rule(:args) {
(
(
arg.as(:arg_) >>
(comma >> arg.as(:arg_)).repeat
).maybe
).as(:args_)
}
rule(:factor) {
(
lparen >> expr.as(:factor_expr_) >> rparen
).as(:factor_) |
int |
ident
}
rule(:binop) {
(
str("+") | str("*") | str("==") | str("!=")
).as(:binop_) >> spaces?
}
rule(:expr) {
(
factor.as(:lhs_) >>
(binop.as(:binop_) >> factor.as(:rhs_)).repeat(1)
).as(:expr_) |
factor
}
rule(:stmt_return) {
(
str("return") >>
(spaces >> expr.as(:return_expr_)).maybe >>
semicolon
).as(:stmt_return_)
}
rule(:stmt_var) {
(
str("var") >> spaces >> ident.as(:var_name_) >>
(equal >> expr.as(:expr_)).maybe >>
semicolon
).as(:stmt_var_)
}
rule(:stmt_set) {
(
str("set") >> spaces >>
ident.as(:var_name_) >> equal >> expr.as(:expr_) >>
semicolon
).as(:stmt_set_)
}
rule(:funcall) {
(
ident.as(:fn_name_) >>
lparen >> args.as(:args_) >> rparen
).as(:funcall_)
}
rule(:stmt_call) {
(
str("call") >> spaces >>
funcall >>
semicolon
).as(:stmt_call_)
}
rule(:stmt_call_set) {
(
str("call_set") >> spaces >>
ident.as(:var_name_) >> equal >> funcall.as(:funcall_) >>
semicolon
).as(:stmt_call_set_)
}
rule(:stmt_while) {
(
str("while") >> spaces? >>
lparen >> expr.as(:expr_) >> rparen >>
lbrace >> stmts.as(:stmts_) >> rbrace
).as(:stmt_while_)
}
rule(:when_clause) {
(
str("when") >> spaces? >>
lparen >> expr.as(:expr_) >> rparen >>
lbrace >> stmts.as(:stmts_) >> rbrace
).as(:when_clause_)
}
rule(:stmt_case) {
(
str("case") >> spaces >>
when_clause.repeat.as(:when_clauses_)
).as(:stmt_case_)
}
rule(:stmt_vm_comment) {
(
str("_cmt") >>
lparen >> string.as(:cmt_) >> rparen >>
semicolon
).as(:stmt_vm_comment_)
}
rule(:stmt_debug) {
(
str("_debug") >> lparen >> rparen >> semicolon
).as(:stmt_debug_)
}
rule(:stmt) {
stmt_return |
stmt_var |
stmt_set |
stmt_call |
stmt_call_set |
stmt_while |
stmt_case |
stmt_vm_comment |
stmt_debug
}
rule(:stmts) {
(stmt.repeat).as(:stmts_)
}
rule(:func_def) {
(
str("func") >> spaces >>
ident.as(:fn_name_) >>
lparen >> args.as(:fn_args_) >> rparen >>
lbrace >> stmts.as(:fn_stmts_) >> rbrace
).as(:func_def_)
}
rule(:top_stmt) {
func_def.as(:top_stmt_)
}
rule(:program) {
spaces? >> (top_stmt.repeat).as(:top_stmts_)
}
root(:program)
end
メモ
- PEGベースのパーサを使ったことがなかったので、今回触ってみて雰囲気が知れてよかった
- [ruby] Parsletで構文解析する[その1] - Qiita 〜 その3 までを読むと基本的な使い方はほぼ分かる。ありがとうございます
- 他には github.com/kschiess/parslet の
example/
に入っているサンプルを見て参考にしたり-
json.rb
やstring_parser.rb
など
-
- もう少し大きめのサンプルとして thnad を参考にしたり
「○○以外の文字の連続」
今回書いたものでいえば、コメント(改行以外の文字の連続)や文字列("
以外の文字の連続)の部分。
match
で正規表現が使えるので最初はこのように書いていました:
match('[^\n]').repeat
これでも動きます。ただ、公式のサンプルを見ると absent?
と any
を組み合わせていたので、それに倣って次のように書きました。こういうのは知らないと何をやってるのかわかりにくいかも。
(str("\n").absent? >> any).repeat
この記事を読んだ人はこちらの記事も読んでいます(たぶん)