Encoding machine configurations as bitstrings — proof internals #
The completeness direction of Cobham's theorem simulates a polynomial-time
machine inside the function algebra, so a configuration has to become a single
bitstring. This module fixes that encoding and proves the arithmetic facts about
it; the algebra-side operations that act on it live in
Complexitylib.Classes.P.Cobham.Internal.StepAlgebra.
The two design choices #
Two bits per symbol, with blank = 00. Fixed-width blocks are padded with
zeros (Complexity.padTo), so making blank the all-zero code means padding a
tape block with zeros is extending it with blanks — the padding needs no
special treatment anywhere.
Tapes split at the head. A tape is stored as its cells to the left of the
head, nearest first, and its cells from the head rightwards. Then a head move is
transferring one symbol between the two sides, i.e. a take/drop/append of
two bits, rather than arithmetic on a position index. Reading is the first two
bits of the right part.
Cell 0 is the only ▷ (the writable alphabet Γw excludes it), so "the head
is at cell 0" is exactly "the read symbol is ▷" — and in that case
TM.δ_right_of_start forces a move right. The left part is therefore never
consulted when it is empty, which is why it needs no emptiness test.
Main definitions #
Complexity.Cobham.symCode— two-bit code forΓComplexity.Cobham.cellsCode— a window of cells as a bitstringComplexity.Cobham.leftCode,Complexity.Cobham.rightCode— a tape split at its head
Main results #
The six lemmas that make the split representation simulate Tape.writeAndMove,
each expressing one head move as two bits crossing the split:
leftCode_write_stay,rightCode_write_stayleftCode_write_right,rightCode_write_rightleftCode_write_left,rightCode_write_left
Every right-hand side is built from take 2, drop 2, ++ and the constant
symCode s — all of which the algebra has (Cobham.takeFn, Cobham.dropFn,
Cobham.appendFn, Cobham.const).
The symbol code #
Two-bit code for the tape alphabet. Blank is 00, so zero-padding a block is
blank-padding it.
Equations
Instances For
Decode the leading two bits of a string as a tape symbol; anything shorter than two bits reads as blank.
Equations
- Complexity.Cobham.symDecode (false :: false :: tail) = Complexity.Γ.blank
- Complexity.Cobham.symDecode (false :: true :: tail) = Complexity.Γ.start
- Complexity.Cobham.symDecode (true :: false :: tail) = Complexity.Γ.zero
- Complexity.Cobham.symDecode (true :: true :: tail) = Complexity.Γ.one
- Complexity.Cobham.symDecode x✝ = Complexity.Γ.blank
Instances For
Zero padding decodes as blank: the reason symCode Γ.blank = [0,0].
The control state #
The state is stored one-hot: |Q| bits with a single 1. Fixed width and
injective, and — the point — every state's code is a constant for a fixed
machine, so the transition table is finitely many Cobham.matchPrefixFn tests
against constants (Cobham.tableFn). Binary would need arithmetic; one-hot needs
none.
One-hot code for a control state: one bit per element of Q, set exactly at
the state itself.
Noncomputable only because Finset.toList picks an enumeration order; the code
appears solely in specifications, never in a machine that must run.
Equations
- Complexity.Cobham.stateCode q = List.map (fun (p : Q) => decide (p = q)) Finset.univ.toList
Instances For
Distinct states get distinct codes.
Windows of cells #
The w cells of t starting at cell i, two bits each.
Equations
- Complexity.Cobham.cellsCode t i w = List.flatMap (fun (j : ℕ) => Complexity.Cobham.symCode (t.cells (i + j))) (List.range w)
Instances For
Tapes split at the head #
The cells n-1, n-2, …, 0 of t, nearest first.
Equations
Instances For
The cells strictly left of the head, nearest first.
Equations
Instances For
The cells from the head rightwards, out to cell W.
The width is W + 1 - head, complementary to leftCode's head, so the two
parts always account for exactly the cells 0 … W: their total width is the
constant 2 · (W + 1) and a head move just shifts two bits across the split.
Equations
- Complexity.Cobham.rightCode t W = Complexity.Cobham.cellsCode t t.head (W + 1 - t.head)
Instances For
Congruence #
Both halves read only the cells in their own window, so an update outside that window is invisible to them. These are the lemmas that let a single-cell write be localized.
Writing and moving #
Tape.write never touches cell 0 (the model makes writing there a no-op), and
Γw cannot produce ▷, so cell 0 is permanently the unique ▷. Hence "the
head is at 0" is exactly "the read symbol is ▷", and TM.δ_right_of_start
then forces a right move — which is why the left half is never consulted while
empty.
Writing at the head sets exactly that cell — except at cell 0, where the
model makes the write a no-op, so callers must establish that the modeled symbol
already agrees with what is there. A raw TM transition writes only Γw, which
excludes ▷; the encoded simulator later supplies the corrected symbol through
correctWrite.
Moving right: the written symbol crosses over to the left half. This is
the one direction a head at cell 0 can take, so it is stated with the weaker
hypothesis that the write agrees with cell 0 when the head is there.
Whole configurations #
Every field occupies a block of the same width, so field i is recovered by
Cobham.blockFn … i — the algebra never needs a self-delimiting decoder. A tape
costs two blocks (its two halves); the state costs one, padded to the same
width.
The block width used throughout: wide enough for either half of a tape whose
head stays within 0 … W.
Equations
- Complexity.Cobham.blockWidth W = 2 * (W + 1)
Instances For
The canonical ruler of one block's width.
Equations
Instances For
Both halves of a tape occupy one block each.
A tape as a bitstring: its two half-blocks concatenated.
Equations
Instances For
The work tapes, one after another.
Equations
- Complexity.Cobham.worksCode W work = List.flatMap (fun (i : Fin k) => Complexity.Cobham.tapeCode W (work i)) (List.finRange k)
Instances For
The window invariant #
A head moves at most one cell per step and starts at cell 0, so after t steps
every head is within 0 … t. Taking the window W to be the machine's time
bound therefore discharges the head ≤ W side condition of every encoding lemma
— the simulated machine can never reach outside the encoded window.
Reading a symbol out of an encoded tape. The head symbol is the first two
bits of the padded right half-block — one takeFn in the algebra.
One tape's step #
The encoded step on a tape's two half-blocks. Every right-hand side is
take 2 / drop 2 / ++ / a constant and a re-pad, so the algebra realizes it
with Cobham.takeFn, Cobham.dropFn, Cobham.appendFn, Cobham.const and
Cobham.padFn — and within one branch of Cobham.tableFn the symbol s and the
direction d are constants.
The two half-blocks of a tape after writing s and moving d.
Equations
- Complexity.Cobham.tapeStepBlocks R s Complexity.Dir3.stay L Rt = (L, Complexity.padTo R (Complexity.Cobham.symCode s ++ List.drop 2 Rt))
- Complexity.Cobham.tapeStepBlocks R s Complexity.Dir3.right L Rt = (Complexity.padTo R (Complexity.Cobham.symCode s ++ L), Complexity.padTo R (List.drop 2 Rt))
- Complexity.Cobham.tapeStepBlocks R s Complexity.Dir3.left L Rt = (Complexity.padTo R (List.drop 2 L), Complexity.padTo R (List.take 2 L ++ Complexity.Cobham.symCode s ++ List.drop 2 Rt))
Instances For
The encoded step simulates Tape.writeAndMove on both half-blocks.
The hypotheses are exactly what the corrected encoded action supplies. hs: at
cell 0 the write is a no-op, so correctWrite replaces the raw Γw symbol by
the existing ▷. hne: TM.δ_right_of_start ensures that a head at cell 0
can only move right, so the stay and left cases never arise there.
All the tapes at once #
TM.step writes and moves on every tape independently, so the encoded step is
the same operation applied tapewise. Treating the tapes as one list — input,
output, then work tapes, the order the encoding uses — makes that a
List.zipWith against the transition's per-tape actions, with no positional
index arithmetic.
The transition key #
The transition function is indexed by the current state together with the symbol
under every head. Packing those into one string turns the whole finite case
analysis into Cobham.tableFn: each (state, symbols) combination is a constant
pattern, and there are finitely many of them for a fixed machine.
The state and the symbols under every head, in tape order.
Equations
Instances For
The blocks of a list of tapes: two per tape.
Equations
Instances For
The tapes after one step, given each tape's write and move.
Equations
- Complexity.Cobham.tapesStep acts ts = List.zipWith (fun (a : Complexity.Γ × Complexity.Dir3) (t : Complexity.Tape) => (t.write a.1).move a.2) acts ts
Instances For
The write a transition really performs: at cell 0 the model makes the
write a no-op, and this records that. Under Tape.StartInvariant the test is on
the read symbol, which the transition table already branches on — so the
correction costs the algebra nothing, it just picks a different constant in the
▷ branch.
Equations
Instances For
The corrected write on a tape — a function of its read symbol alone, which is what puts it inside the transition key.
Equations
Instances For
Correcting the write does not change what the write does.
Under the invariant, the corrected write agrees with cell 0 when the head
is there — the hypothesis tapeStepBlocks_eq needs.
The per-tape (write, move) actions a transition prescribes, in encoding
order. The input tape's "write" is the symbol it just read, which by
write_read_self leaves it unchanged — so the read-only input tape fits the
uniform tapewise step with no special case.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The transition key determines the step #
Everything the successor configuration depends on — the new state and every
tape's write and direction — is a function of the state together with the symbol
under each head. That is exactly what a Cobham.tableFn entry can be indexed by,
and it is why any entry matching a configuration's key carries the right
branch.
A transition key's pattern string: the state's one-hot code followed by the
symbol under each head. Constant for each key, so it is what a
Cobham.tableFn entry matches against.
Equations
Instances For
Distinct keys get distinct patterns. Together with the fact that all patterns have the same length, this is what makes at most one table entry match a given key.
A configuration's key is its key's pattern. So the table entry indexed by
(state, reads) is the one that matches.
TM.step is the tapewise action. Every tape writes and moves according
to stepActs, so the whole configuration's tapes step uniformly.
One tape's blocks after a step. Immediate from tapeStepBlocks_eq; this
is the form that lifts tapewise across a whole configuration.
The tapewise step acts blockwise on the encoding.
A whole configuration as a list of equal-width blocks: the one-hot state padded to a block, then the input tape, the output tape, and the work tapes, each as two half-blocks.
Equations
Instances For
A whole configuration as a bitstring.
Equations
Instances For
The encoded configuration steps blockwise. Composing cfgTapes_step
(TM.step is the tapewise action) with tapesBlocks_tapesStep (that action is
blockwise on the encoding): the successor's blocks are the new state block
followed by the old blocks transformed two at a time by tapeStepBlocks.
The Forall₂ hypothesis pairs each tape with its own action, which is what a run
supplies: δ_right_of_start constrains a tape at cell 0 only through its own
transition entry.
The step's side conditions hold in any run. The write-agreement at cell
0 is correctWrite_at_zero, and "a head at cell 0 can only move right" is
exactly TM.δ_right_of_start read through the invariant: at cell 0 the tape
reads ▷, which is the hypothesis that rule fires on.
Tape j lives in blocks 2j and 2j+1 of the tape-block list. Combined
with the state block at the front of cfgBlocks, tape j of a configuration
occupies blocks 2j+1 and 2j+2 — which is how Cobham.blockFn addresses
them.
Field accessors #
The first three blocks — the state and the input tape's two halves — read out
directly. Each is one Cobham.blockFn on the algebra side.
Block 0 holds the state.
Unpadding block 0 recovers the one-hot state code, which the transition
table then matches against its finitely many constants.
Block 1 is the input tape's left half.
Block 2 is the input tape's right half — the one the read symbol comes
from.