Turing Machines
& Undecidability

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.

Machines

Tape, head, states. Simulated in Python.

Universality

One machine runs every other machine.

Limits

Halting, Rice, Post: provably unsolvable.

Self-reference

Diagonals, quines, and Gödel.

Formal definitions, full proofs, and runnable Python. Every output shown was produced by running the code (Python 3.11+).

Roadmap WHERE WE GO

  1. Why Turing machines?
  2. Formal definition
  3. Tape & configurations
  4. A TM simulator in Python
  5. Binary increment
  6. Designing anbncn
  7. Running it: steps & time
  8. Multi-tape machines
  9. Nondeterminism & other variants
  10. Church–Turing thesis
  11. Encodings & enumeration
  12. The universal machine
  13. Decidable, recognizable, co-recognizable
  14. Cantor's diagonal argument
  15. Most languages are undecidable
  16. The halting problem
  17. A Python paradox
  18. Reductions
  19. Reduction examples
  20. Rice's theorem
  21. Post correspondence problem
  22. Busy beaver: definition
  23. Busy beaver: champions
  24. Recursion theorem & quines
  25. Undecidability in practice
  26. The Gödel connection
  27. Map of the classes
  28. Summary

Why Turing Machines? HISTORY

The Entscheidungsproblem (Hilbert, 1928)

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".

1936: three answers at once

  • Church: the λ-calculus.
  • Turing: an idealized human clerk with pencil and paper, reduced to a machine.
  • Post: a similar worker model, independently.

All three turned out equivalent. Each showed the Entscheidungsproblem has no solution.

Turing's reasoning about the clerk

  • Paper is split into squares, each with one symbol from a finite set.
  • The clerk looks at a bounded area at a time. So one square is enough.
  • The clerk has finitely many "states of mind".
  • Each step changes one symbol, moves a little, and changes state.

Turing, "On Computable Numbers, with an Application to the Entscheidungsproblem", Proc. London Math. Soc., 1936.

ModelMemoryPower
DFAfiniteregular
PDAone stackcontext-free
TMunbounded tape, read/write, two-wayeverything computable

Turing Machine DEFINITION

A Turing machine is a 7-tuple M = (Q, Σ, Γ, δ, q0, qacc, qrej):

  • Q: finite set of states.
  • Σ: input alphabet, with blank ␣ ∉ Σ.
  • Γ ⊇ Σ ∪ {␣}: tape alphabet.
  • δ : Q × Γ → Q × Γ × {L, R}: transition function.
  • q0 start, qacc ≠ qrej halting states.

δ(q, a) = (p, b, R): in state q reading a, write b, move right, go to p.

Three possible outcomes on input w

OutcomeMeaning
acceptreaches qacc
rejectreaches qrej
loopruns forever

The third outcome is new. A DFA always stops after reading its input. A TM may never stop, and you cannot always tell.

Differences from a DFA

  • It can write on the tape.
  • The head moves both ways.
  • The tape is unbounded to the right.
  • It halts at once on entering accept or reject, not at the end of input.

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.

Tape & Configurations SNAPSHOTS

XaYbZc␣␣␣ … unbounded state q2 cell 0head reads b configuration: XaY q2 bZc left of head · state · head and right

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.

A TM Simulator in Python CODE

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 ""

Design notes

  • The tape is a 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.

Matching the math

MathPython
Qstrings like "q0"
Γone-character strings
δdict of tuples
configuration(tape, q, head)
⊢one pass of the while loop

Example: Binary Increment COMPUTING A FUNCTION

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
rightcarryacc 0→0,R   1→1,R 1→0,L _→_,L 0→1_→1 label "read → write, move"

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.

Designing a TM for anbncn BEYOND CFL

Strategy: cross off one of each per pass

  1. q0: at an a, mark it X, go right.
  2. q1: skip a/Y. Mark the first b as Y.
  3. q2: skip b/Z. Mark the first c as Z. Turn back.
  4. q3: run left to the last X. Step right. Repeat.
  5. q0 sees Y: no a left. q4 checks only Y/Z remain.

Any other symbol in any state: no rule, so reject.

Tape after each pass on aabbcc

aabbcc  →  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
StateabcXYZ_
q0X,R,q1Y,R,q4acc
q1a,RY,R,q2Y,R
q2b,RZ,L,q3Z,R
q3a,Lb,LX,R,q0Y,LZ,L
q4Y,RZ,Racc

Running It: Steps & Time COMPLEXITY

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

Exactly 4n² + 3n + 1 steps

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).

Multi-Tape Machines VARIANT

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).

Proof idea

  • Store all tapes on one, split by #: #abc#01#xy#.
  • Mark each head position with a dotted copy of the symbol: ȧ.
  • One simulated step = one sweep to read all dotted symbols + one sweep to update them.
  • If a tape grows, shift the rest right by one cell.
  • Each tape has length ≤ t(n), so each step costs O(k·t(n)).
tape 1tape 2tape 3 abc 01 xy_ simulate #aḃc#0̇1#xy_̇#_ one tape; dotted symbols (orange) mark the three heads

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.

Nondeterminism & Other Variants ROBUSTNESS

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.

Proof: breadth-first search of the tree

  • Let b = the max number of choices at any step.
  • Use 3 tapes: input (read-only), simulation, and an address in {1..b}*.
  • For each address in shortlex order, rerun from the start, following those choices.
  • Breadth-first matters. Depth-first could fall into an infinite branch and miss an accepting one.

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.

VariantSame power?
Two-way infinite tapeyes (fold the tape in half)
Stay-put move Syes (R then L)
Tape alphabet {0, 1, _} onlyyes (binary encode Γ)
2-D grid tapeyes
Queue automaton (one queue)yes
PDA with two stacksyes (stacks = left and right of head)
Two-counter machine (Minsky)yes (encode stacks as 2a3b)
Rule 110 cellular automatonyes (Cook 2004)
PDA with one stackno: 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.

The Church–Turing Thesis THESIS

Every function that can be computed by an effective procedure can be computed by a Turing machine.

Why it is a thesis, not a theorem

"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.

The evidence

  • Every proposed model is provably equal: λ-calculus, μ-recursive functions, Post systems, register machines, Python, C, Haskell.
  • The model is robust to changes (last slide).
  • Nobody has found a counterexample in 90 years.

Variants of the thesis

VersionClaimStatus
Classiccomputable = TM-computableaccepted
Physicalno physical device computes morebelieved
Extended (strong)any reasonable model is poly-time equal to a TMdoubted: quantum computers

What this buys us

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.

Encodings & Enumeration MACHINES AS DATA

⟨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.

All strings can be listed

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']

Consequence: there are countably many TMs

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.

The Universal Turing Machine ONE MACHINE TO RUN THEM ALL

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

How a real U works

  • Tape 1: ⟨M⟩, the program. Tape 2: M's tape. Tape 3: M's current state.
  • Each step: scan tape 1 for the rule matching (state, symbol). Apply it on tapes 2 and 3.
  • U has a fixed, small number of states. Yet it runs machines with any number of states.

The stored-program idea

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.

Decidable, Recognizable, Co-Recognizable DEFINITIONS

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.

Examples

LanguageClass
ADFA, ACFG, primesdecidable
ATM = {⟨M,w⟩ : M accepts w}RE, not decidable
ETM = {⟨M⟩ : L(M) = ∅}co-RE, not RE
EQTM, TOTALTMneither

Theorem. L is decidable iff L is both recognizable and co-recognizable.

Proof

(⇒) 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. ∎

Enumerators

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.

Cantor's Diagonal Argument 1891

Theorem (Cantor). For any set S, there is no onto map S → P(S). In particular the subsets of ℕ are uncountable.

Proof

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 ..

Most Languages Are Undecidable COUNTING

Theorem. Some languages are not Turing-recognizable. In fact, almost all of them.

Proof

  1. Each TM has a finite encoding ⟨M⟩ ∈ Σ*. So the TMs are countable.
  2. Each TM recognizes one language. So recognizable languages are countable.
  3. Languages are subsets of Σ*, which is countably infinite. By Cantor, there are uncountably many.
  4. A countable set cannot cover an uncountable one. ∎

This proof is non-constructive. It says undecidable languages exist, but it names none. The next slides build a specific, useful one.

all languages: uncountable (2^ℵ₀) recognizable countable (ℵ₀)

Picked "at random", a language is undecidable with probability 1. The decidable ones are the rare exception.

SetSize
strings Σ*ℵ0
Turing machinesℵ0
languages P(Σ*)2ℵ0 (like the reals)

The Halting Problem UNDECIDABLE

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.

Recognizable

Run U on ⟨M, w⟩. Accept if it accepts. If M loops, so does U, which a recognizer is allowed to do.

Corollary

̅ATM is not recognizable. If it were, ATM would be both RE and co-RE, hence decidable.

Proof: diagonalization

  1. Suppose a decider H exists: H(⟨M,w⟩) accepts if M accepts w, else rejects.
  2. Build D. On input ⟨M⟩: run H(⟨M, ⟨M⟩⟩) and output the opposite.
  3. Now run D on its own code ⟨D⟩:

D(⟨D⟩) accepts  ⇔  H says D rejects ⟨D⟩  ⇔  D(⟨D⟩) rejects

Contradiction. So H does not exist. ∎

⟨M1⟩⟨M2⟩⟨M3⟩…⟨D⟩
M1accrejaccacc
M2accaccaccrej
M3rejrejrejrej
Drejrejacc?

A Python Paradox CODE

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'

What the code shows

  • make_D works for any halts function, however clever.
  • D asks about itself, then does the opposite.
  • So every candidate is wrong on at least one input: its own D.
  • We only run D() when it is predicted to loop. Then it returns at once, which proves the prediction wrong.

Common objections

  • "Use a timeout." Then halts answers "loops" for slow programs that do halt. It is a heuristic, not a decider.
  • "Real computers have finite memory." True: then halting is decidable, but it needs about 2bits of RAM steps.
  • "Just for weird self-referential code." No: via reductions, it infects ordinary questions too (next slides).

Reductions SPREADING UNDECIDABILITY

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

ABΣ*Σ* f: yes → yes f: no → no

Theorem. If A ≤m B:

  • B decidable ⇒ A decidable. So A undecidable ⇒ B undecidable.
  • B recognizable ⇒ A recognizable.

How to use it

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.

Example: ATM ≤m HALTTM

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.

Reduction Examples PROOFS

ETM: is L(M) empty?

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).

EQTM: same language?

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.

REGULARTM: is L(M) regular?

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.

Writing a Reduction Proof: A Recipe STEP BY STEP

Five steps

  1. Pick a known hard problem A, like ATM or HALTTM.
  2. Describe f: turn an instance of A into an instance of your new problem B.
  3. Yes maps to yes: if x ∈ A then f(x) ∈ B.
  4. No maps to no: if x ∉ A then f(x) ∉ B.
  5. f is computable: it only builds program text. It never runs M.

Worked: HALTε is undecidable

HALTε = { ⟨M⟩ : M halts on the blank tape }. Reduce from HALTTM.

  1. Given ⟨M, w⟩, build Mw: "erase the tape, write w, then run M."
  2. If M halts on w, then Mw halts on blank. Yes → yes.
  3. If M loops on w, so does Mw. No → no.
  4. Building Mw is text editing. So HALTTM ≤m HALTε. ∎

The same f, in Python

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')

Common mistakes

  • Wrong direction. Showing B ≤m ATM proves nothing about B. Always reduce from the known hard problem.
  • Running M inside f. Then f might loop, and it is not computable. f only writes code.
  • Checking one direction only. You need yes → yes and no → no.

Rice's Theorem 1953

Theorem (Rice). Let P be any non-trivial property of the language of a TM. Then {⟨M⟩ : L(M) has property P} is undecidable.

  • Of the language: if L(M1) = L(M2), both have P or neither does. It is about behavior, not code.
  • Non-trivial: some TM has P and some TM does not.

Proof

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 MDecidable?
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.

Post Correspondence Problem 1946

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.

Proof idea

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.

Busy Beaver: Definition RADÓ 1962

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.

Busy Beaver: Champions KNOWN VALUES

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.

nS(n) stepsΣ(n) onesProved
111trivial
264Radó 1962
3216Lin & Radó 1965
410713Brady 1983
547,176,8704,098bbchallenge, 2024 (Coq-verified)
6> 2 ↑↑↑ 5> 2 ↑↑↑ 5open

Why BB(6) may never be known

  • Some 6-state machines behave like Collatz-type problems. Deciding if they halt needs new mathematics.
  • Yedidia & Aaronson (2016) built a 7,918-state machine that halts iff ZFC set theory is inconsistent. Later work shrank it to 745 states. So ZFC cannot prove the value of Σ(745).

↑↑↑ is Knuth's arrow notation. 2↑↑5 is already a tower of five 2s, 265536.

Recursion Theorem & Quines SELF-REFERENCE

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.

Proof idea: the two-part trick

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.

A two-line proof of halting

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.

A Python quine: prints its own source

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.

A program that knows its own length

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).

Undecidability in Practice REAL WORLD

Simple code, unknown halting

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.

ToolUndecidable coreHow it copes
Compiler optimizeris this code dead? are these equal?conservative: only safe rewrites
Type checkerwill this crash?rejects some correct programs
Static analyzer (Infer, CodeQL)null deref, leaksfalse positives or misses
Antivirusis this malicious?signatures + heuristics
Termination checker (Coq, Agda)does it halt?accepts only structural recursion
Test suitecorrect on all inputs?checks some inputs

Escape routes

  • Restrict the language: total languages, SQL without recursion, regex.
  • Accept approximation: sound (no misses) or complete (no false alarms), not both.
  • Ask a human: annotations, loop invariants, proof assistants.

The Gödel Connection INCOMPLETENESS

Gödel's first incompleteness theorem (1931). Any consistent, effectively axiomatized theory that includes basic arithmetic has true statements it cannot prove.

A proof from the halting problem

  1. Let T be such a theory, and assume it proves only true statements. Its theorems are RE: a TM can list every valid proof.
  2. "M does not halt on w" can be written as a statement of arithmetic.
  3. Suppose T proved every true statement of that form. Then run two searches in parallel: simulate M on w, and search for a proof that it loops.
  4. One search must finish. That decides halting, which is impossible.
  5. So some true "M does not halt" has no proof in T. ∎
Gödel (1931)Turing (1936)
formulas get numbersmachines get encodings
"this sentence is unprovable""D does the opposite of H on D"
diagonal lemmarecursion theorem
true but unprovablewell-defined but uncomputable
second theorem: T cannot prove its own consistencyΣ(n) unprovable for large n

What it does not say

  • Not "math is inconsistent" or "anything goes".
  • Not about a specific famous conjecture. Most are probably provable.
  • Adding the unprovable sentence as an axiom just creates a new one.

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.

Map of the Classes BIG PICTURE

all languages RE co-RE decidable CFL regular A_TMHALTPCP E_TM¬A_TM¬HALT A_CFG, primes,SAT, CYK EQ_TM, TOTAL
ClassMachine viewLogic view
Decidable (R)always-halting TMΔ1
REhalts on yesΣ1: ∃ proof
co-REhalts on noΠ1: ∀ checks
Beyondneeds a halting oracleΣ2, Π2, …

Facts to remember

  • R = RE ∩ co-RE.
  • R is closed under complement, ∪, ∩. RE is closed under ∪, ∩, but not complement.
  • ATM is RE-complete: every RE language reduces to it.
  • TOTALTM ("halts on every input") is Π2-complete: even a halting oracle cannot decide it.

This ladder is the arithmetical hierarchy (Kleene, Mostowski). Each level adds one quantifier, and one level of oracle.

Check Yourself PRACTICE

Try each one before you open the answer.

1. Is "M halts on the blank tape within 1000 steps" decidable?

Yes. Simulate 1000 steps and answer. The step bound removes the danger of looping.

2. { ⟨M⟩ : M accepts "hello" }: recognizable? decidable?

Recognizable: run M on "hello" and accept if it does. Not decidable: it is a non-trivial property of L(M), so Rice applies.

3. A ≤m B and B is undecidable. What do we learn about A?

Nothing. Any decidable problem also reduces to B. Hardness flows from A to B, never back.

4. Is the complement of HALTTM recognizable?

No. HALTTM is RE. If its complement were RE too, it would be decidable, and it is not.

5. Does Rice say "M has exactly 7 states" is undecidable?

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.

Mistakes students make

MistakeFix
"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.
6. Why is ETM co-RE and not RE?

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.

Summary RECAP

Key takeaways

  • A TM is a finite control with an unbounded read/write tape. It can accept, reject, or loop.
  • Tapes, nondeterminism, and alphabets change speed, not power.
  • Church–Turing: TM = algorithm. What no TM can do, no program can.
  • Machines are data. One universal machine runs them all.
  • Diagonalization shows ATM is undecidable. Reductions spread it.
  • Rice: every non-trivial property of program behavior is undecidable.
  • Busy beaver outgrows every computable function. Self-reference gives quines and Gödel.
ProblemStatus
ADFA, ACFG, ECFGdecidable
ATM, HALT, PCPRE, undecidable
ETM, ̅ATMco-RE, undecidable
EQTM, TOTAL, Σ(n)neither

Further reading

  • Sipser, Introduction to the Theory of Computation, ch. 3–6.
  • Turing, "On Computable Numbers" (1936). Read it with Petzold's The Annotated Turing.
  • Aaronson, "The Busy Beaver Frontier" (2020).
  • bbchallenge.org: the collaborative proof of BB(5).

Previous deck: context-free grammars and pushdown automata. Next: complexity, P vs NP.

Glossary QUICK REFERENCE

TermMeaning
Turing machineFinite control plus an unbounded read/write tape.
ConfigurationA snapshot: state, tape contents, head position.
DeciderA 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-REThe complement is recognizable.
Church–Turing thesisEvery algorithm can be run by a TM.
Universal TM UTakes ⟨M, w⟩ and simulates M on w.
DovetailingRun many computations a few steps at a time, taking turns.
TermMeaning
DiagonalizationBuild an object that differs from item i at spot i.
ATM / HALTDoes M accept / halt on w? RE, not decidable.
Mapping reduction ≤mComputable f with x ∈ A ⇔ f(x) ∈ B.
Rice's theoremEvery non-trivial property of L(M) is undecidable.
PCPDomino matching puzzle. Undecidable, no machines needed.
Busy beaver S(n)Most steps of any halting n-state machine.
QuineA program that prints its own source.
Recursion theoremA program can get its own code and use it.
RE-completeIn RE, and every RE problem reduces to it.