Important: Pyodide takes time to initialize. Initialization completion is indicated by a red border around Run all button.

In the previous post, I showed how to write a simple recursive descent parser by hand – that is using a set of mutually recursive procedures. Actually, I lied when I said context-free. The common hand-written parsers are usually an encoding of a kind of grammar called Parsing Expression Grammar or PEG for short. The difference between PEG and a CFG is that PEG does not admit ambiguity. In particular, PEG uses ordered choice in its alternatives. Due to the ordered choice, the ordering of alternatives is important. A few interesting things about PEG:

• We know that L(PEG) is not a subset of L(CFG) (There are languages that can be expressed with a PEG that can not be expressed with a CFG – for example, $$a^nb^nc^n$$).
• We do not know if L(PEG) is a superset of CFL. However, given that all PEGs can be parsed in $$O(n)$$, and the best general CFG parsers can only reach $$O(n^{(3-\frac{e}{3})})$$ due to the equivalence to boolean matrix multiplication1 2.
• We do know that L(PEG) is at least as large as deterministic CFL.
• We also know that an LL(1) grammar can be interpreted either as a CFG or a PEG and it will describe the same language. Further, any LL(k) grammar can be translated to L(PEG), but reverse is not always true – it will work only if the PEG lookahead pattern can be reduced to a DFA.

The problem with what we did in the previous post is that it is a rather naive implementation. In particular, there could be a lot of backtracking, which can make the runtime explode. One solution to that is incorporating memoization. Since we start with automatic generation of parser from a grammar (unlike previously, where we explored a handwritten parser first), we will take a slightly different tack in writing the algorithm.

The idea behind a simple PEG parser is that, you try to unify the string you want to match with the corresponding key in the grammar. If the key is not present in the grammar, it is a literal, which needs to be matched with string equality. If the key is present in the grammar, get the corresponding productions (rules) for that key, and start unifying each rule one by one on the string to be matched.

For unifying rules, the idea is similar. We take each token in the rule, and try to unify that token with the string to be matched. We rely on unify_key for doing the unification of the token. if the unification fails, we return empty handed.

When we implemented the unify_key, we made an important decision, which was that, we return as soon as a match was found. This is what distinguishes a PEG parser from a general CFG parser. In particular it means that rules have to be ordered. That is, the following grammar wont work:

ifmatch = IF (expr) THEN stmts
| IF (expr) THEN stmts ELSE stmts


It has to be written as the following so that the rule that can match the longest string will come first.

ifmatch = IF (expr) THEN stmts ELSE stmts
| IF (expr) THEN stmts


If we now parse an if statement without else using the above grammar, such a IF (a=b) THEN return 0, the first rule will fail, and the parse will start for the second rule again at IF. This backtracking is unnecessary as one can see that IF (a=b) THEN return 0 is already parsed by all terms in the second rule. What we want is to save the old parses so that we can simply return the already parsed result. That is,

   if seen((token, text, at)):
return old_result


Fortunately, Python makes this easy using functools.lru_cache which provides cheap memoization to functions. Adding memoizaion, and reorganizing code, we have our PEG parser.

The driver:

What we have here is only a subset of PEG grammar. A PEG grammar can contain

• Sequence: e1 e2
• Ordered choice: e1 / e2
• Zero-or-more: e*
• One-or-more: e+
• Optional: e?
• And-predicate: &e – match e but do not consume any input
• Not-predicate: !e

We are yet to provide e*, e+, and e?. However, these are only conveniences. One can easily modify any PEG that uses them to use grammar rules instead. The effect of predicates on the other hand can not be easily produced. However, the lack of predicates does not change3 the class of languages that such grammars can match, and even without the predicates, our PEG can be useful for easily representing a large category of programs.

Note: This implementation will blow the stack pretty fast if we attempt to parse any expressions that are reasonably large (where some node in the derivation tree has a depth of 500) because Python provides very limited stack. One can improve the situation slightly by inlining the unify_rule().

This gets us to derivation trees with at a depth of 1000 (or more if we increase the sys.setrecursionlimit()). We can also turn this to a completely iterative solution if we simulate the stack (formal arguments, locals, return value) ourselves rather than relying on the Python stack frame.

### Context Free.

It is fairly easy to turn this parser into a context-free grammar parser instead. The main idea is to keep a list of parse points, and advance them one at a time.

Driver

The runnable Python source for this notebook is available here.

This implementation is quite limited in that we have lost the ability to memoize (can be added back), and can not handle left recursion. See the Earley parser for a parser without these drawbacks.

1. Valiant, Leslie G. “General context-free recognition in less than cubic time.” Journal of computer and system sciences 10.2 (1975): 308-315.

2. Lee, Lillian. “Fast context-free grammar parsing requires fast boolean matrix multiplication.” Journal of the ACM (JACM) 49.1 (2002): 1-15.

3. Ford, Bryan. “Parsing expression grammars: a recognition-based syntactic foundation.” Proceedings of the 31st ACM SIGPLAN-SIGACT symposium on Principles of programming languages. 2004. https://pdos.csail.mit.edu/~baford/packrat/popl04/peg-popl04.pdf