What's in a Parser Combinator? (2016)
remusao.github.io
remusao.github.io
* Error handling always ends up being non-existent or of the quality of "begin, for, if, while, repeat, identifier, number, float expected" with no good way to override what happens
* Recovery is usually impossible
* Parser generator generates a full model that doesn't match what we need
* Working around the quirks of the input language ends up being more tricky than hand writing (almost every language has some ambigiuty)
* Slow: With ANTLR it's really easy to make it do gigantic amount of look aheads in complex languages, which isn't even really needed
I always end up going back to a simple hand crafted parser which is easier to read and write.
Probably true in many toy-versions.
However parser combinators are very well suited for overriding the default expectation error messages with something custom. For example using a custom combinator `<?>` with low precedence.
parseStatement :: Parser Statement
parseStatement = parseIf
<|> parseBlock
<|> parseWhile
<|> parseRepeat
<?> "statement"
No the parser won't enumerate all the options, but now can give you a high level expectation.> * Recovery is usually impossible
There are a bunch of error correcting combinator libraries out there.
Also, when using monadic combinators you can do all kinds of retrospective work while parsing. Probably more expensive, but entirely possible. Some pseudo Haskell example:
parseHtmlBody :: Parser Html
parseHtmlBody =
do parseOpenTag "body"
contents <- parseHtml
tag <- tryParsingClosingTag "body"
when (tag /= "body") $
raiseWarning ("expected </body> seems like something else: " <> closing)
return contentsMy guess is that it's coming from a difference in the framing of how thins work. In ANTLR, you supply a grammar, and then the grammar gets compiled into a parser, all in one go. That maximizes the elbow room available for fine-tuning the overall product.
Parser combinators, OTOH, frame the problem as building larger parsers by composing smaller parsers. Each of those parsers is presumed to be fully formed. There's a beauty to that approach that I really like. But it does mean that the parser library is forever dealing with arrangements of trees, without much of an option to take a step back and look at the forest.
(Unless you're working in a homoiconic language like lisp, anyway.)
https://hackage.haskell.org/package/uu-parsinglib
To quote:
New version of the Utrecht University parser combinator library, which provides online, error correction, annotation free, applicative style parser combinators. In addition to this we provide a monadic and an idomatic interface. Parsers do analyse themselves to avoid commonly made errors.
So you get online parsing which means you can extract partial results available.
You get error correction which accumulate errors and lets you get partial results ranked on the cumulative severity of errors happened.
You also get a choice of Applicative interface which lets you build parsers that are mostly context-free (but see [2]) and monadic interface which allows you to have context-dependent parsing, online (you may report errors in Ada-like languages "function f ... begin end function g" on the "g" identifier online).
The implementation above also automatically factor prefixes and does not do repeated scanning after, say, rollback.
Would that somehow make your thirst less parching?
[2] https://byorgey.wordpress.com/2012/01/05/parsing-context-sen...
PS My experience with ANTLR is quite the opposite, in the case of VHDL, at the very least (preprocessors required for Verilog aren't even expressible in ANTLR). Either I had to do a lot of work over ANTLR grammar to make it faster and lose a lot of conciseness or it is slow as snail, getting as slow as 4K bytes of source code per second.
PPS With parser combinators I can have an ability to test just parts of grammar in REPL and speed is much more predictable, if not faster.
PPPS I even encountered superlinear optimization gain produced by Glorious Haskell Compiler for some implementation of parser combinators: the parsing time for grammar that must have quadratic parsing time (over the length of input) has leading power of about 1.3. In other words, instead of T=O(L^2) (ghci, code interpreted without optimization, and C#, both gave this) I got T=O(L^1.3) (ghc -O3).
Where he starts from the scratch and explains the process of creating your own library for parsing.
The YT video description has the link to the full version of the library that he starts creating in his tutorial. It's kind of an elegant Haskell programing that I don't think I'll ever achieve. [1]
A parser combinator library is just fancy marketing speak for "recursive descent utility functions". All it is is a a bag of commonly used patterns wrapped up in generic functions.
The hoopla is overblown.
To me, the monad/applicate stuff is a red herring. It's mostly used to simulate imperative sequencing. e.g. the Haskell code `Person <$> parseName <*> parseAddress` would be `return Person { parseName(), parseAddress() };` in C. There's a few tricks but it's not crucial to the parser combinator idea and doesn't help readability.
I suspect the difficulty of error handling or parsing a non-trivial language are exactly why few articles cover them. Crafting Interpreters¹ seems to be a notable exception here.
----
¹: https://www.craftinginterpreters.com/parsing-expressions.htm...
[0] https://github.com/elm/compiler/commit/0e669b8ad076f64e450f2...
[1] https://github.com/elm/compiler/issues/1773#issuecomment-418... the same on master
data Parser a c = Parser { runParser :: String -> Either [c] (a, String) }
and adding a combinator withContext c p = Parser \s -> case runParser p of
Left stackTrace = Left (c:stackTrace)
Right x = Right x
This allows you to write stuff like number = withContext "parsing a number" $ ...
addition = withContext "parsing addition expression" $ ...
expr = withContext "parsing a mathematical expression" $ ...
and combine that with a technique that keeps track of where you are in the string when failure occurs, you can pretty print that to something like:Failed to parse!
2 + 34.0O4
^
While: parsing a numberWhile: parsing addition expression
While: parsing a mathematical expression
I feel the criticism is valid because most parsers in production are done via hand without tooling or fancy techniques.
What? Do you have any data you'd like to share?
I'm given to understand that e.g. the C++ compilers usually have a hand-coded, but AFAIUI that's mostly due to the complexity of actually parsing it (and fitting that into anything other than just raw code).
https://www.drdobbs.com/architecture-and-design/so-you-want-...
A common theme is that the parser generator does not provide you the tools to write the high quality error messages. Having used ANTLR and other tools, I believe it now that I'm trying to ship a real language.
(IME MegaParsec basically solves most of the issues.)
I don't see why you say that, parser combinators are just a structured way to compose recursive descent parsers, you can do everything with them that you can with any other hand-rolled parser. You don't even need to construct an AST at all, a mathematical expression "parser combinator" could just return a single number, the result of evaluating the expression.
In fact, I would say Haskell-style parser combinators are the best way to satisfy those requirements in general.