10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyでちょっとしたパーサー書くならstrscanなのかなぁ

Posted at

Rubyでパーサーを書いてみたくなってちょっと調べてみたら、strscanという標準ライブラリがあるらしいので、使ってみることにした。

以下、単純な四則演算パーサー。

#ライブラリの読み込み
require "strscan"

#構文木
module AST
  BinOp = Struct.new :op, :lhs, :rhs do
    def value
      op.to_proc.call lhs.value, rhs.value
    end
    def to_s
      lhss = lhs.instance_of?(BinOp) && opRank(op) > opRank(lhs.op) ? "(#{lhs})" : lhs.to_s
      rhss = rhs.instance_of?(BinOp) && opRank(op) > opRank(rhs.op) ? "(#{rhs})" : rhs.to_s
      "#{lhss} #{op} #{rhss}"
    end
    
    private
    def opRank(op)
      case op
      when :+, :-
        1
      when :*, :/
        2
      end
    end
  end
  UnOp  = Struct.new :op, :rhs do
    def value
      op.to_proc.call rhs.value
    end
    def to_s
      "#{op}#{rhs}"
    end
  end
  Num   = Struct.new :value do
    def to_s
      value.to_s.gsub(/\.?0+$/, "")
    end
  end
end

#パーサー
class Parser
  def parse(src)
    @scanner = StringScanner.new src
    parse_expr
  end

  private
  include AST

  #expr <- term (('+'|'-') term)*
  def parse_expr
    ast = parse_term

    while op = scan(/[+-]/)
      ast = BinOp.new op.intern, ast, parse_term
    end

    ast
  end

  #term <- fact (('*'|'/') fact)*
  def parse_term
    ast = parse_fact
    
    while op = scan(/[*\/]/)
      ast = BinOp.new op.intern, ast, parse_fact
    end
    
    ast
  end

  #fact <- ('+'|'-') fact
  #      | '(' expr ')'
  #      | float
  def parse_fact
    if op = scan(/[+-]/)
      ast = UnOp.new op.intern, parse_fact
    elsif scan(/\(/)
      ast = parse_expr
      raise "excepted ')'" unless scan /\)/
    else
      ast = parse_float
    end
    
    ast
  end

  def parse_float
    raise "unexcepted" unless value = scan(/([1-9][0-9]*|0+)(\.[0-9]*)?/)
    Num.new value.to_f
  end

  def scan(rex)
    space
    @scanner.scan rex
  end
  
  def space
    @scanner.scan(/\s+/)
  end
end

if __FILE__ == $0
  require "pp"

  parser = Parser.new

  while (print ">>> "; src = gets.chomp) != "quit"
    begin
      ast = parser.parse src
      puts "input  => #{ast}"
      pp ast
      puts "result => #{ast.value.to_s.gsub(/\.0+$/, "")}"
    rescue => e
      puts "parse error", e
    end
  end
end

パーサーの部分はそれなりに短く書けた感じ。

RParsecとかはメンテされてなさそうだけどどうなんだろう?

10
10
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
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?