Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #114 from SergeyTeplyakov/bugs/Roslyn2Awaits
Browse files Browse the repository at this point in the history
Add support for new IL patterns that generates Roslyn compiler for async methods with 2 awaits
  • Loading branch information
SergeyTeplyakov committed Jul 11, 2015
2 parents 61e5709 + 50cd2c9 commit a7e7ab5
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 14 deletions.
69 changes: 66 additions & 3 deletions Foxtrot/Foxtrot/Extraction/ExtractorVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2256,20 +2256,32 @@ private static int ContractStartInMoveNext(ContractNodes contractNodes, Method m
goto OuterLoop;
}

// Roslyn-based compiler introduced new pattern for async methods with 2 await statements.
// Current implementation sets seenFinalCompare to true when `if (num != 0)` code was found in the MoveNext method.
// This usually meant that async preamble is finished and next statement could be a contract statement
// (or legacy contract statement, doesn't matter).
// But VS2015 compiler changes the behavior and right after `if (num != 0)` there another check that should be skipped.
// Please see additional comments at IsRoslynStateCheckForSecondFinishedAwaiter
if (seenFinalCompare && isAsync && IsRoslynStateCheckForSecondFinishedAwaiter(branch.Condition, env, ignoreUnknown: true))
{
// just skipping current statement!
continue;
}

var value = EvaluateExpression(branch.Condition, env, seenFinalCompare, isAsync);
if (value.Two == EvalKind.IsDisposingTest)
{
if (value.One != 0)
{
return FindTargetBlock(blocks, branch.Target, currentBlockIndex);
}

if (i + 1 < block.Statements.Count)
{
statementIndex = i + 1;
return currentBlockIndex;
}

if (currentBlockIndex + 1 < body.Statements.Count)
{
return currentBlockIndex + 1;
Expand All @@ -2292,7 +2304,7 @@ private static int ContractStartInMoveNext(ContractNodes contractNodes, Method m
currentBlockIndex = FindTargetBlock(blocks, branch.Target, currentBlockIndex);
goto OuterLoop;
}

continue;
}

Expand Down Expand Up @@ -2461,6 +2473,57 @@ private static bool IsDoFinallyBodies(Expression expression)
return false;
}

/// <summary>
/// Roslyn-based compiler introduces different pattern for async method with two await statements:
/// Instead of using switch (as it does for 2+ awaits) it generates following code:
///
/// if (num != 0)
/// {
/// if (num == 1) // &lt;-- this if-statement should be processed differently!
/// {
/// taskAwaiter = this.&lt;>u__1;
/// this.&lt;>u__1 = default(TaskAwaiter);
/// this.&lt;>1__state = -1;
/// goto IL_E8;
/// }
/// Contract.Requires(this.str != null);
/// taskAwaiter2 = Task.Delay(42).GetAwaiter();
/// if (!taskAwaiter2.IsCompleted)
/// {
/// this.&lt;>1__state = 0;
/// this.&lt;>u__1 = taskAwaiter2;
/// Foo.&lt;Method2>d__0 &lt;Method2>d__ = this;
/// this.&lt;>t__builder.AwaitUnsafeOnCompleted&lt;TaskAwaiter, Foo.&lt;Method2>d__0>(ref taskAwaiter2, ref &lt;Method2>d__);
/// return;
/// }
/// }
/// else
/// {
/// taskAwaiter2 = this.&lt;>u__1;
/// this.&lt;>u__1 = default(TaskAwaiter);
/// this.&lt;>1__state = -1;
/// }
///
/// Unfortunately, `if (num != 0)` was always used to detect that upcoming statement is a contract statement.
/// That's why after migration to VS2015 ccrewrite was failing trying to compile a method with two awaits.
///
/// Current method helps to distinguish this new pattern and returns true for binary expression that checks state variable with 1.
/// </summary>
private static bool IsRoslynStateCheckForSecondFinishedAwaiter(Expression expression, Dictionary<Variable, Pair<int, EvalKind>> env, bool ignoreUnknown)
{
var binary = expression as BinaryExpression;
if (binary != null)
{
var op1 = EvaluateExpression(binary.Operand1, env, ignoreUnknown, ignoreUnknown);
var op2 = EvaluateExpression(binary.Operand2, env, ignoreUnknown, ignoreUnknown);

// Checking is it "num == 1" statement that will present in Roslyn-based async method with exactly 2 await expressions.
return binary.NodeType == NodeType.Eq && op1.Two == EvalKind.IsStateValue && op2.One == 1;
}

return false;
}

[ContractVerification(true)]
private static Pair<int, EvalKind> EvaluateExpression(Expression expression,
Dictionary<Variable, Pair<int, EvalKind>> env, bool ignoreUnknown, bool isAsync)
Expand Down
13 changes: 13 additions & 0 deletions Foxtrot/Tests/RewriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,19 @@ public void BuildRewriteRunFromSourcesV45()
TestDriver.BuildRewriteRun(options);
}

[DeploymentItem("Foxtrot\\Tests\\TestInputs.xml"), DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\TestInputs.xml", "RoslynCompatibility", DataAccessMethod.Sequential)]
[TestMethod]
[TestCategory("Runtime"), TestCategory("CoreTest"), TestCategory("V4.5")]
public void BuildRewriteRunRoslynTestCasesWithV45()
{
var options = new Options(this.TestContext);
options.FoxtrotOptions = options.FoxtrotOptions + String.Format(" /throwonfailure /rw:{0}.exe,TestInfrastructure.RewriterMethods", Path.GetFileNameWithoutExtension(options.TestName));
options.BuildFramework = @".NETFramework\v4.5";
options.ContractFramework = @".NETFramework\v4.0";
options.UseTestHarness = true;
TestDriver.BuildRewriteRun(options);
}

[DeploymentItem("Foxtrot\\Tests\\TestInputs.xml"), DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML", "|DataDirectory|\\TestInputs.xml", "TestFile", DataAccessMethod.Sequential)]
[TestMethod]
[TestCategory("Runtime"), TestCategory("CoreTest"), TestCategory("Roslyn"), TestCategory("V4.5")]
Expand Down
10 changes: 0 additions & 10 deletions Foxtrot/Tests/RoslynCompatibility/AbbrevGenClosure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,6 @@ public static void MustReturnCollectionWithoutNullItem<T>()
Contract.Ensures(Contract.ForAll(Contract.Result<IEnumerable<T>>(), item => item != null));
}

public string[] GetSubscribers2(bool behave)
{
MustReturnCollectionWithoutNullItem<string>();

if (behave) {
return new string[]{"a","B","c"};
}
return null;
}

}

partial class TestMain
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// CodeContracts
//
// Copyright (c) Microsoft Corporation
//
// All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using System.Threading.Tasks;

namespace Tests.Sources
{
public class Foo
{
public async Task Method1(string str)
{
Contract.Requires(str != null);
await Task.Delay(42);
}

public async Task Method2(string str)
{
// This code lead to failure previously!
Contract.Requires(str != null);

await Task.Delay(42);
await Task.Delay(43);
}

public async Task Method5(string str)
{
Contract.Requires(str != null);
await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
}
}

partial class TestMain
{
partial void Run()
{
if (behave)
{
new Foo().Method2("1");
}
else
{
new Foo().Method2(null);
}
}

public ContractFailureKind NegativeExpectedKind = ContractFailureKind.Precondition;
public string NegativeExpectedCondition = "str != null";
}
}
74 changes: 74 additions & 0 deletions Foxtrot/Tests/RoslynCompatibility/AsyncWithLegacyRequires.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// CodeContracts
//
// Copyright (c) Microsoft Corporation
//
// All rights reserved.
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using System.Threading.Tasks;

namespace Tests.Sources
{
public class Foo
{
public async Task Method1(string str)
{
if (str == null) throw new ArgumentNullException("str");
Contract.EndContractBlock();

await Task.Delay(42);
}

public async Task Method2(string str)
{
// This code lead to failure previously!
if (str == null) throw new ArgumentNullException("str");
Contract.EndContractBlock();

await Task.Delay(42);
await Task.Delay(43);
}

public async Task Method5(string str)
{
if (str == null) throw new ArgumentNullException("str");
Contract.EndContractBlock();

await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
await Task.Delay(42);
}

}

partial class TestMain
{
partial void Run()
{
if (behave)
{
new Foo().Method2("1");
}
else
{
new Foo().Method2(null);
}
}

public ContractFailureKind NegativeExpectedKind = ContractFailureKind.Precondition;
public string NegativeExpectedCondition = "Value cannot be null.\r\nParameter name: str";
}
}
20 changes: 20 additions & 0 deletions Foxtrot/Tests/Sources/ComplexGeneric.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@

namespace Tests.Sources
{
// This bug was relevant only for .NET 4.0+
#if NETFRAMEWORK_3_5

partial class TestMain
{
partial void Run()
{
if (!this.behave)
{
throw new System.ArgumentNullException();
}
}

public ContractFailureKind NegativeExpectedKind = ContractFailureKind.Precondition;
public string NegativeExpectedCondition = "Value cannot be null.";
}

#else

// CCRewriter was unable to read this code from the IL due to an issue in CCI.
// This test just makes sure that the fix is in place.
Expand Down Expand Up @@ -65,4 +83,6 @@ partial void Run()
public string NegativeExpectedCondition = "Value cannot be null.";
}

#endif

}
2 changes: 1 addition & 1 deletion Foxtrot/Tests/Sources/TestHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class Assert
public static void AreEqual(object expected, object actual)
{
if (object.Equals(expected, actual)) return;
var result = String.Format("Expected: {0}, Actual: {1}", expected, actual);
var result = String.Format("Expected: '{0}', Actual: '{1}'", expected, actual);
Console.WriteLine(result);
throw new Exception(result);
}
Expand Down
19 changes: 19 additions & 0 deletions Foxtrot/Tests/TestInputs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@
/>

<!-- Tests for Roslyn-based compiler compatibility -->
<RoslynCompatibility
Name="Foxtrot\Tests\RoslynCompatibility\AsyncWithLegacyRequires.cs"
CompilerOptions=""
References=""
FoxtrotOptions=""
BinDir="false"
Compiler="CS"
/>

<RoslynCompatibility
Name="Foxtrot\Tests\RoslynCompatibility\AsyncRequiresWithMultipleAwaits.cs"
CompilerOptions=""
References=""
FoxtrotOptions=""
BinDir="false"
Compiler="CS"
/>

<RoslynCompatibility
Name="Foxtrot\Tests\Sources\InheritOOBClosure.cs"
CompilerOptions=""
Expand Down Expand Up @@ -271,6 +289,7 @@
BinDir="false"
Compiler="CS"
/>

<TestFile
Name="Foxtrot\Tests\Sources\Async1.cs"
CompilerOptions=""
Expand Down

0 comments on commit a7e7ab5

Please sign in to comment.