It seems that the only thing I don’t want to use Lean for is it’s main purpose. That tracks.

Anyhoo, I’m a big fan of SMT solvers https://en.wikipedia.org/wiki/Satisfiability_modulo_theories . They are the workhouse behind 90% of automated software verification tooling. Three popular solvers probably account for a majority of usage, Z3, CVC5, and Bitwuzla.

One can use them by writing an SMTLib file and calling a subprocess of the solver.

Lean can be used in the same way now thanks to the grind tactic. I think this is an interesting angle. Not everyone is going to switch whole hog into using Lean. But by writing a short lean file analogous to an SMTLib one, you can kind of dip your toe in while staying in your language of choice (python, C++, scala, haskell, whatever). Lean offers more extensibility compared to an SMTLib and a huge library of functionality. You can also have the backstop of going interactive if you truly must. This is kind of using Lean as a Why3 https://www.why3.org/ in that regard.

Brief SMT Spiel

On initial introduction, SMT solvers are usually presented as puzzle / constraint solvers. Things like Sudoku or package versioning or n-queens can be encoded into the solver and you seek a solution (the solver returns SAT) to the given problem.

from z3 import *
# We know each queen must be in a different row.
# So, we represent each queen by a single integer: the column position
Q = [ Int(f"Q_{row + 1}") for row in range(8) ]

# Each queen is in a column {1, ... 8 }
val_c = [ And(1 <= Q[row], Q[row] <= 8) for row in range(8) ]

# At most one queen per column
col_c = [ Distinct(Q) ]

# Diagonal constraint
diag_c = [ If(i == j,
              True,
              And(Q[i] - Q[j] != i - j, Q[i] - Q[j] != j - i))
           for i in range(8) for j in range(i) ]
s = Solver()
s.add(val_c + col_c + diag_c)
print(s.check())
s.model()
sat

[Q_3 = 8, Q_1 = 4, Q_7 = 5, Q_8 = 7, Q_5 = 1, Q_4 = 6, Q_2 = 2, Q_6 = 3]

When you want to do verification or theorem proving, you are seeking counterexamples. You do this by asserting the negation of the theorem in question. The theorem being proven is getting back unsat from the solver, saying that there are no counterexamples.

s = Solver()
p,q = Bools("p q")
s.add(Not(Implies(And(p,q), p)))
print(s.check())
unsat

In addition to the bindings, you can also use a textual format, SMTLIB https://smt-lib.org/ . Making files like this can be useful for inspecting the problems and is easier than developing bindings for a new language.

%%file /tmp/test.smt2

(declare-const p Bool)
(declare-const q Bool)
(assert (not (=> (and p q) p)))
(check-sat)
Writing /tmp/test.smt2
! z3 /tmp/test.smt2
unsat

We can also dump files out of the solver using the bindings. I have found this useful because I have used systems enmeshed with z3, but want to try another solver, or want to fiddle with my encoding to see if I can make it work better without changing the code of the generator.

s = Solver()
p,q = Bools("p q")
s.add(Not(Implies(And(p,q), p)))
print(s.sexpr())
(declare-fun p () Bool)
(declare-fun q () Bool)
(assert (not (=> (and p q) p)))

Using Lean

But we can also spit out lean files.

The advent of the grind tactic https://lean-lang.org/doc/reference/4.22.0-rc2//The--grind--tactic/#grind makes this more compelling. By and large, the most powerful automation of interactive theorems provers has been made by using external solvers. It is a bit unclear if the runaround of emitting the language of the ITP is worth doing compared to just emitting the solver language (smtlib or tptp).

grind however is a native general purpose lean tactic written by Leo de Moura, one of the authors of Z3. My impression is that it is kind of like an SMT solver without the SAT part, a communicating pile of theories communicating through the fabric of an e-graph /congruence closure.

I also suspect it will deal less well than existing SMT solvers with reams of garbage like software verifiers produce. https://lean-lang.org/doc/reference/4.22.0-rc2//The--grind--tactic/What--grind--is--not-___/ But for smaller queries or more nuanced quantifier queries, maybe the different emphasis wins out. I’ve been using SMT solvers in a somewhat unusual way in my project knuckledragger and I suspect they are similarly used in projects like F* or Dafny where really they are pretty small queries with more equational content than the bulk bit blasting like queries that come out of something like a model checker.

Lean also recently gained bv_decide which is powered by the SAT solver cadical. It is actually still kind of intriguing because Lean offers modelling features that honestly would have been kind of gosh darn nice. SMTLib is both extremely rich in that it is basically a full Higher order logic, but also pretty impoverished in terms of syntactic niceties.

The BitVec type does enable you to do arbitrary size bitvector reasoning, a desired feature by compiler verifiers. I would assume this cannot be bit blasted by bv_decide however. How would you?

%%file /tmp/test.lean

theorem mytheorem (p q : Prop) : p  q  p := by grind
#check mytheorem
Overwriting /tmp/test.lean

You can see this passes in a human readable form

! lean /tmp/test.lean 
mytheorem (p q : Prop) : p ∧ q → p

Or you can use json output to make something more easily consumable by another tool.

! lean /tmp/test.lean --json
{"severity":"information","pos":{"line":3,"column":0},"kind":"[anonymous]","keepFullRange":false,"isSilent":false,"fileName":"/tmp/test.lean","endPos":{"line":3,"column":6},"data":"mytheorem (p q : Prop) : p ∧ q → p","caption":""}

This is what it looks like to try and prove a false theorem

%%file /tmp/test2.lean

theorem mytheorem (p q : Prop) : p \/ q -> p := by grind
#check mytheorem
Overwriting /tmp/test2.lean
! lean /tmp/test2.lean
/tmp/test2.lean:2:51: error: `grind` failed
case grind
p q : Prop
h : p ∨ q
h_1 : ¬p
⊢ False
[grind] Goal diagnostics
  [facts] Asserted facts
    [prop] p ∨ q
    [prop] ¬p
  [eqc] True propositions
    [prop] q
    [prop] p ∨ q
  [eqc] False propositions
    [prop] p
mytheorem (p q : Prop) : p ∨ q → p
! lean /tmp/test2.lean --json
{"severity":"error","pos":{"line":2,"column":51},"kind":"[anonymous]","keepFullRange":false,"isSilent":false,"fileName":"/tmp/test2.lean","endPos":{"line":2,"column":56},"data":"`grind` failed\ncase grind\np q : Prop\nh : p ∨ q\nh_1 : ¬p\n⊢ False\n[grind] Goal diagnostics\n  [facts] Asserted facts\n    [prop] p ∨ q\n    [prop] ¬p\n  [eqc] True propositions\n    [prop] q\n    [prop] p ∨ q\n  [eqc] False propositions\n    [prop] p","caption":""}
{"severity":"information","pos":{"line":3,"column":0},"kind":"[anonymous]","keepFullRange":false,"isSilent":false,"fileName":"/tmp/test2.lean","endPos":{"line":3,"column":6},"data":"mytheorem (p q : Prop) : p ∨ q → p","caption":""}

We can wrap this up into a Solver object if you’re willing to just inject strings into the thing. Here I’m using it in the refutational style as I did with Z3. When check is called, it just prints the negation of all of the theorems you have added.

from dataclasses import dataclass, field
import subprocess
import json
@dataclass
class LeanSolver():
    imports : list[str] = field(default_factory=list)
    asserts : list[str] = field(default_factory=list)
    tactic : str = "grind"
    def add(self, expr: str):
        self.asserts.append(expr)
    def check(self):
        with open('/tmp/test.lean', 'w') as f:
            for imp in self.imports:
                f.write(f'import {imp}\n')
            f.write("theorem mythm : Not (True ")
            for expr in self.asserts:
                f.write('  /\\ \n')
                f.write(expr)
            f.write(f') := by {self.tactic}\n')
            f.write('#check mythm\n')
            f.flush()
        result = subprocess.run(['lean', '--json', '/tmp/test.lean'], capture_output=True, text=True)
        if result.returncode != 0:
            return "unknown"
            #raise ValueError(result.stderr)
        else:
            result = json.loads(result.stdout)
            # just a little sanity checking.
            assert result["severity"] == "information"
            assert "mythm" in result["data"]
            return "unsat"
s = LeanSolver()
s.add("Not (forall x y : Nat, x + y = y + x)")
s.check()
'unsat'
! cat /tmp/test.lean
theorem mythm : Not (True   /\ 
Not (forall x y : Nat, x + y = y + x)) := by grind
#check mythm

Printing Lean out of the Z3Py AST

But really I want a drop in replacement for Z3. A interesting emergent design principle of Knuckledragger was to just use the Z3 term and formulas as my terms and formulas. To use other solvers, I just make a new pretty printer out of the Z3 AST. I’ve done this previously to emit TPTP so I can try using Vampire and E-prover and others https://github.com/philzook58/knuckledragger/blob/main/kdrag/solvers/__init__.py so I could reuse functionality from there (BaseSolver etc). By and large my conclusion thus far has been the delta in solving power compared to z3 combined with the remaining jankiness of the translation is just not worth it.

One small translational hiccup is that SMTLib represents propositions as Bool, whereas Lean uses Prop. It is a bit confusing to even navigate the conceptual differences represented by this choice. The things in Bool are true and false. The things in Prop are the propositions themselves, which in turn hold proof objects.

Some other things that are goofy in the translation

  • We need our uninterpreted sorts to have decidable equality and be inhabited
  • Accessors and recognizers are part of the theory of datatypes. Lean will not be happy about this style. I at least tried generating accessors that return a default value if you’re in the wrong constructor.

There are some ways to translate between the two in Init.Classical https://leanprover-community.github.io/mathlib4_docs/Init/Classical.html in particular propDecidable and decide (thanks to Cody Roux for the tips). The main place I think I need these is in forall and exists as I’m not sure what the computable version of them would be for infinite domains like Nat. Lean is also auto coercing between Prop and Bool for me sometimes.

The translation from smt code is at the end of the post and here https://github.com/philzook58/knuckledragger/blob/main/kdrag/printers/lean.py

from kdrag.all import *
import kdrag.printers.lean as lean
from kdrag.solvers import BaseSolver, collect_sorts, collect_decls, predefined_names
from dataclasses import dataclass, field
import subprocess
import json

class LeanSolver(BaseSolver):
    def __init__(self):
        self.imports = []
        self.tactic = "grind" # can replace by bv_decide or even tactic script. The string is just spliced in.
        super().__init__()
    def check(self):
        with open('/tmp/test.lean', 'w') as f:
            for imp in self.imports:
                f.write(f'import {imp}\n')
            f.write("set_option linter.unusedVariables false\n") # To remove an warning
            predefined = set()
            # make inductive datatype definitions
            for sort in collect_sorts(
                self.adds + [thm for thm, name in self.assert_tracks]
            ):
                if isinstance(sort, smt.DatatypeSortRef):
                    f.write(lean.of_datatype(sort))
                    f.write("open " + sort.name() + "\n")
                    for n in range(sort.num_constructors()):
                        cons = sort.constructor(n)
                        predefined.add(cons)
                        for i in range(cons.arity()):
                            f.write(lean.accessor_def(sort, n, i))   
                            predefined.add(sort.accessor(n,i))
                elif sort.name() not in self.predefined_sorts:
                    f.write(lean.sort_axiom(sort))
            # state axioms for all non predefined declarations
            for decl in collect_decls(self.adds + [thm for thm, name in self.assert_tracks]):
                if decl not in predefined and decl.name() not in predefined_names:
                    f.write(lean.decl_axiom(decl))
            # Make the actual goal
            f.write("theorem mythm : Not (True ")
            for expr in self.adds + [thm for thm, name in self.assert_tracks]:
                f.write('  /\\ \n')
                f.write(lean.of_expr(expr))
            f.write(f') := by {self.tactic}\n')
            f.write('#check mythm\n')
            f.flush()
        result = subprocess.run(['lean', '--json', '/tmp/test.lean'], capture_output=True, text=True)
        if result.returncode != 0:
            return smt.unknown
        else:
            result = json.loads(result.stdout)
            assert result["severity"] == "information"
            assert "mythm" in result["data"]
            return smt.unsat



Here’s a simple commutativity problem

x,y,z = smt.Ints("x y z")
s = LeanSolver()
s.add(smt.Not(smt.ForAll([x,y], x + y == y + x)))
assert s.check() == smt.unsat

This is what it generates. Somewhat ugly lean.

! cat /tmp/test.lean
set_option linter.unusedVariables false
theorem mythm : Not (True   /\ 
(not (Classical.propDecidable (∀ (x : Int) (y : Int), (((x : Int) + (y : Int)) = ((y : Int) + (x : Int))))).decide)) := by grind
#check mythm

But the output cleans it up a little of you want to see it.

! lean /tmp/test.lean 
mythm : ¬(True ∧ (!decide (∀ (x y : Int), x + y = y + x)) = true)

Here’s a pile of little queries just to sanity check the thing. I haven’t used it at much scale and there are definitely remaining problems.

s = LeanSolver()
x,y,z = smt.Bools("x y z")
s.add(smt.Not(smt.ForAll([x], x == x)))
assert s.check() == smt.unsat

x,y,z = smt.Ints("x y z")
s = LeanSolver()
s.add(smt.Not(smt.ForAll([x,y], x + y == y + x)))
assert s.check() == smt.unsat

s = LeanSolver()
s.add(smt.Not(smt.ForAll([x,y], x + 1 >= x)))
s.check()


s = LeanSolver()
x,y,z = smt.Bools("x y z")
s.add(smt.Not(smt.Implies(smt.And(x, y), x)))
s.check()

s = LeanSolver()
x,y,z = smt.Bools("x y z")
s.add(smt.Not(smt.Implies(smt.Or(x, y), x)))
assert s.check() != smt.unsat

s = LeanSolver()
x,y,z = smt.Bools("x y z")
s.add(smt.Not(smt.Implies(x, smt.Or(x, y, z))))
assert s.check() == smt.unsat

def testit(expr):
    s = LeanSolver()
    s.add(expr)
    res = s.check()
    assert res == smt.unsat, f"Expected unsat, got {res} for {expr}"

testit(smt.Not(smt.ForAll([x,y], x + y == y + x)))

x,y,z = smt.BitVecs("x y z", 8)
testit(smt.Not(smt.ForAll([x,y], x + y == y + x)))
testit(smt.Not(smt.ForAll([x,y], x | x == x)))


# uninterpreted functions
x,y,z = smt.Ints("x y z")
f = smt.Function("f", smt.IntSort(), smt.IntSort())
s = LeanSolver()
s.add(smt.ForAll([x], f(x) == x))
s.add(smt.Not(f(f(y)) == y))
assert s.check() == smt.unsat


# Uninterpreted sorts
S = smt.DeclareSort("S")
n = smt.Const("n", S)
f = smt.Function("f", S, S)
s = LeanSolver()
s.add(f(n) == n)
s.add(f(f(n)) != n)
assert s.check() == smt.unsat

# Inductive Datatypes
#from kdrag.theories.nat import Nat
MyNat = smt.Datatype("MyNat")
MyNat.declare("Z")
MyNat.declare("S", ("pred", MyNat))
MyNat = MyNat.create()
n = smt.Const("n", MyNat)

s = LeanSolver()

s.add(smt.Not(smt.ForAll([n], MyNat.S(n).pred == n)))
assert s.check() == smt.unsat

It’s not all rosy. You can put your foot into potholes pretty fast. Maybe there is a way to fix it. It seems like this existential goal trips it up. Maybe a little fiddling would fix it.

s = LeanSolver()
s.add(smt.Not(smt.Exists([n], n.pred == MyNat.Z))) # can't do this one?
s.check()

unknown

! cat /tmp/test.lean
set_option linter.unusedVariables false
inductive MyNat : Type where
| Z : MyNat
| S : MyNat -> MyNat
deriving BEq, Inhabited, Repr, DecidableEq, Inhabited
open MyNat

@[grind]
def pred : MyNat -> MyNat
| .S x => x 
| _ => default

theorem mythm : Not (True   /\ 
(not (Classical.propDecidable (∃ (n : MyNat), ((pred (n : MyNat)) = (Z : MyNat)))).decide)) := by grind
#check mythm
! lean /tmp/test.lean
/tmp/test.lean:14:98: error: `grind` failed
case grind
h : ∀ (x : MyNat), ¬pred x = Z
⊢ False
[grind] Goal diagnostics
  [facts] Asserted facts
    [prop] ∀ (x : MyNat), ¬pred x = Z
  [eqc] True propositions
    [prop] ∀ (x : MyNat), ¬pred x = Z
  [ematch] E-matching patterns
    [thm] h: [pred #0]
    [thm] h: [pred #0]
mythm : ¬(True ∧ (!decide (∃ n, pred n = Z)) = true)

Bits and Bobbles

Isabelle can be used a similar way, see below.

Maybe a less antisocial thing for me to do would be to add a Lean backend to PySMT. https://github.com/pysmt/pysmt . PySMT does not bring me joy, sorry.

Some smt resources

Lean-smt https://github.com/ufmg-smite/lean-smt https://arxiv.org/abs/2505.15796 must have already spanned the smt-lean gap in multiple ways. I’m trying to really go the other direction, the dual of lean-smt. I wonder if I can leverage that? Libraries are problematic in Lean and for my purposes

Lean as an smt solver.

SMT -> lean printer Wrap in solver object lemma.ipynb might have stuff

If you’re like me, maybe this is a way to dip your toe into that Lean hotness.

Maybe my story is “deep math in lean” “shallow math in knuckeldragger”

lean is a fun and natural extraction target also for knuckeldragger. I can dynlink into it to get fast execution of knuckledragger. A truly perverse architecture.

Not sure what to do about Reals. mathlib has em. Lean Std don’t. Does grind even recognize anything special?

Lean might also be useful as a well packaged termination checker, something I’ve punted on for a while. I included aprove, but never did much with it. It isn’t clear how to use aprove without exposing all definitions. Maybe that’s the thing to do.

It is nice when grind isn’t working to flip into interactive lean to try to figure out why. Sometimes it doesn’t have enough, sometimes the thing you said is wrong. I hope knuckeldragger might be useful in a similar way for SMTLIB. I needed to build by backward tactic system to understand where the solver go stumped (in addition to figuring out the write induction hypotheses and stuff) or get convinced how I stated the wrong goal.

%%file /tmp/test.lean

theorem mythm (x y : Nat) : x + y = y + x := by grind
#check mythm

Overwriting /tmp/test.lean
! lean --json /tmp/test.lean
{"severity":"information","pos":{"line":3,"column":0},"kind":"[anonymous]","keepFullRange":false,"isSilent":false,"fileName":"/tmp/test.lean","endPos":{"line":3,"column":6},"data":"mythm (x y : Nat) : x + y = y + x","caption":""}
%%file /tmp/test.lean

#eval 1 + 1

Overwriting /tmp/test.lean

It’s pretty slow. It’s dominated by lean boot time I think at least for trivial queries. Haven’t dug into depth. pypantograph or something that keeps a live lean session would be faster.

%%timeit
result = subprocess.run(['lean', '--json', '/tmp/test.lean'], capture_output=True, text=True)
196 ms ± 6.31 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%file /tmp/test.lean

def mytheorem (x y : Nat) : x + y = y + x := by grind

Overwriting /tmp/test.lean
%%timeit
result = subprocess.run(['lean', '--json', '/tmp/test.lean'], capture_output=True, text=True)
195 ms ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
result = subprocess.run(['echo', 'hello world'], capture_output=True, text=True)
1.1 ms ± 14.4 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

CompletedProcess(args=['echo', 'hello world'], returncode=0, stdout='hello world\n', stderr='')

try out some of my z3 tutorial stuff.




---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

Cell In[2], line 74
     72 testit(smt.Not(smt.ForAll([x,y], x + y == y + x)))
     73 testit(smt.Not(smt.ForAll([x,y], x | x == x)))
---> 74 testit(smt.Not(smt.ForAll([x,y], x + 1 >= x)))


Cell In[2], line 42, in testit(expr)
     40 s.add(expr)
     41 res = s.check()
---> 42 assert res == smt.unsat, f"Expected unsat, got {res} for {expr}"


AssertionError: Expected unsat, got unknown for Not(ForAll([x, y], x + 1 >= x))
! cat /tmp/test.lean
set_option linter.unusedVariables false
theorem mythm : Not (True   /\ 
(not (Classical.propDecidable (∀ (x : (BitVec 8)) (y : (BitVec 8)), ((bvadd (x : (BitVec 8)) (y : (BitVec 8))) = (bvadd (y : (BitVec 8)) (x : (BitVec 8)))))).decide)) := by grind
#check mythm

Py pantograph

Pantograph is a more complete lean binding that can take in strings. This might be a better for faster thing than opening up new processes, but otherwise for my purposes feels about the same?

https://github.com/stanford-centaur/PyPantograph

https://arxiv.org/abs/2410.16429

import nest_asyncio
nest_asyncio.apply() # some funkiness about async using in a jupyuter notebook.
from pantograph.server import Server
server = Server()#imports=['Init'])
state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
state1 = server.goal_tactic(state0, goal_id=0, tactic="intro")
print(state1)
p✝ : Prop
⊢ ∀ (q : Prop), p✝ ∨ q → q ∨ p✝
from pantograph.server import Server
server = Server()#imports=['Init'])
state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
print(state0.goals)
state1 = server.goal_tactic(state0, goal_id=0, tactic="grind")
state1.goals
server = Server()#imports=['Init'])
state0 = server.goal_start("forall (p q: Prop), Or q p -> p")
print(state0.goals)
state1 = server.goal_tactic(state0, goal_id=0, tactic="grind")
state1.goals
from kdrag.all import *
class LeanSolver():
    def __init__(self):
        self.server = Server()
        self.goals
    def add(self, goal):
        self.goals.append(goal)
    def check(self):
        state0 = self.server.goal_start(smt.Not(smt.And(self.goals)))
        try:
            state1 = self.server.goal_tactic(state0, goal_id=0, tactic="grind")
            if len(state1.goals) == 0:
                return smt.unsat
            else:
                return smt.unknown
        except Exception as e:
            return smt.unknown

A different direction to go is import of Lean theorems. Somehow convert them to smtlib

https://arxiv.org/pdf/2505.15796


Isabelle

I tried a while back top use Isabelle in a similar wya. I’m just not as familiar with it. Isabelle does have a reputation of being some of the best automation available for an ITP

isabelle process -T

! ~/Downloads/Isabelle2025_linux/Isabelle2025/bin/isabelle tptp_sledgehammer
Usage: isabelle tptp_sledgehammer TIMEOUT FILES

  Runs Sledgehammer on TPTP problems.
  Each problem is allocated at most TIMEOUT seconds.
%%file /tmp/Test.thy

theory Test
    imports Main
begin

theorem mytheorem: " 1 + 1 = 2" by auto
theorem mytheorem2: " 1 + 1 = 2" 
    sledgehammer
    by auto

end



Overwriting /tmp/Test.thy

The bootup time for Isabelle seems a bit slow (3s). Would probably want it being kept online, which complicates things.

! cd /tmp && ~/Downloads/Isabelle2025_linux/Isabelle2025/bin/isabelle process -T Test
Loading theory "Draft.Test"
Sledgehammering...
### theory "Draft.Test"
### 0.064s elapsed time, 0.100s cpu time, 0.000s GC time
*** Outer syntax error (line 12 of "/tmp/Test.thy"): command expected,
*** but symbolic identifier # (line 12 of "/tmp/Test.thy") was found
*** At command "<malformed>" (line 11 of "/tmp/Test.thy")
Exception- TOPLEVEL_ERROR raised
from kdrag.solvers import BaseSolver
class IsabelleSolver(BaseSolver):
    def __init__(self):
        super().__init__()

    def check(self):
        self.write_tptp("/tmp/isabelle.p")
        cmd = [
            "/home/philip/Downloads/Isabelle2025_linux/Isabelle2025/bin/isabelle",  "tptp_sledgehammer",
            "/tmp/isabelle.p",
        ]
        #if "timeout" in self.options:
        #    cmd.extend(["-t", str(self.options["timeout"] // 100) + "d"])
        self.res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        return self.check_tptp_status(self.res.stdout)

s = IsabelleSolver()
#x,y = smt.Ints("x y")
#s.add(smt.Not(smt.ForAll([x,y], x + y == y + x)))
p,q = smt.Bools("p q")
s.add(smt.Implies(smt.Or(p, q), smt.Or(q, p)))
s.check()
---------------------------------------------------------------------------

Exception                                 Traceback (most recent call last)

Cell In[6], line 22
     20 p,q = smt.Bools("p q")
     21 s.add(smt.Implies(smt.Or(p, q), smt.Or(q, p)))
---> 22 s.check()


Cell In[6], line 15, in IsabelleSolver.check(self)
     12 #if "timeout" in self.options:
     13 #    cmd.extend(["-t", str(self.options["timeout"] // 100) + "d"])
     14 self.res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
---> 15 return self.check_tptp_status(self.res.stdout)


File ~/Documents/python/knuckledragger/kdrag/solvers/__init__.py:433, in BaseSolver.check_tptp_status(self, res)
    431     return smt.unknown
    432 else:
--> 433     raise Exception("Unexpected return from solver", res)


Exception: ('Unexpected return from solver', b'')
from kdrag.all  import *
f = smt.Function("f", smt.IntSort(), smt.IntSort(), smt.BoolSort())
f.domain(0)

Int

from kdrag.theories.nat import Nat
dir(Nat)
Nat.accessor(1,0)

pred

A Copy of the lean printing code

This version will continue to evolve if this proves to be useful https://github.com/philzook58/knuckledragger/blob/main/kdrag/printers/lean.py

import kdrag.smt as smt
import kdrag as kd
import subprocess


def decl_sig(f: smt.FuncDeclRef) -> str:
    """
    Convert a function declaration to a Lean signature.

    >>> f = smt.Function("f", smt.IntSort(), smt.IntSort(), smt.BoolSort())
    >>> decl_sig(f)
    'f : Int -> Int -> Bool'
    """
    typs = [f.domain(n) for n in range(f.arity())] + [f.range()]
    typ = " -> ".join(map(of_sort, typs))
    return f"{f.name()} : {typ}"


def decl_axiom(f: smt.FuncDeclRef) -> str:
    """
    Convert a function declaration to a Lean axiom definition.

    >>> f = smt.Function("f", smt.IntSort(), smt.IntSort(), smt.BoolSort())
    >>> decl_axiom(f)
    'axiom f : Int -> Int -> Bool\\n'
    """
    return f"axiom {decl_sig(f)}\n"


def sort_axiom(s: smt.SortRef) -> str:
    """
    Convert uninterpreted sort to a Lean axiom definition.
    """
    name = s.name()
    assert name not in ["Bool", "Int"]
    return f"""
axiom {name} : Type
axiom {name}_Inhabited : Inhabited {name}
axiom {name}_BEq : BEq {name}
axiom {name}_DecidableEq : DecidableEq {name}

"""


def of_sort(s: smt.SortRef) -> str:
    """
    Convert a sort to a Lean type.

    >>> of_sort(smt.BoolSort())
    'Bool'
    >>> of_sort(smt.BitVecSort(8))
    '(BitVec 8)'
    >>> of_sort(smt.ArraySort(smt.BitVecSort(8), smt.BitVecSort(16)))
    '((BitVec 8) -> (BitVec 16))'
    """
    if s == smt.BoolSort():
        return "Bool"
    elif isinstance(s, smt.BitVecSortRef):
        return f"(BitVec {s.size()})"
    elif s == smt.IntSort():
        return "Int"
    elif s == smt.StringSort():
        return "String"
    elif isinstance(s, smt.SeqSortRef):
        return f"(Array {of_sort(s.basis())})"
    elif isinstance(s, smt.ArraySortRef):
        # TODO: multi arity
        return f"({of_sort(s.domain())} -> {of_sort(s.range())})"
    else:
        return s.name()
        # raise NotImplementedError(f"Cannot convert {s} to Lean type")


def of_datatype(dt: smt.DatatypeSortRef) -> str:
    """
    Convert a datatype to a Lean inductive type definition.

    >>> Nat = smt.Datatype("Nat")
    >>> Nat.declare("Zero")
    >>> Nat.declare("Succ", ("pred", Nat))
    >>> Nat = Nat.create()
    >>> of_datatype(Nat)
    'inductive Nat : Type where\\n| Zero : Nat\\n| Succ : Nat -> Nat\\nderiving BEq, Inhabited, Repr, DecidableEq\\n'
    """
    name = dt.name()
    output = [f"inductive {name} : Type where"]
    for n in range(dt.num_constructors()):
        cons = dt.constructor(n)
        output.append(f"| {decl_sig(cons)}")
    output.append("deriving BEq, Inhabited, Repr, DecidableEq, Inhabited\n")
    return "\n".join(output)


def accessor_def(dt: smt.DatatypeSortRef, n, i) -> str:
    """
    Make a lean definition that matches accessor, otherwise returns default.
    This might not be a perfect translation of accessor behavior in SMTLIB
    """

    cons = dt.constructor(n)
    acc = dt.accessor(n, i)
    pargs = " ".join(["_" if j != i else "x" for j in range(cons.arity())])
    return f"""
@[grind]
def {decl_sig(acc)}
| .{dt.constructor(n).name()} {pargs} => x 
| _ => default

"""


def of_expr(e: smt.ExprRef):
    """

    >>> x,y,z = smt.Ints("x y z")
    >>> of_expr(x)
    '(x : Int)'
    >>> of_expr(x + y + z)
    '(((x : Int) + (y : Int)) + (z : Int))'
    >>> of_expr(smt.If(x == x, y, z))
    '(if ((x : Int) = (x : Int)) then (y : Int) else (z : Int))'
    """
    if isinstance(e, smt.QuantifierRef):
        vs, body = kd.utils.open_binder_unhygienic(e)
        vs = " ".join([f"({v.decl().name()} : {of_sort(v.sort())})" for v in vs])
        body = of_expr(body)
        if e.is_forall():
            return f"(Classical.propDecidable (∀ {vs}, {body})).decide"
        elif e.is_exists():
            return f"(Classical.propDecidable (∃ {vs}, {body})).decide"
        elif e.is_lambda():
            return f"(λ {vs}, {body})"
        else:
            raise NotImplementedError(
                "Cannot convert unknown quantifier to Lean expression."
            )
    if isinstance(e, smt.IntNumRef):
        return str(e.as_long())
    elif isinstance(e, smt.BitVecNumRef):
        return f"{e.as_long()}#{e.size()}"
    elif smt.is_app(e):
        decl = e.decl()
        name = decl.name()
        args = [of_expr(arg) for arg in e.children()]
        if smt.is_select(e):
            assert len(args) == 2
            return f"({args[0]} {args[1]})"
        # special case store? fun k -> if k = v then d else a k
        elif smt.is_if(e):
            return f"(if {args[0]} then {args[1]} else {args[2]})"
        elif len(args) == 0:
            return f"({name} : {of_sort(e.sort())})"
        elif name == "distinct":
            assert len(args) == 2
            return f"(Not ({args[0]} = {args[1]}))"
        elif name == "=>":
            assert len(args) == 2
            return f"(not {args[0]} || {args[1]})"
        elif name == "or":
            return f"({' || '.join(args)})"
        elif name == "and":
            return f"({' && '.join(args)})"
        elif name == "bvand":
            return f"({' &&& '.join(args)})"
        elif name == "bvor":
            return f"({' ||| '.join(args)})"
        elif name == "bvadd":
            return f"({' + '.join(args)})"
        elif name == "bvsub":
            return f"({' - '.join(args)})"
        elif name == "bvmul":
            return f"({' * '.join(args)})"
        elif not name[0].isalpha() and len(args) == 2:
            return f"({args[0]} {name} {args[1]})"
        else:
            return f"({decl.name()} {' '.join(args)})"
    else:
        raise NotImplementedError(f"Cannot convert {e} to Lean expression. ", e)


def run_lean(filename: str):
    return subprocess.run(["lean", filename], check=True, capture_output=True)