views:

1785

answers:

1

I've recently being trying to teach myself how parsers (for languages/context-free grammars) work, and most of it seems to be making sense, except for one thing. I'm focusing my attention in particular on LL(k) grammars, for which the two main algorithms seem to be the LL parser (using stack/parse table) and the Recursive Descent parser (simply using recursion).

As far as I can see, the recursive descent algorithm works on all LL(k) grammars and possibly more, whereas an LL parser works on all LL(k) grammars. A recursive descent parser is clearly much simpler than an LL parser to implement, however (just as an LL one is simply than an LR one).

So my question is, what are the advantages/problems one might encounter when using either of the algorithms? Why might one ever pick LL over recursive descent, given that it works on the same set of grammars and is trickier to implement?

Hopefully this question makes some amount of sense. Sorry if it doesn't - I blame my the fact that this entire subject is almost entirely new to me.

+20  A: 

LL is usually a more efficient parsing technique than recursive-descent. In fact, a naive recursive-descent parser will actually be O(k^n) (where n is the input size) in the worst case. Some techniques such as memoization (which yields a Packrat parser) can improve this as well as extend the class of grammars accepted by the parser, but there is always a space tradeoff. LL parsers are (to my knowledge) always linear time.

On the flip side, you are correct in your intuition that recursive-descent parsers can handle a greater class of grammars than LL. Recursive-descent can handle any grammar which is LL(*) (that is, unlimited lookahead) as well as a small set of ambiguous grammars. This is because recursive-descent is actually a directly-encoded implementation of PEGs, or Parser Expression Grammar(s). Specifically, the disjunctive operator (a | b) is not commutative, meaning that a | b does not equal b | a. A recursive-descent parser will try each alternative in order. So if a matches the input, it will succede even if b would have matched the input. This allows classic "longest match" ambiguities like the dangling else problem to be handled simply by ordering disjunctions correctly.

With all of that said, it is possible to implement an LL(k) parser using recursive-descent so that it runs in linear time. This is done by essentially inlining the predict sets so that each parse routine determines the appropriate production for a given input in constant time. Unfortunately, such a technique eliminates an entire class of grammars from being handled. Once we get into predictive parsing, problems like dangling else are no longer solvable with such ease.

As for why LL would be chosen over recursive-descent, it's mainly a question of efficiency and maintainability. Recursive-descent parsers are markedly easier to implement, but they're usually harder to maintain since the grammar they represent does not exist in any declarative form. Most non-trivial parser use-cases employ a parser generator such as ANTLR or Bison. With such tools, it really doesn't matter if the algorithm is directly-encoded recursive-descent or table-driven LL(k).

As a matter of interest, it is also worth looking into recursive-ascent, which is a parsing algorithm directly encoded after the fashion of recursive-descent, but capable of handling any LALR grammar. I would also dig into parser combinators, which are a functional way of composing recursive-descent parsers together.

Daniel Spiewak
Very much the response I was hoping for! :) Thanks for all the info there (including last bit, of which I wasn't even aware). It will probably take a bit more reading before I understand all the concepts you've presented in this answer, but you've certainly answered my question and pointed me in the right direction for further study. The main thing I'm fuzzy abuot now is how PEGs relate to recursive descent parsers, and also how exactly a parser combinator combines various parsers. If you could clarify either/both of these, I would be very grateful.
Noldorin
Also, just to confirm; is "inlining the predict sets" effectively predictive parsing? In this case, what precisely is the "entire class of grammars"?
Noldorin
A PEG is the formal-ish description of a recursive-descent parser. Since recursive-descent isn't actually LL parsing, a different model was needed. You can kind of think of them like LALR and Bison, one being the formalism, the other being the implementation.
Daniel Spiewak
Predict sets: it really depends on the strategy used. If you *exclusively* rely on those predict sets and perform no backtracking, then the class of grammars is precisely LL(k), where k is the amount of lookahead used to compute the predict sets. However, you can get a lot of the benefits of predictive parsing by inlining predict sets in R-D without completely eliminating backtracking. This allows parsers which accept all grammars normally handled by R-D, but with a faster average case. Unfortunately, the worst-case for such parsers is still exponential.
Daniel Spiewak
Most recursive-descent parsers (even hand-written ones) will use inlined predict sets to *limit* the alternates, restricting backtracking without constraining flexibility. The end result is a parser which is nearly linear-time for everything but the most pathological grammars, and which still accepts the entire class of PEGs.
Daniel Spiewak
Good stuff. One nitpick though: "most nontrivial use cases are implemented in some sort of parser generator...". That's not true. Most widely used compilers and IDEs (C#, VB, Visual C++, and GCC being good examples) use hand written parsers. Those are arguably some of the most non-trivial uses.
Scott Wisniewski
Very true (well, almost; GCC uses Bison). Java famously uses a hand-written parser (though Java 7 has a rewritten parser based on ANTLR), as does Scala, Python, Haskell, and many more. However, I think if you did a full roll call, including the likes of C/C++, Ruby, etc, I think you would find that the parser generator crowd wins overall.
Daniel Spiewak
Can you please give an example of RD parser that is exponential?
Helltone