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 compiled from and the text . A configuration is a pair of an instruction number and a position . Its steps are: from char at to when and ; from jmp to ; from split to and then , in this order of preference; from save to , writing into slot ; no step from fail or match. A leaf is a configuration at a char or match instruction. A path from determines the slots it has written, so a thread created at position is the end of a path from to with , the start of the thread.
Lemma 1 (the program). For each subexpression , let be the first instruction of its code and the instruction right after it. For all , there is a path from to that stays inside the code of before it reaches if and only if . Consequently a path from to a match configuration exists if and only if , and on such a path slots 0 and 1 hold and .
Proof. By induction on , following the code shapes. For the code is fail, with no step, and . For the code is empty, so and the only path is the empty one, with . For a symbol the only path is one char step, which needs and . For the code of starts where that of ends, and any path from to passes the boundary once, so it splits into a path for and one for . For a path goes from the split into one branch, and the left branch ends with jmp to , so it is a path for or for . For a group, the two save instructions do not move. For and , a path from the split to passes through the body some number of times, each pass from the body's first instruction to the jmp back, so it reads a word of , and every such word gives such a path. The last statement follows by applying the claim to inside "save 0; code; save 1; match".
ADD and the lists. ADD with an explicit stack visits configurations at position depth first, the preferred target of a split first, and skips any pc already marked in (line L14). Call the calls of ADD on a list its entries. The list at position has as entries the successors of the char leaves of the list at that read , in the order of that list; the list at has as last entry the start if no match was recorded before (line L3).
Lemma 2 (depth first search with a shared mark set). Let be the entries of one list, in order. A pc is marked during entry for the least such that some path of steps at position leads from to .
Proof. Let be least with a path from to . No earlier entry reaches , so is not marked before . During , a depth first search from reaches every pc that has a path from avoiding the pcs marked before . If every path from to met such a pc , then was reached from some with , and would be reachable from through , against the choice of .
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 come from the list at in order and keep their starts, then possibly the start entry with start , which is larger than every start so far. Each entry appends threads with its own start.
Theorem 1 (the match exists and starts leftmost). PIKE-VM returns a match if and only if some substring is in , and then the match has the least such start , and is in for its end .
Proof. Every recorded match is the end of a real path from , so by Lemma 1 it spells a substring of . If no substring is in , nothing is recorded and the VM returns none.
Suppose some substring is in , let be the least start of one, and fix a path from to a match configuration . For each from to let be the leaf of at position (the last configuration of at position , which is a char leaf for and the match for ). Call a thread good if a match is reachable from its configuration. A good thread has start at least , since its start reaches a match through it, so by the choice of every good thread has start exactly or more.
First, a match with start is recorded at some point. Suppose not. Then the start entry is added at position : a match recorded before round would end before , so its start would be less than , which is impossible. We show by induction that 's pc is in the list at for . The pc is reached from an entry of start at most by Lemma 2 (the entry on reaches it), so its thread has start at most and is good, so its start is . It can be dropped only by line L10 for a match ahead of it, which by Lemma 3 has start at most , so equal to , against the assumption. Then its char step leads into the next list. At the thread for is a match with start , which is recorded unless an earlier match in the list is recorded first, with start at most , again a contradiction.
Second, once a match with start 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 by Lemma 3, hence exactly , since recorded matches are good. So the final match has start .
To describe which match from is returned, define the reference search BT: for in order, run a depth first search from 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 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 , 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 , its list at holds exactly the leaves at position in the order of , each with the slots of the path by which BT first visits it.
Proof. By induction on . Configurations at different positions are different, so the visited set of BT, restricted to position , is a mark set for position only. BT enters position either through the char step of a leaf at , which it takes right after first visiting that leaf, or from the start , after all searches from smaller starts are finished. By the induction hypothesis the char leaves at are first visited by BT in the order of the VM's list at , so BT makes its entries into position in the order of the VM's entries, ending with . After each entry, BT finishes the depth first search below it before the next entry. Inside position 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 . So both visit the configurations at in the same order, through the same paths, and the leaves come out in the same order.
In , a char leaf at position has one step, to position , and every configuration that BT first visits from comes right after and before the next configuration at position or earlier. Call these the descendants of . A match leaf at position is never a descendant of a char leaf at position . So for a char leaf and a match leaf at the same position: if comes before in , so do all descendants of ; if comes after , so do all its descendants.
Lemma 5 (the cut). For the real VM, let be the match recorded before round (or none). Then the list at holds, in the order of , exactly the leaves at position that come before every match leaf at a position less than , and is the first match leaf in among those at positions less than .
Proof. By induction on . At nothing is recorded and the list is the closure of , as in Lemma 4. Assume the claim at . The round at processes the list in order and stops at its first match leaf , if any.
The new : if exists, it comes before every match leaf at positions less than , by the hypothesis, and before every other match leaf at position : 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 . So is the first in among match leaves at positions up to , and the VM records it. If does not exist, every match leaf at position is outside the list, so it comes after , and is still first.
The list at : a leaf at is a descendant of some char leaf at , or comes from the start , which comes after everything at smaller starts. By the remark above, comes before every match leaf at positions up to exactly when does, that is, when is in the list at and ahead of . These are exactly the char leaves whose steps the VM follows at line L7. The start 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.
Theorem 2 (leftmost-first). PIKE-VM returns the first match configuration of , with the slots of its path, which is the result of the bit-state backtracker.
Proof. The VM ends after round , or earlier when a list is empty. If it ends after round , then is the first match leaf in by Lemma 5. If the list at is empty, then by Lemma 5 no leaf at position comes before . A leaf at a later position is a descendant of a leaf at , or comes from a start larger than , which comes after if exists. If does not exist, the start was added and gave no leaf, so the closure of has no leaf at any position, and there is no leaf after position at all. In both cases no match leaf at a later position comes before in , so the VM returns the first one.
Remark (plain backtracking). If no star of 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 distinct pcs per list, by line L14, and each pc does a bounded amount of work. There are at most rounds. So the VM stops.