SMT-LIB Version 3.0 - Preliminary Proposal

by C. Barrett, P. Fontaine, and C. Tinelli

Last updated: 2026-03-16

Overview

This page contains a high-level proposal for SMT-LIB Version 3. While some aspects of the language are still being worked out, this document is a fairly accurate description of the new format. A reference document is under construction and will contain a description of the proposal in full detail. This page focuses on the most salient aspects of the proposal, for a quicker overview. More content will be added with time as needed.

Preamble

The great majority of the changes described below will affect the way theories and logics are defined. It will minimally affect scripts that rely on (the equivalent of) SMT-LIB 2.7 logics. This means that most of the features of the Version 3 will not have to be supported by current SMT solvers. Support for some of new features of SMT-LIB 3 can be introduced gradually over time in a solver as a consequence of deciding to support new theories and logics that rely on those features.

New underlying logic

The main new aspect of the proposed new version is the move from (an extension of) many-sorted first-order logic as the underlying logic of SMT-LIB to a higher-order logic with polymorphism and dependent types but classical semantics. An important aspect of this change is that the new language for SMT-LIB scripts strives to be as backward-compatible as possible to that of Version 2.7. This is achieved in two ways:

  1. Giving new meaning to old syntax as needed.
  2. Defining the formal semantics so that it is essentially the same as the old one over the old syntax.
For example, monomorphic sorts from SMT-LIB 2.7 are interpreted as simple types, and polymorphic sorts are interpreted as polymorphic types; maps from Version 2.7 are identified with functions, which are now first class and can be passed to and returned by other functions; sorted constant and function symbols are interpreted as typed constants, with function symbols of arity .> 1 becoming higher-order constants, Curry-style; index symbols, as in (_ extract i j) or (_ BitVec 4), are now seen as syntactic sugar for symbols with a dependent type/kind. As in SMT-LIB 2, a number of additional syntactic restrictions on scripts are imposed to obtain various fragments of interests or logics, in SMT-LIB 2 terminology.

The underlying logic of SMT-LIB 3 has many elements of the Calculus of Inductive Constructions (CIC), which are at the basis of the formal foundation of proof assistants like Rocq and Lean. The main restriction is allowing only rank-1 (prenex) polymorphism and not having a Prop-like kind for (constructive) propositions. So, contrary to CIC, formulas are not types and instead continue to be expressed as terms of a two-element Bool type; this is more similar to the logic of the PVS proof assistant or proof assistants of the HOL family. However, as in SMT-LIB 2.7, type polymorphism is approximated by having global (i.e., top-level) type variables, as opposed to actual quantification over types, and so making polymorphic constants actually families of overloaded symbols. In addition to being polymorphic, and contrary to Version 2.7, types can now depend on values as in dependently-typed logics.

Motivation

The main motivation for the move to a CIC-like logic is manyfold.

  1. Having a more expressive underlying logic considerably simplifies the design and the formal foundations of the SMT-LIB standard. It obviates the need for most of the ad-hoc extensions to many-sorted first-order logic we had to introduce in Version 2 while also providing enough expressive power to define most theories formally, as opposed to textually as is done in SMT-LIB 2. That includes theories that are not easily definable in Version 2 such as the theory of parametric bit vectors.
  2. It facilitates the definition of a single language to define theories, logics, and benchmarks.
  3. It extends the possibility for SMT solvers to provide support for higher-order reasoning, facilitating their integration as automated reasoning back ends into higher-order theorem provers and into proof assistants, which are also based on a higher-order logic. Current integrations rely on complex encodings from the higher-order logics supported by these tools to the many-sorted logic of SMT-LIB 2. Moving to a higher-order logic should dramatically simplify these encodings, improving the trustworthiness of the integration. Natively reasoning with higher-order constructs (e.g., via higher-order equational reasoning) by the SMT solver could also considerably improve solving times on goals coming from these tools.
  4. It enables the introduction of a number of new SMT-LIB theories (for sets, relations, database tables, sequences) which benefit from the availability of second-order functions such as as fold, map, filter and so on, both to define common operations and to reason about them.

The move to the new logic maintains backward compatibility with SMT-LIB 2.7 to a very large extent. In particular, it should minimally affect SMT-LIB 2.7-compliant solvers whose developers choose not to support the new features.

The core language

Technically, the core language of SMT-LIB 3 is a higher-order logic with dependent types and rank-1 polymorphism. Terms in this logic are built out of variables, constants, applications, Π-abstractions, and λ-abstractions, as in the CIC calculus. There are three classes of (well-formed) terms in the language:

  • terms denoting values, such as 3, (+ x 3), (lambda ((x Int)) (+ x 3));
  • terms denoting types, such as Bool, Int, (-.> Int Real), (Array Int Bool), (List Int), (BitVec 3);
  • terms denoting kinds, such as Type, (-> Int Type).
Types classify values and kinds classify types. Except for Type, the kind of all types, kinds are actually not directly expressible in the concrete syntax of SMT-LIB 3, as that is not necessary. They appear only in abtract syntax where they are used to specify the logic's type system. The symbol -.>, which in Version 2.7 denotes the constructor of the map sort, now denotes the function type/kind constructor (as maps and functions are idenfitied in Version 3). We will write it as in this document from now on, for readability.

Well-formed value terms have an associated type term (or, simply, type). Well-formed type terms have an associated kind term (or, simply, kind). For instance,

  • 3 is a constant of type Int, which has kind Type;
  • not is a constant of type (→ Bool Bool), which has kind Type;
  • List is a type constant of kind (→ Type Type);
  • BitVec is a type constant of kind (→ Int Type).
Note that is both a type and a kind constant. The syntax (→ α β) can be understood as an abbreviation of the CIC type or kind Π x:α β. The constant can be used as a multiarity right-associative symbol. This allows one, for example, to write the type (→ A (→ B C)) as (→ A B C), and the kind (→ Type (→ Int Type)) as (→ Type Int Type). Function symbols of rank (τ₁ ⋅⋅⋅ τᵢ) in Version 2.7 become constants of type (→ τ₁ ⋅⋅⋅ τᵢ) in Version 3. This means that function symbols with more than one argument become higher-order in Version 3. For instance, the integer addition operator + now has type (→ Int Int Int), that is, (→ Int (→ Int Int)).

With the shift to seeing funtion symbols as constants of type, the command declare-const becomes primitive, and declare-fun and define-fun are now derived from it. For example, following commands

  (declare-fun h (Int Real) Real)
  (define-fun avg ((x Real) (y Real)) Real (/ (+ x y) 2))
are defined as having the same effect as
  (declare-const h (→ Int Real Real))
  (define-const avg (→ Real Real Real)
    (lambda ((x Real) (y Real)) (/ (+ x y) 2)))
For similar reasons, define-fun-rec is now defined in terms of the new primitive command define-const-rec. (See the Commands section for more details on commands.)

Name Space and Shadowing

There is a single name space for identifiers, regardless of whether they are used as value constants, type constants, or variables. Within this name space, bound variables, that is, variables local to a specific binder, can shadow any identifier with the same name, including other variables, in accordance with the standard lexical scoping discipline. Note that this policy is more flexible than in Version 2, where bound variables cannot have the same name as, and so cannot shadow, theory symbols.

Dependent Types

In Version 3, constants that have a type τ dependent on a value take as extra argument also the value that τ depends on. For instance, bit vector function symbols take as argument also the size of the bit vector. For conciseness and backward compatibility, however, such arguments are declared as implicit any time they can be inferred from later arguments.

Concretely, dependent function types are expressed with the syntax illustrated in this example:

  (→ (! Int :var n) (BitVec n) (BitVec (+ n 1)))
This expression denotes the type of a function that takes as input an integer n and returns a function that takes a bit vector of size n, and returns a bit vector of size n + 1. Note that (! Int :var n) uses the same term annotation syntax as in Version 2 but now applied to types. The new predefined :var attribute annotating Int in this case provides a name (n) for the first input. The general syntax for dependent types is
  (→ (! τ₁ :var x) τ₂) 
where x is a bound variable whose scope is τ₂. An alternative syntax, more reminiscent of that of dependently type logics, might have been:

(Pi (x τ₁) τ₂) or (Forall (x τ₁) τ₂)

which perhaps makes it clearer that x is a variable bound by the binder Pi or Forall in the scope τ₂. However, the annotation-based syntax is possibly more legible and, more importantly, more flexible because it allows the annotation of function inputs with further attributes. In particular, it allows one to declare an input argument as implicit. This is done with the new valueless attribute :implicit that can be associated to a function argument. For example,
  (→ (! Int :var n :implicit) (BitVec n) (BitVec n))
makes the first argument implicit — in the sense that it is not needed in applications of functions of that type. This is sensible since the value of the first argument can be inferred from the type of the second argument.

A further attribute, :restrict, permits the imposition of semantic restrictions on input and output values of a function, something that, in its full generality, effectively allows the definition of PVS-style predicate subtypes (aka, refinement types). For instance, in type

  (→ (! Int :var n :implicit :restrict (> n 0)) (BitVec n) (BitVec n))
the implicit argument n is required to be a positive integer. In PVS syntax, this would be the dependent type
  [n: {m : Int | m > 0} → BitVec(n) → BitVec(n)]
This notation can also express relational constraints on the input and output types, as for instance, in
  (→ (! Int :var m :implicit :restrict (> m 0)) 
     (! Int :var n :implicit :restrict (> n m)) 
     (BitVec m) (BitVec n) (BitVec (- n m)))
Yet another attribute, :syntax, allows the introduction of syntactic restrictions on input terms:
  (→ (! Int :var n :implicit :restrict (> n 0) :syntax <.numeral.>) 
     (BitVec n) (BitVec n))
The additional restriction :syntax <.numeral.> specifies that the only permitted applications of functions of the type above are those where the argument n is (αβ-reducible to) a concrete constant (0, 1, 2, ...) and not a symbolic term (x, (+ x 1), ...). This is convenient, for instance, when defining the current SMT-LIB 2 logics with bit vectors, where bit vector sizes cannot be symbolic. The language includes constructs to define syntactic categories like <.numeral.> (see later).

Polymorphic Types

Polymorphic types reproduce the polymorphic sorts of Version 2.7 through the introduction of globally-declared type variables. A polymorphic type is either a type variable, a nullary type constant, or the application of a non-nullary type constant to value terms and/or polymorphic types. For instance, if X and Y are type variables, and Int and Array are type constants, then (Array Int Int), (Array Int Y), (Array X Int), and (Array X Y) are all polymorphic types. A monomorphic type is a polymorphic type with no occurrences of type variables. For example, (Array Int Int) is a polymorphic type that is also monomorphic.

Note that this version of polymorphism is (intentionally) not equivalent to polymorphism in logics like CIC, where polymorphic functions take types as input. For example, the array select function in does not have type:

  (→ (! Type :var I :implicit) (! Type :var E :implicit) (Array I E) I E) 
where the first two, implicit arguments I and E are types. In contrast, and in full analogy with SMT-LIB 2.7, it has type
  (→ (Array I E) I E) 
where I and E are two previously declared type variables. The difference between the two types is subtle but significant. A constant of type (→ (Array I E) I E) does not denote a single function but a family of functions of monomorphic type, one for each monomorphic instance of (→ (Array I E) I E). This effectively allows only prenex polymorphism, in contrast to the unrestricted polymorphism of logics like CIC. In fact, it does not even allow (ML-style) let-polymorphism since the let binder does not locally bind type variables.

Terms

A consequence of identifying functions and maps from Version 2.7 is that the lambda binder of Version 2.7 can now be used to construct the lamba abstractions for all types, not just map types. Its general form is now

  (lambda ((x τ₁)) t)
where t is a value term and x is a bound variable of type τ₁ with scope t. As usual, the abstraction has type (→ τ₁ τ₂) if t has type τ₂ in the typing context that assigns type τ₁ to x. For instance, this allows the construction of lambda abstractions such as
  (lambda ((x A)) (lambda ((y A)) (not (= x y))))
or, more concisely, (lambda ((x A) (y A)) (not (= x y)), of type (→ A Bool).

Since a constant f of type (→ σ₁ ⋅⋅⋅ σᵢ) is a higher-order function when i > 2, partial applications of such constants, e.g., (f t), are meaningful and legal. The syntax (f t₁ ⋅⋅⋅ tᵢ) for the full application of f remains the same as in Version 2.7 although it is now seen as an abbreviation of ( ⋅⋅⋅ ((f t₁) t₂) ⋅⋅⋅ tᵢ).

Version 2.7 allows the application of maps via the apply operator @, as in (@ f a). This syntax is maintained but is now extended to all functions, because of identification of maps with functions.

Formulas

As in Version 2, formulas are terms of type Bool. Predefined constants include

  • the constants true and false of type Bool,
  • the equality operator =, now a polymorphic constant of type (→ α α Bool),
  • the ite operator, now a polymorphic constant of type (→ Bool α α α),
where α is a type variable.

A core language of commands allows one to declare new type variables, type constants, and value constants, and to assert formulas.

  (declare-type-var X)  ; declares a new type variable 
  (declare-type A ())   ; declares a new simple type (a nullary type constant) 
  (declare-type B ()) 
  (declare-const a A)        ; declares a constant of type A
  (declare-const f (→ A B))  ; declares a constant of type (→ A B)
  (declare-const g (→ A B))
  (declare-const p (→ A B Bool))
  (declare-const id (→ X X)) ; a polymorphic function 

  (assert (= b (f a))) 
  (assert (= f g))                                   ; higher-order assertion
  (assert (= p (lambda ((x A) (y B)) (= (g x) y))))  ; higher-order assertion
  (assert (forall ((x X)) (= x (id x))))             ; polymorphic assertion
Constants for polymorphic/dependent types can be declared as in the examples below:
  (declare-type Collection (Type))
  (declare-type Pair (Type Type)) 
  (declare-type BitVec (Int)) 
  (declare-type Vector (Type Int)) 
All these constants take types and/or values as input and construct a type. This means that type constant Collection has kind (→ Type Type). Type constant Pair has kind (→ Type Type Type). Type constant BitVec has kind (→ Int Type).
Type constant Vector has kind (→ Type Int Type). If X is a type variable, the type (→ (! Int :var n) (Vector X n)) is both polymorphic (in X), and dependent (on n).

Note that since declare-type is essentially a constant declaration but the at level of kinds, the command (declare-type Vector (Type Int)), say, could be understood as an abbreviation of (declare-const Vector (→ Type Int Type)), although the latter syntax is not actually allowed.

Semantic restrictions on types

While semantic restrictions on types yield the full power of predicate subtyping, their intended use in SMT-LIB 3 is to document precisely the input domain over which a partial function or type constant is defined. So they are meant to be used as formal documentation, not as the definition of a subtype. The type system SMT-LIB 3 remains without subtypes which means that semantic restrictions on types can be completely ignored for type checking purposes. That said, solvers can use type restriction information to provide more informative messages in case of scripts that use partial functions outside of their domain of definition. As an example, the integer division operator in Version 3 is defined as follows

  (declare-const div (→ Int (! Int :var n :restrict (distinct n 0)) Int) 
A solver could use the knowledge of the official restriction above for instance to issue a warning when it returns a model that gives value 0 to a term that occurs as the second argument of a div application in an assertion. Alternatively, it could try to determine statically, before solving an input problem, if all applications of partial functions in the problem are provably to values within the function's domain, and issue a warning otherwise.

Semantic restrictions can be introduced in function declarations and definitions, and can apply also to the returned value, as shown in the following example.

  (declare-const f 
    (-> (! Int :var m1 :restrict (> m1 0)) 
        (! Int :var m2 :restrict (> m2 m1)) 
        (! Int :var n :restrict (exists ((x Int)) (= n (* 2 x)))))
  )
or, equivalently
  (declare-fun f ((m1 Int :restrict (> m1 0)) (m2 Int :restrict (> m2 m1)))
    (! Int :var n :restrict (exists ((x Int)) (= n (* 2 x))))
  )
Both command introduce a function f that takes two integers as input and returns an integer. The semantic restrictions indicate that the function is not arbitrary: whenever the first argument is greater than 0 and the second argument is greater than the first, the returned value is an even integer. Note how in the case of declare-fun the :restrict attribute is added directly to the declaration of each named input. Moreover, the :var attribute is unnecessary.

As another example, the actual bit vector type in the Version 3 bit vector theory is defined as follows:

  (declare-type BitVec ((! Int :var m :restrict (>= m 0)))
Since the parameter m expresses the size of the vector, the added restriction limits the value of m to the positive integers.

Syntactic restrictions on types

Syntactic restrictions on types are orthogonal to semantic ones and have a different purpose: to restrict the set of expressible terms and formulas in a benchmark so as to achieve decidability of the satisfiability problem or some other computational objective — as done, for instance, in the various BV logics of SMT-LIB 2. Concretely, in modules corresponding to SMT-LIB logics (see later), the parameter m of the bit vector type above is further constrained to be a numeral, as opposed to an arbitrary Int integer term, as follows:

  (declare-type BitVec ((! Int :var m :restrict (>= m 0) :syntax <.numeral.>)))
This means that, in those logics, the BitVec constant can be applied only to terms that evaluate (that is, αβ-reduce) to numerals. The same mechanism is applied in logics of linear integer arithmetic to restrict applications of the multiplication operator to linear multiplications. For instance, in linear integer arithmetic, multiplication could be defined as follows (the actual definition is more elaborate):
  (define-fun-rec * ((x Int :syntax <.signed_numeral.>) (y Int)) 
    (ite (= x 0) 0
      (ite (< x 0) (* (- x) y) 
        (+ x (* ((- x 1) y)))))
   :left-assoc)
where <.signed numeral.> denotes the set of numerals and negated positive numerals ((- 1), (- 2), ...). Note that the first argument in the recursive calls in the definition of * are terms that reduce to a term in <.signed numeral.> whenever the value of argument x is a term in that set.

Contrary to semantic restrictions, syntactic restrictions must be enforced by compliant SMT solvers.

Commands

The SMT-LIB 3 language contains almost all commands in Version 2.7 as well as an additional number of new constructs and commands. A large number of them, however, are defined in terms of the core language above. This includes all the commands from Version 2.7, none of which change semantics in practice in Version 3. Most of them become syntactic sugar of core Version 3 commands. Some of them will be deprecated and eventually phased out. For instance, declare-const is now a core command while declare-fun is not. The SMT-LIB 2.7 expression

  (declare-fun f (τ₁ ⋅⋅⋅ τᵢ) τ)
with i > 0 is now an abbreviation of
  (declare-const f (→ τ₁ ⋅⋅⋅ τᵢ τ))
Similarly,
  (define-fun f ((x₁ τ₁) ⋅⋅⋅ ((xᵢ τᵢ)) τ t)
and
  (define-fun-rec f ((x₁ τ₁) ⋅⋅⋅ ((xᵢ τᵢ)) τ t)
are now respective abbreviations of
  (define-const f (→ τ₁ ⋅⋅⋅ τᵢ τ) (lambda ((x₁ τ₁) ⋅⋅⋅ ((xᵢ τᵢ)) t))
and
  (define-const-rec f (→ τ₁ ⋅⋅⋅ τᵢ τ) (lambda ((x₁ τ₁) ⋅⋅⋅ ((xᵢ τᵢ)) t))
where define-const-rec is a new core command.

In a similar vein, sorts are not primitive anymore and become types. Correspondingly, declare-sort is now a special case of the new core command declare-type. For instance,

  (declare-sort S n)
with n ≥ 0 now becomes an alternative syntax for
  (declare-type S (Type ⋅⋅⋅ Type))
with n occurrences of Type. Similarly,
  (define-sort S (u₁ ⋅⋅⋅ uᵢ) σ)
now becomes an alternative syntax for
  (define-type S ((u₁ Type) ⋅⋅⋅ (uᵢ Type)) σ)
Note that declare-type and define-type are more general than their Version 2 counterparts because they can introduce (value) dependent types as well.
  (declare-type Vector (Type (! Int :var m :restrict (>= m 0)))
  (define-type NonEmptyRealVector ((n Int :restrict (> n 0))) (Vector Real n))
  (define-type RealVector8 () (NonEmptyRealVector 8))

Command attributes

The syntax of commands is extended with the addition of optional attributes which can inserted in arbitrary order after the commands argument. This is most useful for declare-const and similar commands as it allows the incorporation of Version 2 attributes for the theory definition sublanguage, as well as new ones.
  (declare-type-var A)
  ...
  (declare-const = (-> A A Bool)
   :pairwise 
   :builtin 
   "= denotes the identity function, which returns true iff 
    its two arguments are identical.") 

  (define-fun or ((b1 Bool) (b2 Bool)) Bool
    (ite b1 true b2) 
   :left-assoc)
The full list of attributes per command will be provided in the reference document. More examples are in the sample theory modules below.

Binders

The primitive binders in Version 3 are

  • let, the parallel version of local definitions ((let ((x₁ t₁) ⋅⋅⋅ (xn tn)) t));
  • lambda, for function (λ) abstraction ((lambda ((x₁ τ₁) ⋅⋅⋅ (xn τn)) t)).
The let binder is the same as in Version 2 with the addition that now it can be used to define higher-order variables. The lambda binder is an extension to functions of the corresponding binder of Version 2.7.

The other binders are the following:

  • match, for cases analysis of algebraic datatypes;
  • forall, the universal quantifier;
  • exists, the existential quantifier;
  • choose, the choise operator.
They are non-primitive as they can be defined in terms of the primitive ones. The match has the same syntax as in Version 2.7 and is defined in terms of let, also as in Version 2.7. The forall and exists quantifiers have the same syntax as in Version 2, however, they are not primitive anymore and are instead defined as higher-order functions. The binder syntax is now syntactic sugar for terms based on lambda. For instance, forall is now defined as the polymorphic function
  (lambda ((p (→ A bool))) (= p (lambda ((x A)) true)))))
of type (→ (→ A Bool) Bool) where A is a type-variable. The binder syntax, with expressions of the form
  (forall ((x₁ τ₁) ⋅⋅⋅ (xᵢ τᵢ)) φ) 
is understood as an abbreviation of
  (forall (lambda ((x₁ τ₁) ⋅⋅⋅ (xᵢ τᵢ)) φ)) 
The exists binder is defined similarly. The choose binder, for the Hilbert choice operator ε, is new. It too is a second-order function that can be used with a binder syntax. The expression
  (choose (x τ) φ)
abbreviates
  (choose (lambda ((x τ)) φ))
where the function choose has the polymorphic type (→ (→ A bool) A). The function is defined axiomatically in terms of exists, as usual.

Polymorphism in declarations, definitions and assertions

Exactly as in Version 2.7, polymorphic functions can be introduced only globally, with the commands for declaring or defining constants. It is not possible to have (let-introduced) polymorphic variables or to pass polymorphic functions as argument to other functions. In contrast, it is possible to make polymorphic assertions. This is illustrated by the following example, where we (illegally) name the type variable α, for emphasis:
  (declare-type-var α)
  (declare-const f (→ α α))

  (assert (forall ((a α)) (= (f a) a)))

  (define-fun g1 ((a α) (p (→ α Bool))) Bool (h a)) 
  (define-fun g2 ((a α) (p (→ α Bool))) Bool (h 1)) 

  (assert 
    (let ((id (lambda ((x α)) x)))
      (= 1 (id 1))
    )
  )
Function f is polymorphic. Its type, in math notation, is ∀α.(α → α). So it stands for the family of all functions whose type is a monomorphic instance of (α → α) ((→ Bool Bool), (→ Int Int), ...). Correspondingly, the first assertion stands for a family of assertions, again one for each monomorphic instance of (α → α).

Function g1 is also polymorphic, with type ∀α.(α → (α → Bool) → Bool). However, its input predicate p is not polymorphic. For each monomorphic instance of g1 of type (τ → (τ → Bool) → Bool), the input function h is of type τ → Bool, not ∀α.(α → Bool). This means, that function g2 has in fact an ill-typed body since its instance of type (String → (String → Bool) → Bool), say, applies its input function to a value whose type is not String.

The last assert is ill-typed in a similar way, since the variable id has type α → α, not ∀α.(α → α). In more technical terms then, Version 3's type system does not allow let-polymorphism. Only globally defined symbols can be polymorphic.

Overloading

Within an SMT-LIB 3 script, some symbols can be overloaded by redeclaring them, others cannot. Specifically, type constants and type variables cannot be overloaded. In particular, they cannot be redeclared as a type constant, type variable, value constant or local variable. In contrast, value constants can always be redeclared. We say that a constant f has a, possibly polymorphic, principal type τ in a script if the script contains a declaration or definition of f with type τ. A constant has more than one principal type in a script if and only if it is overloaded. As in previous versions, Version 3 enforces a type discipline that allows simple bottom-up typing. If a constant f is overloaded, the type of each occurrence of f in a term is determined as follows:

  1. If f occurs in a function position, as in (f t₁ ··· tₙ), the type of f is the one in the most recent declaration of f that makes (f t₁ ··· tₙ) well typed;
  2. If f occurs as an argument, as in (t₁ ··· f ··· tₙ), its type is the one in the most recent declaration of f.

This disambiguation policy can be overridden by explicitly ascribing to f one of its types using the as operator, in an expression of the form (as f τ) where τ is the intended type of that occurrence, that is, an instance of a principal type of f. Specifically, the f selected by (as f τ) is be the most recently declared f whose type has τ as an instance.

  (declare-type-var X)
  (declare-const f (→ (List X) X))    ; variant 1, polymorphic
  (declare-const f (→ Int Real Int))  ; variant 2
  (declare-const f (→ Int Int))       ; variant 3
  (declare-const f (→ Int Real))      ; variant 4
  (declare-const p (→ X Bool))

  (assert (f 5 4.3))  ; no ambiguity, variant 2 of f inferred here
  (assert (f 8))      ; ambiguity resolved in favor of variant 4, the most recent
  (assert (p f))      ; ambiguity resolved in favor of variant 4, the most recent
Constant f in the script fragment above is both polymorphic and overloaded. In the first assertion, the f in question is variant 2, the only one with an Int and a Real input. In contrast, the application (f 5) in the second assertion cannot be disambiguated among variants 2, 3 and 4. (For variant 2, note that its actual type is (→ Int (→ Real Int)) and partial applications are permitted.) In this case, variant 4, the most recent, is the default. If variant 3, say, was meant, the application should have been written as ((as f (→ Int Int)) 8). In the last assertion, where f is not applied but is passed as an argument, the ambiguity is resolved again in favor of variant 4; the use of as is necessary to pass a different variant.

Note that as stands now for the standard ascription operator. This is different with the use of as in Version 2, which provides only the return type of the function. This one of the very few cases that breaks backward-compatibility.

In extensions of the script above with later commands, f cannot be reintroduced with type (→ Int Real Int), (→ Int Int), (→ Int Real), or (→ (List τ) τ) for any type τ, since such types would all be instances of a type already assigned to f. The different use of as in SMT-LIB 3 versus Version 2 represents one of the few changes that are not backward compatible with Version 2.7. However, this is not a serious concern since as has been used mostly for 0-arity constants (like the nil list constructor) where the new and the old use coincide.

New language constructs

Some of the new constructs for terms, such as lambda and choose, are specific to the move to higher-order logic. Other novel constructs are orthogonal to that extension and are motivated by a desire to unify in a single language the various sublanguages of SMT-LIB 2.7 for theory definitions, logic definitions, and command scripts. This is done by extending the command language to allow the definition of theory symbols in the same style as user-defined symbols and by introducing a notion of module that can be used to define both theories and logics.

Modules

Modules are a general construct to structure scripts in units with their own name space and provide a basic form of encapsulation and information hiding. For the medium term, modules will not be allowed in user scripts. The hope, however, is that with time they will be supported by SMT solvers and so will eventually be available to regular users. For now, they will be used only to define SMT-LIB theories and logics.

(define-module M (
  (declare-type-var A)
  (declare-type ping ()) 
  (declare-type pong ()) 
  (declare-const a ping)
  (declare-const f (→ A pong))  
  ; The scope (visibility) of the symbols 
  ; A, ping, pong, a, and f is limited to module M
  )
)
; A, ping, pong, a, and f are not directly accessible here
A module is introduced by the define-module command which takes two arguments: a symbol, which constitutes the name of the module, and a list of SMT-LIB commands. The set of commands allowed in a module is restricted. The main ones are assert and all the commands that introduce new types and constants.

The scope, that is, the visibility, of symbols introduced in a module is limited to the module. Such symbols can be accessed outside the module only if they are exported by the module’s interface and only with a qualified name of the form <.module name.>::<.symbol.> (e.g., M::ping).

Module Interfaces

By default, all the symbols (i.e., constants and type constants) declared and defined in a module M are public, i.e., visible by modules importing M. Optionally, however, it is possible to specify explicitly an interface for the module, which selects the symbols that are exported, that is, made visible. Interfaces are used to construct modules that correspond to logics in the sense of SMT-LIB 2. The interface is specified with two attributes in a module definition:

  • :type-vars whose value is the list of exported type variables,
  • :types whose value is the list of exported type constants with their associated input kind or type, if any, and
  • :consts whose value is the list of exported constants with their associated type.
(The type/kind constant does not need to be exported because it a primitive symbol of the underlying logic.)

(define-module M (
  (declare-type-var X)
  (declare-type-var Y)
  (declare-type A ())
  (declare-type B ())
  (declare-type C ()) 
  (declare-type Pair (Type Type))   

  (declare-const a A)
  (declare-const b B)
  (declare-const f (→ A B))
  (declare-const g (→ B A)) 
  (declare-const c (→ A C))
  (declare-const d (→ X Y X))

  (define-const  h (→ A A) (lambda ((x A)) (g (f x))))
  )
 :type-vars ( X )
 :types ( A B (Pair (Type Type)) )
 :consts ( (a A) (f (→ A B)) (h (→ A A)) (d (→ X X X)) )
)
In the example above, only one of the type variables, three of the type constants and four of the constants are exported. The list of exported type constants contain a constant directly if it is nullary (as in the case of A and B), otherwise it contains a pair consisting of the constant and the list of kinds/types of its inputs (as in the case of Pair). Note that the types of the exported constants must be constructible from the exported type variables and type constants for the interface to be well formed. However, these types can be restricted to be instances of the constant's principal type. Also note that any relationship among the exported constants established in the module through non-exported constants (or through assertions) is maintained when the module is imported. For example, the formula
  (forall ((x1 A) (x2 A)) (=> (= (f x1) (f x2)) (= (h x1) (h x2))))
is valid not just in the module M above but also in any module that imports M. Concretely, this means that importing a module has always the effect of declaring and asserting everything in the module. The only effect of the interface is to prohibit in the importing module any direct reference to non-exported symbols.

All three interface attributes are optional. The absence of the :types attribute, say, causes all all type constants in the module are exported in their full generality. In contrast, giving it value () causes no type constants to be exported. The same policy applies to :type-vars and :consts. When a datatype constant (e.g., List) is exported, the datatype's recursor and all of its constructors, selectors, and testers are exported by default.

The reason constants are listed in a module's interface with an associated type is that its is possible to assign them a more restricted type than the one they have in the module. For instance, the array select function, which has type

  (→ (Array I E) I E) 
in the module declaring it, where I and E are type variables, could be exported as follows:
  :type-vars ( I E ) 
  :types ( Bool Int (Array (Type Type)) ... ) 
  :consts ( (select (→ (Array Int E) Int E))
            (select (→ (Array I Bool) I Bool))
            ... ; constants other than select 
          )
This would limit the application of select only to values of the listed types. Note that the interface above is not exporting two select functions; rather, it is exporting the same polymorphic function but with two (disjunctive) restrictions on its instances.

The exported type constants can be restricted in similar way, disallowing in the importing modulo any terms whose types does not conform to the restrictions. For example, a module having a type constant Seq (for generic sequences of len n) of kind: (→ (! Int :var n :restrict (>= n 0)) Type Type) could export it as follows:

  :types 
  ( (Seq (! Int :var n :syntax <.numeral.> :restrict (even n)) Type)
    ... 
  )
which (cumulatively) restricts the application of Seq only to even numerals. The general rule is that the restrictions imposed on an exported symbol in a module interface are in addition to any restrictions already imposed on the symbol within the module. The meaning of multiple syntactic restrictions is the intersection of the languages denoted by the individual restrictions. The meaning of multiple semantic restrictions is the conjunction of the individual restrictions.

For polymorphic types, it is possible to impose instantiation restrictions on the type parameters, using a similar syntax using a (limited) language of constraints over types.

(define-module M2 (
  ...
  (declare-type Array (Type Type))
  ...
  )
 :types ( ... (Array (! I Type :restrict (= I Int)) Type) ... )
 :consts ( ... )
)
Module M2 restricts the exported Array type constant to take only type Int as its first input.

A natural question is why add restrictions on an exported symbol (type constant or constant) in the interface of a module and not directly inside the module when the symbol is declared. The reason is that a module can import a symbol from another module and export a restricted version of it. This approach is followed in defining modules that correspond to SMT-LIB 2 logics. Such modules import symbols from theory modules in their full generality and then export them with restrictions. For instance, a linear integer arithmetic module would import the arithmetic symbols from the module defining the integers and export the multiplication symbol with the additional restriction that at least one of its arguments is a (concrete) integer value:

  :consts 
  ( (* (→ (! Int :syntax <.int_value.>) Int Int))
    (* (→ Int (! Int :syntax <.int_value.>) Int)
    ... 
  )

Module Imports

Modules can be imported into another module or at the top level in an SMT-LIB 3 script with the new command import-modules which lists all the modules to be imported. At most one import command is allowed in a module. Moreover, the command has to occur before any command that modifies the context (declarations, assertions, ...). At the top level, later import commands are allowed only if interleaved with reset commands.

  (import-modules (M1 M2)) 
  ...
  (reset)
  (import-modules (M3)) 
  ...

Qualified names

Every module automatically defines a name space corresponding to it. The name space is reflected in the generation of qualified names for the symbols exported by a module. Qualified names have the form M::s where M is the module's name and s is a symbol exported by M. Note that using :: as a separator in qualified names does not break backward compatibility because :: is not allowed in Version 2.7 identifiers.

  (import-modules (Ints Reals))

  (declare-const n Ints::Int)
  (declare-const x Reals::Real)
  (define-const i Ints::Int (Ints::+ n Ints::2))
  (define-const y Reals::Real (Reals::+ x Reals::4.3))

The new command open-module can be used to allow unqualified names for the (exported) symbols of an imported module.

;; Example 1
(define-module M1 (
  (declare-type A ())
  (declare-type B ())
  (declare-type C ()) 
  (declare-const a A)
  (declare-const f (→ A B)) 
  (declare-const P (→ A B Bool))
  (assert (P b (f a)))
 )
 :types (A B)
 :consts ((a A) (f (→ A B)))
)

(import-modules (M1))
; all exported symbols of M1 are now visible with their fully qualified name
; all assertions in M1 are now in the assertion context.

(declare-const a1 M1::A) ; declares a1 in the top-level namespace
(assert (= a1 M1::a))    ; adds this equality to the assertion context
(open-module M1) 

; now all symbols of M1 can be used without qualification
(declare-const b2 B) 
(assert (= b2 (f a))) ; B, f and a are from M1

The effect of opening a module M is to create a local alias for each symbol exported by M. In Example 1 above, (open-module M1) can be understood as an abbreviation of

  (define-type A () M1::A)
  (define-type B () M1::B)
  (define-const a A M1::a)
  (define-const f (→ A B) M1::f)

Opening one of more modules creates the possibility of overloading at the level of the importing module, as in the example below. That overloading is resolved with the same policy as for overloading caused by local declarations. Since the policy is based on the order in which two constants with the same name are introduced, the order in which two modules are open is then meaningful.

; Example 2
(import-modules (Ints Reals))
(open-module Ints)
(open-module Reals)
(declare-const n Int)           ; Int is an alias of Ints::Int
(declare-const x Real)          ; Real is an alias of Reals::Real
(define-const i Int (+ n 2))    ; + and 2 are aliases of Ints::+ and Ints::2
(define-const y Real (+ x 4.3)) ; + and 4.3 are aliases of Reals::+ and Reals::4.3

A module M importing a module N can export the symbols exported by N as if they were its own. It has the option, however, to export them with stronger restrictions. The name used in M's interface to export N's symbols can be its fully qualified name (with the prefix N::) or its short version if N is opened in M.

An important point is that all module imports declare their symbols at the top level, which means that qualified names are never nested. Importing a module M at the top level that in turn imports a module N internally has the effect of first importing N at the top level and then M. However, since M is the module being directly imported at the top level, the only symbols of N visible (that is, usable in later commands) are those re-exported by M, if any. If M opens N, the module prefix of the fully qualified names of the re-exported symbols of N is indifferently M:: or N::. Concretely, if s is a type constant or constant exported by N and then re-exported by M, it is accessible at the top level as N::s or as M::s, or simply as s once M has been opened; it is not accessible as M::N::c. (The latter would be the case if N's definition itself was inside M's which is, however, not allowed since all modules are defined at the top level.)

Import Graph

Considering the top level as an implicit (special) module, let's say that a module N is imported directly by a module M if N appears explicitly in an import command in M. We say that N is imported indirectly by M if N is imported, directly or indirectly, by a module imported by M. Circular import dependencies are disallowed, that is, a module cannot import itself, directly or indirectly. In other words, the "directly imports" relation always defines a direct acyclic graph over modules.

Without interleaving calls to the reset command, a module N can be imported directly at most once but can be imported indirectly many times as a result of importing several modules that in turn import N. Another consequence on the import structure is that a module's symbol can be imported in a module multiple times and with different restrictions. The effect of two imports of symbols from the same module is additive: symbols imported with the second import that had not already been imported with the first are added to the top level; however, no already imported symbols are removed.

If the same dependent constant c is imported once with a (restricted) type τ₁ and then with a (restricted) type τ₂ then it can be used with either type. (Note that c is not overloaded in this case because type τ₁ and τ₂ both are restrictions of the same principal type that c has in the module it originally was introduced in.)

(define-module M0 (
  (declare-type-var X)
  (declare-type-var Y)
  (declare-const p (→ X Y Bool))
  )
 ; X, Y and p are exported in full generality
)
(define-module M1 (
  (import-modules (M0))
  (open-module M0)
  (declare-type A ())
  )
 :type-vars (Y)
 :consts (p (→ A Y Bool))
)
(define-module M2 (
  (import-modules (M0))
  (open-module M0)
  (declare-type B ())
  )
 :type-vars (X)
 :consts (p (→ X B Bool))
)
(define-module M3 (
  (import-modules (M1 M2))
  (open-module M1)
  (open-module M2)
  ...
  )
)
Module M3 above imports (the same) constant p twice. The net effect is that p has two principal (polymorphic) types in M3: (→ A Y Bool) and (→ X B Bool).

Algebraic Data Types

Algebraic data types in SMT-LIB 3 are unchanged from version 2.7, both in syntax and semantics.

  (declare-datatypes ((Size 0) (Option 1) (List 1) (Pair 2)) ()
    ; Size
    ((small) (medium) (large))
    ; Option
    (par (V) ((none) (some (val V)) ))
    ; List
    (par (E) ((nil) (cons (head E) (tail (List E)))))
    ; Pair
    (par (A B) ((pair (first A) (second B))))
    )
  )

  (declare-datatype BinTree (par (X)
    ((leaf) (node (left (BinTree X) (val X) (right (BinTree X)))))
  )
In the commands above, the only difference is that now we refer to the local symbols introduced by the par binder (such as V), as type variables instead of sort parameters.

A consequence of maintaining the old syntax from Version 2.7 is that, while algebraic datatypes can be polymorphic, they cannot be dependent (on values). This restriction is intentional but could be lifted in future releases of Version 3. Another restriction is that constructors of arity greater than 1, which are now higher-order functions, can be applied only in full. For example, a term like (node leaf) is not well-formed, whereas one like (node leaf 4 leaf) is.

Testers and recursors

Every inductive type definition induces an implicit definition of testers for each constant as well as general recursor for the type. For example, for the List type defined above there would be the following tester functions

  (is-nil (→ (List E) Bool))
  (is-cons (→ (List E) Bool))
where E is a type variable. Note that the syntax for testers is different from SMT-LIB 2.7, where those two functions would have an indexed name: (_ is nil) and (_ is cons), respectively. The indexed-name form is still allowed as syntactic sugar for backward compatibility but is deprecated.

Recursors are a new feature in SMT-LIB 3. Using again as an example the List datatype introduced above, its definition also defines automatically the recursor reduce of type (→ Y (→ X (List X) Y Y) (List X) Y) where X and Y are type variables. The List recursor behaves as if it had been directly defined as follows

  (declare-type-var X)
  (declare-type-var Y)
  (define-fun-rec reduce 
    ( (y0 Y) (comb (→ X (List X) Y Y)) (l (List X)) ) ; input vars
    Y                                                 ; output type
    (match l ( 
      (nil y0)
      ((cons h t) (comb h t (reduce y0 comb t))))))
The recursor above reduces any list l of type (List X) to a value of type Y with the aid of a value y0 of type Y for the case where l is empty, and of a function comb that computes the reduction in the non-empty case from the head h and tail t of l and the (recursively computed) reduction of t. Analogously, the recursor for the BinTree datatype defined above would defined as follow
  (define-fun-rec reduce 
    ( (y0 Y) (comb (→ (BinTree X) X (BinTree X) Y Y Y)) (t (Tree X)) ) Y
    (match t ( 
      (leaf y0)
      ((node t1 v t2) (comb t1 v t2 (reduce y0 comb t1) (reduce y0 comb t2))))))

For each datatype, the recursor can be defined automatically based on the datatype's constructors and their type. The rationale for having recursors supported directly by the SMT solver is that a wealth of functions over a recursive datatype can be defined, non-recursively, by the user in terms of reduce. This in principle should facilitate reasoning about functions over datatypes as the solver can concentrate on reasoning about reduce. For the List datatype, the functions that can be defined with reduce include the following:

  ; Sum over the elements of an Int list
  (define-const sum ((l (List Int))) Int
    (reduce 0 (lambda ((n Int) (t (List Int)) (s Int)) (+ n s)) l) 
  )
  ; Length of a list
  (declare-type-var A)
  (define-fun len ((l (List A))) Int
    (reduce 0 (lambda ((a A) (t (List A)) (n Int)) (+1 n)) l)
  )
  ; List concatenation
  (define-fun append ((l1 (List A))) (l2 (List A)) (List A)
    (reduce l2 (lambda ((a A) (t (List A)) (r (List A))) (cons a r)) l1)
  )
  ; List reverse
  (define-fun reverse ((l (List A))) (List A)
    (reduce (as nil (List A)) 
      (lambda ((a A) (t (List A)) (r (List A)))
        (append r (cons a (as nil (List A))))) l)
  )
  ; List of elements of input list that satisfy input predicate
  (define-fun filter ((p (-> A Bool)) (l (List A))) (List A)
    (reduce (as nil (List A)) 
      (lambda ((a A) (t (List A)) (r (List A))) (ite (p a) (cons a r) r)) l)
  )
  (declare-type-var B)
  ; List map function
  (define-fun map ((f (→ A B)) (l (List A))) (List B)
    (reduce (as nil (List A)) 
      (lambda ((a A) (t (List A)) (r (List B))) (cons (f a) r)) l)
  )

External Symbols

When defining new theories there is sometimes the need for two theories to refer to each other's symbols. This cannot be done with imports because it would create a circular dependency. To address this issue while keeping theories in separate modules the language allows the definition of external types and constants in a module.

More generally, one imports a module A in a module B if B is meant to be an extension of A. If, on the other hand, A and B are conceptually separate modules where, however, one module may need to use a type or a constant in the other, then they can be introduced as external. Types constants and constants in module can be declared as external with an :extern attribute which connects them to symbols exported by another module.

Suppose a module Ints defines the integer theory with its usual operators and a unary operator +1 for the integer successor function. A module for the theory of lists may be defined as follows.

(define-module Lists (
  (declare-datatype List (par (X)
    ((nil) (cons (head X) (tail (List X))))))
  ...
  (declare-type Int () :extern Ints::Int)
  (declare-const 0 Int :extern Ints::0)
  (declare-const +1 (→ Int Int) :extern Ints::+1)
  (define-fun len ((l (List A))) Int
    (reduce 0 (lambda ((a A) (t (List A)) (n Int)) (+1 n)) l))
  ...
  )
  :type-vars ( E )
  :types ( (List Type) )
  :consts ( ... (len (→ (List E) Ints::Int) )
)
Using the :extern attribute, the type Int in the module Lists above is declared to be external and a proxy for Ints::Int. The constants 0 and +1 are introduced similarly.

In general, symbols declared as :extern in a module M cannot be exported by M. This entails that if M exports a symbol s that depends on external symbols from a module N, a module importing M can use s in a command only if it imports N too. [To do: define precisely what it means for a symbol to depend on external ones.]

In the example above, since Lists exports a function whose type and definition depend on external symbols from module Ints, any module importing Lists must also import Ints so that the reference to the external symbols in Lists can be resolved.

  ; Example 1
  (import-modules (Lists))
  ; Illegal import since Lists::len depends on Ints.
  ...
  ; Example 2
  (import-modules (Ints Lists))
  ; Lists::len can be used here
  ...

Syntactic categories for syntax restrictions

[To do: definition of <.int_value.> <.pos_numeral.> and so on via an attribute grammar. Syntax categories can be defined in modules and should be exported like types and constants.]

SMT-LIB 3 Theories

The standard SMT-LIB 3 theories are now defined just as modules. A first draft of the new definition for some of the standard theories in Version 2.7 is available below. Note that the modules are still at the pre-proposal stage. They are incomplete and subject to change. However, they should offer a pretty good idea of the expressive power of SMT-LIB 3. The theories are defined intentionally to be rather rich. Any restriction on theory signature and language is left to modules that define restricted logics.

In the current formalization, the theories below are essentially conservative extensions of the corresponding SMT-LIB 2.7 theories, that is, they have more symbols and richer types but do not change the semantics of the old ones in any meaningful way.

; Core SMT-LIB theory
(define-module Core ( 
  (set-info :smt-lib-version 3.0)
  (set-info :smt-lib-release "2026-XX-XX")
  (set-info :written-by "Pascal Fontaine and Cesare Tinelli")
  (set-info :date "2026-03-15")
  (set-info :last-updated "2026-03-15")
  (set-info :update-history
   "TODO.")

  (declare-type-var A)
  (declare-type-var B)
  (declare-type-var C)

  ;-----------------
  ; Builtin symbols
  ;----------------- 

  ; Booleans
  (declare-datatype Bool ((true) (false)))

  ; Equality  
  (declare-const = (-> A A Bool)
   :pairwise 
   :builtin 
   "= denotes the identity function, which returns true iff 
    its two arguments are identical.")

  ;-------------------
  ; Defined constants
  ;------------------- 
  ; Apply
  (define-fun @ ((f (-> A B)) (a A)) B
    (f a)
   :left-assoc) ; (@ f a b) = (@ (@ f a) b)
  ; Identity
  (define-const iden ((a A)) A
    a)
  ; if-the-else 
  (define-fun ite ((b Bool) (a1 A) (a2 A)) A
    (match b (true a1) (false a2))) 
  ; not
  (define-fun not ((b Bool)) Bool
    (ite b false true))
  ; !=
  (define-fun != ((a1 A) (a2 A)) Bool 
    (not (= a1 a2)))
  ; distinct
  (define-fun distinct ((a1 A) (a2 A)) Bool 
    (!= a1 a2)
   :pairwise)
  ; or
  (define-fun or ((b1 Bool) (b2 Bool)) Bool
    (ite b1 true b2) 
   :left-assoc)
  ; and
  (define-fun and ((b1 Bool) (b2 Bool)) bool
    (ite b1 b2 false)
   :left-assoc)
  ; implies
  (define-fun => ((b1 Bool) (b2 Bool)) Bool
    (ite b1 b2 true)
   :right-assoc)
  ; iff
  (define-fun <=> ((b1 Bool) (b2 Bool)) Bool
    (= b1 b2)
   :right-assoc)
  ; xor
  (define-fun xor ((b1 Bool) (b2 Bool)) Bool 
    (!= b1 b2)
   :left-assoc)
  ; forall
  (define-fun forall ((p (-> A Bool))) Bool
    (= p (lambda ((x A)) true)))
  ;
  (set-info :notation 
   "(forall ((x τ)) φ) abbreviates 
    (forall (lambda ((x τ)) φ))
  
    (forall ((x₁ τ₁) ⋅⋅⋅ (xᵢ τᵢ)) φ) abbreviates 
    (forall ((x₁ τ₁)) ⋅⋅⋅ (forall ((xᵢ τᵢ)) φ))")
  ; exists
  (define-fun exists ((p (-> A Bool))) Bool
    (!= p (lambda ((x A)) false)))
  ;
  (set-info :notation 
   "(exists ((x τ)) φ) abbreviates 
    (exists (lambda ((x τ)) φ))

    (exists ((x₁ τ₁) ⋅⋅⋅ (xᵢ τᵢ)) φ) abbreviates 
    (exists ((x₁ τ₁)) ⋅⋅⋅ (exists ((xᵢ τᵢ)) φ))")
  ; Hilbert's choice (ε)
  (declare-const choose (-> A Bool) A)
  ; choose is defined axiomatically   
  (assert (forall ((p (-> A Bool))) (<=> (exists p) (p (choose p)))))
  ;
  (set-info :notation 
   "(choose (x τ) φ)  abbreviates  (choose (lambda ((x τ)) φ))")
  ; compose
  (define-fun compose ((f (-> B C)) (g (-> A B))) (-> A C)
    (lambda ((a A)) (f (g a)))
    :note 
    "compose is not declared left-associative on purpose otherwise
     there would be ambiguity between (compose f g h) where f g and h 
     are functions to be composed and (compose f g a) where a is 
     the input of g.")

  ;-----------------------
  ; Abstract value syntax
  ;-----------------------
  (define-syntax 
    ; non-terminals
    ( <.abstract_value.> <.bool_value.> )
    ; production rules
    ( (<.bool_value.> (true false))
      (<.abstract_value.> ((! <.symbol.> :restrict "starts with @")))))

  ;-------------
  ; Bool values
  ;-------------
  (define-values <.bool_value.> () Bool)


  ;-------------------
  ; Import Attributes
  ;-------------------

  (set-info :FOL
   "The import attribute :FOL restricts Core
      to the first-order applicative fragment:
      - no lambdas
      - no partial applications: 
        the first argument of an application can only be a constant
      - all arguments of an application have a type of order 0
      - can only declare or define constants whose type has order <= 1
      - can use forall, exists, and choose only in their
        abbreviated form.
   ")

  (set-info :no-new-types 
   "The import attribute :no-new-types disallows the use  
    of the declare-type command.")

  (set-info :no-new-functions 
   "The import attribute :no-new-functions disallows the declaration 
    of constants of arrow type.")
))
(raw file)
(define-module Ints (
  (set-info :smt-lib-version 3.0)
  (set-info :smt-lib-release "2026-XX-XX")
  (set-info :written-by "Pascal Fontaine and Cesare Tinelli")
  (set-info :date "2026-03-15")
  (set-info :last-updated "2026-03-15")
  (set-info :update-history
   "TODO.
   "
  )
  (set-info :notes 
   "Integer arithmetic")
 
  (import (Core))
  (open Core)

  ;---------------
  ; Builtin types
  ;--------------- 
  ; Int
  (declare-type Int () :builtin 
  "Int denotes the set of all integer numbers.")

  ;-------------------
  ; Builtin constants
  ;------------------- 
  ; 0, 1, 2, ...
  (declare-consts <.numeral.> Int 
   :builtin "Each numeral denotes the corresponding integer as usual.") 
  ; successor
  (declare-const +1 (-> Int Int) 
   :builtin "+1 denotes the integer successor function")
  ; predecessor
  (declare-const -1 (-> Int Int) 
   :builtin "-1 denotes the integer predecessor function")
  ; <
  (declare-const < (-> Int Int Bool)
   :builtin "< denotes the standard strict total ordering over the integers."
   :chainable)

  ;-----------------------------------
  ; Axiomatized and defined constants
  ;----------------------------------- 

  ; <=
  (define-fun <= ((x Int) (y Int)) Bool
    (or (= x y) (< x y))
   :chainable)
  ; >
  (define-fun > ((x Int) (y Int)) Bool
    (< y x)
   :chainable)
  ; >=
  (define-fun >= ((x Int) (y Int)) Bool
    (or (= x y) (> x y))
   :chainable)
  ; unary -
  (define-fun-rec - ((x Int)) Int
    (ite (< x 0) (+1 (- (+1 x))) 
      (ite (= x 0) 0
        (-1 (- (-1 x))))))
  ; +
  (define-fun-rec + ((x Int) (y Int)) Int
    (ite (< y 0) (-1 (+ x (+1 y))) 
      (ite (= y 0) x 
        (+1 (+ x (-1 y)))))
   :left-assoc)
  ; binary -
  (define-fun - ((x Int) (y Int)) Int
    (+ x (- y))
   :left-assoc
   :no-partial 
   ; this constant cannot be applied partially:
   ; (- t) is not allowed but ((as - (-> Int Int Int)) t) is allowed
   )
  ; * 
  (define-fun-rec * ( (x Int) (y Int) ) Int
    (ite (< y 0) (- (* x (- y))) 
      (ite (= y 0) 0
        (+ x (* x (-1 y)))))
   :left-assoc)
  ; abs
  (define-fun abs ((x Int)) Int
    (ite (>= x 0) x (- x)))

  ; div 
  (declare-const div (-> Int (! Int :var n :restrict (!= n 0)) Int) 
   :left-assoc)
  ; mod
  (declare-fun mod (-> Int Int Int))
  ; div/mod axiom
  (assert (forall ((m Int) (n Int)) (=> (!= n 0)
    (let ((q (div m n)) (r (mod m n)))
      (and (= m (+ (* n q) r))
           (<= 0 r (-1 (abs n))))))))
      
  ; divisible by constant n
  (define-fun divisible ( (m Int) (n Int :restrict (> n 0)) ) Bool
    (= (mod m n) 0))


  (set-info :notes
   "The axiomatization of div and mod is based on [1].
   
    Regardless of the sign of m, 
    - when n is positive, (div m n) is the floor of the rational number m/n;
    - when n is negative, (div m n) is the ceiling of m/n.

    This contrasts with alternative but less robust definitions of div and mod
    where (div m n) is 
    - always the integer part of m/n (rounding towards 0), or 
    - always the floor of x/y (rounding towards -infinity).

    [1] Boute, Raymond T. (April 1992). 
        The Euclidean definition of the functions div and mod. 
        ACM Transactions on Programming Languages and Systems (TOPLAS) 
        ACM Press. 14 (2): 127 - 144. doi:10.1145/128861.128862.
   ")

  ; summation
  (define-fun-rec sum ((a Int) (b Int) (f (-> Int Int))) Int
    (ite (> a b) 0 
      (+ (f a) (sum (+1 a) b f))))

  ;-------- 
  ; Values
  ;--------
  ; The set of values for type Int consists of 
  ;  - all numerals,
  ;  - all terms of the form (- n) where n is a numeral other than 0.
  (define-syntax 
    ; non-terminals
    (<.int_value.> <.pos_numeral.>.>) 
    ; production rules
    ((<.int_value.> (<.numeral.> ("(" "-" <.pos_numeral.> ")")))
     (<.pos_numeral.> ((! <.numeral.> :syntax "numeral other than 0")))))

  (define-values () Int <.int_value.>)

  ;-------------------------------
  ; External and Bridging symbols
  ;-------------------------------
  ; Real
  (declare-type Real () :extern Reals::Real)

  (declare-const to_int (-> Real Int) 
   :builtin 
   "to_int denotes the function that maps each real number r 
    to its integer part, that is, to the largest integer n 
    that satisfies (<= (to_real n) r).")
))
(raw file)
(define-module Reals (
  (set-info :smt-lib-version 3.0)
  (set-info :smt-lib-release "2026-XX-XX")
  (set-info :written-by "Pascal Fontaine and Cesare Tinelli")
  (set-info :date "2026-03-15")
  (set-info :last-updated "2026-03-15")
  (set-info :update-history
   "TODO.
   "
  )
  (set-info :notes  "Real arithmetic")

  (import (Core))
  (open Core)

  ;---------------
  ; Builtin types
  ;---------------
  ; Real
  (declare-type Real () 
   :builtin "Real denotes the set of all real numbers."
  )

  ;-------------------------------
  ; External and Bridging symbols
  ;-------------------------------
  ; Int
  (declare-type Int () :extern Ints::Int)

  (declare-const to_real (-> Int Real) 
   :builtin "to_real as the standard injection of the integers into the reals.")

  ;-------------------
  ; Builtin constants
  ;------------------- 
  ; Decimals
  (declare-consts <.decimal.> Real 
   :builtin
   "Each <.decimal.> denotes the corresponding real number as usual.")

  ; negation
  (declare-const - (-> Real Real) :builtin "Unary negation.")
  ; +
  (declare-const + (-> Real Real Real) :left-assoc :builtin "Addition.")
  ; *
  (declare-const * (-> Real Real Real) :left-assoc :builtin "Multiplication.")
  ; <=
  (declare-const <= (-> Real Real Bool) 
   :chainable 
   :builtin "Usual non-strict total ordering over the reals.")
  ; is_int
  (declare-const is_int (-> Real Bool)
   :builtin "is_int maps whole real numbers to true and all the other reals to false.")

  ;-----------------------
  ; Axiomatized constants
  ;----------------------- 

  ; subtraction -
  (define-fun - ((x Real) (y Real)) Real
    (+ x (- y))
   :left-assoc)
  ; division /
  (declare-const / (-> Real Real Real) :left-assoc :restrict (!= y 0.0))
  (assert (forall ((x Real) (y Real)) (=> (distinct y 0.0)
    (= x (* y (/ x y))))))
  ; <
  (define-fun < ((x Real) (y Real)) Bool
    (and (<= x y) (!= x y))
   :chainable)
  ; >
  (define-const > ((x Real) (y Real)) Bool
    (< y x)
   :chainable)
  ; >=
  (define-const >= ((x Real) (y Real)) Bool
    (<= y x)
   :chainable)
  ; Summation 
  ; we use a step different from 1 and provide it as additional input
  (define-fun-rec sum ((a Real) (b Real) (f (-> Real Real))) Real
    (ite (> a b)  0.0 
      (+ (f a) (sum (+ a 1.0) b f))))
  ; rational constants
  (define-const / ((n Int :value) (d Int :value :restrict (> (to_real d) 0.0))) Real
    (/ (to_real n) (to_real d)))


  ;-------------
  ; Real values
  ;-------------

  ; The set of values for the sort Real consists of 
  ;  - an abstract value for each irrational algebraic number
  ;  - all decimals of the form n.0
  ;  - all terms of the form (- n.0) where n is a <.numeral.> other than 0
  ;  - all terms of the form (/ m n) or (- (/ m n)) where 
  ;    - m is a <.numeral.> other than 0,
  ;    - n is a <.numeral.> other than 0 and 1,
  ;    - as integers, m and n have no common factors besides 1.
  (define-syntax 
    ; non-terminals
    (<.real_value.> <.p.> <.f.> <.m.> <.n.>) 
    ; production rules
    ((<.real_value.> (<.abstract.> ; for irrational algebraic numbers
                      "0.0"
                      <.p.>
                      "(" "-" <.p.> ")"
                      <.f.>
                      "(" "-" <.f.> ")")
     (<.f.> ((! "(" "/" <.m.> <.n.> ")" :syntax 
                          "as integers, <.m.> and <.n.> have no common factors besides 1")))
     (<.p.> ((! <.decimal.> :syntax "it has the form n.0 except for 0.0")))
     (<.m.> ((! <.numeral.> :syntax "it is not 0")))
     (<.n.> ((! <.numeral.> :syntax "it is neither 0 nor 1"))))))

  (define-values () Real <.real_value.>)

  (set-info :notes
   "In Real values decimals are used only for whole numbers, e.g., 42.0. 
   For all other rational numbers the fractional notation is necessary, e.g., (/ 1 2).
   This way, there is unique value for each rational."
  )

  (set-info :notes
   "The restriction of Reals over the signature having just the symbols 
    (0.0 Real)
    (1.0 Real)
    (- (-> Real Real))
    (+ (-> Real Real Real))
    (* (-> Real Real Real))
    (<= (-> Real Real Bool))
    (<  (-> Real Real Bool))
    coincides with the theory of real closed fields, axiomatized by
    the formulas below: 

    - associativity of +
      (forall ((x Real) (y Real) (z Real)) (= (+ (+ x y) z) (+ x (+ y z))))

    - commutativity of +
      (forall ((x Real) (y Real)) (= (* x y) (* y x)))

    - 0.0 is the right (and by commutativity, left) unit of +
      (forall ((x Real)) (= (+ x 0.0) x))

    - right (and left) inverse wrt +
      (forall ((x Real)) (= (+ x (- x)) 0.0))

    - associativity of *
      (forall ((x Real) (y Real) (z Real)) (= (* (* x y) z) (* x (* y z))))

    - commutativity of *
      (forall ((x Real) (y Real)) (= (* x y) (* y x)))

    - 1.0 is the right (and by commutativity, left) unit of *
      (forall ((x Real)) (= (* x 1.0) x))

    - existence of right (and left) inverse wrt *
      (forall ((x Real)) (or (= x 0) (exists (y Real) (= (* x y) 1.0))))

    - left distributivity of * over +
      (forall ((x Real) (y Real) (z Real)) (= (* x (+ y z)) (+ (* x y) (* x z))))

    - right distributivity of * over +
      (forall ((x Real) (y Real) (z Real)) (= (* (+ x y) z) (+ (* x z) (* y z))))

    - non-triviality
      (!= 0.0 1.0)

    - all positive elements have a square root
      (forall ((x Real)) (exists ((y Real))
        (or (= x (* y y)) (= (- x) (* y y)))))

    - axiom schemas for all n > 0
      (forall ((x_1 Real) ... (x_n Real))
        (distinct (- 1) (+ (* x_1 x_1) (+ ... (* x_n x_n))))) 

    - axiom schemas for all odd n > 0 where (^ y n) abbreviates
      the n-fold product of y with itself
      (forall ((x_1 Real) ... (x_n Real)) (exists ((y Real))
        (= 0 (+ (^ y n) (+ (* x_1 (^ y n-1)) (+  ... (+ (* x_{n-1} y) x_n)))))))

    - reflexivity of <=
      (forall ((x Real)) (<= x x))

    - antisymmetry of <=
      (forall ((x Real) (y Real)) (=> (and (<= x y) (<= y x)) (= x y)))

    - transitivity of <=
      (forall ((x Real) (y Real) (z Real)) (=> (<= x y z)) (<= x z))

    - totality of <=
      (forall ((x Real) (y Real)) (or (<= x y) (<= y x)))
    
    - monotonicity of <= wrt +
      (forall ((x Real) (y Real) (z Real)) (=> (<= x y) (<= (+ x z) (+ y z))))

    - monotonicity of <= wrt *
      (forall (x Real) (y Real) (z Real) 
        (=> (and (<= x y) (<= 0 z)) (<= (* z x) (* z y))))

    - definition of <
      (forall ((x Real) (y Real)) 
        (= (< x y) (and (<= x y) (distinct x y))))

    References:
    1) W. Hodges. Model theory. Cambridge University Press, 1993.
    2) PlanetMath, http://planetmath.org/encyclopedia/RealClosedFields.html
  ")
))
(raw file)
[Coming soon.]
[Coming soon.]

SMT-LIB 3 Logics

One of the goals of SMT-LIB 3 is to get rid of the notion of SMT-LIB logic (such as QF_UF, LIA, and so on) and provide users the ability, in effect, to construct logics on the fly by importing the right modules with the right restrictions at the beginning of an SMT-LIB script.

With some limitations, modules, interfaces and imports provide a suitable mechanism for indicating to an SMT solver a specific fragment in which to reason, allowing the solver to apply the best reasoning techniques at its disposal for that fragment, or recognize it as unsupported.

SMT-LIB logics, however, have been used historically also for another, orthogonal purpose: providing a way to index/structure the SMT-LIB benchmark library. Concurrently, they have also been used by the SMT-COMP competition as a convenient way to structure the competition in divisions. These uses of logics make them still necessary. So the set-logic will not go away. However, in SMT-LIB 3 it becomes an abbreviation of a particular sequence of module declarations and imports.

For example, (set-logic QF_UF) could become an abbreviation of

(define-module QF_UF (
  (import (Core))
  (open Core) 
 )  
 :type-vars (A)
 :types (Bool)
 :consts (
    (true Bool)
    (false Bool)
    (ite (→ Bool A A A))
    (not (→ Bool Bool))
    (= (→ A A Bool) :pairwise)
    (!= (→ A A Bool))
    (distinct (→ (A A Bool)) :pairwise)
    (=> (→ Bool Bool Bool) :right-assoc)
    (and (→ Bool Bool Bool) :left-assoc)
    (or (→ Bool Bool Bool) :left-assoc)
    (xor (→ Bool Bool Bool) :left-assoc)
    (<=> (→ Bool Bool Bool) :left-assoc)
    ; quantifiers and other higher-order constants are not exported
 )
 :FOL ; only FOL syntax allowed
)
(import (QF_UF))
(open QF_UF)
...
The command (set-info :FOL) is a (imperfect) solution of the problem of capturing the Version 2.7 restriction for terms to be in the applicative fragment where all function symbols (i.e., non-nullary constants of type (→ τ₁ ⋅⋅⋅ τᵢ)) are fully applied (i,e., applied to exactly i arguments).

Similarly, for the logic LIA (set-logic LIA) would be and abbreviation of

(define-module LIA (
  (import (Core Ints))
  (open Core)
  (open Ints)
 )
 :type-vars (A)
 :types (Bool Int)
 :consts (
    (<.numeral.> Int)
    (+ (→ Int Int Int) :left-assoc)
    (- (→ Int Int Int) :left-assoc)
    (* (→ (! Int :syntax <.int_value.>) (! Int :syntax <.symbol.>) Int)) ; linear mul.
    (* (→ (! Int :syntax <.symbol.>) (! Int :syntax <.int_value.>) Int)) ; linear mul.
    (* (→ (! Int :syntax <.symbol.>) (! Int :syntax <.symbol.>) Int)
      :left-assoc) ; linear mul.
    (> (→ Int Int Bool) :chainable)
    (>= (→ Int Int Bool) :chainable)
    (< (→ Int Int Bool) :chainable)
    (<= (→ Int Int Bool) :chainable)
    (true Bool)
    (false Bool)
    (ite (→ Bool A A A))
    (not (→ Bool Bool))
    (= (→ A A Bool) :pairwise)
    (!= (→ A A Bool))
    (distinct (→ (A A Bool)) :pairwise)
    (=> (→ Bool Bool Bool) :right-assoc)
    (and (→ Bool Bool Bool) :left-assoc)
    (or (→ Bool Bool Bool) :left-assoc)
    (xor (→ Bool Bool Bool) :left-assoc)
    (<=> (→ Bool Bool Bool) :left-assoc)
    (forall (→ (→ A Bool) Bool))
    (exists (→ (→ A Bool) Bool))
 )
 :FOL ; only FOL syntax allowed
)
(import (LIA))
(open LIA)
Note that the linearity restriction does not need an ad-hoc attribute thanks to the restriction imposed on the type of the multiplication symbol * requiring at leas one of its two arguments to be a concrete integer value. Also note that * is (intentionally) not given a :left-assoc designation as terms like (* 3 x y) would not satisfy the linearity restrictions.