Skip to content

Commit 535adf0

Browse files
authored
Reconstruct data types and apply initial changes (ufmg-smite#221)
* datatypes * changes * reconstruction * all changes and reconstruction * string tests * updates
1 parent f58d19d commit 535adf0

12 files changed

Lines changed: 375 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/.lake
2+
/.claude

Smt.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Authors: Abdalrhman Mohamed
88
import Smt.BitVec
99
import Smt.Bool
1010
import Smt.Builtin
11+
import Smt.Datatype
1112
import Smt.Int
1213
import Smt.Nat
1314
import Smt.Options

Smt/Datatype.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/-
2+
Copyright (c) 2021-2023 by the authors listed in the file AUTHORS and their
3+
institutional affiliations. All rights reserved.
4+
Released under Apache 2.0 license as described in the file LICENSE.
5+
Authors: Abdalrhman Mohamed
6+
-/
7+
8+
import Smt.Translate.Datatype
9+
import Smt.Reconstruct.Datatype

Smt/Reconstruct/Datatype.lean

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/-
2+
Copyright (c) 2021-2023 by the authors listed in the file AUTHORS and their
3+
institutional affiliations. All rights reserved.
4+
Released under Apache 2.0 license as described in the file LICENSE.
5+
Authors: Harun Khan
6+
-/
7+
8+
import Smt.Reconstruct
9+
10+
namespace Smt.Reconstruct.Datatype
11+
12+
open Lean Meta Qq
13+
14+
/-- Names of types that already have dedicated sort/term/proof reconstructors
15+
and should not be handled by the generic datatype reconstructor. -/
16+
private def builtinTypeNames : Std.HashSet Name :=
17+
Std.HashSet.ofList [
18+
``Bool, `Prop, ``True, ``False,
19+
``Or, ``And, ``Iff, ``Exists,
20+
``Nat, ``Int, `Rat, `Real,
21+
``String, ``BitVec
22+
]
23+
24+
/-- Return `true` when the name (or its prefix for a constructor) belongs to a
25+
type with dedicated built-in reconstruction support. -/
26+
private def isBuiltinName (n : Name) : Bool :=
27+
builtinTypeNames.contains n ||
28+
-- Also catch constructor names like `Bool.true`, `Nat.zero`, etc.
29+
match n with
30+
| .str p _ => builtinTypeNames.contains p
31+
| _ => false
32+
33+
-- Strip SMT-LIB2 pipe quoting (|name|) from a symbol if present.
34+
private def stripSMTPipes (s : String) : String :=
35+
if s.startsWith "|" && s.endsWith "|" then (s.drop 1 |>.dropEnd 1).toString else s
36+
37+
private def getFVarOrConstExpr! (n : String) : ReconstructM Expr := do
38+
match (← read).userNames[n]? with
39+
| some e => return e
40+
| none => match (← getLCtx).findFromUserName? n.toName with
41+
| some d => return d.toExpr
42+
| none =>
43+
let c ← getConstInfo n.toName
44+
return .const c.name (c.numLevelParams.repeat (.zero :: ·) [])
45+
46+
@[smt_sort_reconstruct] def reconstructDatatypeSort : SortReconstructor := fun s => do
47+
match s.getKind! with
48+
| .DATATYPE_SORT =>
49+
let name := s.getDatatype!.getName!
50+
if isBuiltinName name.toName then return none
51+
getFVarOrConstExpr! name
52+
| _ => return none
53+
54+
@[smt_term_reconstruct] def reconstructDatatypeTerm : TermReconstructor := fun t => do
55+
match t.getKind! with
56+
| .APPLY_CONSTRUCTOR =>
57+
-- t[0]! is the constructor symbol. It has INTERNAL_KIND in cvc5 (even for non-zero-arity),
58+
-- so we look it up by name instead of calling reconstructTerm recursively.
59+
-- toString may include SMT-LIB pipe escaping (e.g., "|mynat'.succ|"), so strip it.
60+
let ctorName := stripSMTPipes t[0]!.toString
61+
if isBuiltinName ctorName.toName then return none
62+
let mut curr ← getFVarOrConstExpr! ctorName
63+
for i in [1:t.getNumChildren] do
64+
curr := .app curr (← reconstructTerm t[i]!)
65+
return curr
66+
| _ => return none
67+
68+
/-- Build a Lean proof of `(t = s) = False` where `t` and `s` are
69+
distinct constructors of the same inductive type. Uses the `noConfusion`
70+
principle that Lean auto-generates for every inductive type. -/
71+
private def proveConsClash (t s : Expr) : MetaM Expr := do
72+
let α ← inferType t
73+
let .const αName _ := α.getAppFn
74+
| throwError "Smt.Reconstruct.Datatype: expected inductive type, got {α}"
75+
let ncName := αName ++ `noConfusion
76+
let heq ← mkEq t s
77+
-- Forward: (t = s) → False via noConfusion with motive False
78+
let fwd ← withLocalDeclD `h heq fun h => do
79+
let nc ← mkAppOptM ncName #[some (mkConst ``False), some t, some s, some h]
80+
mkLambdaFVars #[h] nc
81+
-- Backward: False → (t = s) via False.elim
82+
let bwd ← withLocalDeclD `h (mkConst ``False) fun h => do
83+
let fe ← mkAppOptM ``False.elim #[some heq, some h]
84+
mkLambdaFVars #[h] fe
85+
let iff_pf ← mkAppM ``Iff.intro #[fwd, bwd]
86+
mkAppM ``propext #[iff_pf]
87+
88+
def reconstructRewrite (pf : cvc5.Proof) : ReconstructM (Option Expr) := do
89+
match pf.getRewriteRule! with
90+
| .MACRO_DT_CONS_EQ
91+
| .DT_CONS_EQ_CLASH =>
92+
let result := pf.getResult
93+
-- Only handle the "clash" case: (t = s) = false (distinct constructors)
94+
if result[1]!.getKind! == .CONST_BOOLEAN && !result[1]!.getBooleanValue! then
95+
let sort := result[0]![0]!.getSort!
96+
if sort.getKind! == .DATATYPE_SORT && isBuiltinName sort.getDatatype!.getName!.toName then
97+
return none
98+
let (u, (α : Q(Sort u))) ← reconstructSortLevelAndSort sort
99+
let t : Q($α) ← reconstructTerm result[0]![0]!
100+
let s : Q($α) ← reconstructTerm result[0]![1]!
101+
addTac q(($t = $s) = False) fun mv => do
102+
let proof ← mv.withContext (proveConsClash t s)
103+
mv.assign proof
104+
else
105+
return none
106+
| _ => return none
107+
108+
/-- Build a Lean proof of `¬(t = s)` where `t` and `s` are distinct constructors.
109+
Uses the `noConfusion` principle: `fun h : t = s => noConfusion False t s h`. -/
110+
private def proveDistinctValues (t s : Expr) : MetaM Expr := do
111+
let α ← inferType t
112+
let .const αName _ := α.getAppFn
113+
| throwError "Smt.Reconstruct.Datatype: expected inductive type, got {α}"
114+
let ncName := αName ++ `noConfusion
115+
let heq ← mkEq t s
116+
withLocalDeclD `h heq fun h => do
117+
let nc ← mkAppOptM ncName #[some (mkConst ``False), some t, some s, some h]
118+
mkLambdaFVars #[h] nc
119+
120+
@[smt_proof_reconstruct] def reconstructDatatypeProof : ProofReconstructor := fun pf => do
121+
match pf.getRule with
122+
| .DSL_REWRITE
123+
| .THEORY_REWRITE => reconstructRewrite pf
124+
| .DISTINCT_VALUES =>
125+
let sort := pf.getArguments[0]!.getSort!
126+
if sort.getKind! == .DATATYPE_SORT && isBuiltinName sort.getDatatype!.getName!.toName then
127+
return none
128+
let (u, (α : Q(Sort u))) ← reconstructSortLevelAndSort sort
129+
let t : Q($α) ← reconstructTerm pf.getArguments[0]!
130+
let s : Q($α) ← reconstructTerm pf.getArguments[1]!
131+
addTac q($t ≠ $s) fun mv => do
132+
let proof ← mv.withContext (proveDistinctValues t s)
133+
mv.assign proof
134+
| _ => return none
135+
136+
end Smt.Reconstruct.Datatype

Smt/Reconstruct/UF.lean

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ def getFVarOrConstExpr! (n : String) : ReconstructM Expr := do
2626
| .UNINTERPRETED_SORT => getFVarOrConstExpr! s.getSymbol!
2727
| _ =>
2828
if s.isInstantiated then
29-
let base ← reconstructSort s.getUninterpretedSortConstructor!
30-
let params ← s.getInstantiatedParameters!.mapM reconstructSort
31-
return some (mkAppN base params)
29+
if let some ctor := s.getUninterpretedSortConstructor? then
30+
let base ← reconstructSort ctor
31+
let params ← s.getInstantiatedParameters!.mapM reconstructSort
32+
return some (mkAppN base params)
33+
else
34+
return none
3235
else
3336
return none
3437

Smt/Tactic/Smt.lean

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,19 @@ macro "smt_show " c:optConfig h:smtHints : tactic => do
227227

228228
declare_config_elab elabConfig Smt.Config
229229

230+
/-- If `nm` names a non-Prop definition with auto-generated equation lemmas `nm.eq_1`, `nm.eq_2`, …,
231+
return those as expressions. Returns an empty array for theorems/props or missing lemmas. -/
232+
private def getEqLemmas (nm : Name) : MetaM (Array Expr) := do
233+
let env ← getEnv
234+
let some info := env.find? nm | return #[]
235+
-- Only expand non-Prop definitions (i.e., functions, not theorems)
236+
if info.type.isProp then return #[]
237+
-- Use Lean's proper API to retrieve the auto-generated equation lemmas.
238+
let some eqNames ← Lean.Meta.getEqnsFor? nm | return #[]
239+
return eqNames.map fun eqNm =>
240+
let lvls := (env.find? eqNm).map (·.levelParams.map .param) |>.getD []
241+
.const eqNm lvls
242+
230243
def elabSmtHintElem : TSyntax ``smtHintElem → TacticM (Array (Expr × (TSyntax ``smtHintElem)) × Array Expr)
231244
| `(smtHintElem| *) => do
232245
let fvs ← Smt.Preprocess.getPropHyps
@@ -243,6 +256,22 @@ def elabSmtHintElem : TSyntax ``smtHintElem → TacticM (Array (Expr × (TSyntax
243256
`(smtHintElem| *)
244257
return (hs.zip ss, hs)
245258
| `(smtHintElem| $h:term) => do
259+
-- If the hint is a bare identifier naming a non-Prop definition, expand it to its
260+
-- auto-generated equation lemmas (nm.eq_1, nm.eq_2, …) automatically.
261+
let eqExprs ← do
262+
if h.raw.isIdent then
263+
let nm := h.raw.getId
264+
let env ← getEnv
265+
if let some info := env.find? nm then
266+
if !info.type.isProp then
267+
getEqLemmas nm
268+
else pure #[]
269+
else pure #[]
270+
else pure #[]
271+
if !eqExprs.isEmpty then
272+
let pairs ← eqExprs.mapM fun e => return (e, ← `(smtHintElem| $h:term))
273+
return (pairs, eqExprs)
274+
-- Fall through: treat as a regular lemma (prop hypothesis or theorem).
246275
let h' ← Auto.Prep.elabLemma h (.leaf s!"❰{h}❱")
247276
return (#[(h'.proof, ← `(smtHintElem| $h:term))], #[h'.proof])
248277
| _ => throwUnsupportedSyntax

Smt/Translate/Commands.lean

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ inductive Command where
2020
| declareSort (nm : String) (arity : Nat)
2121
| defineSort (nm : String) (ps : List Term) (tm : Term)
2222
| declare (nm : String) (st : Term)
23+
| declareDatatypes (sorts : List (String × Nat))
24+
(dtypes : List (List (String × List (String × Term))))
2325
| defineFun (nm : String) (ps : List (String × Term)) (cod : Term) (tm : Term) (rec : Bool)
2426
| assert (tm : Term)
2527
| checkSat
@@ -52,6 +54,18 @@ protected def toSexp : Command → Sexp
5254
| .declare nm st => sexp!{(declare-const {quoteSymbol nm} {st})}
5355
| .declareSort nm arity =>
5456
sexp!{(declare-sort {quoteSymbol nm} {toString arity})}
57+
| .declareDatatypes sorts dtypes =>
58+
let sortDecls : List Sexp :=
59+
sorts.map fun (nm, ar) => sexp!{({quoteSymbol nm} {toString ar})}
60+
let dtypeDecls : List Sexp :=
61+
dtypes.map fun ctors =>
62+
let ctorDecls : List Sexp :=
63+
ctors.map fun (ctorNm, fields) =>
64+
let flds : List Sexp :=
65+
fields.map fun (fnm, fsort) => sexp!{({quoteSymbol fnm} {fsort})}
66+
sexp!{({quoteSymbol ctorNm} ...{flds})}
67+
sexp!{(...{ctorDecls})}
68+
sexp!{(declare-datatypes (...{sortDecls}) (...{dtypeDecls}))}
5569
| .defineSort nm ps tm =>
5670
sexp!{(define-sort {quoteSymbol nm} (...{ps.map toSexp}) {tm})}
5771
| .defineFun nm ps cod tm false =>

Smt/Translate/Datatype.lean

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/-
2+
Copyright (c) 2021-2022 by the authors listed in the file AUTHORS and their
3+
institutional affiliations. All rights reserved.
4+
Released under Apache 2.0 license as described in the file LICENSE.
5+
Authors: Abdalrhman Mohamed, Wojciech Nawrocki
6+
-/
7+
8+
import Smt.Translate
9+
import Smt.Util
10+
11+
namespace Smt.Translate.Datatype
12+
13+
open Translator Term Lean
14+
15+
/-- Inductive types that have dedicated translators and should not be handled
16+
as generic SMT-LIB datatypes. -/
17+
private def builtinInductives : Std.HashSet Name :=
18+
Std.HashSet.ofList [
19+
``Bool, ``True, ``False,
20+
``Or, ``And, ``Iff, ``Exists,
21+
``Nat, ``Int, ``String, ``BitVec
22+
]
23+
24+
/-- Translate a constructor of a simple (non-parametric) inductive type.
25+
The inductive type itself is marked as a dependency so that the query builder will emit a
26+
`declare-datatypes` command for it. Constructor arguments are translated recursively.
27+
28+
Constructors of parametric inductives, indexed inductives, or inductives whose sort name is
29+
already known to SMT-LIB (e.g. `Bool`) are left to other translators or the fallthrough
30+
mechanism. Only fully applied constructors are handled. -/
31+
@[smt_translate] def translateConstructor : Translator := fun e => do
32+
let some (v, args) ← Lean.Meta.constructorApp? e | return none
33+
let env ← getEnv
34+
let inductName := v.induct
35+
-- Skip types that SMT-LIB already knows about or that have dedicated translators.
36+
if Util.smtConsts.contains inductName.toString then return none
37+
if builtinInductives.contains inductName then return none
38+
-- Only handle simple non-parametric, non-indexed inductives.
39+
let some (.inductInfo iVal) := env.find? inductName | return none
40+
if iVal.numParams != 0 || iVal.numIndices != 0 then return none
41+
-- Only handle fully applied constructors.
42+
if args.size != v.numFields then return none
43+
-- Mark the inductive type as a dependency; the query builder will declare it.
44+
modify fun st => { st with depConstants := st.depConstants.insert inductName }
45+
-- Translate constructor arguments and build the application.
46+
let translatedArgs ← args.mapM applyTranslators!
47+
return some (translatedArgs.foldl appT (symbolT v.name.toString))
48+
49+
end Smt.Translate.Datatype

Smt/Translate/Query.lean

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,53 @@ def addDefineCommandFor (nm : String) (e : Expr) (params : Array Expr) (cod : Ex
135135
def addDeclareCommandFor (nm : String) (e tp : Expr) (params : Array Expr) (cod : Expr)
136136
: QueryBuilderM (Array Expr) := do
137137
if cod.isSort && !cod.isProp then
138+
-- For simple (non-parametric) inductive types, emit a
139+
-- `declare-datatypes` command so the solver has full algebraic datatype support.
140+
if params.isEmpty then
141+
if let some cname := e.constName? then
142+
if let some (.inductInfo iVal) := (← getEnv).find? cname then
143+
if iVal.numParams == 0 && iVal.numIndices == 0 then
144+
-- Build the list of constructor declarations with field selectors.
145+
let mut allDeps : Array Expr := #[]
146+
let mut ctorDecls : List (String × List (String × Term)) := []
147+
let mut allOk := true
148+
for ctorNm in iVal.ctors do
149+
match (← getEnv).find? ctorNm with
150+
| some (.ctorInfo cVal) =>
151+
let fields := ctorFieldTypes cVal.numFields cVal.type
152+
let mut fieldDecls : List (String × Term) := []
153+
let mut selectorNames : Std.HashSet String := {}
154+
let mut idx := 0
155+
for (fname, ftype) in fields do
156+
let baseName :=
157+
if fname == Name.anonymous then s!"field{idx + 1}" else fname.toString
158+
let mut selectorName := s!"{ctorNm}.{baseName}"
159+
let mut suffix := 1
160+
while selectorNames.contains selectorName do
161+
selectorName := s!"{ctorNm}.{baseName}_{suffix}"
162+
suffix := suffix + 1
163+
selectorNames := selectorNames.insert selectorName
164+
let (tmSort, deps) ← translateAndFindDeps ftype (fvarDeps := false)
165+
fieldDecls := fieldDecls ++ [(selectorName, tmSort)]
166+
allDeps := allDeps ++ deps
167+
idx := idx + 1
168+
ctorDecls := ctorDecls ++ [(ctorNm.toString, fieldDecls)]
169+
| _ => allOk := false
170+
if allOk then
171+
addCommand e <| .declareDatatypes [(nm, 0)] [ctorDecls]
172+
return allDeps
138173
addCommand e <| .declareSort nm params.size
139174
return #[]
140175
else
141176
let (tmTp, deps) ← translateAndFindDeps tp
142177
addCommand e <| .declare nm tmTp
143178
return deps
179+
where
180+
/-- Extract the first `n` field name/type pairs from a constructor's type (a chain of `forallE`). -/
181+
ctorFieldTypes : Nat → Expr → List (Name × Expr)
182+
| 0, _ => []
183+
| n + 1, .forallE nm ftype body _ => (nm, ftype) :: ctorFieldTypes n body
184+
| _, _ => []
144185

145186
/-- Build the command for `e : tp` and add it to the graph. Return the command's dependencies. -/
146187
def addCommandFor (e tp : Expr) : QueryBuilderM (Array Expr) := do

Test/Int/DefineSort.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ goal: a.add b = b.add a
33
query:
44
(set-logic ALL)
55
(declare-sort MyInt 0)
6-
(declare-fun |a✝²| (MyInt MyInt) MyInt)
7-
(declare-sort |a✝¹| 0)
6+
(declare-fun MyInt.add (MyInt MyInt) MyInt)
7+
(assert (forall ((a MyInt)) (forall ((b MyInt)) (= (MyInt.add a b) (+ a b)))))
8+
(assert (= MyInt Int))
89
(declare-const b MyInt)
910
(declare-const a MyInt)
10-
(declare-fun MyInt.add (MyInt MyInt) MyInt)
1111
(assert (not (= (MyInt.add a b) (MyInt.add b a))))
1212
(check-sat)
1313
Test/Int/DefineSort.lean:6:0: warning: declaration uses `sorry`

0 commit comments

Comments
 (0)