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 plugin to create wrapper functions/methods to check contracts at runtime #2712

Merged
merged 38 commits into from
Sep 27, 2022

Conversation

atomb
Copy link
Member

@atomb atomb commented Sep 8, 2022

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:

/testContracts:<AllExterns|ExternsInTests>
    Enable run-time testing of compiled function or method contracts in
    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.
    Externs - Check contracts on every call to a function or method
        marked :extern when it occurs in one marked :test.

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.

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.
@atomb
Copy link
Member Author

atomb commented Sep 9, 2022

The following Dafny and C# files, together, demonstrate the basic operation of this for checking calls to externs from tests.

The Dafny code (check-extern.dfy):

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 (FooBar.cs):

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:

Dafny program verifier finished with 5 verified, 0 errors
Wrote textual form of target program to check-extern.cs
Running...

FooTest: FAILED
	check-extern.dfy(3,12): Runtime failure of ensures clause from check-extern.dfy:3:13
BarTest: FAILED
	check-extern.dfy(20,12): Runtime failure of ensures clause from check-extern.dfy:20:13
Test failures occurred: see above.

@atomb atomb self-assigned this Sep 20, 2022
@atomb
Copy link
Member Author

atomb commented Sep 22, 2022

I think this PR is largely complete. The one key open question in my mind is whether the current set of /testContracts parameters is the right one. It could be that ExternsInTests is too specific to be very useful. (Compiling only your test code with /AllExterns could be the more common use case.) And it could be than an All option, to always test all (compilable) contracts would be valuable. I'm pretty convinced that /AllExterns is a useful mode, however, and perhaps the only one we need to support in this PR.

@atomb atomb marked this pull request as ready for review September 23, 2022 15:14
@MikaelMayer
Copy link
Member

I'm ready to review it ! Let me have a look.

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.

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);
Copy link
Member

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 ^^

@@ -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.
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
certain situations, currently focused on :extern code.
certain situations, currently focused on {:extern} code.

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.
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
method marked :extern, regardless of where it occurs.
method with the {:extern} attribute, regardless of where it occurs.

Comment on lines 1454 to 1455
method marked :extern when it occurs in one marked :test, and
warn if no corresponding :test exists for a given :extern.
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
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.

@@ -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>
Copy link
Member

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?

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 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);
Copy link
Member

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.

Copy link
Member Author

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) {
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 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);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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;
Copy link
Member

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> {
Copy link
Member

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?

@@ -99,7 +99,7 @@ public enum PrintModes {
public enum ContractTestingMode {
None,
AllExterns,
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 rename this so that it matches the option?

@@ -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
Copy link
Member

@MikaelMayer MikaelMayer Sep 26, 2022

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.

Copy link
Member Author

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.

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.

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?

@atomb
Copy link
Member Author

atomb commented Sep 27, 2022

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.

@atomb atomb merged commit b2f3249 into dafny-lang:master Sep 27, 2022
@@ -1668,3 +1668,27 @@ periods.
compiled assembly rather than including `DafnyRuntime.cs` in the build
process.


* `-testContracts:<mode>` - test certain function and method contracts
Copy link
Collaborator

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?

Copy link
Member Author

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?

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.

feat: Add resolver plugin that automatically creates test wrappers for contracts
3 participants