2021-11-06に投稿

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

image

<自作言語処理系の説明用テンプレ>

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

<説明用テンプレおわり>


もともとパーサ部分は手書きの再帰下降パーサでしたが、PEGベースのパーサライブラリ Parslet 版を作ってみました。

できたもの

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

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

ここでは雰囲気程度ということで 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 までを読むと基本的な使い方はほぼ分かる。ありがとうございます :pray:
  • 他には github.com/kschiess/parsletexample/ に入っているサンプルを見て参考にしたり
    • json.rbstring_parser.rb など
  • もう少し大きめのサンプルとして thnad を参考にしたり

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

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

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

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

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

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

この記事を読んだ人はこちらの記事も読んでいます(たぶん)

Originally published at qiita.com
ツイッターでシェア
みんなに共有、忘れないようにメモ

sonota88

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント