Skip to content

Commit 7ec2f6a

Browse files
levineuwirthclaude
andcommitted
Alethe walker: equality cluster (refl / symm / trans / cong)
Adds the four equality rules cvc5's alethe-2024 emits for UF and equality reasoning. All four reconstruct the kernel proof from premise proofs directly (no decision procedure), so the resulting theorems are axiom-free. * `refl`: Eq.refl on the LHS of (cl (= t t)). * `symm`: Eq.symm on the single premise. * `trans`: left-fold of Eq.trans over the premise list. * `cong`: left-fold of Lean.Meta.mkCongr over premise equations, seeded with Eq.refl on the (Sexp-equal) function head. `sexpToExpr`'s `listToExpr` gains a generic-application fallback: unknown-head atoms are resolved through `ctx.vars` and applied to their reified args via `mkAppM'`. This is what lets `cong` translate `(f a1 … an)` for an arbitrary UF symbol `f` in scope. Six new tests in Test/Tactic.lean exercise each rule in isolation plus an end-to-end UF refutation (`x = y, ¬(f x = f y) ⊢ False` via cong + resolution); all six close axiom-free. Allowlist entries updated accordingly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 69fa842 commit 7ec2f6a

3 files changed

Lines changed: 218 additions & 7 deletions

File tree

lean-bridge/ProofBroker/Alethe.lean

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,21 @@ partial def listToExpr (ctx : WalkerContext) : List Sexp → MetaM Expr
341341
let aE ← sexpToExpr ctx a
342342
let bE ← sexpToExpr ctx b
343343
return mkForall .anonymous .default aE bE
344+
-- Generic application fallback. Used by `cong` to translate
345+
-- `(f a1 … an)` into `f a1 … an` for UF symbols. The head is
346+
-- looked up in `ctx.vars` (UF symbols are local free vars in
347+
-- the home-system goal); arguments recurse through
348+
-- `sexpToExpr`. Falls through to the catch-all error if the
349+
-- head is not in scope, keeping unrecognized shapes as honest
350+
-- failures rather than silently building ill-typed apps.
351+
| (.atom name) :: args => do
352+
match ctx.vars.find? (Name.mkSimple name) with
353+
| some fE => do
354+
let argEs ← args.mapM (sexpToExpr ctx)
355+
Lean.Meta.mkAppM' fE argEs.toArray
356+
| none =>
357+
throwError m!"alethe walker: unsupported applied head '{name}' \
358+
(not a recognized operator, not in local scope)"
344359
| other =>
345360
throwError m!"alethe walker: unsupported Sexp shape: \
346361
({String.intercalate " " (other.map fun s => reprStr s)})"
@@ -548,6 +563,105 @@ private def elabResolution (ctx : WalkerContext) (s : Step)
548563
throwError m!"alethe walker: 'resolution' needs at least one \
549564
premise, got {repr s.premises}"
550565

566+
/- ----------------------------------------------------------------
567+
Equality cluster: `refl` / `symm` / `trans` / `cong`.
568+
569+
These are the rules cvc5's `alethe-2024` emits for the UF /
570+
equality fragment. None of them touch a decision procedure
571+
(unlike `la_generic` / `la_mult_neg` which omega-discharge a
572+
tautological leaf): they reconstruct the kernel proof from the
573+
premises directly, so the resulting proof terms are axiom-free
574+
(no `propext` / `Classical.choice`, just `Eq.rec` underneath).
575+
---------------------------------------------------------------- -/
576+
577+
/-- `refl`: a leaf rule with no premises, concluding `(cl (= t t))`
578+
for any term `t`. The walker requires LHS and RHS to be
579+
syntactically identical at the Sexp level (the form cvc5
580+
emits after preprocessing); the proof term is `Eq.refl t`. -/
581+
private def elabRefl (ctx : WalkerContext) (s : Step)
582+
: WalkerM (Expr × List Sexp) := do
583+
match s.clause with
584+
| [.list [.atom "=", lhs, rhs]] => do
585+
unless lhs == rhs do
586+
throwError m!"alethe walker: 'refl' expects (= t t) with \
587+
identical sides, got (= {repr lhs} {repr rhs})"
588+
let lhsE ← sexpToExpr ctx lhs
589+
let proof ← mkAppM ``Eq.refl #[lhsE]
590+
pure (proof, s.clause)
591+
| _ =>
592+
throwError m!"alethe walker: 'refl' expects clause \
593+
(cl (= t t)), got {repr s.clause}"
594+
595+
/-- `symm`: one premise proving `(= t u)`, conclusion `(= u t)`.
596+
The proof term is `Eq.symm` of the premise. -/
597+
private def elabSymm (s : Step) : WalkerM (Expr × List Sexp) := do
598+
match s.premises with
599+
| some [p] => do
600+
let (eP, _) ← lookupStep p
601+
let proof ← mkAppM ``Eq.symm #[eP]
602+
pure (proof, s.clause)
603+
| _ =>
604+
throwError m!"alethe walker: 'symm' expects exactly one \
605+
premise, got {repr s.premises}"
606+
607+
/-- `trans`: n premises proving `(= t1 t2)`, `(= t2 t3)`, …,
608+
`(= t_{n} t_{n+1})`, conclusion `(= t1 t_{n+1})`. The proof
609+
term is the left-fold of `Eq.trans` over the premise list.
610+
A single-premise `trans` is a no-op (passthrough). -/
611+
private def elabTrans (s : Step) : WalkerM (Expr × List Sexp) := do
612+
match s.premises with
613+
| some (p0 :: rest) => do
614+
let (e0, _) ← lookupStep p0
615+
let mut acc := e0
616+
for pi in rest do
617+
let (ei, _) ← lookupStep pi
618+
acc ← mkAppM ``Eq.trans #[acc, ei]
619+
pure (acc, s.clause)
620+
| _ =>
621+
throwError m!"alethe walker: 'trans' expects at least one \
622+
premise, got {repr s.premises}"
623+
624+
/-- `cong`: n premises proving `(= a1 b1)`, …, `(= an bn)`,
625+
conclusion `(= (f a1 … an) (f b1 … bn))`. The proof term is
626+
built by left-folding `Lean.Meta.mkCongr` over the premise list,
627+
starting from `Eq.refl f`. `mkCongr` collapses the
628+
`Eq.refl f` seed into `mkCongrArg` automatically, then chains
629+
through `mkCongr`'s general case for each subsequent
630+
argument — so the resulting term is a curried congruence
631+
cascade (`(f a1) a2 = (f b1) b2` etc.) matching Lean's own
632+
curried application convention.
633+
634+
The function head is required to be identical on both sides
635+
(Sexp `BEq`); typically `fA = .atom "f"` for a UF symbol the
636+
walker resolves through `sexpToExpr`'s context lookup, but a
637+
higher-order head (an applied list) is also accepted as long
638+
as both sides agree structurally. Arity mismatch or differing
639+
heads throw a clear error. -/
640+
private def elabCong (ctx : WalkerContext) (s : Step)
641+
: WalkerM (Expr × List Sexp) := do
642+
match s.clause, s.premises with
643+
| [.list [.atom "=", .list (fA :: argsA), .list (fB :: argsB)]],
644+
some pids => do
645+
unless fA == fB do
646+
throwError m!"alethe walker: 'cong' function heads differ: \
647+
{repr fA} vs {repr fB}"
648+
unless argsA.length == argsB.length do
649+
throwError m!"alethe walker: 'cong' arity mismatch: LHS has \
650+
{argsA.length} args, RHS has {argsB.length}"
651+
unless argsA.length == pids.length do
652+
throwError m!"alethe walker: 'cong' has {pids.length} \
653+
premises but {argsA.length} argument pairs"
654+
let fExpr ← sexpToExpr ctx fA
655+
let mut acc ← mkAppM ``Eq.refl #[fExpr]
656+
for pid in pids do
657+
let (eqProof, _) ← lookupStep pid
658+
acc ← Lean.Meta.mkCongr acc eqProof
659+
pure (acc, s.clause)
660+
| _, _ =>
661+
throwError m!"alethe walker: 'cong' expects clause \
662+
(cl (= (f …) (f …))) with a premise list, got \
663+
clause {repr s.clause}, premises {repr s.premises}"
664+
551665
/-- LIA-tautology leaf rules (`la_generic`, `la_mult_neg`). The
552666
step's clause is a linear-arithmetic tautology — its negation
553667
is LIA-unsatisfiable, with the Farkas multipliers carried in
@@ -591,16 +705,19 @@ def elabStep (ctx : WalkerContext) (s : Step) : WalkerM Unit := do
591705
| "false" => elabFalseStep ctx s
592706
| "la_generic" => elabLiaLeaf ctx s
593707
| "la_mult_neg" => elabLiaLeaf ctx s
708+
| "refl" => elabRefl ctx s
709+
| "symm" => elabSymm s
710+
| "trans" => elabTrans s
711+
| "cong" => elabCong ctx s
594712
| other =>
595713
throwError m!"alethe walker: rule '{other}' not yet \
596714
supported (current scope: resolution / or / \
597-
false / la_generic / la_mult_neg, plus \
598-
seeded assumes. Subsequent PRs add the \
599-
equality (cong / refl / trans / symm) and \
600-
boolean-cleanup (hole / rare_rewrite / \
601-
equiv_* / implies / and_neg) clusters — the \
602-
omega fallback handles full cvc5 traces in \
603-
the meantime)."
715+
false / la_generic / la_mult_neg / refl / \
716+
symm / trans / cong, plus seeded assumes. \
717+
Subsequent PRs add the boolean-cleanup \
718+
cluster (hole / rare_rewrite / equiv_* / \
719+
implies / and_neg) — the omega fallback \
720+
handles full cvc5 traces in the meantime)."
604721
storeStep s.id proof clause
605722

606723
/-- Walk an Alethe proof and return the `Expr` proving the final

lean-bridge/Test/Tactic.lean

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,86 @@ theorem alethe_walker_resolution_axiom_free
525525

526526
#print axioms alethe_walker_resolution_axiom_free
527527

528+
/- Alethe walker — equality cluster (`refl` / `symm` / `trans` /
529+
`cong`).
530+
531+
These rules are how cvc5's `alethe-2024` walks
532+
uninterpreted-function and equality reasoning. None of them
533+
touch a decision procedure (unlike `la_generic`): the proof
534+
term is reconstructed kernel-side from the premise proofs
535+
directly, so the resulting theorems are axiom-free (no
536+
`propext` / `Classical.choice`, just `Eq.rec` underneath). -/
537+
538+
/-- `refl`: a leaf rule concluding `t = t` for any `t`. No
539+
premises, just `Eq.refl t`. -/
540+
theorem alethe_walker_refl_axiom_free (a : Int) : a = a := by
541+
alethe_walker_test
542+
"( (step t0 (cl (= a a)) :rule refl) )"
543+
544+
#print axioms alethe_walker_refl_axiom_free
545+
546+
/-- `symm`: flips an equality. Premise `a = b`, conclusion
547+
`b = a`; proof term is `Eq.symm h`. -/
548+
theorem alethe_walker_symm_axiom_free
549+
(a b : Int) (h : a = b) : b = a := by
550+
alethe_walker_test
551+
"( (assume a0 (= a b)) \
552+
(step t0 (cl (= b a)) :rule symm :premises (a0)) )"
553+
554+
#print axioms alethe_walker_symm_axiom_free
555+
556+
/-- `trans`: chains equalities. Premises `a = b`, `b = c`;
557+
conclusion `a = c`; proof term is the left-fold of
558+
`Eq.trans`. Exercises the n-ary premise list. -/
559+
theorem alethe_walker_trans_axiom_free
560+
(a b c : Int) (h1 : a = b) (h2 : b = c) : a = c := by
561+
alethe_walker_test
562+
"( (assume a0 (= a b)) \
563+
(assume a1 (= b c)) \
564+
(step t0 (cl (= a c)) :rule trans :premises (a0 a1)) )"
565+
566+
#print axioms alethe_walker_trans_axiom_free
567+
568+
/-- `cong` over a unary UF symbol: `x = y ⊢ f x = f y`. The
569+
walker's generic application case translates `(f x)` /
570+
`(f y)` by looking up `f` in the local context; the proof
571+
term is `mkCongr (Eq.refl f) h` — i.e., `congrArg f h`. -/
572+
theorem alethe_walker_cong_axiom_free
573+
(f : Int → Int) (x y : Int) (h : x = y) : f x = f y := by
574+
alethe_walker_test
575+
"( (assume a0 (= x y)) \
576+
(step t0 (cl (= (f x) (f y))) :rule cong :premises (a0)) )"
577+
578+
#print axioms alethe_walker_cong_axiom_free
579+
580+
/-- `cong` over a 2-arg UF symbol: `a = c ∧ b = d ⊢ f a b = f c d`.
581+
The `mkCongr` left-fold builds the curried cascade
582+
`(f a) b = (f c) d` via two `mkCongr` steps. -/
583+
theorem alethe_walker_cong_two_arg_axiom_free
584+
(f : Int → Int → Int) (a b c d : Int)
585+
(h1 : a = c) (h2 : b = d) : f a b = f c d := by
586+
alethe_walker_test
587+
"( (assume a0 (= a c)) \
588+
(assume a1 (= b d)) \
589+
(step t0 (cl (= (f a b) (f c d))) \
590+
:rule cong :premises (a0 a1)) )"
591+
592+
#print axioms alethe_walker_cong_two_arg_axiom_free
593+
594+
/-- End-to-end UF refutation: combine `cong` with the clausal
595+
layer. From `x = y` and `f x ≠ f y`, derive `False` via
596+
`cong` (to get `f x = f y`) + `resolution` against the
597+
inequality. The walker reconstructs the full proof skeleton
598+
axiom-free — UF reasoning all the way down. -/
599+
theorem alethe_walker_cong_refutation_axiom_free
600+
(f : Int → Int) (x y : Int)
601+
(h : x = y) (hne : ¬(f x = f y)) : False := by
602+
alethe_walker_test
603+
"( (assume a0 (= x y)) \
604+
(assume a1 (not (= (f x) (f y)))) \
605+
(step t0 (cl (= (f x) (f y))) :rule cong :premises (a0)) \
606+
(step t1 (cl) :rule resolution :premises (t0 a1)) )"
607+
608+
#print axioms alethe_walker_cong_refutation_axiom_free
609+
528610
end ProofBroker.Test

tools/axiom_allowlist.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@
5858
["propext", "Classical.choice", "Quot.sound"],
5959
"ProofBroker.Test.alethe_walker_resolution_axiom_free":
6060
[],
61+
"ProofBroker.Test.alethe_walker_refl_axiom_free":
62+
[],
63+
"ProofBroker.Test.alethe_walker_symm_axiom_free":
64+
[],
65+
"ProofBroker.Test.alethe_walker_trans_axiom_free":
66+
[],
67+
"ProofBroker.Test.alethe_walker_cong_axiom_free":
68+
[],
69+
"ProofBroker.Test.alethe_walker_cong_two_arg_axiom_free":
70+
[],
71+
"ProofBroker.Test.alethe_walker_cong_refutation_axiom_free":
72+
[],
6173
"ProofBroker.TestMathlib.lra_axiom_free":
6274
["propext", "Classical.choice", "Quot.sound"],
6375
"ProofBroker.TestMathlib.pb_term_case_split_axiom_free":

0 commit comments

Comments
 (0)