Skip to content

Commit

Permalink
Avoid NRE when Contract.Result uses ForAll with capturing lambda
Browse files Browse the repository at this point in the history
Root cause of the NRE is "local variable name erasure" that happens
during closure cloning.
Fix preserves closure names for async contracts.
Related work items: microsoft#72, microsoft#69
  • Loading branch information
SergeyTeplyakov committed Jun 24, 2015
1 parent 97bd0df commit 55c9f2f
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 6 deletions.
15 changes: 12 additions & 3 deletions Foxtrot/Foxtrot/Rewriter.cs
Expand Up @@ -3537,12 +3537,12 @@ public EmitAsyncClosure(Method from, Rewriter parent)
this.forwarder.VisitTypeParameterList(this.closureClass.TemplateParameters);

taskType = this.forwarder.VisitTypeReference(taskType);

}
else
{
this.closureClassInstance = this.closureClass;
}

var taskTemplate = HelperMethods.Unspecialize(taskType);
var continueWithCandidates = taskTemplate.GetMembersNamed(Identifier.For("ContinueWith"));
Method continueWithMethod = null;
Expand All @@ -3554,13 +3554,16 @@ public EmitAsyncClosure(Method from, Rewriter parent)
{
if (cand.IsGeneric) continue;
if (cand.ParameterCount != 1) continue;

var p = cand.Parameters[0];
var ptype = p.Type;
var ptypeTemplate = ptype;

while (ptypeTemplate.Template != null)
{
ptypeTemplate = ptypeTemplate.Template;
}

if (ptypeTemplate.Name.Name != "Action`1") continue;

continueWithMethod = cand;
Expand All @@ -3587,6 +3590,7 @@ public EmitAsyncClosure(Method from, Rewriter parent)
break;
}
}

if (continueWithMethod != null)
{
this.continuewithMethod = continueWithMethod;
Expand All @@ -3610,16 +3614,19 @@ public EmitAsyncClosure(Method from, Rewriter parent)
var consArgs = new TypeNodeList();
var args = new TypeNodeList();
var parentCount = this.closureClass.DeclaringType.ConsolidatedTemplateParameters == null ? 0 : this.closureClass.DeclaringType.ConsolidatedTemplateParameters.Count;

for (int i = 0; i < parentCount; i++)
{
consArgs.Add(this.closureClass.DeclaringType.ConsolidatedTemplateParameters[i]);
}

var methodCount = from.TemplateParameters == null ? 0: from.TemplateParameters.Count;
for (int i = 0; i < methodCount; i++)
{
consArgs.Add(from.TemplateParameters[i]);
args.Add(from.TemplateParameters[i]);
}

this.closureClassInstance = (Class)this.closureClass.GetConsolidatedTemplateInstance(this.parent.assemblyBeingRewritten, closureClass.DeclaringType, closureClass.DeclaringType, args, consArgs);
}

Expand All @@ -3628,7 +3635,6 @@ public EmitAsyncClosure(Method from, Rewriter parent)
this.ClosureInitializer = new Block(new StatementList());

this.ClosureInitializer.Statements.Add(new AssignmentStatement(this.ClosureLocal, new Construct(new MemberBinding(null, this.Ctor), new ExpressionList())));

}

private void EmitCheckMethod(TypeNode taskType, bool hasResult)
Expand Down Expand Up @@ -3991,7 +3997,10 @@ public override Expression VisitLocal(Local local)
MemberBinding mb;
if (!closureLocals.TryGetValue(local, out mb))
{
var closureField = new Field(this.closureClass, null, FieldFlags.Public, local.Name, this.forwarder.VisitTypeReference(local.Type), null);
// Forwarder would be null, if enclosing method with async closure is not generic
var localType = forwarder != null ? forwarder.VisitTypeReference(local.Type) : local.Type;

var closureField = new Field(this.closureClass, null, FieldFlags.Public, local.Name, localType, null);
this.closureClass.Members.Add(closureField);
mb = new MemberBinding(this.checkMethod.ThisParameter, closureField);
closureLocals.Add(local, mb);
Expand Down
22 changes: 20 additions & 2 deletions Foxtrot/Foxtrot/Utility.cs
Expand Up @@ -2392,6 +2392,8 @@ public DuplicatorForContractsAndClosures(Module module, Method sourceMethod, Met
this.sourceMethod = sourceMethod;
this.targetMethod = targetMethod;

this.RemoveNameForLocals = true;

Duplicator dup = this;

if (mapParameters) {
Expand Down Expand Up @@ -2428,9 +2430,13 @@ public DuplicatorForContractsAndClosures(Module module, Method sourceMethod, Met
//dup.DuplicateFor[contractType.UniqueKey] = originalType;
}
#endregion

}

/// <summary>
/// If true, all names for duplicated local variables would be removed.
/// </summary>
public bool RemoveNameForLocals { get; set; }

public TypeNode PossiblyRemapContractClassToInterface(TypeNode candidate)
{
Contract.Requires(candidate != null);
Expand All @@ -2455,7 +2461,7 @@ public override Expression VisitLocal(Local local)
{
var result = base.VisitLocal(local);
var asLoc = result as Local;
if (asLoc != null)
if (asLoc != null && RemoveNameForLocals)
{
asLoc.Name = null; // Don't clash with original local name
}
Expand Down Expand Up @@ -2727,6 +2733,18 @@ static void DeleteClosureInitialization(Method containing, MethodContract mc, Lo
Module targetModule = targetMethod.DeclaringType.DeclaringModule;

var dup = new DuplicatorForContractsAndClosures(targetModule, sourceMethod, targetMethod, contractNodes);

if (sourceMethod.Contract.AsyncEnsuresCount != 0)
{
// Removing the name lead to NRE for async method that uses Contract.Ensures(Contract.ForAll)
// with a capturing lambda.
// To preserve as much of backward compatible behavior as possible the fix will affect
// only method contracts with async closures. It still possible that this fix will break existing code
// and it is possible that there is no any issues by preserving name of the locals.
// This fix could be enhanced in the future when this issue would be clear.
dup.RemoveNameForLocals = false;
}

return DuplicateContractAndClosureParts(dup, targetMethod, sourceMethod, contractNodes, copyValidations);
}
internal static MethodContract DuplicateContractAndClosureParts(
Expand Down
95 changes: 95 additions & 0 deletions Foxtrot/Tests/Sources/Async_EnsuresWithCapturedForAll.cs
@@ -0,0 +1,95 @@
// 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.Text;
using System.Diagnostics.Contracts;

namespace Tests.Sources
{
#if NETFRAMEWORK_4_5
using System.Threading.Tasks;

class NonGen {
public async Task<List<int>> FooAsync(int limit)
{
Contract.Ensures(Contract.ForAll(Contract.Result<List<int>>(), i => i < limit));

await Task.Delay(42);

return new List<int>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
}
}
partial class TestMain
{
void TestMe(NonGen g, int limit, bool mustThrow) {
try {
var z = g.FooAsync(limit);

foreach (var i in z.Result) {
Console.WriteLine("Result = {0}", i);
}

if (mustThrow) {
throw new Exception("failed to throw");
}
}
catch (AggregateException ae) {
if (!mustThrow) {
throw;
}
}
}

void Test()
{
var g = new NonGen();
TestMe(g, 20, false);
TestMe(g, 5, true);

if (!behave) {
TestMe(g, 5, false);
}
}

partial void Run()
{
this.Test();
}

public ContractFailureKind NegativeExpectedKind = ContractFailureKind.Postcondition;
public string NegativeExpectedCondition = "Contract.ForAll(Contract.Result<List<int>>(), i => i < limit)";
}
#else
// Dummy for Pre 4.5
partial class TestMain
{

void Test() {
Contract.Requires(false);
}

partial void Run()
{
if (!behave) {
this.Test();
}
}

public ContractFailureKind NegativeExpectedKind = ContractFailureKind.Precondition;
public string NegativeExpectedCondition = "false";
}
#endif
}
2 changes: 1 addition & 1 deletion Foxtrot/Tests/TestInputs.xml
Expand Up @@ -370,7 +370,7 @@
Compiler="CS"
/>
<TestFile
Name="Foxtrot\Tests\Sources\Async12.cs"
Name="Foxtrot\Tests\Sources\Async_EnsuresWithCapturedForAll.cs"
CompilerOptions=""
FoxtrotOptions=""
References="AsyncCtpLibrary.dll"
Expand Down

0 comments on commit 55c9f2f

Please sign in to comment.