Closed atomb closed 2 years ago
Here's a proposal for the next step we might take.
/testContracts:<modes>
to enable testing contracts, where <modes>
is a comma-separated list containing any of the following possible values:
tests
- check contracts on functions and methods when calling them directly from {:test}
methodsexterns
- check contracts on code marked with {:extern}
, whether being used to import or export, regardless of where it’s being called fromexpect
It’s possible that the contract on a method could be difficult to prove due to SMT solver limitations, such as the following.
method HardCastMethod(i: nat) returns (res: nat)
requires i < 64
ensures res == (64 - i as bv64) as int
{
return (64 - i as bv8) as int;
}
Or, alternatively, it could be difficult to prove because it’s written in a different language, such as the following.
method {:extern} GenerateBytes(i: int32) returns (res: seq<uint8>)
requires i >= 0
ensures |res| == i as int
In either case, we will use the same approach to test the contract contract before and after calling the underlying method, such as the following.
method HardCastMethod_checked(i: nat) returns (res: nat)
requires i < 64
ensures res == (64 - i as bv64) as int
{
expect i < 64;
res := HardCastMethod(i);
expect res == (64 - i as bv64) as int;
}
method {:extern} GenerateBytes_checked(i: int32) returns (res: seq<uint8>)
requires i >= 0
ensures |res| == i as int
{
expect i >= 0;
res := GenerateBytes(i);
expect |res| == i as int;
}
Functions would behave similarly, but with one additional factor. Consider this functional equivalent of the above method.
function HardCastFunction(i: nat): (res: nat)
requires i < 64
ensures res == (64 - i as bv64) as int
{
(64 - i as bv8) as int
}
It could have a similar checked wrapper using by method
.
function HardCastFunction_checked(i: nat): (res: nat)
requires i < 64
ensures res == (64 - i as bv64) as int
{
HardCastFunction(i)
} by method {
expect i < 64;
res := HardCastFunction(i);
expect res == (64 - i as bv64) as int;
}
Otherwise, functions would behave identically to methods. Supporting this approach suggests continuing to allow expect
in function by method
definitions (see #1376).
expect
statement. Future improvements may include extending the range of specifications that can be compiled.expect
as the current {:test}
code doesBecause contract-checking wrappers will exist as separate but directly-callable code in the target language, anyone who wants to use them from property-based tests, fuzzing harnesses, or bounded-model-checking harnesses can do so, and that allows us to experiment with the utility of those connections before automating them.
After this first iteration, there are a number of improvements that may be valuable. These could include:
Loving this direction. My only ask is that you're very explicit in the documentation that /testContracts:<modes>
is experimental and very likely to change. I'm very confident that checking specifications at runtime is valuable, but I'm much less confident the two modes you suggest specifically will be the most useful and/or scale to real code.
My only other ask is that you rename HardCastMeth
to HardCastMethod
, because the former keeps making me think of Breaking Bad. :D
Yep, that sounds great. I'm not fully confident that these are the right modes, either, but they at least give us something to start experimenting with.
And I edited it to fully spell out Method
and Function
everywhere. 🤣
Could you say something about what error messages are generated if you use non-compilable expression in your ensures clauses combined with /testContracts
?
What's the use-case of /testContracts:tests
? Would you always use this in combination with /noVerify
, so you don't have to prove the ensures clauses of the ensures that you want to check at runtime?
I think a message something along the lines of /testContracts can only be used with compilable contracts, and the ensures clause of method m at foo.dfy(1,3) called from the test at bar.dfy(5,3) is not compilable
is what I have in mind.
I have two use cases in mind for /testContracts:tests
:
{:extern}
that will never be verified but can be tested.{:verify false}
, though you could use /noVerify
more broadly. It might also be valuable to allow {:verify false}
on individual contract clauses, though I'd be inclined to hold off on that until we've identified specific cases where we know we need it.
The Dafny reference manual, in the documentation for the
expect
statement, suggests that it can be used as a way to dynamically check the contract of a method.However, doing this manually in Dafny as-is introduces the possibility that the
expect
statement and theensures
clause could get out of sync. Instead, we could have a plugin that would automatically generate a wrapper to check theensures
clauses (and possibly evenrequires
clauses) of a method usingexpect
, guaranteeing that the dynamic checks would always match the stated contracts.Further comments on this issue will elaborate a proposed design.