diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 0c67e1035..3702f04d2 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -305,6 +305,8 @@ namespace ICSharpCode.Decompiler.CSharp return true; if (settings.AsyncAwait && AsyncAwaitDecompiler.IsCompilerGeneratedStateMachine(typeHandle, metadata)) return true; + if (settings.AsyncEnumerator && AsyncAwaitDecompiler.IsCompilerGeneratorAsyncEnumerator(typeHandle, metadata)) + return true; if (settings.FixedBuffers && name.StartsWith("<", StringComparison.Ordinal) && name.Contains("__FixedBuffer")) return true; } else if (type.IsCompilerGenerated(metadata)) { diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 40aa62e4a..febdbd6c1 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -108,12 +108,13 @@ namespace ICSharpCode.Decompiler if (languageVersion < CSharp.LanguageVersion.CSharp8_0) { nullableReferenceTypes = false; readOnlyMethods = false; + asyncEnumerator = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nullableReferenceTypes || readOnlyMethods) + if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator) return CSharp.LanguageVersion.CSharp8_0; if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers) return CSharp.LanguageVersion.CSharp7_3; @@ -273,6 +274,24 @@ namespace ICSharpCode.Decompiler } } + bool asyncEnumerator = true; + + /// + /// Decompile IAsyncEnumerator/IAsyncEnumerable. + /// Only has an effect if is enabled. + /// + [Category("C# 8.0 / VS 2019")] + [Description("DecompilerSettings.AsyncEnumerator")] + public bool AsyncEnumerator { + get { return asyncEnumerator; } + set { + if (asyncEnumerator != value) { + asyncEnumerator = value; + OnPropertyChanged(); + } + } + } + bool decimalConstants = true; /// @@ -846,7 +865,7 @@ namespace ICSharpCode.Decompiler bool readOnlyMethods = true; [Category("C# 8.0 / VS 2019")] - [Description("DecompilerSettings.IsReadOnlyAttributeShouldBeReplacedWithReadonlyInModifiersOnStructsParameters")] + [Description("DecompilerSettings.ReadOnlyMethods")] public bool ReadOnlyMethods { get { return readOnlyMethods; } set { diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs index 63610c57e..1919ec106 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs @@ -62,14 +62,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { Void, Task, - TaskOfT + TaskOfT, + AsyncEnumerator, + AsyncEnumerable } ILTransformContext context; - // These fields are set by MatchTaskCreationPattern() - IType taskType; // return type of the async method - IType underlyingReturnType; // return type of the method (only the "T" for Task{T}) + // These fields are set by MatchTaskCreationPattern() or MatchEnumeratorCreationNewObj() + IType taskType; // return type of the async method; or IAsyncEnumerable{T}/IAsyncEnumerator{T} + IType underlyingReturnType; // return type of the method (only the "T" for Task{T}), for async enumerators this is the type being yielded AsyncMethodType methodType; ITypeDefinition stateMachineType; ITypeDefinition builderType; @@ -84,9 +86,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow ILVariable cachedStateVar; // variable in MoveNext that caches the stateField. TryCatch mainTryCatch; Block setResultAndExitBlock; // block that is jumped to for return statements + // Note: for async enumerators, a jump to setResultAndExitBlock is a 'yield break;' int finalState; // final state after the setResultAndExitBlock bool finalStateKnown; ILVariable resultVar; // the variable that gets returned by the setResultAndExitBlock + Block setResultYieldBlock; // block that is jumped to for 'yield return' statements ILVariable doFinallyBodies; // These fields are set by AnalyzeStateMachine(): @@ -110,7 +114,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow awaitBlocks.Clear(); awaitDebugInfos.Clear(); moveNextLeaves.Clear(); - if (!MatchTaskCreationPattern(function)) + if (!MatchTaskCreationPattern(function) && !MatchAsyncEnumeratorCreationPattern(function)) return; try { AnalyzeMoveNext(); @@ -133,7 +137,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow TranslateCachedFieldsToLocals(); FinalizeInlineMoveNext(function); - ((BlockContainer)function.Body).ExpectedResultType = underlyingReturnType.GetStackType(); + if (methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator) { + ((BlockContainer)function.Body).ExpectedResultType = StackType.Void; + } else { + ((BlockContainer)function.Body).ExpectedResultType = underlyingReturnType.GetStackType(); + } // Re-run control flow simplification over the newly constructed set of gotos, // and inlining because TranslateFieldsToLocalAccess() might have opened up new inlining opportunities. @@ -337,6 +345,118 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } #endregion + #region MatchAsyncEnumeratorCreationPattern + private bool MatchAsyncEnumeratorCreationPattern(ILFunction function) + { + if (!context.Settings.AsyncEnumerator) + return false; + taskType = function.ReturnType; + if (taskType.IsKnownType(KnownTypeCode.IAsyncEnumeratorOfT)) { + methodType = AsyncMethodType.AsyncEnumerator; + } else if (taskType.IsKnownType(KnownTypeCode.IAsyncEnumerableOfT)) { + methodType = AsyncMethodType.AsyncEnumerable; + } else { + return false; + } + underlyingReturnType = taskType.TypeArguments.Single(); + if (!(function.Body is BlockContainer blockContainer)) + return false; + if (blockContainer.Blocks.Count != 1) + return false; + var body = blockContainer.EntryPoint; + if (body.Instructions.Count == 1) { + // No parameters passed to enumerator (not even 'this'): + // ret(newobj(...)) + if (!body.Instructions[0].MatchReturn(out var newObj)) + return false; + if (MatchEnumeratorCreationNewObj(newObj, context, out initialState, out stateMachineType)) { + // HACK: the normal async/await logic expects 'initialState' to be the 'in progress' state + initialState = -1; + try { + AnalyzeEnumeratorCtor(((NewObj)newObj).Method, context, out builderField, out stateField); + } catch (SymbolicAnalysisFailedException) { + return false; + } + builderType = builderField.Type.GetDefinition(); + if (builderType == null) + return false; + return true; + } else { + return false; + } + } + // TODO: enumerator creation with parameters + return false; + } + + static bool MatchEnumeratorCreationNewObj(ILInstruction inst, ILTransformContext context, + out int initialState, out ITypeDefinition stateMachineType) + { + initialState = default; + stateMachineType = default; + // newobj(CurrentType/...::.ctor, ldc.i4(-2)) + if (!(inst is NewObj newObj)) + return false; + if (newObj.Arguments.Count != 1) + return false; + if (!newObj.Arguments[0].MatchLdcI4(out initialState)) + return false; + stateMachineType = newObj.Method.DeclaringTypeDefinition; + if (stateMachineType == null) + return false; + if (stateMachineType.DeclaringTypeDefinition != context.Function.Method.DeclaringTypeDefinition) + return false; + return IsCompilerGeneratorAsyncEnumerator( + (TypeDefinitionHandle)stateMachineType.MetadataToken, + context.TypeSystem.MainModule.metadata); + } + + public static bool IsCompilerGeneratorAsyncEnumerator(TypeDefinitionHandle type, MetadataReader metadata) + { + TypeDefinition td; + if (type.IsNil || !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil) + return false; + foreach (var i in td.GetInterfaceImplementations()) { + var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata); + if (!tr.IsNested && tr.TopLevelTypeName.Namespace == "System.Collections.Generic" && tr.TopLevelTypeName.Name == "IAsyncEnumerator" && tr.TopLevelTypeName.TypeParameterCount == 1) + return true; + } + return false; + } + + static void AnalyzeEnumeratorCtor(IMethod ctor, ILTransformContext context, out IField builderField, out IField stateField) + { + builderField = null; + stateField = null; + + var ctorHandle = (MethodDefinitionHandle)ctor.MetadataToken; + Block body = YieldReturnDecompiler.SingleBlock(YieldReturnDecompiler.CreateILAst(ctorHandle, context).Body); + if (body == null) + throw new SymbolicAnalysisFailedException("Missing enumeratorCtor.Body"); + // Block IL_0000 (incoming: 1) { + // call Object..ctor(ldloc this) + // stfld <>1__state(ldloc this, ldloc <>1__state) + // stfld <>t__builder(ldloc this, call Create()) + // leave IL_0000 (nop) + // } + if (body.Instructions.ElementAtOrDefault(1).MatchStFld(out var target, out var field, out var value) + && target.MatchLdThis() + && value.MatchLdLoc(out var arg) + && arg.Kind == VariableKind.Parameter && arg.Index == 0) { + stateField = (IField)field.MemberDefinition; + } else { + throw new SymbolicAnalysisFailedException("Could not find stateField"); + } + if (body.Instructions.ElementAtOrDefault(2).MatchStFld(out target, out field, out value) + && target.MatchLdThis() + && value is Call call && call.Method.Name == "Create") { + builderField = (IField)field.MemberDefinition; + } else { + throw new SymbolicAnalysisFailedException("Could not find builderField"); + } + } + #endregion + #region AnalyzeMoveNext /// /// First peek into MoveNext(); analyzes everything outside the big try-catch. @@ -353,8 +473,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow moveNextFunction = YieldReturnDecompiler.CreateILAst(moveNextMethod, context); if (!(moveNextFunction.Body is BlockContainer blockContainer)) throw new SymbolicAnalysisFailedException(); - if (blockContainer.Blocks.Count != 2 && blockContainer.Blocks.Count != 1) - throw new SymbolicAnalysisFailedException(); if (blockContainer.EntryPoint.IncomingEdgeCount != 1) throw new SymbolicAnalysisFailedException(); cachedStateVar = null; @@ -385,12 +503,49 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow if (((BlockContainer)mainTryCatch.TryBlock).EntryPoint.Instructions[0] is StLoc initDoFinallyBodies && initDoFinallyBodies.Variable.Kind == VariableKind.Local && initDoFinallyBodies.Variable.Type.IsKnownType(KnownTypeCode.Boolean) - && initDoFinallyBodies.Value.MatchLdcI4(1)) - { + && initDoFinallyBodies.Value.MatchLdcI4(1)) { doFinallyBodies = initDoFinallyBodies.Variable; } - setResultAndExitBlock = blockContainer.Blocks.ElementAtOrDefault(1); + Debug.Assert(blockContainer.Blocks[0] == blockContainer.EntryPoint); // already checked this block + pos = 1; + if (MatchYieldBlock(blockContainer, pos)) { + setResultYieldBlock = blockContainer.Blocks[pos]; + pos++; + } else { + setResultYieldBlock = null; + } + + setResultAndExitBlock = blockContainer.Blocks.ElementAtOrDefault(pos); + CheckSetResultAndExitBlock(blockContainer); + + if (pos + 1 < blockContainer.Blocks.Count) + throw new SymbolicAnalysisFailedException("too many blocks"); + } + + private bool IsAsyncEnumerator => methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator; + + bool MatchYieldBlock(BlockContainer blockContainer, int pos) + { + if (!IsAsyncEnumerator) + return false; + var block = blockContainer.Blocks.ElementAtOrDefault(pos); + // call SetResult(ldflda <>v__promiseOfValueOrEnd(ldloc this), ldc.i4 1) + // leave IL_0000(nop) + if (block.Instructions.Count != 2) + return false; + if (!MatchCall(block.Instructions[0], "SetResult", out var args)) + return false; + if (args.Count != 2) + return false; + if (!IsBuilderOrPromiseFieldOnThis(args[0])) + return false; + if (!args[1].MatchLdcI4(1)) + return false; + return block.Instructions[1].MatchLeave(blockContainer); + } + void CheckSetResultAndExitBlock(BlockContainer blockContainer) + { if (setResultAndExitBlock == null) { // This block can be absent if the function never exits normally, // but always throws an exception/loops infinitely. @@ -408,17 +563,29 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow finalStateKnown = true; if (!MatchCall(setResultAndExitBlock.Instructions[1], "SetResult", out var args)) throw new SymbolicAnalysisFailedException(); - if (!IsBuilderFieldOnThis(args[0])) + if (!IsBuilderOrPromiseFieldOnThis(args[0])) throw new SymbolicAnalysisFailedException(); - if (methodType == AsyncMethodType.TaskOfT) { - if (args.Count != 2) - throw new SymbolicAnalysisFailedException(); - if (!args[1].MatchLdLoc(out resultVar)) - throw new SymbolicAnalysisFailedException(); - } else { - resultVar = null; - if (args.Count != 1) - throw new SymbolicAnalysisFailedException(); + switch (methodType) { + case AsyncMethodType.TaskOfT: + if (args.Count != 2) + throw new SymbolicAnalysisFailedException(); + if (!args[1].MatchLdLoc(out resultVar)) + throw new SymbolicAnalysisFailedException(); + break; + case AsyncMethodType.Task: + case AsyncMethodType.Void: + resultVar = null; + if (args.Count != 1) + throw new SymbolicAnalysisFailedException(); + break; + case AsyncMethodType.AsyncEnumerable: + case AsyncMethodType.AsyncEnumerator: + resultVar = null; + if (args.Count != 2) + throw new SymbolicAnalysisFailedException(); + if (!args[1].MatchLdcI4(0)) + throw new SymbolicAnalysisFailedException(); + break; } if (!setResultAndExitBlock.Instructions[2].MatchLeave(blockContainer)) throw new SymbolicAnalysisFailedException(); @@ -465,7 +632,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow throw new SymbolicAnalysisFailedException(); if (args.Count != 2) throw new SymbolicAnalysisFailedException(); - if (!IsBuilderFieldOnThis(args[0])) + if (!IsBuilderOrPromiseFieldOnThis(args[0])) throw new SymbolicAnalysisFailedException(); if (!args[1].MatchLdLoc(stloc.Variable)) throw new SymbolicAnalysisFailedException(); @@ -490,13 +657,22 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow return target.MatchLdThis() && field.MemberDefinition == builderField; } + bool IsBuilderOrPromiseFieldOnThis(ILInstruction inst) + { + if (methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator) { + return true; // TODO: check against uses of promise fields in other methods? + } else { + return IsBuilderFieldOnThis(inst); + } + } + bool MatchStateAssignment(ILInstruction inst, out int newState) { // stfld(StateMachine::<>1__state, ldloc(this), ldc.i4(stateId)) if (inst.MatchStFld(out var target, out var field, out var value) - && target.MatchLdThis() + && StackSlotValue(target).MatchLdThis() && field.MemberDefinition == stateField - && value.MatchLdcI4(out newState)) + && StackSlotValue(value).MatchLdcI4(out newState)) { return true; } @@ -520,6 +696,10 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow branch.ReplaceWith(new Leave((BlockContainer)function.Body, resultVar == null ? null : new LdLoc(resultVar)).WithILRange(branch)); } } + if (setResultYieldBlock != null) { + // We still might have branches to this block; and we can't quite yet get rid of it. + ((BlockContainer)function.Body).Blocks.Add(setResultYieldBlock); + } foreach (var leave in function.Descendants.OfType()) { if (leave.TargetContainer == moveNextFunction.Body) { leave.TargetContainer = (BlockContainer)function.Body; @@ -591,6 +771,19 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow smallestAwaiterVarIndex = awaiterVar.Index.Value; } } + } else if (block.Instructions.Last().MatchBranch(setResultYieldBlock)) { + // This is a 'yield' in an async enumerator. + if (AnalyzeYieldReturn(block, out var yieldValue, out int state)) { + block.Instructions.Add(new YieldReturn(yieldValue)); + Block targetBlock = stateToBlockMap.GetOrDefault(state); + if (targetBlock != null) { + block.Instructions.Add(new Branch(targetBlock)); + } else { + block.Instructions.Add(new InvalidBranch("Could not find block for state " + state)); + } + } else { + block.Instructions.Add(new InvalidBranch("Could not detect 'yield return'")); + } } } // Skip the state dispatcher and directly jump to the initial state @@ -606,6 +799,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } } + bool AnalyzeAwaitBlock(Block block, out ILVariable awaiter, out IField awaiterField, out int state, out int yieldOffset) { awaiter = null; @@ -705,6 +899,49 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } return inst; } + + private bool AnalyzeYieldReturn(Block block, out ILInstruction yieldValue, out int newState) + { + yieldValue = default; + newState = default; + Debug.Assert(block.Instructions.Last().MatchBranch(setResultYieldBlock)); + // stfld current(ldloc this, ldstr "yieldValue") + // stloc S_45(ldloc this) + // stloc S_46(ldc.i4 -5) + // stloc V_0(ldloc S_46) + // stfld stateField(ldloc S_45, ldloc S_46) + // br setResultYieldBlock + + int pos = block.Instructions.Count - 2; + // Immediately before the 'yield return', there should be a state assignment: + if (pos < 0 || !MatchStateAssignment(block.Instructions[pos], out newState)) + return false; + pos--; + + if (pos >= 0 && block.Instructions[pos].MatchStLoc(cachedStateVar, out var cachedStateNewValue)) { + if (StackSlotValue(cachedStateNewValue).MatchLdcI4(newState)) { + pos--; // OK, ignore V_0 store + } else { + return false; + } + } + + while (pos >= 0 && block.Instructions[pos] is StLoc stloc) { + if (stloc.Variable.Kind != VariableKind.StackSlot) + return false; + if (!SemanticHelper.IsPure(stloc.Value.Flags)) + return false; + } + + if (pos < 0 || !block.Instructions[pos].MatchStFld(out var target, out var field, out yieldValue)) + return false; + if (!StackSlotValue(target).MatchLdThis()) + return false; + // TODO: check that we are accessing the current field (compare with get_Current) + + block.Instructions.RemoveRange(pos, block.Instructions.Count - pos); + return true; + } #endregion #region DetectAwaitPattern diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs index bd1c76287..7df129a49 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs @@ -197,6 +197,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow { public static void Run(ILFunction function, ILTransformContext context) { + if (!context.Settings.AwaitInCatchFinally) + return; HashSet changedContainers = new HashSet(); // analyze all try-catch statements in the function diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs index bcd9e047b..a4e90674b 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs @@ -305,6 +305,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow /// bool MatchEnumeratorCreationNewObj(ILInstruction inst) { + return MatchEnumeratorCreationNewObj(inst, metadata, currentType, + out enumeratorCtor, out enumeratorType); + } + + internal static bool MatchEnumeratorCreationNewObj(ILInstruction inst, + MetadataReader metadata, TypeDefinitionHandle currentType, + out MethodDefinitionHandle enumeratorCtor, out TypeDefinitionHandle enumeratorType) + { + enumeratorCtor = default; + enumeratorType = default; // newobj(CurrentType/...::.ctor, ldc.i4(-2)) if (!(inst is NewObj newObj)) return false; diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 3283a7103..d51ba0b1f 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -556,6 +556,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Decompile async IAsyncEnumerator methods. + /// + public static string DecompilerSettings_AsyncEnumerator { + get { + return ResourceManager.GetString("DecompilerSettings.AsyncEnumerator", resourceCulture); + } + } + /// /// Looks up a localized string similar to Decompile ?. and ?[] operators. /// @@ -810,6 +819,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Read-only methods. + /// + public static string DecompilerSettings_ReadOnlyMethods { + get { + return ResourceManager.GetString("DecompilerSettings.ReadOnlyMethods", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove dead and side effect free code (use with caution!). /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index eb20f2e6e..dbe93bffd 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -769,4 +769,10 @@ Are you sure you want to continue? Decompile to new tab + + Decompile async IAsyncEnumerator methods + + + Read-only methods + \ No newline at end of file