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

Implement verification coverage analysis #741

Merged
merged 29 commits into from
Jun 6, 2023

Conversation

atomb
Copy link
Collaborator

@atomb atomb commented Jun 1, 2023

This generalizes the previous feature of tracking "necessary assumptions".

That feature, previously enabled with -printNecessaryAssumptions, allowed each assume statement to be annotated with an {:id ...} attribute, and Boogie would then report which assumptions were necessary to complete the verification by requesting an unsat core from Z3.

Now that attribute can be placed on many other program elements:

  • assume statements
  • assert statements
  • assignments
  • calls
  • requires clauses
  • ensures clauses

Each of these is then labeled in the SMT encoding and will potentially show up in the unsat core.

The new -printVerificationCoverage option (which replaces the old one) prints the members of this broader set of program elements that show up in the unsat core. With -trace, it also prints the coverage information for each procedure as it finishes. The coverage information for each procedure is also available in the VCResult produced at the end of each verification.

Fixes #730.

@atomb atomb marked this pull request as ready for review June 1, 2023 18:19
@keyboardDrummer
Copy link
Collaborator

The new -printVerificationCoverage option prints the members of this broader set of program elements that show up in the unsat core.

Is there a need for a new option then? Why not expand the support of the existing option? (and possibly rename it)

Copy link
Collaborator

@keyboardDrummer keyboardDrummer left a comment

Choose a reason for hiding this comment

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

Is there existing documentation on the id attribute? If not could you add it?

@@ -3720,6 +3720,7 @@ protected override Cmd ComputeDesugaring(PrintOptions options)
Dictionary<Variable, Expr> substMapBound = new Dictionary<Variable, Expr>();
List<Variable> /*!*/
tempVars = new List<Variable>();
string idAttr = QKeyValue.FindStringAttribute(Attributes, "id");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be named id, since it's a string not an attribute?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point.

@@ -3846,6 +3848,11 @@ protected override Cmd ComputeDesugaring(PrintOptions options)
a.Attributes = attrCopy;
}

// Do this after copying the attributes so it doesn't get overwritten
Copy link
Collaborator

Choose a reason for hiding this comment

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

The method being changed is very large, and uses regions instead of submethods to organize code. I want to ask you apply the boyscout principle of "leaving things behind nicer than they were", and to chop this method up into pieces, and also to move the class SugaredCmd out into it's own file, since it's in a file that's way too large.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Created #743 to track this, as I think we need to do it very carefully.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to do it very carefully

Why? Splitting a method up into pieces can be done in a mostly automated way using Rider. Same for moving a class into a separate file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, but reorganizing those methods so that they are well-organized and use better abstractions, rather than just slicing them into pieces, would require more thought.

Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 6, 2023

Choose a reason for hiding this comment

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

I'm paraphrasing an offline discussion I had with @atomb on the above, for the sake of getting some additional eyes on it.

@atomb prefers keeping the pacify implementations for various commands in a single method, since that allows having them be vertically adjacent to each other which follows the structure of Figure 3 in this paper, which is what the code is based on. Keeping the structure of the two most similar provides the best way of doing a side-by-side comparison.

I would prefer keeping the code for different commands separate. I would even prefer having an abstract Pacify method in the Cmd class, and having the commands in separate files. The number of commands is not fixed, we might add more in the future, and I think in this way the codebase scales well to that.

I believe that while the paper is structured by putting the concepts at the top-level, and then discussing all commands within each concept, this doesn't mean that's also the right structure for the code. It's easier to discuss just one pair of concept and command at a time, and while the paper doesn't have room to go that slow, in our codebase we can use as many separators, such as files and methods, as we like. Also, the code is more verbose so there is more need to keep the scope of each block to a minimum.

I don't think optimizing for comparability between the code and the paper is important. Modern IDEs make it easy to navigate between the code for various commands, so I don't think there's a particular difficulty in making the comparison in any case.

Source/VCGeneration/Wlp.cs Outdated Show resolved Hide resolved
@@ -270,7 +270,9 @@ public override bool Visit(VCExprVar node, bool arg)
RegisterType(node.Type);
string decl =
"(declare-fun " + printedName + " () " + TypeToString(node.Type) + ")";
if (!(printedName.StartsWith("assume$$") || printedName.StartsWith("soft$$") ||
if (!(printedName.StartsWith("assume$$") ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where does this list of constants come from? Can they be injected from their origin instead of hardcoded 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.

That certainly would be nicer.

@@ -21,6 +21,7 @@
<ProjectReference Include="..\Houdini\Houdini.csproj" />
<ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\VCGeneration\VCGeneration.csproj" />
<ProjectReference Include="..\Provers\SMTLib\SMTLib.csproj" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can be removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The build of Boogie itself works without it, but I discovered it was necessary for Dafny (and presumably other Boogie clients?) to build against a Boogie source tree instead of a pre-built package.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK, the alternative is that the Boogie clients such as Dafny also depend on SMTLib, but this seems like a nicer UX.

{
var arg = QKeyValue.FindStringAttribute(src.Attributes, attr);
if (arg is not null) {
dest.Attributes = new QKeyValue(tok, attr, new List<object>( ){arg + suffix}, dest.Attributes);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why did you create static methods instead of instance ones?

I suggest you move Declaration.FindStringAttribute to ICarriesAttributes.FindStringAttribute and add a method ICarriesAttributes.AddAttribute, then CopyStringAttributeWithSuffix becomes:

var arg = src.FindStringAttribute(attr);
if (arg is not null) {
  dest.AddAttribute(tok, attr, arg + suffix);
}

@@ -729,7 +732,13 @@ private void ConvertCFG2DAGStandard(Implementation impl, Dictionary<Block, List<
b = new LoopInitAssertCmd(c.tok, c.Expr, c);
}

b.Attributes = c.Attributes;
b.Attributes = (QKeyValue)c.Attributes?.Clone();
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 clone necessary? It doesn't seem like attributes are generally modified, instead they seem to be treated more as an immutable linked list.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's necessary, since the id attribute on the new command will have a suffix appended. In principle, I guess the two commands could share an attribute list tail, but that seems more complex than it's worth.

Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 5, 2023

Choose a reason for hiding this comment

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

Note that the original command is discarded so its attributes won't be used by it and you could get away with one less clone, but either is OK with me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It feels fragile to me to depend on the fact that the original command is never re-used. I agree that it isn't at the moment, but future work could change that.

}
}

public static void CopyStringAttributeWithSuffix(IToken tok, ICarriesAttributes src, string attr, string suffix, ICarriesAttributes dest)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This method is only used with id as a value for attr. I suggest you hardcode that in the implementation and remove the parameter, and rename to CopyIdWithSuffix

@@ -4003,6 +4015,9 @@ protected override Cmd ComputeDesugaring(PrintOptions options)
Contract.Assert(e != null);
Expr copy = Substituter.ApplyReplacingOldExprs(calleeSubstitution, calleeSubstitutionOld, e.Condition);
AssumeCmd assume = new AssumeCmd(this.tok, copy);
if (idAttr is not null) {
ICarriesAttributes.CopyStringAttributeWithSuffix(tok, e, "id", $"${idAttr}$ensures", assume);
Copy link
Collaborator

Choose a reason for hiding this comment

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

On line 4026, the assume.Attributes can be overridden.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, good catch. I should move this down.

@@ -4003,6 +4015,9 @@ protected override Cmd ComputeDesugaring(PrintOptions options)
Contract.Assert(e != null);
Expr copy = Substituter.ApplyReplacingOldExprs(calleeSubstitution, calleeSubstitutionOld, e.Condition);
AssumeCmd assume = new AssumeCmd(this.tok, copy);
if (idAttr is not null) {
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 copying the id from the ensures clause of the callee, over to the assumes that placed after the call ? Doesn't that lead to duplicate ids if there is more than one call to a callee?

Can you add a test-case where there is more than 1 call to a callee, and the coverage results are different per call?

Also, what is the purpose of tracking coverage results for procedure calls? I would have expected tracking coverage for requires/ensures/assume/assert, but not call.

Copy link
Collaborator Author

@atomb atomb Jun 2, 2023

Choose a reason for hiding this comment

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

The id of the call statement itself is included in the id for the postcondition to avoid exactly that duplication.

It's useful to know whether a postcondition (or a precondition) from a particular call is used, rather than whether the postcondition is used in general. Some of the things that it can help you identify:

  • a context-dependent contradictory postcondition (and, in particular, what parts of it lead to a contradiction)
  • a postcondition that's only rarely used, and perhaps better established in other ways (such as by splitting a lemma into multiple lemmas)

When combined with split-on-every-assert, this can also help you identify exactly how each lemma you use contributes to each goal you're trying to prove. That could help in refactoring the lemmas you're using, or the structure of the current proof.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The id of the call statement itself is included in the id for the postcondition to avoid exactly that duplication.

I see! Could you rename id to callId ?

Can you add a test-case where there is more than 1 call to a callee, and the coverage results are different per call?

Consider still doing this.

@atomb
Copy link
Collaborator Author

atomb commented Jun 2, 2023

The new -printVerificationCoverage option prints the members of this broader set of program elements that show up in the unsat core.

Is there a need for a new option then? Why not expand the support of the existing option? (and possibly rename it)

I indeed did that at first, but then decided that there may be cases where someone would want the old behavior so I decided not to break it. I'm quite happy to merge the two, with the new name.

@atomb atomb requested a review from keyboardDrummer June 2, 2023 19:00
@shazqadeer
Copy link
Contributor

shazqadeer commented Jun 3, 2023

Before this PR, I did not know about this "coverage" feature in Boogie. Interesting stuff! This PR would make the feature more obvious to Boogie users and also make the feature more comprehensive. Nice, @atomb .

Is the plan to land a PR addressing #743 first and then rebase this PR?

@keyboardDrummer
Copy link
Collaborator

I indeed did that at first, but then decided that there may be cases where someone would want the old behavior so I decided not to break it. I'm quite happy to merge the two, with the new name.

It seems to me that if you only have id attributes on places where the old option supported them, then the new option already gives the same behavior as the old one. In that case, let's merge them.

@@ -729,7 +732,13 @@ private void ConvertCFG2DAGStandard(Implementation impl, Dictionary<Block, List<
b = new LoopInitAssertCmd(c.tok, c.Expr, c);
}

b.Attributes = c.Attributes;
b.Attributes = (QKeyValue)c.Attributes?.Clone();
if (Options.PrintVerificationCoverage) {
Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 5, 2023

Choose a reason for hiding this comment

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

I don't think Print should be in the name of this field, since you should also be able to use it in a fully programmatic way in which the consumer never prints anything. Maybe the Dafny IDE presents it through some UI for example.

@@ -749,9 +758,22 @@ private void ConvertCFG2DAGStandard(Implementation impl, Dictionary<Block, List<
b = new Bpl.LoopInvMaintainedAssertCmd(c.tok, c.Expr, c);
}

b.Attributes = c.Attributes;
b.Attributes = (QKeyValue)c.Attributes?.Clone();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you improve the names of b and c and stop them from being assigned multiple times?

@@ -4003,6 +4015,9 @@ protected override Cmd ComputeDesugaring(PrintOptions options)
Contract.Assert(e != null);
Expr copy = Substituter.ApplyReplacingOldExprs(calleeSubstitution, calleeSubstitutionOld, e.Condition);
Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 5, 2023

Choose a reason for hiding this comment

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

I'd consider this a favor, but it would be amazing if you could follow-up this PR with one where many of the AST classes are moved to separate files. Here for example it's relatively difficult for me to see what this in this.Proc.Ensures points to, since CallCmd is not in its own file. Placing classes in separate files is a Rider supported refactoring, so it should be fairly trivial to do.

I can also do it if you like.

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 can probably do this in a couple of weeks, but certainly wouldn't object if you did it sooner! :)

@atomb
Copy link
Collaborator Author

atomb commented Jun 5, 2023

Is the plan to land a PR addressing #743 first and then rebase this PR?

My preference would be to address #743 later (though probably not a lot later).

@atomb atomb requested a review from keyboardDrummer June 5, 2023 16:37
@@ -280,6 +280,33 @@ public List<int> FindLayers()
}
return layers.Distinct().OrderBy(l => l).ToList();
}

// Look for {:name string} in list of attributes.
public string FindStringAttribute(string name)
Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 6, 2023

Choose a reason for hiding this comment

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

If you define these methods as extension methods, then you can also call them without casting to ICarriesAttributes first.

}
}

public void CopyIdWithSuffixFrom(IToken tok, ICarriesAttributes src, string suffix)
Copy link
Collaborator

Choose a reason for hiding this comment

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

target.Copy reads strangely. I would expect the src to be on the left side of the copy verb

{
if (a is AssertCmd)
if (predicateCmd is AssertCmd)
Copy link
Collaborator

@keyboardDrummer keyboardDrummer Jun 6, 2023

Choose a reason for hiding this comment

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

You can use if (predicateCmd is AssertCmd assertCmd) here

PredicateCmd a = header.Cmds[i] as PredicateCmd;
if (a != null)
PredicateCmd predicateCmd = header.Cmds[i] as PredicateCmd;
if (predicateCmd != null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can use if (header.Cmds[i] is PredicateCmd predicateCmd) here

@keyboardDrummer keyboardDrummer merged commit ac9493b into boogie-org:master Jun 6, 2023
@atomb atomb deleted the broader-unsat-cores branch January 4, 2024 17:07
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.

Allow any construct to be tracked by "necessary assumptions"
3 participants