Proofs: LL(*) parsing
These are the human proofs of LL(*) parsing, at full length.
ALL-STAR-PARSE accepts exactly the sentences of a grammar without left recursion, returns their parse trees and reports errors at the first token that no sentence allows full
This proof shows: correctness.
Statement. Let be a grammar in which every nonterminal derives some terminal string and no nonterminal satisfies (no left recursion, direct or hidden). For every input , ALL-STAR-PARSE stops, and:
- it accepts if and only if , and then it returns a parse tree of with root ;
- if it reports an error at position (with meaning the end of the input), then is a prefix of a sentence, and is not when .
The DFA cache does not change the result, so the statement holds for any contents of the cache built by earlier inputs.
Setup. Write for one rewriting step and for the terminal strings that a string of symbols derives; for every because every symbol is productive. A head step on a string either replaces its first symbol by the right side of a production , or removes its first symbol when it is the next input token (a match). This is the nondeterministic top-down parser: for a terminal string if and only if some sequence of head steps turns into while matching exactly , because a leftmost derivation rewrites the first nonterminal, and every terminal before it can be matched first. Call settled when it is or starts with a terminal.
Step 1: head expansions terminate. From a fixed string , there is no infinite sequence of head steps without a match. Suppose there were one. Its expansions form a forest of partial derivation trees whose roots are the symbols of , and an infinite sequence would expand infinitely many nodes of the tree of one symbol, so by König's lemma (each node has at most children) that tree has an infinite path . Each is expanded only after all of its left siblings have been expanded to , since there was no match, so with . Some label repeats, with , and then , which is left recursion. By König's lemma again, the set of strings reachable from by head expansions is finite. So every CLOSURE call terminates, and the set it returns is the set of settled strings reachable by head expansions, with the stack entries of the parser read in when the local part becomes empty (line L27).
Step 2: what the configuration sets contain. Consider a call of ADAPTIVE-PREDICT for at position , with the parser stack below (top first), and write for the right side of the production of .
- SLL. For , as long as the loop reaches the step , the set at that step is the set of pairs with settled such that head steps turn into while matching exactly . For this is the definition of the start state. A step from to happens only when no pair of has (line L15), so every pair is or does not start with ; line L18 keeps the pairs that can match and closes them, which by Step 1 gives exactly the settled strings reachable after matching . A cached edge was computed by the same function of , so it is the same set.
- LL. A configuration stands for the full stack . By the same induction, the set at step represents exactly the settled full stacks reachable from by head steps that match , and each such stack is represented at least once. The representation is the only one of the empty stack.
Step 3: the choice of a production is safe. Say that a production of fits when . Claim: if some production fits, ADAPTIVE-PREDICT returns a production that fits. Fix a fitting and a sequence of head steps from that matches and ends with the empty stack. Let be the first position at which its local part, the part that comes from , is empty and settled; exists, at the latest at the end.
- SLL, before . At every step the run has a pair in by Step 2, with for , so the loop never reaches line L17 or L19 before . If it stops at line L14 at some , every pair has the alternative , which is returned. If it stops at line L16 at some , then the stacks of and of the returned alternative are the same nonempty strings , and since is one of them, the same head steps that finish the run of from finish a run of : so fits. At step the pair is in , so the loop stops at line L14 with or calls LL-PREDICT at line L15.
- LL. Now the run of gives, at every step , a configuration of in , and at the configuration . So the loop never reports an error. If it stops at line L22, it returns . If it stops at line L23, the represented stacks of the returned include those of , and the run of continues from one of them, so fits. If it reaches line L24, holds exactly for the productions whose head steps match all of and empty the stack, that is, the fitting ones; the smallest of them is returned.
Step 4: acceptance. Invariant J: at each test of line L2, with matched and the stack , the tree built so far is the tree of a leftmost derivation , with one empty slot for each stack entry; and if , then . Initially the stack is . A match keeps both parts. An expansion of by performs one leftmost derivation step, so the first part holds; for the second, if , then some production of fits, and by Step 3 the production chosen fits too (when has a single production, it is the fitting one). So if , line L6 is never reached (the top terminal is , by the second part and since a settled string that derives starts with ), no prediction reports an error (Step 3), and when the stack is empty the second part says : the parser accepts. Conversely, when it accepts, the stack is empty and , so the first part gives a leftmost derivation whose tree has been returned, and .
Step 5: termination. Every prediction loop advances by one at each round and stops at at the latest, and each CLOSURE call terminates (Step 1). In the main loop, each round matches a token or expands a nonterminal, and between two matches the expansions are head expansions of the stack, finitely many by Step 1. So the parser stops.
Step 6: errors. Say that a step of the parser inspects the tokens it compares, at line L5 and in the prediction loops, and the end of the input when it tests or ; let be the largest inspected position so far, with for the end. Invariant K: at each test of line L2, the stack derives a string that starts with (empty when ), and when the end has been inspected, it derives itself. A match keeps K. At a prediction that returns after inspecting up to the step , the set at step holds a configuration of (lines L14, L16, L22, L23) or, at line L24, ; by Step 2, derives, by productivity, a string that starts with , or exactly at line L24. If an earlier step had inspected up to , then by K a run from the stack before the expansion matches ; it starts with some production of , which is still alive at step , so it is when the loop stops at line L14 or L22, and at line L16 or L23 the same continuation works for , as in Step 3. So K holds after the expansion.
Now let the parser report an error at position . By K, before the failing step: if , a run that matches past exists, so the top terminal is at line L5, and a prediction keeps a configuration alive past . The failing step inspects , and is a prefix of a sentence: the stack, or a configuration alive at step , derives a string that starts with , and with the first part of J and productivity this extends to a sentence. Suppose that and some sentence starts with . Every step of the parser up to the failing one inspects only positions up to , where and agree (the end of the input is never inspected, since and ), and the DFA cache returns the same sets for the same arguments. So the parser on makes the same moves and reports the same error, which contradicts Step 4 for . Hence is not a prefix of a sentence.