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

Add warnings about possibly confused operator precedence #2783

Merged
merged 29 commits into from
Nov 16, 2022

Conversation

RustanLeino
Copy link
Collaborator

@RustanLeino RustanLeino commented Sep 21, 2022

A common mistake is to write an expression like

&& A
&& B ==> C
&& D

with the intention of writing

&& A
&& (B ==> C)
&& D

This PR emits warnings when certain operator arguments seem to go on for too long, where "seem to" is judged based on identation.

The basic idea is to look where the right-hand operand of ==> starts and then insist that all tokens in the RHS are aligned at, or to the right of, the column of the RHS start. This same idea is used for other kinds of "right-hand sides", too, like the else branch of an if-then-else expression and the body ("term") of logical quantifiers.

When looking at the RHS, certain parts of the RHS expression are ignored (or, rather, are treated separately). This happens when an expression has a "middle" part, like the then branch of an if-then-else expression. For example, no warning is produced for

  if x < 0 then
-x
    else x

If an expression of interest e is found in the RHS, then e's internal structure is analyzed for e but not for the enclosing expression. For example, no warning is produced for

  forall x :: P(x) ==>
Q(x)

The left-hand side of ==> is also analyzed. Some more details are found in the VisitOneExpr method in PrecedenceLinter.cs.

The binary operators ==>, <==, and <==> are treated as mentioned above, since these operators have relatively low binding powers. The linter does not do this processing for any other binary operators, like &&, ||, +, and *.

Fixes #1311

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

@RustanLeino RustanLeino marked this pull request as ready for review September 29, 2022 15:55
Copy link
Member

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

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

A few options to consider to make this PR simpler, but I like the idea !
I think I found at least one false positive warning, and I suggest a fix.

VisitLhsComponent(expr.tok, bin.E0,
// For
// a) LHS ==> RHS
// b) LHS ==>
Copy link
Member

Choose a reason for hiding this comment

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

Where is the RHS here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was unclear. My intention was to say the RHS is somewhere on the next line (in particular, the RHS is not on the same line as the ==>). I have clarified this in the source-file comments.

} else if (expr is ITEExpr ifThenElse) {
VisitIndependentComponent(ifThenElse.Test);
VisitIndependentComponent(ifThenElse.Thn);
VisitRhsComponent(expr.tok, ifThenElse.Els, "else branch of if-then-else");
Copy link
Member

Choose a reason for hiding this comment

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

what about

if A then B else match C {
  case Z =>
} 

The formatter is happy because it finds that the match's content does not need to be aligned (although there is a deactivated function for that)
Maybe in your case, you could issue the warning only if the expression after the else is a binary or a chaining operator ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What you're suggesting is what is happening. More precisely, the body of the match is visited independently of the context it's found in. So, no warning is generated for the example you gave.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added the example as a test case, showing that it does not generate any warnings.

VisitIndependentComponent(quantifierExpr.Range);
}
VisitRhsComponent(expr.tok, quantifierExpr.Term,
expr.tok.line == quantifierExpr.Term.StartToken.line ? expr.tok.col + 1 : quantifierExpr.Term.StartToken.col,
Copy link
Member

Choose a reason for hiding this comment

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

So you are suggesting that, if there is no newline after the "::", then everything should be after the "::", correct ? If so, can you please add a comment in plain English about that.

Also, the following code is legitimate (the Dafny formatter will detect a match expression and allow the line wrap to be past before the :: column), and it would still trigger a warning in your setting.

(forall x: nat | x < 10 :: x match {
     case P(x) => true
     case Q(y) => x == false
 })

To avoid that, you could put a warning only if the expression after :: is a parenthesesless expression, like for the else example above.

Copy link
Member

Choose a reason for hiding this comment

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

Could you please ensure there is a test for the expression above, to show that it does not trigger errors? After that, I'm good approving it !

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't quite follow your example, because it is not legal Dafny. I added the following test case, which I hope captures the spirit of your example:

  datatype PQ = P(int) | Q(bool)

  method M(s: set<PQ>)
    requires
      (forall pq | pq in s :: match pq {
          case P(x) => true
          case Q(y) => y == false
      })
  {
  }

No precedence warning is generated for this example.

Source/DafnyCore/PrecedenceLinter.cs Show resolved Hide resolved
Copy link
Member

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

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

This PR is good to merge. It would have been useful to me a few days ago ... will surely be useful to others !

@RustanLeino RustanLeino merged commit 06c52ec into dafny-lang:master Nov 16, 2022
@RustanLeino RustanLeino deleted the precedence-warnings branch November 16, 2022 23:03
atomb added a commit to atomb/dafny that referenced this pull request Dec 12, 2022
This uses `FormatTokens` to keep track of the beginning of a sequence
of expressions prefixed by `&&` or `||` instead of a synthetic identity
expression.

Not quite complete, as it causes occasional null pointer exceptions
when retrieving the range of certain `TokenWrapper` instances that
seem to be wrapped around null.
atomb added a commit that referenced this pull request Dec 15, 2022
This uses `FormatTokens` to keep track of the beginning of a sequence of
expressions prefixed by `&&` or `||` instead of a synthetic identity
expression.

Fixes a performance regression caused by #2783.

By submitting this pull request, I confirm that my contribution
is made under the terms of the [MIT
license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).
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.

Linter to catch accidental implication capturing?
2 participants