@@ -198,24 +198,31 @@ structure WalkerContext where
198198 vars : NameMap Expr
199199 deriving Inhabited
200200
201- /-- Walker state: map from step id to the `Expr` proving that
202- step's clause (a disjunction of literals — `False` for the
203- empty clause). Built up by folding [ Step.elab ] over the
204- proof's step list. -/
201+ /-- Walker state: map from step id to `(proof, clause)` — the
202+ `Expr` proving that step's clause, paired with the clause's
203+ literal list (in `Sexp` form). The literal list is needed by
204+ `resolution`: pivot finding compares literals, and the
205+ `Or`-injection that builds the resolvent proof needs each
206+ leftover literal's position. For non-resolution rules the
207+ clause list is just the step's stated `clause`; for
208+ `resolution` it is the computed resolvent (equal as a set to
209+ the stated clause; literal order follows the left-fold). -/
205210structure WalkerState where
206- proven : NameMap Expr
211+ proven : NameMap ( Expr × List Sexp)
207212 deriving Inhabited
208213
209214abbrev WalkerM := StateRefT WalkerState MetaM
210215
211- private def lookupStep (id : String) : WalkerM Expr := do
216+ private def lookupStep (id : String) : WalkerM ( Expr × List Sexp) := do
212217 let st ← get
213218 match st.proven.find? (Name.mkSimple id) with
214- | some e => pure e
219+ | some pc => pure pc
215220 | none => throwError m! "alethe walker: step '{ id} ' not proven yet"
216221
217- private def storeStep (id : String) (e : Expr) : WalkerM Unit :=
218- modify fun st => { st with proven := st.proven.insert (.mkSimple id) e }
222+ private def storeStep (id : String) (e : Expr) (clause : List Sexp)
223+ : WalkerM Unit :=
224+ modify fun st =>
225+ { st with proven := st.proven.insert (.mkSimple id) (e, clause) }
219226
220227/-- Build a [ WalkerContext ] from the current goal's local
221228 context. Every named hypothesis / free var becomes an atom
@@ -353,7 +360,118 @@ partial def andOrChain (ctx : WalkerContext) (conn : Name)
353360end
354361
355362/- ----------------------------------------------------------------
356- Rule elaborators (clausal layer)
363+ Clause manipulation: negation, injection, case analysis
364+
365+ A clause `(cl L1 … Ln)` is the right-associated disjunction
366+ `L1 ∨ … ∨ Ln` (empty → `False`, singleton → `L1`). These
367+ helpers build and destruct that structure as Lean proof
368+ terms; they are the substrate `resolution` is built on.
369+ ---------------------------------------------------------------- -/
370+
371+ /-- Sexp-level literal negation: `(not X)` ↦ `X`, `X` ↦
372+ `(not X)`. Involutive, so two literals are complementary iff
373+ `negateLit a == b`. -/
374+ def negateLit : Sexp → Sexp
375+ | .list [.atom "not" , x] => x
376+ | other => .list [.atom "not" , other]
377+
378+ /-- True iff the literal is syntactically `(not _)`. Picks which
379+ side of a complementary pair carries the `Not` (i.e. is the
380+ function in the `Not p` ≡ `p → False` application). -/
381+ def isNotForm : Sexp → Bool
382+ | .list [.atom "not" , _] => true
383+ | _ => false
384+
385+ /-- The clause-as-Prop of a literal list (`(cl …)` semantics:
386+ empty → `False`, singleton → the literal, n-ary → the
387+ right-associated `∨`). -/
388+ def clauseTypeOf (ctx : WalkerContext) (lits : List Sexp) : MetaM Expr :=
389+ sexpToExpr ctx (Sexp.list (.atom "cl" :: lits))
390+
391+ /-- Given a proof of `target[idx]`, build a proof of the whole
392+ clause `⋁target` via the right `Or.inl`/`Or.inr` chain. -/
393+ partial def injectLit (ctx : WalkerContext) (target : List Sexp)
394+ (idx : Nat) (litProof : Expr) : MetaM Expr := do
395+ match target, idx with
396+ | [_], 0 => pure litProof
397+ | (l :: rest), 0 => do
398+ let aTy ← sexpToExpr ctx l
399+ let bTy ← clauseTypeOf ctx rest
400+ mkAppOptM ``Or.inl #[some aTy, some bTy, some litProof]
401+ | (l :: rest), (k + 1 ) => do
402+ let aTy ← sexpToExpr ctx l
403+ let bTy ← clauseTypeOf ctx rest
404+ let inner ← injectLit ctx rest k litProof
405+ mkAppOptM ``Or.inr #[some aTy, some bTy, some inner]
406+ | _, _ =>
407+ throwError m! "alethe walker: injectLit index { idx} out of \
408+ range for a { target.length} -literal clause"
409+
410+ /-- Case-analyse `clauseProof : ⋁lits`. For each disjunct, call
411+ `handler idx litProof` (which must return a proof of
412+ `resultTy`); chain the cases with `Or.elim`. -/
413+ partial def casesClause (ctx : WalkerContext) (clauseProof : Expr)
414+ (lits : List Sexp) (resultTy : Expr)
415+ (handler : Nat → Expr → MetaM Expr) : MetaM Expr := do
416+ match lits with
417+ | [] => throwError "alethe walker: casesClause on an empty clause"
418+ | [_] => handler 0 clauseProof
419+ | (l :: rest) => do
420+ let lTy ← sexpToExpr ctx l
421+ let restTy ← clauseTypeOf ctx rest
422+ let lamL ← withLocalDeclD `hl lTy fun hl => do
423+ mkLambdaFVars #[hl] (← handler 0 hl)
424+ let lamR ← withLocalDeclD `hr restTy fun hr => do
425+ let body ← casesClause ctx hr rest resultTy
426+ (fun i p => handler (i + 1 ) p)
427+ mkLambdaFVars #[hr] body
428+ mkAppOptM ``Or.elim
429+ #[some lTy, some restTy, some resultTy,
430+ some clauseProof, some lamL, some lamR]
431+
432+ /-- Binary clausal resolution. `(eA : ⋁A)` and `(eB : ⋁B)` must
433+ contain a complementary literal pair (the pivot). Produces
434+ `(proof, R)` with `R = (A∖pivot) ++ (B∖pivot)` and
435+ `proof : ⋁R`. The proof case-splits `eA`: the pivot disjunct
436+ case-splits `eB` and closes the complementary pair with
437+ `False.elim`; every non-pivot disjunct is injected into `R`
438+ at its post-erasure position. Throws if no pivot exists. -/
439+ def binaryResolve (ctx : WalkerContext)
440+ (eA : Expr) (A : List Sexp) (eB : Expr) (B : List Sexp)
441+ : MetaM (Expr × List Sexp) := do
442+ let pivot? : Option (Nat × Nat) := Id.run do
443+ for i in [0 :A.length] do
444+ for j in [0 :B.length] do
445+ if negateLit A[i]! == B[j]! then
446+ return some (i, j)
447+ return none
448+ match pivot? with
449+ | none =>
450+ throwError m! "alethe walker: resolution premises share no \
451+ complementary literal — no pivot"
452+ | some (i, j) => do
453+ let aIsNot := isNotForm A[i]!
454+ let R := A.eraseIdx i ++ B.eraseIdx j
455+ let resultTy ← clauseTypeOf ctx R
456+ let aLen1 := A.length - 1
457+ let proof ← casesClause ctx eA A resultTy (fun i' hA' => do
458+ if i' == i then
459+ casesClause ctx eB B resultTy (fun j' hB' => do
460+ if j' == j then
461+ -- complementary pair: the `Not`-side applied to the
462+ -- other gives `False`, eliminated into `resultTy`.
463+ let falseProof :=
464+ if aIsNot then mkApp hA' hB' else mkApp hB' hA'
465+ mkAppOptM ``False.elim #[some resultTy, some falseProof]
466+ else
467+ let pos := aLen1 + (if j' < j then j' else j' - 1 )
468+ injectLit ctx R pos hB')
469+ else
470+ injectLit ctx R (if i' < i then i' else i' - 1 ) hA')
471+ return (proof, R)
472+
473+ /- ----------------------------------------------------------------
474+ Rule elaborators
357475 ---------------------------------------------------------------- -/
358476
359477/-- An Alethe top-level `(assume id L)`: the proof of `L` is the
@@ -379,67 +497,56 @@ private def elabAssumeLiteral (ctx : WalkerContext) (id : String)
379497 throwError m! "alethe walker: assume '{ id} ' states { stmt} , \
380498 but no local hypothesis matches that type"
381499
382- /-- `(cl)` from a `false`-rule step. Alethe's `(step _ (cl (not false)) :rule false)`
383- is the standard premise for the final empty-cl resolution. The
384- proof of `(cl (not false))` is `(fun (h : False) => h) : ¬False`. -/
385- private def elabFalseStep (_ctx : WalkerContext) (s : Step) : WalkerM Expr := do
500+ /-- `(cl (not false))` from a `false`-rule step — the standard
501+ premise for the final empty-cl resolution. The proof of
502+ `¬False` (≡ `False → False`) is the identity. -/
503+ private def elabFalseStep (_ctx : WalkerContext) (s : Step)
504+ : WalkerM (Expr × List Sexp) := do
386505 match s.clause with
387506 | [.list [.atom "not" , .atom "false" ]] =>
388- -- ¬False ≡ False → False ≡ id : False → False
389507 let falseExpr := mkConst ``False
390- return .lam .anonymous falseExpr (.bvar 0 ) .default
508+ pure ( .lam .anonymous falseExpr (.bvar 0 ) .default, s.clause)
391509 | _ =>
392510 throwError m! "alethe walker: 'false' rule expects clause \
393511 (cl (not false)), got { repr s.clause} "
394512
395- /-- `or` rule: from a single premise that is the n-ary `or` of
396- literals, produce the same clause. Alethe's `or` rule
397- decomposes a non-clausal Prop of the form `(or L1 L2 ... Ln)`
398- into the clause `(cl L1 L2 ... Ln)`. Logically the
399- proposition is unchanged (clause IS disjunction at the Prop
400- level under our `cl` encoding), so the elaborator just
401- returns the premise expression with the same type. -/
402- private def elabOr (s : Step) : WalkerM Expr := do
513+ /-- `or` rule: restates a single premise — whose one literal is
514+ an n-ary `(or L1 … Ln)` — as the {e clause} `(cl L1 … Ln)`.
515+ The Prop is unchanged (`clauseTypeOf [(or L1 … Ln)]` and
516+ `clauseTypeOf [L1, …, Ln]` are the same right-associated
517+ `∨`), so the elaborator forwards the premise's proof term but
518+ swaps in the step's own flattened literal list — that
519+ re-grouping is exactly what lets `resolution` peel the
520+ individual literals afterwards. -/
521+ private def elabOr (s : Step) : WalkerM (Expr × List Sexp) := do
403522 match s.premises with
404- | some [p] => lookupStep p
523+ | some [p] => do
524+ let (proof, _) ← lookupStep p
525+ pure (proof, s.clause)
405526 | _ =>
406527 throwError m! "alethe walker: 'or' rule expects exactly one \
407528 premise, got { repr s.premises} "
408529
409- /-- `resolution`: clausal resolution between premise clauses.
410- Alethe's `resolution` is multi-way (resolves over multiple
411- pivot literals in one step). M1.β implements the binary
412- case (two premises) for clauses with one or two literals
413- each — the most common shape in the boolean-cleanup steps
414- that close cvc5 LIA proofs. Larger resolutions surface as
415- `throwError`, and the omega fallback runs. -/
416- private def elabResolution (s : Step) : WalkerM Expr := do
530+ /-- `resolution`: n-ary clausal resolution. Alethe's `resolution`
531+ is a left-fold of binary resolutions over the premise list;
532+ each binary step cancels one complementary literal pair
533+ (`binaryResolve` finds the pivot — cvc5 does not list pivots
534+ explicitly). The result `(proof, clause)` carries the
535+ computed resolvent; for a closing step the resolvent is the
536+ empty clause and the proof has type `False`. -/
537+ private def elabResolution (ctx : WalkerContext) (s : Step)
538+ : WalkerM (Expr × List Sexp) := do
417539 match s.premises with
418- | some [p1, p2] => do
419- let e1 ← lookupStep p1
420- let e2 ← lookupStep p2
421- -- For M1.β we only handle the simplest case: one premise
422- -- has type `L`, the other has type `¬L`, and we derive
423- -- `False`. This corresponds to the final closing step of
424- -- many cvc5 proofs (`(step closing (cl) :rule resolution
425- -- :premises (some_L some_neg_L))`).
426- let t1 ← inferType e1
427- let t2 ← inferType e2
428- -- Try e1 : L, e2 : L → False
429- let negT1 := mkApp (mkConst ``Not) t1
430- if ← isDefEq t2 negT1 then
431- return mkApp e2 e1
432- -- Or e2 : L, e1 : L → False
433- let negT2 := mkApp (mkConst ``Not) t2
434- if ← isDefEq t1 negT2 then
435- return mkApp e1 e2
436- throwError m! "alethe walker: resolution between { t1} and { t2} \
437- is not the simple ¬-elimination shape M1.β \
438- handles (multi-literal clauses + pivot \
439- selection are M1.γ scope)."
540+ | some (p0 :: rest) => do
541+ let (e0, c0) ← lookupStep p0
542+ let mut acc : Expr × List Sexp := (e0, c0)
543+ for pi in rest do
544+ let (ei, ci) ← lookupStep pi
545+ acc ← binaryResolve ctx acc.1 acc.2 ei ci
546+ return acc
440547 | _ =>
441- throwError m! "alethe walker: 'resolution' M1.β scope handles \
442- exactly 2 premises , got { repr s.premises} "
548+ throwError m! "alethe walker: 'resolution' needs at least one \
549+ premise , got { repr s.premises} "
443550
444551/-- LIA-tautology leaf rules (`la_generic`, `la_mult_neg`). The
445552 step's clause is a linear-arithmetic tautology — its negation
@@ -457,7 +564,8 @@ private def elabResolution (s : Step) : WalkerM Expr := do
457564 mvar of the clause type, `falseOrByContra` into a `False`
458565 goal, then the `MetaM`-level `omega` entry over the local
459566 hypotheses. -/
460- private def elabLiaLeaf (ctx : WalkerContext) (s : Step) : WalkerM Expr := do
567+ private def elabLiaLeaf (ctx : WalkerContext) (s : Step)
568+ : WalkerM (Expr × List Sexp) := do
461569 let clauseProp ← sexpToExpr ctx (Sexp.list (.atom "cl" :: s.clause))
462570 let mvar ← mkFreshExprSyntheticOpaqueMVar clauseProp
463571 match ← mvar.mvarId!.falseOrByContra with
@@ -466,7 +574,7 @@ private def elabLiaLeaf (ctx : WalkerContext) (s : Step) : WalkerM Expr := do
466574 gFalse.withContext do
467575 let hyps := (← getLocalHyps).toList
468576 Lean.Elab.Tactic.Omega.omega hyps gFalse
469- instantiateMVars mvar
577+ return (← instantiateMVars mvar, s.clause)
470578
471579/-- Elaborate a single step: dispatch on `rule` to a per-rule
472580 elaborator, store the result under the step's `id`. Unknown
@@ -477,23 +585,23 @@ private def elabLiaLeaf (ctx : WalkerContext) (s : Step) : WalkerM Expr := do
477585 top-level command (`Proof.assumes`), seeded into the walker
478586 state by `walkProof` before the step list is walked. -/
479587def elabStep (ctx : WalkerContext) (s : Step) : WalkerM Unit := do
480- let result ← match s.rule with
588+ let (proof, clause) ← match s.rule with
481589 | "or" => elabOr s
482- | "resolution" => elabResolution s
590+ | "resolution" => elabResolution ctx s
483591 | "false" => elabFalseStep ctx s
484592 | "la_generic" => elabLiaLeaf ctx s
485593 | "la_mult_neg" => elabLiaLeaf ctx s
486594 | other =>
487595 throwError m! "alethe walker: rule '{ other} ' not yet \
488596 supported (current scope: resolution / or / \
489597 false / la_generic / la_mult_neg, plus \
490- seeded assumes. Subsequent PRs add \
491- multi-literal resolution and the equality \
492- (cong / refl / trans) + boolean-cleanup \
493- (hole / rare_rewrite / equiv_* / implies / \
494- and_neg) clusters — the omega fallback \
495- handles full cvc5 traces in the meantime)."
496- storeStep s.id result
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)."
604+ storeStep s.id proof clause
497605
498606/-- Walk an Alethe proof and return the `Expr` proving the final
499607 step's clause. For a closing alethe-2024 proof the last step
@@ -509,14 +617,16 @@ def walkProof (ctx : WalkerContext) (proof : Proof) : MetaM Expr := do
509617 let walk : WalkerM Expr := do
510618 for a in proof.assumes do
511619 let e ← elabAssumeLiteral ctx a.id a.literal
512- storeStep a.id e
620+ storeStep a.id e [a.literal]
513621 proof.steps.forM (elabStep ctx)
514622 match proof.steps.getLast? with
515623 | none =>
516624 throwError m! "alethe walker: proof has no steps — nothing to \
517625 conclude (a well-formed alethe-2024 proof ends \
518626 in an empty-clause resolution step)"
519- | some last => lookupStep last.id
627+ | some last =>
628+ let (e, _) ← lookupStep last.id
629+ pure e
520630 let (result, _) ← walk.run initial
521631 return result
522632
0 commit comments