Algorithm Search⌘K

Proofs: One-pass regex matching

These are the human proofs of One-pass regex matching, at full length.

The one-pass matcher is correct, and its compile test is exact full

This proof shows: correctness, termination.

Let rr have positions 1,…,k1, \dots, k and marked expression r′r'. Write π\pi for the map that replaces each position pp by sym(p)\mathrm{sym}(p), extended to sequences. A direct induction on rr shows L(r)=π(L(r′))L(r) = \pi(L(r')), since marking and π\pi commute with union, concatenation and star. The sets Nullable, First, Last and Follow are computed by the usual rules: for s∣ts \mid t they are unions; for stst, First=First(s)∪(First(t)\mathrm{First} = \mathrm{First}(s) \cup (\mathrm{First}(t) if ss is nullable)), Last=Last(t)∪(Last(s)\mathrm{Last} = \mathrm{Last}(t) \cup (\mathrm{Last}(s) if tt is nullable)), and each p∈Last(s)p \in \mathrm{Last}(s) gets First(t)\mathrm{First}(t) added to its Follow set; for s∗s^*, each p∈Last(s)p \in \mathrm{Last}(s) gets First(s)\mathrm{First}(s) added. A sequence p1⋯pnp_1 \cdots p_n with n≥1n \ge 1 is a chain of rr when p1∈First(r)p_1 \in \mathrm{First}(r), pn∈Last(r)p_n \in \mathrm{Last}(r) and pi+1∈Followr(pi)p_{i+1} \in \mathrm{Follow}_r(p_i) for each i<ni < n.

Lemma 1 (Glushkov). ε∈L(r′)\varepsilon \in L(r') if and only if rr is nullable, and a nonempty sequence is in L(r′)L(r') if and only if it is a chain of rr.

Proof. By induction on rr. The nullable part is the usual induction, so we treat nonempty sequences. For ∅\emptyset and ε\varepsilon there are no positions and no nonempty sequences in L(r′)L(r'), and First is empty, so there are no chains. For a single position pp, L(r′)={p}L(r') = \{p\}, and since First=Last={p}\mathrm{First} = \mathrm{Last} = \{p\} and Follow(p)=∅\mathrm{Follow}(p) = \emptyset, the only chain is pp.

For s∣ts \mid t, the positions of ss and of tt are disjoint, and Follow never links them. So a chain of s∣ts \mid t lies in ss or in tt and is then a chain of that part, and conversely. The claim follows from the induction hypothesis.

For stst, Follow adds only edges from Last(s)\mathrm{Last}(s) to First(t)\mathrm{First}(t), and no edge leads from a position of tt to one of ss. Take a sequence uvuv with u∈L(s′)u \in L(s') and v∈L(t′)v \in L(t'), not both empty. If both are nonempty, uu and vv are chains of ss and tt, the last element of uu is in Last(s)\mathrm{Last}(s) and the first of vv in First(t)\mathrm{First}(t), so uvuv is a chain of stst. If u=εu = \varepsilon, then ss is nullable, so First(t)⊆First(st)\mathrm{First}(t) \subseteq \mathrm{First}(st) and the chain vv of tt is a chain of stst. The case v=εv = \varepsilon is symmetric. Conversely, take a chain xx of stst and cut it before its first position of tt, as x=uvx = uv. Every step inside uu is in Follows\mathrm{Follow}_s and every step inside vv is in Followt\mathrm{Follow}_t. If uu is empty, then xx starts in First(st)\mathrm{First}(st) at a position of tt, which needs ss nullable, so ε∈L(s′)\varepsilon \in L(s'), and vv is a chain of tt. If vv is empty, then xx ends in Last(st)\mathrm{Last}(st) at a position of ss, which needs tt nullable, and uu is a chain of ss. If both are nonempty, the step from uu to vv is an added edge, so uu ends in Last(s)\mathrm{Last}(s) and vv starts in First(t)\mathrm{First}(t), and both are chains. In each case x∈L(s′)L(t′)x \in L(s')L(t').

For s∗s^*, First and Last are those of ss, and Follow adds edges from Last(s)\mathrm{Last}(s) to First(s)\mathrm{First}(s). A nonempty sequence of L(s′∗)L(s'^*) is u1⋯uju_1 \cdots u_j with each uiu_i a nonempty sequence of L(s′)L(s'), so a chain of ss, and joining them with added edges gives a chain of s∗s^*. Conversely, cut a chain of s∗s^* at each step p→qp \to q with q∉Follows(p)q \notin \mathrm{Follow}_s(p). Such a step is an added edge, so p∈Last(s)p \in \mathrm{Last}(s) and q∈First(s)q \in \mathrm{First}(s). Each piece starts in First(s)\mathrm{First}(s), ends in Last(s)\mathrm{Last}(s) and steps inside Follows\mathrm{Follow}_s, so it is a chain of ss and is in L(s′)L(s'). □\square

Let GG be the graph on {0,1,…,k}\{0, 1, \dots, k\} with an edge p→qp \to q for each q∈Follow(p)q \in \mathrm{Follow}(p), where Follow(0)=First\mathrm{Follow}(0) = \mathrm{First}, and let FF be the set of line L2. By Lemma 1, the sequences of L(r′)L(r') are exactly the sequences p1⋯pnp_1 \cdots p_n such that 0→p1→⋯→pn0 \to p_1 \to \cdots \to p_n is a path of GG ending in FF (for n=0n = 0, the path is 00 alone and 0∈F0 \in F exactly when rr is nullable). Call such a path accepting. The set UU of line L3 is the set of nodes that lie on some accepting path, apart from 00, which may or may not be in UU.

Lemma 2 (the table). If ONE-PASS-COMPILE returns (T,F)(T, F), then for each p∈U∪{0}p \in U \cup \{0\} and symbol aa, T[p][a]T[p][a] is defined exactly when some q∈Follow(p)∩Uq \in \mathrm{Follow}(p) \cap U has sym(q)=a\mathrm{sym}(q) = a, and then T[p][a]T[p][a] is that qq, which is unique.

Proof. Lines L5 to L8 insert each q∈Follow(p)∩Uq \in \mathrm{Follow}(p) \cap U under the key sym(q)\mathrm{sym}(q), and line L7 returns before a second qq with the same symbol is inserted. So, when the compile ends at line L9, each key has exactly one such qq, and every such qq is a key. □\square

Lemma 3 (one-pass runs follow accepting paths). Suppose the compile returns (T,F)(T, F). Let w=a1⋯anw = a_1 \cdots a_n and let 0→p1→⋯→pn0 \to p_1 \to \cdots \to p_n be an accepting path with sym(pi)=ai\mathrm{sym}(p_i) = a_i. Then the matcher, on ww, takes p=pip = p_i after reading aia_i for each ii, and accepts with P=p1⋯pnP = p_1 \cdots p_n.

Proof. Every pip_i lies on an accepting path, so pi∈Up_i \in U. By induction on ii, with p0=0p_0 = 0: before round ii the matcher has p=pi−1p = p_{i-1}. Since pi∈Follow(pi−1)∩Up_i \in \mathrm{Follow}(p_{i-1}) \cap U and sym(pi)=ai\mathrm{sym}(p_i) = a_i, Lemma 2 gives T[pi−1][ai]=piT[p_{i-1}][a_i] = p_i, so line L12 does not reject and line L13 sets p=pip = p_i and appends it to PP. After the loop p=pn∈Fp = p_n \in F (or p=0∈Fp = 0 \in F when n=0n = 0), so line L14 accepts with P=p1⋯pnP = p_1 \cdots p_n. □\square

Theorem 1 (the matcher). If the compile returns (T,F)(T, F), then ONE-PASS-MATCH(T,F,w)(T, F, w) accepts if and only if w∈L(r)w \in L(r). When it accepts, its list PP is in L(r′)L(r') with π(P)=w\pi(P) = w, and it is the only such sequence.

Proof. If w∈L(r)w \in L(r), then w=π(x)w = \pi(x) for some x∈L(r′)x \in L(r'), and xx gives an accepting path with the symbols of ww, so the matcher accepts by Lemma 3. Conversely, if the matcher accepts with P=p1⋯pnP = p_1 \cdots p_n, then each pi=T[pi−1][ai]p_i = T[p_{i-1}][a_i], so by Lemma 2 pi∈Follow(pi−1)p_i \in \mathrm{Follow}(p_{i-1}) and sym(pi)=ai\mathrm{sym}(p_i) = a_i, and line L14 gives pn∈Fp_n \in F. So 0→p1→⋯→pn0 \to p_1 \to \cdots \to p_n is an accepting path, P∈L(r′)P \in L(r'), and w=π(P)∈L(r)w = \pi(P) \in L(r). For uniqueness, any x∈L(r′)x \in L(r') with π(x)=w\pi(x) = w gives an accepting path, and by Lemma 3 the matcher's list equals xx, so x=Px = P. □\square

Theorem 2 (the test is exact). The compile returns NOT-ONE-PASS if and only if rr is not one-unambiguous, that is, if and only if there are upvupv and uqv′uqv' in L(r′)L(r') with p≠qp \ne q and sym(p)=sym(q)\mathrm{sym}(p) = \mathrm{sym}(q).

Proof. Suppose line L7 fires at a node p0∈U∪{0}p_0 \in U \cup \{0\} for two positions q≠q′q \ne q' in Follow(p0)∩U\mathrm{Follow}(p_0) \cap U with the same symbol. Because qq and q′q' are in UU, they are reachable from 00, and so p0p_0, which has an edge to them, is reachable from 00 too (if p0≠0p_0 \ne 0, it is in UU). Take a path from 00 to p0p_0 with the sequence of positions uu. Since qq is in UU, there is a path from qq to a node of FF with sequence qvq v, and in the same way a path from q′q' to FF with sequence q′v′q' v'. Then 0→u→q→v0 \to u \to q \to v and 0→u→q′→v′0 \to u \to q' \to v' are accepting paths, so uqvuqv and uq′v′uq'v' are in L(r′)L(r'), which breaks one-unambiguity.

Conversely, suppose upvupv and uqv′uqv' are in L(r′)L(r') with p≠qp \ne q and sym(p)=sym(q)\mathrm{sym}(p) = \mathrm{sym}(q). Their accepting paths share the prefix for uu, which ends at a node p0p_0 (the last element of uu, or 00 if uu is empty). All nodes of both paths after 00 lie on accepting paths, so they are in UU; in particular p0∈U∪{0}p_0 \in U \cup \{0\} and p,q∈Follow(p0)∩Up, q \in \mathrm{Follow}(p_0) \cap U. When the loop of line L4 reaches p0p_0, if it reaches it, line L6 meets both pp and qq with the same symbol, and line L7 returns NOT-ONE-PASS. If the loop never reaches p0p_0, it has already returned NOT-ONE-PASS at an earlier node. Either way the compile reports NOT-ONE-PASS. □\square

Termination. The Glushkov sets are computed by one walk over the mm nodes of rr. Line L3 runs two graph searches over the finite graph GG, each adding a node at most once. The loop at lines L4 to L8 runs over at most k+1k + 1 nodes and their finite Follow sets. The matcher loop runs once for each of the nn symbols of ww, with one table lookup each. So both procedures stop.

Conclusion. By Theorem 2, the compile succeeds exactly on one-unambiguous expressions, and by Theorem 1 the matcher then decides w∈L(r)w \in L(r) and returns the unique sequence of positions that spells ww. ■\blacksquare

Back to One-pass regex matching