These are OCaml bindings for the Z3 SMT solver that closely follow the Z3 C API. This differs from the official Z3 OCaml bindings which provide a more object-oriented wrapper interface.
Goals:
-
The binding layer should be as thin as possible, avoiding surprises. The OCaml binding of a C function converts the arguments, calls the C function, and converts the results. Intermediate data structure conversions, hidden caches, etc. are avoided. OCaml functions that do more, e.g.
mk_contextandmk_datatypes, have OCaml API documentation. -
The OCaml interface should be close enough to the C interface that it is effective for users to read the Z3 C API documentation and follow the types of the OCaml functions. Function signatures are translated systematically with very few exceptions, so the friction should be low.
opam install nonstd-z3Note this requires the conf-z3 opam package, which locates an already-built Z3 library and header files from system packages or a local build.
The bindings automatically manage Z3's reference counting. The inc_ref and dec_ref functions are not exposed in the OCaml API. Instead, inc_ref is called when Z3 values are returned to OCaml, and dec_ref is called by OCaml finalizers when the values are garbage collected.
Z3 errors raise a Fatal exception. These exceptions are fatal: Z3 may be left in an inconsistent internal state, and it is unsafe to call any Z3 function after a Fatal exception has been raised.
This is a design trade-off for efficiency. The OCaml exception is raised from within Z3's C++ code, which means the C++ stack is not unwound correctly. An alternative design could propagate errors to the C/OCaml boundary and only raise an OCaml exception there.
Z3 contexts are intentionally never freed. This is because OCaml finalizers for Z3 values may call dec_ref at any time, and calling dec_ref after del_context is unsafe. The memory management functions reset_memory and finalize_memory are also not exposed for the same reason.
This is a design trade-off for efficiency. Alternative designs could prevent contexts from being freed while live Z3 values exist.
The test/ directory contains a test suite demonstrating usage of various Z3 features including user propagators and parametric datatypes.
The Z3 bindings are generated through several stages involving opam package configuration, header file parsing, IDL generation, and CamlIDL processing.
The conf-z3 opam package provides the location of the Z3 library installation. This can be either a system-installed package or a local build from source. The dune file queries opam for:
opam var conf-z3:includedir- location of Z3 header filesopam var conf-z3:cflags- compiler flags for C compilationopam var conf-z3:libs- linker flags for the Z3 library
The dune file passes the Z3 include directory to the gen_z3_idl.ml script, which reads the Z3 header files and generates IDL code. CamlIDL then processes this IDL code to produce the z3.ml, z3.mli, and z3_stubs.c files that form the bindings.
The gen_z3_idl.ml script performs several steps:
-
Type conversions: Defines custom conversions between C and OCaml types. These handle the interoperation between Z3's reference counting and OCaml's garbage collection. Reference-counted Z3 types are wrapped in OCaml custom blocks with finalizers that call the appropriate
dec_reffunctions. Theinc_refcalls happen when values are returned from Z3 to OCaml. Some Z3 types (Z3_ast and its subtypes) also support polymorphic comparison and hashing. -
Enum translation: Reads the Z3 header files to find
typedef enumdefinitions and translates them to IDL enum declarations. This header file parsing is hacky. -
API translation: Reads the Z3 header files to find
def_APIandextra_APIcomments that declare the exposed C API. These are parsed and translated into IDL function declarations with type annotations for input/output parameters, arrays with length parameters, nullable pointers, etc. Thedef_APIannotations do not fully express the information that CamlIDL needs, so a few functions require hand-written IDL declarations. -
Safe OCaml interfaces: Emits custom wrappers for Z3 features that need a different style of interface than the C API to be safe in OCaml:
- Datatype creation (
mk_datatype,mk_polymorphic_datatype,mk_datatypes) without exposing theZ3_constructortype, whose values cannot be freed safely - Context creation (
mk_context) without exposing theZ3_configtype
- Datatype creation (
-
Function pointer callbacks: Emits custom bindings for Z3 features that rely on function pointers, such as the user propagator interface.