6. Datatypes and Records
A datatype is declared in the current scope, like a sort of your own: declaring the same datatype twice gives two different sorts. A record is the opposite, a sort cvc5 builds from its fields, the same one each time.
6.1. Declaring a Datatype
A declaration lists the constructors and their fields (selectors in cvc5's terms). This one is Lst := nil | cons (head : Int) (tail : Lst):
section variable [Ω]
def declareLst : Env Srt := do
let nil ← Datatype.Ctor.Decl.mk "nil"
let cons ← Datatype.Ctor.Decl.mk "cons"
let cons ← cons.addSelector "head" (← Srt.int)
let cons ← cons.addSelectorSelf "tail"
let decl ← Datatype.Decl.mk "Lst"
Srt.datatype (← (← decl.addCtor nil).addCtor cons)
end
Srt.datatypes declares mutually recursive datatypes together, and Solver.declareDatatype is a shortcut for one datatype. A name can be declared only once per scope.
6.2. A Lean Type for a Datatype
Terms of a datatype are indexed by a Lean type that stands for it, tied to the datatype's name by a ToTyp instance. It is never built, only named in types.
structure Lst where private mk ::
deriving DecidableEq, Ord, Hashable
instance : ToTyp Lst := ⟨.datatype "Lst"⟩
A Term Lst can then be built wherever Lst is declared; elsewhere, building its sort fails at run time. Constructors and fields are looked up by name and sort, once, and checked:
#eval Env.runIO do
let _ ← declareLst
let l ← Term.mkSymbolAs Lst "l"
let consC ← Datatype.ctor Lst "cons"
let nilC ← Datatype.ctor Lst "nil"
let head ← consC.field Int "head"
let tail ← consC.field Lst "tail"
let one ← consC.apply2 (← Term.mkInt 1) (← nilC.apply)
println! "value : {one}"
println! "head : {← head.get one}"
println! "is cons : {← consC.is l}"
let s ← Solver.new
s.setOpt .smt_produceModels true
s.assert (← consC.is l)
s.assert (← smt! ![head.get l] = 3)
let empty ← nilC.apply
s.assert (← smt! ![tail.get l] = empty)
s.checkSat (ifSat := do
-- datatypes have no Lean values: print the term
println! "model : l = {← s.getValueTerm l}")
smt! matches on a datatype as Lean does. _ is the catch-all; any other identifier names a constructor.
#eval Env.runIO do
let _ ← declareLst
let l ← Term.mkSymbolAs Lst "l"
let m ← smt!
match l with | cons h t => h * 2 | nil => 0
println! "{m}"
6.3. Records
A record's index lists its fields, in order: Record [("x", Int), ("y", Bool)]. The order is part of the sort, so {x, y} and {y, x} are different sorts. A record's model value is a Lean Record value.
#eval Env.runIO do
let p ←
Term.mkSymbolAs (Record [("x", Int), ("y", Bool)]) "p"
let q ← smt! {p with x := p.x + 1}
println! "q := {q}"
let s ← Solver.new
s.setOpt .smt_produceModels true
s.assert (← smt! p.y ∧ q = {x := 5, y := true})
s.checkSat (ifSat := do
println! "p := {← s.getValue p}")
Tuples are indexed by Lean products instead, Term (Int × Bool), and built with Term.mkTuple. Products flatten to the right, so Int × Bool × String is one tuple of three components, and a tuple's model value is the Lean product.