In [1]:
from tock import *

Pushdown automata

Pushdown automata (PDA) are NFAs equipped with a stack (or pushdown store). At every step, they are allowed to pop a symbol from the stack and/or push a symbol onto the stack. By default, they are nondeterministic. The PDA in Example 2.14 is:


In [2]:
m = read_csv("../examples/sipser-2-14.csv")
m.is_finite(), m.is_pushdown(), m.is_deterministic()


Out[2]:
(False, True, True)

In [3]:
to_table(m)


Out[3]:
ε,ε ε,$ 0,ε 1,0
>q1 q2,$
q2 q2,0 q3,ε
q3 q4,ε q3,ε
@q4

The first column lists the states, just as in a FA. The first row lists pairs of input symbols and popped stack symbols. Note that this is a little different from Sipser, who uses two header rows, one for stack symbols and one for input symbols.

The cells contain pairs of new states and pushed stack symbols. So, if the machine is in state q2, and the next input symbol is 0, then the machine stays in state q2 and pushes a 0 onto the stack. If a cell has multiple tuples, then each tuple must be enclosed in parentheses.

Here's the state transition diagram:


In [4]:
to_graph(m)


%3 _START 0 q1 _START->0 1 q2 0->1 ε,ε → $ 1->1 0,ε → 0 2 q3 1->2 1,0 → ε 2->2 1,0 → ε 3 q4 2->3 ε,$ → ε

Now it's easier to see that it accepts strings of the form $\texttt{0}^n\texttt{1}^n$. If you draw the graph in a graph editor, use -> for a right arrow.


In [5]:
run(m, "0 0 0 1 1 1")


%3 _START 1 q1,ε _START->1 0 q2,$ 2 q2,[0] $ 0->2 1->0 3 q2,[0] 0 $ 2->3 4 q2,[0] 0 0 … 3->4 5 q3,[0] 0 $ 4->5 6 q3,[0] $ 5->6 7 q3,$ 6->7 8 q4,ε 7->8 9 10 9->10 0 11 10->11 0 12 11->12 0 13 12->13 1 14 13->14 1 15 14->15 1

The nodes of the run now show the contents of the stack as well. The top symbol of the stack is marked with square brackets. If the stack gets too deep, then only the top few elements are shown, with an ellipsis instead of the rest of the stack. You can change how many stack elements are displayed using the show_stack option, which defaults to 3. (More about ellipses below.)

This is a deterministic PDA. That's not very exciting, so let's try a nondeterministic PDA. This one (Example 2.18) accepts strings of the form $\{ww^R\}$.


In [6]:
m = read_csv("../examples/sipser-2-18.csv")
m.is_finite(), m.is_pushdown(), m.is_deterministic()


Out[6]:
(False, True, False)

In [7]:
to_table(m)


Out[7]:
ε,ε ε,$ 0,ε 0,0 1,ε 1,1
@>q1 q2,$
q2 q3,ε q2,0 q2,1
q3 q4,ε q3,ε q3,ε
@q4

In [8]:
to_graph(m)


%3 _START 0 q1 _START->0 1 q2 0->1 ε,ε → $ 1->1 0,ε → 0 1,ε → 1 2 q3 1->2 ε,ε → ε 2->2 0,0 → ε 1,1 → ε 3 q4 2->3 ε,$ → ε

In [9]:
run(m, "0 0 1 1 0 0")


%3 _START 0 q1,ε _START->0 1 q2,$ 0->1 2 q2,[0] $ 1->2 3 q3,$ 1->3 4 q2,[0] 0 $ 2->4 5 q3,[0] $ 2->5 6 q4,ε 3->6 7 q3,[0] 0 $ 4->7 9 q2,[1] 0 0 … 4->9 8 q3,$ 5->8 10 q4,ε 8->10 11 q3,[1] 0 0 … 9->11 12 q2,[1] 1 0 … 9->12 14 q3,[0] 0 $ 11->14 13 q3,[1] 1 0 … 12->13 15 q2,[0] 1 1 … 12->15 16 q3,[0] $ 14->16 17 q3,[0] 1 1 … 15->17 19 q2,[0] 0 1 … 15->19 18 q3,$ 16->18 22 q3,[1] 1 0 … 17->22 20 q4,ε 18->20 21 q3,[0] 0 1 … 19->21 23 24 23->24 0 25 24->25 0 26 25->26 1 27 26->27 1 28 27->28 0 29 28->29 0

Notice the nondeterminism: at every step, the automaton considers whether it might be at the midpoint of the string.

We saw that in a nondeterministic FA, there could be infinitely many runs for an input string, and the run graph indicated this using a cycle. With nondeterministic PDAs, we have a new problem: they can push/pop as many times as they want without reading in any input, so the infinitely many runs also go through infinitely many configurations! Consider the following PDA, which (for some reason) pushes an arbitrary number of # signs, reads in a single 0, then pops all the # signs again:


In [10]:
m = read_csv("../examples/pdaloop.csv")
to_graph(m)


%3 _START 0 q1 _START->0 1 q2 0->1 ε,ε → $ 1->1 ε,ε → # 2 q3 1->2 0,ε → ε 2->2 ε,# → ε 3 q4 2->3 ε,$ → ε

In [11]:
run(m, "0")


%3 _START 3 q1,ε _START->3 0 q2,[#] $ 4 q2,[#] # $ 0->4 7 q3,[#] $ 0->7 1 q3,$ 5 q4,ε 1->5 2 q2,$ 2->0 2->1 3->2 6 q2,[#] # # … 4->6 8 q3,[#] # $ 4->8 6->6 9 q3,[#] # # … 6->9 7->1 8->7 9->8 9->9 10 11 10->11 0

The graph shows us the first few pushes, but after the stack gets deep enough, only the top few items are shown, and the pushes stop creating new nodes. What isn't apparent from the run graph is that the simulator does make sure that the same number of # signs are pushed and popped.