Compare commits

...

1 Commits
dev ... main

Author SHA1 Message Date
Bram van den Heuvel ea47c34308 Create initial proof builder POC 2026-07-13 16:48:56 +02:00
4 changed files with 523 additions and 39 deletions

View File

@ -3,10 +3,12 @@
"""
from __future__ import annotations
from typing import Callable
from dataclasses import dataclass
from .context import Context
from .props import And, Eq, Or, Prop
from .props import And, Eq, Exists, ForAll, Or, Prop
from .terms import Const, Term
class ProofCheckerFailedException(Exception):
pass
@ -20,14 +22,26 @@ class Proof:
context : Context
statement : Prop
def check(self) -> None:
def __repr__(self) -> str:
return (
# "Γ" +
", ".join([ str(p) for p in self.context.props]) +
"" +
str(self.statement)
)
def check(self, is_lemma : bool = False, to_stdout : bool = False) -> None:
"""
Check whether the proof is self-sufficient. That is, the proof
relies on zero context in order to be considered true.
"""
if self.context.isempty():
if to_stdout:
print(f"Successfully proved that {self}")
return
elif is_lemma:
if to_stdout:
print(f"Proved lemma: {self}")
else:
raise ProofCheckerFailedException(
f"Proof still relies on {len(self.context.props)} assumptions."
@ -107,6 +121,105 @@ def assume(statement : Prop) -> Proof:
statement=statement,
)
def eq_l(proof : Proof, eq : Eq, statement : Prop) -> Proof:
"""
Use an equality to the left of the statement to substitute values.
$$
Γ P[y]
-----------------
Γ, x = y P[x]
$$
:param proof: The proof supporting the claim.
:type proof: Proof
:param eq: The equality, where the statement on the left is meant to be
replaced by the statement on the right.
:type eq: Eq
:param statement: The final statement that one should end up at.
:type statement: Prop
"""
if proof.statement.normalize() != statement.normalize().replace(old=eq.x, new=eq.y).normalize():
raise ProofCheckerFailedException(
f"Failed to arrive at {proof.statement} "
f"when substituting {eq} into {statement}"
)
return Proof(
context=proof.context.with_prop(eq),
statement=statement,
)
def forall_l(proof : Proof, statement : Callable[[Term], Prop], term : Term) -> Proof:
"""
Generalize a hypothesis by making a term more generalized.
$$
Γ, P[x] Q
--------------
Γ, x.P Q
$$
:param proof: The proof with the hypothesis to generalize.
:type proof: Proof
:param statement: The statement to generalize.
:type statement: Callable[[Term], Proof]
:param term: The term to substitute.
:type term: Term
:return: The generalized proof.
:rtype: Proof
"""
s = statement(term)
# TODO: Return new proof
return proof
def forall_r(proof : Proof, statement : Callable[[Term], Prop], free_var_name : str) -> Proof:
"""
Generalize a proof for all instances of a given value.
$$
Γ P[x]
-----------------
Γ \forall x.P
$$
:param proof: The proof to generalize.
:type proof: Proof
:param statement: The forall-statement.
:type statement: Callable[[Term], Prop]
:param free_var_name: The free variable that can be generalized.
:type free_var_name: str
:return: The generalized proof.
:rtype: Proof
"""
fv = Const(free_var_name)
nfv = Const("not_" + free_var_name)
# Make sure the free variable isn't bound within the statement
s = statement(nfv)
if s.contains(fv):
raise ProofCheckerFailedException(
f"Some occurrences of {free_var_name} were found to not be "
f"substituted in {s}"
)
# Make sure the free variable doesn't appear in the context
s = statement(fv)
for p in proof.context.props:
if p.contains(fv):
raise ProofCheckerFailedException(
f"Free variable {fv} was found in context hypothesis: {p}"
)
# Make sure the proof statement matches the statement with the free variable
if s != proof.statement:
raise ProofCheckerFailedException(
f"Expected proof to prove that {s}, found {proof.statement} instead."
)
return Proof(context=proof.context, statement=Exists(statement))
def from_assumption(ctx : Context, statement : Prop) -> Proof:
"""
Gain a proof by including the statement in the assumptions.
@ -230,7 +343,7 @@ def reflexivity(eq : Eq) -> Proof:
demonstrate that the two sides are equal.
"""
l = eq.x.normalize()
r = eq.x.normalize()
r = eq.y.normalize()
if l == r:
return Proof(

View File

@ -5,7 +5,7 @@
from __future__ import annotations
from typing import Callable
from .terms import Term
from .terms import USABLE_VAR_NAMES, Const, Term, Var, ecsl
from dataclasses import dataclass
class Prop:
@ -15,42 +15,88 @@ class Prop:
proposition.
"""
def as_assumption(self, goal : Prop) -> list[list[tuple[list[Prop], Prop]]]:
def __repr__(self) -> str:
"""
Propositions can simplify the proof by creating simpler theorems
and sub-lemmas to prove instead. If this proposition is part of
the assumptions, this function helps transform the goal into
simpler sub-goals.
Create a representation of a proposition.
:return: Tuples of added assumptions with a new goal.
:rtype: list[list[tuple[list[Prop], Prop]]]
:return: String representation of the proposition.
:rtype: str
"""
return []
return self.to_str(vars=set())
def as_proof(self) -> list[list[tuple[list[Prop], Prop]]]:
def contains(self, term : Term) -> bool:
"""
Determine which hypotheses are required in the context to prove
this proposition.
Determine whether a given term, value or variable is contained or
described by this proposition.
The function returns a list of various methods to prove the
proposition. If any of the methods is an empty list, it means
the proposition requires no assumptions and can instead be
computed.
:return: Methods to prove this proposition.
:rtype: list[tuple[list[Prop], Prop]]
:param term: The term to look for.
:type term: Term
:return: Whether the term is contained in this proposition.
:rtype: bool
"""
return []
return False
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return self
def replace(self, old : Term, new : Term) -> Prop:
"""
Find any occurrence of a given term, and replace it with the new
term.
:param old: The term to replace.
:type old: Term
:param new: The term to replace it with.
:type new: Term
:return: The proposition with the term replaced.
:rtype: Prop
"""
return self
def to_str(self, vars : set[str]) -> str:
return "<undefined prop>"
@dataclass(frozen=True)
class And(Prop):
"""
The logical and-operator `P ^ Q`.
The logical and-operator `P Q`.
"""
P : Prop
Q : Prop
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term: Term) -> bool:
return self.P.contains(term) or self.Q.contains(term)
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return And(P=self.P.normalize(), Q=self.Q.normalize())
def replace(self, old: Term, new: Term) -> Prop:
return And(
P=self.P.replace(old=old, new=new),
Q=self.Q.replace(old=old, new=new),
)
def to_str(self, vars: set[str]) -> str:
p_str = ecsl(self.P.to_str(vars=vars))
q_str = ecsl(self.Q.to_str(vars=vars))
return f"{p_str}{q_str}"
@dataclass(frozen=True)
class Eq(Prop):
"""
@ -60,11 +106,34 @@ class Eq(Prop):
x : Term
y : Term
def as_assumption(self, goal: Prop) -> list[list[tuple[list[Prop], Prop]]]:
return []
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term: Term) -> bool:
return self.x.contains(term) or self.y.contains(term)
def as_proof(self) -> list[list[tuple[list[Prop], Prop]]]:
return super().as_proof()
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return Eq(x=self.x.normalize(), y=self.y.normalize())
def replace(self, old: Term, new: Term) -> Prop:
return Eq(
x=self.x.replace(old=old, new=new),
y=self.y.replace(old=old, new=new),
)
def to_str(self, vars : set[str]) -> str:
x_str = ecsl(self.x.to_str(vars={ key : Var() for key in vars }))
y_str = ecsl(self.y.to_str(vars={ key : Var() for key in vars }))
return f"{x_str} = {y_str}"
@dataclass(frozen=True)
class Exists(Prop):
@ -75,6 +144,69 @@ class Exists(Prop):
x : Callable[[Term], Prop]
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term : Term) -> bool:
i, c = 0, Const("")
while i == 0 or term.contains(c):
i += 1
c = Const("temp_exists_variable_{i}")
return self.x(c).contains(term=term)
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return Exists(lambda n : self.x(n).normalize())
def replace(self, old: Term, new: Term) -> Prop:
def replaced_func(input_term : Term) -> Prop:
c = unique_const(input_term=input_term)
return (
self.x(c)
.replace(old=old, new=new)
.replace(old=c, new=input_term)
)
def unique_const(input_term : Term) -> Term:
"""
Get a unique const value that doesn't interfere with either the
old term that needs to be replaced, nor with the input term.
"""
i, c = 0, Const("")
while i == 0 or input_term.contains(c) or old.contains(c):
i += 1
c = Const(f"temp_unique_value_{i}")
return c
return Exists(replaced_func)
def to_str(self, vars : set[str]) -> str:
for c in USABLE_VAR_NAMES:
if c in vars:
continue
vars.add(c)
s = self.x(Const(c)).to_str(vars=vars)
vars.remove(c)
return "" + c + "." + ecsl(s)
else:
raise ValueError(
"Ran out of usable var names!"
)
@dataclass(frozen=True)
class ForAll(Prop):
"""
@ -83,6 +215,69 @@ class ForAll(Prop):
x : Callable[[Term], Prop]
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term : Term) -> bool:
i, c = 0, Const("")
while i == 0 or term.contains(c):
i += 1
c = Const("temp_forall_variable_{i}")
return self.x(c).contains(term=term)
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return ForAll(lambda n : self.x(n).normalize())
def replace(self, old: Term, new: Term) -> Prop:
def replaced_func(input_term : Term) -> Prop:
c = unique_const(input_term=input_term)
return (
self.x(c)
.replace(old=old, new=new)
.replace(old=c, new=input_term)
)
def unique_const(input_term : Term) -> Term:
"""
Get a unique const value that doesn't interfere with either the
old term that needs to be replaced, nor with the input term.
"""
i, c = 0, Const("")
while i == 0 or input_term.contains(c) or old.contains(c):
i += 1
c = Const(f"temp_unique_value_{i}")
return c
return ForAll(replaced_func)
def to_str(self, vars : set[str]) -> str:
for c in USABLE_VAR_NAMES:
if c in vars:
continue
vars.add(c)
s = self.x(Const(c)).to_str(vars=vars)
vars.remove(c)
return "" + c + "." + ecsl(s)
else:
raise ValueError(
"Ran out of usable var names!"
)
@dataclass(frozen=True)
class Implies(Prop):
"""
@ -92,6 +287,35 @@ class Implies(Prop):
P : Prop
Q : Prop
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term: Term) -> bool:
return self.P.contains(term) or self.Q.contains(term)
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return Implies(P=self.P.normalize(), Q=self.Q.normalize())
def replace(self, old: Term, new: Term) -> Prop:
return Implies(
P=self.P.replace(old=old, new=new),
Q=self.Q.replace(old=old, new=new),
)
def to_str(self, vars: set[str]) -> str:
p_str = ecsl(self.P.to_str(vars=vars))
q_str = ecsl(self.Q.to_str(vars=vars))
return f"{p_str}{q_str}"
@dataclass(frozen=True)
class Or(Prop):
"""
@ -100,5 +324,33 @@ class Or(Prop):
P : Prop
Q : Prop
def __repr__(self) -> str:
"""
Create a representation of a proposition.
:return: String representation of the proposition.
:rtype: str
"""
return self.to_str(vars=set())
def contains(self, term: Term) -> bool:
return self.P.contains(term) or self.Q.contains(term)
def normalize(self) -> Prop:
"""
Simplify the terms of a proposition.
"""
return Or(P=self.P.normalize(), Q=self.Q.normalize())
def replace(self, old: Term, new: Term) -> Prop:
return Or(
P=self.P.replace(old=old, new=new),
Q=self.Q.replace(old=old, new=new),
)
def to_str(self, vars: set[str]) -> str:
p_str = ecsl(self.P.to_str(vars=vars))
q_str = ecsl(self.Q.to_str(vars=vars))
return f"{p_str} {q_str}"

View File

@ -42,6 +42,38 @@ def contains(haystack : Term, needle : Term) -> bool:
else:
return False
def ecsl(s : str) -> str:
"""
Encapsulate something if it contains non-encapsulated spaces.
:param s: The string to potentially encapsulate.
:type s: str
:return: An encapsulated string.
:rtype: str
"""
def needs_encapsulation() -> bool:
if " " not in s:
return False
level = 0
for c in s:
match c:
case " ":
if level <= 0:
return True
case "(":
level += 1
case ")":
level -= 1
else:
return False
if needs_encapsulation():
return "(" + s + ")"
else:
return s
def normalize(term : Term) -> Term:
"""
Create a normalization function. This function simplifies functions as
@ -210,6 +242,17 @@ class Term:
"""
return self.to_str(vars={})
def contains(self, needle : Term) -> bool:
"""
Determine whether this term is or contains any given term.
:param needle: The term to look for.
:type needle: Term
:return: Whether the term is found.
:rtype: bool
"""
return contains(haystack=self, needle=needle)
def normalize(self) -> Term:
return normalize(term=self)
@ -220,7 +263,7 @@ class Term:
yield self
def to_str(self, vars : dict[str, Var]) -> str:
return "<undefined>"
return "<undefined term>"
@dataclass(frozen=True)
class App(Term):
@ -244,10 +287,24 @@ class App(Term):
yield from self.x.rec_iter()
def to_str(self, vars: dict[str, Var]) -> str:
# Reformat pre-defined common functions
match self:
case App(f=App(f=Const("Nat.add"), x=a), x=b):
return f"{ecsl(a.to_str(vars=vars))} + {ecsl(b.to_str(vars=vars))}"
# Display numbers
n, c = 0, self
while isinstance(c, App) and c.f == Const("Nat.Succ"):
n, c = n + 1, c.x
if c == Const("Nat.Zero"):
return str(n)
# elif n > 0:
# return f"{ecsl(c.to_str(vars=vars))} + {n}"
f_str = self.f.to_str(vars=vars)
x_str = self.x.to_str(vars=vars)
return f_str + " " + (f"({x_str})" if " " in x_str else x_str)
return f_str + " " + ecsl(x_str)
A1 = lambda fn, a : App(f=fn, x=a)
A2 = lambda fn, a, b : App(f=A1(fn, a), x=b)
@ -274,6 +331,8 @@ class Const(Term):
return self.to_str(vars={})
def to_str(self, vars: dict[str, Var]) -> str:
if self.name == "Nat.Zero":
return "0"
return self.name
class Lambda(Term):
@ -357,7 +416,7 @@ class Lambda(Term):
s = self.out.to_str(vars=vars)
del vars[char]
return "λ" + char + "." + (f"({s})" if " " in s else s)
return "λ" + char + "." + ecsl(s)
L1 = lambda f : Lambda(lambda x : f(x))
L2 = lambda f : Lambda(lambda x : L1(lambda y : f(x, y)))
@ -398,10 +457,13 @@ if __name__ == "__main__":
# x
print(Const("x"))
# λx.x
print(Lambda(lambda a : a))
# λx.(f x)
print(Lambda(lambda a : App(f=Const("f"), x=a)))
# λx.(λy.(x y))
# λx.λy.(x y)
print(Lambda(lambda b : Lambda(lambda c : App(f=b, x=c))))
# Nat.Succ (Nat.Succ Nat.Zero)
@ -464,12 +526,12 @@ if __name__ == "__main__":
print("\nSHORTHAND NOTATION")
print(30 * "=")
# λx.(λy.(λz.(x y z))
# λx.λy.λz.(x y z)
e = L3(lambda a, b, c : App(f=App(f=a, x=b), x=c))
print(e)
# λx.(λy.(λz.(x y z))
# λx.λy.λz.(x y z)
f = L3(lambda a, b, c : A3(L3(lambda a, b, c : A2(a, b, c)), a, b, c))
print(f)
print(f.normalize())

View File

@ -1,6 +1,7 @@
from checker import lib, proof, props
from checker.terms import A2, App, Const
from checker.terms import A1, A2, App, Const
from checker.lib.basic import kernel_from_int
from checker.props import Eq
# Prove : true == not false
p = proof.reflexivity(
@ -9,7 +10,7 @@ p = proof.reflexivity(
y=App(f=Const("Bool.not"), x=Const("Bool.False"))
)
)
p.check()
p.check(to_stdout=True)
# Prove : false == not true
p = proof.reflexivity(
@ -18,7 +19,7 @@ p = proof.reflexivity(
y=App(f=Const("Bool.not"), x=Const("Bool.True"))
)
)
p.check()
p.check(to_stdout=True)
# Prove : 4 + 1 == 2 + 3
p = proof.reflexivity(
@ -27,3 +28,59 @@ p = proof.reflexivity(
y=A2(Const("Nat.add"), kernel_from_int(2), kernel_from_int(3)),
)
)
p.check(to_stdout=True)
# Prove : 0 + x = x
p = proof.reflexivity(
props.Eq(
x=A2(Const("Nat.add"), kernel_from_int(0), Const("x")),
y=Const("x"),
)
)
p.check(to_stdout=True)
# Prove : x + 0 = x
# # SHOULD RAISE EXCEPTION
# p = proof.reflexivity(
# props.Eq(
# x=A2(Const("Nat.add"), Const("x"), kernel_from_int(0)),
# y=Const("x"),
# )
# )
# p.check(to_stdout=True)
case_1 = proof.eq_l(
proof=proof.reflexivity(
Eq(
x = A2(Const("Nat.add"), Const("Nat.Zero"), Const("Nat.Zero")),
y = Const("Nat.Zero")
)
),
eq=Eq(x=Const("x"), y=Const("Nat.Zero")),
statement=Eq(
x=A2(Const("Nat.add"), Const("x"), Const("Nat.Zero")),
y=Const("x")
),
)
case_1.check(is_lemma=True, to_stdout=True)
case_2 = proof.eq_l(
proof=proof.eq_l(
proof=proof.reflexivity(Eq(
x=A1(Const("Nat.Succ"), Const("n")),
y=A1(Const("Nat.Succ"), Const("n")),
)),
eq=Eq(x=A2(Const("Nat.add"), Const("n"), Const("Nat.Zero")), y=Const("n")),
statement=Eq(
x=A2(Const("Nat.add"), A1(Const("Nat.Succ"), Const("n")), Const("Nat.Zero")),
y=A1(Const("Nat.Succ"), Const("n")),
),
),
eq=Eq(x=Const("x"), y=A1(Const("Nat.Succ"), Const("n"))),
statement=Eq(
x=A2(Const("Nat.add"), Const("x"), Const("Nat.Zero")),
y=Const("x"),
),
)
case_2.check(is_lemma=True, to_stdout=True)