Skip to content
The Lumenary

Math laboratory

Simulating Turing machines: a binary counter and a unary adder

Simulates a one-state binary incrementer on every input below 256 and a three-state unary adder on all pairs m, n <= 12, checking every output exactly and recording step counts. Exhaustive for those ranges only: it is evidence of correctness, not a proof for all inputs.

Back to the labExact computationExhaustive finite check
Simulating Turing machines: a binary counter and a unary adderPythonExact computationExhaustive finite checkComputationally tested
"""Two small Turing machines, simulated and checked on many inputs.

A Turing machine is a finite table: in state q reading symbol s, write a
symbol, move one cell left or right, and go to a new state (or halt). We
simulate two machines from the lesson:

* a one-state binary incrementer, checked on every input n < 256, with the
  number of steps it takes (the amortised cost of counting is below 2 steps);
* a three-state unary adder turning 1^m 0 1^n into 1^(m+n), checked for all
  m, n <= 12.

The checks are exhaustive for the stated ranges only. They are evidence that
the machines are correct, not a proof for all inputs; the proofs are short
inductions given in the lesson's exercises.
"""

import textwrap

HALT = None
LEFT, RIGHT = -1, 1

#: Binary incrementer. Head starts on the least significant bit.
INCREMENT = {
    ("carry", 1): (0, LEFT, "carry"),
    ("carry", 0): (1, LEFT, HALT),
}

#: Unary adder: input 1^m 0 1^n with the head on the leftmost cell.
ADD = {
    ("A", 1): (1, RIGHT, "A"),
    ("A", 0): (1, RIGHT, "B"),
    ("B", 1): (1, RIGHT, "B"),
    ("B", 0): (0, LEFT, "C"),
    ("C", 1): (0, LEFT, HALT),
    ("C", 0): (0, LEFT, HALT),
}

def run(machine, tape, head, state, max_steps=10_000):
    """Run until the machine halts. Return (tape dict, head, steps).

    ``tape`` maps positions to symbols (missing cells are blank 0). Raises
    RuntimeError if the machine has not halted after ``max_steps`` steps:
    a step bound can never certify that a machine runs forever.
    """
    tape = dict(tape)
    for steps in range(1, max_steps + 1):
        write, move, state = machine[(state, tape.get(head, 0))]
        tape[head] = write
        head += move
        if state is HALT:
            return tape, head, steps
    raise RuntimeError("no halt within the step bound")

def binary_tape(n):
    """Binary digits of n, least significant bit at position 0, others left."""
    bits = bin(n)[2:]
    return {-i: int(b) for i, b in enumerate(reversed(bits))}

def read_binary(tape):
    """Read the number written on the tape (the most significant 1 first)."""
    cells = [p for p, s in tape.items() if s == 1]
    if not cells:
        return 0
    return sum(2 ** (-p) for p in cells if p <= 0)

def increment(n):
    """Run the incrementer on n; return (output, steps)."""
    tape, _, steps = run(INCREMENT, binary_tape(n), 0, "carry")
    return read_binary(tape), steps

def unary_tape(m, n):
    """The input 1^m 0 1^n written from position 0 to the right."""
    cells = [1] * m + [0] + [1] * n
    return {i: s for i, s in enumerate(cells)}

def add(m, n):
    """Run the unary adder on (m, n); return (number of 1s left, steps)."""
    tape, _, steps = run(ADD, unary_tape(m, n), 0, "A")
    return sum(tape.values()), steps

def trailing_ones(n):
    """Number of 1s at the end of the binary expansion of n."""
    count = 0
    while n & 1:
        n >>= 1
        count += 1
    return count

def main():
    print("1. Binary incrementer (1 state): carry 1s to the left, then halt.")
    rows = [(n, *increment(n)) for n in range(256)]
    bad = [n for n, out, _ in rows if out != n + 1]
    steps_ok = all(s == trailing_ones(n) + 1 for n, _, s in rows)
    print(f"   inputs 0..255 checked: {len(rows)}, wrong outputs: {len(bad)}")
    print(f"   steps = (trailing 1s) + 1 for every input: {steps_ok}")
    for n in (11, 127, 128):
        out, s = increment(n)
        unit = "step" if s == 1 else "steps"
        print(f"   {n:>3} = {n:08b} -> {out:>3} = {out:08b} in {s} {unit}")
    total = sum(s for _, _, s in rows)
    print(f"   average steps over 0..255: {total}/256 = {total / 256:.4f}")
    print()
    print("2. Unary adder (3 states): 1^m 0 1^n -> 1^(m+n).")
    pairs = [(m, n) for m in range(13) for n in range(13)]
    wrong = [(m, n) for m, n in pairs if add(m, n)[0] != m + n]
    print(f"   pairs with m, n <= 12 checked: {len(pairs)}, wrong: {len(wrong)}")
    print(f"   steps for (3, 4): {add(3, 4)[1]}; for (12, 12): {add(12, 12)[1]}")
    print("   steps = m + n + 3 on every pair: "
          f"{all(add(m, n)[1] == m + n + 3 for m, n in pairs)}")
    print()
    print(textwrap.fill(
        "Epistemic status: exact simulation, exhaustive for the 256 inputs and "
        "169 pairs above, so it is evidence of correctness and of the step "
        "formulas, not a proof for all inputs (that needs an induction on the "
        "length of the input). Every run here halted; a run that had not halted "
        "would have proved nothing, since no step bound separates slow machines "
        "from machines that never halt.",
        width=78,
    ))

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in