Lisp-Z3 Interface
- Lisp-Z3 is a Common Lisp interface to the Z3 SMT solver that integrates with ACL2s/ASPF, enabling hybrid reasoning without depending solely on ACL2/s.
- Its two-layer architecture combines low-level CFFI wrappers and a high-level SMT-LIB2-like API to manage contexts, solvers, and efficient variable declarations.
- Practical applications such as Sudoku solving, SeqSolve, and wireless-router fuzzing demonstrate its capabilities in achieving high performance and incremental constraint solving.
Searching arXiv for papers directly relevant to Lisp-Z3 and closely related ACL2/Z3 integrations. Lisp-Z3 is a Common Lisp interface to the Z3 satisfiability modulo theories solver, introduced as an extension to the ACL2s Systems Programming Framework (ASPF) that allows tools written using the full feature set of Common Lisp to use both ACL2/s and Z3 as services. Although designed to integrate smoothly with ACL2 and ACL2s, it does not depend on the availability of ACL2/s and is usable by anyone who would like to interact with Z3 from Common Lisp (Walter et al., 25 Jul 2025).
1. Definition and position within the ACL2s ecosystem
Lisp-Z3 is described as a major extension of ASPF. In the terminology used around ACL2, ACL2 is a first-order, untyped theorem prover based on a subset of Common Lisp, ACL2s is an extension of ACL2 with facilities including Defdata types, definec/property, counterexample generation, and strong termination analysis, and ASPF lets one write full Common Lisp code that uses ACL2 or ACL2s as a backend prover (Walter et al., 25 Jul 2025).
Within that setting, Lisp-Z3 adds a second backend reasoner. A single Common Lisp tool can call ACL2 or ACL2s for interactive theorem proving, type checking, termination reasoning, and related services, while also calling Z3 for SMT solving. The same interface is also usable in plain Common Lisp, without ACL2 or ACL2s. This division of labor is central to the design: ACL2/ACL2s and Z3 are not merged into a single logic, but exposed as complementary services through one systems-programming interface (Walter et al., 25 Jul 2025).
The motivating requirements were incremental solving, low latency, integration with ASPF, and portability across Lisp implementations. The paper states that existing interfaces either did not integrate well with ACL2s/ASPF, did not provide the required latency or incremental capabilities, or were not sufficiently portable across Lisp implementations. Lisp-Z3 was therefore designed as both an ACL2s-oriented extension and a general-purpose Common Lisp interface to Z3 (Walter et al., 25 Jul 2025).
2. Architecture, bindings, and internal representation
Lisp-Z3 has a two-layer architecture. The low-level layer consists of direct wrappers for Z3’s C API via CFFI. The high-level layer manages Z3 contexts and solvers, translates Lisp S-expressions into Z3 abstract syntax trees, mimics SMT-LIB2 commands such as declare-const, assert, and check-sat, and converts models back into Lisp data (Walter et al., 25 Jul 2025).
The binding infrastructure uses CFFI and the CFFI Groveller. A Grovel file describes Z3 types of interest, the Groveller generates and runs C code, and the result is turned into Lisp information about type sizes and layouts. A Python script reads Z3’s headers and generates the Grovel file so that Lisp-Z3 can stay synchronized with relatively recent Z3 versions. This architecture is explicitly intended to support portability across multiple Common Lisp implementations and platforms (Walter et al., 25 Jul 2025).
At the solver level, Lisp-Z3 mirrors Z3’s context-based execution model. On initialization, solver-init creates a Z3 context and a default solver. Lisp-side wrapper objects carry both the underlying C pointer and the corresponding Z3 context. Lisp-Z3 also maintains an environment stack aligned with the solver’s assertion stack. Variable declarations are attached to assertion levels, and the environment enforces a Common Lisp-like discipline in which, at any time, a name denotes at most one free variable, with no overloading by sort (Walter et al., 25 Jul 2025).
The sort interface follows Z3’s many-sorted logic while remaining Lisp-oriented. Sort specifiers can denote non-parametric sorts such as :int, :bool, and :string, parametric sorts such as (:seq :int), and function ranks of the form (:fn (<p1> ... <pn>) <r>). Lisp-Z3 also exposes user-defined sorts through register-enum-sort and register-tuple-sort, implemented internally using Z3’s datatype mechanisms (Walter et al., 25 Jul 2025).
Memory management is handled by combining Z3’s reference counting with Lisp finalization. The paper states that Lisp-Z3 uses the trivial-garbage library so that each wrapper object registers a finalizer which decrements the Z3 reference count when the Lisp object is garbage-collected. This avoids manual reference-count management in user code (Walter et al., 25 Jul 2025).
3. Programming model and solver interaction
The high-level interface is SMT-LIB2-like. A typical workflow is to call solver-init, declare variables with declare-const, assert constraints with z3-assert, call check-sat, and inspect the resulting model through get-model, get-model-as-assignment, or eval-under-model. The paper’s basic example declares x : Bool and y : Int, asserts (and x (>= y 5)), receives :SAT from check-sat, and obtains the assignment ((X T) (Y 5)) from get-model-as-assignment (Walter et al., 25 Jul 2025).
z3-assert also supports inline declarations. Instead of declaring variables separately, one may write a declaration list such as (x :bool y :int) as the first argument to z3-assert. Those declarations are inserted into the solver’s environment stack and remain available at the appropriate assertion levels. This gives the interface a direct resemblance to SMT-LIB2 while preserving normal Lisp dataflow and macro expansion patterns (Walter et al., 25 Jul 2025).
Incremental solving is exposed through solver-push and solver-pop, directly reflecting Z3’s assertion stack. This is not merely a convenience feature; it is one of the design points emphasized in the paper because it supports applications that repeatedly refine or backtrack constraints, such as SeqSolve and the Sudoku solver. The environment stack is synchronized with these push/pop operations (Walter et al., 25 Jul 2025).
Model interpretation is intentionally Lisp-facing. Sequences represented by Z3 as nested concatenations are converted into Lisp values when possible. Function interpretations are converted into map-like Lisp structures consisting of explicit entries together with a :default value. eval-under-model allows a Lisp expression parsed in the same way as z3-assert to be evaluated in a model, optionally completing the model if some symbols do not yet have interpretations (Walter et al., 25 Jul 2025).
The interface also exposes lower-level access close to the C API, including contexts, solvers, AST construction, and model traversal. The paper presents this layer as deliberately verbose and tedious compared with the high-level API; its role is completeness and direct access rather than everyday usability (Walter et al., 25 Jul 2025).
4. Semantics, soundness boundaries, and relation to ACL2 tooling
Expressions passed through z3-assert are interpreted with Z3 semantics, not ACL2 semantics and not ordinary Common Lisp semantics. The paper emphasizes this with a concrete mismatch: an ACL2 property asserting (= (+ x y) y) for x :bool and y :int is true in ACL2 because non-numeric arguments to + are treated as 0, whereas a naïve Z3 translation via Lisp-Z3 can make the negation satisfiable because Z3 treats true as 1 and false as 0 in arithmetic (Walter et al., 25 Jul 2025).
For that reason, Lisp-Z3 is not presented as a proof tactic or clause processor. It is a tool-level integration. ACL2s may be used for definitional reasoning, theorem proving, type discipline, and counterexample infrastructure, while Z3 is used for SMT-native reasoning over arithmetic, arrays, sequences, strings, bit-vectors, and related theories. When soundness inside ACL2 is required, Lisp-Z3 can be used as an oracle whose outputs are subsequently validated in ACL2 rather than trusted as a semantics-preserving ACL2 proof step (Walter et al., 25 Jul 2025).
This design differs sharply from Smtlink 2.0, which is a verified bridge from ACL2 to SMT solvers. Smtlink 2.0 translates ACL2 goals using a sequence of verified clause processors and computed hints, and only the final transliteration from ACL2 to Z3’s Python interface requires a trusted clause processor. It also supports fty::defprod, deflist, defalist, and defoption by using Z3’s arrays and user-defined data types (Peng et al., 2018). Lisp-Z3, by contrast, prioritizes systems programming, latency, incremental interaction, and direct Common Lisp access to Z3 rather than proof-producing translation from ACL2 logic.
A plausible implication is that Lisp-Z3 and Smtlink occupy complementary positions. One provides a programming-oriented bridge from Common Lisp and ACL2s-centered tools to Z3; the other provides a proof-oriented, soundness-focused translation pipeline from ACL2 formulas to SMT obligations.
5. Applications and reported performance
The paper discusses three applications: a Sudoku solver, SeqSolve, and hardware-in-the-loop fuzzing of wireless routers. In all three cases, Lisp-Z3 is used as a service inside a larger Common Lisp system, and in the latter two cases the applications explicitly leverage the ability to integrate Z3 with ACL2s code (Walter et al., 25 Jul 2025).
| Application | Role of Lisp-Z3 | Reported outcome |
|---|---|---|
| Sudoku solver | Integer variables, range constraints, distinct, incremental push/pop |
Pure Common Lisp + Lisp-Z3; ACL2/ACL2s not required |
| SeqSolve | Incremental LIA solving over string lengths within an ACL2s-based string solver | Solved a larger number of benchmark problems more quickly than any other existing solver at the time of its publishing |
| Wireless-router fuzzing | Z3 solves numeric constraints; ACL2s enumerators generate structured frames | Combined method was nearly two orders of magnitude faster than pure Z3 and pure ACL2s over the satisfiable range |
The Sudoku solver is encoded in the standard SMT style: 81 integer variables constrained to the range $1$ through $9$, together with row, column, and box distinctness constraints. The structural constraints are asserted once, and individual puzzles are solved by pushing a solver frame, asserting clue constraints, checking satisfiability, extracting a model, and popping the frame (Walter et al., 25 Jul 2025).
SeqSolve is a string solver that combines an algorithm due to Kumar and Manolios, ACL2s code for structured data and invariants, and Lisp-Z3 for the linear arithmetic constraints over string lengths that arise during solving. The paper states that SeqSolve solved a larger number of benchmark problems more quickly than any other existing solver at the time of its publishing. This application highlights incremental solving and low-overhead repeated Z3 invocation (Walter et al., 25 Jul 2025).
The wireless-router application targets IEEE 802.11 probe request frames. The frame body is a sequence of elements whose structure depends on element identifiers, and valid frame sizes lie between 172 and 2741 bytes. A pure Z3 approach and a pure ACL2s enumerative approach were both considered. The successful design, called ACL2s-ETC, splits the task: Z3 solves numeric constraints such as frame size, and ACL2s defdata enumerators generate structured packet contents consistent with those constraints. The reported result is nearly two orders of magnitude more throughput than pure Z3 or pure ACL2s over the satisfiable range, while low latency remained a central goal (Walter et al., 25 Jul 2025).
6. Ecosystem, limitations, and future directions
Because Lisp-Z3 is a Common Lisp interface to the Z3 C API rather than a separate solver, it inherits changes in the underlying Z3 ecosystem. This matters for backend substitution. Z3-Noodler, for example, is described as a fork of Z3 that preserves the interface, and for users of language bindings, including Lisp, it should be a drop-in replacement binary or library while preserving Lisp-side code unchanged (Chen et al., 2023). Likewise, PolySAT is integrated into Z3, and from the perspective of users driving Z3 through language bindings, it changes how many bit-vector problems are solved by keeping arithmetic-heavy reasoning at the word level rather than immediately bit-blasting (Rath et al., 2024).
The paper also records several limitations of Lisp-Z3 itself. Some Z3 model values are only approximately translated back to Lisp: algebraic numbers are approximated as floating-point numbers by default, losing exactness. Not all Z3 features and SMT-LIB2 constructs are supported. Only single-rank function declarations are currently allowed, and some advanced datatypes and theories may require extending Lisp-Z3. Keeping the low-level bindings aligned with different Z3 versions also requires ongoing effort, even though Grovel-file generation is partially automated (Walter et al., 25 Jul 2025).
The stated future direction is integration into ACL2s for more powerful automated support for dependent types and, in particular, more efficient generation of counterexamples to properties involving dependent types. The paper connects this to the broader idea of Enumerative Data Types Modulo Theories, in which type parameters are constrained by SMT-decidable theories: Z3 is used to solve for parameter values, and ACL2s enumerators are then used to generate concrete data consistent with those parameters (Walter et al., 25 Jul 2025).
This suggests a long-term role for Lisp-Z3 beyond simple FFI access. It functions as infrastructure for hybrid reasoning systems in which Common Lisp orchestrates interaction, ACL2s contributes definitional and proof-oriented machinery, and Z3 provides theory solving with incremental and low-latency access. Within that architecture, Lisp-Z3 is less a proof bridge than a programmable mediation layer between interactive theorem proving and SMT solving (Walter et al., 25 Jul 2025).