diff --git a/Source/Test/Rewriting/AssemblyInfo.cs b/Source/Test/Rewriting/AssemblyInfo.cs
index c7ddf31f4..cd715e3a3 100644
--- a/Source/Test/Rewriting/AssemblyInfo.cs
+++ b/Source/Test/Rewriting/AssemblyInfo.cs
@@ -169,7 +169,7 @@ internal void Invoke(Pass pass)
foreach (var method in type.Methods.ToArray())
{
- if (method.Body is null)
+ if (method.Body is null && !(pass is MemberTypeRewritingPass))
{
continue;
}
diff --git a/Source/Test/Rewriting/Passes/Rewriting/Types/MemberTypeRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/Types/MemberTypeRewritingPass.cs
index b9c0e3781..b020b8b50 100644
--- a/Source/Test/Rewriting/Passes/Rewriting/Types/MemberTypeRewritingPass.cs
+++ b/Source/Test/Rewriting/Passes/Rewriting/Types/MemberTypeRewritingPass.cs
@@ -20,6 +20,56 @@ internal MemberTypeRewritingPass(RewritingOptions options, IEnumerable
+ protected internal override void VisitType(TypeDefinition type)
+ {
+ base.VisitType(type);
+ this.RewriteGenericConstraints(type.GenericParameters);
+ if (type.BaseType != null &&
+ this.TryRewriteType(type.BaseType, out TypeReference newBaseType) &&
+ this.TryResolve(newBaseType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] base type '{0}'", type.BaseType);
+ type.BaseType = newBaseType;
+ this.LogWriter.LogDebug("............. [+] base type '{0}'", type.BaseType);
+ }
+
+ foreach (var implementedInterface in type.Interfaces)
+ {
+ if (this.TryRewriteType(implementedInterface.InterfaceType, out TypeReference newInterfaceType) &&
+ this.TryResolve(newInterfaceType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] interface '{0}'", implementedInterface.InterfaceType);
+ implementedInterface.InterfaceType = newInterfaceType;
+ this.LogWriter.LogDebug("............. [+] interface '{0}'", implementedInterface.InterfaceType);
+ }
+ }
+
+ foreach (var property in type.Properties)
+ {
+ if (this.TryRewriteType(property.PropertyType, out TypeReference newPropertyType) &&
+ this.TryResolve(newPropertyType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] property '{0}'", property);
+ property.PropertyType = newPropertyType;
+ this.LogWriter.LogDebug("............. [+] property '{0}'", property);
+ }
+
+ this.RewriteParameters(property.Parameters);
+ }
+
+ foreach (var eventDefinition in type.Events)
+ {
+ if (this.TryRewriteType(eventDefinition.EventType, out TypeReference newEventType) &&
+ this.TryResolve(newEventType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] event '{0}'", eventDefinition);
+ eventDefinition.EventType = newEventType;
+ this.LogWriter.LogDebug("............. [+] event '{0}'", eventDefinition);
+ }
+ }
+ }
+
///
protected internal override void VisitField(FieldDefinition field)
{
@@ -36,6 +86,27 @@ protected internal override void VisitField(FieldDefinition field)
protected internal override void VisitMethod(MethodDefinition method)
{
// TODO: guard against rewriting methods that have been inherited by non rewritten assemblies.
+ this.RewriteMethodSignature(method);
+ this.RewriteGenericConstraints(method.GenericParameters);
+ foreach (var methodOverride in method.Overrides)
+ {
+ if (this.TryRewriteType(methodOverride.DeclaringType, out TypeReference newDeclaringType) &&
+ this.TryResolve(newDeclaringType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] override '{0}'", methodOverride);
+ methodOverride.DeclaringType = newDeclaringType;
+ this.LogWriter.LogDebug("............. [+] override '{0}'", methodOverride);
+ }
+
+ this.RewriteMethodSignature(methodOverride);
+ }
+ }
+
+ ///
+ /// Rewrites the return and parameter types of a method declaration.
+ ///
+ private void RewriteMethodSignature(MethodReference method)
+ {
// Try to rewrite the return type.
if (this.TryRewriteType(method.ReturnType, out TypeReference newReturnType) &&
this.TryResolve(newReturnType, out TypeDefinition _))
@@ -45,17 +116,41 @@ protected internal override void VisitMethod(MethodDefinition method)
this.LogWriter.LogDebug("............. [+] return type '{0}'", method.ReturnType);
}
- if (method.HasParameters)
+ this.RewriteParameters(method.Parameters);
+ }
+
+ ///
+ /// Rewrites method or indexer parameter types.
+ ///
+ private void RewriteParameters(IEnumerable parameters)
+ {
+ foreach (var parameter in parameters)
+ {
+ if (this.TryRewriteType(parameter.ParameterType, out TypeReference newParameterType) &&
+ this.TryResolve(newParameterType, out TypeDefinition _))
+ {
+ this.LogWriter.LogDebug("............. [-] parameter '{0} {1}'", parameter.ParameterType, parameter.Name);
+ parameter.ParameterType = newParameterType;
+ this.LogWriter.LogDebug("............. [+] parameter '{0} {1}'", parameter.ParameterType, parameter.Name);
+ }
+ }
+ }
+
+ ///
+ /// Rewrites constraints on type or method generic parameters.
+ ///
+ private void RewriteGenericConstraints(IEnumerable parameters)
+ {
+ foreach (var parameter in parameters)
{
- // Try to rewrite the parameter types.
- foreach (var parameter in method.Parameters)
+ foreach (var constraint in parameter.Constraints)
{
- if (this.TryRewriteType(parameter.ParameterType, out TypeReference newParameterType) &&
- this.TryResolve(newParameterType, out TypeDefinition _))
+ if (this.TryRewriteType(constraint.ConstraintType, out TypeReference newConstraintType) &&
+ this.TryResolve(newConstraintType, out TypeDefinition _))
{
- this.LogWriter.LogDebug("............. [-] parameter '{0} {1}'", parameter.ParameterType, parameter.Name);
- parameter.ParameterType = newParameterType;
- this.LogWriter.LogDebug("............. [+] parameter '{0} {1}'", parameter.ParameterType, parameter.Name);
+ this.LogWriter.LogDebug("............. [-] constraint '{0}'", constraint.ConstraintType);
+ constraint.ConstraintType = newConstraintType;
+ this.LogWriter.LogDebug("............. [+] constraint '{0}'", constraint.ConstraintType);
}
}
}
diff --git a/Source/Test/Rewriting/Passes/Rewriting/Types/MethodBodyTypeRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/Types/MethodBodyTypeRewritingPass.cs
index 490199202..0b029b199 100644
--- a/Source/Test/Rewriting/Passes/Rewriting/Types/MethodBodyTypeRewritingPass.cs
+++ b/Source/Test/Rewriting/Passes/Rewriting/Types/MethodBodyTypeRewritingPass.cs
@@ -46,50 +46,54 @@ protected override Instruction VisitInstruction(Instruction instruction)
// Note that the C# compiler is not generating `OpCodes.Calli` instructions:
// https://docs.microsoft.com/en-us/archive/blogs/shawnfa/calli-is-not-verifiable.
- // TODO: what about ldsfld, for static fields?
- if (instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldflda)
+ if (instruction.Operand is FieldReference field)
{
- if (instruction.Operand is FieldDefinition fd &&
- this.TryRewriteType(fd.FieldType, out TypeReference newFieldType) &&
+ if (this.TryRewriteType(field.FieldType, out TypeReference newFieldType) &&
this.TryResolve(newFieldType, out TypeDefinition _))
{
this.LogWriter.LogDebug("............. [-] {0}", instruction);
- fd.FieldType = newFieldType;
+ field.FieldType = newFieldType;
this.IsMethodBodyModified = true;
this.LogWriter.LogDebug("............. [+] {0}", instruction);
}
- else if (instruction.Operand is FieldReference fr &&
- this.TryRewriteType(fr.FieldType, out newFieldType) &&
- this.TryResolve(newFieldType, out TypeDefinition _))
+
+ if (this.TryRewriteType(field.DeclaringType, out TypeReference newDeclaringType) &&
+ this.TryResolve(newDeclaringType, out TypeDefinition _))
{
this.LogWriter.LogDebug("............. [-] {0}", instruction);
- fr.FieldType = newFieldType;
+ field.DeclaringType = newDeclaringType;
this.IsMethodBodyModified = true;
this.LogWriter.LogDebug("............. [+] {0}", instruction);
}
}
- else if (instruction.OpCode == OpCodes.Initobj)
+ else if (instruction.Operand is TypeReference)
+ {
+ instruction = this.VisitTypeInstruction(instruction);
+ }
+ else if (instruction.Operand is MethodReference arrayMethod && arrayMethod.DeclaringType is ArrayType)
{
- instruction = this.VisitInitobjInstruction(instruction);
+ instruction = this.VisitArrayMethodInstruction(instruction, arrayMethod);
}
else if (instruction.OpCode == OpCodes.Newobj)
{
instruction = this.VisitNewobjInstruction(instruction);
}
- else if ((instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt) &&
+ else if ((instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt ||
+ instruction.OpCode == OpCodes.Ldftn || instruction.OpCode == OpCodes.Ldvirtftn ||
+ instruction.OpCode == OpCodes.Ldtoken) &&
instruction.Operand is MethodReference methodReference)
{
- instruction = this.VisitCallInstruction(instruction, methodReference);
+ instruction = this.VisitMethodInstruction(instruction, methodReference);
}
return instruction;
}
///
- /// Rewrites the specified instruction.
+ /// Rewrites a type operand while preserving its instruction opcode.
///
/// The unmodified instruction, or the newly replaced instruction.
- private Instruction VisitInitobjInstruction(Instruction instruction)
+ private Instruction VisitTypeInstruction(Instruction instruction)
{
TypeReference type = instruction.Operand as TypeReference;
if (this.TryRewriteType(type, out TypeReference newType) &&
@@ -108,6 +112,38 @@ private Instruction VisitInitobjInstruction(Instruction instruction)
return instruction;
}
+ ///
+ /// Rewrites synthetic array methods, which have no method definitions to resolve.
+ ///
+ /// The unmodified instruction, or the newly replaced instruction.
+ private Instruction VisitArrayMethodInstruction(Instruction instruction, MethodReference method)
+ {
+ if (this.TryRewriteType(method.DeclaringType, out TypeReference newDeclaringType))
+ {
+ var newMethod = new MethodReference(method.Name,
+ this.RewriteType(method.ReturnType, Options.None), newDeclaringType)
+ {
+ HasThis = method.HasThis,
+ ExplicitThis = method.ExplicitThis,
+ CallingConvention = method.CallingConvention
+ };
+ foreach (var parameter in method.Parameters)
+ {
+ newMethod.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes,
+ this.RewriteType(parameter.ParameterType, Options.None)));
+ }
+
+ var newInstruction = Instruction.Create(instruction.OpCode, this.Module.ImportReference(newMethod));
+ newInstruction.Offset = instruction.Offset;
+ this.LogWriter.LogDebug("............. [-] {0}", instruction);
+ this.Replace(instruction, newInstruction);
+ this.LogWriter.LogDebug("............. [+] {0}", newInstruction);
+ instruction = newInstruction;
+ }
+
+ return instruction;
+ }
+
///
/// Rewrites the specified instruction.
///
@@ -134,17 +170,21 @@ private Instruction VisitNewobjInstruction(Instruction instruction)
}
///
- /// Rewrites the specified non-generic or instruction.
+ /// Rewrites a call, function-pointer or method-token instruction.
///
/// The unmodified instruction, or the newly replaced instruction.
- private Instruction VisitCallInstruction(Instruction instruction, MethodReference method)
+ private Instruction VisitMethodInstruction(Instruction instruction, MethodReference method)
{
- if (this.TryRewriteMethodReference(method, out MethodReference newMethod) &&
- this.TryResolve(newMethod, out MethodDefinition resolvedMethod))
+ bool preserveBinding = instruction.OpCode == OpCodes.Ldftn || instruction.OpCode == OpCodes.Ldvirtftn ||
+ instruction.OpCode == OpCodes.Ldtoken;
+ if (this.TryRewriteMethodReference(method, out MethodReference newMethod, preserveBinding,
+ isMethodToken: instruction.OpCode == OpCodes.Ldtoken) &&
+ this.TryResolve(newMethod, out MethodDefinition resolvedMethod) &&
+ (!preserveBinding || method.HasThis == newMethod.HasThis))
{
// Create and return the new instruction.
- Instruction newInstruction = Instruction.Create(resolvedMethod.IsVirtual ?
- OpCodes.Callvirt : OpCodes.Call, newMethod);
+ Instruction newInstruction = Instruction.Create(preserveBinding || !resolvedMethod.IsStatic ?
+ instruction.OpCode : OpCodes.Call, newMethod);
newInstruction.Offset = instruction.Offset;
this.LogWriter.LogDebug("............. [-] {0}", instruction);
@@ -160,19 +200,66 @@ private Instruction VisitCallInstruction(Instruction instruction, MethodReferenc
///
/// Tries to rewrite the specified .
///
- private bool TryRewriteMethodReference(MethodReference method, out MethodReference result) =>
- this.TryRewriteMethodReference(method, null, out result);
+ private bool TryRewriteMethodReference(MethodReference method, out MethodReference result,
+ bool preserveInstanceBinding = false, bool isMethodToken = false) =>
+ this.TryRewriteMethodReference(method, null, out result, preserveInstanceBinding, isMethodToken);
///
/// Tries to rewrite the specified .
///
- private bool TryRewriteMethodReference(MethodReference method, string matchName, out MethodReference result)
+ private bool TryRewriteMethodReference(MethodReference method, string matchName, out MethodReference result,
+ bool preserveInstanceBinding = false, bool isMethodToken = false)
{
result = method;
TypeDefinition resolvedDeclaringType = method.DeclaringType.Resolve();
if (!this.IsRewritableType(resolvedDeclaringType))
{
- return false;
+ // External concrete signatures stay native, but generic arguments belong to the caller.
+ bool areArgumentsRewritten = false;
+ TypeReference externalDeclaringType = this.RewriteType(method.DeclaringType,
+ Options.None, ref areArgumentsRewritten);
+ MethodReference externalMethod = (method as GenericInstanceMethod)?.ElementMethod ?? method;
+ if (areArgumentsRewritten)
+ {
+ var newExternalMethod = new MethodReference(externalMethod.Name,
+ externalMethod.ReturnType, externalDeclaringType)
+ {
+ HasThis = externalMethod.HasThis,
+ ExplicitThis = externalMethod.ExplicitThis,
+ CallingConvention = externalMethod.CallingConvention
+ };
+ foreach (var parameter in externalMethod.GenericParameters)
+ {
+ newExternalMethod.GenericParameters.Add(new GenericParameter(parameter.Name, newExternalMethod));
+ }
+
+ foreach (var parameter in externalMethod.Parameters)
+ {
+ newExternalMethod.Parameters.Add(new ParameterDefinition(parameter.Name,
+ parameter.Attributes, parameter.ParameterType));
+ }
+
+ externalMethod = newExternalMethod;
+ }
+
+ if (method is GenericInstanceMethod genericMethod)
+ {
+ var newGenericMethod = new GenericInstanceMethod(externalMethod);
+ foreach (var argument in genericMethod.GenericArguments)
+ {
+ newGenericMethod.GenericArguments.Add(
+ this.RewriteType(argument, Options.None, ref areArgumentsRewritten));
+ }
+
+ externalMethod = newGenericMethod;
+ }
+
+ if (areArgumentsRewritten)
+ {
+ result = this.Module.ImportReference(externalMethod);
+ }
+
+ return areArgumentsRewritten;
}
// Variable that is passed by reference to rewriting methods keeping
@@ -200,9 +287,14 @@ private bool TryRewriteMethodReference(MethodReference method, string matchName,
isRewritten = true;
}
- // Try to rewrite the declaring type.
+ // Method handles, bound delegates and base-constructor calls must retain instance binding.
+ // Generic method tokens must also agree with their separately emitted type handles.
+ Options declaringTypeOptions = (preserveInstanceBinding && method.HasThis) ||
+ (isMethodToken && method.DeclaringType.IsGenericInstance) ||
+ (resolvedMethod.IsConstructor && matchName is null) ?
+ Options.None : Options.AllowStaticRewrittenType;
TypeReference newDeclaringType = this.RewriteType(method.DeclaringType,
- Options.AllowStaticRewrittenType, ref isRewritten);
+ declaringTypeOptions, ref isRewritten);
if (!this.TryResolve(newDeclaringType, out TypeDefinition resolvedNewDeclaringType))
{
// Unable to resolve the declaring type of the method.
diff --git a/Source/Test/Rewriting/Passes/Rewriting/Types/TypeRewritingPass.cs b/Source/Test/Rewriting/Passes/Rewriting/Types/TypeRewritingPass.cs
index c169a91ae..1cad7e34b 100644
--- a/Source/Test/Rewriting/Passes/Rewriting/Types/TypeRewritingPass.cs
+++ b/Source/Test/Rewriting/Passes/Rewriting/Types/TypeRewritingPass.cs
@@ -71,6 +71,15 @@ internal TypeRewritingPass(RewritingOptions options, IEnumerable v
#endif
this.KnownTypes[NameCache.GenericTaskCompletionSource] = typeof(Types.Threading.Tasks.TaskCompletionSource<>);
this.KnownTypes[NameCache.TaskExtensions] = typeof(Types.TaskExtensions);
+#if NET
+ this.KnownTypes[NameCache.TaskAsyncEnumerableExtensions] = typeof(Types.Threading.Tasks.TaskAsyncEnumerableExtensions);
+ this.KnownTypes[NameCache.ConfiguredCancelableAsyncEnumerable] =
+ typeof(Types.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable<>);
+ this.KnownTypes[NameCache.ConfiguredAsyncEnumerator] =
+ typeof(Types.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable<>.Enumerator);
+ this.KnownTypes[NameCache.ConfiguredAsyncDisposable] =
+ typeof(Types.Runtime.CompilerServices.ConfiguredAsyncDisposable);
+#endif
this.KnownTypes[NameCache.TaskFactory] = typeof(Types.Threading.Tasks.TaskFactory);
this.KnownTypes[NameCache.GenericTaskFactory] = typeof(Types.Threading.Tasks.TaskFactory<>);
this.KnownTypes[NameCache.TaskParallel] = typeof(Types.Threading.Tasks.Parallel);
diff --git a/Source/Test/Rewriting/Types/NameCache.cs b/Source/Test/Rewriting/Types/NameCache.cs
index 1e7be6d59..891c954ac 100644
--- a/Source/Test/Rewriting/Types/NameCache.cs
+++ b/Source/Test/Rewriting/Types/NameCache.cs
@@ -64,6 +64,14 @@ internal static class NameCache
typeof(SystemCompiler.YieldAwaitable).FullName + "/YieldAwaiter";
internal static string TaskExtensions { get; } = typeof(SystemTasks.TaskExtensions).FullName;
+#if NET
+ internal static string TaskAsyncEnumerableExtensions { get; } = typeof(SystemTasks.TaskAsyncEnumerableExtensions).FullName;
+ internal static string ConfiguredCancelableAsyncEnumerable { get; } =
+ typeof(SystemCompiler.ConfiguredCancelableAsyncEnumerable<>).FullName;
+ internal static string ConfiguredAsyncEnumerator { get; } =
+ typeof(SystemCompiler.ConfiguredCancelableAsyncEnumerable<>).FullName + "/Enumerator";
+ internal static string ConfiguredAsyncDisposable { get; } = typeof(SystemCompiler.ConfiguredAsyncDisposable).FullName;
+#endif
internal static string TaskFactory { get; } = typeof(SystemTasks.TaskFactory).FullName;
internal static string GenericTaskFactory { get; } = typeof(SystemTasks.TaskFactory<>).FullName;
internal static string TaskParallel { get; } = typeof(SystemTasks.Parallel).FullName;
diff --git a/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredAsyncDisposable.cs b/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredAsyncDisposable.cs
new file mode 100644
index 000000000..bcf0ec3d3
--- /dev/null
+++ b/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredAsyncDisposable.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if NET
+using System;
+
+namespace Microsoft.Coyote.Rewriting.Types.Runtime.CompilerServices
+{
+ ///
+ /// Provides an async disposable whose configured await can be controlled during testing.
+ ///
+ /// This type is intended for compiler use only.
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ public readonly struct ConfiguredAsyncDisposable
+ {
+ private readonly IAsyncDisposable Disposable;
+ private readonly bool ContinueOnCapturedContext;
+
+ internal ConfiguredAsyncDisposable(IAsyncDisposable disposable, bool continueOnCapturedContext)
+ {
+ this.Disposable = disposable;
+ this.ContinueOnCapturedContext = continueOnCapturedContext;
+ }
+
+ ///
+ /// Disposes the underlying resource.
+ ///
+ public ConfiguredValueTaskAwaitable DisposeAsync()
+ {
+ var task = this.Disposable.DisposeAsync();
+ return new ConfiguredValueTaskAwaitable(ref task, this.ContinueOnCapturedContext);
+ }
+ }
+}
+#endif
diff --git a/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredCancelableAsyncEnumerable.cs b/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredCancelableAsyncEnumerable.cs
new file mode 100644
index 000000000..6adc0392b
--- /dev/null
+++ b/Source/Test/Rewriting/Types/Runtime/CompilerServices/ConfiguredCancelableAsyncEnumerable.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if NET
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Microsoft.Coyote.Rewriting.Types.Runtime.CompilerServices
+{
+ ///
+ /// Provides an async enumerable whose configured awaits can be controlled during testing.
+ ///
+ /// The type of each element.
+ /// This type is intended for compiler use only.
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ public readonly struct ConfiguredCancelableAsyncEnumerable
+ {
+ private readonly IAsyncEnumerable Enumerable;
+ private readonly CancellationToken CancellationToken;
+ private readonly bool ContinueOnCapturedContext;
+
+ internal ConfiguredCancelableAsyncEnumerable(IAsyncEnumerable enumerable,
+ bool continueOnCapturedContext, CancellationToken cancellationToken)
+ {
+ this.Enumerable = enumerable;
+ this.CancellationToken = cancellationToken;
+ this.ContinueOnCapturedContext = continueOnCapturedContext;
+ }
+
+ ///
+ /// Configures continuation context capture for iteration and disposal.
+ ///
+ public ConfiguredCancelableAsyncEnumerable ConfigureAwait(bool continueOnCapturedContext) =>
+ new ConfiguredCancelableAsyncEnumerable(this.Enumerable, continueOnCapturedContext, this.CancellationToken);
+
+ ///
+ /// Replaces the cancellation token passed to the underlying enumerable.
+ ///
+ public ConfiguredCancelableAsyncEnumerable WithCancellation(CancellationToken cancellationToken) =>
+ new ConfiguredCancelableAsyncEnumerable(this.Enumerable, this.ContinueOnCapturedContext, cancellationToken);
+
+ ///
+ /// Creates the configured enumerator.
+ ///
+ public Enumerator GetAsyncEnumerator() =>
+ new Enumerator(this.Enumerable.GetAsyncEnumerator(this.CancellationToken), this.ContinueOnCapturedContext);
+
+ ///
+ /// Wraps iteration and disposal in controllable configured awaitables.
+ ///
+ public readonly struct Enumerator
+ {
+ private readonly IAsyncEnumerator AsyncEnumerator;
+ private readonly bool ContinueOnCapturedContext;
+
+ internal Enumerator(IAsyncEnumerator enumerator, bool continueOnCapturedContext)
+ {
+ this.AsyncEnumerator = enumerator;
+ this.ContinueOnCapturedContext = continueOnCapturedContext;
+ }
+
+ ///
+ /// Gets the current element.
+ ///
+ public T Current => this.AsyncEnumerator.Current;
+
+ ///
+ /// Advances the underlying enumerator.
+ ///
+ public ConfiguredValueTaskAwaitable MoveNextAsync()
+ {
+ var task = this.AsyncEnumerator.MoveNextAsync();
+ return new ConfiguredValueTaskAwaitable(ref task, this.ContinueOnCapturedContext);
+ }
+
+ ///
+ /// Disposes the underlying enumerator.
+ ///
+ public ConfiguredValueTaskAwaitable DisposeAsync()
+ {
+ var task = this.AsyncEnumerator.DisposeAsync();
+ return new ConfiguredValueTaskAwaitable(ref task, this.ContinueOnCapturedContext);
+ }
+ }
+ }
+}
+#endif
diff --git a/Source/Test/Rewriting/Types/Threading/Tasks/TaskAsyncEnumerableExtensions.cs b/Source/Test/Rewriting/Types/Threading/Tasks/TaskAsyncEnumerableExtensions.cs
new file mode 100644
index 000000000..5f4605f21
--- /dev/null
+++ b/Source/Test/Rewriting/Types/Threading/Tasks/TaskAsyncEnumerableExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#if NET
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.Coyote.Rewriting.Types.Runtime.CompilerServices;
+
+namespace Microsoft.Coyote.Rewriting.Types.Threading.Tasks
+{
+ ///
+ /// Configures async enumeration and disposal using controllable awaitables.
+ ///
+ /// This type is intended for compiler use only.
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ public static class TaskAsyncEnumerableExtensions
+ {
+ ///
+ /// Configures context capture when disposing an async resource.
+ ///
+ public static ConfiguredAsyncDisposable ConfigureAwait(IAsyncDisposable source, bool continueOnCapturedContext) =>
+ new ConfiguredAsyncDisposable(source, continueOnCapturedContext);
+
+ ///
+ /// Configures context capture when iterating an async enumerable.
+ ///
+ public static ConfiguredCancelableAsyncEnumerable ConfigureAwait(
+ IAsyncEnumerable source, bool continueOnCapturedContext) =>
+ new ConfiguredCancelableAsyncEnumerable(source, continueOnCapturedContext, default);
+
+ ///
+ /// Configures the cancellation token used when iterating an async enumerable.
+ ///
+ public static ConfiguredCancelableAsyncEnumerable WithCancellation(
+ IAsyncEnumerable source, CancellationToken cancellationToken) =>
+ new ConfiguredCancelableAsyncEnumerable(source, true, cancellationToken);
+
+#if NET7_0_OR_GREATER
+ ///
+ /// Converts an async enumerable into a blocking enumerable.
+ ///
+ public static IEnumerable ToBlockingEnumerable(
+ IAsyncEnumerable source, CancellationToken cancellationToken = default) =>
+ System.Threading.Tasks.TaskAsyncEnumerableExtensions.ToBlockingEnumerable(source, cancellationToken);
+#endif
+ }
+}
+#endif
diff --git a/Tests/Tests.BugFinding/Tasks/ConfiguredAsyncEnumerableTests.cs b/Tests/Tests.BugFinding/Tasks/ConfiguredAsyncEnumerableTests.cs
new file mode 100644
index 000000000..1bb8cfe74
--- /dev/null
+++ b/Tests/Tests.BugFinding/Tasks/ConfiguredAsyncEnumerableTests.cs
@@ -0,0 +1,826 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Coyote.Specifications;
+using Microsoft.Coyote.Tests.Common.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Coyote.BugFinding.Tests
+{
+#if NET
+ public class ConfiguredAsyncEnumerableTests : BaseBugFindingTest
+ {
+ public ConfiguredAsyncEnumerableTests(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestWithCancellation(bool asynchronous)
+ {
+ this.Test(async () =>
+ {
+ SharedEntry disposed = new SharedEntry();
+ int sum = 0;
+ await foreach (int item in EnumerateAsync(disposed, asynchronous).WithCancellation(default))
+ {
+ sum += item;
+ }
+
+ Assert.Equal(3, sum);
+ Assert.Equal(1, disposed.Value);
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50));
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public void TestConfigureAwait(bool asynchronous, bool continueOnCapturedContext)
+ {
+ this.Test(async () =>
+ {
+ SharedEntry disposed = new SharedEntry();
+ int sum = 0;
+ await foreach (int item in EnumerateAsync(disposed, asynchronous).ConfigureAwait(continueOnCapturedContext))
+ {
+ sum += item;
+ }
+
+ Assert.Equal(3, sum);
+ Assert.Equal(1, disposed.Value);
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50));
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestChainedConfigurationAndCancellation(bool cancellationFirst)
+ {
+ this.Test(async () =>
+ {
+ using var source = new CancellationTokenSource();
+ using var replacedSource = new CancellationTokenSource();
+ replacedSource.Cancel();
+ SharedEntry disposed = new SharedEntry();
+ var items = cancellationFirst ?
+ EnumerateAsync(disposed, true).WithCancellation(replacedSource.Token).ConfigureAwait(false) :
+ EnumerateAsync(disposed, true).ConfigureAwait(false).WithCancellation(replacedSource.Token);
+ items = items.ConfigureAwait(true).WithCancellation(source.Token);
+ int count = 0;
+ bool canceled = false;
+ try
+ {
+ await foreach (int item in items)
+ {
+ count++;
+ source.Cancel();
+ }
+ }
+ catch (OperationCanceledException exception)
+ {
+ Assert.Equal(source.Token, exception.CancellationToken);
+ canceled = true;
+ }
+
+ Assert.True(canceled);
+ Assert.Equal(1, count);
+ Assert.Equal(1, disposed.Value);
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50));
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestConfiguredAsyncDisposal(bool continueOnCapturedContext)
+ {
+ this.Test(async () =>
+ {
+ SharedEntry disposed = new SharedEntry();
+ await using (new AsyncDisposable(disposed).ConfigureAwait(continueOnCapturedContext))
+ {
+ Assert.Equal(0, disposed.Value);
+ }
+
+ Assert.Equal(1, disposed.Value);
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50));
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestEarlyExitDisposesEnumerator(bool continueOnCapturedContext)
+ {
+ this.Test(async () =>
+ {
+ SharedEntry disposed = new SharedEntry();
+ await foreach (int item in EnumerateAsync(disposed, true).ConfigureAwait(continueOnCapturedContext))
+ {
+ Assert.Equal(1, item);
+ break;
+ }
+
+ Assert.Equal(1, disposed.Value);
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50));
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredEnumerationFailureReplay()
+ {
+ this.TestWithError(async () =>
+ {
+ SharedEntry disposed = new SharedEntry();
+ await foreach (int item in EnumerateAsync(disposed, true).WithCancellation(default).ConfigureAwait(false))
+ {
+ Specification.Assert(item != 2, "Reached the second item.");
+ }
+ },
+ configuration: this.GetConfiguration().WithTestingIterations(50),
+ expectedError: "Reached the second item.",
+ replay: true);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredEnumerableInGenericConstructor()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ var box = new Box>(source.WithCancellation(cancellation.Token));
+ Assert.NotNull(box);
+ box.Value.GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredEnumeratorInGenericConstructor()
+ {
+ var source = new EmptyEnumerable();
+ var enumerator = source.WithCancellation(default).GetAsyncEnumerator();
+ var box = new Box.Enumerator>(enumerator);
+ Assert.NotNull(box);
+ Assert.Equal(0, box.Value.Current);
+ box.Value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredDisposableInGenericConstructor()
+ {
+ var source = new EmptyEnumerable();
+ var box = new Box(((IAsyncDisposable)source).ConfigureAwait(false));
+ Assert.NotNull(box);
+ box.Value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfigurationMethodGroup()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ Func, CancellationToken, ConfiguredCancelableAsyncEnumerable> configure =
+ TaskAsyncEnumerableExtensions.WithCancellation;
+ configure(source, cancellation.Token).GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestCustomConfigurationMethodGroup()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ ConfigureCallback configure = TaskAsyncEnumerableExtensions.WithCancellation;
+ configure(source, cancellation.Token).GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestDisposalConfigurationMethodGroup()
+ {
+ var source = new EmptyEnumerable();
+ Func configure =
+ TaskAsyncEnumerableExtensions.ConfigureAwait;
+ configure(source, false).DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoundConfiguredDisposableMethodGroup()
+ {
+ var source = new EmptyEnumerable();
+ var items = new List();
+ Action add = items.Add;
+ add(((IAsyncDisposable)source).ConfigureAwait(false));
+ Assert.Single(items);
+ items[0].DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestVirtualConfiguredDisposableMethodGroup()
+ {
+ var source = new EmptyEnumerable();
+ ICollection items = new List();
+ Action add = items.Add;
+ add(((IAsyncDisposable)source).ConfigureAwait(false));
+ Assert.Single(items);
+ foreach (var item in items)
+ {
+ item.DisposeAsync().GetAwaiter().GetResult();
+ }
+
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoundConfiguredStructMethodGroup()
+ {
+ var source = new EmptyEnumerable();
+ var configured = ((IAsyncDisposable)source).ConfigureAwait(false);
+ Func dispose = configured.DisposeAsync;
+ dispose().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoundMethodGroupPreservesInstanceBinding()
+ {
+ Action wait = Task.CompletedTask.Wait;
+ wait();
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoxedConfiguredEnumerable()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ object boxed = source.WithCancellation(cancellation.Token);
+ Assert.IsType>(boxed);
+ var value = (ConfiguredCancelableAsyncEnumerable)boxed;
+ value.GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoxedConfiguredEnumerator()
+ {
+ var source = new EmptyEnumerable();
+ object boxed = source.WithCancellation(default).GetAsyncEnumerator();
+ Assert.IsType.Enumerator>(boxed);
+ var value = (ConfiguredCancelableAsyncEnumerable.Enumerator)boxed;
+ Assert.Equal(0, value.Current);
+ value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestBoxedConfiguredDisposable()
+ {
+ var source = new EmptyEnumerable();
+ object boxed = ((IAsyncDisposable)source).ConfigureAwait(false);
+ Assert.IsType(boxed);
+ var value = (ConfiguredAsyncDisposable)boxed;
+ value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredValueArrays()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ var enumerableItems = new[] { source.WithCancellation(default) };
+ enumerableItems[0] = source.WithCancellation(cancellation.Token);
+ var enumerable = enumerableItems[0];
+ var enumeratorItems = new[] { enumerable.GetAsyncEnumerator() };
+ var enumerator = enumeratorItems[0];
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ Assert.Equal(0, enumerator.Current);
+ enumeratorItems[0].DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+
+ var disposableSource = new EmptyEnumerable();
+ var disposableItems = new[] { ((IAsyncDisposable)disposableSource).ConfigureAwait(false) };
+ var disposable = disposableItems[0];
+ disposable.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(disposableSource.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestDerivedConfiguredResourceCollection()
+ {
+ var source = new EmptyEnumerable();
+ var resources = new ConfiguredResources
+ {
+ ((IAsyncDisposable)source).ConfigureAwait(false)
+ };
+ IEnumerable items = resources;
+ foreach (var item in items)
+ {
+ item.DisposeAsync().GetAwaiter().GetResult();
+ }
+
+ Assert.True(source.IsDisposed);
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestConfiguredResourceSignatures(bool throughInterface)
+ {
+ var source = new EmptyEnumerable();
+ var resource = new ConfiguredResource(source);
+ var configured = throughInterface ?
+ ((IConfiguredResource)resource).Configure() :
+ ((ConfiguredResourceBase)resource).Configure();
+ configured = throughInterface ?
+ ((IConfiguredResource)resource).Echo(configured) :
+ ((ConfiguredResourceBase)resource).Echo(configured);
+ configured.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestClosedGenericExplicitInterface()
+ {
+ var source = new EmptyEnumerable();
+ var configured = ((IAsyncDisposable)source).ConfigureAwait(false);
+ IComparer comparer = new ConfiguredResourceComparer();
+ Assert.Equal(0, comparer.Compare(configured, configured));
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredCollectionVirtualBaseCall()
+ {
+ var source = new EmptyEnumerable();
+ var resources = new ConfiguredResourceCollection();
+ resources.Add(((IAsyncDisposable)source).ConfigureAwait(false));
+ Assert.Equal(1, resources.Insertions);
+ Assert.Single(resources).DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredMethodConstraint()
+ {
+ var source = new EmptyEnumerable();
+ var resources = new ConfiguredResources
+ {
+ ((IAsyncDisposable)source).ConfigureAwait(false)
+ };
+ DisposeConstrainedResources(resources);
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredTypeConstraint()
+ {
+ var source = new EmptyEnumerable();
+ var resources = new ConfiguredResources
+ {
+ ((IAsyncDisposable)source).ConfigureAwait(false)
+ };
+ new ConfiguredResourceConsumer(resources).Dispose();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredTupleFields()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ var disposable = new EmptyEnumerable();
+ var pair = (source.WithCancellation(default), ((IAsyncDisposable)disposable).ConfigureAwait(false));
+ pair.Item1 = source.WithCancellation(cancellation.Token);
+ var enumerable = pair.Item1;
+ enumerable.GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ pair.Item2.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(disposable.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredStaticGenericField()
+ {
+ var first = new EmptyEnumerable();
+ ConfiguredResourceSlot.Value = ((IAsyncDisposable)first).ConfigureAwait(false);
+ var configured = ConfiguredResourceSlot.Value;
+ configured.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(first.IsDisposed);
+
+ var second = new EmptyEnumerable();
+ ConfiguredResourceSlot.Value = ((IAsyncDisposable)second).ConfigureAwait(false);
+ ConfiguredResourceSlot.Value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(second.IsDisposed);
+ ConfiguredResourceSlot.Value = default;
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ public void TestNullEnumerableConfiguration(bool withCancellation, bool continueOnCapturedContext)
+ {
+ IAsyncEnumerable source = null;
+ // Reflect over CoreLib so the control invokes the framework API, not its rewritten replacement.
+ var nativeExtensions = typeof(object).Assembly.GetType("System.Threading.Tasks.TaskAsyncEnumerableExtensions", true);
+ string methodName = withCancellation ? "WithCancellation" : "ConfigureAwait";
+ var nativeMethod = Array.Find(nativeExtensions.GetMethods(),
+ method => method.Name == methodName && method.IsGenericMethodDefinition);
+ Assert.NotNull(nativeMethod);
+ var nativeConfigured = nativeMethod.MakeGenericMethod(typeof(int)).Invoke(null,
+ new object[] { source, withCancellation ? (object)default(CancellationToken) : continueOnCapturedContext });
+ Assert.NotNull(nativeConfigured);
+ var nativeException = Assert.Throws(() =>
+ nativeConfigured.GetType().GetMethod("GetAsyncEnumerator").Invoke(nativeConfigured, null));
+ Assert.IsType(nativeException.InnerException);
+
+ var configured = withCancellation ?
+ source.WithCancellation(default) :
+ source.ConfigureAwait(continueOnCapturedContext);
+ Assert.Throws(() => configured.GetAsyncEnumerator());
+ }
+
+#pragma warning disable CA1814 // Rectangular arrays are required to exercise their synthetic methods.
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredRectangularArrays()
+ {
+ using var cancellation = new CancellationTokenSource();
+ var source = new EmptyEnumerable();
+ var enumerables = new ConfiguredCancelableAsyncEnumerable[1, 1];
+ enumerables[0, 0] = source.WithCancellation(cancellation.Token);
+ var enumerable = enumerables[0, 0];
+ var enumerators = new ConfiguredCancelableAsyncEnumerable.Enumerator[1, 1];
+ enumerators[0, 0] = enumerable.GetAsyncEnumerator();
+ Assert.Equal(cancellation.Token, source.ObservedToken);
+ Assert.Equal(0, enumerators[0, 0].Current);
+ var enumerator = enumerators[0, 0];
+ enumerator.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+
+ var first = new EmptyEnumerable();
+ var second = new EmptyEnumerable();
+ var disposables = new ConfiguredAsyncDisposable[1, 2];
+ disposables[0, 0] = ((IAsyncDisposable)first).ConfigureAwait(false);
+ disposables[0, 1] = ((IAsyncDisposable)second).ConfigureAwait(false);
+ var disposable = disposables[0, 0];
+ disposable.DisposeAsync().GetAwaiter().GetResult();
+ disposables[0, 1].DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(first.IsDisposed);
+ Assert.True(second.IsDisposed);
+ }
+#pragma warning restore CA1814
+
+ [Fact(Timeout = 5000)]
+ public void TestExternalConfiguredGenericContainer()
+ {
+ var source = new EmptyEnumerable();
+ var box = new UncontrolledGenericBox(((IAsyncDisposable)source).ConfigureAwait(false));
+ var configured = box.Select(value => value);
+ Assert.Equal(box.Value, configured);
+ configured.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestExternalConfiguredGenericBase()
+ {
+ var source = new EmptyEnumerable();
+ var resource = new ExternalConfiguredResource(source);
+ resource.Value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredDisposalExpression()
+ {
+ var source = new EmptyEnumerable();
+ Expression> expression = () => ((IAsyncDisposable)source).ConfigureAwait(false);
+ expression.Compile()().DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredGenericConstructorExpression()
+ {
+ var source = new EmptyEnumerable();
+ Expression>> expression =
+ () => new Box(((IAsyncDisposable)source).ConfigureAwait(false));
+ expression.Compile()().Value.DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestInstanceMethodExpressionBinding()
+ {
+ Task task = Task.CompletedTask;
+ Expression expression = () => task.Wait();
+ expression.Compile()();
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredPropertyExpression()
+ {
+ var source = new EmptyEnumerable();
+ var holder = new ConfiguredResourceHolder(((IAsyncDisposable)source).ConfigureAwait(false));
+ Expression> expression = value => value.Resource;
+ expression.Compile()(holder).DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredIndexerExpression()
+ {
+ var source = new EmptyEnumerable();
+ var configured = ((IAsyncDisposable)source).ConfigureAwait(false);
+ var holder = new ConfiguredResourceHolder(configured);
+ var indexer = Expression.MakeIndex(Expression.Constant(holder),
+ typeof(ConfiguredResourceHolder).GetProperty("Item"), new[] { Expression.Constant(configured) });
+ Expression.Lambda>(indexer)
+ .Compile()().DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredEventMetadata()
+ {
+ var source = new EmptyEnumerable();
+ var holder = new ConfiguredResourceHolder(((IAsyncDisposable)source).ConfigureAwait(false));
+ Assert.Equal(typeof(Action),
+ typeof(ConfiguredResourceHolder).GetEvent(nameof(ConfiguredResourceHolder.Configured)).EventHandlerType);
+ holder.Configured += value => value.DisposeAsync().GetAwaiter().GetResult();
+ holder.Notify();
+ Assert.True(source.IsDisposed);
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestGenericStaticMemberExpressionHandles()
+ {
+ Expression>> expression = () => Task.Factory;
+ Assert.Same(typeof(Task).GetProperty(nameof(Task.Factory)).GetValue(null), expression.Compile()());
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestConfiguredGenericStaticMemberExpressionHandles()
+ {
+ Expression>> expression =
+ () => Task.Factory;
+ Assert.Same(typeof(Task).GetProperty(nameof(Task.Factory)).GetValue(null),
+ expression.Compile()());
+ }
+
+ [Theory(Timeout = 5000)]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void TestConfiguredBlockingEnumerable(bool cancel)
+ {
+ using var cancellation = new CancellationTokenSource();
+ if (cancel)
+ {
+ cancellation.Cancel();
+ }
+
+ var source = new EmptyEnumerable();
+ var items = EnumerateValue(((IAsyncDisposable)source).ConfigureAwait(false))
+ .ToBlockingEnumerable(cancellation.Token);
+ if (cancel)
+ {
+ var exception = Assert.Throws(() => Assert.Single(items));
+ Assert.Equal(cancellation.Token, exception.CancellationToken);
+ }
+ else
+ {
+ Assert.Single(items).DisposeAsync().GetAwaiter().GetResult();
+ Assert.True(source.IsDisposed);
+ }
+ }
+
+ [Fact(Timeout = 5000)]
+ public void TestAwaiterBlockingEnumerable()
+ {
+ var awaiter = Assert.Single(EnumerateValue(Task.FromResult(3).GetAwaiter()).ToBlockingEnumerable());
+ Assert.Equal(3, awaiter.GetResult());
+ }
+
+ private static void DisposeConstrainedResources(T resources)
+ where T : IEnumerable
+ {
+ foreach (var resource in resources)
+ {
+ resource.DisposeAsync().GetAwaiter().GetResult();
+ }
+ }
+
+ private static async IAsyncEnumerable EnumerateValue(T value,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.CompletedTask;
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return value;
+ }
+
+ private static async IAsyncEnumerable EnumerateAsync(SharedEntry disposed, bool asynchronous,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ for (int i = 1; i <= 2; i++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (asynchronous)
+ {
+ await Task.Delay(1);
+ }
+
+ yield return i;
+ }
+ }
+ finally
+ {
+ if (asynchronous)
+ {
+ await Task.Delay(1);
+ }
+
+ disposed.Value++;
+ }
+ }
+
+ private sealed class AsyncDisposable : IAsyncDisposable
+ {
+ private readonly SharedEntry Disposed;
+
+ internal AsyncDisposable(SharedEntry disposed)
+ {
+ this.Disposed = disposed;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await Task.Delay(1);
+ this.Disposed.Value++;
+ }
+ }
+
+ private delegate ConfiguredCancelableAsyncEnumerable ConfigureCallback(
+ IAsyncEnumerable source, CancellationToken cancellationToken);
+
+ private sealed class ConfiguredResources : List, IEnumerable
+ {
+ }
+
+ private interface IConfiguredResource
+ {
+ ConfiguredAsyncDisposable Configure();
+
+ ConfiguredAsyncDisposable Echo(ConfiguredAsyncDisposable value);
+ }
+
+ private abstract class ConfiguredResourceBase : IConfiguredResource
+ {
+ public abstract ConfiguredAsyncDisposable Configure();
+
+ public abstract ConfiguredAsyncDisposable Echo(ConfiguredAsyncDisposable value);
+ }
+
+ private sealed class ConfiguredResource : ConfiguredResourceBase, IConfiguredResource
+ {
+ private readonly IAsyncDisposable Disposable;
+
+ internal ConfiguredResource(IAsyncDisposable disposable)
+ {
+ this.Disposable = disposable;
+ }
+
+ public override ConfiguredAsyncDisposable Configure() => this.Disposable.ConfigureAwait(false);
+
+ public override ConfiguredAsyncDisposable Echo(ConfiguredAsyncDisposable value) => value;
+
+ ConfiguredAsyncDisposable IConfiguredResource.Configure() => this.Configure();
+
+ ConfiguredAsyncDisposable IConfiguredResource.Echo(ConfiguredAsyncDisposable value) => this.Echo(value);
+ }
+
+ private sealed class ConfiguredResourceComparer : IComparer
+ {
+ int IComparer.Compare(ConfiguredAsyncDisposable x, ConfiguredAsyncDisposable y) =>
+ x.Equals(y) ? 0 : 1;
+ }
+
+ private sealed class ConfiguredResourceCollection : Collection
+ {
+ internal int Insertions { get; private set; }
+
+ protected override void InsertItem(int index, ConfiguredAsyncDisposable item)
+ {
+ // Fail deterministically instead of overflowing the stack if the base call is made virtual.
+ if (++this.Insertions > 1)
+ {
+ throw new InvalidOperationException("Base insertion dispatched back into the override.");
+ }
+
+ base.InsertItem(index, item);
+ }
+ }
+
+ private sealed class ConfiguredResourceConsumer
+ where T : IEnumerable
+ {
+ private readonly T Resources;
+
+ internal ConfiguredResourceConsumer(T resources)
+ {
+ this.Resources = resources;
+ }
+
+ internal void Dispose() => DisposeConstrainedResources(this.Resources);
+ }
+
+ private sealed class ExternalConfiguredResource : UncontrolledGenericBox
+ {
+ internal ExternalConfiguredResource(IAsyncDisposable source)
+ : base(source.ConfigureAwait(false))
+ {
+ }
+ }
+
+ private sealed class Box
+ {
+ internal T Value { get; }
+
+ internal Box(T value)
+ {
+ this.Value = value;
+ }
+ }
+
+ private sealed class ConfiguredResourceHolder
+ {
+ public ConfiguredAsyncDisposable Resource { get; }
+
+ public ConfiguredAsyncDisposable this[ConfiguredAsyncDisposable value]
+ {
+ get
+ {
+ Assert.Equal(this.Resource, value);
+ return this.Resource;
+ }
+ }
+
+ public event Action Configured;
+
+ internal ConfiguredResourceHolder(ConfiguredAsyncDisposable resource)
+ {
+ this.Resource = resource;
+ }
+
+ internal void Notify() => this.Configured?.Invoke(this.Resource);
+ }
+
+ private static class ConfiguredResourceSlot
+ {
+ internal static T Value;
+ }
+
+ private sealed class EmptyEnumerable : IAsyncEnumerable, IAsyncEnumerator
+ {
+ public int Current => 0;
+
+ internal CancellationToken ObservedToken { get; private set; }
+
+ internal bool IsDisposed { get; private set; }
+
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ {
+ this.ObservedToken = cancellationToken;
+ return this;
+ }
+
+ public ValueTask MoveNextAsync() => new ValueTask(false);
+
+ public ValueTask DisposeAsync()
+ {
+ this.IsDisposed = true;
+ return default;
+ }
+ }
+ }
+#endif
+}
diff --git a/Tests/Tests.Common/Tasks/UncontrolledGenericBox.cs b/Tests/Tests.Common/Tasks/UncontrolledGenericBox.cs
new file mode 100644
index 000000000..d5cc9d656
--- /dev/null
+++ b/Tests/Tests.Common/Tasks/UncontrolledGenericBox.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.Coyote.Tests.Common.Tasks
+{
+ ///
+ /// An unrewritten generic container for testing caller-supplied type arguments.
+ ///
+ /// The contained value type.
+ public class UncontrolledGenericBox
+ {
+ public T Value { get; }
+
+ public UncontrolledGenericBox(T value)
+ {
+ this.Value = value;
+ }
+
+ public TResult Select(Func selector) => selector(this.Value);
+ }
+}
diff --git a/Tests/compare-rewriting-diff-logs.ps1 b/Tests/compare-rewriting-diff-logs.ps1
index fcb8c8cf9..485dc8197 100644
--- a/Tests/compare-rewriting-diff-logs.ps1
+++ b/Tests/compare-rewriting-diff-logs.ps1
@@ -13,9 +13,9 @@ $targets = [ordered]@{
}
$expected_hashes = [ordered]@{
- "rewriting" = "6F4CD8237151E464053CA7509DD05D7CBEE606A1A28CDE2A66B5426AAC83E788"
+ "rewriting" = "0C785AA9B30B5FD08CDBBAB2FF928D4EED7A73AD1D18792C950D4ACB7432893F"
"rewriting-helpers" = "DF8CF299C162ECA5392793BF5E3E6D7C8B61A75E029501ED6F41C1DD1AD3183B"
- "testing" = "60F9D004CB61985FCFE111D2E25912124353FB3A97F25A75F301F5B647E7136B"
+ "testing" = "6312A437E63E3A731DDEC0A9096D2C0BBE1A61A8E3ADA25A33860EB22064B56F"
"actors" = "11AAFFA693116B24EB3EA152EE96757D8E9F7C57FFD3213ED02CDF1E9180D3D7"
"actors-testing" = "492B76D5ADE4E6EEA94A4AD1A37AD9DF911C81660600609FA1E9DF5D1B7AD860"
}