Parsletでかんたんな自作言語のパーサを書いた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/9ebeab7b-e274-4b72-4c9a-1a69b3c3c2c9.png)

<!--
line-height: 140%
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/baf39a40-daa5-37dc-7f2f-72776a96bdd0.png)
-->

＜自作言語処理系の説明用テンプレ＞

自分がコンパイラ実装に入門するために作った素朴なトイ言語とその処理系です。簡単に概要を書くと下記のような感じ。

- リポジトリ: [github.com/sonota88/vm2gol-v2](https://github.com/sonota88/vm2gol-v2)
- 小規模: コンパイラ部分は 1,000 行程度
- pure Ruby / 標準ライブラリ以外への依存なし
- 独自VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
  - 変数宣言、代入、反復、条件分岐、関数呼び出し
  - 演算子: `+`, `*`, `==`, `!=` のみ（優先順位なし）
  - 型なし（値は整数のみ）
- 作ったときに書いた備忘記事
  - [RubyでオレオレVMとアセンブラとコード生成器を2週間で作ってライフゲームを動かした話](https://qiita.com/sonota88/items/f9cb3fc4a496b354b729)
  - [Rubyでかんたんな自作言語のコンパイラを作った](https://qiita.com/sonota88/items/2b95378b43a22109513c)
- [Ruby 以外の言語への移植](https://memo88.hatenablog.com/entry/2020/08/30/132314)（コンパイラ部分のみ）
- [セルフホスト版](https://qiita.com/sonota88/items/1e683276541cf1b87b76)（別リポジトリ）

＜説明用テンプレおわり＞

----

<!--
もともとは手書きの再帰下降パーサでしたが、Racc を使うとどんな感じになるか試してみました。
-->

もともとパーサ部分は手書きの再帰下降パーサでしたが、PEGベースのパーサライブラリ [Parslet](https://github.com/kschiess/parslet) 版を作ってみました。

# できたもの

`vgparser_parslet.rb` を追加したブランチです。

https://github.com/sonota88/vm2gol-v2/tree/alt_parser_parslet

ここでは雰囲気程度ということで Parser クラスのみ貼ります。全体は GitHub の方で見てください。transform なども合わせると全体は 340 行くらいです。

Parslet を使うのは今回初めてで、まだ慣れてなくて、こなれていない感じがします。もっといい書き方ができそう。

```ruby
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](https://qiita.com/pocari/items/048b878575bb28a40732) 〜 その3 までを読むと基本的な使い方はほぼ分かる。ありがとうございます :pray:
- 他には [github.com/kschiess/parslet](https://github.com/kschiess/parslet) の `example/` に入っているサンプルを見て参考にしたり
    - `json.rb` や `string_parser.rb` など
- もう少し大きめのサンプルとして [thnad](https://github.com/undees/thnad) を参考にしたり


---

「○○以外の文字の連続」

今回書いたものでいえば、コメント（改行以外の文字の連続）や文字列（`"` 以外の文字の連続）の部分。

`match` で正規表現が使えるので最初はこのように書いていました:

```ruby
match('[^\n]').repeat
```

これでも動きます。ただ、公式のサンプルを見ると `absent?` と `any` を組み合わせていたので、それに倣って次のように書きました。こういうのは知らないと何をやってるのかわかりにくいかも。

```ruby
(str("\n").absent? >> any).repeat
```




# この記事を読んだ人はこちらの記事も読んでいます（たぶん）

<!-- Raccでかんたんな自作言語のパーサを書いた - Qiita -->
https://qiita.com/sonota88/items/97ba1f0c377fbe86d5b1

<!-- 『RubyでつくるRuby』のMinRubyのパーサを書いた（手書きの再帰下降パーサ） - Qiita -->
https://qiita.com/sonota88/items/84e4c16e5602b9771e92