-
Notifications
You must be signed in to change notification settings - Fork 151
ccrewrite removes non-Contract method call, resulting in NullReferenceException #242
Comments
I have the same problem. In my case, the rewriting works well in VS2013, but has improperly eliminated some non-contract code in VS2015. I can provide more details if needed, but at the moment I believe the original post provides a perfect repro. If I can ask, this should be handled as one of the highest priorities. |
@yaakov-h This is a very interesting bug. Consider following code: Contract.Assume(SomeMethod()); What ccrewrite should do in this case? Currently, ccrewrite will remove Now, switching back to the original example: var result = SomeMethod();
Contract.Assume(result); In Debug build there would be a local variable and ccrewrite can understand this and leave So from ccrewrite perspective there is no way to distinguish those cases at all... So current proposal sounds like this to me: Let ccrewrite leave all the expressions evaluated as a first argument on evaluation stack. I do agree that in some cases this could be very beneficial, but I do believe that it could be not what other people want from the ccrewrite. Consider following code: Contract.Assume(CheckSomeHeavyweightState()); This check would be removed right now and for some users this could be exactly what they need (and this is exactly what is required for few of my projects). It means that proposed change would be a breaking change and not everyone will agree that new behavior is desirable. P.S. I do believe we need to do something with this issue, but I'm not sure that simple approach (just not remove the call to |
BTW, as far as I remeber, ccrewrite has very weird bug, that ccrewrite will never remove complex expressions. Consider following: Contract.Assume(Foo()); In this case, But in this case: Contract.Assume(Foo() || Boo()); In this case call to This definitely a bug that could be very hard to tackle. For instance, you can avoid NRE in you code simply by changing your code to: |
@yaakov-h ok, It seems that this behaviour needs to be changed to proposed one. |
@SergeyTeplyakov I think my case is #242 and not #191. The only difference I can tell was that in my case, it has worked correctly up to VS2013, and appeared in VS2015 Release builds, but I think there might be some explanation for it that we just do not know at the moment. Besides this, everything else is practically identical, and it is VS2015 that concerns me now, and with that we are all seeing the same. I have read the analysis and it makes sense. Unfortunately, there is probably no "good" solution based just on IL, if the source code that should in end behave differently compiles to the same IL. I think that therefore we need to look at this form the practical point of view: If we simply allow the current behavior of the rewriter and claim it as "standard", lots of code that will appear correct will end up being rewritten to incorrect code, and it would be (at least in case of my large projects) very difficult if not impossible to scan all the code for places where this danger appears, and make sure to work around it somehow. If, on the other hand, we go with the proposal "Let ccrewrite leave all the expressions evaluated as a first argument on evaluation stack.", all code will behave correctly, and when writing new code, the developer will not have to always think about whether some of his statements are not going to disappear. The only negative thing that can happen is that an argument to Assume or Assert will get evaluated when it "should not". That may be sub-optimal because a) it has some side effect, or b) is heavy to compute. The side effect thing I think should not be an issue at all, because that's not how contracts should be written, and in the Debug build that same thing would be called anyway, so it makes even less sense to claim that the same side effect would be undesirable in Release build. The heavy-weight computation of a contract condition might be a problem sometimes, but is relatively rare, and as such I think can be avoided with extra conditional directive - I think that this is something the developer can keep an eye on, much easier than trying to figure out whether a statement that is not in any Assert or Assume would be subject to removal. |
@SergeyTeplyakov Maybe just to sum up my argument: I think it is far better to end up with a program that has some extra code that is just evaluating a contract condition (without actually enforcing it), rather than end up with a program that has pieces of code removed, where the actual scope of the removed piece depends on an implementation detail of a compiler, and as such it is generally unpredictable. |
@SergeyTeplyakov thanks for looking into this, I'll try that workaround. @ZbynekZ this issue (#242) occurs in both 2013 and 2015. If your problem only occurs in 2015, this may not be your problem. |
@yaakov-h, @ZbynekZ I think that current behavior needs to be changed to proposed one. The reason for that is simple: VS2015 is doing much more optimization that VS2013. Consider following code: var hashSet = new HashSet<int>();
bool b = hashSet.Add(42);
Contract.Assert(b); In VS2013 even with Release mode local |
@yaakov-h My proposed workaround will not work, because C# compiler is smart enough to entirely remove ccrewrite would preserve complex expressions (I do believe this is a bug, but anyway) but you need more complex non-constant expression. |
@SergeyTeplyakov The code you posted regarding the hashset adds
This makes me think that Roslyn is smart enough to tell the difference between what is and isn't part of the Here's the IL output of Release with optimizations from VS2015 (Update 1 RC):
I assume there's no way to match this behaviour when IL is the input, as is the case with Code Contracts? |
@yaakov-h Sorry, I've copy-pasted wrong code. I meant
|
@SergeyTeplyakov That's what I thought, though Roslyn seems a bit more selective about what it removes and what it keeps. So with Contracts interrogating the IL, is the only way an all-or-nothing approach (keep all expressions inside I don't like the idea of leaving potentially expensive checks in, but I can't see any other way. |
@yaakov-h Unfortunately I don't have any ideas, how to distinguish between those two cases. The only way to think about it: not to remove (or warn) if non-pure method was put into I don't like the idea of leaving checks on the stack, but I can't see other options right now as well... |
@SergeyTeplyakov Good point on using |
@yaakov-h I'm thinking about long-term and short-term fixes for this issue. I really hope to release new version very soon, and maybe for this version I'll just fix this stuff to leave all expressions on the evaluation stack and will create an work item to fix this properly... What do you think? |
@SergeyTeplyakov Sounds like a decent short-term fix, as long as there's still a long-term one in the works. |
Agreed with the recent proposals. If the rewriter manages to remove stuff that it identifies as "pure" - be it for the presence of [Pure} attribute, or other simple cases (foo != null) - then even better so. |
We ran into this issue as well. A couple of options:
Most likely I'll be implementing option 1 until we have something better :) |
Also ran into this issue. |
Somewhat ironically, Code Contracts has introduced a null reference exception into my code by removing a function call.
The repro case is simple: When
ccrewrite
removes calls to Contract methods (e.g. inReleaseRequires
mode), it will also remove any semi-inlined methods.Code:
IL of
Main
:IL of
Main
with compiler optimizations:As you can see, instead of storing the result of
TryDoThing
and re-loading it, it leaves it on the expression stack. This is equivalent toContract.Assume(TryDoThing(out thing), null, "didTheThing")
.IL of
Main
with compiler optimizations,ReleaseRequires
and Public Surface Contracts only:Both the call to
Contract.Assume
and the call toTryDoThing
have been eliminated byccrewrite
. This results in aNullReferenceException
at runtime.This happens in both VS2013 and VS2015. IL above is from 2015.
The text was updated successfully, but these errors were encountered: