LoginSignup
7
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-11-01

image.png

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

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

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


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

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

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

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

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

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

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

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

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4