z3/doc/design_recfuns.md

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)