Algorithm Search⌘K

Proofs: grep

These are the human proofs of grep, at full length.

GREP prints exactly the lines that contain a substring in the language of the expression full

This proof shows: correctness, termination.

Let NN be the NFA built at line L1, with start state ss and accepting state ff, and assume L(N)=L(r)L(N) = L(r), which is the correctness of the Thompson construction. Fix a line x=x1⋯xℓx = x_1 \cdots x_\ell and write x[j:i]=xj+1⋯xix[j:i] = x_{j+1} \cdots x_i for 0≤j≤i≤ℓ0 \le j \le i \le \ell.

A run of NN on a string vv is a path of moves that starts in ss and whose symbol labels, in order, spell vv, with any number of ε\varepsilon-moves before, between and after them. Let R(v)R(v) be the set of the last states of the runs on vv. Then v∈L(N)v \in L(N) if and only if f∈R(v)f \in R(v). For a set XX of states, CLOSE(X)\mathrm{CLOSE}(X) is the set of states that a path of zero or more ε\varepsilon-moves reaches from XX; the procedure with an explicit stack computes exactly this set, as proved for NFA simulation.

Lemma 1. For every string vv and symbol aa, R(va)=CLOSE({q′:q∈R(v) has a move on a to q′})R(va) = \mathrm{CLOSE}(\{q' : q \in R(v) \text{ has a move on } a \text{ to } q'\}).

Proof. A run on vava reads aa in exactly one symbol move, from some qq to some q′q', after the part that reads vv. That part is a run on vv ending in qq, so q∈R(v)q \in R(v), and the rest is a path of ε\varepsilon-moves from q′q'. Conversely, a run on vv ending in qq, a move on aa from qq to q′q' and a path of ε\varepsilon-moves from q′q' form a run on vava. □\square

Lemma 2 (invariant). When the test of line L6 is evaluated after ii passes of the loop, S=SiS = S_i with

Si=⋃j=0iR(x[j:i]).S_i = \bigcup_{j=0}^{i} R(x[j:i]).

Proof. By induction on ii. Before the loop, line L3 sets S=CLOSE({s})=R(ε)=R(x[0:0])=S0S = \mathrm{CLOSE}(\{s\}) = R(\varepsilon) = R(x[0:0]) = S_0. Assume S=Si−1S = S_{i-1} before pass ii. Line L8 sets T=M∪{s}T = M \cup \{s\}, where MM is the set of states q′q' such that some q∈Si−1q \in S_{i-1} has a move on xix_i to q′q'. CLOSE distributes over union, so line L9 gives

S=CLOSE(M)∪CLOSE({s})=⋃j=0i−1CLOSE({q′:q∈R(x[j:i−1]) has a move on xi to q′})∪R(ε),S = \mathrm{CLOSE}(M) \cup \mathrm{CLOSE}(\{s\}) = \bigcup_{j=0}^{i-1} \mathrm{CLOSE}(\{q' : q \in R(x[j:i-1]) \text{ has a move on } x_i \text{ to } q'\}) \cup R(\varepsilon),

and by Lemma 1 the union over jj is ⋃j=0i−1R(x[j:i])\bigcup_{j=0}^{i-1} R(x[j:i]), while R(ε)=R(x[i:i])R(\varepsilon) = R(x[i:i]). So S=SiS = S_i. □\square

Lemma 3. After line L4 or line L10 in pass ii (with i=0i = 0 for line L4), matched is true if and only if some substring x[j:i]x[j:i] with j≤ij \le i is in L(r)L(r).

Proof. By Lemma 2, f∈Sif \in S_i if and only if f∈R(x[j:i])f \in R(x[j:i]) for some j≤ij \le i, that is, if and only if x[j:i]∈L(N)=L(r)x[j:i] \in L(N) = L(r) for some j≤ij \le i. □\square

Theorem. For each line, the loop of lines L6 to L10 stops, and line L11 prints the line if and only if some substring of it is in L(r)L(r).

Proof. Each pass increases ii by 1, and the loop stops when i=ℓi = \ell at the latest, so it makes at most ℓ\ell passes. Each CLOSE stops because each state is pushed at most once.

If the loop stops with matched true after pass ii, then by Lemma 3 some substring x[j:i]x[j:i] is in L(r)L(r), and the line is printed.

If it stops with matched false, then i=ℓi = \ell and matched was false after every pass 0,1,…,ℓ0, 1, \ldots, \ell. By Lemma 3, no substring x[j:i]x[j:i] with 0≤j≤i≤ℓ0 \le j \le i \le \ell is in L(r)L(r). These are all the substrings of xx, so the line is correctly not printed. The lines are handled one after another in order, so the output is the selected lines in order. ■\blacksquare

Cost. The NFA has at most 2m2m states and each state has at most two moves. Each pass of the loop handles each state of SS once at line L8 and pushes each state at most once in CLOSE, so a pass costs O(m)O(m), and a line of length ℓ\ell costs O(m(ℓ+1))O(m(\ell + 1)).

Back to grep