Closed someplaceguy closed 1 month ago
FYI, as a curiosity, this week I was playing around with proving some theorems and I came across an interesting example where HolSmt was able to prove a goal much more easily than what I could do manually with metis_tac
, ARITH_TAC
, rw
and friends.
Namely, I had defined this recursive function:
Definition n_trits_def:
n_trits n = if n < 3 then 1n else 1 + n_trits (n DIV 3)
End
And I wanted to prove this theorem: (n <> 0 /\ n_trits n = k) <=> (3 ** (k - 1) <= n /\ n < 3 ** k)
.
At first, I proved the theorem without HolSmt, which came out like this (please don't judge :sweat_smile:):
Theorem n_trits_bounds:
(n <> 0 /\ n_trits n = k) <=> (3 ** (k - 1) <= n /\ n < 3 ** k)
Proof
qid_spec_tac `n`
>> Induct_on `k`
>> rw[Once n_trits_def]
>- (
EQ_TAC
>> rw[]
>- (
`1 <= 3 ** k` by rw[]
>> rw[]
)
>> `3 ** k < 3 ** 1` by metis_tac[LESS_EQ_LESS_TRANS, EXP_1]
>> fs[]
)
>> last_x_assum $ qspec_then `n DIV 3` assume_tac
>> EQ_TAC
>- (
strip_tac
>> `n_trits (n DIV 3) = k` by rw[]
>> `n DIV 3 <> 0` by rw[DIV_EQ_0]
>> conj_tac
>- (
`(3 ** SUC (k - 1)) <= (n DIV 3) * 3` by rw[EXP]
>> Cases_on `k = 0`
>- rw[]
>> metis_tac[num_CASES, SUC_SUB1, LESS_EQ_TRANS, DIV_MULT_LE, DECIDE ``0 < 3``]
)
>> metis_tac[EXP, DIV_LT_X, DECIDE ``0 < 3``, MULT_COMM]
)
>> rw[]
>> `n_trits (n DIV 3) = k` suffices_by rw[]
>> Cases_on `k = 0`
>- fs[]
>> `?p. k = SUC p` by metis_tac[num_CASES]
>> `k - 1 = p` by rw[]
>> fs[]
>> `(3 ** SUC p) DIV 3 <= n DIV 3` by ARITH_TAC
>> metis_tac[EXP, MULT_COMM, MULT_DIV, DIV_LT_X, DECIDE ``0 < 3``]
QED
I'm sure this could be greatly improved and simplified (perhaps with recInduct
?), especially for someone with more expertise, but to be honest I didn't want to spend more effort on this.
When I attempted to prove this theorem with HolSmt I only needed the following:
Theorem n_trits_bounds:
(n <> 0 /\ n_trits n = k) <=> (3 ** (k - 1) <= n /\ n < 3 ** k)
Proof
qid_spec_tac `n`
>> Induct_on `k`
>> rw[Once n_trits_def]
>> z3o_tac[]
QED
Note:
rw[Once n_trits_def]
) if I provided n_trits_def
to z3o_tac[]
, but surprisingly it wasn't able to do so -- probably because of the recursive function definition issue that I mentioned before.Still, I found it very surprising that its performance was so good in this instance, especially considering that:
num
s, which are more difficult for SMT solvers to handle since they don't support them nativelynum
division, even), which SMT solvers don't support directly because in their theory, division has different behaviorNote that there is a real -> real -> real
in HOL4 with name powr
(also overloaded to rpow
) defined in transcTheory
. Most theorems about this have names with prefix RPOW
.
Thanks for all the cool work!
Note that there is a
real -> real -> real
in HOL4 with namepowr
(also overloaded torpow
) defined intranscTheory
. Most theorems about this have names with prefixRPOW
.
That's good to know, thanks!
Unfortunately, even though they have the same type, the definition of powr
doesn't seem to match Z3's real power operator, as far as I can tell.
Believe it or not, I cannot find a definition for Z3's real power operator anywhere, but from the source code I can tell that Z3 simplifies x ^ 1r
to x
, which means that !x. x ^ 1r = x
is a theorem in Z3 (I have checked that Z3 verifies this).
In HOL4, however, I have proven a theorem that states the opposite when using powr
:
Theorem RPOW_1_NOT:
~(!x. x powr 1 = x)
Proof
rw[]
>> qexists_tac `-1`
>> rw[rpow_def]
>> qspec_then `ln (-1)` assume_tac EXP_POS_LE
>> pop_assum mp_tac
>> RealField.REAL_ARITH_TAC
QED
This is because the theorem I can prove in Z3 is not true in HOL4 for negative x
...
That said, if I can somehow infer exactly what definition Z3 is using for its real power operator, I could probably make HolSmt handle powr
as well (for some goals, at least).
Yes, x powr r
is only defined for non-negative x
:
[rpow_def] Definition
⊢ ∀a b. a powr b = exp (b * ln a)
This is reasonable, because x powr r
is not defined if x
is negative and r
is not integer. For x ^ 1r
, if (somehow) you can detect 1r
is an integer, then x ^ 1r = x ** 1
where 1
is either :num
or :int
, then x ** 1 = 1
would be provable.
Yes,
x powr r
is only defined for non-negativex
:
Is that true, though? As far as I can tell, it's not just that (-3r) powr 1
is undefined in HOL4 (i.e. equivalent to ARB
). In HOL4, I can actually prove that (-3r) powr 1 = -3r
is false.
In contrast, in Z3 I can prove that (-3r) ^ 1 = -3r
is true. So for example, Mathematica seems to agree with Z3 but not with HOL4 (although I'm not sure what Mathematica considers a real or an int, so I'm not sure what to make of this).
As a disclaimer, I am not a mathematician, or logician, or computer scientist... but as far as I can see, I think Z3's definition makes more sense to me, because like you say, when you can prove that y
is an integer, then x ^ y
in Z3 is defined for all x
and is equal to HOL4's x pow y
(that is, assuming Z3 is doing what I think it is doing, but I'm not sure because I can't find Z3's definition of ^
).
However, in HOL4, even when you prove that y
is an integer and (say) equal to real_of_num 1
, then you can still prove ?x. x powr y <> x
. So for example, in HOL4 ?x. x powr 1 <> x pow 1
is provable, which is not what I would expect...
Yes,
x powr r
is only defined for non-negativex
:Is that true, though? As far as I can tell, it's not just that
(-3r) powr 1
is undefined in HOL4 (i.e. equivalent toARB
). In HOL4, I can actually prove that(-3r) powr 1 = -3r
is false.
This is because, by HOL4's definition of powr
, x powr 1
is "unspecified" when x <= 0
, and the currently definition easily lead to 0 <= powr x r
for any x and r. Therefore not only you can "prove" (-3r) powr 1
is not -3r
but also you can prove it's not any negative real numbers. However, the present definition of powr
is not "wrong" in the sense that, if you use a powr b
with its correct domain (a > 0), it matches the textbook definition.
I think extending the domain of the existing powr
(to match Z3's behavior) is possible: the extended definition should include the case "the power is the real value of integers", so that for any integer n, x powr &n
is defined for whatever x:real
. But this change may introduce minor incompatibilities. I can have a try.
On Fri, 2024-05-31 at 21:48 -0700, Chun Tian wrote:
I think extending the domain of the existing powr (to match Z3's behavior) is possible: the extended definition should include the case "the power is the real value of integers", so that for any integer n,x powr &n is defined for whatever x:real. But this change may introduce minor incompatibilities. I can have a try.
Given its type (real -> real -> real), it might make sense to extend powr to arbitrary real exponents?
Best, Tjark
När du har kontakt med oss på Uppsala universitet med e-post så innebär det att vi behandlar dina personuppgifter. För att läsa mer om hur vi gör det kan du läsa här: http://www.uu.se/om-uu/dataskydd-personuppgifter/
E-mailing Uppsala University means that we will process your personal data. For more information on how this is performed, please read here: http://www.uu.se/en/about-uu/data-protection-policy
On Fri, 2024-05-31 at 21:48 -0700, Chun Tian wrote: I think extending the domain of the existing powr (to match Z3's behavior) is possible: the extended definition should include the case "the power is the real value of integers", so that for any integer n,x powr &n is defined for whatever x:real. But this change may introduce minor incompatibilities. I can have a try. Given its type (real -> real -> real), it might make sense to extend powr to arbitrary real exponents?
I meant that, when the base is negative, the exponent must be integer (of type :real), e.g. (-2) powr 3 = -8
, (-2) powr (-3) = -1/8
, because otherwise the value (mostly, the sign) is not defined: what's the sign and value of, say, (-1) powr (2/3)
?
P. S. Those undefined values are actually complex numbers with non-zero imaginary parts.
P. S. Those undefined values are actually complex numbers with non-zero imaginary parts.
Interestingly, Z3 doesn't support complex numbers (although you can encode them as a pair of reals), but even so, Z3 does rewrite -- (-3r) ^ (-1/2r)
into (-1/3r) ^ (1/2r)
, for instance, even though that's a complex numberactually, this is not a valid rewrite for fractional exponents, I think -- so maybe I'm reading the code wrong -- actually, I just tested it and verified that Z3 does indeed consider (-3r) ^ (-1/2r)
to be equal to (-1/3r) ^ (1/2r)
, so I'm not sure if this is a bug... or is it assuming that since the real part of both expressions match, then it's as if they were equivalent? :thinking:
It also does something special with irrational algebraic numbers when processing the power operator, but it was a bit too dense for me to follow when I last looked into it...
Currently, and in part based on what I wrote on my previous comment, I am heavily leaning into not adding specialized support for powr
in HolSmt. For me to do so would require me to faithfully define the real-valued version of Z3's power operator in HOL4 and I am not confident that I would be able to do so correctly. This is a major problem because any mismatch introduces the possibility of the external oracle tactics in HolSmt to become unsound, which I'd like to avoid at all costs.
For example, that Z3 considers two power expressions, each of them a complex number, to be equal to each other even when their imaginary parts are different is not something that I was expecting. Furthermore, their processing of irrational numbers is also a bit hard for me to understand.
When both operands are integers, then defining the power operator is more feasible, but even then there are some differences compared to HOL4. For example, I discovered that Z3 considers 0^0
to be undefined. I also discovered that 0^x = 0
when x
is a negative integer. The first one is understandable since it matches the usual mathematical definition, but the second one was unexpected (and I filed a bug asking for clarification, just in case).
Even taking into account the above, it does seem straightforward to define the integer version of the power operator, but even so, I'd still like to be 100% sure that there are no more surprises and that what Z3 is doing is sound -- in particular, I'm worried about the rewrites that it does when the operands are not constant values / integer literals.
Note that Z3 doesn't actually have two separate power operators (one for integers and one for reals), they are both the same operator. This means that even if both operands are integers, the function that processes the power operator still does everything that is needed to handle real values, which makes for less duplicate code, but at the same time it makes it harder to understand than would otherwise be required when only dealing with integers...
Edit: for future reference, here you can see the Z3 code I'm looking at: https://github.com/Z3Prover/z3/blob/770c51ad2b257e59710041894581ec0411000018/src/ast/rewriter/arith_rewriter.cpp#L1513-L1679
Note: I think I just figured out that in Z3, their "numeral" terminology represents a rational literal an expression that can definitely be expressed as a rational literal and therefore does not include numbers which are definitely irrational (but don't take my word for it!). If this is indeed the case, this makes the code easier to understand, I think. Even so, I think there is more code where the power operator is handled -- I think this is just a subset of the code, which handles rewrites.
See #1252 for a proposal on extending the application domain of rpow
(powr
), which now guarantees !x. x powr 1 = x
(transcTheory.RPOW_1
).
This week I've had the opportunity to use HolSmt more extensively and noticed a few shortcomings. This PR fixes a few of them.
It contains the following 3 minor improvements:
Allow SMT solvers to peek under abbreviations
Otherwise, they would only be able to see the variables that are being abbreviated to, but not the terms being abbreviated. This problem was causing some goals that can be solved with
metis_tac
to fail with HolSmt. The fix is to add the definition of theAbbrev
symbol to the list of theorems that are added to the assumptions prior to calling the SMT solver.Add
z3_tac
andz3o_tac
tacticsThese accept a list of theorems, like
metis_tac
. The free vars in the theorems are automatically quantified withDrule.GEN_ALL
and added to the list of assumptions in the goal, prior to sending the goal to the SMT solver.This is needed to avoid any passed-in theorems to be specialized to free vars in the goal that happen to have the same name, which is almost always not what the user wants (and if the user really wants it, they can always assume such theorems manually if needed). This matches the behavior of
metis_tac
, thus avoiding unwanted surprises.Note: in particular, when using these tactics and HolSmt is configured to call Z3 with a timeout,
z3o_tac[]
basically works as a (usually) more powerful "metis_tac[]
+DECIDE_TAC/intLib.ARITH_TAC
+RealField.REAL_ARITH_TAC
+wordsLib.WORD_DECIDE
+blastLib.BBLAST_TAC
+HolSatLib.SAT_ORACLE
+ nonlinear int and real arithmetic DP", all at once (and usually faster as well) -- although keep in mind thatz3o_tac
works as an external oracle, which is not appropriate in some circumstances. However, even in these cases it's probably still great for prototyping, i.e. as something in-between a full proof andcheat
. The main limitation I've found in arithmetic-related goals is that Z3 often seems to get stuck finding proofs if the user provides definitions of recursive functions (similar torw[recursive_function_def]
without theOnce
modifier). This could be solved in the future using the same technique that F* uses to encode recursive function definitions: by adding a (configurable) fuel parameter that limits how many times a function definition can be unfolded during the proof search.Initial support for the
num
,int
andreal
exponential functionsUnfortunately, this feature is more complicated than it seems. For now, all I did was add a couple of useful theorems to the proving context, so that the SMT solvers can "understand" what these exponential functions are supposed to do, but this only allows them to solve some simpler goals. As you can see from the self-tests, the coverage is extremely spotty: some
num
andreal
theorems can be solved, but other simple ones can't, and almost noint
theorems can be solved. This is in part due to some theorems causing SMT solvers to become stuck finding a proof and also in part due to some theorems that are missing in HOL4. However, even if such missing theorems would be added, I wouldn't expect them to go very far in terms of solving goals efficiently. That said, it's still surprising to me that Z3 can solve a lot morenum
theorems thanint
ones, given that Z3 doesn't even support natural numbers natively.Since I'm currently working with goals with many exponentials, I am planning to improve this feature in an upcoming PR. Namely, it turns out that Z3 also has native support for an exponential/power operator. If other arithmetic operators are any indication, I would expect Z3's native power operator to solve a lot more goals than relying on adding theorems to the proving context. However, these operators in the Z3 and HOL4 theories all have different domains and codomains. Namely (written below in SML signature syntax):
val ** : num -> num -> num
val ** : int -> num -> int
val pow : real -> num -> real
val ^ : int -> int -> real
val ^ : real -> real -> real
As you can see, Z3's native power operators do not exactly match any of HOL4's operators, so implementing support for them would be similar to the previous implementation of integer division (which also didn't match any of HOL4's division functions). Namely, this would require:
**
]:-- this is not correct because in Z3, 0^0 is undefinedval z3ipow_def = bossLib.Define `z3ipow x y = if 0 <= y then real_of_int (x ** (Num y)) else 1 / (real_of_int (x ** (Num (-y))))`
pow
]:-- again, this is not correct because in Z3, 0^0 is undefinedval z3ipow_def = bossLib.Define `z3ipow x y = if 0 <= y then (real_of_int x) pow (Num y) else 1 / ((real_of_int x) pow (Num (-y)))`
real_of_int
, if it's not implemented yet)Note, however, that Z3's native power operators are a Z3 extension and not part of the SMT-LIB standard. Therefore, since it would only work for Z3 and proof reconstruction would not work, HolSmt would only use it with
z3o_tac
orZ3_ORACLE_{TAC,PROVE}
and not with the other tactics.Note: no code outside HolSmt was changed in this PR.
cc @tjark