Picture this: you've built a verification tool for a control system, and you want to prove it handles edge cases correctly. You'd reach for an SMT solver like Z3—except the math you're verifying involves calculus, and Z3 wasn't designed for that. That's the problem one developer explores, attempting to teach Z3 how to handle differentiation.
It's a quirky challenge, and the solution isn't production-ready. But the work reveals clever techniques that any developer working with mathematical software should understand.
At a glance
- Z3 is an SMT solver used for program verification, but it lacks built-in support for differentiation
- The approach involves representing functions as arrays with real-to-real sorting
- Combinators can replace explicit variables, avoiding common pitfalls in first-order logic
- Transcendental functions like sin and cos require "loose" constraints rather than full definitions
- The project is labeled "Thoughts" because it doesn't deliver a complete solution
The Core Problem
Z3 excels at checking whether logical formulas are satisfiable. It handles booleans, integers, real numbers, and arrays. What it doesn't handle is calculus. When you try to express something like "the derivative of sin(x) is cos(x)," you're outside Z3's comfort zone.
Physics depends heavily on differentiation and integrals, but mainstream computer science and mathematical logic treat these operations as afterthoughts. The goal here is to see if Z3 could validate hybrid control systems, perform numerical calculations with various number formats, or verify results from symbolic math tools.
The catch? Differentiation doesn't play nicely with first-order logic, which is Z3's underlying framework.
Why Standard Syntax Breaks Down
A naive approach might look like diff(sin(cos(x)), x)—treating differentiation as a function that takes an expression and a variable. This syntax is deceptively dangerous. When you throw purely syntactic structures into Z3, you're asking for trouble unless you understand what each part semantically means.
Consider the variable x. Is it a concrete real number like 7? If so, how do you differentiate with respect to 7? That doesn't even make sense. And what is cos(x)? A real number? Not really—cos represents a function from real numbers to real numbers, while cos(x) represents that function applied to a specific argument.
The same ambiguity haunts the diff operator itself. Does diff(sin(cos(x)), x) return a function or a concrete value? The syntax can't answer that question cleanly.
A Combinator-Based Approach
The solution sidesteps these problems by removing explicit variables from the syntax. Instead of writing diff(sin(cos(x)), x), you write diff(comp(sin, cos))—referencing the composition of functions without mentioning any variable.
Helper functions make this more readable:
“`
Comp = Function("comp", FUN, FUN, FUN)
Id_ = Const("id", FUN)
“`
This lets Z3 work with function compositions while maintaining the appearance of intuitive point notation. The approach is related to the Hughes/Kylie/Poland technique for handling partial application of associative binary operators.
Representing Functions as Arrays
Z3's Array type can represent functions when given appropriate sorting. Rather than using integers or bit vectors as the domain, you can define:
“`
FUN = Array(RealSort(), RealSort())
“`
This creates a function from real numbers to real numbers. Constants like sin and cos become Consts("cos sin") within this framework. The array theory handles function application naturally through the Select operation, which acts like function application.
Handling Transcendental Functions
Functions like exp, sin, and cos have no complete definition within Z3's native capabilities. The workaround uses loose constraints—partial specifications that capture essential properties without fully defining the functions.
Axioms include:
- Cos[0] == 1, sin[0] == 0, sin[pi] == 0
- ForAll([x], exp[x + y] == exp[x] * exp[y])
- ForAll([x], sin[x] * sin[x] + cos[x] * cos[x] == 1)
These constraints let Z3 reason about transcendental functions without requiring complete definitions. The approach sacrifices precision for flexibility.
Why It Matters
This work highlights a fundamental tension in formal verification: expressive power versus decidability. Full differentiation would require Z3 to handle real analysis, which is computationally intractable. By working with partial specifications and clever syntax tricks, you can get most of the benefits without crossing into undecidable territory.
The combinator approach is particularly elegant. It mirrors how functional programmers avoid explicit loops—instead of manipulating values directly, you compose functions. This keeps the logic at a higher level where Z3 can reason about it.
The real value isn't the specific techniques. It's the demonstration that SMT solvers can be bent beyond their original design through careful encoding. For developers working with formal verification or symbolic computation, understanding these patterns opens up new possibilities.
Who Should Care
- Formal methods engineers**: Exploring hybrid verification combining discrete logic with continuous mathematics
- Control systems developers**: Validating properties of systems with both digital and analog components
- Mathematical software authors**: Verifying that symbolic tools produce correct results
- Language designers**: Understanding how to encode calculus-like reasoning in formal systems
What to Watch Next
This post is explicitly a starting point, not an endpoint. The "Thoughts" label acknowledges that real differentiation support in Z3 would require deeper integration with the solver's core. If you're working on verification tools that involve continuous mathematics, keep an eye on how the formal methods community addresses these gaps.
For now, if you need to verify properties involving derivatives, consider combining Z3 with external symbolic tools, using Z3 only for the discrete parts of your system.
Key takeaways
- Z3 can represent functions using Array(RealSort(), RealSort()), enabling function-level reasoning
- Explicit variables cause semantic problems in first-order logic differentiation; combinators provide a cleaner alternative
- Transcendental functions require loose specifications rather than complete definitions
- The techniques here are experimental but demonstrate how to extend SMT solving beyond its native capabilities
- For practical verification involving calculus, consider hybrid approaches combining Z3 with symbolic math tools
Frequently asked questions
Can I use Z3 to calculate derivatives for my app?
Not directly. Z3 isn't designed for computation—it's designed for verification. You'd use tools like SymPy for actual derivative calculations and potentially Z3 to verify properties about those calculations.
What is an SMT solver used for?
SMT solvers check whether logical formulas are satisfiable. They're commonly used in program verification, automated testing, and formal methods to prove that code meets specifications.
Why can't Z3 natively handle differentiation?
Differentiation involves real analysis, which leads to undecidable problems in the general case. Z3 prioritizes decidability and efficiency for first-order theories over mathematical expressiveness.
What are combinators in this context?
Combinators are higher-order functions that combine simpler functions. Instead of diff(sin(x), x), you write diff(comp(sin, cos)), letting the solver work with function-level representations without explicit variables.
Is this approach production-ready?
No. The blog post is explicitly labeled "Thoughts" because it doesn't provide a complete or polished solution. It's an exploration of what might be possible with creative encoding.