3.9 KiB
Design for handling recursive functions
Main source of inspiration is [Sutter, Köksal & Kuncak 2011], as implemented in Leon, but the main differences is that we should unroll function definitions directly from the inside of Z3, in a backtracking way. Termination and fairness are ensured by iterative-deepening on the maximum number of unrollings in a given branch.
Unfolding
The idea is that every function definition f(x1…xn) := rhs[x1…xn]
is
compiled into:
- a list of cases
A_f_i[x1…xn] => f(x1…xn) = rhs_i[x1…xn]
. WhenA_f_i[t1…tn]
becomes true in the model,f(t1…tn)
is said to be unfolded and the clauseA_f_i[t1…tn] => f(t1…tn) = rhs_i[t1…tn]
is added as an auxiliary clause. - a list of constraints
Γ_f_i[x1…xn] <=> A_f_i[x1…xn]
that states whenA_f_i[x1…xn]
should be true, depending on inputsx1…xn
. For every termf(t1…tn)
present in congruence closure, we immediately add all theΓ_f_i[t1…tn] <=> A_f_i[t1…tn]
as auxiliary clauses (maybe during internalization off(t1…tn)
?).
where each A_f_i[x1…xn]
is a special new predicate representing the
given case of f
, and rhs_i
does not contain any ite
.
We assume pattern matching has been compiled to ite
beforehand.
For example, fact(n) := if n<2 then 1 else n * fact(n-1)
is compiled into:
A_fact_0[n] => fact(n) = 1
A_fact_1[n] => fact(n) = n * fact(n-1)
A_fact_0[n] <=> n < 2
A_fact_1[n] <=> ¬(n < 2)
The 2 first clauses are only added when A_fact_0[t]
is true
(respectively A_fact_1[t]
is true).
The 2 other clauses are added as soon as fact(t)
is internalized.
Termination
To ensure termination, we define variables:
unfold_depth: int
current_max_unfold_depth: int
global_max_unfold_depth: int
and a special literal [max_depth=$n]
for each n:int
.
Solving is done under the local assumption
[max_depth=$current_max_unfold_depth]
(this should be handled in some outer
loop, e.g. in a custom tactic).
Whenever A_f_i[t1…tn]
becomes true (for any f
), we increment
unfold_depth
. If unfold_depth > current_max_unfold_depth
, then
the conflict clause [max_depth=$current_max_unfold_depth] => Γ => false
where Γ
is the conjunction of all A_f_i[t1…tn]
true in the trail.
For non-recursive functions, we don't have to increment unfold_depth
. Some other functions that are known
If the solver answers "SAT", we have a model.
Otherwise, if [max_depth=$current_max_unfold_depth]
is part of the
unsat-core, then we increase current_max_unfold_depth
.
If current_max_unfold_depth == global_max_unfold_depth
then
we report "UNKNOWN" (reached global depth limit), otherwise we can
try to solve()
again with the new assumption (higher depth limit).
Tactic
there should be a parametrized tactic funrec(t, n)
where t
is the tactic
used to solve (under assumption that depth is limited to current_max_unfold_depth
)
and n
is an integer that is assigned to global_max_unfold_depth
.
This way, to try and find models for a problem with recursive functions + LIA,
one could use something like (funrec (then simplify dl smt) 100)
.
Expected benefits
This addition to Z3 would bring many benefits compared to current alternatives (Leon, quantifiers, …)
- should be very fast and lightweight (compared to Leon or quantifiers). In particular, every function call is very lightweight even compared to Leon (no need for full model building, followed by unsat core extraction)
- possibility of answering "SAT" for any
QF_*
fragment + recursive functions - makes
define-funs-rec
a first-class citizen of the language, usable to model user-defined theories or to analyze functional programs directly
Optimizations
- maybe
C_f_i
literals should never be decided on (they can always be propagated). Even stronger: they should not be part of conflicts? (i.e. tune conflict resolution to always resolve these literals away, disregarding their level)