Algorithm Search⌘K

Proofs: Recursive ascent parsing

These are the human proofs of Recursive ascent parsing, at full length.

The recursive ascent parser makes exactly the moves of the table-driven LR parser, so it accepts L(G), builds the right tree, and stops full

This proof shows: correctness, termination.

We use the notation of the context-free grammar page and the pseudocode of the page: the driver RECURSIVE-ASCENT-PARSE (lines L1 and L2) and the procedures STATEq_q (lines L3 to L13). The grammar GG is SLR(1), so its ACTION table has at most one action per entry, and GOTO is the goto table of its LR(0) automaton. The reference is the table-driven parser SLR-PARSE of SLR parsing, which uses the same two tables.

Configurations

A configuration of the table-driven parser is a stack of states q0q1⋯qmq_0 q_1 \cdots q_m with q0=0q_0 = 0, a list of trees v1,…,vmv_1, \ldots, v_m (the tree vtv_t belongs to the symbol on the transition into qtq_t), and an input position ii. A move is a shift, a reduction followed by a goto, an accept, or an error.

At any moment of the recursive ascent parser, the active procedure calls form a chain: STATEq0_{q_0} called by the driver, STATEq1_{q_1} called by it, and so on up to the running call STATEqm_{q_m}. Call tt received the argument vtv_t (for t=0t = 0, nothing). Every call except the last is suspended at one of the two call sites, line L4 or line L13, and when it resumes it continues at line L10. We say that the recursive ascent parser is in configuration (q0⋯qm; v1…vm; i)(q_0 \cdots q_m;\ v_1 \ldots v_m;\ i) when this is the chain, the input position is ii, and the call STATEqm_{q_m} is about to execute line L3.

Lemma 1: one move

Suppose the recursive ascent parser is in configuration C=(q0⋯qm; v1…vm; i)C = (q_0 \cdots q_m;\ v_1 \ldots v_m;\ i), and let aa be the token at position ii.

  1. If ACTION[qm,a][q_m, a] = shift jj, then it next reaches the configuration (q0⋯qmj; v1…vma; i+1)(q_0 \cdots q_m j;\ v_1 \ldots v_m a;\ i + 1).
  2. If ACTION[qm,a][q_m, a] = reduce A→βA \to \beta with k=∣β∣k = |\beta|, then k≤mk \le m, and it next reaches the configuration (q0⋯qm−k g; v1…vm−k u; i)(q_0 \cdots q_{m-k}\, g;\ v_1 \ldots v_{m-k}\, u;\ i) with g=GOTO[qm−k,A]g = \mathrm{GOTO}[q_{m-k}, A] and uu the node AA with the children vm−k+1,…,vmv_{m-k+1}, \ldots, v_m.
  3. If ACTION[qm,a][q_m, a] = accept, then every call returns ACCEPT(vm)(v_m) and the driver returns vmv_m.
  4. If the entry is empty, line L9 reports an error at position ii.

These are exactly the next configurations of the table-driven parser.

Proof. Case 1: line L4 advances the input and calls STATEj_j with the leaf aa; the new call is about to execute line L3. Case 4 is line L9.

Case 2 with k=0k = 0: line L7 sets rr = REDUCE(A,0,⟨⟩)(A, 0, \langle\rangle), and the loop of line L10 starts in the call qm=qm−kq_m = q_{m-k}. Line L11 does nothing, and since c=0c = 0, line L13 calls STATEg_g with the node AA over no children, which is uu.

Case 2 with k≥1k \ge 1: we show by induction on s=1,…,ks = 1, \ldots, k that the call qm−sq_{m-s} resumes at line L10 with rr = REDUCE(A,k−s,⟨vm−s+1,…,vm⟩)(A, k - s, \langle v_{m-s+1}, \ldots, v_m \rangle). For s=1s = 1: line L6 of call qmq_m returns REDUCE(A,k−1,⟨vm⟩)(A, k - 1, \langle v_m \rangle), and its caller qm−1q_{m-1} was suspended at a call site, so it resumes at line L10 with this value. The call qm−1q_{m-1} exists because m≥k≥1m \ge k \ge 1. For s<ks < k: in call qm−sq_{m-s}, c=k−s>0c = k - s > 0, so line L12 returns REDUCE(A,k−s−1,⟨vm−s,vm−s+1,…,vm⟩)(A, k - s - 1, \langle v_{m-s}, v_{m-s+1}, \ldots, v_m \rangle), and call qm−s−1q_{m-s-1} resumes with it. For s=ks = k, the call qm−kq_{m-k} has c=0c = 0 and the children ⟨vm−k+1,…,vm⟩\langle v_{m-k+1}, \ldots, v_m \rangle in order, and line L13 calls STATEg_g with uu. The calls qm−k+1,…,qmq_{m-k+1}, \ldots, q_m have returned, so the chain is q0⋯qm−k gq_0 \cdots q_{m-k}\, g, and the new call is about to execute line L3.

Case 3: line L8 returns ACCEPT(vm)(v_m). Each suspended call resumes at line L10, and line L11 returns the same value, so the value reaches the driver, and line L2 returns vmv_m. □\square

The bound k≤mk \le m holds because the table-driven parser in the same configuration pops kk states above q0q_0: by Lemma 2 of the SLR proof, its stack symbols form a viable prefix whose last kk symbols are β\beta.

Theorem

On every input ww, the recursive ascent parser makes the same sequence of moves as the table-driven SLR(1) parser with the same tables. So it stops, it accepts exactly the sentences of L(G)L(G), on acceptance it returns the same parse tree and the same sequence of reductions (a reverse rightmost derivation of ww), and on rejection it reports the error at the same position.

Proof. Initially, line L1 calls STATE0_0 with nothing, and the input position is 0: the recursive ascent parser is in configuration (0; ; 0)(0;\ ;\ 0), the initial configuration of the table-driven parser. By Lemma 1 and induction on the number of moves, after each move both parsers are in the same configuration, and both make the same next move. The reductions are recorded at lines L6 and L7, at the same moves as at line L7 of SLR-PARSE.

The table-driven parser accepts exactly L(G)L(G), with a correct tree, and stops after O(n)O(n) moves for a fixed grammar; these are Theorems 4 and 7 of the SLR parsing correctness proof. Each move of the recursive ascent parser costs O(1)O(1) calls and returns, plus kk returns for a reduction by a production with kk right-side symbols, the same as the kk pops of SLR-PARSE. So it stops after O(n)O(n) steps too. □\square

Remarks

Other tables. The proof uses only that ACTION has at most one action per entry and that reductions find their right side on the stack. So the same procedures work with LR(0), LALR(1) or canonical LR(1) tables.

The depth of the calls. The chain of active calls is the LR stack, so its length is at most the largest stack height, which is O(n)O(n) for a fixed grammar and can reach nn (for right recursion or nested parentheses). The implementation keeps the chain in a list of Python generators instead of on the machine stack, which changes nothing in the argument above: a yield is a call, and a return is a return.

Back to Recursive ascent parsing