mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1786 lines
62 KiB
1786 lines
62 KiB
// Copyright (c) 2017 Daniel Grunwald |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this |
|
// software and associated documentation files (the "Software"), to deal in the Software |
|
// without restriction, including without limitation the rights to use, copy, modify, merge, |
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
|
// to whom the Software is furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in all copies or |
|
// substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
// DEALINGS IN THE SOFTWARE. |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using System.Diagnostics; |
|
using System.Linq; |
|
using System.Reflection.Metadata; |
|
|
|
using ICSharpCode.Decompiler.CSharp; |
|
using ICSharpCode.Decompiler.DebugInfo; |
|
using ICSharpCode.Decompiler.IL.Transforms; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.TypeSystem.Implementation; |
|
using ICSharpCode.Decompiler.Util; |
|
|
|
namespace ICSharpCode.Decompiler.IL.ControlFlow |
|
{ |
|
/// <summary> |
|
/// Decompiler step for C# 5 async/await. |
|
/// </summary> |
|
public class AsyncAwaitDecompiler : IILTransform |
|
{ |
|
internal static bool IsCompilerGeneratedStateMachine(TypeDefinitionHandle type, MetadataReader metadata) |
|
{ |
|
TypeDefinition td; |
|
if (type.IsNil || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil) |
|
return false; |
|
if (!(type.IsCompilerGenerated(metadata) || td.GetDeclaringType().IsCompilerGenerated(metadata))) |
|
return false; |
|
foreach (var i in td.GetInterfaceImplementations()) |
|
{ |
|
var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata); |
|
if (!tr.IsNested && tr.TopLevelTypeName.Namespace == "System.Runtime.CompilerServices" && tr.TopLevelTypeName.Name == "IAsyncStateMachine") |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
internal static bool IsCompilerGeneratedMainMethod(Metadata.PEFile module, MethodDefinitionHandle method) |
|
{ |
|
var metadata = module.Metadata; |
|
var definition = metadata.GetMethodDefinition(method); |
|
var entrypoint = System.Reflection.Metadata.Ecma335.MetadataTokens.MethodDefinitionHandle(module.Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress); |
|
return method == entrypoint && metadata.GetString(definition.Name).Equals("<Main>", StringComparison.Ordinal); |
|
} |
|
|
|
enum AsyncMethodType |
|
{ |
|
Void, |
|
Task, |
|
TaskOfT, |
|
AsyncEnumerator, |
|
AsyncEnumerable |
|
} |
|
|
|
ILTransformContext context; |
|
|
|
// 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; |
|
IType builderType; |
|
IField builderField; |
|
IField stateField; |
|
int initialState; |
|
Dictionary<IField, ILVariable> fieldToParameterMap = new Dictionary<IField, ILVariable>(); |
|
Dictionary<ILVariable, ILVariable> cachedFieldToParameterMap = new Dictionary<ILVariable, ILVariable>(); |
|
IField disposeModeField; // 'disposeMode' field (IAsyncEnumerable/IAsyncEnumerator only) |
|
|
|
// These fields are set by AnalyzeMoveNext(): |
|
ILFunction moveNextFunction; |
|
ILVariable cachedStateVar; // variable in MoveNext that caches the stateField. |
|
TryCatch mainTryCatch; |
|
Block setResultReturnBlock; // block that is jumped to for return statements |
|
// Note: for async enumerators, a jump to setResultReturnBlock 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(): |
|
int smallestAwaiterVarIndex; |
|
HashSet<Leave> moveNextLeaves = new HashSet<Leave>(); |
|
|
|
// For each block containing an 'await', stores the awaiter variable, and the field storing the awaiter |
|
// across the yield point. |
|
Dictionary<Block, (ILVariable awaiterVar, IField awaiterField)> awaitBlocks = new Dictionary<Block, (ILVariable awaiterVar, IField awaiterField)>(); |
|
|
|
int catchHandlerOffset; |
|
List<AsyncDebugInfo.Await> awaitDebugInfos = new List<AsyncDebugInfo.Await>(); |
|
|
|
public void Run(ILFunction function, ILTransformContext context) |
|
{ |
|
if (!context.Settings.AsyncAwait) |
|
return; // abort if async/await decompilation is disabled |
|
this.context = context; |
|
fieldToParameterMap.Clear(); |
|
cachedFieldToParameterMap.Clear(); |
|
awaitBlocks.Clear(); |
|
awaitDebugInfos.Clear(); |
|
moveNextLeaves.Clear(); |
|
if (!MatchTaskCreationPattern(function) && !MatchAsyncEnumeratorCreationPattern(function)) |
|
return; |
|
try |
|
{ |
|
AnalyzeMoveNext(); |
|
ValidateCatchBlock(); |
|
AnalyzeDisposeAsync(); |
|
} |
|
catch (SymbolicAnalysisFailedException) |
|
{ |
|
return; |
|
} |
|
|
|
InlineBodyOfMoveNext(function); |
|
function.CheckInvariant(ILPhase.InAsyncAwait); |
|
CleanUpBodyOfMoveNext(function); |
|
function.CheckInvariant(ILPhase.InAsyncAwait); |
|
|
|
AnalyzeStateMachine(function); |
|
DetectAwaitPattern(function); |
|
CleanDoFinallyBodies(function); |
|
|
|
context.Step("Translate fields to local accesses", function); |
|
YieldReturnDecompiler.TranslateFieldsToLocalAccess(function, function, fieldToParameterMap); |
|
TranslateCachedFieldsToLocals(); |
|
|
|
FinalizeInlineMoveNext(function); |
|
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. |
|
function.RunTransforms(CSharpDecompiler.EarlyILTransforms(), context); |
|
|
|
AwaitInCatchTransform.Run(function, context); |
|
AwaitInFinallyTransform.Run(function, context); |
|
|
|
awaitDebugInfos.SortBy(row => row.YieldOffset); |
|
function.AsyncDebugInfo = new AsyncDebugInfo(catchHandlerOffset, awaitDebugInfos.ToImmutableArray()); |
|
} |
|
|
|
private void CleanUpBodyOfMoveNext(ILFunction function) |
|
{ |
|
context.StepStartGroup("CleanUpBodyOfMoveNext", function); |
|
// Copy-propagate stack slots holding an 'ldloca': |
|
foreach (var stloc in function.Descendants.OfType<StLoc>().Where(s => s.Variable.Kind == VariableKind.StackSlot && s.Variable.IsSingleDefinition && s.Value is LdLoca).ToList()) |
|
{ |
|
CopyPropagation.Propagate(stloc, context); |
|
} |
|
|
|
// Simplify stobj(ldloca) -> stloc |
|
foreach (var stobj in function.Descendants.OfType<StObj>()) |
|
{ |
|
EarlyExpressionTransforms.StObjToStLoc(stobj, context); |
|
} |
|
|
|
// Copy-propagate temporaries holding a copy of 'this'. |
|
foreach (var stloc in function.Descendants.OfType<StLoc>().Where(s => s.Variable.IsSingleDefinition && s.Value.MatchLdThis()).ToList()) |
|
{ |
|
CopyPropagation.Propagate(stloc, context); |
|
} |
|
new RemoveDeadVariableInit().Run(function, context); |
|
foreach (var block in function.Descendants.OfType<Block>()) |
|
{ |
|
// Run inlining, but don't remove dead variables (they might get revived by TranslateFieldsToLocalAccess) |
|
ILInlining.InlineAllInBlock(function, block, InliningOptions.None, context); |
|
if (IsAsyncEnumerator) |
|
{ |
|
// Remove lone 'ldc.i4', those are sometimes left over after C# compiler |
|
// optimizes out stores to the state variable. |
|
block.Instructions.RemoveAll(inst => inst.OpCode == OpCode.LdcI4); |
|
} |
|
} |
|
context.StepEndGroup(); |
|
} |
|
|
|
#region MatchTaskCreationPattern |
|
bool MatchTaskCreationPattern(ILFunction function) |
|
{ |
|
if (!(function.Body is BlockContainer blockContainer)) |
|
return false; |
|
if (blockContainer.Blocks.Count != 1) |
|
return false; |
|
var body = blockContainer.EntryPoint.Instructions; |
|
if (body.Count < 4) |
|
return false; |
|
/* Example: |
|
V_0 is an instance of the compiler-generated struct/class, |
|
V_1 is an instance of the builder struct/class |
|
Block IL_0000 (incoming: 1) { |
|
... |
|
stobj AsyncVoidMethodBuilder(ldflda [Field Async+<AwaitYield>d__3.<>t__builder](ldloca V_0), call Create()) |
|
stobj System.Int32(ldflda [Field Async+<AwaitYield>d__3.<>1__state](ldloca V_0), ldc.i4 -1) |
|
stloc V_1(ldobj System.Runtime.CompilerServices.AsyncVoidMethodBuilder(ldflda [Field Async+<AwaitYield>d__3.<>t__builder](ldloc V_0))) |
|
call Start(ldloca V_1, ldloca V_0) |
|
leave IL_0000 (or ret for non-void async methods) |
|
} |
|
With custom task types, it's possible that the builder is a reference type. |
|
In that case, the variable V_1 may be inlined. |
|
*/ |
|
|
|
// Check the second-to-last instruction (the start call) first, as we can get the most information from that |
|
int pos = body.Count - 2; |
|
if (!(body[pos] is CallInstruction startCall)) |
|
return false; |
|
if (startCall.Method.Name != "Start") |
|
return false; |
|
taskType = function.Method.ReturnType; |
|
builderType = startCall.Method.DeclaringType; |
|
FullTypeName builderTypeName; |
|
if (builderType?.GetDefinition() is { } builderTypeDef) |
|
{ |
|
builderTypeName = builderTypeDef.FullTypeName; |
|
} |
|
else if (builderType is UnknownType unknownBuilderType) |
|
{ |
|
builderTypeName = unknownBuilderType.FullTypeName; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
if (taskType.IsKnownType(KnownTypeCode.Void)) |
|
{ |
|
methodType = AsyncMethodType.Void; |
|
underlyingReturnType = taskType; |
|
if (builderTypeName != new TopLevelTypeName("System.Runtime.CompilerServices", "AsyncVoidMethodBuilder")) |
|
return false; |
|
} |
|
else if (TaskType.IsNonGenericTaskType(taskType, out var builderTypeNameFromTask)) |
|
{ |
|
methodType = AsyncMethodType.Task; |
|
underlyingReturnType = context.TypeSystem.FindType(KnownTypeCode.Void); |
|
if (builderTypeNameFromTask != builderTypeName) |
|
return false; |
|
} |
|
else if (TaskType.IsGenericTaskType(taskType, out builderTypeNameFromTask)) |
|
{ |
|
methodType = AsyncMethodType.TaskOfT; |
|
if (taskType.IsKnownType(KnownTypeCode.TaskOfT)) |
|
underlyingReturnType = TaskType.UnpackTask(context.TypeSystem, taskType); |
|
else |
|
underlyingReturnType = startCall.Method.DeclaringType.TypeArguments[0]; |
|
if (builderTypeNameFromTask != builderTypeName) |
|
return false; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
if (startCall.Arguments.Count != 2) |
|
return false; |
|
ILInstruction loadBuilderExpr = startCall.Arguments[0]; |
|
if (!startCall.Arguments[1].MatchLdLoca(out ILVariable stateMachineVar)) |
|
return false; |
|
stateMachineType = stateMachineVar.Type.GetDefinition(); |
|
if (stateMachineType == null) |
|
return false; |
|
pos--; |
|
|
|
if (loadBuilderExpr.MatchLdLocRef(out ILVariable builderVar)) |
|
{ |
|
// Check third-to-last instruction (copy of builder) |
|
// stloc builder(ldfld StateMachine::<>t__builder(ldloc stateMachine)) |
|
if (!body[pos].MatchStLoc(builderVar, out loadBuilderExpr)) |
|
return false; |
|
pos--; |
|
} |
|
if (loadBuilderExpr.MatchLdFld(out var loadStateMachineForBuilderExpr, out builderField)) |
|
{ |
|
// OK, calling Start on copy of stateMachine.<>t__builder |
|
} |
|
else if (loadBuilderExpr.MatchLdFlda(out loadStateMachineForBuilderExpr, out builderField)) |
|
{ |
|
// OK, Roslyn 3.6 started directly calling Start without making a copy |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
builderField = (IField)builderField.MemberDefinition; |
|
if (!(loadStateMachineForBuilderExpr.MatchLdLocRef(stateMachineVar) || loadStateMachineForBuilderExpr.MatchLdLoc(stateMachineVar))) |
|
return false; |
|
|
|
// Check the last instruction (ret) |
|
if (methodType == AsyncMethodType.Void) |
|
{ |
|
if (!body.Last().MatchLeave(blockContainer)) |
|
return false; |
|
} |
|
else |
|
{ |
|
// ret(call(AsyncTaskMethodBuilder::get_Task, ldflda(StateMachine::<>t__builder, ldloca(stateMachine)))) |
|
if (!body.Last().MatchReturn(out var returnValue)) |
|
return false; |
|
if (!MatchCall(returnValue, "get_Task", out var getTaskArgs) || getTaskArgs.Count != 1) |
|
return false; |
|
ILInstruction target; |
|
IField builderField2; |
|
if (builderType.IsReferenceType == true) |
|
{ |
|
if (!getTaskArgs[0].MatchLdFld(out target, out builderField2)) |
|
return false; |
|
} |
|
else |
|
{ |
|
if (!getTaskArgs[0].MatchLdFlda(out target, out builderField2)) |
|
return false; |
|
} |
|
if (builderField2.MemberDefinition != builderField) |
|
return false; |
|
if (!(target.MatchLdLoc(stateMachineVar) || target.MatchLdLoca(stateMachineVar))) |
|
return false; |
|
} |
|
|
|
// Check the last field assignment - this should be the state field |
|
// stfld <>1__state(ldloca stateField, ldc.i4 -1) |
|
if (!MatchStFld(body[pos], stateMachineVar, out stateField, out var initialStateExpr)) |
|
return false; |
|
if (!initialStateExpr.MatchLdcI4(out initialState)) |
|
return false; |
|
if (initialState != -1) |
|
return false; |
|
|
|
int stopPos = pos; |
|
pos = 0; |
|
if (stateMachineType.Kind == TypeKind.Class) |
|
{ |
|
// If state machine is a class, the first instruction creates an instance: |
|
// stloc stateMachine(newobj StateMachine.ctor()) |
|
if (!body[pos].MatchStLoc(stateMachineVar, out var init)) |
|
return false; |
|
if (!(init is NewObj newobj && newobj.Arguments.Count == 0 && newobj.Method.DeclaringTypeDefinition == stateMachineType)) |
|
return false; |
|
pos++; |
|
} |
|
bool builderFieldIsInitialized = false; |
|
for (; pos < stopPos; pos++) |
|
{ |
|
// stfld StateMachine.field(ldloca stateMachine, ldvar(param)) |
|
if (!MatchStFld(body[pos], stateMachineVar, out var field, out var fieldInit)) |
|
return false; |
|
if (field == builderField) |
|
{ |
|
// stfld StateMachine.builder(ldloca stateMachine, call Create()) |
|
if (!(fieldInit is Call { Method: { Name: "Create" }, Arguments: { Count: 0 } })) |
|
return false; |
|
builderFieldIsInitialized = true; |
|
} |
|
else if (fieldInit.MatchLdLoc(out var v) && v.Kind == VariableKind.Parameter) |
|
{ |
|
// OK, copies parameter into state machine |
|
fieldToParameterMap[field] = v; |
|
} |
|
else if (fieldInit is LdObj ldobj && ldobj.Target.MatchLdThis()) |
|
{ |
|
// stfld <>4__this(ldloc stateMachine, ldobj AsyncInStruct(ldloc this)) |
|
fieldToParameterMap[field] = ((LdLoc)ldobj.Target).Variable; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
} |
|
|
|
return builderFieldIsInitialized; |
|
} |
|
|
|
/// <summary> |
|
/// Matches a (potentially virtual) instance method call. |
|
/// </summary> |
|
static bool MatchCall(ILInstruction inst, string name, out InstructionCollection<ILInstruction> args) |
|
{ |
|
if (inst is CallInstruction call && (call.OpCode == OpCode.Call || call.OpCode == OpCode.CallVirt) |
|
&& call.Method.Name == name && !call.Method.IsStatic) |
|
{ |
|
args = call.Arguments; |
|
return args.Count > 0; |
|
} |
|
args = null; |
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Matches a store to the state machine. |
|
/// </summary> |
|
static bool MatchStFld(ILInstruction stfld, ILVariable stateMachineVar, out IField field, out ILInstruction value) |
|
{ |
|
if (!stfld.MatchStFld(out var target, out field, out value)) |
|
return false; |
|
field = field.MemberDefinition as IField; |
|
return field != null && target.MatchLdLocRef(stateMachineVar); |
|
} |
|
#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 builderType, out stateField); |
|
} |
|
catch (SymbolicAnalysisFailedException) |
|
{ |
|
return false; |
|
} |
|
return true; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
} |
|
else |
|
{ |
|
|
|
// stloc v(newobj<CountUpSlowly> d__0..ctor(ldc.i4 - 2)) |
|
// stfld <>4__this(ldloc v, ldloc this) |
|
// stfld <>3__otherParam(ldloc v, ldloc otherParam) |
|
// leave IL_0000(ldloc v) |
|
int pos = 0; |
|
if (!body.Instructions[pos].MatchStLoc(out var v, out var newObj)) |
|
return false; |
|
if (!MatchEnumeratorCreationNewObj(newObj, context, out initialState, out stateMachineType)) |
|
return false; |
|
pos++; |
|
|
|
while (MatchStFld(body.Instructions[pos], v, out var field, out var value)) |
|
{ |
|
if (value.MatchLdLoc(out var p) && p.Kind == VariableKind.Parameter) |
|
{ |
|
fieldToParameterMap[field] = p; |
|
} |
|
else if (value is LdObj ldobj && ldobj.Target.MatchLdThis()) |
|
{ |
|
fieldToParameterMap[field] = ((LdLoc)ldobj.Target).Variable; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
pos++; |
|
} |
|
if (!body.Instructions[pos].MatchReturn(out var returnValue)) |
|
return false; |
|
if (!returnValue.MatchLdLoc(v)) |
|
return false; |
|
|
|
// 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 builderType, out stateField); |
|
if (methodType == AsyncMethodType.AsyncEnumerable) |
|
{ |
|
ResolveIEnumerableIEnumeratorFieldMapping(); |
|
} |
|
} |
|
catch (SymbolicAnalysisFailedException) |
|
{ |
|
return false; |
|
} |
|
return true; |
|
} |
|
} |
|
|
|
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 IType builderType, 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) |
|
// } |
|
foreach (var inst in body.Instructions) |
|
{ |
|
if (inst.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; |
|
} |
|
if (inst.MatchStFld(out target, out field, out value) |
|
&& target.MatchLdThis() |
|
&& value is Call call && call.Method.Name == "Create") |
|
{ |
|
builderField = (IField)field.MemberDefinition; |
|
} |
|
} |
|
if (stateField == null || builderField == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
|
|
builderType = builderField.Type; |
|
if (builderType == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
|
|
private void ResolveIEnumerableIEnumeratorFieldMapping() |
|
{ |
|
var getAsyncEnumerator = stateMachineType.Methods.FirstOrDefault(m => m.Name.EndsWith(".GetAsyncEnumerator", StringComparison.Ordinal)); |
|
if (getAsyncEnumerator == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
YieldReturnDecompiler.ResolveIEnumerableIEnumeratorFieldMapping((MethodDefinitionHandle)getAsyncEnumerator.MetadataToken, context, fieldToParameterMap); |
|
} |
|
#endregion |
|
|
|
#region AnalyzeMoveNext |
|
/// <summary> |
|
/// First peek into MoveNext(); analyzes everything outside the big try-catch. |
|
/// </summary> |
|
void AnalyzeMoveNext() |
|
{ |
|
if (stateMachineType.MetadataToken.IsNil) |
|
throw new SymbolicAnalysisFailedException(); |
|
var metadata = context.PEFile.Metadata; |
|
var moveNextMethod = metadata.GetTypeDefinition((TypeDefinitionHandle)stateMachineType.MetadataToken) |
|
.GetMethods().FirstOrDefault(f => metadata.GetString(metadata.GetMethodDefinition(f).Name) == "MoveNext"); |
|
if (moveNextMethod == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
moveNextFunction = YieldReturnDecompiler.CreateILAst(moveNextMethod, context); |
|
if (!(moveNextFunction.Body is BlockContainer blockContainer)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (blockContainer.EntryPoint.IncomingEdgeCount != 1) |
|
throw new SymbolicAnalysisFailedException(); |
|
bool[] blocksAnalyzed = new bool[blockContainer.Blocks.Count]; |
|
cachedStateVar = null; |
|
int pos = 0; |
|
while (blockContainer.EntryPoint.Instructions[pos] is StLoc stloc) |
|
{ |
|
// stloc V_1(ldfld <>4__this(ldloc this)) |
|
if (!stloc.Value.MatchLdFld(out var target, out var field)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!target.MatchLdThis()) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (field.MemberDefinition == stateField && cachedStateVar == null) |
|
{ |
|
// stloc(cachedState, ldfld(valuetype StateMachineStruct::<>1__state, ldloc(this))) |
|
cachedStateVar = stloc.Variable; |
|
} |
|
else if (fieldToParameterMap.TryGetValue((IField)field.MemberDefinition, out var param)) |
|
{ |
|
if (!stloc.Variable.IsSingleDefinition) |
|
throw new SymbolicAnalysisFailedException(); |
|
cachedFieldToParameterMap[stloc.Variable] = param; |
|
} |
|
else |
|
{ |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
pos++; |
|
} |
|
mainTryCatch = blockContainer.EntryPoint.Instructions[pos] as TryCatch; |
|
if (mainTryCatch == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
// CatchHandler will be validated in ValidateCatchBlock() |
|
|
|
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)) |
|
{ |
|
doFinallyBodies = initDoFinallyBodies.Variable; |
|
} |
|
|
|
Debug.Assert(blockContainer.Blocks[0] == blockContainer.EntryPoint); // already checked this block |
|
blocksAnalyzed[0] = true; |
|
pos = 1; |
|
if (MatchYieldBlock(blockContainer, pos)) |
|
{ |
|
setResultYieldBlock = blockContainer.Blocks[pos]; |
|
blocksAnalyzed[pos] = true; |
|
pos++; |
|
} |
|
else |
|
{ |
|
setResultYieldBlock = null; |
|
} |
|
|
|
setResultReturnBlock = CheckSetResultReturnBlock(blockContainer, pos, blocksAnalyzed); |
|
|
|
if (!blocksAnalyzed.All(b => b)) |
|
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); |
|
if (block == null) |
|
return false; |
|
// 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); |
|
} |
|
|
|
private Block CheckSetResultReturnBlock(BlockContainer blockContainer, int setResultReturnBlockIndex, bool[] blocksAnalyzed) |
|
{ |
|
if (setResultReturnBlockIndex >= blockContainer.Blocks.Count) |
|
{ |
|
// This block can be absent if the function never exits normally, |
|
// but always throws an exception/loops infinitely. |
|
resultVar = null; |
|
finalStateKnown = false; // final state will be detected in ValidateCatchBlock() instead |
|
return null; |
|
} |
|
|
|
var block = blockContainer.Blocks[setResultReturnBlockIndex]; |
|
// stfld <>1__state(ldloc this, ldc.i4 -2) |
|
int pos = 0; |
|
if (!MatchStateAssignment(block.Instructions[pos], out finalState)) |
|
throw new SymbolicAnalysisFailedException(); |
|
finalStateKnown = true; |
|
pos++; |
|
|
|
if (pos + 2 == block.Instructions.Count && block.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) |
|
{ |
|
if (MatchDisposeCombinedTokens(blockContainer, condition, trueInst, falseInst, blocksAnalyzed, out var setResultAndExitBlock)) |
|
{ |
|
blocksAnalyzed[block.ChildIndex] = true; |
|
block = setResultAndExitBlock; |
|
pos = 0; |
|
} |
|
else |
|
{ |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
} |
|
|
|
// [optional] stfld <>u__N(ldloc this, ldnull) |
|
MatchHoistedLocalCleanup(block, ref pos); |
|
CheckSetResultAndExit(blockContainer, block, ref pos); |
|
blocksAnalyzed[block.ChildIndex] = true; |
|
return blockContainer.Blocks[setResultReturnBlockIndex]; |
|
} |
|
|
|
private bool MatchDisposeCombinedTokens(BlockContainer blockContainer, ILInstruction condition, ILInstruction trueInst, ILInstruction falseInst, bool[] blocksAnalyzed, out Block setResultAndExitBlock) |
|
{ |
|
setResultAndExitBlock = null; |
|
// ... |
|
// if (comp.o(ldfld <>x__combinedTokens(ldloc this) == ldnull)) br setResultAndExit |
|
// br disposeCombinedTokens |
|
// } |
|
// |
|
// Block disposeCombinedTokens (incoming: 1) { |
|
// callvirt Dispose(ldfld <>x__combinedTokens(ldloc this)) |
|
// stfld <>x__combinedTokens(ldloc this, ldnull) |
|
// br setResultAndExit |
|
// } |
|
if (!condition.MatchCompEqualsNull(out var testedInst)) |
|
return false; |
|
if (!testedInst.MatchLdFld(out var target, out var ctsField)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (!(ctsField.Type is ITypeDefinition { FullTypeName: { IsNested: false, TopLevelTypeName: { Name: "CancellationTokenSource", Namespace: "System.Threading" } } })) |
|
return false; |
|
if (!(trueInst.MatchBranch(out setResultAndExitBlock) && falseInst.MatchBranch(out var disposedCombinedTokensBlock))) |
|
return false; |
|
if (!(setResultAndExitBlock.Parent == blockContainer && disposedCombinedTokensBlock.Parent == blockContainer)) |
|
return false; |
|
|
|
var block = disposedCombinedTokensBlock; |
|
int pos = 0; |
|
// callvirt Dispose(ldfld <>x__combinedTokens(ldloc this)) |
|
if (!(block.Instructions[pos] is CallVirt { Method: { Name: "Dispose" } } disposeCall)) |
|
return false; |
|
if (disposeCall.Arguments.Count != 1) |
|
return false; |
|
if (!disposeCall.Arguments[0].MatchLdFld(out var target2, out var ctsField2)) |
|
return false; |
|
if (!(target2.MatchLdThis() && ctsField2.Equals(ctsField))) |
|
return false; |
|
pos++; |
|
// stfld <>x__combinedTokens(ldloc this, ldnull) |
|
if (!block.Instructions[pos].MatchStFld(out var target3, out var ctsField3, out var storedValue)) |
|
return false; |
|
if (!(target3.MatchLdThis() && ctsField3.Equals(ctsField))) |
|
return false; |
|
if (!storedValue.MatchLdNull()) |
|
return false; |
|
pos++; |
|
// br setResultAndExit |
|
if (!block.Instructions[pos].MatchBranch(setResultAndExitBlock)) |
|
return false; |
|
blocksAnalyzed[block.ChildIndex] = true; |
|
|
|
return true; |
|
} |
|
|
|
private void MatchHoistedLocalCleanup(Block block, ref int pos) |
|
{ |
|
while (block.Instructions[pos].MatchStFld(out var target, out _, out var value)) |
|
{ |
|
// https://github.com/dotnet/roslyn/pull/39735 hoisted local cleanup |
|
if (!target.MatchLdThis()) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!value.MatchDefaultOrNullOrZero()) |
|
throw new SymbolicAnalysisFailedException(); |
|
pos++; |
|
} |
|
} |
|
|
|
void CheckSetResultAndExit(BlockContainer blockContainer, Block block, ref int pos) |
|
{ |
|
// [optional] call Complete(ldflda <>t__builder(ldloc this)) (Roslyn >=3.9) |
|
// call SetResult(ldflda <>t__builder(ldloc this), ldloc result) |
|
// [optional] call Complete(ldflda <>t__builder(ldloc this)) |
|
// leave IL_0000 |
|
MatchCompleteCall(block, ref pos); |
|
if (!MatchCall(block.Instructions[pos], "SetResult", out var args)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!IsBuilderOrPromiseFieldOnThis(args[0])) |
|
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; |
|
} |
|
pos++; |
|
MatchCompleteCall(block, ref pos); |
|
if (!block.Instructions[pos].MatchLeave(blockContainer)) |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
|
|
void ValidateCatchBlock() |
|
{ |
|
// catch E_143 : System.Exception if (ldc.i4 1) BlockContainer { |
|
// Block IL_008f (incoming: 1) { |
|
// stloc exception(ldloc E_143) |
|
// stfld <>1__state(ldloc this, ldc.i4 -2) |
|
// [optional] stfld <>u__N(ldloc this, ldnull) |
|
// call SetException(ldfld <>t__builder(ldloc this), ldloc exception) |
|
// [optional] call Complete(ldfld <>t__builder(ldloc this)) |
|
// leave IL_0000 |
|
// } |
|
// } |
|
if (mainTryCatch?.Handlers.Count != 1) |
|
throw new SymbolicAnalysisFailedException(); |
|
var handler = mainTryCatch.Handlers[0]; |
|
if (!handler.Variable.Type.IsKnownType(KnownTypeCode.Exception)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!handler.Filter.MatchLdcI4(1)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!(handler.Body is BlockContainer handlerContainer)) |
|
throw new SymbolicAnalysisFailedException(); |
|
bool[] blocksAnalyzed = new bool[handlerContainer.Blocks.Count]; |
|
var catchBlock = handlerContainer.EntryPoint; |
|
catchHandlerOffset = catchBlock.StartILOffset; |
|
// stloc exception(ldloc E_143) |
|
if (!(catchBlock.Instructions[0] is StLoc stloc)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!stloc.Value.MatchLdLoc(handler.Variable)) |
|
throw new SymbolicAnalysisFailedException(); |
|
// stfld <>1__state(ldloc this, ldc.i4 -2) |
|
if (!MatchStateAssignment(catchBlock.Instructions[1], out int newState)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (finalStateKnown) |
|
{ |
|
if (newState != finalState) |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
else |
|
{ |
|
finalState = newState; |
|
finalStateKnown = true; |
|
} |
|
int pos = 2; |
|
if (pos + 2 == catchBlock.Instructions.Count && catchBlock.MatchIfAtEndOfBlock(out var condition, out var trueInst, out var falseInst)) |
|
{ |
|
if (MatchDisposeCombinedTokens(handlerContainer, condition, trueInst, falseInst, blocksAnalyzed, out var setResultAndExitBlock)) |
|
{ |
|
blocksAnalyzed[catchBlock.ChildIndex] = true; |
|
catchBlock = setResultAndExitBlock; |
|
pos = 0; |
|
} |
|
else |
|
{ |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
} |
|
MatchHoistedLocalCleanup(catchBlock, ref pos); |
|
// [optional] call Complete(ldfld <>t__builder(ldloc this)) |
|
MatchCompleteCall(catchBlock, ref pos); |
|
|
|
// call SetException(ldfld <>t__builder(ldloc this), ldloc exception) |
|
if (!MatchCall(catchBlock.Instructions[pos], "SetException", out var args)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (args.Count != 2) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!IsBuilderOrPromiseFieldOnThis(args[0])) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (!args[1].MatchLdLoc(stloc.Variable)) |
|
throw new SymbolicAnalysisFailedException(); |
|
|
|
pos++; |
|
// [optional] call Complete(ldfld <>t__builder(ldloc this)) |
|
MatchCompleteCall(catchBlock, ref pos); |
|
|
|
// leave IL_0000 |
|
if (!catchBlock.Instructions[pos].MatchLeave((BlockContainer)moveNextFunction.Body)) |
|
throw new SymbolicAnalysisFailedException(); |
|
blocksAnalyzed[catchBlock.ChildIndex] = true; |
|
if (!blocksAnalyzed.All(b => b)) |
|
throw new SymbolicAnalysisFailedException(); |
|
} |
|
|
|
private void MatchCompleteCall(Block block, ref int pos) |
|
{ |
|
if (MatchCall(block.Instructions[pos], "Complete", out var args)) |
|
{ |
|
if (!(args.Count == 1 && IsBuilderFieldOnThis(args[0]))) |
|
throw new SymbolicAnalysisFailedException(); |
|
pos++; |
|
} |
|
} |
|
|
|
bool IsBuilderFieldOnThis(ILInstruction inst) |
|
{ |
|
IField field; |
|
ILInstruction target; |
|
if (builderType.IsReferenceType == true) |
|
{ |
|
// ldfld(StateMachine::<>t__builder, ldloc(this)) |
|
if (!inst.MatchLdFld(out target, out field)) |
|
return false; |
|
} |
|
else |
|
{ |
|
// ldflda(StateMachine::<>t__builder, ldloc(this)) |
|
if (!inst.MatchLdFlda(out target, out field)) |
|
return false; |
|
} |
|
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) |
|
&& StackSlotValue(target).MatchLdThis() |
|
&& field.MemberDefinition == stateField |
|
&& StackSlotValue(value).MatchLdcI4(out newState)) |
|
{ |
|
return true; |
|
} |
|
newState = 0; |
|
return false; |
|
} |
|
|
|
/// <summary> |
|
/// Analyse the DisposeAsync() method in order to find the disposeModeField. |
|
/// </summary> |
|
private void AnalyzeDisposeAsync() |
|
{ |
|
disposeModeField = null; |
|
if (!IsAsyncEnumerator) |
|
{ |
|
return; |
|
} |
|
var disposeAsync = stateMachineType.Methods.FirstOrDefault(m => m.Name.EndsWith(".DisposeAsync", StringComparison.Ordinal)); |
|
if (disposeAsync == null) |
|
throw new SymbolicAnalysisFailedException("Could not find DisposeAsync()"); |
|
var disposeAsyncHandle = (MethodDefinitionHandle)disposeAsync.MetadataToken; |
|
var function = YieldReturnDecompiler.CreateILAst(disposeAsyncHandle, context); |
|
foreach (var store in function.Descendants) |
|
{ |
|
if (!store.MatchStFld(out var target, out var field, out var value)) |
|
continue; |
|
if (!target.MatchLdThis()) |
|
continue; |
|
if (!value.MatchLdcI4(1)) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (disposeModeField != null) |
|
throw new SymbolicAnalysisFailedException("Multiple stores to disposeMode in DisposeAsync()"); |
|
disposeModeField = (IField)field.MemberDefinition; |
|
} |
|
} |
|
#endregion |
|
|
|
#region InlineBodyOfMoveNext |
|
void InlineBodyOfMoveNext(ILFunction function) |
|
{ |
|
context.Step("Inline body of MoveNext()", function); |
|
function.Body = mainTryCatch.TryBlock; |
|
function.AsyncReturnType = underlyingReturnType; |
|
function.MoveNextMethod = moveNextFunction.Method; |
|
function.SequencePointCandidates = moveNextFunction.SequencePointCandidates; |
|
function.CodeSize = moveNextFunction.CodeSize; |
|
function.IsIterator = IsAsyncEnumerator; |
|
moveNextFunction.Variables.Clear(); |
|
moveNextFunction.ReleaseRef(); |
|
foreach (var branch in function.Descendants.OfType<Branch>()) |
|
{ |
|
if (branch.TargetBlock == setResultReturnBlock) |
|
{ |
|
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<Leave>()) |
|
{ |
|
if (leave.TargetContainer == moveNextFunction.Body) |
|
{ |
|
leave.TargetContainer = (BlockContainer)function.Body; |
|
moveNextLeaves.Add(leave); |
|
} |
|
} |
|
function.Variables.AddRange(function.Descendants.OfType<IInstructionWithVariableOperand>().Select(inst => inst.Variable).Distinct()); |
|
function.Variables.RemoveDead(); |
|
function.Variables.AddRange(fieldToParameterMap.Values); |
|
} |
|
|
|
void FinalizeInlineMoveNext(ILFunction function) |
|
{ |
|
context.Step("FinalizeInlineMoveNext()", function); |
|
foreach (var leave in function.Descendants.OfType<Leave>()) |
|
{ |
|
if (moveNextLeaves.Contains(leave)) |
|
{ |
|
leave.ReplaceWith(new InvalidBranch { |
|
Message = "leave MoveNext - await not detected correctly" |
|
}.WithILRange(leave)); |
|
} |
|
} |
|
// Delete dead loads of the state cache variable: |
|
foreach (var block in function.Descendants.OfType<Block>()) |
|
{ |
|
for (int i = block.Instructions.Count - 1; i >= 0; i--) |
|
{ |
|
if (block.Instructions[i].MatchStLoc(out var v, out var value) |
|
&& v.IsSingleDefinition && v.LoadCount == 0 |
|
&& value.MatchLdLoc(cachedStateVar)) |
|
{ |
|
block.Instructions.RemoveAt(i); |
|
} |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
#region AnalyzeStateMachine |
|
|
|
/// <summary> |
|
/// Analyze the the state machine; and replace 'leave IL_0000' with await+jump to block that gets |
|
/// entered on the next MoveNext() call. |
|
/// </summary> |
|
void AnalyzeStateMachine(ILFunction function) |
|
{ |
|
context.StepStartGroup("AnalyzeStateMachine()", function); |
|
smallestAwaiterVarIndex = int.MaxValue; |
|
foreach (var container in function.Descendants.OfType<BlockContainer>()) |
|
{ |
|
// Use a separate state range analysis per container. |
|
var sra = new StateRangeAnalysis(StateRangeAnalysisMode.AsyncMoveNext, stateField, cachedStateVar); |
|
sra.CancellationToken = context.CancellationToken; |
|
sra.doFinallyBodies = doFinallyBodies; |
|
sra.AssignStateRanges(container, LongSet.Universe); |
|
var stateToBlockMap = sra.GetBlockStateSetMapping(container); |
|
|
|
foreach (var block in container.Blocks) |
|
{ |
|
context.CancellationToken.ThrowIfCancellationRequested(); |
|
if (block.Instructions.Last() is Leave leave && moveNextLeaves.Contains(leave)) |
|
{ |
|
// This is likely an 'await' block |
|
context.Step($"AnalyzeAwaitBlock({block.StartILOffset:x4})", block); |
|
if (AnalyzeAwaitBlock(block, out var awaiterVar, out var awaiterField, out int state, out int yieldOffset)) |
|
{ |
|
block.Instructions.Add(new Await(new LdLoca(awaiterVar))); |
|
Block targetBlock = stateToBlockMap.GetOrDefault(state); |
|
if (targetBlock != null) |
|
{ |
|
awaitDebugInfos.Add(new AsyncDebugInfo.Await(yieldOffset, targetBlock.StartILOffset)); |
|
block.Instructions.Add(new Branch(targetBlock)); |
|
} |
|
else |
|
{ |
|
block.Instructions.Add(new InvalidBranch("Could not find block for state " + state)); |
|
} |
|
awaitBlocks.Add(block, (awaiterVar, awaiterField)); |
|
if (awaiterVar.Index < smallestAwaiterVarIndex) |
|
{ |
|
smallestAwaiterVarIndex = awaiterVar.Index.Value; |
|
} |
|
} |
|
} |
|
else if (block.Instructions.Last().MatchBranch(setResultYieldBlock)) |
|
{ |
|
// This is a 'yield return' in an async enumerator. |
|
context.Step($"AnalyzeYieldReturn({block.StartILOffset:x4})", block); |
|
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'")); |
|
} |
|
} |
|
TransformYieldBreak(block); |
|
} |
|
foreach (var block in container.Blocks) |
|
{ |
|
SimplifyIfDisposeMode(block); |
|
} |
|
// Skip the state dispatcher and directly jump to the initial state |
|
var entryPoint = stateToBlockMap.GetOrDefault(initialState); |
|
if (entryPoint != null) |
|
{ |
|
container.Blocks.Insert(0, new Block { |
|
Instructions = { |
|
new Branch(entryPoint) |
|
} |
|
}); |
|
} |
|
container.SortBlocks(deleteUnreachableBlocks: true); |
|
} |
|
context.StepEndGroup(); |
|
} |
|
|
|
private bool TransformYieldBreak(Block block) |
|
{ |
|
// stfld disposeMode(ldloc this, ldc.i4 1) |
|
// br nextBlock |
|
if (block.Instructions.Count < 2) |
|
return false; |
|
if (!(block.Instructions.Last() is Branch branch)) |
|
return false; |
|
if (!block.Instructions[block.Instructions.Count - 2].MatchStFld(out var target, out var field, out var value)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (field.MemberDefinition != disposeModeField) |
|
return false; |
|
if (!value.MatchLdcI4(1)) |
|
return false; |
|
|
|
// Detected a 'yield break;' |
|
context.Step($"TransformYieldBreak({block.StartILOffset:x4})", block); |
|
var breakTarget = FindYieldBreakTarget(branch.TargetBlock); |
|
if (breakTarget is Block targetBlock) |
|
{ |
|
branch.TargetBlock = targetBlock; |
|
} |
|
else |
|
{ |
|
Debug.Assert(breakTarget is BlockContainer); |
|
branch.ReplaceWith(new Leave((BlockContainer)breakTarget).WithILRange(branch)); |
|
} |
|
return true; |
|
} |
|
|
|
ILInstruction FindYieldBreakTarget(Block block) |
|
{ |
|
// We'll follow the branch and evaluate the following instructions |
|
// under the assumption that disposeModeField==1, which lets us follow a series of jumps |
|
// to determine the final target. |
|
var visited = new HashSet<Block>(); |
|
var evalContext = new SymbolicEvaluationContext(disposeModeField); |
|
while (true) |
|
{ |
|
for (int i = 0; i < block.Instructions.Count; i++) |
|
{ |
|
ILInstruction inst = block.Instructions[i]; |
|
while (inst.MatchIfInstruction(out var condition, out var trueInst, out var falseInst)) |
|
{ |
|
var condVal = evalContext.Eval(condition).AsBool(); |
|
if (condVal.Type == SymbolicValueType.IntegerConstant) |
|
{ |
|
inst = condVal.Constant != 0 ? trueInst : falseInst; |
|
} |
|
else if (condVal.Type == SymbolicValueType.StateInSet) |
|
{ |
|
inst = condVal.ValueSet.Contains(1) ? trueInst : falseInst; |
|
} |
|
else |
|
{ |
|
return block; |
|
} |
|
} |
|
if (inst.MatchBranch(out var targetBlock)) |
|
{ |
|
if (visited.Add(block)) |
|
{ |
|
block = targetBlock; |
|
break; // continue with next block |
|
} |
|
else |
|
{ |
|
return block; // infinite loop detected |
|
} |
|
} |
|
else if (inst is Leave leave && leave.Value.OpCode == OpCode.Nop) |
|
{ |
|
return leave.TargetContainer; |
|
} |
|
else if (inst.OpCode == OpCode.Nop) |
|
{ |
|
continue; // continue with next instruction in this block |
|
} |
|
else |
|
{ |
|
return block; |
|
} |
|
} |
|
} |
|
} |
|
|
|
private bool SimplifyIfDisposeMode(Block block) |
|
{ |
|
// Occasionally Roslyn optimizes out an "if (disposeMode)", but keeps the |
|
// disposeMode field access. Get rid of those field accesses: |
|
block.Instructions.RemoveAll(MatchLdDisposeMode); |
|
|
|
// if (logic.not(ldfld disposeMode(ldloc this))) br falseInst |
|
// br trueInst |
|
if (!block.MatchIfAtEndOfBlock(out var condition, out _, out var falseInst)) |
|
return false; |
|
if (!MatchLdDisposeMode(condition)) |
|
return false; |
|
context.Step($"SimplifyIfDisposeMode({block.StartILOffset:x4})", block); |
|
block.Instructions[block.Instructions.Count - 2] = falseInst; |
|
block.Instructions.RemoveAt(block.Instructions.Count - 1); |
|
return true; |
|
|
|
bool MatchLdDisposeMode(ILInstruction inst) |
|
{ |
|
if (!inst.MatchLdFld(out var target, out var field)) |
|
return false; |
|
return target.MatchLdThis() && field.MemberDefinition == disposeModeField; |
|
} |
|
} |
|
|
|
bool AnalyzeAwaitBlock(Block block, out ILVariable awaiter, out IField awaiterField, out int state, out int yieldOffset) |
|
{ |
|
awaiter = null; |
|
awaiterField = null; |
|
state = 0; |
|
yieldOffset = -1; |
|
int pos = block.Instructions.Count - 2; |
|
if (pos >= 0 && doFinallyBodies != null && block.Instructions[pos] is StLoc storeDoFinallyBodies) |
|
{ |
|
if (!(storeDoFinallyBodies.Variable.Kind == VariableKind.Local |
|
&& storeDoFinallyBodies.Variable.Type.IsKnownType(KnownTypeCode.Boolean) |
|
&& storeDoFinallyBodies.Variable.Index == doFinallyBodies.Index)) |
|
{ |
|
return false; |
|
} |
|
if (!storeDoFinallyBodies.Value.MatchLdcI4(0)) |
|
return false; |
|
pos--; |
|
} |
|
|
|
if (pos >= 0 && MatchCall(block.Instructions[pos], "AwaitUnsafeOnCompleted", out var callArgs)) |
|
{ |
|
// call AwaitUnsafeOnCompleted(ldflda <>t__builder(ldloc this), ldloca awaiter, ldloc this) |
|
} |
|
else if (pos >= 0 && MatchCall(block.Instructions[pos], "AwaitOnCompleted", out callArgs)) |
|
{ |
|
// call AwaitOnCompleted(ldflda <>t__builder(ldloc this), ldloca awaiter, ldloc this) |
|
// The C# compiler emits the non-unsafe call when the awaiter does not implement |
|
// ICriticalNotifyCompletion. |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
if (callArgs.Count != 3) |
|
return false; |
|
if (!IsBuilderFieldOnThis(callArgs[0])) |
|
return false; |
|
if (!callArgs[1].MatchLdLoca(out awaiter)) |
|
return false; |
|
if (callArgs[2].MatchLdThis()) |
|
{ |
|
// OK (if state machine is a struct) |
|
pos--; |
|
} |
|
else if (callArgs[2].MatchLdLoca(out var tempVar)) |
|
{ |
|
// Roslyn, non-optimized uses a class for the state machine. |
|
// stloc tempVar(ldloc this) |
|
// call AwaitUnsafeOnCompleted(ldflda <>t__builder](ldloc this), ldloca awaiter, ldloca tempVar) |
|
if (!(pos > 0 && block.Instructions[pos - 1].MatchStLoc(tempVar, out var tempVal))) |
|
return false; |
|
if (!tempVal.MatchLdThis()) |
|
return false; |
|
pos -= 2; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
// stfld StateMachine.<>awaiter(ldloc this, ldloc awaiter) |
|
if (!block.Instructions[pos].MatchStFld(out var target, out awaiterField, out var value)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (!value.MatchLdLoc(awaiter)) |
|
return false; |
|
pos--; |
|
// Store IL offset for debug info: |
|
yieldOffset = block.Instructions[pos].EndILOffset; |
|
|
|
// stloc S_10(ldloc this) |
|
// stloc S_11(ldc.i4 0) |
|
// stloc cachedStateVar(ldloc S_11) |
|
// stfld <>1__state(ldloc S_10, ldloc S_11) |
|
if (!block.Instructions[pos].MatchStFld(out target, out var field, out value)) |
|
return false; |
|
if (!StackSlotValue(target).MatchLdThis()) |
|
return false; |
|
if (field.MemberDefinition != stateField) |
|
return false; |
|
if (!StackSlotValue(value).MatchLdcI4(out state)) |
|
return false; |
|
if (pos > 0 && block.Instructions[pos - 1] is StLoc stloc |
|
&& stloc.Variable.Kind == VariableKind.Local && stloc.Variable.Index == cachedStateVar.Index |
|
&& StackSlotValue(stloc.Value).MatchLdcI4(state)) |
|
{ |
|
// also delete the assignment to cachedStateVar |
|
pos--; |
|
} |
|
block.Instructions.RemoveRange(pos, block.Instructions.Count - pos); |
|
// delete preceding dead stores: |
|
while (pos > 0 && block.Instructions[pos - 1] is StLoc stloc2 |
|
&& stloc2.Variable.IsSingleDefinition && stloc2.Variable.LoadCount == 0 |
|
&& stloc2.Variable.Kind == VariableKind.StackSlot |
|
&& SemanticHelper.IsPure(stloc2.Value.Flags)) |
|
{ |
|
pos--; |
|
} |
|
block.Instructions.RemoveRange(pos, block.Instructions.Count - pos); |
|
return true; |
|
} |
|
|
|
static ILInstruction StackSlotValue(ILInstruction inst) |
|
{ |
|
if (inst.MatchLdLoc(out var v) && v.Kind == VariableKind.StackSlot && v.IsSingleDefinition) |
|
{ |
|
if (v.StoreInstructions[0] is StLoc stloc) |
|
{ |
|
return stloc.Value; |
|
} |
|
} |
|
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; |
|
pos--; |
|
} |
|
|
|
if (pos < 0 || !MatchCurrentAssignment(block.Instructions[pos], out yieldValue)) |
|
return false; |
|
|
|
block.Instructions.RemoveRange(pos, block.Instructions.Count - pos); |
|
return true; |
|
} |
|
|
|
bool MatchCurrentAssignment(ILInstruction inst, out ILInstruction value) |
|
{ |
|
if (!inst.MatchStFld(out var target, out var field, out value)) |
|
return false; |
|
if (!StackSlotValue(target).MatchLdThis()) |
|
return false; |
|
// TODO: check that we are accessing the current field (compare with get_Current) |
|
return true; |
|
} |
|
#endregion |
|
|
|
#region DetectAwaitPattern |
|
void DetectAwaitPattern(ILFunction function) |
|
{ |
|
context.StepStartGroup("DetectAwaitPattern", function); |
|
foreach (var container in function.Descendants.OfType<BlockContainer>()) |
|
{ |
|
foreach (var block in container.Blocks) |
|
{ |
|
context.CancellationToken.ThrowIfCancellationRequested(); |
|
DetectAwaitPattern(block); |
|
} |
|
container.SortBlocks(deleteUnreachableBlocks: true); |
|
} |
|
context.StepEndGroup(keepIfEmpty: true); |
|
} |
|
|
|
void DetectAwaitPattern(Block block) |
|
{ |
|
// block: |
|
// stloc awaiterVar(callvirt GetAwaiter(...)) |
|
// if (call get_IsCompleted(ldloca awaiterVar)) br completedBlock |
|
// br awaitBlock |
|
// awaitBlock: |
|
// .. |
|
// br resumeBlock |
|
// resumeBlock: |
|
// .. |
|
// br completedBlock |
|
if (block.Instructions.Count < 3) |
|
return; |
|
// stloc awaiterVar(callvirt GetAwaiter(...)) |
|
if (!(block.Instructions[block.Instructions.Count - 3] is StLoc stLocAwaiter)) |
|
return; |
|
ILVariable awaiterVar = stLocAwaiter.Variable; |
|
if (!(stLocAwaiter.Value is CallInstruction getAwaiterCall)) |
|
return; |
|
if (!(getAwaiterCall.Method.Name == "GetAwaiter" && (!getAwaiterCall.Method.IsStatic || getAwaiterCall.Method.IsExtensionMethod))) |
|
return; |
|
if (getAwaiterCall.Arguments.Count != 1) |
|
return; |
|
// if (call get_IsCompleted(ldloca awaiterVar)) br completedBlock |
|
if (!block.Instructions[block.Instructions.Count - 2].MatchIfInstruction(out var condition, out var trueInst)) |
|
return; |
|
if (!trueInst.MatchBranch(out var completedBlock)) |
|
return; |
|
// br awaitBlock |
|
if (!block.Instructions.Last().MatchBranch(out var awaitBlock)) |
|
return; |
|
// condition might be inverted, swap branches: |
|
if (condition.MatchLogicNot(out var negatedCondition)) |
|
{ |
|
condition = negatedCondition; |
|
ExtensionMethods.Swap(ref completedBlock, ref awaitBlock); |
|
} |
|
// continue matching call get_IsCompleted(ldloca awaiterVar) |
|
if (!MatchCall(condition, "get_IsCompleted", out var isCompletedArgs) || isCompletedArgs.Count != 1) |
|
return; |
|
if (!UnwrapConvUnknown(isCompletedArgs[0]).MatchLdLocRef(awaiterVar)) |
|
return; |
|
// Check awaitBlock and resumeBlock: |
|
if (!awaitBlocks.TryGetValue(awaitBlock, out var awaitBlockData)) |
|
return; |
|
if (awaitBlockData.awaiterVar != awaiterVar) |
|
return; |
|
if (!CheckAwaitBlock(awaitBlock, out var resumeBlock, out var stackField)) |
|
return; |
|
if (!CheckResumeBlock(resumeBlock, awaiterVar, awaitBlockData.awaiterField, completedBlock, stackField)) |
|
return; |
|
// Check completedBlock. The first instruction involves the GetResult call, but it might have |
|
// been inlined into another instruction. |
|
var getResultCall = ILInlining.FindFirstInlinedCall(completedBlock.Instructions[0]); |
|
if (getResultCall == null) |
|
return; |
|
if (!MatchCall(getResultCall, "GetResult", out var getResultArgs) || getResultArgs.Count != 1) |
|
return; |
|
if (!UnwrapConvUnknown(getResultArgs[0]).MatchLdLocRef(awaiterVar)) |
|
return; |
|
// All checks successful, let's transform. |
|
context.Step("Transform await pattern", block); |
|
block.Instructions.RemoveAt(block.Instructions.Count - 3); // remove getAwaiter call |
|
block.Instructions.RemoveAt(block.Instructions.Count - 2); // remove if (isCompleted) |
|
((Branch)block.Instructions.Last()).TargetBlock = completedBlock; // instead, directly jump to completed block |
|
Await awaitInst = new Await(UnwrapConvUnknown(getAwaiterCall.Arguments.Single())); |
|
awaitInst.GetResultMethod = getResultCall.Method; |
|
awaitInst.GetAwaiterMethod = getAwaiterCall.Method; |
|
getResultCall.ReplaceWith(awaitInst); |
|
|
|
// Remove useless reset of awaiterVar. |
|
if (completedBlock.Instructions.ElementAtOrDefault(1) is StObj stobj) |
|
{ |
|
if (stobj.Target.MatchLdLoca(awaiterVar) && stobj.Value.OpCode == OpCode.DefaultValue) |
|
{ |
|
completedBlock.Instructions.RemoveAt(1); |
|
} |
|
} |
|
} |
|
|
|
static ILInstruction UnwrapConvUnknown(ILInstruction inst) |
|
{ |
|
if (inst is Conv conv && conv.TargetType == PrimitiveType.Unknown) |
|
{ |
|
return conv.Argument; |
|
} |
|
return inst; |
|
} |
|
|
|
bool CheckAwaitBlock(Block block, out Block resumeBlock, out IField stackField) |
|
{ |
|
// awaitBlock: |
|
// (pre-roslyn: save stack) |
|
// await(ldloca V_2) |
|
// br resumeBlock |
|
resumeBlock = null; |
|
stackField = null; |
|
if (block.Instructions.Count < 2) |
|
return false; |
|
int pos = 0; |
|
if (block.Instructions[pos] is StLoc stloc && stloc.Variable.IsSingleDefinition) |
|
{ |
|
if (!block.Instructions[pos + 1].MatchStFld(out var target, out stackField, out var value)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
pos += 2; |
|
} |
|
// await(ldloca awaiterVar) |
|
if (block.Instructions[pos].OpCode != OpCode.Await) |
|
return false; |
|
// br resumeBlock |
|
return block.Instructions[pos + 1].MatchBranch(out resumeBlock); |
|
} |
|
|
|
bool CheckResumeBlock(Block block, ILVariable awaiterVar, IField awaiterField, Block completedBlock, IField stackField) |
|
{ |
|
int pos = 0; |
|
if (!RestoreStack(block, ref pos, stackField)) |
|
return false; |
|
|
|
// stloc awaiterVar(ldfld awaiterField(ldloc this)) |
|
if (!block.Instructions[pos].MatchStLoc(awaiterVar, out var value)) |
|
return false; |
|
if (value is CastClass cast && cast.Type.Equals(awaiterVar.Type)) |
|
{ |
|
// If the awaiter is a reference type, it might get stored in a field of type `object` |
|
// and cast back to the awaiter type in the resume block |
|
value = cast.Argument; |
|
} |
|
if (!value.MatchLdFld(out var target, out var field)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (!field.Equals(awaiterField)) |
|
return false; |
|
pos++; |
|
|
|
// stfld awaiterField(ldloc this, default.value) |
|
if (block.Instructions[pos].MatchStFld(out target, out field, out value) |
|
&& target.MatchLdThis() |
|
&& field.Equals(awaiterField) |
|
&& (value.OpCode == OpCode.DefaultValue || value.OpCode == OpCode.LdNull)) |
|
{ |
|
pos++; |
|
} |
|
else |
|
{ |
|
// {stloc V_6(default.value System.Runtime.CompilerServices.TaskAwaiter)} |
|
// {stobj System.Runtime.CompilerServices.TaskAwaiter`1[[System.Int32]](ldflda <>u__$awaiter4(ldloc this), ldloc V_6) at IL_0163} |
|
if (block.Instructions[pos].MatchStLoc(out var variable, out value) && value.OpCode == OpCode.DefaultValue |
|
&& block.Instructions[pos + 1].MatchStFld(out target, out field, out value) |
|
&& field.Equals(awaiterField) |
|
&& value.MatchLdLoc(variable)) |
|
{ |
|
pos += 2; |
|
} |
|
} |
|
|
|
// stloc S_28(ldc.i4 -1) |
|
// stloc cachedStateVar(ldloc S_28) |
|
// stfld <>1__state(ldloc this, ldloc S_28) |
|
ILVariable m1Var = null; |
|
if (block.Instructions[pos] is StLoc stlocM1 && stlocM1.Value.MatchLdcI4(initialState) && stlocM1.Variable.Kind == VariableKind.StackSlot) |
|
{ |
|
m1Var = stlocM1.Variable; |
|
pos++; |
|
} |
|
if (block.Instructions[pos] is StLoc stlocCachedState) |
|
{ |
|
if (stlocCachedState.Variable.Kind == VariableKind.Local && stlocCachedState.Variable.Index == cachedStateVar?.Index) |
|
{ |
|
if (stlocCachedState.Value.MatchLdLoc(m1Var) || stlocCachedState.Value.MatchLdcI4(initialState)) |
|
pos++; |
|
} |
|
} |
|
if (block.Instructions[pos].MatchStFld(out target, out field, out value)) |
|
{ |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (!field.MemberDefinition.Equals(stateField.MemberDefinition)) |
|
return false; |
|
if (!(value.MatchLdcI4(initialState) || value.MatchLdLoc(m1Var))) |
|
return false; |
|
pos++; |
|
} |
|
else |
|
{ |
|
return false; |
|
} |
|
|
|
return block.Instructions[pos].MatchBranch(completedBlock); |
|
} |
|
|
|
private bool RestoreStack(Block block, ref int pos, IField stackField) |
|
{ |
|
if (stackField == null) |
|
{ |
|
return true; // nothing to restore |
|
} |
|
// stloc temp(unbox.any T(ldfld <>t__stack(ldloc this))) |
|
if (!(block.Instructions[pos] is StLoc stloc)) |
|
return false; |
|
if (!stloc.Variable.IsSingleDefinition) |
|
return false; |
|
if (!(stloc.Value is UnboxAny unbox)) |
|
return false; |
|
if (!unbox.Argument.MatchLdFld(out var target, out var field)) |
|
return false; |
|
if (!target.MatchLdThis()) |
|
return false; |
|
if (!field.Equals(stackField)) |
|
return false; |
|
pos++; |
|
// restoring stack slots |
|
while (block.Instructions[pos].MatchStLoc(out var v) && v.Kind == VariableKind.StackSlot) |
|
{ |
|
pos++; |
|
} |
|
// stfld <>t__stack(ldloc this, ldnull) |
|
if (block.Instructions[pos].MatchStFld(out target, out field, out var value)) |
|
{ |
|
if (target.MatchLdThis() && field.Equals(stackField) && value.MatchLdNull()) |
|
{ |
|
pos++; |
|
} |
|
} |
|
return true; |
|
} |
|
#endregion |
|
|
|
/// <summary> |
|
/// Eliminates usage of doFinallyBodies |
|
/// </summary> |
|
private void CleanDoFinallyBodies(ILFunction function) |
|
{ |
|
if (doFinallyBodies == null) |
|
{ |
|
return; // roslyn-compiled code doesn't use doFinallyBodies |
|
} |
|
context.StepStartGroup("CleanDoFinallyBodies", function); |
|
Block entryPoint = GetBodyEntryPoint(function.Body as BlockContainer); |
|
if (entryPoint != null && entryPoint.Instructions[0].MatchStLoc(doFinallyBodies, out var value) && value.MatchLdcI4(1)) |
|
{ |
|
// Remove initial doFinallyBodies assignment, if it wasn't already removed when |
|
// we rearranged the control flow. |
|
entryPoint.Instructions.RemoveAt(0); |
|
} |
|
if (doFinallyBodies.StoreInstructions.Count != 0 || doFinallyBodies.AddressCount != 0) |
|
{ |
|
// misdetected another variable as doFinallyBodies? |
|
// reintroduce the initial store of ldc.i4(1) |
|
context.Step("Re-introduce misdetected doFinallyBodies", function); |
|
((BlockContainer)function.Body).EntryPoint.Instructions.Insert(0, |
|
new StLoc(doFinallyBodies, new LdcI4(1))); |
|
return; |
|
} |
|
foreach (var tryFinally in function.Descendants.OfType<TryFinally>()) |
|
{ |
|
entryPoint = GetBodyEntryPoint(tryFinally.FinallyBlock as BlockContainer); |
|
if (entryPoint?.Instructions[0] is IfInstruction ifInst) |
|
{ |
|
if (ifInst.Condition.MatchLogicNot(out var logicNotArg) && logicNotArg.MatchLdLoc(doFinallyBodies)) |
|
{ |
|
context.Step("Remove if(doFinallyBodies) from try-finally", tryFinally); |
|
// condition will always be false now that we're using 'await' instructions |
|
entryPoint.Instructions.RemoveAt(0); |
|
} |
|
} |
|
} |
|
// if there's any remaining loads (there shouldn't be), replace them with the constant 1 |
|
foreach (LdLoc load in doFinallyBodies.LoadInstructions.ToArray()) |
|
{ |
|
load.ReplaceWith(new LdcI4(1).WithILRange(load)); |
|
} |
|
context.StepEndGroup(keepIfEmpty: true); |
|
} |
|
|
|
internal static Block GetBodyEntryPoint(BlockContainer body) |
|
{ |
|
if (body == null) |
|
return null; |
|
Block entryPoint = body.EntryPoint; |
|
while (entryPoint.Instructions[0].MatchBranch(out var targetBlock) && targetBlock.IncomingEdgeCount == 1 && targetBlock.Parent == body) |
|
{ |
|
entryPoint = targetBlock; |
|
} |
|
return entryPoint; |
|
} |
|
|
|
void TranslateCachedFieldsToLocals() |
|
{ |
|
foreach (var (cachedVar, param) in cachedFieldToParameterMap) |
|
{ |
|
Debug.Assert(cachedVar.StoreCount <= 1); |
|
foreach (var inst in cachedVar.LoadInstructions.ToArray()) |
|
{ |
|
inst.Variable = param; |
|
} |
|
foreach (var inst in cachedVar.AddressInstructions.ToArray()) |
|
{ |
|
inst.Variable = param; |
|
} |
|
} |
|
} |
|
} |
|
}
|
|
|