<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DZone Snippets: rdparser code</title>
    <link>http://snippets.dzone.com/posts</link>
    <pubDate>Fri, 25 Jul 2008 07:02:11 GMT</pubDate>
    <description>DZone Snippets: rdparser code</description>
    <item>
      <title>Recursive descent parser for Ruby - RDParser</title>
      <link>http://snippets.dzone.com/posts/show/2190</link>
      <description>A work of genius by Dennis Ranke (see original post at http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/174842 ). I want to keep this for posterity, but can't wait to see if this gets improved / extended, etc.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;class RDParser&lt;br /&gt;   attr_accessor :pos&lt;br /&gt;   attr_reader :rules&lt;br /&gt;&lt;br /&gt;   def initialize(&amp;block)&lt;br /&gt;     @lex_tokens = []&lt;br /&gt;     @rules = {}&lt;br /&gt;     @start = nil&lt;br /&gt;     instance_eval(&amp;block)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def parse(string)&lt;br /&gt;     @tokens = []&lt;br /&gt;     until string.empty?&lt;br /&gt;       raise "unable to lex '#{string}" unless @lex_tokens.any? do |tok|&lt;br /&gt;         match = tok.pattern.match(string)&lt;br /&gt;         if match&lt;br /&gt;           @tokens &lt;&lt; tok.block.call(match.to_s) if tok.block&lt;br /&gt;           string = match.post_match&lt;br /&gt;           true&lt;br /&gt;         else&lt;br /&gt;           false&lt;br /&gt;         end&lt;br /&gt;       end&lt;br /&gt;     end&lt;br /&gt;     @pos = 0&lt;br /&gt;     @max_pos = 0&lt;br /&gt;     @expected = []&lt;br /&gt;     result = @start.parse&lt;br /&gt;     if @pos != @tokens.size&lt;br /&gt;       raise "Parse error. expected: '#{@expected.join(', ')}', found &lt;br /&gt;'#{@tokens[@max_pos]}'"&lt;br /&gt;     end&lt;br /&gt;     return result&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def next_token&lt;br /&gt;     @pos += 1&lt;br /&gt;     return @tokens[@pos - 1]&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def expect(tok)&lt;br /&gt;     t = next_token&lt;br /&gt;     if @pos - 1 &gt; @max_pos&lt;br /&gt;       @max_pos = @pos - 1&lt;br /&gt;       @expected = []&lt;br /&gt;     end&lt;br /&gt;     return t if tok === t&lt;br /&gt;     @expected &lt;&lt; tok if @max_pos == @pos - 1 &amp;&amp; !@expected.include?(tok)&lt;br /&gt;     return nil&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   private&lt;br /&gt;&lt;br /&gt;   LexToken = Struct.new(:pattern, :block)&lt;br /&gt;&lt;br /&gt;   def token(pattern, &amp;block)&lt;br /&gt;     @lex_tokens &lt;&lt; LexToken.new(Regexp.new('\\A' + pattern.source), block)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def start(name, &amp;block)&lt;br /&gt;     rule(name, &amp;block)&lt;br /&gt;     @start = @rules[name]&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def rule(name)&lt;br /&gt;     @current_rule = Rule.new(name, self)&lt;br /&gt;     @rules[name] = @current_rule&lt;br /&gt;     yield&lt;br /&gt;     @current_rule = nil&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def match(*pattern, &amp;block)&lt;br /&gt;     @current_rule.add_match(pattern, block)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   class Rule&lt;br /&gt;     Match = Struct.new :pattern, :block&lt;br /&gt;&lt;br /&gt;     def initialize(name, parser)&lt;br /&gt;       @name = name&lt;br /&gt;       @parser = parser&lt;br /&gt;       @matches = []&lt;br /&gt;       @lrmatches = []&lt;br /&gt;     end&lt;br /&gt;&lt;br /&gt;     def add_match(pattern, block)&lt;br /&gt;       match = Match.new(pattern, block)&lt;br /&gt;       if pattern[0] == @name&lt;br /&gt;         pattern.shift&lt;br /&gt;         @lrmatches &lt;&lt; match&lt;br /&gt;       else&lt;br /&gt;         @matches &lt;&lt; match&lt;br /&gt;       end&lt;br /&gt;     end&lt;br /&gt;&lt;br /&gt;     def parse&lt;br /&gt;       match_result = try_matches(@matches)&lt;br /&gt;       return nil unless match_result&lt;br /&gt;       loop do&lt;br /&gt;         result = try_matches(@lrmatches, match_result)&lt;br /&gt;         return match_result unless result&lt;br /&gt;         match_result = result&lt;br /&gt;       end&lt;br /&gt;     end&lt;br /&gt;&lt;br /&gt;     private&lt;br /&gt;&lt;br /&gt;     def try_matches(matches, pre_result = nil)&lt;br /&gt;       match_result = nil&lt;br /&gt;       start = @parser.pos&lt;br /&gt;       matches.each do |match|&lt;br /&gt;         r = pre_result ? [pre_result] : []&lt;br /&gt;         match.pattern.each do |token|&lt;br /&gt;           if @parser.rules[token]&lt;br /&gt;             r &lt;&lt; @parser.rules[token].parse&lt;br /&gt;             unless r.last&lt;br /&gt;               r = nil&lt;br /&gt;               break&lt;br /&gt;             end&lt;br /&gt;           else&lt;br /&gt;             nt = @parser.expect(token)&lt;br /&gt;             if nt&lt;br /&gt;               r &lt;&lt; nt&lt;br /&gt;             else&lt;br /&gt;               r = nil&lt;br /&gt;               break&lt;br /&gt;             end&lt;br /&gt;           end&lt;br /&gt;         end&lt;br /&gt;         if r&lt;br /&gt;           if match.block&lt;br /&gt;             match_result = match.block.call(*r)&lt;br /&gt;           else&lt;br /&gt;             match_result = r[0]&lt;br /&gt;           end&lt;br /&gt;           break&lt;br /&gt;         else&lt;br /&gt;           @parser.pos = start&lt;br /&gt;         end&lt;br /&gt;       end&lt;br /&gt;       return match_result&lt;br /&gt;     end&lt;br /&gt;   end&lt;br /&gt;end&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;To use:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;parser = RDParser.new do&lt;br /&gt;   token(/\s+/)&lt;br /&gt;   token(/\d+/) {|m| m.to_i }&lt;br /&gt;   token(/./) {|m| m }&lt;br /&gt;&lt;br /&gt;   start :expr do&lt;br /&gt;     match(:expr, '+', :term) {|a, _, b| a + b }&lt;br /&gt;     match(:expr, '-', :term) {|a, _, b| a - b }&lt;br /&gt;     match(:term)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   rule :term do&lt;br /&gt;     match(:term, '*', :dice) {|a, _, b| a * b }&lt;br /&gt;     match(:term, '/', :dice) {|a, _, b| a / b }&lt;br /&gt;     match(:dice)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   def roll(times, sides)&lt;br /&gt;     (1..times).inject(0) {|a, b| a + rand(sides) + 1 }&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   rule :dice do&lt;br /&gt;     match(:atom, 'd', :sides) {|a, _, b| roll(a, b) }&lt;br /&gt;     match('d', :sides) {|_, b| roll(1, b) }&lt;br /&gt;     match(:atom)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   rule :sides do&lt;br /&gt;     match('%') { 100 }&lt;br /&gt;     match(:atom)&lt;br /&gt;   end&lt;br /&gt;&lt;br /&gt;   rule :atom do&lt;br /&gt;     match(Integer)&lt;br /&gt;     match('(', :expr, ')') {|_, a, _| a }&lt;br /&gt;   end&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;puts "#{parser.parse(supply_string_here)}"&lt;br /&gt;&lt;/code&gt;</description>
      <pubDate>Thu, 15 Jun 2006 05:57:17 GMT</pubDate>
      <guid>http://snippets.dzone.com/posts/show/2190</guid>
      <author>peter (Peter Cooperx)</author>
    </item>
  </channel>
</rss>
