Skip to content

While loop - #184

Draft
crei wants to merge 24 commits into
origin_mainfrom
while_loop
Draft

crei wants to merge 24 commits into
origin_mainfrom
while_loop

Conversation

@crei

@crei crei commented Sep 5, 2026

Copy link
Copy Markdown
Owner

No description provided.

mathlib-nightly-testing Bot and others added 24 commits August 31, 2026 20:30
Bump `mathlib` dependency to
[e06eff5](leanprover-community/mathlib4@e06eff5):
chore(Data): move some files to Basic (#43176) (2026-08-31)
Previously at:
[0fa18d4](leanprover-community/mathlib4@0fa18d4):
refactor: change the default value of `Measure.map` to a Dirac mass
(#42322) (2026-08-26)

Closes leanprover#847

Failure log from the validation run:
[download](https://github.com/leanprover-community/downstream-reports/actions/runs/33406502145/artifacts/9763801315)
_(link expires after 1 year)_

---

This PR bumps `mathlib` to an identified incompatible (first-known-bad)
commit (`e06eff5`) so you can reproduce and fix the incompatibility
locally by checking out this branch.

_Opened automatically by
[downstream-reports/track-incompatibility](https://github.com/leanprover-community/downstream-reports)
via [this workflow
run](https://github.com/leanprover/cslib/actions/runs/33434556067)._

---------

Co-authored-by: mathlib-nightly-testing[bot] <mathlib-nightly-testing[bot]@users.noreply.github.com>
Co-authored-by: Chris Henson <chrishenson.net@gmail.com>
Drop an `obtain` that the following `grind` does not need, as flagged by
[the weekly `mergeWithGrind`
report](https://leanprover.zulipchat.com/#narrow/channel/513188-CSLib/topic/Weekly.20linting.20log/near/620201866).
…eanprover#851)

Golf a heavy proof that was recently flagged as such (in leanprover#850) by
splitting `all_goals` into bullets and doing the work.
Restructure the complexity definitions and add the scaffolding for the
loop combinators of multi-tape Turing machines.

Deterministic.lean:
* rework the hierarchy of computability definitions into three layers:
  `ComputesFunInTimeAndSpace` (behaviour of a given machine, generic
  alphabet), `ComputableInTimeAndSpace` (existence of a finite machine
  over `Bool`) and `ComputableInTimeAndSpaceOfLength` (bounds depending
  only on the input length), with all bounds depending on the input
  rather than on its length
* add `Function.Embedding.listMap`, the `mono` lemmas and
  `ComputableInTimeAndSpace.congr`, which reads a computation as a
  computation of a different function at different encodings

Combinators:
* `Loop`: the loop with a fused body `α → Option α`, its partial
  function `loopFunction` and the statement of its complexity
* `While`: the loop with an explicit condition, characterised by
  `WhileRel` and by the iterates of its body, with its complexity
  derived from the loop by fusing the condition into the body
* `Ite`, `Comp`: the complexity of a conditional and of a composition

Encodings:
* requirements on encodings instead of fixed encodings, starting with
  `IsOptionEncoding` and the canonical `encOption`

Plumbing:
* the machine-level constructions the combinators are built from:
  sequential composition (proved), running a machine on other tapes,
  the clean-halting normal form, tape contents and redirecting the
  input and output of a machine to work tapes

The constructions themselves are still missing; the files state their
results as `proof_wanted` and both new directories have a README
describing the plan.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prove `computableInTimeAndSpace_loopFunction` modulo the plumbing, which is
stated as `sorry`ed theorems. `Loop.lean` itself contains no `sorry`.

`proof_wanted` emits a private declaration and its `❰…❱` reference syntax
resolves only within one file, so the plumbing results a combinator builds on
have to be real declarations. The ones the loop needs are therefore stated as
theorems with a `sorry` body; `lake build --wfail` fails on them until they are
filled in.

Words.lean (new): the interface the combinators use.
* `TapesHold` and `TransformsTapes`: `TransformsCfg` restricted to
  configurations in which every work tape holds a word, with the input head
  normalised to position `1` and the output preserved. Untouched tapes are
  described by the postcondition rather than by a set of tape indices, so
  `Cfg.AgreesOutside` never appears in a combinator.
* scratch tapes are hidden behind `∃ m c, ∀ k i o keep, …`, so a caller names
  an input tape, an output tape and the tapes to keep, and never an embedding.
* `exists_transformsTapes_repeat`: the loop-back that `seq` cannot express.
  `seq` sends the halting state of one machine to the initial state of the
  *next* one; a loop sends it back to the initial state of the *same* one.
  This was missing from the plan and is the only reason the loop combinator
  needs the machine level at all.
* `exists_transformsTapes_branch` is family-indexed. Since the bounds of
  `TransformsCfg` are numbers, a specification whose bounds depend on the input
  is a family over one fixed machine, which the unindexed `exists_branchOnTape`
  cannot provide.
* `exists_transformsTapes_ofComputable` and its input-tape variant fold together
  the clean normal form, `onTape` and `liftTapes`.

Loop:
* `mem_loopFunction_iff` and the lemmas on `loopIterate` it rests on, relating
  the function the loop computes to the iterates the machine runs through
  (proved, and free of `sorry`).
* `computableInTimeAndSpace_loopFunction`: the machine copies its input onto
  `T1`, runs the body into `T3`, and then repeats "compute the flag on `T4`;
  either stop or clear `T4`, clear `T1`, destruct `T3` onto `T1`, clear `T3`
  and run the body again", finally emitting `T1`. The loop invariant is keyed
  on the current value rather than on the round index, so one round is a single
  `TransformsTapes` statement per input.

Deterministic:
* `length_output_runFrom_le` and `ComputableInTimeAndSpace.length_encOut_le`:
  a machine emits at most one symbol per step. This is the only bound available
  on the length of an intermediate result, since a machine can produce an output
  much longer than the space it uses; the loop needs it to bound
  `(encOpt (some x)).length`, which `hsize` does not bound.

Encodings/Option:
* `computableInTimeAndSpace_isNone` is proved: it is `Option.isNone` being
  constant outside `{none}`.

Comp:
* `computableInTimeAndSpace_id`, the streaming copy, used to move the input of
  the loop onto a work tape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ite` is not primitive in Lean: `ite c t e` is `Decidable.casesOn`, the recursor
of the two-constructor inductive `Decidable c`, whose constructors carry only
proofs. `Prop` is erased, so the computational content of a conditional is
exactly the recursor of `Bool`, and the `Decidable` layer contributes nothing
beyond a computable Boolean test. `Combinators/Ite.lean` is restructured around
that primitive.

* `computableInTimeAndSpace_cond` is the primitive, the recursor of `Bool`. This
  is the previous `computableInTimeAndSpace_ite` renamed; its statement is
  unchanged, since its condition was already `α → Bool`.
* `computableInTimeAndSpace_ite` is now Lean's `ite`, for a decidable predicate,
  and is derived from the primitive through `decide`. For a language the test it
  asks for is `DecidableInTimeAndSpace`.
* `computableInTimeAndSpace_casesOn` is a `match` on a finite type, derived by
  nesting the primitive once per constructor. Only the branch that is taken is
  executed and the number of tests is a constant of the type, so the branches
  contribute their supremum and the tests are absorbed into the constant factor.
* `computableInTimeAndSpace_match` is the same from a computable scrutinee,
  which is the form to use: the tests come for free, since deciding which value
  the scrutinee has is a function on a finite type and hence computable in
  constant time.

Only `computableInTimeAndSpace_cond` remains a `sorry`; everything else in the
file is proved from it.

The module docstring records why this cannot be reduced to composition.
`cond` viewed as a function `Bool × β × β → β` is perfectly computable — it
reads a tag and streams out the component it selects — but

    fun a => if c a then f a else g a  =  cond ∘ (fun a => (c a, f a, g a))

computes both branches. That costs `tf + tg` rather than `max tf tg`, stores
both encoded results on work tapes, and, worst, evaluates `2 ^ n` branches for
`n` nested conditionals instead of `n`. The content of a case analysis is that
the branch not taken is never run, and that laziness is not expressible by
composing total functions: the machine has to choose before it runs. So this is
a combinator with a machine-level branch behind it, not a consequence of
`computableInTimeAndSpace_comp`.

The docstring of `computableInTimeAndSpace_casesOn` also records where the
analogy stops: for a *recursive* inductive type the recursor is a fold, not a
case analysis, and its combinator is the loop of `Combinators/Loop.lean` with an
iteration bound.

`Comp`: `computableInTimeAndSpace_comp` becomes a `sorry`ed theorem, since
`computableInTimeAndSpace_match` composes the constructor test with the
scrutinee.

`CslibTests/Complexity/Combinators.lean`: worked examples of a conditional on a
decidable predicate and of a `match` on a three-constructor enumeration, the
latter needing nothing of the type beyond `Fintype`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A non-recursive inductive type is a finite sum of finite products, so its
constructors and its eliminator are the introduction rule of a finite product
and the elimination rule of a finite coproduct. `Combinators/Ite.lean` is the
second; this is the first. The two are built the same way, by nesting the binary
case once per field resp. per constructor:

* the eliminator nests `computableInTimeAndSpace_cond`, and the branches
  contribute their supremum, since only the branch that is taken runs;
* the constructor nests `computableInTimeAndSpace_concat`, and the fields
  contribute their sum, since every field is computed.

Combinators/Tuple.lean (new):
* `computableInTimeAndSpace_flatten`: finitely many computations, concatenated.
  Proved by induction on the number of fields; the base case is
  `computableInTimeAndSpace_of_const`, since a constructor with no fields is a
  constant.
* `computableInTimeAndSpace_ctor`: the same read as a constructor, for any
  encoding that is the concatenation of the encoded fields. The pair
  constructor is the two-field instance.

Nothing is asked of the encoding beyond that syntactic factorisation. Building a
value needs no computability assumption on its encoding, unlike taking one
apart, which is why the destructors of a type are collected as requirements in
`Encodings/` while the constructors are not.

The arity has to be a constant of the type: each nesting costs one rewind of the
input tape, so `k` fields cost `k` times the input length in time, which is
absorbed into the existential constant only because `k` is fixed. A structure
with a variable number of components is not a finite product and needs the loop
with an iteration bound instead — the same boundary as for the eliminator, where
a recursive type needs a fold rather than a case analysis.

Combinators/Concat.lean (new): the concatenation of two computations, as a
`sorry`ed theorem in the style of the other machine-level combinators on this
branch, together with `computableInTimeAndSpace_pair` derived from it. It is
proved against the plumbing of the `samschles/tm-04-rewind` chain, which is an
independent development from the one here — the two share filenames for
`Deterministic`, `Plumbing/Basic` and `Plumbing/Sequential` but differ by around
a hundred lines in each, off a merge base five commits before either — so the
proof cannot be carried over without merging the chains. Only the statement is
duplicated, so the sorried file can be replaced wholesale once they meet.

CslibTests/Complexity/Combinators.lean, worked examples on Lean's own types:

* `Ordering`, for the `match` on a finite type, replacing an invented
  enumeration;
* `Option.some` at the canonical `encOption` of `Encodings/Option.lean`, showing
  that a tagged constructor is built by treating the tag as a field computed by
  a constant function;
* a subtype constructor, which costs nothing at all: the proof field is erased,
  so the encoding of the constructed value is the encoding of the data field and
  the constructor is a change of coordinates, `ComputableInTimeAndSpace.congr`,
  with no combinator involved;
* a three-field heterogeneous tuple encoded flat rather than as nested pairs,
  which is where `computableInTimeAndSpace_flatten` is the form to use, since
  the index family of `computableInTimeAndSpace_ctor` is dependent and awkward
  to instantiate when the fields have different types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`computableInTimeAndSpace_pair` assumed the encoding of a pair *is* the
concatenation of the encoded components. That is a condition on the shape of the
encoded string, of a different kind from the hypotheses of the eliminator, which
are all of the form "this function is computable"; and it rules out interleaved,
repackaged or otherwise non-concatenated layouts.

Encodings/Pair.lean (new) replaces it with complexity requirements, at the
bounds a machine can actually meet — linear time and constant space, i.e. by
streaming with only the finite control:

* `pack`: the components, one after the other, can be turned into the encoded
  pair;
* `fst_computable`, `snd_computable`: the components can be read back.

Zero space, which is what `Encodings/Option.lean` asks, is too strong here: a
single tag bit can be dropped while streaming, but finding where a component
ends may need the control to keep count.

One field is not a complexity. `cat_injective` says the components laid out one
after the other determine the pair. It is not a condition on the pair's encoding
— it says nothing about it — but on the component encodings: the first has to be
self-delimiting in front of the second. It is unavoidable, since without it "the
components one after the other" is not an encoding and `pack` cannot be stated;
and it is exactly what a parenthesis- or escape-based encoding is built to have.

`computableInTimeAndSpace_pair_of_isPairEncoding` derives the pair from the
requirements: run the two machines one after the other, which produces the
components in order, then pack. The encoded components appear in both bounds
because the concatenation is handed to the packing machine as its input and
`computableInTimeAndSpace_comp` parks an intermediate result on a work tape;
the shape-assuming `computableInTimeAndSpace_pair` avoids that step and keeps
the sharper bounds, so both are kept, labelled.

Producing and consuming stay asymmetric, and that is not an artifact: with a
concatenation-style layout `pack` is free — a machine emitting the components in
order has already produced the pair — while reading a component back is not,
since the boundary has to be found. Concatenating is easier than parsing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Shape` has only one constructor of arity two, so the tag never has to
distinguish alternatives with the same payload shape. `Span` does: its two
alternatives both carry two natural numbers and are told apart by the tag alone.

* `span_ctor`: either alternative built from the tag and the two fields, the
  construction shared between them since only the tag and the constructor
  differ.
* a function into `Span` that chooses between the alternatives, which is
  `computableInTimeAndSpace_cond` over two instances of that construction. Only
  the branch that is taken is built, so the field bounds appear once rather than
  twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`computableInTimeAndSpace_cond` was the sorried primitive and the finite case
analysis was built from it by nesting once per constructor. That is the wrong
way round. A machine does not nest: deciding among `n` cases is one machine
reading a scrutinee of constant length and dispatching from its finite control,
which is no harder than deciding among two. The nested construction therefore
decomposed nothing — it replayed the same argument `n - 1` times, and since each
replay multiplies the constants, the bounds had to be renormalised into a fixed
shape at every step for the induction to go through.

`computableInTimeAndSpace_match` is now the primitive, for a scrutinee in any
finite type, and carries the `hagree` weakening. `cond`, `ite` and the new
`dite` are its instances at `Bool`. The two forms are interderivable at constant
cost — the tag gives every test by one composition with a function on a finite
type, the tests give the tag by running all of them — so nothing is lost, and
`computableInTimeAndSpace_casesOn`, which consumed per-case tests, is dropped
rather than restated.

The file loses the induction, the renormalisation, the empty-scrutinee case and
the manufacturing of tests inside `_match`: 282 lines to 191, of which the
arithmetic is now two `Finset.sup_le`s at `Bool`.

`dite` is what the `hagree` weakening was for. Its branches are not functions of
the input alone, so neither can be asked to be computable as it stands; what is
asked instead is a computable total function agreeing with the branch where that
branch is taken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`computableInTimeAndSpace_match` took a bound for the scrutinee and a family of
bounds for the branches, and concluded with their sum-of-suprema. Since those
only ever appear added together in the conclusion, a single pair of bounds
covering all of them says the same thing: given separate bounds, weaken each to
their supremum and apply the collapsed form, and the old conclusion comes back
verbatim. So the two are interderivable and `Finset.sup` leaves the statement.
`Fintype ι` weakens to `Finite ι` with it, which is the honest hypothesis.

`computableInTimeAndSpace_flatten` and `_ctor` collapse the same way, which also
retires the claim that the eliminator's supremum and the constructor's sum were
the interesting difference between them. They are not: the number of branches
and the number of fields are both constants of the type, so sum and supremum
differ by a constant factor, and an implementation running *every* branch would
meet either bound. What does distinguish them is that no time bound appears in
the case analysis' space bound — a machine computing all the branches has to
park their encoded outputs on work tapes, and an output's length is bounded only
by the time that produced it. The sum/supremum distinction becomes real only for
a variable number of items, which is the loop.

`flatten` loses its `key` helper entirely: with the bounds no longer indexed by
the field, the induction runs directly on the statement.

Also gone from the tests: the `Fin.sum_univ_*` and `Matrix.cons_val_*`
normalisation at every call site, and with it the `Lean.Expr.appArg!` simproc
panic that the three-field example was tripping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`f` carries no information: it is `fun a => br (sel a) a` up to `funext`, and
stating the conclusion for that function directly is equivalent. Keeping it
costs a `fun _ => rfl` where the two agree definitionally, and saves the caller
a `ComputableInTimeAndSpace.congr` where they do not — which is the case the
combinator exists for, since a caller's goal is a `match` and not an application
of the branch family.

What made it look expensive was applying the theorem with `obtain`, where there
is no expected type to infer `f` from and `hagree` fixes it to the wrong
function. With `exact` or `refine` it is inferred, so the explicit `(f := …)` in
`cond` and in the `Shape` example goes away and the latter drops from six lines
to one. Said so in the docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Span` was only exercised on the constructor side, and the one eliminator
example, on `Shape`, went as far as the tag and no further — so the case the
`hagree` weakening exists for had never been checked. Two examples now do.

`Span` is the easy half: both alternatives carry two naturals, so `Span.fst` and
`Span.snd` are total and the branches read them with nothing invented. The
destructuring is entirely in `hagree`, which is `rfl` once the scrutinee is case
split, since the projections agree with the pattern variables by definition.

`Shape` is the half that needs the weakening. Its arities are zero, one and two,
so `Shape.r` has to return something on a `rect` and `Shape.w` on a `circle`,
and the value is arbitrary. Each branch reads that junk nowhere, because it is
only run where its own alternative holds; a combinator demanding the branches
equal the `match` everywhere could not be applied at all.

Both go through unchanged. What is left over in each is `hbr`, which asks that a
branch be computable as a function of the input rather than of the payload —
that is a computable destructor per field plus pairing, which are requirements
on the encoding and not further combinators.

One trap recorded in the docstring: the branch family has to be written
`fun a => ![…] i` rather than `![…] i` with the input abstracted inside each
entry. The two are equal by `funext`, but `cases` cannot generalise the
scrutinee under a binder, and with the second spelling `hagree` is not `rfl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The destructuring example carried an input type and a `sel : α → Span` in front
of the match, which is a composition the theorem already handles and which the
`Shape` examples already show. Taking the input to be `Span` leaves the
eliminator of the type and nothing else: the branches take the payload rather
than the input alongside it, and what the type has to supply is visible as two
hypotheses — that the alternative can be read off the encoding, and that a
branch is computable as a function of the value being matched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every inductive type comes with `ctorIdx`, generated from `casesOn`, which is
exactly the hand-written tag: `Span.ctorIdx` is `fun x => Span.casesOn x (fun _
_ => 0) fun _ _ => 1`. It lands in `ℕ`, so `[Finite ι]` barred it.

That hypothesis was stronger than the machine needs. What the machine needs is
finitely many possible contents of the work tape holding the encoded scrutinee,
which is `(Set.range sel).Finite`, and for a finite type that is `Set.toFinite
_`. Branches outside the range are correspondingly no longer asked to be
computable — with an `ℕ`-indexed family all but finitely many of them are junk,
and quantifying over all of `ℕ` would make the hypothesis unsatisfiable for any
nonconstant bound.

This matters for mechanising the combinator rather than for using it by hand,
where a `Bool` or `Fin n` tag is still pleasanter: a tactic cannot invent a
bespoke tag type for an arbitrary inductive, but `ctorIdx` is always there, and
both side conditions — finite range, and the branches agreeing — are discharged
by case analysis on the scrutinee. An example on `Span` does exactly that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The step-gluing in `computableInTimeAndSpace_loopFunction` was repetitive
because the plumbing leaves were not of the right form: `clear` returned
`ws' i = [] ∧ ∀ l ≠ i, ws' l = ws l`, `ofComputable` a four-way conjunction,
and every seam between two machines re-proved the frame — which tapes survived —
by a case analysis per tape.

Under their own preconditions those postconditions are each a single equality of
the whole vector. `ofComputable` changes exactly one word: tape `i` and the
`keep` tapes are untouched and the scratch tapes were blank before, so ending
blank is *unchanged*. Hence:

* `exists_transformsTapes_clear`:   `ws' = Function.update ws i []`
* `exists_transformsTapes_ofComputable`, `_ofComputableInput`:
                                    `ws' = Function.update ws o (encOut (g a))`

In the loop proof, the bespoke predicates `Both`/`Only1`/`Only3`/`InvMid`
become one sealed vector family `V w1 w3 w4` with projection, blankness and
`Function.update` equations proved once; each of the eight step-gluings is then
one or two `rw`s instead of an unpack-repack of frame conditions, and the
composite of the five-machine round is taken apart by `rfl`-patterns.

The net line count is flat (748 → 757): the vector family and its nine
single-purpose lemmas cost up front what the seams used to pay severally. What
changes is that the per-seam content is now only what is specific to that seam.
The other half of the proof's bulk — the `A₁…A₇`/`B₁…B₅` renormalisations of
each machine's bound into `a`-units — is untouched: it is not a statement
problem but the price of charging round-`x` costs to the loop input `a`, which
the repeat combinator's uniform per-round bounds force.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two pieces of `computableInTimeAndSpace_loopFunction` were self-contained and
are now file-level declarations:

* `loopIterate_succ_of_lt` (public): as long as the loop has not stopped, each
  iterate comes from the previous one by a successful call of the body. This
  was `hnext`, and it is pure recursion theory with no machine in it.
* the `tapeWords` algebra (private): the vector of words holding three values
  on three named tapes, with its projection, blankness and `Function.update`
  equations. This was the sealed `V` family inside the proof; as named lemmas
  the equations are stated once, independently checkable, and the proof's
  gluing rewrites name what they use.

Also drops `nat_bound_sum`, which nothing used.

The theorem's proof shrinks from ~560 to ~475 lines; the file grows to 800
because the extracted pieces carry their own documentation. The remaining
monolith is deliberate: the round machines (`hExit`/`hCont`/`hRound`) share the
obtains of the five component machines and the `W`-renormalisation, so
extracting them as lemmas would cost each a ~40-line hypothesis list restating
the machine specifications and the bound facts — more text than the ~90-line
proof bodies they would remove. The `###` section comments inside the proof
mark the honest module boundaries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants