Partial definitions in proofs
The rule: #[partial] exempts executable code from the termination check.
It cannot exempt a proof. A theorem — its statement or its proof term — may not
reach a #[partial] definition, directly or through any chain of calls, and
the compiler refuses one that does with E0063.
What the compiler refuses
This is the golden test tests/golden/error/termination_partial_in_proof.tg:
// == a theorem may not reach a #[partial] constant, even through a wrapper
type Lst = Nil | Cons(Nat, Lst)
#[partial]
fn spin(l: Lst) -> Nat { spin(l) }
fn wrapper(l: Lst) -> Nat { spin(l) }
theorem wrapped(l: Lst): Nat { wrapper(l) }
fn main() -> Nat { 0 }
albite check wrapped.tg prints:
[E0063] Error: `wrapped` is a proof, so it may not depend on the partial constant `spin`
╭─[wrapped.tg:9:1]
│
9 │ theorem wrapped(l: Lst): Nat { wrapper(l) }
│ ─────────────────────┬─────────────────────
│ ╰───────────────────────
│
│ Help: a proof may not depend on a partial definition — prove the recursion terminates, or state the theorem about a total one
│
│ Note: reaches `spin` (marked `#[partial]`) through wrapper
───╯
wrapper itself is fine — it is ordinary executable code, and calling a
partial function from executable code is always allowed. The error is on the
theorem, and the Note: line names the chain it travelled.
What it means
albite explain error E0063 says:
#[partial]buys executable code an exemption from termination checking. It cannot buy a proof one: a proof that may reference a possibly-diverging constant proves nothing.The restriction is transitive, which is the point. A wrapper that merely calls a partial function is itself partial, so routing a theorem through one more layer does not launder it. The message names the chain it travelled.
A definition’s type counts as well as its body — an equality statement embeds terms, so a theorem can reach a partial constant without its proof term mentioning one.
The fix is to prove the recursion terminates (rewrite it structurally, or name the decreasing parameter), or to state the theorem about a definition that is already total. Executable code is unaffected: a partial constant is welcome anywhere outside a proof.
What to write instead
Keep the partial function where it belongs — in code that runs — and state the
theorem about a total one. This is tests/golden/check/docs_partial_in_proofs.tg:
// Rejected: a theorem that reaches a `#[partial]` function.
// Accepted: the theorem is about a total function; the partial one
// stays in executable code, where it is welcome.
type Lst = Nil | Cons(Nat, Lst)
#[partial]
fn digits(n: Nat) -> Nat { if n < 10 { 1 } else { 1 + digits(n / 10) } }
fn length(l: Lst) -> Nat { match l { Nil => 0, Cons(h, t) => 1 + length(t) } }
theorem measured(l: Lst): Nat { length(l) }
fn main() -> Nat { digits(length(Cons(1, Nil)) * 100) }
albite run prints 3.
When the function you want to reason about is the partial one, the only way
through is to make it total: rewrite the recursion on a structural subterm, or
name the decreasing parameter with #[decreasing(arg)]. See
Recursion the compiler can see ends.
Ask the compiler
albite explain error E0063
albite doctor check type termination <file>
The census lists every definition’s termination verdict, including which ones are partial and which are partial only because they call one.