A tape, a head, and a finite table of rules. That is enough to compute anything a computer can.
It is also enough to prove that some questions have no algorithm at all.
Tape, head, states. Simulated in Python.
One machine runs every other machine.
Halting, Rice, Post: provably unsolvable.
Diagonals, quines, and Gödel.
Formal definitions, full proofs, and runnable Python. Every output shown was produced by running the code (Python 3.11+).
Is there a mechanical method that decides, for any statement of first-order logic, whether it is provable?
To answer "no", you first need a precise meaning for "mechanical method".
All three turned out equivalent. Each showed the Entscheidungsproblem has no solution.
Turing, "On Computable Numbers, with an Application to the Entscheidungsproblem", Proc. London Math. Soc., 1936.
| Model | Memory | Power |
|---|---|---|
| DFA | finite | regular |
| PDA | one stack | context-free |
| TM | unbounded tape, read/write, two-way | everything computable |
A Turing machine is a 7-tuple M = (Q, Σ, Γ, δ, q0, qacc, qrej):
δ(q, a) = (p, b, R): in state q reading a, write b, move right, go to p.
| Outcome | Meaning |
|---|---|
| accept | reaches qacc |
| reject | reaches qrej |
| loop | runs forever |
The third outcome is new. A DFA always stops after reading its input. A TM may never stop, and you cannot always tell.
Conventions vary: some books allow a stay move S, a two-way infinite tape, or a partial δ where "no rule" means reject. All give the same power.
A configuration u q v: tape holds uv then blanks, state is q, head on the first symbol of v.
u a q b v ⊢ u p a c v if δ(q, b) = (p, c, L)
u q b v ⊢ u c p v if δ(q, b) = (p, c, R)
Start configuration: q0 w. M accepts w if there are configurations C1 ⊢ C2 ⊢ … ⊢ Ck with C1 = q0w and Ck in state qacc.
The sequence is a computation history. It is a finite string, so it can be checked, encoded, and reasoned about.
from collections import defaultdict
class TM:
"""Deterministic single-tape TM.
delta[(state, sym)] = (new_state, write, move), move in "LRS".
A missing rule means reject."""
def __init__(self, delta, start, accept="qacc", reject="qrej"):
self.delta, self.start = delta, start
self.accept, self.reject, self.blank = accept, reject, "_"
def run(self, w, max_steps=10_000, on_step=None):
tape = defaultdict(lambda: self.blank, enumerate(w))
q, head, steps = self.start, 0, 0
while q not in (self.accept, self.reject):
if on_step: on_step(tape, q, head)
if steps == max_steps:
return "timeout", steps, self.contents(tape)
if (q, tape[head]) not in self.delta:
q = self.reject
break
q, tape[head], move = self.delta[(q, tape[head])]
head += {"L": -1, "R": 1, "S": 0}[move]
steps += 1
if on_step: on_step(tape, q, head)
return q, steps, self.contents(tape)
def contents(self, tape):
used = [i for i, s in tape.items() if s != self.blank]
return "".join(tape[i] for i in range(min(used), max(used) + 1)
) if used else ""
defaultdict. Any cell never written reads as blank _. So the tape is unbounded in both directions for free.delta is a plain dict. It is the whole "program".max_steps is a safety fuse. A real TM has none, which is the whole problem with halting.on_step lets us watch the configurations.| Math | Python |
|---|---|
| Q | strings like "q0" |
| Γ | one-character strings |
| δ | dict of tuples |
| configuration | (tape, q, head) |
| ⊢ | one pass of the while loop |
inc = TM({("right", "0"): ("right", "0", "R"), # go to the end
("right", "1"): ("right", "1", "R"),
("right", "_"): ("carry", "_", "L"),
("carry", "1"): ("carry", "0", "L"), # 1 + carry = 0
("carry", "0"): ("qacc", "1", "S"), # 0 + carry = 1
("carry", "_"): ("qacc", "1", "S")}, # new leading 1
start="right")
def show(tape, q, head):
lo, hi = min([head, *tape]), max([head, *tape])
left = "".join(tape[i] for i in range(lo, head)).strip("_")
right = "".join(tape[i] for i in range(head, hi + 1)).rstrip("_")
print(f"{left}[{q}]{right or '_'}")
inc.run("1011", on_step=show)
for w in ["0", "1", "111", "1001"]:
print(w, "->", inc.run(w)[2])
[right]1011 1[right]011 10[right]11 101[right]1 1011[right]_ 101[carry]1 10[carry]10 1[carry]000 1[qacc]100 0 -> 1 1 -> 10 111 -> 1000 1001 -> 1010
A TM can compute a function, not just accept. The output is what is left on the tape when it halts.
f : Σ* → Σ* is computable if some TM, on every input w, halts with just f(w) on the tape.
Each trace line is one configuration u q v. Work: O(n) steps for n bits.
a, mark it X, go right.a/Y. Mark the first b as Y.b/Z. Mark the first c as Z. Turn back.X. Step right. Repeat.Y: no a left. q4 checks only Y/Z remain.Any other symbol in any state: no rule, so reject.
aabbccaabbcc → XaYbZc → XXYYZZ → accept
We proved with the pumping lemma that no PDA accepts this language. The TM's secret: it can re-read and rewrite the input.
d = {("q0", "a"): ("q1", "X", "R"),
("q0", "Y"): ("q4", "Y", "R"),
("q0", "_"): ("qacc", "_", "S"), # n = 0
("q1", "b"): ("q2", "Y", "R"),
("q2", "c"): ("q3", "Z", "L"),
("q3", "X"): ("q0", "X", "R"),
("q4", "_"): ("qacc", "_", "S")}
for s in "aY": d[("q1", s)] = ("q1", s, "R") # skip right
for s in "bZ": d[("q2", s)] = ("q2", s, "R")
for s in "abYZ": d[("q3", s)] = ("q3", s, "L") # rewind
for s in "YZ": d[("q4", s)] = ("q4", s, "R") # final check
abc = TM(d, start="q0")
print(len(d), "rules")
17 rules
| State | a | b | c | X | Y | Z | _ |
|---|---|---|---|---|---|---|---|
| q0 | X,R,q1 | Y,R,q4 | acc | ||||
| q1 | a,R | Y,R,q2 | Y,R | ||||
| q2 | b,R | Z,L,q3 | Z,R | ||||
| q3 | a,L | b,L | X,R,q0 | Y,L | Z,L | ||
| q4 | Y,R | Z,R | acc |
tests = ["", "abc", "aabbcc", "aabbc", "abcabc",
"aaabbbccc", "acb", "aabbbcc"]
for w in tests:
result, steps, _ = abc.run(w)
print(f"{w!r:12} {result:5} after {steps:3} steps")
'' qacc after 1 steps 'abc' qacc after 8 steps 'aabbcc' qacc after 23 steps 'aabbc' qrej after 13 steps 'abcabc' qrej after 7 steps 'aaabbbccc' qacc after 46 steps 'acb' qrej after 1 steps 'aabbbcc' qrej after 24 steps
for n in range(1, 8):
steps = abc.run("a" * n + "b" * n + "c" * n)[1]
print(n, steps, 4 * n * n + 3 * n + 1)
1 8 8 2 23 23 3 46 46 4 77 77 5 116 116 6 163 163 7 218 218
Each of the n passes walks right to the next c and back. That walk has length about 2n each way. So the total is Θ(n2).
The fit 4n2 + 3n + 1 matches every measured value in the table.
The running time of a decider M is t(n) = the max steps on any input of length n.
TIME(t(n)) = { L : some 1-tape TM decides L in O(t(n)) }
P = ⋃k TIME(nk)
With a second tape, copy the as to it and match in one sweep: O(n). On one tape, any TM for this language needs Ω(n log n) steps (Hennie 1965, via crossing sequences).
A k-tape TM has k tapes and k independent heads:
δ : Q × Γk → Q × Γk × {L, R, S}k
Input starts on tape 1. The others start blank.
Theorem. Every k-tape TM has an equivalent 1-tape TM. If the k-tape machine runs in t(n) ≥ n steps, the 1-tape one runs in O(t(n)2).
#: #abc#01#xy#.ȧ.So extra tapes add speed, not power. The quadratic gap is real for some problems: palindromes need Θ(n2) on one tape but O(n) on two.
Hennie–Stearns (1966): k tapes can be simulated on two tapes with only an O(log t) factor slowdown.
A nondeterministic TM has δ : Q × Γ → P(Q × Γ × {L,R}). It accepts if some branch of the computation tree accepts.
Theorem. Every NTM has an equivalent deterministic TM.
Cost: t(n) steps on the NTM become 2O(t(n)) on the DTM. Whether this can be made polynomial is the P vs NP question.
| Variant | Same power? |
|---|---|
| Two-way infinite tape | yes (fold the tape in half) |
| Stay-put move S | yes (R then L) |
| Tape alphabet {0, 1, _} only | yes (binary encode Γ) |
| 2-D grid tape | yes |
| Queue automaton (one queue) | yes |
| PDA with two stacks | yes (stacks = left and right of head) |
| Two-counter machine (Minsky) | yes (encode stacks as 2a3b) |
| Rule 110 cellular automaton | yes (Cook 2004) |
| PDA with one stack | no: CFL only |
| Read-only TM (no writing) | no: regular only |
| TM with tape bounded by input (LBA) | context-sensitive only |
The class of recognizable languages does not care about these details. That robustness is strong evidence that it is the "right" definition.
"Effective procedure" is an informal idea: finite instructions, followed mechanically, no insight needed. You cannot prove a formal claim about an informal notion.
The thesis defines "algorithm" as "Turing machine". It could in principle be refuted by a physical device that computes more.
| Version | Claim | Status |
|---|---|---|
| Classic | computable = TM-computable | accepted |
| Physical | no physical device computes more | believed |
| Extended (strong) | any reasonable model is poly-time equal to a TM | doubted: quantum computers |
From here on we may describe machines in plain English or Python. Any clear step-by-step procedure can be turned into a TM. If we prove "no TM does X", then no program in any language does X.
Quantum computers do not break the classic thesis. A TM can simulate them, only slowly.
〈O〉 denotes a string encoding of an object O: a number, a graph, a grammar, or a Turing machine.
A TM is a finite table. Write out its rules as text and you have 〈M〉.
def encode(tm):
"""<M>: every rule as q,s,p,w,m; then the start state."""
rules = sorted(tm.delta.items())
body = ";".join(f"{q},{s},{p},{w},{m}"
for (q, s), (p, w, m) in rules)
return body + "#" + tm.start
code = encode(inc)
print(len(code), "chars:", code[:44] + "...")
111 chars: carry,0,qacc,1,S;carry,1,carry,0,L;carry,_,q...
Any reasonable encoding works. The machine that reads it can translate between encodings.
from itertools import count, islice, product
def shortlex(alphabet):
"""Every string, shortest first, each exactly once."""
for n in count():
for t in product(alphabet, repeat=n):
yield "".join(t)
print(list(islice(shortlex("01"), 15)))
['', '0', '1', '00', '01', '10', '11', '000', '001', '010', '011', '100', '101', '110', '111']
Walk through all strings in shortlex order. Keep those that are valid encodings. That lists every TM: M1, M2, M3, …
This list is the backbone of every diagonal argument ahead.
Theorem (Turing 1936). There is a TM U such that for every TM M and input w:
U(〈M, w〉) behaves exactly like M(w)
It accepts, rejects, or loops, just as M does.
def U(code, w, max_steps=10_000):
"""Universal machine: parse <M>, then simulate M on w."""
body, start = code.split("#")
delta = {}
for rule in body.split(";"):
q, s, p, wr, m = rule.split(",")
delta[(q, s)] = (p, wr, m)
return TM(delta, start).run(w, max_steps)
print(U(encode(inc), "1011"))
print(U(encode(abc), "aabbcc")[0], U(encode(abc), "aabcc")[0])
('qacc', 8, '1100')
qacc qrej
Programs are data. One fixed machine runs any program you give it. This idea predates, and inspired, the von Neumann architecture (1945). Your CPU is a universal machine. The Python interpreter is too.
Small universal TMs exist: Minsky (1962) found one with 7 states and 4 symbols. Wolfram's 2-state 3-symbol machine was shown universal by Alex Smith (2007), in a weak sense.
L is Turing-recognizable (recursively enumerable, RE) if some TM accepts exactly the strings in L. On w ∉ L it may reject or loop.
L is decidable (recursive, R) if some TM accepts every w ∈ L and rejects every w ∉ L. It always halts. Such a TM is a decider.
L is co-recognizable (co-RE) if its complement ̅L is recognizable.
| Language | Class |
|---|---|
| ADFA, ACFG, primes | decidable |
| ATM = {〈M,w〉 : M accepts w} | RE, not decidable |
| ETM = {〈M〉 : L(M) = ∅} | co-RE, not RE |
| EQTM, TOTALTM | neither |
Theorem. L is decidable iff L is both recognizable and co-recognizable.
(⇒) A decider for L recognizes L. Swap its answers and it recognizes ̅L.
(⇐) Let M1 recognize L and M2 recognize ̅L. On input w, run both in parallel, one step each in turn. Every w is in L or ̅L, so one of them accepts. If M1 accepts, accept. If M2 accepts, reject. ∎
RE also means "enumerable": some TM prints every string of L (in some order, maybe with repeats). Recognizer → enumerator uses dovetailing: for i = 1, 2, 3, … run the recognizer i steps on the first i strings.
Theorem (Cantor). For any set S, there is no onto map S → P(S). In particular the subsets of ℕ are uncountable.
Suppose L0, L1, L2, … lists every subset of ℕ. Define
D = { i : i ∉ Li }
For every i, D and Li disagree about i. So D is not on the list. Contradiction. ∎
Picture an infinite table: row i is Li, column n says whether n ∈ Li. D flips the diagonal, so it differs from every row somewhere.
The same trick shows the reals are uncountable. Turing reused it for halting, and Gödel for incompleteness.
# A (finite) list of "languages" over N, as predicates
langs = [lambda n: n % 2 == 0,
lambda n: n > 3,
lambda n: n in {1, 2, 3},
lambda n: False,
lambda n: bin(n).count("1") == 2,
lambda n: True]
D = lambda n: not langs[n](n) # flip the diagonal
N = len(langs)
for i, L in enumerate(langs):
row = " ".join("1" if L(n) else "." for n in range(N))
print(f"L{i}: {row} D differs at {i}: {D(i) != L(i)}")
print(" D:", " ".join("1" if D(n) else "." for n in range(N)))
L0: 1 . 1 . 1 . D differs at 0: True L1: . . . . 1 1 D differs at 1: True L2: . 1 1 1 . . D differs at 2: True L3: . . . . . . D differs at 3: True L4: . . . 1 . 1 D differs at 4: True L5: 1 1 1 1 1 1 D differs at 5: True D: . 1 . 1 1 .
Read down the diagonal of the table: 1 . 1 . . 1. D is its flip: . 1 . 1 1 ..
Theorem. Some languages are not Turing-recognizable. In fact, almost all of them.
This proof is non-constructive. It says undecidable languages exist, but it names none. The next slides build a specific, useful one.
Picked "at random", a language is undecidable with probability 1. The decidable ones are the rare exception.
| Set | Size |
|---|---|
| strings Σ* | ℵ0 |
| Turing machines | ℵ0 |
| languages P(Σ*) | 2ℵ0 (like the reals) |
ATM = { 〈M, w〉 : M accepts w }
HALTTM = { 〈M, w〉 : M halts on w }
Theorem (Turing 1936). ATM is recognizable but not decidable. The same holds for HALTTM.
Run U on 〈M, w〉. Accept if it accepts. If M loops, so does U, which a recognizer is allowed to do.
̅ATM is not recognizable. If it were, ATM would be both RE and co-RE, hence decidable.
D(〈D〉) accepts ⇔ H says D rejects 〈D〉 ⇔ D(〈D〉) rejects
Contradiction. So H does not exist. ∎
| 〈M1〉 | 〈M2〉 | 〈M3〉 | … | 〈D〉 | |
|---|---|---|---|---|---|
| M1 | acc | rej | acc | acc | |
| M2 | acc | acc | acc | rej | |
| M3 | rej | rej | rej | rej | |
| D | rej | rej | acc | ? |
Suppose someone hands us halts(f): it returns True iff calling f() would finish. We build a program that does the opposite of whatever halts predicts about it.
def make_D(halts):
"""Build the program that defeats a claimed halting decider."""
def D():
if halts(D):
while True: # predicted to halt? loop forever
pass
return "halted" # predicted to loop? halt at once
return D
candidates = {"always yes": lambda f: True,
"always no": lambda f: False,
"by name": lambda f: f.__name__ != "D"}
for name, halts in candidates.items():
D = make_D(halts)
says = halts(D)
if says:
truth = "loops forever" # D would enter while True
else:
truth = "returns " + repr(D()) # safe: we actually run it
print(f"{name:10} halts(D) = {says!s:5} but D {truth}")
always yes halts(D) = True but D loops forever always no halts(D) = False but D returns 'halted' by name halts(D) = False but D returns 'halted'
make_D works for any halts function, however clever.D asks about itself, then does the opposite.D.D() when it is predicted to loop. Then it returns at once, which proves the prediction wrong.halts answers "loops" for slow programs that do halt. It is a heuristic, not a decider.A mapping-reduces to B, written A ≤m B, if there is a computable function f with
w ∈ A ⇔ f(w) ∈ B for all w
Theorem. If A ≤m B:
To prove a new problem B undecidable: take a known one, like ATM. Show how a decider for B would give a decider for ATM.
The direction matters: reduce from the known hard problem to the new one.
f(〈M, w〉) = 〈M', w〉, where M' runs M and, if M rejects, loops instead.
Then M' halts on w iff M accepts w. f just edits the program text, so it is computable.
Turing reductions (A ≤T B) are more general: decide A with an oracle for B, asking many questions. They preserve decidability but not recognizability.
Given 〈M, w〉, build Mw:
M_w(x): if x != w: reject run M on w accept if M accepts
L(Mw) ≠ ∅ iff M accepts w. So a decider for ETM decides ATM.
ETM is co-RE: its complement is RE (search for an accepted string by dovetailing).
Reduce from ETM. Let M∅ reject everything.
f(<M>) = <M, M_∅>
L(M) = L(M∅) iff L(M) = ∅.
EQTM is neither RE nor co-RE. So no tool can prove two arbitrary programs equivalent, or ever find all differences.
Given 〈M, w〉, build R:
R(x): if x = 0^n 1^n: accept run M on w accept if M accepts
If M accepts w: L(R) = Σ*, regular. Otherwise: L(R) = {0n1n}, not regular.
The same template shows "is L(M) context-free?" and "is L(M) finite?" are undecidable.
Pattern: build a machine that ignores or filters its own input, and whose language depends on whether M accepts w. Rice's theorem generalizes this.
HALTε = { 〈M〉 : M halts on the blank tape }. Reduce from HALTTM.
def f(program, w):
"""Map <M, w> to <M_w>: a program that takes no input.
M_w halts iff M halts on w."""
return program + f"\nmain({w!r})\n"
M = '''def main(x):
while x != "stop":
pass'''
print(f(M, "stop"))
def main(x):
while x != "stop":
pass
main('stop')
Theorem (Rice). Let P be any non-trivial property of the language of a TM. Then {〈M〉 : L(M) has property P} is undecidable.
WLOG ∅ lacks P (else use ¬P). Pick MP whose language has P. Given 〈M, w〉, build:
T(x): run M on w # may loop
if it accepts: run M_P on x
M accepts w ⇒ L(T) = L(MP), has P. Otherwise L(T) = ∅, lacks P. So deciding P decides ATM. ∎
| Question about program M | Decidable? |
|---|---|
| Does M accept "hello"? | no (Rice) |
| Is L(M) finite? regular? empty? | no (Rice) |
| Does M ever print its password? | no (Rice) |
| Does M have more than 5 states? | yes: about code, not language |
| Does M halt on "" within 100 steps? | yes: just run it |
| Does M ever move its head left on input w? | yes, surprisingly |
| Is L(M) recognizable? | yes: trivial, always true |
Practical meaning: every static analyzer, virus scanner, or type checker is approximate. It must sometimes say "maybe", reject correct programs, or miss bugs.
Given dominoes [t1/b1], …, [tk/bk], is there a sequence i1, …, im (m ≥ 1, repeats allowed) with
ti1 ti2 … tim = bi1 bi2 … bim ?
Theorem (Post). PCP is undecidable. It is RE: search all sequences.
Reduce from ATM. Build dominoes whose matches spell an accepting computation history #C1#C2#…. The bottom row runs one configuration ahead of the top. Each domino copies a symbol or applies one rule of δ. The rows only catch up once qacc appears.
PCP has no machines in it: just strings. That makes it a great source for reductions, like CFG ambiguity and CFG intersection.
from collections import deque
def pcp(dominoes, max_len=10):
"""BFS for a match; keep only states where one row
is a prefix of the other."""
todo = deque([((), "", "")])
while todo:
seq, top, bot = todo.popleft()
if seq and top == bot:
return [i + 1 for i in seq]
if len(seq) == max_len:
continue
for i, (t, b) in enumerate(dominoes):
T, B = top + t, bot + b
if T.startswith(B) or B.startswith(T):
todo.append((seq + (i,), T, B))
return None # none found up to max_len
sipser = [("b", "ca"), ("a", "ab"), ("ca", "a"), ("abc", "c")]
print(pcp(sipser))
print(pcp([("abc", "ab"), ("ca", "a"), ("acc", "ba")]))
print(pcp([("a", "baa"), ("ab", "aa"), ("bba", "bb")]))
[2, 1, 3, 2, 4] None [3, 2, 3, 1]
First: a|b|ca|a|abc over ab|ca|a|ab|c both spell abcaaabc. Second: every top is longer, so no match ever. None from a bounded search proves nothing in general, which is the whole point.
Consider TMs with n states, symbols {0, 1}, a halt state, started on an all-0 tape.
Σ(n) = most 1s left on the tape by any such machine that halts.
S(n) = most steps taken by any such machine that halts.
Theorem. S(n) grows faster than every computable function. So S is not computable.
If S were computable, halting would be decidable: to check whether an n-state M halts on blank tape, run it S(n) steps. If it has not halted by then, it never will.
import itertools
from collections import defaultdict
def bb_run(table, max_steps=10**6):
"""table["A"] = (rule on 0, rule on 1); rule like "1RB".
Returns (steps, ones) if it halts, else None."""
tape, head, q, steps = defaultdict(int), 0, "A", 0
while q != "H" and steps < max_steps:
write, move, q = table[q][tape[head]]
tape[head] = int(write)
head += 1 if move == "R" else -1
steps += 1
return (steps, sum(tape.values())) if q == "H" else None
# Exhaustive search over ALL 2-state machines
rules = [w + m + n for w in "01" for m in "LR" for n in "ABH"]
best, halted, total = (0, 0), 0, 0
for c in itertools.product(rules, repeat=4):
total += 1
r = bb_run({"A": c[:2], "B": c[2:]}, max_steps=100)
if r:
halted += 1
best = (max(best[0], r[0]), max(best[1], r[1]))
print(total, "machines,", halted, "halt")
print("S(2) =", best[0], " Sigma(2) =", best[1])
20736 machines, 9784 halt S(2) = 6 Sigma(2) = 4
A 100-step cap is safe here only because we already know S(2) = 6. For large n, no cap is ever known to be safe.
def parse(s): # standard text notation
return {chr(65 + i): (r[:3], r[3:])
for i, r in enumerate(s.split("_"))}
champions = {
"BB(2)": "1RB1LB_1LA1RH",
"BB(3) Sigma": "1RB1RH_0RC1RB_1LC1LA",
"BB(3) S": "1RB1RH_1LB0RC_1LC1LA",
"BB(4)": "1RB1LB_1LA0LC_1RH1LD_1RD0RA",
}
for name, s in champions.items():
steps, ones = bb_run(parse(s))
print(f"{name:12} {steps:4} steps, {ones:3} ones")
BB(2) 6 steps, 4 ones BB(3) Sigma 14 steps, 6 ones BB(3) S 21 steps, 5 ones BB(4) 107 steps, 13 ones
Notation: 1RB = write 1, move right, go to B. Groups are states A, B, C, …, each with its rule for 0 then for 1. H halts.
For 3 states, the most-ones machine and the most-steps machine differ.
| n | S(n) steps | Σ(n) ones | Proved |
|---|---|---|---|
| 1 | 1 | 1 | trivial |
| 2 | 6 | 4 | Radó 1962 |
| 3 | 21 | 6 | Lin & Radó 1965 |
| 4 | 107 | 13 | Brady 1983 |
| 5 | 47,176,870 | 4,098 | bbchallenge, 2024 (Coq-verified) |
| 6 | > 2 ↑↑↑ 5 | > 2 ↑↑↑ 5 | open |
↑↑↑ is Knuth's arrow notation. 2↑↑5 is already a tower of five 2s, 265536.
Kleene's recursion theorem (1938). For any TM T computing t(x, y), there is a TM R with
R(w) = t(〈R〉, w)
In words: a machine may obtain its own description and then compute with it.
Part B is a program that, given a text x, prints x twice: once quoted as data, once as code. Part A is the text of B, as data. Run A then B and the output is AB: the whole program.
Suppose H decides ATM. Build B: "get my own code 〈B〉, ask H(〈B, w〉), do the opposite." Contradiction. The recursion theorem replaces the diagonal table.
s = 's = %r\nprint(s %% s)' print(s % s)
s = 's = %r\nprint(s %% s)' print(s % s)
%r inserts s quoted (part A as data). %% becomes %. The test file checks the output equals the source exactly.
t = 't = %r\nprint(len(t %% t))' print(len(t % t))
50
Uses: compilers that compile themselves, self-reproducing viruses and worms, Thompson's "Reflections on Trusting Trust" (1984). It also gives the fixed-point theorem: for any computable transform f of programs, some M has L(f(M)) = L(M).
def collatz_steps(n):
"""Does this loop end for every n >= 1? Nobody knows."""
steps = 0
while n != 1:
n = 3 * n + 1 if n % 2 else n // 2
steps += 1
return steps
print([collatz_steps(n) for n in range(1, 13)])
print(collatz_steps(27), collatz_steps(97), collatz_steps(871))
[0, 1, 7, 2, 5, 8, 16, 3, 19, 6, 14, 9] 111 118 178
The Collatz conjecture says this halts for every n. Checked past 268, still unproved. A general halting decider would settle it, and Goldbach, and many more.
| Tool | Undecidable core | How it copes |
|---|---|---|
| Compiler optimizer | is this code dead? are these equal? | conservative: only safe rewrites |
| Type checker | will this crash? | rejects some correct programs |
| Static analyzer (Infer, CodeQL) | null deref, leaks | false positives or misses |
| Antivirus | is this malicious? | signatures + heuristics |
| Termination checker (Coq, Agda) | does it halt? | accepts only structural recursion |
| Test suite | correct on all inputs? | checks some inputs |
Gödel's first incompleteness theorem (1931). Any consistent, effectively axiomatized theory that includes basic arithmetic has true statements it cannot prove.
| Gödel (1931) | Turing (1936) |
|---|---|
| formulas get numbers | machines get encodings |
| "this sentence is unprovable" | "D does the opposite of H on D" |
| diagonal lemma | recursion theorem |
| true but unprovable | well-defined but uncomputable |
| second theorem: T cannot prove its own consistency | Σ(n) unprovable for large n |
Hilbert's program asked for a complete, consistent, decidable foundation. Gödel ruled out the first two together. Church and Turing ruled out the third.
| Class | Machine view | Logic view |
|---|---|---|
| Decidable (R) | always-halting TM | Δ1 |
| RE | halts on yes | Σ1: ∃ proof |
| co-RE | halts on no | Π1: ∀ checks |
| Beyond | needs a halting oracle | Σ2, Π2, … |
This ladder is the arithmetical hierarchy (Kleene, Mostowski). Each level adds one quantifier, and one level of oracle.
Try each one before you open the answer.
Yes. Simulate 1000 steps and answer. The step bound removes the danger of looping.
Recognizable: run M on "hello" and accept if it does. Not decidable: it is a non-trivial property of L(M), so Rice applies.
Nothing. Any decidable problem also reduces to B. Hardness flows from A to B, never back.
No. HALTTM is RE. If its complement were RE too, it would be decidable, and it is not.
No. That is a property of the code, not of L(M). Two machines with the same language can have different state counts. Just count them.
| Mistake | Fix |
|---|---|
| "Recognizable" and "decidable" mean the same. | A recognizer may loop on "no". A decider always halts. |
| "Undecidable means no program can ever answer it." | Many instances are easy. No single program is right on all inputs. |
| "The halting proof only works for weird code." | Reductions carry it to normal questions: dead code, equality, bugs. |
| "A faster computer would solve it." | Undecidable is about logic, not speed. No amount of time helps. |
| "Rice covers every question about programs." | Only non-trivial language properties. Step counts and code shape are not covered. |
Its complement "L(M) is not empty" is RE: run M on all strings by dovetailing, and accept when one is accepted. ETM is undecidable, so it cannot also be RE.
| Problem | Status |
|---|---|
| ADFA, ACFG, ECFG | decidable |
| ATM, HALT, PCP | RE, undecidable |
| ETM, ̅ATM | co-RE, undecidable |
| EQTM, TOTAL, Σ(n) | neither |
Previous deck: context-free grammars and pushdown automata. Next: complexity, P vs NP.
| Term | Meaning |
|---|---|
| Turing machine | Finite control plus an unbounded read/write tape. |
| Configuration | A snapshot: state, tape contents, head position. |
| Decider | A TM that halts on every input. |
| Decidable (R) | Some decider accepts exactly this language. |
| Recognizable (RE) | Some TM accepts exactly the yes-strings. It may loop on no. |
| co-RE | The complement is recognizable. |
| Church–Turing thesis | Every algorithm can be run by a TM. |
| Universal TM U | Takes 〈M, w〉 and simulates M on w. |
| Dovetailing | Run many computations a few steps at a time, taking turns. |
| Term | Meaning |
|---|---|
| Diagonalization | Build an object that differs from item i at spot i. |
| ATM / HALT | Does M accept / halt on w? RE, not decidable. |
| Mapping reduction ≤m | Computable f with x ∈ A ⇔ f(x) ∈ B. |
| Rice's theorem | Every non-trivial property of L(M) is undecidable. |
| PCP | Domino matching puzzle. Undecidable, no machines needed. |
| Busy beaver S(n) | Most steps of any halting n-state machine. |
| Quine | A program that prints its own source. |
| Recursion theorem | A program can get its own code and use it. |
| RE-complete | In RE, and every RE problem reduces to it. |