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

![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)
-->

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

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

- 小規模: コンパイラ部分は 1,000 行程度
- pure Ruby / 標準ライブラリ以外への依存なし
- 独自VM向けにコンパイルする
- ライフゲームのために必要な機能だけ
  - 変数宣言、代入、反復、条件分岐、関数呼び出し
  - 演算子: `+`, `*`, `==`, `!=` のみ（優先順位なし）
  - 型なし（値は整数のみ）
- [Ruby 以外の言語への移植](https://memo88.hatenablog.com/entry/2020/08/30/132314)（コンパイラ部分のみ）
- [セルフホスト版](https://qiita.com/sonota88/items/1e683276541cf1b87b76)（別リポジトリ）

下記も参照してください。

https://qiita.com/sonota88/items/f9cb3fc4a496b354b729

https://qiita.com/sonota88/items/2b95378b43a22109513c

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

----

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

もともとパーサ部分は手書きの再帰下降パーサでしたが、Racc 版を作ってみました。


Racc で四則演算のパーサを作る方法は分かったがもう少しプログラム言語らしきものを扱っているサンプルを見たいとか、パースしたそばから実行する方式（インタプリタ方式）ではなく構文木が欲しいんだけど、という感じの人には参考になるかもしれません（昔の自分に送ってあげたい）。


# できたもの

`vgparser_racc.y` を追加したブランチです。

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

300行弱なので全部貼ってもいいのですが、雰囲気程度ということで途中を省略したものを貼ります。全体は GitHub の方で見てください。

```ruby
class Parser

  prechigh
    left "+" "*"
  preclow

rule

  program:
    top_stmts
      {
        top_stmts = val[0]
        result = ["top_stmts", *top_stmts]
      }

  top_stmts:
    top_stmt
      {
        top_stmt = val[0]
        result = [top_stmt]
      }
  | top_stmts top_stmt
      {
        top_stmts, top_stmt = val
        result = [*top_stmts, top_stmt]
      }

  top_stmt:
    func_def

  func_def:
    "func" IDENT "(" args ")" "{" stmts "}"
      {
        _, fn_name, _, args, _, _, stmts, _, = val
        result = ["func", fn_name, args, stmts]
      }

  args:
    # nothing
      {
        result = []
      }
  | arg
      {
        arg = val[0]
        result = [arg]
      }
  | args "," arg
      {
        args, _, arg = val
        result = [*args, arg]
      }

  arg:
    IDENT | INT

  stmts:
    # nothing
      {
        result = []
      }
  | stmt
      {
        stmt = val[0]
        result = [stmt]
      }
  | stmts stmt
      {
        stmts, stmt = val
        result = [*stmts, stmt]
      }

  stmt:
    stmt_var
  | stmt_set
  | stmt_return
  | stmt_call
  | stmt_call_set
  | stmt_while
  | stmt_case
  | stmt_vm_comment
  | stmt_debug

  stmt_var:
    "var" IDENT ";"
      {
        _, ident, _ = val
        result = ["var", ident]
      }
  | "var" IDENT "=" expr ";"
      {
        _, ident, _, expr = val
        result = ["var", ident, expr]
      }

# ... 途中省略 ...

---- header

require "json"
require_relative "common"

---- inner

def next_token
  @tokens.shift
end

def to_token(line)
  token = Token.from_line(line)
  return nil if token.nil?

  if token.kind == :int
    Token.new(token.kind, token.value.to_i)
  else
    token
  end
end

def read_tokens(src)
  tokens = []

  src.each_line do |line|
    token = to_token(line)
    next if token.nil?

    tokens << token
  end

  tokens
end

def to_racc_token(token)
  kind =
    case token.kind
    when :ident then :IDENT
    when :int   then :INT
    when :str   then :STR
    else
      token.value
    end

  [kind, token.value]
end

def parse(src)
  tokens = read_tokens(src)
  @tokens = tokens.map { |token| to_racc_token(token) }
  @tokens << [false, false]

  do_parse()
end

---- footer

if $0 == __FILE__
  ast = Parser.new.parse(ARGF.read)
  puts JSON.pretty_generate(ast)
end
```

# 実行の例

入力とするプログラムの例です。

```javascript
// sample.vg.txt
func main() {
  return 1 + (2 * 3);
}
```

次のように実行するとASTに変換できます。

```sh
## vgparser_racc.rb を生成
$ bundle exec racc -t -o vgparser_racc.rb vgparser_racc.y

$ ruby vglexer.rb sample.vg.txt > tmp/tokens.txt
$ ruby vgparser_racc.rb tmp/tokens.txt 
[
  "top_stmts",
  [
    "func",
    "main",
    [

    ],
    [
      [
        "return",
        [
          "+",
          1,
          [
            "*",
            2,
            3
          ]
        ]
      ]
    ]
  ]
]
```


# スタックの動きを見てみる

せっかく Racc を使っているので、適当なサンプルコード（下記）をパースさせてスタックの動きを図にしてみました。

```javascript
func add(a, b) {
  var c = a + b;
  return c;
}

func main() {
  var x = -1;
  var i = 0;

  while (i != 10) {
    case when (i == 1) {
      call_set x = add(i, x);
    } when (i == 2) {
      set x = 1 + 2;
    } when (1) {
      set x = 1 + (2 * 3);
    }

    set i = i + 1;
  }
}
```

図の描き方については [Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする - Qiita](https://qiita.com/sonota88/items/6a96d2bcea9d134e38b7) を参照。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/66f17460-1b71-e4bc-f82a-7a48d3fa269c.png)

左端の、全体の 1/5 くらいの小さめの山が `add` 関数で、残りが `main` 関数ですね。


# メモ

- レキサはすでにあるのでそれを流用した
    - `to_racc_token` メソッドで Token オブジェクトを Racc が期待する形式に変換
- 1時間くらいで書けた。これより大きめの SQL パーサを少し前にすでに書いていてある程度慣れていたのと、パーサのテストがそのまま使えたのが良かった。



# 関連

他に Racc 関連で書いたもの。

<!--
Ruby/Racc: パース時のスタックの動きをFlameGraphっぽくビジュアライズする - Qiita
-->

https://qiita.com/sonota88/items/6a96d2bcea9d134e38b7

<!--
Ruby/Racc: パースに失敗した位置（行、桁）を得る - Qiita
-->

https://qiita.com/sonota88/items/f55c1654fe101fa0e557


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

<!-- Parslet -->
https://qiita.com/sonota88/items/edce05e86d3248f49401

<!-- Lramaで簡単な自作言語のパーサを書いた - Qiita -->
https://qiita.com/sonota88/items/372da20581c16cbd4eea