leah blogs

« Heidenröslein, Muselmänner und Tomatografie October 2005 Moving to Atom 1.0 »

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 Leah Neukirchen

AltStyle によって変換されたページ (->オリジナル) /