-
Notifications
You must be signed in to change notification settings - Fork 268
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 plugin to create wrapper functions/methods to check contracts at runtime #2712
Conversation
This works on one example, but is know to fail for polymorphic code and probably many other types, as well.
* Generate fewer resolved AST nodes before resolution happens * Don't run the redirection code while it's not working
Still doesn't work, though
Now it runs during `PostResolveIntermediate` so that it has easier access to attributes.
The following Dafny and C# files, together, demonstrate the basic operation of this for checking calls to externs from tests. The Dafny code ( method {:extern} Foo(x: int) returns (y: int)
requires 1 < x < 10;
ensures y >= 10;
method FooCaller(x: int) returns (y: int)
requires 1 < x < 10;
ensures y >= 10;
{
y := Foo(x);
}
method {:test} FooTest()
{
var y := Foo(3);
expect y == 30;
}
function method {:extern} Bar(x: int): (y: int)
requires 1 < x < 10;
ensures y >= 10;
function method BarCaller(x: int): (y: int)
requires 1 < x < 10;
ensures y >= 10;
{
Bar(x)
}
method {:test} BarTest()
{
var y := Bar(3);
expect y == 30;
}
And the C# code ( using System.Numerics;
namespace _module {
public partial class __default {
public static BigInteger Foo(BigInteger x)
{
return BigInteger.Zero;
}
public static BigInteger Bar(BigInteger x)
{
return BigInteger.Zero;
}
}
} These can be run together with the following command: dafny /compile:3 /runAllTests:1 /testContracts:Tests /useBaseNameForFileName check-extern.dfy FooBar.cs This yields the following:
|
I think this PR is largely complete. The one key open question in my mind is whether the current set of |
I'm ready to review it ! Let me have a look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job ! This will be very valuable. Would it be possible that /runAllTests automatically triggers ExternsInTest to be set by default? This would not completely be a breaking change, as you'd only issue warnings.
Also I suggested a few doc upgrades
@@ -673,6 +673,8 @@ public class Cloner { | |||
var body = s.Body == null ? null : CloneBlockStmt(s.Body); | |||
r = new ModifyStmt(Tok(s.Tok), Tok(s.EndTok), mod.Expressions, mod.Attributes, body); | |||
|
|||
} else if (stmt is CallStmt s) { | |||
r = new CallStmt(Tok(s.Tok), Tok(s.EndTok), s.Lhs, s.MethodSelect, s.Args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good catch ^^
Source/DafnyCore/DafnyOptions.cs
Outdated
@@ -1425,6 +1444,16 @@ verification outcome. | |||
option is useful in a diamond dependency situation, to prevent code | |||
from the bottom dependency from being generated more than once. | |||
|
|||
/testContracts:<AllExterns|ExternsInTests> | |||
Enable run-time testing of compiled function or method contracts in | |||
certain situations, currently focused on :extern code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
certain situations, currently focused on :extern code. | |
certain situations, currently focused on {:extern} code. |
Source/DafnyCore/DafnyOptions.cs
Outdated
certain situations, currently focused on :extern code. | ||
|
||
AllExterns - Check contracts on every call to a function or | ||
method marked :extern, regardless of where it occurs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
method marked :extern, regardless of where it occurs. | |
method with the {:extern} attribute, regardless of where it occurs. |
Source/DafnyCore/DafnyOptions.cs
Outdated
method marked :extern when it occurs in one marked :test, and | ||
warn if no corresponding :test exists for a given :extern. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
method marked :extern when it occurs in one marked :test, and | |
warn if no corresponding :test exists for a given :extern. | |
method with the {:extern} attribute when it occurs in a method with the {:test} attribute, and | |
warn if no corresponding method with a {:test} attribute exists for a given method with the {:extern} attribute. |
Source/DafnyCore/DafnyOptions.cs
Outdated
@@ -1425,6 +1444,16 @@ verification outcome. | |||
option is useful in a diamond dependency situation, to prevent code | |||
from the bottom dependency from being generated more than once. | |||
|
|||
/testContracts:<AllExterns|ExternsInTests> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I've read the doc, let me rephrase what these options do:
AllExterns will run the contract specifications on every extern method
ExternsInTests will run the contract specifications on every extern method, except those that are not part of any {:test} and thus will only issue a warning about the lack of test.
I see the two options to be useful, but 1 seems even more valuable, so let me suggest a different name
AllExterns => externs
ExternsInTests => testedExterns
What do you think of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I like that naming. I'll switch to it.
@@ -546,6 +550,11 @@ enum ValuetypeVariety { | |||
// Now we're ready to resolve the cloned module definition, using the compile signature | |||
|
|||
ResolveModuleDefinition(nw, compileSig); | |||
|
|||
foreach (var rewriter in rewriters) { | |||
rewriter.PostCompileCloneAndResolve(nw); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new phase in the rewriter ! Fascinating.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took an in-depth debugging session with @RustanLeino to understand that that was necessary! It's likely to be useful for other rewriter plugins that operate (even partly) post-resolution and need to make sure their effects persist in the compiled code.
return new BlockStmt(callStmt.Tok, callStmt.EndTok, bodyStatements.ToList()); | ||
} | ||
|
||
private static Expression MakeApplySuffix(IToken tok, string name, List<Expression> args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please move this generic expression builder to Expressions.cs or somewhere similar?
var expectEnsuresStmts = ensures.Select(ens => | ||
CreateContractExpectStatement(ens, "ensures")); | ||
var callStmtList = new List<Statement>() { callStmt }; | ||
var bodyStatements = expectRequiresStmts.Concat(callStmtList).Concat(expectEnsuresStmts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's interesting. So your approach is to rewrite any extern method call to inline the requires call and the expects clause.
Do you need to check the requires? Isn't that going to be checked by Dafny statically?
For the ensures, it's great, because it means one can write a test fuzzer without any expect, and the method's postcondition will still be checked. Could you please add this idea in documentation of /testContracts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For externs (of the form that this is focused on, where the implementation is written in the target language), that's true. Dafny will prove that the requires clause is satisfied. However, if you were to apply a fuzzer to the target language code, the existence of a branch on a requires clause is likely to make it easier to come up with inputs that satisfy that requires clause. Arguably, perhaps, it might make sense to check all of the requires clauses together in one expect
statement, but check the ensures clauses separately, as the current code does.
I think any changes like that would be driven by how well it works in practice, though.
I'll add some reference manual documentation of /testContracts
that goes into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, if you were to apply a fuzzer to the target language code, the existence of a branch on a requires clause is likely to make it easier to come up with inputs that satisfy that requires clause.
If you were to apply a fuzzer to the target language code, should you not gracefully exit if the requires is not satisfied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, yes, though I think that the current structure would at least give you enough to get started. If you run a fuzzer, and filter out the cases of failing requires clauses, you'll get a useful outcome.
In the long run, I'd like this code to integrate with target language fuzzing/verification APIs, using assume
-like things for requires
clauses and assert
-like things for ensures
clauses. I think that's a bit down the road, though.
if (origFunc.Result?.Name is null) { | ||
body.AppendStmt(new ReturnStmt(tok, tok, new List<AssignmentRhs> { new ExprRhs(localExpr) })); | ||
} | ||
newFunc.ByMethodBody = body; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clever ! You transform the ByMethod body of an extern function with these expect statements. Love it.
} | ||
} | ||
|
||
private class CallRedirector : TopDownVisitor<MemberDecl> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please add a comment about this class?
Source/DafnyCore/DafnyOptions.cs
Outdated
@@ -99,7 +99,7 @@ public enum PrintModes { | |||
public enum ContractTestingMode { | |||
None, | |||
AllExterns, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please rename this so that it matches the option?
Source/DafnyCore/DafnyOptions.cs
Outdated
@@ -1444,15 +1444,16 @@ verification outcome. | |||
option is useful in a diamond dependency situation, to prevent code | |||
from the bottom dependency from being generated more than once. | |||
|
|||
/testContracts:<AllExterns|ExternsInTests> | |||
/testContracts:<Externs|TestedExterns> | |||
Enable run-time testing of compiled function or method contracts in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realize one important thing. You are not testing "compiled functions", you are testing "compilable (function or method) contracts". Could you please rephrase it like the compilable contracts of methods or functions that have the {:extern} attribute, at the call site
? I think it's important to mention the call site.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah, my original intent of the word "compiled" was just to say that this only applied to functions and methods that are being compiled. I think your use of the word is better, though! So I'll change the text accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job ! A good step forward to make it possible to rely more on contracts.
Down the road, how hard do you think it would be to integrate testing as part of the IDE extension?
I suspect it'd be fairly easy, though I don't know the details of how testing works through LSP. I think that'd be a very nice thing to dig into soon. |
@@ -1668,3 +1668,27 @@ periods. | |||
compiled assembly rather than including `DafnyRuntime.cs` in the build | |||
process. | |||
|
|||
|
|||
* `-testContracts:<mode>` - test certain function and method contracts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"test certain ... contracts" -- which ones? all non-ghost ones? all non-ghost ones that are executable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I updated the help text with what I think is better wording and forgot to update the RM! What do you think of the wording in DafnyOptions.cs
?
This PR adds a flag to allow certain function and method contracts to be tested at runtime. For the moment, it focuses on
{:extern}
code, but could be applied to other code in the future. Some details of the design are described in the comment thread of issue #2235.It is usable with the following:
I'm not entirely happy that it required removing a few
readonly
qualifiers from AST fields, but that seems unavoidable without major rewriter infrastructure refactoring.Fixes #2235.
By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.