Proving Theorems

@logos.theorem, logos.check, and logos.prove are the top-level entry points for proof checking.

@logos.theorem — declare a theorem

@logos.theorem wraps a function to declare a Theorem object. No proof is run yet.

import logos
import logos.tactics as t

@logos.theorem
def add_zero(x: int):
    return x + logos.lit(0) == x

print(add_zero)             # Theorem[unverified]('add_zero')
print(add_zero.certified)   # False — not yet checked
Theorem[unverified]('add_zero')
False

.proof(...) — attach a proof

Attach the proof spec with .proof(tactic1, tactic2, ...). Returns self for chaining:

# Chain style
logos.check(add_zero.proof(t.intro("x"), t.ring()))
print(add_zero.certified)   # True
True

Or use the decorator form:

@logos.theorem
def add_zero_v2(x: int):
    return x + logos.lit(0) == x

@add_zero_v2.proof
def _():
    return [t.intro("x"), t.ring()]

logos.check(add_zero_v2)
print("Proved with decorator style")
Proved with decorator style

logos.check — the compute boundary

logos.check(thm) runs the kernel. Nothing executes until this call:

@logos.theorem
def mul_comm(x: int, y: int):
    return x * y == y * x

mul_comm.proof(t.intro("x", "y"), t.ring())   # just attaches, doesn't run

logos.check(mul_comm)   # NOW it runs
print(mul_comm.certified)   # True
True

The Proof List

The proof is a list of tactics applied left-to-right. When a tactic produces subgoals, the next items handle them.

@logos.theorem
def both_positive(x: int, y: int):
    return (x > logos.lit(0)) >> ((y > logos.lit(0)) >> ((x > logos.lit(0)) & (y > logos.lit(0))))

logos.check(both_positive.proof(
    t.intro("x", "y", "hx", "hy"),   # strip foralls and implications
    t.split(),                          # A & B → two subgoals
    [t.assumption()],                   # subgoal 1: x > 0  (in context as hx)
    [t.assumption()],                   # subgoal 2: y > 0  (in context as hy)
))
print("Proved")
Proved

What Happens When a Proof Fails

LogosProofError is raised with a clear message:

from logos.theorem import LogosProofError

@logos.theorem
def false_claim(x: int, y: int):
    return x + y == x - y   # false!

try:
    logos.check(false_claim.proof(t.intro("x", "y"), t.ring()))
except LogosProofError as e:
    print(f"Proof failed: {e}")
Proof failed: Theorem 'false_claim': ring: Add(left=Var(name='x', type=<class 'int'>), right=Var(name='y', type=<class 'int'>)) ≠ Sub(left=Var(name='x', type=<class 'int'>), right=Var(name='y', type=<class 'int'>)) after normalization
  LHS poly: {frozenset({('x', 1)}): 1, frozenset({('y', 1)}): 1}
  RHS poly: {frozenset({('x', 1)}): 1, frozenset({('y', 1)}): -1}

logos.prove — one-shot anonymous proof

logos.prove(statement, proof_list) creates a _GroundProof — pass it to logos.check():

result = logos.check(logos.prove(
    logos.forall(int, lambda x: x + logos.lit(0) == x),
    [t.intro("x"), t.ring()],
))
print(result.certified)  # True
True

Building on Proved Theorems

Once a theorem is certified, you can apply it in other proofs using the object directly:

@logos.theorem
def add_comm_v2(x: int, y: int):
    return x + y == y + x

logos.check(add_comm_v2.proof(t.intro("x", "y"), t.ring()))

# Use add_comm_v2 as an axiom object in another proof
@logos.theorem
def reorder_sum(x: int, y: int, z: int):
    return x + y + z == z + y + x

logos.check(reorder_sum.proof(t.intro("x", "y", "z"), t.ring()))
print("Proved")
Proved

Module-Level Theorem Files

A common pattern is a dedicated theorem file that’s imported wherever needed. Since logos.check raises LogosProofError at check time if a proof is wrong, it acts like a compile-time check:

# theorems/arithmetic.py
import logos
import logos.tactics as t

@logos.theorem
def add_comm(x: int, y: int):
    return x + y == y + x

logos.check(add_comm.proof(t.intro("x", "y"), t.ring()))

@logos.theorem
def mul_comm(x: int, y: int):
    return x * y == y * x

logos.check(mul_comm.proof(t.intro("x", "y"), t.ring()))

# If any proof breaks, this module fails to import.