Cobham's characterization of FP — proof internals #
The assembly of CobhamFP = FP (Complexitylib.Classes.P.Cobham). Not meant
for human review of the mathematics — the surface file carries the auditable
statements; the type checker carries this.
The machines are in sibling modules (Internal.BlockScan, Internal.Cat,
Internal.ConsBit, Internal.Reorder, Internal.MulLen, Internal.Iterate),
the algebra toolkit in Internal.Algebra, and the interpreter of the
completeness direction in Internal.Encoding, Internal.StepAlgebra,
Internal.Extract and Internal.Simulate. What remains here is the soundness
induction and the boundedRec loop.
Contents #
- the six constructor cases
fpn_empty,fpn_proj,fpn_bit,fpn_smash,fpn_comp,fpn_boundedRec, and the inductioncobham_imp_FPnover them; - the
FPclosure lemmas they need:pairFn_mem_FP,appendFn_mem_FP,selectHeadFn_mem_FP(branching on a bit, viaComplexity.headFlag),takeLenFn_mem_FP,assembleVec_mem_FP; - the
boundedRecloop:recNotation_eq_foldr,recFold_eq_recNotation,recFoldClamp_eq_recFold,loopStep_iterateandrecFoldClamp_mem_FP, on top ofiterate_mem_FP; - the rulers
exists_rulerandexists_exact_rulerthat carry the loop's width clamp as data.
The canonical tuple encoding is in the algebra #
The nested tuple encoding is a Cobham function at every fixed arity.
Soundness: Cobham f → FPn f, constructor by constructor #
proj case: extracting the i-th component of an encoded vector is FP.
The extraction is sndBlock after i-fold fstBlock: peel i leading blocks to
reach the encoding of components i, i+1, …, then read its head with sndBlock.
Proved here by induction on the arity; each atomic step is FP
(fstBlock_mem_FP, sndBlock_mem_FP) and FP is closed under composition
(mem_FP_comp), so only those two machine lemmas remain open.
Pairing two FP functions of the same input is FP.
Built without a two-output machine: mem_FP_pairWithInput gives the nested triple
z ↦ pair (a z) (pair (b z) z) (pairing each computed value against the raw
input, then again), and the self-contained reorder drops the trailing input
copy to leave pair (a z) (b z). This is what lets fpn_comp avoid a bespoke
tuple-assembly machine.
Emitting |a z| · |b z| copies of false is FP when a, b are. This
zero-filled ruler is an internal length-arithmetic helper, not Cobham's public
all-one smash. It is built as the self-contained mulUnpair (see
Complexitylib.Classes.P.Cobham.Internal.MulLen) after pairFn a b.
Select x or y according to the leading bit of s; nothing when s is
empty. This is the only shape of value-dependent branching the algebra's loop
needs, and Complexity.headFlag is what makes it expressible.
Equations
Instances For
Selection is masking. Exactly one of the two masks is full width, so the concatenation returns exactly one branch.
smash case: the smash function is FPn. On encodeVec ![x, y] the two
components are sndBlock and sndBlock ∘ fstBlock; smash x y is
|x| · |y| copies of true, so the witness first computes a zero-filled ruler
with mulLenFn_mem_FP and then applies unaryLength_mem_FP.
Assembling an encoded vector out of FP component functions of a common input
is FP. Proved by induction on the arity: the empty vector is the constant [],
and the successor step is one pairFn_mem_FP.
comp case: FPn is closed under Cobham composition. On encodeVec v, each
inner gs i is computed by its FP witness G i, the results are assembled into
encodeVec (fun i => gs i v) (assembleVec_mem_FP), and the outer f's witness
is applied; FP is closed under composition. Rests only on pairFn_mem_FP.
One step of recursion on notation viewed as a fold operation: extend the
running suffix p.1 by the bit b and update the running recursive value p.2 by
the bit-selected step function. Folding this over a string with List.foldr
reproduces recNotation (see recNotation_eq_foldr); it is the per-iteration
body a loop machine runs.
Equations
Instances For
The first component of the recursion-on-notation fold accumulates exactly the bits processed so far — i.e. it rebuilds the input string.
Recursion on notation is a fold. recNotation g h₀ h₁ s w is the second
component of folding recNotationStep over s from the empty suffix and base
value g w. This reduces the boundedRec case to iterating a single step
function over the bits of s — exactly what a loop machine computes — and is the
target identity for fpn_boundedRec.
The boundedRec loop #
The boundedRec case runs the recursion as a loop on encoded arguments:
recFold A B e W s threads a running suffix t of s and the running
accumulator a through the argument encoding pair (pair W a) t, which is
exactly encodeVec (Fin.cons t (Fin.cons a w)) when W = encodeVec w.
A machine cannot run recFold as written: nothing stops the accumulator from
doubling in length at every iteration, so intermediate values would need
exponential space. recFoldClamp truncates every intermediate value to a
prescribed width, which makes the loop unconditionally polynomial-time
(recFoldClamp_mem_FP); Cobham's limited-recursion side condition is then
exactly what shows the truncation never fires (recFoldClamp_eq_recFold).
The recursion-on-notation loop on encoded arguments: fold the bit-selected
step functions A (bit false) and B (bit true) over s, threading the
running suffix and accumulator through the argument encoding.
Equations
- Complexity.Cobham.recFold A B e W [] = e
- Complexity.Cobham.recFold A B e W (b :: t) = (bif b then B else A) (Complexity.pair (Complexity.pair W (Complexity.Cobham.recFold A B e W t)) t)
Instances For
recFold with every intermediate value truncated to bound bits. This is
the loop a machine can actually run: each iteration's state is length-bounded,
so the whole loop takes polynomial time.
Equations
- Complexity.Cobham.recFoldClamp A B bound e W [] = List.take bound e
- Complexity.Cobham.recFoldClamp A B bound e W (b :: t) = List.take bound ((bif b then B else A) (Complexity.pair (Complexity.pair W (Complexity.Cobham.recFoldClamp A B bound e W t)) t))
Instances For
Rulers. For every constant c and exponent d there is an FP function
whose output is at least c · (|z| + 1) ^ d bits long. Rulers let the loop of the
boundedRec case carry its width clamp as data — truncating to a string costs
linear time, whereas truncating to a computed number would not.
Every polynomial bound has an FP ruler.
Exact rulers #
exists_ruler builds an FP string at least p.eval |z| bits long, which is
all a clamp needs. The loop needs an exact one: the width it truncates to is the
ruler's length, and that has to be the bound the statement names. Exactness comes
from Complexity.unaryLength_mem_FP together with the two exact length
arithmetic operations now available — mulLenFn_mem_FP multiplies lengths and
appendFn_mem_FP adds them.
Constants of any width are FP.
A ruler of length exactly p.eval |z|.
The loop as an iteration #
recFoldClamp is an iteration of a single FP step function on a packed
state. Writing s for sndBlock z, the state after m iterations is
pair (pair R (pair W s)) (pair (s.drop (|s| - m)) (recFoldClamp … (s.drop (|s| - m))))
so the answer is the accumulator after |s| iterations. Every ingredient of the
step is now FP: the suffix grows by Complexity.takeLen against a ruler one
longer, read off s.reverse; the branch on the new leading bit is selectHead;
and the clamp is takeLen against R.
The loop's step function #
Complexity.iterate_input_mem_FP supplies a machine that applies an FP
function once per bit of its own input, starting from pair [] x. The state
below is pair (pair C v) x: a counter C, the running value v, and the
machine's input x kept verbatim. Keeping x is what makes the whole
construction work: the ruler and the width stay readable at every step, and
truncating the new state to |x| bounds the state length globally — the
machine's contract needs a bound that holds for every input, not just for the
well-formed ones.
A flag whose leading bit is true exactly when s is empty — the one test
Complexity.selectHead cannot make directly.
Equations
Instances For
The value of the next iteration: the initial value on the first step, then
F of the current value until the counter saturates.
Equations
- One or more equations did not get rendered due to their size.
Instances For
The loop's trajectory. With the counter growing one mark per iteration
and the state always fitting in the input, the i+1-st state is exactly the
counter (Krev.take (i+1)).reverse beside the value iterVal … i.
The value sequence is the iterate. While the counter has marks left the
step applies F; once it saturates the value stops changing. The clamp is a
no-op because every intermediate value fits in W.
FP is closed under bounded iteration — the one machine-level fact the
soundness direction needs.
Construction. The machine is assembled in
Complexitylib.Classes.P.Cobham.Internal.Iterate out of the phase contracts of
Complexitylib.Classes.P.Cobham.Internal.IterateLayout; iterate_input_mem_FP is its
interface. Three details are worth recording, because three earlier plans died
on them.
Why resetting scratch is the crux. F's machine M comes from an
existential (F ∈ FP), so nothing is known about the shape it leaves its
scratch tapes in. Re-running it needs those tapes genuinely blank, but a
content-driven eraser (TM.blankWorkTM scans right to the first blank)
under-wipes whenever M left a gap — an isolated blank cell with more content
beyond it. TM.wipeStepTM therefore writes blank unconditionally, and
Complexity.resetTapesTM drives it a fixed number of times off a fuel register
that is unrelated to the wiped tapes' content. TM.reachesIn_work_cells_far
supplies the bound that makes the fixed count sufficient: a t-step run cannot
have touched anything past head + t. Complexity.iterTail is the resulting
five-phase cleanup, shared by the loop body and the setup; its first two phases
are not bookkeeping either, since δ_right_of_start only forces a head
reading ▷ to move right, so an arbitrary witness machine may legitimately
halt with a head at cell 0.
Why the state carries the machine's own input. TM.ComputesInTime quantifies
over all inputs, so the loop's contract has to survive malformed ones: the
state is pair (pair C v) x with the machine's input x kept verbatim, and
every new state is truncated to |x| (iterStep). That makes
iterStep_iterate_length_le — a state-length bound holding for every input,
not just the well-formed ones — available for free, and keeps the ruler and the
width readable at every step. On the intended trajectory the truncation is a
no-op (iterStep_iterate).
How the counter avoids a second fuel value. The loop runs |x| + 1 times, one
per bit of the machine's own input (TM.inputLenRegTM), which is more
iterations than needed; the surplus is absorbed by a counter that grows one mark
of Krev = 0^(m+1) 1 per step, whose leading bit turns true exactly when the
m real applications are done (counter_head_false, counter_head_true). So
iterVal is F iterated min i m times, and over-iteration is harmless
(iterVal_eq_iterate). The wipe width is a different register, p.eval |x|,
computed by TM.polyEvalTM — the state is longer than the input, so |x|
alone cannot pay for the reset.
Time. Each iteration costs iterStep's own polynomial bound at width
(width z).length — which is why hbound is a hypothesis — plus the linear
copies and the wipe, and there are |x| + 1 of them, so the total is polynomial
(polyBnd_iterBound).
The loop of the boundedRec case. recFoldClamp is loopStep iterated
once per bit of sndBlock z (loopStep_iterate), started from the packed state
pair (pair R (pair W s)) (pair [] (e.take |R|)) — with R an exact ruler for
the clamp (exists_exact_ruler) — and read off with two sndBlocks.
On encoded arguments the loop computes recursion on notation: recFold over
the FP witnesses of g, h₀, h₁ reproduces recNotation.
Every FP function has polynomially bounded output length: a time bound is
also an output-length bound (TM.ComputesInTime.output_length_le).
boundedRec case: FPn is closed under limited recursion on notation.
By recFold_eq_recNotation the value is the encoded-argument loop recFold run
over the bits of v 0. Cobham's limited-recursion side condition hbound caps
every intermediate accumulator by |j (…)|, which is polynomial in |encodeVec v|
(output_length_poly_of_mem_FP), so the clamped loop recFoldClamp — which a
machine can run in polynomial time (recFoldClamp_mem_FP) — never truncates and
therefore agrees with recFold.
Arity-one specialization: from the multi-arity soundness induction, the
unary fragment CobhamFP lands in FP.
Completeness: FP ⊆ CobhamFP #
Completeness direction. Every polynomial-time function belongs to Cobham's algebra.
Construction: a polynomial-time Turing machine is simulated inside the algebra.
- A whole configuration — state, input tape, output tape, work tapes and every
head position — is one bitstring of equal-width blocks, each tape split at its
head so that a head move is a two-bit shift (
Cobham.cfgCode). - The one-step transition is a finite case split on (state, symbols read), which
is
Cobham.tableFnagainst the finitely many constant key patterns, with each branch built fromtakeFn/dropFn/appendFn/padFn(Cobham.stepFn). At the halting state the branch is the identity, so the encoding is a fixed point once the machine stops. - The step is iterated once per bit of a clock string built from
smash(Cobham.exists_pow_clock), long enough by the polynomial normal formmem_FP_iff_computesInTime_polynomial. - A second iteration walks the output head back to cell
0(Cobham.rewindFn), after which that tape's right half-block is the whole tape in order, and the output is read off it by twoComplexity.cellBitsrecursions and oneComplexity.runTrue(Cobham.simFn). The length bounds throughout are polynomial, so everyboundedRecside condition is met.