Skip to content

Only accept SELF on the left for left-associative levels#21126

Merged
coqbot-app[bot] merged 8 commits into
rocq-prover:masterfrom
ia0:assoc
Nov 24, 2025
Merged

Only accept SELF on the left for left-associative levels#21126
coqbot-app[bot] merged 8 commits into
rocq-prover:masterfrom
ia0:assoc

Conversation

@ia0

@ia0 ia0 commented Sep 26, 2025

Copy link
Copy Markdown
Contributor

This PR is a follow-up of the discussion in #21072, initially started by #21029. This can also be seen as the continuation of the work started in #17876. In particular, this PR provides proper support for non-associativity and fixes leniency in right-associativity (namely that the left component recognizes the non-recursive part of the same level instead of the next level only).

Depends on: #21244 (merged)

Overlays (to be merged before the current PR)

Overlays (to be merged in sync with the current PR)

@ia0 ia0 requested a review from a team as a code owner September 26, 2025 19:14
@coqbot-app coqbot-app Bot added the needs: full CI The latest GitLab pipeline that ran was a light CI. Say "@coqbot run full ci" to get a full CI. label Sep 26, 2025
@ia0

ia0 commented Sep 26, 2025

Copy link
Copy Markdown
Contributor Author

I completely broke associativity... i.e. atom1 op12 atom2 op12 atom2 and atom1 op10 atom1 op10 atom0. I'll take a look when I get time.

@ia0 ia0 marked this pull request as draft September 26, 2025 20:01
@ia0

ia0 commented Sep 27, 2025

Copy link
Copy Markdown
Contributor Author

Closing this draft because @proux01 fixed it, see #21072 (comment).

@ia0 ia0 closed this Sep 27, 2025
@ia0

ia0 commented Sep 27, 2025

Copy link
Copy Markdown
Contributor Author

Actually I don't think this was buggy (it passes the same tests as the commit from @proux01). The output/Notations4.v test case is buggy.

Print Custom Grammar expr gives:

Entry custom:expr is
[ "2" RIGHTA
  [ SELF; "+"; NEXT ]
| "1" LEFTA
  [ SELF; NEXT ]
| "0" RIGHTA
  [ "{"; term LEVEL "200"; "}"
  | "("; custom:expr LEVEL "2"; ")"
  | NUMBER "1"
  | identref ] ]

Level 2 written like that makes no sense. It essentially means + is non-associative since SELF on the left of RIGHTA is NEXT.

@proux01

proux01 commented Sep 28, 2025

Copy link
Copy Markdown
Contributor

Indeed, this seems to work, let's reopen.
The only advantage I can see to proux01@4fdb719 is that warning seem better located (on the left of right assoc).
Yes, a few output tests will need an update.

@proux01 proux01 reopened this Sep 28, 2025
Comment thread foo.v Outdated
@ia0

ia0 commented Sep 28, 2025

Copy link
Copy Markdown
Contributor Author

The only advantage I can see to proux01@4fdb719 is that warning seem better located (on the left of right assoc).

I don't know about the warnings. I already don't find them correct on master.

Regarding the diff between both commits, in my opinion the biggest difference is the approach. In your case you reject a recovery on a wrong level after running the suffix tree, while in my case I restrict the range of levels to continue which won't even try running the suffix tree for the wrong level if not gstate.recover.

Another minor (but semantically significant in my opinion) difference is that you check lev.assoc = RightA while I believe the correct check is lev.assoc <> LeftA. This recovery mechanism is only meant for left-associativity, so it's normal to disable it for the other cases (right-associativity and non-associativity).

Yes, a few output tests will need an update.

Should I give it a go? (I'm not convinced I should produce a PR if there's too much OCaml involved.)

@proux01

proux01 commented Sep 29, 2025

Copy link
Copy Markdown
Contributor

The only advantage I can see to proux01@4fdb719 is that warning seem better located (on the left of right assoc).

I don't know about the warnings. I already don't find them correct on master.

Maybe the wording could be improved? These warnings are kinda important since they are the main tool for users to adapt to the future change.

Regarding the diff between both commits, in my opinion the biggest difference is the approach. In your case you reject a recovery on a wrong level after running the suffix tree, while in my case I restrict the range of levels to continue which won't even try running the suffix tree for the wrong level if not gstate.recover.

No opinion on which implem is better. I just think that it is important for the warning to be precisely located (in this case, indicating at which character position the user can add parentheses).

Another minor (but semantically significant in my opinion) difference is that you check lev.assoc = RightA while I believe the correct check is lev.assoc <> LeftA. This recovery mechanism is only meant for left-associativity, so it's normal to disable it for the other cases (right-associativity and non-associativity).

Indeed, you're right

Yes, a few output tests will need an update.

Should I give it a go?

Feel free to

(I'm not convinced I should produce a PR if there's too much OCaml involved.)

Well, this repo is essentially OCaml code, so PRs are essentially OCaml.

@ia0

ia0 commented Sep 29, 2025

Copy link
Copy Markdown
Contributor Author

Maybe the wording could be improved? These warnings are kinda important since they are the main tool for users to adapt to the future change.

Yes I agree they are important. The problem I'm referring to is not really the formulation, but the levels (and maybe sometime the location? I'll need to find examples). An example that I find confusing for levels is:

Check 0 > ~ 0.

It gives the following warning (on the correct location of ~ 0):

Error: In term, tolerating this expression at level 60 while it is expected to be at level 200. This tolerance
will be eventually removed. Insert parentheses or try to lower the level at which the top symbol of this
expression is parsed. [level-tolerance,deprecated-since-9.2,deprecated,parsing,default]

I would expect level 75 instead of level 200, since that's the most precise level of ~ 0. It is indeed also at level 200 but that seems imprecise and possibly confusing. Although one could argue that the implementation would indeed tolerate anything up to level 200. So yes, maybe it's just the formulation.

@proux01

proux01 commented Sep 30, 2025

Copy link
Copy Markdown
Contributor

Honestly, in my experience as a user, whereas the location is super important, the levels in the warning message aren't very useful: either you decide to add parentheses and you don't care about the levels, or you decide to change some levels which means you need to find where the notations are reserved and you'll then also get their levels. So maybe our best move would just be to get rid of the levels in the message.

@ia0

ia0 commented Sep 30, 2025

Copy link
Copy Markdown
Contributor Author

I see, makes sense.

Comment thread test-suite/output/StrictAssociativity.v Outdated
@proux01

proux01 commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

I see, makes sense.

C.f. #21166

@github-actions github-actions Bot added the needs: rebase Should be rebased on the latest master to solve conflicts or have a newer CI run. label Oct 6, 2025
@coqbot-app coqbot-app Bot removed the needs: rebase Should be rebased on the latest master to solve conflicts or have a newer CI run. label Oct 8, 2025
@proux01

proux01 commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

Rebased to fix the conflict I created with #21166

FTR, this was discussed during yesterday's call, there was a general agreeement we should do it, with the following requests:

  • try to get the warning locations right
  • which should enable quickfixes (offering to add the extra parentheses)
  • fix the issue with the tactic => intro_pattern ssreflect syntax (I'm looking at it)
  • have an extra reviewer

@SkySkimmer

Copy link
Copy Markdown
Contributor

which should enable quickfixes (offering to add the extra parentheses)

Adding parentheses is not always guaranteed to work, in particular custom entries don't have to have a rule with parentheses embedding the top level into the bottom level.

@proux01

proux01 commented Oct 8, 2025

Copy link
Copy Markdown
Contributor

Yes, it's more a best effort that will work in many cases than a 100% foolproof solution.

@ia0

ia0 commented Oct 13, 2025

Copy link
Copy Markdown
Contributor Author

I've rebased onto master and split into 2 commits: one adding the test file and one with the actual change (which I changed to use the level-tolerance warning such that the error locations are correct). In particular, the .out file in the second commit is exactly the diff of the change and enough to review to check the error locations (which I find correct). If we can have @herbelin reviewing that would be great since this touches level-tolerance.

@ia0 ia0 marked this pull request as ready for review October 13, 2025 20:26
@ia0 ia0 changed the title Experiment: stricter associativity Do not tolerate SELF on the left of right-associative levels Oct 13, 2025

@proux01 proux01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be nice to have an indication in the message on whether the issue lies on the left (all warnings prior to the current PR) or on the right (the new warnings from this PR) of the flagged location.
Otherwise LGTM, we still need before merging (potentially in separate PR) to:

  • fix the ssreflect _ => _ issue (I'm looking at it)
  • try to improve the recovery locations so that we can provide a quickfix suggesting adding parentheses (maybe only on constr were we are sure the parentheses do exist)
  • get an extra review

Comment thread test-suite/output/Notations4.v Outdated
Comment on lines +70 to +71
> Check Reject 10 [2 atom3 ].
> ^^^^^^^

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Definitely independent from the current PR but here, I'd rather expect the following location

Suggested change
> Check Reject 10 [2 atom3 ].
> ^^^^^^^
> Check Reject 10 [2 atom3 ].
> ^^^^^

not sure if this could be reasonably obtained.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I was also expecting that more narrow error location.

@proux01 proux01 self-assigned this Nov 12, 2025

@SkySkimmer SkySkimmer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This creates a warning in the ltac2/custom_entry test. Ltac2 custom entries declare levels to be RIGHTA so that self on the right and on the left always means the current level and leaves associativity to the rule writer, this PR would break that.

IDK if there should be a new associativity that doesn't warn or what but this shouldn't be merged as-is.

@SkySkimmer SkySkimmer added the needs: fixing The proposed code change is broken. label Nov 12, 2025
@ia0

ia0 commented Nov 12, 2025

Copy link
Copy Markdown
Contributor Author

Ltac2 custom entries declare levels to be RIGHTA so that self on the right and on the left always means the current level.

This is not what right-associativity means. Right-associativity means that NEXT op SELF.

IDK if there should be a new associativity that doesn't warn.

What you want would be all-associativity defined by SELF op SELF. That's indeed the only missing option of the 4 possibilities:

  • no-associativity is NEXT op NEXT
  • left-associativity is SELF op NEXT
  • right-associativity is NEXT op SELF
  • all-associativity is SELF op SELF

Note that all-associativity is ambiguous if no care is taken. The user needs to write their rules (e.g. notations) such that no ambiguity is possible, i.e. NEXT op SELF or SELF op NEXT for each production rule.

I don't think this would be difficult to implement once we have add the ALLA variant to NONEA, LEFTA, and RIGHTA.

However, I wonder how niche this is. Do we have concrete examples of users that need to control associativity for each rule rather than each level?

The examples in ltac2/custom_entry.v are rather artificial. They abuse the fact that you can do left-associativity rules in a right-associative level. Most (if not all) users (see the mathcomp overlay) consider this a hack and would prefer to get a warning to fix it.

@proux01

proux01 commented Nov 13, 2025

Copy link
Copy Markdown
Contributor

This creates a warning in the ltac2/custom_entry test. Ltac2 custom entries declare levels to be RIGHTA so that self on the right and on the left always means the current level and leaves associativity to the rule writer, this PR would break that.

My understanding of this hack is that you use RIGHTA as a way of trying to convince gramlib to "stop trying to handle associativity for me". Note that the ultimate goal of this PR is to ultimately get rid of any associativity notion in gramlib (and have higher levels be the only one to handle it, through proper use of SELF and NEXT). Unfortunately, note that this doesn't perfectly work, when you write

(* we can also do right associative notations *)
Ltac2 Notation x(self) "-" y(self) : mycustom(1) := (x, y).

what you'd really like to write is rather

(* we can also do right associative notations *)
Ltac2 Notation x(next) "-" y(self) : mycustom(1) := (x, y).

but unfortunately this doesn't quite work as the parser doesn't handle it since all levels are righta and you need to write

Ltac2 Notation x(next) "-" y(self) : mycustom(1) := (x, y).
Ltac2 Notation x(next) : mycustom(1) := x.

Not sure what's the best solution. Maybe we should just allocate "correct" associativities rather than the default righta for now (this would restrain a tiny bit the expressivity of ltac2 custom entries (at least as long as gramlib insists on managing them) but probably nothing unacceptable)

@ia0

ia0 commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

I think there's some confusion due to the meaning of SELF and NEXT. There are 3 different meanings:

  • The syntax at Rocq level (at next level, x(self), and x(next) for example).
  • The syntax at Gramlib level (Sself and Snext).
  • The semantics at Gramlib level (what grammar is actually recognized).

I've been using SELF and NEXT with this last meaning, because that's what matters (and that's what SELF means in the title of this PR).

Ideally we could make the Rocq syntax match the Gramlib semantics. But we won't be able to make the Gramlib syntax match its semantics without major changes because of the strong distinction between start and continue trees (which hardcodes the meaning of Sself on the left in the design).

Note that the ultimate goal of this PR is to ultimately get rid of any associativity notion in gramlib (and have higher levels be the only one to handle it, through proper use of SELF and NEXT).

I would say this PR does actually the complete opposite of that. Also this seems to contradict one of your previous comment:

IIRC, the outcome of the discussion was that we absolutely don't want that. Mixing different associativities at the same level would be way to confusing. Associativity is a level thing. The fact that it currently seems attached to rules is an interface defect

So maybe we should try to go back to the blackboard and see what we want to do. I see at least 2 toplevel options:

  • Make associativity a level decision in Rocq (as it is the case in Gramlib).
  • Make associativity a rule decision in Rocq and Gramlib.

I believe both are possible (ignoring the amount of effort), so the question is what we want. I'll try to give pros and cons, but I'm sure this is non-exhaustive.

Pros of level-based associativity (thus cons of rule-based associativity):

  • Minimum changes to Gramlib implementation.

Pros of rule-based associativity (thus cons of level-based associativity):

Ignoring difficulty and sunk-cost fallacy, rule-based associativity seems better. This would align with what both @SkySkimmer and @proux01 seemed to prefer in their last comment. If we want to go that direction, then indeed this PR is completely useless, because we will remove NONEA, LEFTA, and RIGHTA altogether. All levels will have all-associativity (i.e. syntactic SELF on either end means SELF and it's up to the user to avoid ambiguities).

I don't know if I will be able to implement the change to Gramlib but I could take a look if that's the decision. (That would be a much more ambitious project than just fixing Gramlib as this PR is doing.)

@jfehrle

jfehrle commented Nov 13, 2025

Copy link
Copy Markdown
Member

The term "associative" seems like it's probably misleading. The last time I looked at the parser a couple years ago, there was no way that a OP b OP c could be parsed as a OP (b OP c), ie right associatively in the most common sense. That generally requires a parser stack, which gramlib doesn't have. Gramlib seems to give "associative" a different meaning. Right?

@ia0

ia0 commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

there was no way that a OP b OP c could be parsed as a OP (b OP c), ie right associatively in the most common sense.

I believe this always worked:

Notation "x 'op' y" := (x, y) (at level 60, only parsing, right associativity).
Check I op I op I.
(* (I, (I, I)) : True * (True * True) *)

However, I agree that we use the word associative in a more general meaning than just binary operators. We use associative to describe the meaning of SELF on the left and on the right, regardless of the symbols in-between (there could be 0, 1, or more).

@proux01

proux01 commented Nov 14, 2025

Copy link
Copy Markdown
Contributor

Well, the current PR is essentially just introducing a deprecation warning.
Currently, associativities are managed in multiple ways both by gramlib and higher levels (be it (term) notations or Ltac2 tactic notations). This duplication leads to many bugs / hard to understand behaviors. Once we get rid of the bahavior that is deprecated in the current PR, we can get rid of associativities in gramlib (and other recovery mechanisms) and massively simplify it. Until then, we have plenty of time (at least a year) to discuss on whether we want to keep associativity a level based thing or make it rule based.
So the only "urgent" thing is to get Ltac2 Notation somehow compatible with the new warning, so that we can merge the current PR (it's a bit unfortunate that both get implemented in the same time frame, Ltac2 custom entries would have been much easier to implement with the simplified gramlib, not requiring all those hacks with RightA).

About level/rule based: IIRC, the major argument in favor of level based associativity was to reduce the risk of surprising behavior (what happens when mixing left and right associative notations at the same level). I'd say the major argument for rule based would be increased compatibility between libraries (no more dreaded error "level n is left-assoc in library A and right-assoc in library B"). In any case, I tend to think that interface/user facing arguments should come first before implementation cost.

The term "associative" seems like it's probably misleading. The last time I looked at the parser a couple years ago, there was no way that a OP b OP c could be parsed as a OP (b OP c), ie right associatively in the most common sense. That generally requires a parser stack, which gramlib doesn't have. Gramlib seems to give "associative" a different meaning. Right?

wrong

@ia0

ia0 commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

Once we get rid of the bahavior that is deprecated in the current PR, we can get rid of associativities in gramlib

I'm not able to follow this step. I don't see why we need strict associativity (this PR after deprecation) to get rid of associativities in gramlib. It seems to me we could just straight get rid of them now.

So the only "urgent" thing is to get Ltac2 Notation somehow compatible with the new warning

Ok, then if I find time this week-end I'll try to add the AllA variant and make that the default for Ltac2 custom entries. This AllA would behave the same as RightA today.

it's a bit unfortunate that both get implemented in the same time frame

Ah ok, Ltac2 custom entries is something new. That's why I couldn't find anything in the documentation. I was trying Print Custom Grammar mycustom in ltac2/custom_entry.v without success and had to guess what grammar was generated.

About level/rule based: IIRC, the major argument in favor of level based associativity was to reduce the risk of surprising behavior (what happens when mixing left and right associative notations at the same level). I'd say the major argument for rule based would be increased compatibility between libraries (no more dreaded error "level n is left-assoc in library A and right-assoc in library B").

Ok so it really seems rule-based is better, because it should be possible to avoid surprising behavior with a good syntax to describe production rules.

In any case, I tend to think that interface/user facing arguments should come first before implementation cost.

I totally agree.

@proux01

proux01 commented Nov 14, 2025

Copy link
Copy Markdown
Contributor

Once we get rid of the bahavior that is deprecated in the current PR, we can get rid of associativities in gramlib

I'm not able to follow this step. I don't see why we need strict associativity (this PR after deprecation) to get rid of associativities in gramlib. It seems to me we could just straight get rid of them now.

Don't we need to temporarily keep the associativities, at the very least to trigger the warning?

Ah ok, Ltac2 custom entries is something new. That's why I couldn't find anything in the documentation. I was trying Print Custom Grammar mycustom in ltac2/custom_entry.v without success and had to guess what grammar was generated.

Print Grammar (then looking for mycustom) will tell you. Unfortunately the full name contains a dash which prevents Print Grammar custom-ltac2:custom_entry.mycustom to work (we should probably fix that in a separate PR).

Ok so it really seems rule-based is better, because it should be possible to avoid surprising behavior with a good syntax to describe production rules.

Maybe we should also keep a warning like "using left associativity on level xyz previously only used for right associativity" to limit the risk of confusing behavior? But no strong opinion yet.

@ia0

ia0 commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

Once we get rid of the bahavior that is deprecated in the current PR, we can get rid of associativities in gramlib

I'm not able to follow this step. I don't see why we need strict associativity (this PR after deprecation) to get rid of associativities in gramlib. It seems to me we could just straight get rid of them now.

Don't we need to temporarily keep the associativities, at the very least to trigger the warning?

I'm not sure which warning you're talking about. I'm assuming "we can get rid of associativities in gramlib" means we only implement associativities at Rocq-level. To do this, we just need to make every level RIGHTA because the current semantics of RIGHTA is buggy (this PR is fixing it). It has the semantics of FULLA/ALLA which is the one you want if you had to choose only one (which is equivalent to having both associativities), because that's the most permissive and from that one you can either implement left-associativity or right-associativity (but not no-associativity, see below). That's essentially what ltac2 custom entries is doing. So this PR is not needed for that (actually this PR is bad for that, because it fixes RIGHTA to only be right-associativity and not all-associativity).

Actually, only using RIGHTA is not enough. Gramlib needs to be extended to support annotations when a continuation is after SELF or NEXT. It would be like having 3 trees instead of 2:

  • start (for rules that don't start with SELF or NEXT)
  • self-continue (for rules that start with SELF)
  • next-continue (for rules that start with NEXT)

Otherwise we still have the bug that right-associativity is all-associativity and no-associativity is left-associativity.

This change would actually be the change needed to get rid of associativities in Gramlib (thus have rule-based associativities in Gramlib).

Print Grammar (then looking for mycustom) will tell you.

Thanks, I forgot about that. I don't know why company-coq doesn't complete to it.

Maybe we should also keep a warning like "using left associativity on level xyz previously only used for right associativity" to limit the risk of confusing behavior? But no strong opinion yet.

Yes, we'll most probably need some warnings for the transition. But it's not clear to me yet how this is going to happen. I'll need to learn all the quirks of the translation from Rocq notations to Gramlib syntax.

@ia0 ia0 requested a review from a team as a code owner November 14, 2025 20:12
@ia0

ia0 commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

I've pushed a commit that adds BothA (called multi-associativity) that behaves like RightA before this PR, such that this PR can fix RightA to be right-associativity. I'm also making ltac2 custom entries use BothA instead of RightA, thus restoring the current behavior (no warning).

Also I'm using assert false in a lot of places because only ltac2 custom entries should use BothA. It's not reachable by users (but they can see it in Print Grammar of ltac2 custom entries). But maybe some of those assert false are reachable from ltac2 custom entries, I don't know. If that's the case please provide an example (or feel free to replace the assert false with the correct code).

ia0 and others added 7 commits November 18, 2025 08:55
@proux01

proux01 commented Nov 24, 2025

Copy link
Copy Markdown
Contributor

@coqbot merge now

@coqbot-app

coqbot-app Bot commented Nov 24, 2025

Copy link
Copy Markdown
Contributor

@proux01: You cannot merge this PR because:

  • There is still a needs: fixing label.

@proux01

proux01 commented Nov 24, 2025

Copy link
Copy Markdown
Contributor

@coqbot merge now

@coqbot-app

coqbot-app Bot commented Nov 24, 2025

Copy link
Copy Markdown
Contributor

@proux01: Please take care of the following overlays:

  • 21126-ia0-assoc.sh

@ia0

ia0 commented Nov 24, 2025

Copy link
Copy Markdown
Contributor Author

I'll send a PR to update docgram and refman to fix #21029 and #21072.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind: cleanup Code removal, deprecation, refactorings, etc. part: parser The parser (also called gramlib, forked from camlp5)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants