Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HolSmt: usability improvements, initial support for exponentials #1247

Merged
merged 3 commits into from
May 31, 2024

Conversation

someplaceguy
Copy link
Contributor

@someplaceguy someplaceguy commented May 30, 2024

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

Otherwise, they won't be able to see what the variables are being abbreviated
to. This problem would cause 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.
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.
This initial support is very limited and only consists in adding a
couple of useful theorems to the proving context.

Hopefully, in the future more extensive support is implemented by
adding support for Z3's native power operator, which should allow
HolSmt to prove a lot more goals. This implementation would need to be
similar to the implementation of integer division, since it doesn't
exactly match HOL4's exponential functions.

The latter would also have a couple of limitations, namely:

1. it would only work for Z3, since this operator is a Z3 extension
and not part of the SMT-LIB standard
2. proof reconstruction would not work
@someplaceguy
Copy link
Contributor Author

someplaceguy commented May 30, 2024

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 😅):

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
Copy link
Member

mn200 commented May 31, 2024

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
Copy link
Member

mn200 commented May 31, 2024

Thanks for all the cool work!

@mn200 mn200 merged commit 495e1a7 into HOL-Theorem-Prover:develop May 31, 2024
4 checks passed
@someplaceguy
Copy link
Contributor Author

someplaceguy commented May 31, 2024

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
Copy link
Member

binghe commented May 31, 2024

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
Copy link
Contributor Author

someplaceguy commented May 31, 2024

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
Copy link
Member

binghe commented Jun 1, 2024

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
Copy link

tjark commented Jun 3, 2024 via email

@binghe
Copy link
Member

binghe commented Jun 3, 2024

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
Copy link
Contributor Author

someplaceguy commented Jun 3, 2024

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? 🤔

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
Copy link
Contributor Author

someplaceguy commented Jun 3, 2024

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
Copy link
Member

binghe commented Jun 7, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants