Algorithm Search⌘K

Proofs: Pike VM

These are the human proofs of Pike VM, at full length.

The Pike VM returns the leftmost-first match full

This proof shows: correctness, termination.

Fix the program prog\mathit{prog} compiled from rr and the text w=a1⋯anw = a_1 \cdots a_n. A configuration is a pair (p,i)(p, i) of an instruction number and a position 0≤i≤n0 \le i \le n. Its steps are: from char aa at (p,i)(p, i) to (p+1,i+1)(p + 1, i + 1) when i<ni < n and ai+1=aa_{i+1} = a; from jmp xx to (x,i)(x, i); from split x,yx, y to (x,i)(x, i) and then (y,i)(y, i), in this order of preference; from save kk to (p+1,i)(p + 1, i), writing ii into slot kk; no step from fail or match. A leaf is a configuration at a char or match instruction. A path from (0,s)(0, s) determines the slots it has written, so a thread (p,caps)(p, \mathit{caps}) created at position ii is the end of a path from (0,s)(0, s) to (p,i)(p, i) with s=caps[0]s = \mathit{caps}[0], the start of the thread.

Lemma 1 (the program). For each subexpression tt, let bb be the first instruction of its code and ee the instruction right after it. For all i≤ji \le j, there is a path from (b,i)(b, i) to (e,j)(e, j) that stays inside the code of tt before it reaches ee if and only if ai+1⋯aj∈L(t)a_{i+1} \cdots a_j \in L(t). Consequently a path from (0,s)(0, s) to a match configuration (p,e)(p, e) exists if and only if as+1⋯ae∈L(r)a_{s+1} \cdots a_e \in L(r), and on such a path slots 0 and 1 hold ss and ee.

Proof. By induction on tt, following the code shapes. For ∅\emptyset the code is fail, with no step, and L(∅)=∅L(\emptyset) = \emptyset. For ε\varepsilon the code is empty, so b=eb = e and the only path is the empty one, with i=ji = j. For a symbol aa the only path is one char step, which needs j=i+1j = i + 1 and ai+1=aa_{i+1} = a. For t1t2t_1 t_2 the code of t2t_2 starts where that of t1t_1 ends, and any path from bb to ee passes the boundary once, so it splits into a path for t1t_1 and one for t2t_2. For t1∣t2t_1 \mid t_2 a path goes from the split into one branch, and the left branch ends with jmp to ee, so it is a path for t1t_1 or for t2t_2. For a group, the two save instructions do not move. For t1∗t_1^* and t1∗?t_1^{*?}, a path from the split to ee passes through the body some number q≥0q \ge 0 of times, each pass from the body's first instruction to the jmp back, so it reads a word of L(t1)qL(t_1)^q, and every such word gives such a path. The last statement follows by applying the claim to rr inside "save 0; code; save 1; match". □\square

ADD and the lists. ADD(L,p,caps,i)(L, p, \mathit{caps}, i) with an explicit stack visits configurations at position ii depth first, the preferred target of a split first, and skips any pc already marked in LL (line L14). Call the calls of ADD on a list its entries. The list at position i+1i + 1 has as entries the successors of the char leaves of the list at ii that read ai+1a_{i+1}, in the order of that list; the list at ii has as last entry the start (0,i)(0, i) if no match was recorded before (line L3).

Lemma 2 (depth first search with a shared mark set). Let e1,e2,…e_1, e_2, \dots be the entries of one list, in order. A pc xx is marked during entry ete_t for the least tt such that some path of steps at position ii leads from ete_t to xx.

Proof. Let tt be least with a path from ete_t to xx. No earlier entry reaches xx, so xx is not marked before ete_t. During ete_t, a depth first search from ete_t reaches every pc that has a path from ete_t avoiding the pcs marked before ete_t. If every path from ete_t to xx met such a pc yy, then yy was reached from some et′e_{t'} with t′<tt' < t, and xx would be reachable from et′e_{t'} through yy, against the choice of tt. □\square

Lemma 3 (sorted by start). In every list, the starts of the threads are nondecreasing in list order, and the starts of the entries are too.

Proof. By induction on the position. The entries of the list at i+1i + 1 come from the list at ii in order and keep their starts, then possibly the start entry with start i+1i + 1, which is larger than every start so far. Each entry appends threads with its own start. □\square

Theorem 1 (the match exists and starts leftmost). PIKE-VM returns a match if and only if some substring as+1⋯aea_{s+1} \cdots a_e is in L(r)L(r), and then the match has the least such start s∗s^*, and as∗+1⋯ae′a_{s^*+1} \cdots a_{e'} is in L(r)L(r) for its end e′e'.

Proof. Every recorded match is the end of a real path from (0,s)(0, s), so by Lemma 1 it spells a substring of L(r)L(r). If no substring is in L(r)L(r), nothing is recorded and the VM returns none.

Suppose some substring is in L(r)L(r), let s∗s^* be the least start of one, and fix a path π\pi from (0,s∗)(0, s^*) to a match configuration (pm,e)(p_m, e). For each ii from s∗s^* to ee let xix_i be the leaf of π\pi at position ii (the last configuration of π\pi at position ii, which is a char leaf for i<ei < e and the match for i=ei = e). Call a thread good if a match is reachable from its configuration. A good thread has start at least s∗s^*, since its start reaches a match through it, so by the choice of s∗s^* every good thread has start exactly s∗s^* or more.

First, a match with start s∗s^* is recorded at some point. Suppose not. Then the start entry (0,s∗)(0, s^*) is added at position s∗s^*: a match recorded before round s∗s^* would end before s∗s^*, so its start would be less than s∗s^*, which is impossible. We show by induction that xix_i's pc is in the list at ii for i=s∗,…,ei = s^*, \dots, e. The pc is reached from an entry of start at most s∗s^* by Lemma 2 (the entry on π\pi reaches it), so its thread has start at most s∗s^* and is good, so its start is s∗s^*. It can be dropped only by line L10 for a match ahead of it, which by Lemma 3 has start at most s∗s^*, so equal to s∗s^*, against the assumption. Then its char step leads into the next list. At i=ei = e the thread for pmp_m is a match with start s∗s^*, which is recorded unless an earlier match in the list is recorded first, with start at most s∗s^*, again a contradiction.

Second, once a match with start s∗s^* is recorded, every later recorded match comes from a thread ahead of it in some list, or from a descendant of such a thread, and so has start at most s∗s^* by Lemma 3, hence exactly s∗s^*, since recorded matches are good. So the final match has start s∗s^*. □\square

To describe which match from s∗s^* is returned, define the reference search BT: for s=0,1,…,ns = 0, 1, \dots, n in order, run a depth first search from (0,s)(0, s) that follows the steps of a configuration in order of preference, keeps one global set of visited configurations, and skips a configuration already visited. Let P\mathcal{P} be the order in which BT, run to the end without stopping, first visits configurations. The bit-state backtracker returns the first match configuration in P\mathcal{P}, with the slots of the path that first visited it.

Lemma 4 (same order). Consider the VM with line L10 disabled and line L3 always active. For every position ii, its list at ii holds exactly the leaves at position ii in the order of P\mathcal{P}, each with the slots of the path by which BT first visits it.

Proof. By induction on ii. Configurations at different positions are different, so the visited set of BT, restricted to position ii, is a mark set for position ii only. BT enters position ii either through the char step of a leaf at i−1i - 1, which it takes right after first visiting that leaf, or from the start (0,i)(0, i), after all searches from smaller starts are finished. By the induction hypothesis the char leaves at i−1i - 1 are first visited by BT in the order of the VM's list at i−1i - 1, so BT makes its entries into position ii in the order of the VM's entries, ending with (0,i)(0, i). After each entry, BT finishes the depth first search below it before the next entry. Inside position ii this search follows the same steps in the same order as ADD, with the same marks, and the searches at later positions that it starts do not touch position ii. So both visit the configurations at ii in the same order, through the same paths, and the leaves come out in the same order. □\square

In P\mathcal{P}, a char leaf uu at position ii has one step, to position i+1i + 1, and every configuration that BT first visits from uu comes right after uu and before the next configuration at position ii or earlier. Call these the descendants of uu. A match leaf at position ii is never a descendant of a char leaf at position ii. So for a char leaf uu and a match leaf qq at the same position: if uu comes before qq in P\mathcal{P}, so do all descendants of uu; if uu comes after qq, so do all its descendants.

Lemma 5 (the cut). For the real VM, let RiR_i be the match recorded before round ii (or none). Then the list at ii holds, in the order of P\mathcal{P}, exactly the leaves at position ii that come before every match leaf at a position less than ii, and RiR_i is the first match leaf in P\mathcal{P} among those at positions less than ii.

Proof. By induction on ii. At i=0i = 0 nothing is recorded and the list is the closure of (0,0)(0, 0), as in Lemma 4. Assume the claim at ii. The round at ii processes the list in order and stops at its first match leaf qq, if any.

The new Ri+1R_{i+1}: if qq exists, it comes before every match leaf at positions less than ii, by the hypothesis, and before every other match leaf at position ii: those in the list come after it, and those not in the list come after some match leaf at a smaller position, which comes after qq. So qq is the first in P\mathcal{P} among match leaves at positions up to ii, and the VM records it. If qq does not exist, every match leaf at position ii is outside the list, so it comes after RiR_i, and Ri+1=RiR_{i+1} = R_i is still first.

The list at i+1i + 1: a leaf vv at i+1i + 1 is a descendant of some char leaf uu at ii, or comes from the start (0,i+1)(0, i + 1), which comes after everything at smaller starts. By the remark above, vv comes before every match leaf at positions up to ii exactly when uu does, that is, when uu is in the list at ii and ahead of qq. These are exactly the char leaves whose steps the VM follows at line L7. The start (0,i+1)(0, i + 1) comes before every match leaf at smaller positions exactly when there is none, which is when line L3 adds it. Since the dropped entries come after the kept ones, the kept entries see the same marks as in Lemma 4, and the leaves come out in the same order, with the same slots. □\square

Theorem 2 (leftmost-first). PIKE-VM returns the first match configuration of P\mathcal{P}, with the slots of its path, which is the result of the bit-state backtracker.

Proof. The VM ends after round nn, or earlier when a list is empty. If it ends after round nn, then Rn+1R_{n+1} is the first match leaf in P\mathcal{P} by Lemma 5. If the list at ii is empty, then by Lemma 5 no leaf at position ii comes before RiR_i. A leaf at a later position is a descendant of a leaf at ii, or comes from a start larger than ii, which comes after RiR_i if RiR_i exists. If RiR_i does not exist, the start (0,i)(0, i) was added and gave no leaf, so the closure of (0,⋅)(0, \cdot) has no leaf at any position, and there is no leaf after position ii at all. In both cases no match leaf at a later position comes before RiR_i in P\mathcal{P}, so the VM returns the first one. □\square

Remark (plain backtracking). If no star of rr has a nullable body, there is no cycle of steps at one position: every backward jump closes a star, and going around it at one position means reading the empty word through its body, which by Lemma 1 needs a nullable body. Then the configurations form an acyclic graph, every path is simple, and a configuration that BT skips was fully searched before without finding a match, so skipping it changes nothing. BT then returns the first accepting path in the order of preference, which is what a backtracking matcher without a visited set returns.

Termination. Each round calls ADD on at most mm distinct pcs per list, by line L14, and each pc does a bounded amount of work. There are at most n+1n + 1 rounds. So the VM stops. ■\blacksquare

Back to Pike VM