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" }