HOL-Theorem-Prover / HOL

Canonical sources for HOL4 theorem-proving system. Branch develop is where “mainline development” occurs; when develop passes our regression tests, master is merged forward to catch up.
https://hol-theorem-prover.org
Other
607 stars 132 forks source link

HolSmt: usability improvements, initial support for exponentials #1247

Closed someplaceguy closed 1 month ago

someplaceguy commented 1 month ago

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:

  1. 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 the Abbrev symbol to the list of theorems that are added to the assumptions prior to calling the SMT solver.

  2. Add z3_tac and z3o_tac tactics

    These accept a list of theorems, like metis_tac. The free vars in the theorems are automatically quantified with Drule.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 that z3o_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 and cheat. 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 to rw[recursive_function_def] without the Once 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.

  3. Initial support for the num, int and real exponential functions

    Unfortunately, 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 and real theorems can be solved, but other simple ones can't, and almost no int 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 more num theorems than int 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):

    • HOL4's arithmeticTheory: val ** : num -> num -> num
    • HOL4's integerTheory: val ** : int -> num -> int
    • HOL4's realaxTheory: val pow : real -> num -> real
    • Z3 int: val ^ : int -> int -> real
    • Z3 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:

    • Defining Z3's integer power (^) operator as a function in HOL4 (e.g. in HolSmtTheory)
      • e.g. something like:
      • [in terms of int's **]: val 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))))` -- this is not correct because in Z3, 0^0 is undefined
      • [in terms of real's pow]: val 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)))` -- again, this is not correct because in Z3, 0^0 is undefined
    • Proving theorems that allow converting between the Z3 integer power operator and HOL4's existing exponential functions
    • Adding the above definition and theorems to the list of theorems passed to the SMT solvers, so they can reason about them
    • Add grammar support for translating and parsing Z3's power operator (and possibly for type conversion operators such as real_of_int, if it's not implemented yet)
    • Proof reconstruction would not work because (similarly to the DIV and MOD operators) Z3 would solve these proof steps as an ordinary arithmetic proof step, relying on internal knowledge of its power operator. HolSmt wouldn't be able to reconstruct this because HOL4's arithmetic decision procedures don't seem to support exponential functions and none of the Z3 proof rules are related to exponential functions in particular.
      • Ideally, solving proof reconstruction for this, similarly for the DIV and MOD operators, would likely require Z3 to provide more information in its proof log, which would likely require new proof inference rules to be created, which would likely require significant development effort in HolSmt and especially in Z3.

    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 or Z3_ORACLE_{TAC,PROVE} and not with the other tactics.

Note: no code outside HolSmt was changed in this PR.

cc @tjark

someplaceguy commented 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:

  1. This is not really representative of HolSmt's performance, it's just the most extreme example I ran into so far (excluding the HolSmt self-tests related to words, which are probably even more extreme).
  2. Again, this is using Z3 as an oracle, thus not using the HOL4 kernel to verify the proof is correct. However, so far I have never detected any soundness issues in Z3 or HolSmt; and although I'm sure Z3 still has such issues in more obscure functionality, the core functionality seems pretty solid (as far as I can tell).
  3. I expected Z3 to be able to solve at least the first inductive subgoal (which was solved by 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:

  1. I'm not even using Z3's native power operator yet!
  2. This theorem is just about nums, which are more difficult for SMT solvers to handle since they don't support them natively
  3. It also requires reasoning about division (num division, even), which SMT solvers don't support directly because in their theory, division has different behavior
mn200 commented 1 month ago

Note 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.

mn200 commented 1 month ago

Thanks for all the cool work!

someplaceguy commented 1 month ago

Note 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.

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).

binghe commented 1 month ago

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 ris 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.

someplaceguy commented 1 month ago

Yes, x powr r is only defined for non-negative x:

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...

binghe commented 1 month ago

Yes, x powr r is only defined for non-negative x:

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.

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.

tjark commented 4 weeks ago

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

binghe commented 3 weeks ago

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.

someplaceguy commented 3 weeks ago

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 number -- actually, 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...

someplaceguy commented 3 weeks ago

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.

binghe commented 3 weeks ago

See #1252 for a proposal on extending the application domain of rpow (powr), which now guarantees !x. x powr 1 = x (transcTheory.RPOW_1).