Algorithm Search⌘K

Proofs: Parsing with derivatives

These are the human proofs of Parsing with derivatives, at full length.

Memoized derivatives of a cyclic grammar graph denote the Brzozowski derivatives, so the recognizer accepts exactly L(G), and every call stops full

This proof shows: correctness, termination.

We use the notation of the context-free grammar page: productions A→α∣βA \to \alpha \mid \beta, ε\varepsilon for the empty string, SS for the start symbol, nn for the input length and ∣G∣|G| for the grammar size. The line numbers are those of RECOGNIZE, DcD_c and NULLABLE on the page. For a language LL and a token cc, the Brzozowski derivative is ∂cL={v:cv∈L}\partial_c L = \{ v : c v \in L \}, and ∂cwL=∂w∂cL\partial_{c w} L = \partial_w \partial_c L.

The meaning of a graph

A node is ∅\emptyset, ε\varepsilon, a token aa, a union x∪yx \cup y or a concatenation x∘yx \circ y, where xx and yy are nodes; the graph may have cycles. The language [x][x] of every node is defined by the least sets that satisfy

[∅]=∅,[ε]={ε},[a]={a},[x∪y]=[x]∪[y],[x∘y]=[x] [y].[\emptyset] = \emptyset, \quad [\varepsilon] = \{\varepsilon\}, \quad [a] = \{a\}, \quad [x \cup y] = [x] \cup [y], \quad [x \circ y] = [x]\,[y].

Equivalently, s∈[x]s \in [x] when there is a finite membership proof: a tree whose root is the claim s∈[x]s \in [x], where a claim about x∪yx \cup y has one child, s∈[x]s \in [x] or s∈[y]s \in [y], a claim about x∘yx \circ y has two children s1∈[x]s_1 \in [x] and s2∈[y]s_2 \in [y] with s=s1s2s = s_1 s_2, and the leaves are ε∈[ε]\varepsilon \in [\varepsilon] and a∈[a]a \in [a]. The size of a claim is the number of nodes of its smallest proof.

Lemma 0 (the grammar graph). For the graph built from GG, with one union node per nonterminal (a chain of unions over its productions, ended by ∅\emptyset) and one right-nested chain of concatenations per production, [topA]=L(A)[\mathrm{top}_A] = L(A) for every nonterminal AA.

Proof. A membership proof for s∈[topA]s \in [\mathrm{top}_A] picks one production of AA in the union chain and splits ss along its right side, and each piece is again a membership proof for a symbol. Contracting the chains turns it into a parse tree of ss with root AA, and expanding a parse tree gives a membership proof. □\square

Lemma 1: compaction

COMPACT(x)(x) (line L3) returns a node with the same language as xx, and if s∈[x]s \in [x] has size mm, then s∈[COMPACT(x)]s \in [\mathrm{COMPACT}(x)] has size at most mm.

Proof. One step replaces ∅∪y\emptyset \cup y or y∪∅y \cup \emptyset by yy, ε∘y\varepsilon \circ y by yy, and returns ∅\emptyset for ∅∘y\emptyset \circ y or y∘∅y \circ \emptyset. Each is an identity of languages, and in the first two cases a proof for xx is a proof for yy with one or two extra nodes on top, so sizes do not grow. If the steps come back to a node already seen, the nodes x0,…,xr=x0x_0, \ldots, x_r = x_0 on the loop satisfy [xt]=[xt+1][x_t] = [x_{t+1}] through equations with no other contribution, so a membership proof for any of them would contain a proof for the next one, then the next, without end; there is none, all of them denote ∅\emptyset, and returning ∅\emptyset is correct. □\square

Lemma 2: nullability

After NULLABLE(x)(x) returns, every node reachable from xx is decided, and a node is marked nullable exactly when ε∈[ ⋅ ]\varepsilon \in [\,\cdot\,].

Proof. We have ε∈[x∪y]\varepsilon \in [x \cup y] iff ε∈[x]\varepsilon \in [x] or ε∈[y]\varepsilon \in [y], and ε∈[x∘y]\varepsilon \in [x \circ y] iff both, since ε=s1s2\varepsilon = s_1 s_2 forces s1=s2=εs_1 = s_2 = \varepsilon; tokens and ∅\emptyset never contain ε\varepsilon. So the nullable nodes are the least solution of these Boolean equations. Assume, by induction over the calls, that the nodes decided before the call are decided correctly. Line L11 collects the undecided nodes reachable from xx; all their children are collected or decided. For each collected node, the counter holds the number of collected children that must still be marked: 0 when the node is ε\varepsilon or a union with a child already known nullable, 1 for another union, and the number of undecided children for a concatenation whose children are not known to be non-nullable. Line L12 marks the nodes whose counter is 0 and propagates: a newly marked node decrements the counters of its users, and a counter reaching 0 marks its node. All counters are computed before any node is marked, so each child is counted once.

Soundness: by induction on the order of marking, every marked node has ε\varepsilon in its language, since its counter reached 0 only after the required children were marked or known. Completeness: if ε∈[x]\varepsilon \in [x] for a collected node, induction on the size of the claim ε∈[x]\varepsilon \in [x] shows that the children used by its smallest proof are marked or were known nullable before the call, so the counter of xx reaches 0. Line L13 then sets the rest to non-nullable, which is correct. Each collected node and each edge is handled O(1)O(1) times. □\square

Lemma 3: one derivative

Let DcD_c be called on a root during one token step. When it returns, every node uu returned by a call Dc(y)D_c(y) (line L4, L5, L6 or L10), in this step or an earlier step with the same token, satisfies [u]=∂c[y][u] = \partial_c [y].

Proof. The nodes created in this step are the memo nodes uxu_x, one for each compacted union or concatenation node xx reached in this step (plus the inner concatenation node of line L9). Write d(y)d(y) for the value returned by Dc(y)D_c(y): ∅\emptyset for ∅\emptyset and ε\varepsilon, ε\varepsilon or ∅\emptyset for a token, and otherwise the memo node of COMPACT(y)\mathrm{COMPACT}(y). When the stack is empty, every memo node has its children (lines L7 to L9):

  • ux=d(x1)∪d(x2)u_x = d(x_1) \cup d(x_2) for x=x1∪x2x = x_1 \cup x_2;
  • ux=d(x1)∘x2u_x = d(x_1) \circ x_2 for x=x1∘x2x = x_1 \circ x_2 with x1x_1 not nullable;
  • ux=(d(x1)∘x2)∪d(x2)u_x = (d(x_1) \circ x_2) \cup d(x_2) for x=x1∘x2x = x_1 \circ x_2 with x1x_1 nullable.

The nodes xx, x1x_1, x2x_2 are old: they were reachable from the root, and every node reachable from the root had its children before the step began, so NULLABLE(x1)(x_1) is decided on a complete graph and is correct by Lemma 2. Memo nodes from earlier steps with the same token are correct by induction on the steps.

⊆\subseteq: the family Xx=∂c[x]X_x = \partial_c [x] satisfies the equations of the memo nodes, because ∂c(A∪B)=∂cA∪∂cB\partial_c (A \cup B) = \partial_c A \cup \partial_c B, ∂c(AB)=(∂cA)B\partial_c (A B) = (\partial_c A) B when ε∉A\varepsilon \notin A, ∂c(AB)=(∂cA)B∪∂cB\partial_c (A B) = (\partial_c A) B \cup \partial_c B when ε∈A\varepsilon \in A, ∂c{ε}=∂c∅=∅\partial_c \{\varepsilon\} = \partial_c \emptyset = \emptyset, and ∂c{a}\partial_c \{a\} is {ε}\{\varepsilon\} for a=ca = c and ∅\emptyset otherwise; and [COMPACT(y)]=[y][\mathrm{COMPACT}(y)] = [y] by Lemma 1. The languages of the memo nodes are the least solution, so [ux]⊆∂c[x][u_x] \subseteq \partial_c [x].

⊇\supseteq: we show that cv∈[y]c v \in [y] implies v∈[d(y)]v \in [d(y)], by induction on the size of the claim cv∈[y]c v \in [y]. By Lemma 1 we may replace yy by x=COMPACT(y)x = \mathrm{COMPACT}(y) without increasing the size. If xx is a token, then x=cx = c, v=εv = \varepsilon and d(x)=εd(x) = \varepsilon. If x=x1∪x2x = x_1 \cup x_2, then cv∈[xi]c v \in [x_i] with a smaller claim, so v∈[d(xi)]⊆[ux]v \in [d(x_i)] \subseteq [u_x]. If x=x1∘x2x = x_1 \circ x_2, then cv=s1s2c v = s_1 s_2 with s1∈[x1]s_1 \in [x_1] and s2∈[x2]s_2 \in [x_2]. If s1=cv1s_1 = c v_1, then v1∈[d(x1)]v_1 \in [d(x_1)] by induction, so v=v1s2∈[d(x1)∘x2]⊆[ux]v = v_1 s_2 \in [d(x_1) \circ x_2] \subseteq [u_x]. If s1=εs_1 = \varepsilon, then x1x_1 is nullable, uxu_x has the branch d(x2)d(x_2), and cv=s2∈[x2]c v = s_2 \in [x_2] with a smaller claim, so v∈[d(x2)]⊆[ux]v \in [d(x_2)] \subseteq [u_x]. □\square

Theorem

RECOGNIZE stops on every input and returns true exactly when w∈L(G)w \in L(G).

Proof. By Lemma 0 the initial node denotes L(G)L(G). By Lemma 3 and induction on ii, after ii iterations of line L1 the current node denotes ∂a1⋯aiL(G)\partial_{a_1 \cdots a_i} L(G). So the final node denotes ∂wL(G)={v:wv∈L(G)}\partial_w L(G) = \{ v : w v \in L(G) \}, which contains ε\varepsilon exactly when w∈L(G)w \in L(G), and line L2 returns this by Lemma 2.

Termination: COMPACT stops because it never visits a node twice. One call of DcD_c on a root pushes one stack entry per new memo key; the keys are pairs (old node, cc), and there are finitely many old nodes, so the loop stops. NULLABLE handles each collected node and edge a constant number of times. □\square

Remarks

The running time. Adams, Hollenbeck and Might name every node created during parsing by the node it was derived from and the tokens it was derived by, and show that the names have the form NuN u or Nu∙vN u \bullet v with NN a node of the initial grammar and uu, uvuv substrings of the input. There are O(∣G∣n3)O(|G| n^3) such names, so O(∣G∣n3)O(|G| n^3) nodes are created; each creation costs O(1)O(1) apart from nullability, which is linear in the number of nodes in total by the counter method of Lemma 2. They note that the count holds with compaction too, since compaction only removes nodes.

Why memoization matters. Without the memo table, the rule for x1∘x2x_1 \circ x_2 with nullable x1x_1 uses x2x_2 twice, once inside d(x1)∘x2d(x_1) \circ x_2 and once as d(x2)d(x_2), and repeated derivatives copy the copies; even an acyclic grammar can then make the graph grow exponentially, and a cyclic one makes the computation loop. The memo table both shares the copies and, because it stores uxu_x before its children are computed, closes the cycles.

Back to Parsing with derivatives