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

Feat: assert {:only} to temporarily work on a single assertion #3933

Merged
merged 20 commits into from
May 3, 2023

Conversation

MikaelMayer
Copy link
Member

@MikaelMayer MikaelMayer commented May 1, 2023

This PR fixes #3095 by introducing assert {:only}. As stated in the documentation:

assert {:only} X; temporarily transforms all other non-{:only} assertions in the surrounding declaration into assumptions.

method Test() {
  assert true;                  // Unchecked
  assert {:only} true by {      // Checked
    assert true;                // Checked
  }
  assert true;                  // Unchecked
  assert {:only "after"} true;  // Checked
  assert true;                  // Checked
  assert {:only "before"} true; // Checked
  assert true;                  // Unchecked
}

{:only} can help focusing on a particular proof or a particular branch, as it transforms not only other explicit assertions, but also other implicit assertions, and call requirements, into assumptions.
Since it's meant to be a temporary construct, it always emits a warning.
It also has two variants assert {:only "before"} and assert {:only "after"}.
Here is precisely how Dafny determines what to verify or not.
{:only} annotations define "verification interval" which are visual:

  • assert {:only} X [by {...} | ;] sets a verification interval that starts at the keyword assert and ends either at the end of the proof } or the semicolon ;, depending on which variant of assert is being used.
  • assert {:only} ... inside an other verification interval removes that verification interval and sets a new one.
  • assert {:only "before"} ... inside another verification interval finishes that verification interval earlier at the end of this assertion. Outside verification intervals, it sets a verification interval from the beginning of the declaration to the end of this assertion, but only if there were no other verification intervals before.
  • assert {:only "after"} ... inside another verification interval moves the start of that verification interval to the start of this new assert. Outside verification interval, it sets a verification interval from the beginning of this assert to the end of the declaration.

The start of an asserted expression is used to determines if it's inside a verification interval or not.
For example, in assert B ==> (assert {:only "after"} true; C), C is actually the start of the asserted expression, so it is verified because it's after assert {:only "after"} true.

As soon as a declaration contains one assert {:only}, none of the postconditions are verified; you'd need to explicit them with assertions if you wanted to verify them at the same time.

Integration

  • I updated the auditor to report usages of assert {:only}
  • I updated the doc
  • I added a code action to suggest to remove the {:only ...} in VSCode.

Alternatives considered

It's currently possible to use {:selective_checking true} on a declaration and and {:start_checking_there} on an assertion to "comment out" all previous assertions.
However, this mechanism is not suitable for us, even transpiling our attributes to that, for the following reasons:. It works at the Boogie level, but at the Dafny level it's often misleading. For example, {:start_checking_there} on an assert {:start_checking_there} X by { Y } will actually start checking X but not the proof Y because in Boogie the proof is written before the assertion.

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

@MikaelMayer MikaelMayer requested a review from atomb May 1, 2023 14:42
@MikaelMayer MikaelMayer self-assigned this May 1, 2023
Copy link
Member

@atomb atomb left a comment

Choose a reason for hiding this comment

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

This is a feature I expect I'll use frequently. :) I had a few nits about wording, and a broader comment about whether the notion of "range" used here is the right one. It might be, but I'm not yet 100% convinced.


public static AssumptionDescription AssertOnly = new(
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 use field labels here for consistency with the other instances above? In particular, it's hard to remember which of the bool fields is which without the labels. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Could you use field labels here for consistency with the other instances above? In particular, it's hard to remember which of the bool fields is which without the labels. :)

Indeed, I hadn't seen that these labels weren't explicit as my IDE displays them if they are not provided ^^. That made review more confusing than necessary.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, right! Sometimes helpful things aren't as helpful. :)

static ResolutionErrors() {
Add(ErrorId.r_assert_only_assumes_others,
@"
When annotating an assert with the `{:only}` attribute, all other implicit and explicit assertions
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When annotating an assert with the `{:only}` attribute, all other implicit and explicit assertions
When annotating an assertion with the `{:only}` attribute, all other implicit and explicit assertions

Add(ErrorId.r_assert_only_assumes_others,
@"
When annotating an assert with the `{:only}` attribute, all other implicit and explicit assertions
are assumed. This is a way to focus on an assertion and its proof, but this annotation has to be removed once finished.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
are assumed. This is a way to focus on an assertion and its proof, but this annotation has to be removed once finished.
are transformed into assumptions. This is a way to focus on an assertion and its proof, but this annotation has to be removed once finished.


if (assertStmt != null && Attributes.Find(assertStmt.Attributes, "only") is UserSuppliedAttributes attribute) {
reporter.Warning(MessageSource.Verifier, ResolutionErrors.ErrorId.r_assert_only_assumes_others.ToString(), attribute.RangeToken.ToToken(),
"Assertion with {:only} temporarily transforms other assertions in assumptions");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"Assertion with {:only} temporarily transforms other assertions in assumptions");
"Assertion with {:only} temporarily transforms other assertions into assumptions");

Since it's meant to be a temporary construct, it always emits a warning.
It also has two variants `assert {:only "before"}` and `assert {:only "after"}`.
Here is precisely how Dafny determines what to verify or not.
`{:only}` annotations define "verification interval" which are visual:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`{:only}` annotations define "verification interval" which are visual:
Each `{:only}` annotation defines a "verification interval" which is visual:

@@ -3240,6 +3240,7 @@ class Specialization {
bool inBodyInitContext = false; // true during the translation of the .BodyInit portion of a divided constructor body
readonly Dictionary<string, Bpl.IdentifierExpr> definiteAssignmentTrackers = new Dictionary<string, Bpl.IdentifierExpr>();
bool assertAsAssume = false; // generate assume statements instead of assert statements
Func<IToken, bool> assertionOnlyFilter = null; // generate assume statements instead of assert statements if not targetted by {:only}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Func<IToken, bool> assertionOnlyFilter = null; // generate assume statements instead of assert statements if not targetted by {:only}
Func<IToken, bool> assertionOnlyFilter = null; // generate assume statements instead of assert statements if not targeted by {:only}

@@ -7296,7 +7297,9 @@ public ForceCheckToken(IToken tok)
Contract.Requires(condition != null);
Contract.Ensures(Contract.Result<Bpl.PredicateCmd>() != null);

if (assertAsAssume || (RefinementToken.IsInherited(refinesToken, currentModule) && (codeContext == null || !codeContext.MustReverify))) {
if (assertAsAssume
|| (assertionOnlyFilter != null && !assertionOnlyFilter(tok))
Copy link
Member

Choose a reason for hiding this comment

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

Convenient that this code was already here, and you just had to add another condition under which to enable it! :)

@@ -26,6 +26,7 @@ public record DafnyAction(string Title, IReadOnlyList<DafnyCodeActionEdit> Edits

public static class ErrorRegistry {

// Replace any NoneId by ParseErrors.p_... or ResolutionErrors.r_
Copy link
Member

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 this comment, or why it's in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I came across warnings and errors which were using ErrorRegistry.NoneId as identifier. However, at first, I did not know how to put an identifier that has some meaning, so I had to do research and found out that meaningful identifiers are actually defined in ParseErrors.p_.... so I created the same for ResolutionErrors.r_.... and put a comment so that if a resolution error is using ErrorRegistry.NoneId and someone wants to create a rich error (e.g. that contains extended error text or a code action), they know where to go and what to do.

Copy link
Member

Choose a reason for hiding this comment

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

Got it.

m.Visit(node => {
if (node is AssertStmt assertStmt && Attributes.Find(assertStmt.Attributes, "only") is UserSuppliedAttributes attribute) {
var (isBefore, isAfter) = attribute.Args.Count == 1 && attribute.Args[0] is LiteralExpr { Value: var value } &&
value is "after" or "before" ? (value is "before", value is "after") : (false, false);
Copy link
Member

Choose a reason for hiding this comment

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

If it were me, I'd move the logic for extracting the {:only} attribute to a separate definition. Then maybe you'd have a datatype AssertOnlyKind = After | Before | Only and a bool HasAssertOnlyAttribute(out var assertOnlyKind), or something like that.

@@ -1353,6 +1355,49 @@ public partial class Translator {
builder.Add(Assert(tok, q, new PODesc.TraitFrame(m.WhatKind, true), kv));
}

// Return a way to know if an assertion should be converted to an assumptions
Copy link
Member

Choose a reason for hiding this comment

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

The notion of "before" and "after" here seems potentially confusing. For instance, if you were to say assert {:only "before"} P; in the else branch of an if statement, it would enable assertions in the other branch of the if statement. It's a simple notion, though, so perhaps the strangeness of it isn't fatal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I thought about that too. The advantage is that the notions are straightforward indeed for now. I primarily envision users to use {:only} by itself for a single proof, and if they really want, they can delimit without surprise which assertions should be verified using {:only "before"} and {:only "after"}. It's just that I had to give meaning to single instances of {:only "before"} and {:only "after"}.

I also thought of a more coarse-grained numerical notion of "level", which is one that @jtristan suggested one day. For example, assert {:only 0} will be the same as assert {:only}, whereas assert {:only 1} will define a verification interval on the surrounding block, assert {:only 2} on the block containing that block, etc. Because we traverse the program using a visitor pattern and nodes, this would be quite easy to implement.
If you like this idea, that would make three of us to like it so I would happily implement it as part of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that makes sense. And adding those would be backward-compatible, so no need to hold up this PR for it.

Copy link
Member

@atomb atomb left a comment

Choose a reason for hiding this comment

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

Thanks for implementing this!

@MikaelMayer MikaelMayer enabled auto-merge (squash) May 3, 2023 15:29
@MikaelMayer MikaelMayer merged commit e47bb85 into master May 3, 2023
19 checks passed
@MikaelMayer MikaelMayer deleted the fix-3095-assert-only branch May 3, 2023 17:51
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.

[Feature request]: {:only} on assertions
2 participants