forked from openkylin/z3
94 lines
3.9 KiB
Markdown
94 lines
3.9 KiB
Markdown
|
# 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]`.
|
||
|
When `A_f_i[t1…tn]` becomes true in the model, `f(t1…tn)` is said to be
|
||
|
*unfolded* and the clause `A_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 when `A_f_i[x1…xn]` should be true, depending on inputs `x1…xn`.
|
||
|
For every term `f(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 of `f(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)
|