Skip to content

Add new tactic "ensatz" for proving polynomial equalities with existential quantifier #160

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

lyonel2017
Copy link

@lyonel2017 lyonel2017 commented Jun 2, 2025

This PR introduce the ensatz tactic that can prove automatically goals of the form:

$$\begin{array}{l} \forall X_1, \ldots, X_n \in A, \\\ P_1(X_1, \ldots, X_n) = Q_1(X_1, \ldots, X_n), \ldots, P_s(X_1, \ldots, X_n) = Q_s(X_1, \ldots, X_n) \\\ \vdash \exists Y_1, \ldots, Y_m \in A, P(X_1, \ldots, X_n) = Y_1 * Q'_1(X_1, \ldots, X_n) + \dots + Y_m * Q'_m(X_1, \ldots, X_n) \end{array}$$

Tests have be added to the test-suite in file test-suite/success/ENsatz.v.

  • Added / updated documentation:

An entry to the sphinx documentation of Rocq is available here and ready for a PR.

  • Added changelog.

@andres-erbsen
Copy link
Collaborator

This is an exciting feature. Would it be feasible to add this without another copy of the nsatz Ltac and especially the reification procedure? (Is this tactic intended to also prove all goals the previous one proves?)

@andres-erbsen
Copy link
Collaborator

Reading https://www.cl.cam.ac.uk/~jrh13/papers/divisibility.pdf theorem 5 i understand how to solve goals where Y_1..Y_m appear as linear-combination coefficients. Is the algorithm in this PR more general, or should Q(...) above be restricted more?

@lyonel2017
Copy link
Author

Would it be feasible to add this without another copy of the nsatz Ltac and especially the reification procedure? (Is this tactic intended to also prove all goals the previous one proves?)

The ensatz tactic is not intended to replace the nsatz tactic. The ensatz tactic cannot solve goals without existential quantifier. Maybe there's a way to unify the two, but I can't see it at the moment. Especially, the part concerning the reifiction are quite different. But maybe I missed something.

@lyonel2017
Copy link
Author

Reading https://www.cl.cam.ac.uk/~jrh13/papers/divisibility.pdf theorem 5 i understand how to solve goals where Y_1..Y_m appear as linear-combination coefficients. Is the algorithm in this PR more general, or should Q(...) above be restricted more?

You are right, Q(...) is restricted to the cases where Y_1 ... Y_m appear as linear-combination coefficients.


Examples: see test-suite/success/ENsatz.v

Reification is done using type classes, defined in Ncring_tac.v
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the entry point for reification is a typeclass Hint Extern, but the reification is ltac not typeclass based AFAIK

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the line which was a left over from copying/pasting the NsatzTactic.v file.

@andres-erbsen
Copy link
Collaborator

I think there's a good chance a bunch of code could be shared. Whether it's worth it or not is for us to determine. But for example the hol-light implementation does seem to share code (examples).

@lyonel2017 lyonel2017 changed the title Add support for existentiel quantifier in nsatz Add support for existential quantifier in nsatz Jun 3, 2025
@lyonel2017 lyonel2017 force-pushed the feature-ensatz branch 2 times, most recently from 9474023 to f48b1bc Compare June 4, 2025 15:19
@lyonel2017 lyonel2017 marked this pull request as ready for review June 4, 2025 20:10
Copy link
Collaborator

@andres-erbsen andres-erbsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be good having a tactic like this in stdlib. And I'd like to use it myself, both in stdlib and otherwise. At the same time, the proposed implementation provoked a number of initial responses and questions from me. This initial review is not complete and not everything here is requirement for merging, but I would like to hear your thoughts on these points and overall plan/goals.

Additionally, are you looking to keep using this tactic yourself, and where?

Edit: an important part of my context here is that I am a heavy user of nsatz and have spent a substantial amount of time fighting its implementation issues, including maintaining a part-reimplementation (in fiat-crypto). From this, my overall impression is that nsatz .v-file code is not something to be imitated, and every bit of this kind of in the repo is a liability (it is obviously also an asset). And not-thought-through implementation behaviors do tend to acquire rigid users, making them much harder to change later. rocq-prover/rocq#18325 is an example, #155 (comment) (WIP) is another.

+ Add support for existential quantifier in nsatz
(`#160 <https://github.com/coq/stdlib/pull/160>`_,
by Lionel Blatter).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check add the documentation and link to it here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the remark. Is the changelog entry incomplete in terms of references to the new tactic's documentation? If so, I don't know how to proceed, since the documentation page for nsatz is in the Rocq project and not Stdlib. I wrote documentation for ensatz in my fork of Rocq.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My apologies for the broken sentence. Yes, I meant that it would be nice to have a documentation link in the changelog (and have it committed, somewhere). Example.

Where to actually put the documentation is indeed an interesting question. There are actually three plausible places: rocq refman, stdlib refman, and stdlib html+coqdoc documentation. And I'm not sure what the high-level plan is for documentation or ml-supported tactics in the rocq-stdlib split. But documenting next to nsatz (and making a linked PR) seems fine.

unfold divides, modulo, coprime, ideal, gcd.
preprocess.
Time ensatz.
Qed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More tests would significantly increase my expectations of success for trying to use this tactic. E.g. the hol-light examples that are proven by INTEGER_TAC or INTEGER_RULE should be appropriate. If some of them don't work for a well-understood reason, documenting that is especially important.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a first batch of additional examples. Maybe I'll add a few more.

Definition merge {A R:Type} (p1 p2: A -> @MPoly R) :=
fun x => add_mpoly (p1 x) (opp_mpoly (p2 x)).

Ltac reify_goal :=
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This procedure interleaves generalization of the goal over effective variables (from the perspective of ensatz) and the reification of it. For troubleshooting failing invocations, it is useful to be able to run the generalization phase without reification. (This also enables faster and more predictable reification methods.) Existing stdlib tactics do do something similar to the code here, and I am not going to hold this contribution to a hypocritically high standard, but I do genuinely believe it would be more useful if these steps were factored out.

let lv := fresh "lv" in
set (p21:=p2); set (lp21:=lp2); set (n21:= n); set(lv := fv);
(*We want r to be 1, so we set radicalmax to 1*)
NsatzTactic.nsatz_call 1%N info 0%Z p2 lp2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have two calls to this function, it would be really nice to have at least a basic comment describing what it should and should not do.

let p211 := constr: (PEmul c p21) in
(*If c is not 1/-1, this assert should fail*)
assert (Hg:check lp21 p211 (lci,lq) = true);
[vm_compute; reflexivity |];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this vm_compute always succeed if there are no bugs in this tactic? If yes, consider vm_cast_no_check (eq_refl true)

ltac:(fun c r lq lci =>
let Hg := fresh "Hg" in
let c21 := fresh "c" in
set (c21 := c);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intent to perform unification of all subterms of the entire goal with c here? If not, pose and change.

Copy link
Author

@lyonel2017 lyonel2017 Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is intentional.

@@ -0,0 +1,6 @@
- `ENsatzTactic.v` and `ENsatz.v`

+ Add support for existential quantifier in nsatz
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the plan? Currently, the PR adds new tactic ensatz, without adding support for existential quantifiers to nsatz.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I change the entry message.

@lyonel2017
Copy link
Author

I think it will be good having a tactic like this in stdlib. And I'd like to use it myself, both in stdlib and otherwise. At the same time, the proposed implementation provoked a number of initial responses and questions from me. This initial review is not complete and not everything here is requirement for merging, but I would like to hear your thoughts on these points and overall plan/goals.

Additionally, are you looking to keep using this tactic yourself, and where?

Edit: an important part of my context here is that I am a heavy user of nsatz and have spent a substantial amount of time fighting its implementation issues, including maintaining a part-reimplementation (in fiat-crypto). From this, my overall impression is that nsatz .v-file code is not something to be imitated, and every bit of this kind of in the repo is a liability (it is obviously also an asset). And not-thought-through implementation behaviors do tend to acquire rigid users, making them much harder to change later. rocq-prover/rocq#18325 is an example, #155 (comment) (WIP) is another.

My original plan was to use this tactic in Easycrypt. It is not certain that this will happen in the end. I will apply patches to answer the question as best I can. The ongoing re-implementation of nsatz is in conflict with ensatz and will require more time to patch.

@andres-erbsen
Copy link
Collaborator

The ongoing re-implementation of nsatz is in conflict with ensatz and will require more time to patch.

Just to be clear, I am not asking you to step in to resolve the situation with nsatz being duplicated in stdlib and fiat-crypto. And my plan there is to backport fixes from fiat-crypto in small minimally invasive changes, hopefully reaching a state where stdlib nsatz fits both use cases. The reason I am pointing out this duplication is to advocate for avoiding further duplication within stdlib: code shared between ensatz and nsatz will benefit from these backports, whereas dedicated ensatz code will not.

@lyonel2017 lyonel2017 changed the title Add support for existential quantifier in nsatz Add new tactic "ensatz" for proving polynomial equalities with existential quantifier Jun 13, 2025
@lyonel2017 lyonel2017 force-pushed the feature-ensatz branch 4 times, most recently from e1566d5 to 5b33f05 Compare June 13, 2025 22:33
@lyonel2017
Copy link
Author

I created a file ZENsatz.v to have ensatz for ZArith. The file is almost identical to ZNsatz.v. I have the impression that merging both files would be better.

@lyonel2017 lyonel2017 force-pushed the feature-ensatz branch 2 times, most recently from e68cec1 to 49de007 Compare June 14, 2025 16:57
@andres-erbsen
Copy link
Collaborator

Indeed, merging seems preferable to duplicating opaque-yet-relevant instances. Though, maybe ensatz just works (for Z) when ZNsatz is imported.

@andres-erbsen
Copy link
Collaborator

andres-erbsen commented Jun 14, 2025

After reading the documentation, there's one more thing on my mind: the e prefix is generally used for tactics that instantiate create existential variables. I understand the code currently in this PR doesn't do that, but could it? E.g. Goal forall x : Z, { y : Z | y = x}. esplit. abstract ensatz. Defined.? IIUC the underlying algorithm does compute a witness, and the limitation is in the reification mechanism. I think the name is neat, and yet it would be inconsistent with current usage, unless we implemented this feature. exists_nsatz sounds much less fun.

@SkySkimmer
Copy link
Contributor

there's one more thing on my mind: the e prefix is generally used for tactics that instantiate existential variables.

I wouldn't say so (other than refining the goal evar as all tactics do). For instance what evars does esplit in your example instantiate?
Rather the e prefix is used when the tactic creates new shelved evars. For instance apply -> new evars are all subgoals, eapply -> some of the new evars may be shelved.

@lyonel2017 lyonel2017 force-pushed the feature-ensatz branch 2 times, most recently from 4f090f3 to 314d075 Compare June 16, 2025 21:57
@lyonel2017
Copy link
Author

I have the impression that even if the ensatz tactic would supports examples such as Goal foall x:Z,{y:Z|y=x}, the name ensatz remains inconsistent with the current naming convention. One solution would be to merge nsatz and ensatz, i.e. add the capabilities of ensatz to nsatz, as the original name of this PR suggested.

@andres-erbsen
Copy link
Collaborator

Agreed. Overall, my intuition is that merging ensatz and nsatz would be great, both for user experience and (I hope) maintenance. Do you think it would be practical?

As for support for sig, I feel like building that specifically probably isn't worth it. I would welcome support for instantiating existing existential variables, primarily so I can call nstaz from/after scripts that leave behind evars without rewriting these scripts to work under existential quantifiers using setoid_rewrite.

@andres-erbsen
Copy link
Collaborator

andres-erbsen commented Jun 18, 2025

On a third thought, isn't the difference between ensatz (with the proposed change to operate on evars) and nsatz pretty much the same as between eassumption and assumption? It' s possible that eassumption is an outlier though.

@lyonel2017
Copy link
Author

Apparently, the auto and eauto tactics are another exception. Perhaps it's acceptable to have nsatz and ensatz?

I will give a try to add support for instantiating existential variables.

@lyonel2017
Copy link
Author

I finally found the time to add the support for existential variables. I still have to refactor the code a bit and complete the documentation.

The source of the error in the CI is not clear to me.

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.

3 participants