Yaspar-IR
This crate provides a few representations of SMT scripts and other functionalities.
Introduction
A typical use of the crate
We could use this crate to parse and analyze SMTLib scripts. This can be achieved in the following workflow:
Checked v.s. Unchecked Building APIs
In general, we maintain a global top-level context to keep track of SMTLib objects and their validity:
let mut context = Context::new();
Via context, we can build typed SMTLib objects like Commands, Terms, and Sorts programmatically. Expectedly,
these
objects have well-formednes invariants. Users have two choices: either maintain the invariants manually via untyped
APIs,
or use the checked building APIs, which operations in a type-checking TC<T> monad.
To illustrate, the following snippet uses only the unchecked APIs to build (+ x y):
The code above is error-prone for the following reasons:
- it allocates the
Intsort, which might not exist in the current logic, e.g.QF_NRAincludes noInt. - Either
xorymight not be present in the current context, depending on the setup, as they require declarations.
As a result, the addition itself might not be well-formed. Therefore, this code, even though the assertion will still succeed, might not maintain the invariants.
However, since these APIs are low-level and thus efficient, advanced users could use them for efficient term building, provided that they are responsible for maintaining the invariants.
Instead, we could write the following snippet, which relies on checked APIs to maintain the invariants; hence users are relieved from the responsibility.
All checked APIs are in the form of typed_* for building terms, and wf_* for building sorts. See
the module ast::ctx::checked, and traits CheckedApi and ScopedSortApi for the full list of APIs.
Checked APIs in detail
The checked APIs are organized around two traits and a set of builder context types. Together, they form a scoped, type-safe interface for constructing well-formed SMTLib objects.
The TC<T> monad
All checked APIs return TC<T>, which is an alias for Result<T, String>. When a well-formedness
invariant is violated (e.g. a symbol is not in scope, a sort mismatch occurs, or an argument count
is wrong), the API returns an Err with a descriptive message. This means callers can use ? to
propagate errors ergonomically.
The CheckedApi trait — building terms
CheckedApi is implemented by Context and by all builder context types (see below). It provides
the following categories of functions:
Symbols and identifiers:
| Method | Description |
|---|---|
typed_symbol(name) |
Look up a declared/defined symbol by name. Returns Err if not in scope or ambiguous (e.g. overloaded). |
typed_symbol_with_sort(name, sort) |
Look up a symbol and disambiguate by sort. Useful for polymorphic constructors like nil. |
typed_identifier(qid) |
Look up a fully qualified identifier. |
Function application:
| Method | Description |
|---|---|
typed_app(qid, args) |
Apply a qualified identifier to arguments. Checks arity and argument sorts. |
typed_simp_app(name, args) |
Convenience wrapper: apply a function by name string. |
typed_app_with_kind(kind, args) |
Apply a builtin IdentifierKind (e.g. indexed operators). |
Literals:
| Method | Description |
|---|---|
numeral(n) |
Build a numeral literal from a UBig. |
integer(i) |
Build an integer literal from an IBig (wraps negation if needed). |
typed_constant(c) |
Build a typed constant from a Constant value. |
Logical connectives (all arguments must be Bool):
| Method | Description |
|---|---|
typed_eq(a, b) |
Equality (= a b). Both arguments must have the same sort. |
typed_distinct(ts) |
(distinct ...). At least two arguments required. |
typed_and(ts) |
(and ...). At least one argument. |
typed_or(ts) |
(or ...). At least one argument. |
typed_xor(ts) |
(xor ...). At least two arguments. |
typed_not(t) |
(not t). |
typed_implies(premises, concl) |
(=> p1 p2 ... concl). |
typed_ite(cond, then, else) |
(ite c t e). Condition must be Bool; branches must have the same sort. |
Builder contexts (scoped sub-environments):
| Method | Returns | Purpose |
|---|---|---|
build_quantifier() |
TC<QuantifierContext> |
Enter a scope for building forall/exists. |
build_quantifier_with_domain(bindings) |
TC<QuantifierContext> |
Shorthand: enter a quantifier scope with pre-declared variables. |
build_let(bindings) |
TC<LetContext> |
Enter a scope for building let bindings. |
build_matching(scrutinee) |
TC<MatchContext> |
Enter a scope for building match expressions. |
The ScopedSortApi trait — building sorts
ScopedSortApi is automatically implemented for any type that implements CheckedApi. It provides
well-formedness-checked sort construction:
| Method | Description |
|---|---|
wf_sort(name) |
Look up a sort by name (e.g. "Int", "Bool", a user-defined sort). |
wf_sort_n(name, params) |
Parameterized sort (e.g. wf_sort_n("List", [int]) for (List Int)). |
wf_sort_id(id, params) |
Sort from an Identifier and parameters. |
wf_bv_sort(len) |
Bitvector sort (_ BitVec len). Validates length > 0 and within bounds. |
These return Err when the sort doesn't exist in the current logic, has the wrong number of
parameters, or is otherwise invalid.
Builder context types
Builder contexts are scoped environments that extend the current context with local bindings. They
implement CheckedApi themselves, so all term-building functions are available inside them. The key
pattern is: create a builder, build terms within it, then finalize.
QuantifierContext — for forall and exists:
let mut context = new;
context.ensure_logic;
let int = context.int_sort;
// Option 1: extend incrementally
let mut q_ctx = context.build_quantifier ?;
q_ctx.extend ?.extend ?;
// Option 2: provide domain upfront (equivalent)
let mut q_ctx = context.build_quantifier_with_domain ?;
// Build terms using the local variables
let body = q_ctx.typed_simp_app ?;
// Finalize — consumes the context
let forall_term = q_ctx.typed_forall ?; // or .typed_exists(body)?
The body must be a Bool-sorted term. Duplicate variable names are rejected.
LetContext — for let bindings:
// Bindings are provided at creation time (they are well-formed in the parent scope)
let bound_term = context.typed_simp_app ?;
let mut l_ctx = context.build_let ?;
// "sum" is now available as a local variable
let body = l_ctx.typed_simp_app ?;
// Finalize
let let_term = l_ctx.typed_let;
Note that typed_let does not return TC — it always succeeds because the bindings were already
validated at context creation.
MatchContext and ArmContext — for match expressions:
// Assume List datatype is declared and l1 : (List Int)
let mut m_ctx = context.build_matching ?;
// Build the nil arm (nullary constructor)
let nil_arm = m_ctx.build_arm_nullary ?;
nil_arm.typed_arm ?;
// Build the cons arm with named variables
let mut cons_arm = m_ctx.build_arm ?;
// "h" and "t" are now in scope within cons_arm
let h = cons_arm.typed_symbol ?;
let t = cons_arm.typed_symbol ?;
let body = /* ... build body using h and t ... */;
cons_arm.typed_arm ?;
// Finalize — all constructors must be covered (or a wildcard must be present)
let match_term = m_ctx.typed_matching ?;
The match context tracks constructor coverage. typed_matching() returns Err if not all
constructors are covered. Use build_arm_wildcard(var) or build_arm_catchall() for wildcard
arms. All arm bodies must have the same sort.
FunctionContext — for define-fun:
let int = context.int_sort;
let mut f_ctx = context.build_fun_out_sort ?;
let x = f_ctx.typed_symbol ?;
let body = f_ctx.typed_simp_app ?;
let cmd = f_ctx.typed_define_fun ?;
// "double" is now in the global context
Use build_fun (without output sort) to let the sort be inferred from the body.
RecFunsContext and EachRecFunContext — for define-fun-rec / define-funs-rec:
let int = context.int_sort;
let list_int = context.wf_sort_n ?;
let mut ctx = context.build_rec_funs ?;
let mut f_ctx = ctx.build_function ?;
// The function "length" is already in scope (for recursive calls)
let body = /* ... */;
f_ctx.typed_function ?;
let cmd = ctx.typed_define_funs_rec ?;
All declared functions must be given a body before calling typed_define_funs_rec().
DatatypeContext and DtDeclContext — for declare-datatype(s):
// Simple enum
let cmd = context.typed_enum ?;
// Polymorphic datatype
let mut d_ctx = context.build_datatypes ?;
let mut c_ctx = d_ctx.build_datatype ?;
c_ctx.build_datatype_constructor_nullary ?;
let xvar = c_ctx.wf_sort ?; // sort parameter is in scope
let list_x = c_ctx.wf_sort_n ?;
c_ctx.build_datatype_constructor ?;
c_ctx.typed_datatype ?;
let cmd = d_ctx.typed_declare_datatypes ?;
Datatype contexts validate non-emptiness, constructor uniqueness, and selector name uniqueness.
If the context is dropped without calling typed_declare_datatypes(), no changes are made to the
global context (the operation is transactional).
DefSortContext — for define-sort:
let int = context.int_sort;
let s_ctx = context.build_sort_alias ?;
let cmd = s_ctx.typed_define_sort ?;
// "MyInt" is now an alias for Int
For parameterized sort aliases, the sort parameters are available via wf_sort inside the context.
Top-level command helpers on Context:
| Method | Description |
|---|---|
typed_assert(t) |
Build (assert t). Validates t is Bool and processes :named annotations. |
typed_define_const(name, body) |
Build (define-const name sort body) with inferred sort. |
typed_define_const_sorted(name, sort, body) |
Same, but validates the body matches the declared sort. |
typed_set_option(opt) |
Build (set-option ...) with keyword-specific validation. |
typed_check_sat_assuming(assumptions) |
Build (check-sat-assuming ...). All assumptions must be Bool. |
Context nesting
Builder contexts can be nested. For example, a QuantifierContext can create a LetContext, which
can create another QuantifierContext, and so on. Each nested context sees all bindings from its
ancestors:
let mut q_ctx = context.build_quantifier_with_domain ?;
let inc_x = q_ctx.typed_simp_app ?;
let mut l_ctx = q_ctx.build_let ?;
// "y" and "x" are both in scope here
let body = l_ctx.typed_simp_app ?;
let let_term = l_ctx.typed_let;
let forall = q_ctx.typed_forall ?;
Analyzing hashconsed objects
Typed ASTs in this crate are hashconsed to optimize memory and run time efficiency. It is still possible to pattern
match on terms by calling .repr(). For example, the following function computes the depth of a given term:
More examples
More use of APIs can be found in the tests/ folder.
A parametric, algebraic representation of ASTs
For a package that provides functionalities for SMT scripts, sits at the core a parametric, algebraic representation of
ASTs.
This representation is defined by various enums and structs in raw::alg. Please consult the doc of the module for more
details.
This representation is flexible and allows two benefits:
-
We can instantiate this representation into different instances. In this package, there are two different instances of ASTs:
- an untyped representation with location information (
ast::u), and - a hashcons-ed, typed representation (
ast).
The former is a faithful parsing result of a grammatically correct SMT script, which could be potentially semantically mal-formed. Through type-checking (by calling
.type_check(), we convert an untyped AST to a typed AST, which is more compactly stored in memory via a hashconsing arena. If type-checking fails, the location information of the untyped representation is used for error reporting. - an untyped representation with location information (
-
We can achieve very good code reuse. In particular, functions implemented for the algebraic representation automatically works for all instantiations. For instance, the printing implementation works automatically for both typed and untyped representations. The type-checking algorithm also applies for both untyped (to obtain a typed AST) and typed (to re-check an AST constructed by untyped APIs) ASTs.
Use the macro instantiate_ast! to instantiate more variants!
Functionalities provided by the package
Currently, the crate provides the following functionalities:
- Parsing to an untyped representation: see functions exposed by
ast::u::UntypedAst. This functionality usesyasparunder the hood. - Typechecking: see
ast::Typecheck. After invoking.type_check(), we are handling the typed representation and can assume type invariants of the representation. All typed ASTs are managed byast::Context, which keeps track of the current logic, symbols, sorts, and cache. Type-checking commands has the side effect of potentially extending the context. - A set of unchecked APIs for building typed ASTs. This functionality is achieved using allocator functions exposed by
ast::Context. - A set of checked APIs for building typed ASTs. This functionality is exposed by the
CheckedApiandScopedSortApitraits, and the builder context types inast::ctx. See the Checked APIs in detail section for a comprehensive guide. - Let-elimination: see
ast::LetElim. - Computing free variables of a given term: see
ast::FreeLocalVars. - A fresh variable allocator, which returns a fresh symbol that has not been used prior to the point of allocation: see
FreshVar. - A compact infrastructure for let-introduction based on topological sorting: see
ast::TopoLetIntro. This functionality introduces let-bindings to terms, so that they can be compactly printed with let-bindings inserted for sub-terms appearing multiple times. - Global and local substitutions; see
ast::Substituteandast::GlobalSubst. - NNF and CNF conversion: see
ast::CNFConversion. This functionality requires the featurecnf. - Implicant computation: see
ast::FindImplicant. This functionality requires the featureimplicant-generation. - Translation to cvc5: see the
cvc5module and theConvertToCvc5trait. This functionality requires the featurecvc5. It translates typedSorts,Terms, andCommands to their cvc5-rs counterparts, with caching and support for quantifier:patternannotations.
Translation to cvc5
The cvc5 module exposes the ConvertToCvc5<Env, A> trait, which provides a uniform .to_cvc5(env, ctx) method for
translating sorts, terms, and commands. Two environment types are used:
Cvc5Env— wraps aTermManagerand caches. Used for translatingSorts andTerms.Cvc5EnvSolver— wraps aCvc5Envand aSolver. Used for translatingCommands.
use ;
use ;
use ;
use UntypedAst;
SMTLib compliance
This crate is completely SMTLib-2.7-compliant. Namely, it follows the SMTLib spec and fully supports specified features (with exceptions below), including quantifiers and datatypes. Extension theories supported by z3 or cvc5 are usually not considered.
The following features are intensionally avoided, but we welcome contributors to extend them:
- floating points,
- higher order logic, and
- prenex polymorphism in user-defined sorts and functions.
Datatypes
This crate fully supports features of datatypes as described by the SMTLib standard. More specifically, it supports:
- Polymorphic, mutually recursive datatype declarations,
- Well-formedness and non-emptiness checking, as described by the SMTLib standard,
- Constructors, selectors and testers generation; In particular, this crate supports both
(_ is X)tester (standard), andis-Xtester (common extension and de facto standard).is-Xis defined in terms of(_ is X)as a definition. - Match expressions.
Security
See CONTRIBUTING for more information.
License
This project is licensed under the Apache-2.0 License.