Proofs: Packrat parsing
These are the human proofs of Packrat parsing, at full length.
PACKRAT-PARSE computes the PEG semantics, stops exactly when it is undefined, and runs in time O(|G| (n + 1)) full
This proof shows: correctness.
Statement. Let be a PEG with subexpressions and start expression , and let . Then PACKRAT-PARSE:
- makes at most evaluations at lines L7 to L14, and stops after steps;
- stops at L6 or L12 if and only if is undefined;
- otherwise computes , accepts exactly when , and then returns the parse tree of the match.
Setup. The semantics is defined by the clauses of the parsing expression grammar page. For a repetition we use the clause if and if . A value is defined when it has a finite derivation by these clauses. Each clause is deterministic: given , it names the first pair whose value it needs, and the value of each needed pair determines the next pair needed or the result. Call these pairs the calls of . So a defined value has exactly one derivation, a finite tree whose root is and whose children are the calls of , each with its own derivation.
Step 1: the stored values are correct. Claim: whenever the parser stores in , is defined and equals . Induction on the order of the stores. A terminal or the empty expression is stored at once by its clause. A composite pair is opened at L7 and asks for its calls one after the other; each answer comes from the table at L5, or from a pair opened and stored later but finished earlier, so by the induction hypothesis each answer is the defined value of the call. Lines L9 to L13 then compute the next call, or the result, exactly as the clause does: L10 passes the end of each part as the start of the next and stops at the first ; L11 returns the first alternative that does not fail; L12 returns when fails and otherwise the value at the end of ; L13 turns into and a success into for , and the reverse for . So the stored value is the result of the clause applied to defined values, and is defined with this value.
Step 2: each pair is evaluated at most once. A pair is evaluated when it is requested, is not in the table and is not open. From then until it is stored it is open, and a request for it during that time stops the run at L6; after it is stored, requests are answered at L5. So there are at most evaluations, one for each pair with ; positions never exceed , since a terminal succeeds only when .
Step 3: the number of steps. An evaluation of requests at most calls, where is the number of children of (the is the recursive request of a repetition). Each request costs a constant number of steps besides the evaluation it may start. The sum of over all subexpressions is at most , so the requests number at most and EVAL takes steps. Together with Step 2 this proves the first half of part 1.
Step 4: an early stop means no result. The open pairs at any moment form a chain in which and each pair is a call of the previous one, made after the values of its earlier calls were obtained, which by Step 1 are the defined values. Suppose the run stops at L6: the top pair requests for some . Then the derivation of , if it existed, would contain, along the chain , a derivation of as a proper subtree, which is impossible for a finite tree. Suppose it stops at L12: the repetition at found , so its clause calls itself, and again the derivation would contain itself. In both cases the value of the pair at the stop is undefined, and so are the values of all pairs below it in the chain, since each needs the next. In particular is undefined.
Step 5: no early stop when there is a result. Suppose is defined, with the derivation . By Step 1 and the determinism of the clauses, every pair that the parser opens is a node of , and the open chain is a path of from the root. A stop at L6 would give two nodes on one path of with the same pair, and the subtree of the lower one would be a derivation of the same value inside its own derivation, contradicting uniqueness and finiteness; precisely, the derivation of the upper node strictly contains a copy of itself, so its height exceeds itself. A stop at L12 would give a node of whose call is , the same contradiction. So the parser does not stop early, and by Steps 2 and 3 it finishes the evaluation of ; by Step 1 it obtains . Line L3 accepts exactly when . With Step 4 this proves part 2 and the first half of part 3.
Step 6: the tree. Define the output of a successful pair with : for a terminal, the token ; for and the predicates, nothing; for a nonterminal , one node whose children are the output of ; for a sequence, the outputs of its parts at the successive positions; for a choice, the output of the first alternative that succeeds; for with , the output of followed by that of , and nothing when fails. The parse tree is the node with the output of . Each part referred to here is a call of the pair, so by Step 1 its position and success are those stored in the table, and the alternative of a choice is the one recorded at L11. TREE (lines L15 to L18) computes this output by induction on the derivation: it emits the token at L15, builds the node at L16, and at L17 visits the parts at the stored positions. In the implementation each pair is pushed on a work stack with the list it writes into, and the parts of a sequence are pushed in reverse order, so all output of one part is written before the next part is popped; the lists therefore receive their items from left to right.
A node for at is built once and shared. This does not change the value of the tree, since the output of a pair depends only on the pair.
Step 7: the cost of TREE. A pair that consumes at least one token occurs at most once in the written-out tree: two occurrences would both contain the leaf , so one would lie inside the other, and both would start at with the same expression, which makes contain a derivation of the pair inside itself, excluded as in Step 5. TREE expands each node once. Inside one expansion it visits each subexpression of at most once, except for the iterations of repetitions, and each iteration is a pair that consumes a token, visited once in the whole run. So TREE takes at most steps, which completes part 1, and it returns the parse tree, which completes part 3.