-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Proposal: Method Contracts #119
Comments
Any possibility of getting invariants as well? Would love to see the static analysis tools we have in Code Contracts working with these new contracts. I really like the presented syntax to decide between exceptions and fail-fast for preconditions. |
Could some kind of flag be provided to omit private/internal contracts from release builds if we have static analysis tools to prove them? |
In previous research we have shied way from invariants in part because it's actually incredibly difficult for developers to predict the effect it will have on the code. Typically invariants checks are validated to be true on every exit / entry point of a given method. Given that can you point out all of the places on the following method where that happens? class C
{
invarint Count >= 0
int[] _array;
int _count;
void AddRange(IEnumerable<int> all)
{
EnsureCapacity();
foreach (var cur in all) {
_array[i] = cur;
}
}
} Here are just a few of the places where an invariant check must occur in the above code:
Sure a few of these may be able to be optimized away. But even then on this really simple method the invariant check is occurring at a very high frequency. In the past when we looked deeply at places where developers wanted invariants we found that |
That is fair considering the complexity The real reason I want invariants is just to reduce the amount of redundant contracts I would need to write. As a compromise would it be terrible to have a way to reuse some pre/post conditions? Something like |
@aarondandy Are you referring to reusing a pre-condition as a post-condition, or referring to reusing a precondition across several methods in a class? For the first situation it'd be nice if |
"reusing a precondition across several methods" is what I meant but I like your other thoughts too. I don't really want to have a true provable invariant for |
I wonder if |
Good to see that this is being considered!
How are you going to verify this? For instance, are we allowed to do property access in contracts: Just like in API-based Code Contracts, there should be some way to refer to the old (pre-invocation) value of the field/property/parameter. |
This may or may not be enforced. It might just be a "you shouldn't do that" kind of thing rather than a compile time exception (perhaps static analyzers could look for it). If it is a compile time thing, then regular property access would have to be disallowed in your case to instead do something like |
To what extent can the compiler check a type's contracts for internal consistency? For example, it'd be hard to reliably solve for arbitrary expressions:
There is no value of Would these constraints primarily be evaluated at run-time, with best-effort use from tooling? Also, if arbitrary code can be executed in |
@drewnoakes I'm assuming that an extension will be created that checks all sorts of crazy situations that you'd never want to add to a compiler (for complexity reasons). The compiler can do very minimum checks, and leave most of the checks to extensions. In #98 it was mentioned that there may want to be some shorthand syntax for common checks (null being the most common). Something like An alternative is to use |
what about situations where a compile-time check is nice, but a runtime check may impart more of a performance penalty? For example, there's this comment in
...which states that, while you want compile-time checks for existence of the array, and Maybe the validator could biggyback off of hardware/vm protections? So it could see something like |
We've did pretty extensive profiling of contracts in our project and found that in general the performance hit they caused was minimal. It was usually in the 1-2% range across the system. Sure there was the occasional check that was too expensive to make a contract. In those situations we first looked to the API itself. If verifying the invariants of the type was too expensive to put in a contract then perhaps the API itself needs to be changed. If the API couldn't be made more efficient then we would move the check into Note: contracts do not always result in a negative performance hit. It is possible that they establish invariants (like bounds) that the JIT can optimize against and result in faster code. There were definitely places where that happened. |
Would it be possible to have a method contract in this position? T Item(int index)
{
requires (0 <= index) && (index < Size)
get{ return _a[index]; }
set(T item) { _a[index]=item; }
} Using a trait ( #129) T Item(int index)
{
trait { _a!; range: 0 <= index < _a,Length; }
get { _a[index]; }
set(T item) { _a[index] = item; }
} |
The part about Virtual methods and interface methods made me starting thinking about how this might be seen as an extension to the type system in a way...or rather another kind of type, similar to units of measure in F#. I'm bored, so I'm going to riff on the idea... If we start with the first example in the solution above:
The 'requires' are additional constraints on the methods parameters, similar to how you have a types associated with each parameter. For fun I'm using 'where' below instead of adding new keywords. I think it's worth considering making it look LINQ'ish:
Which then makes me think of the notion of generic type constraints. Similar to how a delegate gives a name to the shape of a function, a typeconstraint is used to give a name to a constraint.
Perhaps you could then use typeconstraints anywhere you would normally use a type. The generic parameters could be bound to other types, constants, members or other arguments. Now the above can be rewritten:
...which is looking pretty clean IMO. And maybe I'm going off the deep end here, but lets consider going a step further by thinking of the traditional type syntax as being syntactic sugar for a specific where clause...
In other words, 'traditional type syntax'...
...is shorthand for...
...or for parameters and returns...
Since non-nullable types are being considered at C#7 design meetings, lets see how that fits in with this:
...and union types (a bit like TypeScript has)
And of course, similar to delegates, an assembly should be able to expose public typeconstraints like any other kind of type. |
I mentioned it in the original discussion, but what about a short hand syntax for the most common scenarios:
expands to:
expands to:
expands to:
expands to:
(this specific case looks messy, thus probably ok to only support short hand for simple cases, like operators and constant literals (no method calls, no refering to other arguments or fields/properties etc, for that you would use the "full" syntax).
expands to:
I think of contracts as a harder "type", thus I feel it might make sense to have the requirements right there with the parameter (it also avoids double typing). The downside is it might look a bit messy, especially if there are lots of parameters (but that's code smell anyways), and I guess it's (much?) harder to parse for the compiler. |
@chrisaut I think the ( #129 ) would be a better approach.
As it doesn't mess with the parameters syntax, yet still available for the IDE to use the user and collapse the trait definition. |
why not?
x needs to be smaller than y, and y needs to be bigger than x (which comes from the first constraint, you wouldn't even need the second constraint). |
May have misread it as |
I don't see why this should be method only. Why not be able to use with variables also. In addition some distinction on runtime, compile time if unable at compile time at runtime and enforceability. |
@jaredpar consider binding this feature to non-nullable reference types. |
Could you explain why run-time contract violation shouldn't throw an exception? I've shied away from using CodeContracts static analysis (build are too slow; too much work to fix all warnings), but find them great for run-time validation and documentation. It's also great to be able to dial up and down the amount of run-time checks for debug and release builds (full checks vs ReleaseRequires). In the case of release builds, you DEFINITELY don't want fail-fast behavior; and arguably you'd want consistent behavior but more checks in debug builds. My biggest problems with CodeContracts have been:
I would expect that this proposal would address 1 and 2 at a minimum, so I'm a huge fan. Another important trait to preserve is the ability to intercept contract failures and customize the response: log them, pop up a dialog, etc. |
@kruncher We may not want extra checks we know are true in performance-sensitive code. For example, it may prevent the JIT compiler from inlining a particular method. For example if you go to corefx where the BCL lives, you'll see that basically all of the checks of the internal/private methods are |
Hmmm, would it be useful to have a build option to simply disable all contracts for internal/private methods (i.e. an option that can be configured on/off for 'Debug', 'Release' or 'Custom')? |
@kruncher Hmm... might be a good idea, actually. A problem though is that you might want to enable the contracts for only certain internal/private methods. |
In the Code Contracts implementation there is a explicit option for only checking 'public surface contracts' which exhibits this behaviour. The clear win here is that if a contract is asserted on private methods the Code Contracts implementation will require another assertion or assumption it on any callee meaning that if all callees have already validated the input you don't take the perf hit on checking again. I'd say this behaviour is the same (or a slight subset of) the statement: "If a compiler could prove the contract was satisfied at compile-time – something ... it was free to elide the check altogether" from earlier in this thread. |
Would not always work. For example, consider this code sample: T Single<T>(IList<T> list)
{
if (list == null || list.Count != 1) throw something;
return SingleInternal(list);
}
T SingleInternal<T>(IList<T> list)
{
Debug.Assert(list != null && list.Count == 1);
return list[0];
} A bad implementation of |
I see what you are saying but I think it is a separate point. I think the point that you could have untrustworthy implementations (note that the MS Code Contracts has contracts for interfaces like IList so you could solve this) is different from needlessly reapplying the same checks all the way down a call stack. It'd be interesting to see what the Code Contracts does around non-predictable return types such as your example and arbitrary yield returns. I've often found it's analyser was quite good at unravelling such problems. |
For that example Code Contracts seems to take the "if it hurts don't do that strategy" and assumes you are not trying to intentionally harm the stability of your system. It does this by requiring methods used in a contract to be |
This is now tracked at dotnet/csharplang#105 |
as I explained in dotnet/csharplang#1198, this syntax will be shorter:
|
@MohammadHamdyGhanem It's also not particular generic. What if you wanted to convey a constraint that doesn't involve ranges? |
Or even within ranges, disjointed ranges? e.g. value must be between 0 and 100, or between 200 and 300. |
What you ask about is a complex condition already, and regular If statements can do it better. |
@MohammadHamdyGhanem If statements only give you runtime checks, not compile-time safety. |
@yaakov-h |
Also: Method Contracts are not run time safe, and will raise exceptions. This is why we use try catch. |
So, there are 4 possible ways to do it:
Sure we all want some syntax to simplify validation codes. |
Maybe invariants could be qualified like Thus, a private methold could violate a This allows to refactor common code into private methods while still upholding the invariants to the outside. |
(Note: this proposal was briefly discussed in #98, the C# design notes for Jan 21, 2015. It has not been updated based on the discussion that's already occurred on that thread.)
Background
"Code Contracts" were introduced in .NET 4 as a feature of the Framework, primarily exposed through the System.Diagnostics.Contracts.Contract class. Through method calls like Contract.Requires and Contract.Ensures, developers could add code to their methods to specify preconditions and postconditions, and then a subsequent tool known as a rewriter could be used to post-process the code generated by the compiler to appropriately implement the expressed contracts, e.g. whereas the developer puts a call to Ensures at the beginning of the method, the rewriter ensures that path through the method that could cause it to exit ends with a validation of the expressed postcondition.
Problem
Very little code actually uses these API-based code contracts. They require additional tooling to be integrated into the build, they're verbose, and it's difficult to write additional robust tooling related to them (e.g. integrating contract information into documentation).
Solution: Language support for method contracts
Basic contract support for preconditions and postconditions can be built directly into the language, without requiring any separate tooling or custom build processes, and more thoroughly integrated throughout the language.
The compiler would generate all of the relevant code to correctly validate the contracts, and the contracts would show up as part of the method signature in the Visual Studio IDE, such as in IntelliSense at call sites to the method.
These contracts would primarily be validated at run time, however if the compiler (or a 3rd-party diagnostic) could statically detect a violated contract (e.g. a precondition requires that an argument be non-null, and null is being passed as the argument), it could choose to fail the compilation rather than allowing the bug to make its way to run time. Similarly, if the compiler (frontend or backend) is able to statically determine that a contract will always be satisfied, it can optimize based on that knowledge.
Unlike .NET Code Contracts, which have configurable but complicated behavior provided by a post-compilation rewriter tool (and which are no-ops if such a rewriter isn’t used), failed validation of ‘requires’ and ‘ensures’ clauses would ideally result in "fail fast" (ala Environment.FailFast(...)), meaning the program abandons immediately. This is very useful for validating preconditions and postconditions, which are typically used to catch usage errors by the developer. Such errors should never make it into production.
The compiler would require that preconditions and postconditions can be validated by the caller, and as such it requires that any members used in the requires and ensures clauses are all accessible to any caller, e.g. the requires clause of a public method may not access internal or private members, and the requires clause of an internal method may not access private members.
Preconditions and postconditions should also not throw exceptions.
Virtual methods and interface methods may have preconditions and postconditions, in which case the compiler guarantees that overrides and implementations of these methods satisfy the preconditions. To make this clear to a developer reading the code, the override or interface implementation would state "requires base" or "ensures base", to indicate that there are imposed constraints, while not forcing the developer writing the code to explicitly type them out.
Alternatives: Fail fast vs exceptions
In support of migrating existing code to use contracts, or for places where an exception is really desired over fail fast, we could optionally consider additional syntax to specify that an exception should be thrown instead of fail fasting.
For example, the previous Insert example's requires and ensures clauses would result in fail fasting if a condition isn't met:
But, the developer could explicitly override the fail fast by specifying what should happen in case of failure:
The text was updated successfully, but these errors were encountered: