leah blogs

October 2005

29oct2005 · The Rightyear parsing library

Last week, I ported Monads in Perl to Ruby just for fun and learning. It’s amazing how one can port code without really understanding it, for programming languages the Chinese Room Argument certainly works.

When I converted all the code from the page, I reread some of my Haskell papers and got hooked by Yet Another Haskell Tutorial which has a good chapter on monads and their use. It also uses monads to build a parser combinator, which is an popular approach to parse in Haskell and other functional languages.

Rather quickly I had a direct port of something akin to Parsec. I needed a name and Martin DeMello proposed “rightyear”, the japanese pronunciation of lightyear, which is about a third parsec, of course. For testing, I wrote a parser to read something like a very primitive kind of XML. This is an easy example of a context-sensitive free grammar in Parsec:

xml = do{ name <- openTag
        ; content <- many xml
        ; endTag name
        ; return (Node name content) }
      <|> xmlText

And that’s the complete Rightyear parser:

class ParseXML < Parser
  def parse_xml
    parsing { xml.passthru eof }.run
  end
  def xml
    choice open_tag.bind { |name|
      many(xml).bind { |content|
        end_tag(name).bind { ret [name, content] }
      }
      }, text
  end
  def open_tag
    char(?<) >> one_or_more(char_matching { |c| c.chr =~ /\w/ }).bind { |t|
      char(?>).bindret t.join
    }
  end
  def end_tag(t)
    char(?<) >> char(?/) >> string(t) >> char(?>)
  end
  def text
    one_or_more(char_matching { |c| c.chr =~ /\w/ }).bind { |s| ret s.join }
  end
end

As you can see, Haskell’s do-notation doesn’t map very well to Ruby; it’s rather cumbersome to write. After a bit of twiddling, I found out that I didn’t actually need monads at all; instead I can use exceptions or throw/catch to trackback. Now, the parser looks like this:

class ParseXML < Rightyear::Parser
  def xml
    choice seq {
      name = open_tag
      content = many { xml }
      end_tag name
      [name, content]
    },
    seq { text }
  end
  def parse_xml
    v = xml; eof; v
  end
  def open_tag
    char(?<)
    t = text
    char(?>)
    t
  end
  def end_tag(t)
    char(?<); char(?/); string(t); char(?>)
  end
  def text
    one_or_more { char_matching { |c| c.chr =~ /\w/ }.chr }.join
  end
end

Which is a lot better for the eyes, no? It’s actually straight-forward to write parsers in this, but you need to ensure you don’t have left-recursion in your grammar, because that borks parser combinators in general. The big advantage over Bison and similar tools is that you easily can mix in your own code and can parse full LL(∞).

Unfortunately, the trackback via exceptions still is rather slow, it takes about 6 seconds to parse "1+#{'1+' * 6000}1" on my iBook. So consider this as an academical exercise for now; I learned a lot about monads and Haskell and how it sucks when you rewrite it in non-Haskell languages. :-)

People have been doing this in Tcl/Tk and Python, and even C too.

Further reading: Fast, Error Correcting Parser Combinators: A Short Tutorial (PDF) by S. Doaitse Swierstra and Pablo R. Azero Alcocer.

NP: Bob Dylan—Tonight I’ll Be Staying Here with You

Copyright © 2004–2022