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.
747 lines
29 KiB
747 lines
29 KiB
// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team |
|
// |
|
// 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 ICSharpCode.Decompiler.CSharp; |
|
using ICSharpCode.Decompiler.FlowAnalysis; |
|
using ICSharpCode.Decompiler.IL.Transforms; |
|
using ICSharpCode.Decompiler.TypeSystem; |
|
using ICSharpCode.Decompiler.Util; |
|
using Mono.Cecil; |
|
using System; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.Linq; |
|
using System.Text; |
|
using System.Threading.Tasks; |
|
|
|
namespace ICSharpCode.Decompiler.IL.ControlFlow |
|
{ |
|
class YieldReturnDecompiler : IILTransform |
|
{ |
|
// For a description on the code generated by the C# compiler for yield return: |
|
// http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx |
|
|
|
// The idea here is: |
|
// - Figure out whether the current method is instanciating an enumerator |
|
// - Figure out which of the fields is the state field |
|
// - Construct an exception table based on states. This allows us to determine, for each state, what the parent try block is. |
|
|
|
// See http://community.sharpdevelop.net/blogs/danielgrunwald/archive/2011/03/06/ilspy-yield-return.aspx |
|
// for a description of this step. |
|
|
|
ILTransformContext context; |
|
|
|
/// <summary>The type that contains the function being decompiled.</summary> |
|
TypeDefinition currentType; |
|
|
|
/// <summary>The compiler-generated enumerator class.</summary> |
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks> |
|
TypeDefinition enumeratorType; |
|
|
|
/// <summary>The constructor of the compiler-generated enumerator class.</summary> |
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks> |
|
MethodDefinition enumeratorCtor; |
|
|
|
/// <summary>The dispose method of the compiler-generated enumerator class.</summary> |
|
/// <remarks>Set in ConstructExceptionTable()</remarks> |
|
MethodDefinition disposeMethod; |
|
|
|
/// <summary>The field in the compiler-generated class holding the current state of the state machine</summary> |
|
/// <remarks>Set in AnalyzeCtor()</remarks> |
|
IField stateField; |
|
|
|
/// <summary>The backing field of the 'Current' property in the compiler-generated class</summary> |
|
/// <remarks>Set in AnalyzeCurrentProperty()</remarks> |
|
IField currentField; |
|
|
|
/// <summary>Maps the fields of the compiler-generated class to the original parameters.</summary> |
|
/// <remarks>Set in MatchEnumeratorCreationPattern() and ResolveIEnumerableIEnumeratorFieldMapping()</remarks> |
|
readonly Dictionary<IField, ILVariable> fieldToParameterMap = new Dictionary<IField, ILVariable>(); |
|
|
|
/// <summary>This dictionary stores the information extracted from the Dispose() method: |
|
/// for each "Finally Method", it stores the set of states for which the method is being called.</summary> |
|
/// <remarks>Set in ConstructExceptionTable()</remarks> |
|
Dictionary<IMethod, LongSet> finallyMethodToStateRange; |
|
|
|
/// <summary> |
|
/// For each finally method, stores the target state when entering the finally block, |
|
/// and the decompiled code of the finally method body. |
|
/// </summary> |
|
readonly Dictionary<IMethod, (int? outerState, BlockContainer body)> decompiledFinallyMethods = new Dictionary<IMethod, (int? outerState, BlockContainer body)>(); |
|
|
|
/// <summary> |
|
/// Temporary stores for 'yield break'. |
|
/// </summary> |
|
readonly List<StLoc> returnStores = new List<StLoc>(); |
|
|
|
#region Run() method |
|
public void Run(ILFunction function, ILTransformContext context) |
|
{ |
|
if (!context.Settings.YieldReturn) |
|
return; // abort if enumerator decompilation is disabled |
|
this.context = context; |
|
this.currentType = function.Method.DeclaringType; |
|
this.enumeratorType = null; |
|
this.enumeratorCtor = null; |
|
this.stateField = null; |
|
this.currentField = null; |
|
this.fieldToParameterMap.Clear(); |
|
this.finallyMethodToStateRange = null; |
|
this.decompiledFinallyMethods.Clear(); |
|
this.returnStores.Clear(); |
|
if (!MatchEnumeratorCreationPattern(function)) |
|
return; |
|
BlockContainer newBody; |
|
try { |
|
AnalyzeCtor(); |
|
AnalyzeCurrentProperty(); |
|
ResolveIEnumerableIEnumeratorFieldMapping(); |
|
ConstructExceptionTable(); |
|
newBody = AnalyzeMoveNext(); |
|
} catch (SymbolicAnalysisFailedException) { |
|
return; |
|
} |
|
|
|
context.Step("Replacing body with MoveNext() body", function); |
|
function.IsIterator = true; |
|
function.Body = newBody; |
|
// register any locals used in newBody |
|
function.Variables.AddRange(newBody.Descendants.OfType<IInstructionWithVariableOperand>().Select(inst => inst.Variable).Distinct()); |
|
function.CheckInvariant(ILPhase.Normal); |
|
|
|
PrintFinallyMethodStateRanges(newBody); |
|
|
|
context.Step("Delete unreachable blocks", function); |
|
// Note: because this only deletes blocks outright, the 'stateChanges' entries remain valid |
|
// (though some may point to now-deleted blocks) |
|
newBody.SortBlocks(deleteUnreachableBlocks: true); |
|
|
|
DecompileFinallyBlocks(); |
|
ReconstructTryFinallyBlocks(newBody); |
|
|
|
context.Step("Translate fields to local accesses", function); |
|
TranslateFieldsToLocalAccess(function, function, fieldToParameterMap); |
|
|
|
if (returnStores.Count > 0) { |
|
context.Step("Remove temporaries", function); |
|
foreach (var store in returnStores) { |
|
if (store.Variable.LoadCount == 0 && store.Variable.AddressCount == 0 && store.Parent is Block block) { |
|
block.Instructions.Remove(store); |
|
} |
|
} |
|
} |
|
|
|
// 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); |
|
} |
|
#endregion |
|
|
|
#region Match the enumerator creation pattern |
|
bool MatchEnumeratorCreationPattern(ILFunction function) |
|
{ |
|
Block body = SingleBlock(function.Body); |
|
if (body == null || body.Instructions.Count == 0) { |
|
return false; |
|
} |
|
|
|
ILInstruction newObj; |
|
if (body.Instructions.Count == 1) { |
|
// No parameters passed to enumerator (not even 'this'): |
|
// ret(newobj(...)) |
|
if (body.Instructions[0].MatchReturn(out newObj)) |
|
return MatchEnumeratorCreationNewObj(newObj); |
|
else |
|
return false; |
|
} |
|
|
|
// If there's parameters passed to the helper class, the class instance is first |
|
// stored in a variable, then the parameters are copied over, then the instance is returned. |
|
|
|
// stloc(var_1, newobj(..)) |
|
if (!body.Instructions[0].MatchStLoc(out var var1, out newObj)) |
|
return false; |
|
if (!MatchEnumeratorCreationNewObj(newObj)) |
|
return false; |
|
|
|
int i; |
|
for (i = 1; i < body.Instructions.Count; i++) { |
|
// stfld(..., ldloc(var_1), ldloc(parameter)) |
|
if (!body.Instructions[i].MatchStFld(out var ldloc, out var storedField, out var loadParameter)) |
|
break; |
|
if (ldloc.MatchLdLoc(var1) |
|
&& loadParameter.MatchLdLoc(out var parameter) |
|
&& parameter.Kind == VariableKind.Parameter) { |
|
fieldToParameterMap[(IField)storedField.MemberDefinition] = parameter; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
// In debug builds, the compiler may copy the var1 into another variable (var2) before returning it. |
|
if (i < body.Instructions.Count |
|
&& body.Instructions[i].MatchStLoc(out var var2, out var ldlocForStloc2) |
|
&& ldlocForStloc2.MatchLdLoc(var1)) { |
|
// stloc(var_2, ldloc(var_1)) |
|
i++; |
|
} else { |
|
// in release builds, var1 is returned directly |
|
var2 = var1; |
|
} |
|
if (i < body.Instructions.Count |
|
&& body.Instructions[i].MatchReturn(out var retVal) |
|
&& retVal.MatchLdLoc(var2)) { |
|
// ret(ldloc(var_2)) |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Matches the body of a method as a single basic block. |
|
/// </summary> |
|
static Block SingleBlock(ILInstruction body) |
|
{ |
|
var block = body as Block; |
|
if (body is BlockContainer blockContainer && blockContainer.Blocks.Count == 1) { |
|
block = blockContainer.Blocks.Single() as Block; |
|
} |
|
return block; |
|
} |
|
|
|
/// <summary> |
|
/// Matches the newobj instruction that creates an instance of the compiler-generated enumerator helper class. |
|
/// </summary> |
|
bool MatchEnumeratorCreationNewObj(ILInstruction inst) |
|
{ |
|
// 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 int initialState)) |
|
return false; |
|
if (!(initialState == -2 || initialState == 0)) |
|
return false; |
|
enumeratorCtor = context.TypeSystem.GetCecil(newObj.Method) as MethodDefinition; |
|
enumeratorType = enumeratorCtor?.DeclaringType; |
|
return enumeratorType?.DeclaringType == currentType |
|
&& IsCompilerGeneratorEnumerator(enumeratorType); |
|
} |
|
|
|
public static bool IsCompilerGeneratorEnumerator(TypeDefinition type) |
|
{ |
|
if (!(type?.DeclaringType != null && type.IsCompilerGenerated())) |
|
return false; |
|
foreach (var i in type.Interfaces) { |
|
var tr = i.InterfaceType; |
|
if (tr.Namespace == "System.Collections" && tr.Name == "IEnumerator") |
|
return true; |
|
} |
|
return false; |
|
} |
|
#endregion |
|
|
|
#region Figure out what the 'state' field is (analysis of .ctor()) |
|
/// <summary> |
|
/// Looks at the enumerator's ctor and figures out which of the fields holds the state. |
|
/// </summary> |
|
void AnalyzeCtor() |
|
{ |
|
Block body = SingleBlock(CreateILAst(enumeratorCtor).Body); |
|
if (body == null) |
|
throw new SymbolicAnalysisFailedException("Missing enumeratorCtor.Body"); |
|
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 (stateField == null) |
|
throw new SymbolicAnalysisFailedException("Could not find stateField"); |
|
} |
|
|
|
/// <summary> |
|
/// Creates ILAst for the specified method, optimized up to before the 'YieldReturn' step. |
|
/// </summary> |
|
ILFunction CreateILAst(MethodDefinition method) |
|
{ |
|
if (method == null || !method.HasBody) |
|
throw new SymbolicAnalysisFailedException(); |
|
|
|
var il = new ILReader(context.TypeSystem).ReadIL(method.Body, context.CancellationToken); |
|
il.RunTransforms(CSharpDecompiler.EarlyILTransforms(), new ILTransformContext { |
|
Settings = context.Settings, |
|
CancellationToken = context.CancellationToken, |
|
TypeSystem = context.TypeSystem |
|
}); |
|
return il; |
|
} |
|
#endregion |
|
|
|
#region Figure out what the 'current' field is (analysis of get_Current()) |
|
/// <summary> |
|
/// Looks at the enumerator's get_Current method and figures out which of the fields holds the current value. |
|
/// </summary> |
|
void AnalyzeCurrentProperty() |
|
{ |
|
MethodDefinition getCurrentMethod = enumeratorType.Methods.FirstOrDefault( |
|
m => m.Name.StartsWith("System.Collections.Generic.IEnumerator", StringComparison.Ordinal) |
|
&& m.Name.EndsWith(".get_Current", StringComparison.Ordinal)); |
|
Block body = SingleBlock(CreateILAst(getCurrentMethod).Body); |
|
if (body == null) |
|
throw new SymbolicAnalysisFailedException(); |
|
if (body.Instructions.Count == 1) { |
|
// release builds directly return the current field |
|
// ret(ldfld F(ldloc(this))) |
|
if (body.Instructions[0].MatchReturn(out var retVal) |
|
&& retVal.MatchLdFld(out var target, out var field) |
|
&& target.MatchLdThis()) { |
|
currentField = (IField)field.MemberDefinition; |
|
} |
|
} else if (body.Instructions.Count == 2) { |
|
// debug builds store the return value in a temporary |
|
// stloc V = ldfld F(ldloc(this)) |
|
// ret(ldloc V) |
|
if (body.Instructions[0].MatchStLoc(out var v, out var ldfld) |
|
&& ldfld.MatchLdFld(out var target, out var field) |
|
&& target.MatchLdThis() |
|
&& body.Instructions[1].MatchReturn(out var retVal) |
|
&& retVal.MatchLdLoc(v)) { |
|
currentField = (IField)field.MemberDefinition; |
|
} |
|
} |
|
if (currentField == null) |
|
throw new SymbolicAnalysisFailedException("Could not find currentField"); |
|
} |
|
#endregion |
|
|
|
#region Figure out the mapping of IEnumerable fields to IEnumerator fields (analysis of GetEnumerator()) |
|
void ResolveIEnumerableIEnumeratorFieldMapping() |
|
{ |
|
MethodDefinition getEnumeratorMethod = enumeratorType.Methods.FirstOrDefault( |
|
m => m.Name.StartsWith("System.Collections.Generic.IEnumerable", StringComparison.Ordinal) |
|
&& m.Name.EndsWith(".GetEnumerator", StringComparison.Ordinal)); |
|
if (getEnumeratorMethod == null) |
|
return; // no mappings (maybe it's just an IEnumerator implementation?) |
|
var function = CreateILAst(getEnumeratorMethod); |
|
foreach (var block in function.Descendants.OfType<Block>()) { |
|
foreach (var inst in block.Instructions) { |
|
// storeTarget.storeField = this.loadField; |
|
if (inst.MatchStFld(out var storeTarget, out var storeField, out var storeValue) |
|
&& storeValue.MatchLdFld(out var loadTarget, out var loadField) |
|
&& loadTarget.MatchLdThis()) { |
|
storeField = (IField)storeField.MemberDefinition; |
|
loadField = (IField)loadField.MemberDefinition; |
|
if (fieldToParameterMap.TryGetValue(loadField, out var mappedParameter)) |
|
fieldToParameterMap[storeField] = mappedParameter; |
|
} |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
#region Construction of the exception table (analysis of Dispose()) |
|
// We construct the exception table by analyzing the enumerator's Dispose() method. |
|
|
|
void ConstructExceptionTable() |
|
{ |
|
disposeMethod = enumeratorType.Methods.FirstOrDefault(m => m.Name == "System.IDisposable.Dispose"); |
|
var function = CreateILAst(disposeMethod); |
|
|
|
var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorDispose, stateField); |
|
rangeAnalysis.AssignStateRanges(function.Body, LongSet.Universe); |
|
finallyMethodToStateRange = rangeAnalysis.finallyMethodToStateRange; |
|
} |
|
|
|
[Conditional("DEBUG")] |
|
void PrintFinallyMethodStateRanges(BlockContainer bc) |
|
{ |
|
foreach (var (method, stateRange) in finallyMethodToStateRange) { |
|
bc.Blocks[0].Instructions.Insert(0, new Nop { |
|
Comment = method.Name + " in " + stateRange |
|
}); |
|
} |
|
} |
|
#endregion |
|
|
|
#region Analyze MoveNext() and generate new body |
|
BlockContainer AnalyzeMoveNext() |
|
{ |
|
MethodDefinition moveNextMethod = enumeratorType.Methods.FirstOrDefault(m => m.Name == "MoveNext"); |
|
ILFunction moveNextFunction = CreateILAst(moveNextMethod); |
|
|
|
var body = (BlockContainer)moveNextFunction.Body; |
|
if (body.Blocks.Count == 1 && body.Blocks[0].Instructions.Count == 1 && body.Blocks[0].Instructions[0] is TryFault tryFault) { |
|
body = (BlockContainer)tryFault.TryBlock; |
|
var faultBlockContainer = tryFault.FaultBlock as BlockContainer; |
|
if (faultBlockContainer?.Blocks.Count != 1) |
|
throw new SymbolicAnalysisFailedException("Unexpected number of blocks in MoveNext() fault block"); |
|
var faultBlock = faultBlockContainer.Blocks.Single(); |
|
if (!(faultBlock.Instructions.Count == 2 |
|
&& faultBlock.Instructions[0] is Call call |
|
&& context.TypeSystem.GetCecil(call.Method) == disposeMethod |
|
&& call.Arguments.Count == 1 |
|
&& call.Arguments[0].MatchLdThis() |
|
&& faultBlock.Instructions[1].MatchLeave(faultBlockContainer))) { |
|
throw new SymbolicAnalysisFailedException("Unexpected fault block contents in MoveNext()"); |
|
} |
|
} |
|
|
|
// Note: body may contain try-catch or try-finally statements that have nested block containers, |
|
// but those cannot contain any yield statements. |
|
// So for reconstructing the control flow, we only need at the blocks directly within body. |
|
|
|
var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorMoveNext, stateField); |
|
rangeAnalysis.AssignStateRanges(body, LongSet.Universe); |
|
|
|
var newBody = ConvertBody(body, rangeAnalysis.GetBlockStateSetMapping(body)); |
|
moveNextFunction.Variables.Clear(); |
|
// release references from old moveNextFunction to instructions that were moved over to newBody |
|
moveNextFunction.ReleaseRef(); |
|
return newBody; |
|
} |
|
|
|
/// <summary> |
|
/// Convert the old body (of MoveNext function) to the new body (of decompiled iterator method). |
|
/// |
|
/// * Replace the sequence |
|
/// this.currentField = expr; |
|
/// this.state = N; |
|
/// return true; |
|
/// with: |
|
/// yield return expr; |
|
/// goto blockForState(N); |
|
/// * Replace the sequence: |
|
/// this._finally2(); |
|
/// this._finally1(); |
|
/// return false; |
|
/// with: |
|
/// yield break; |
|
/// * Reconstruct try-finally blocks from |
|
/// (on enter) this.state = N; |
|
/// (on exit) this._finallyX(); |
|
/// </summary> |
|
private BlockContainer ConvertBody(BlockContainer oldBody, IEnumerable<(Block, LongSet)> blockStateSets) |
|
{ |
|
BlockContainer newBody = new BlockContainer(); |
|
// create all new blocks so that they can be referenced by gotos |
|
for (int blockIndex = 0; blockIndex < oldBody.Blocks.Count; blockIndex++) { |
|
newBody.Blocks.Add(new Block { ILRange = oldBody.Blocks[blockIndex].ILRange }); |
|
} |
|
// convert contents of blocks |
|
|
|
for (int i = 0; i < oldBody.Blocks.Count; i++) { |
|
var oldBlock = oldBody.Blocks[i]; |
|
var newBlock = newBody.Blocks[i]; |
|
foreach (var oldInst in oldBlock.Instructions) { |
|
if (oldInst.MatchStFld(out var target, out var field, out var value) && target.MatchLdThis()) { |
|
if (field.MemberDefinition.Equals(stateField)) { |
|
if (value.MatchLdcI4(out int newState)) { |
|
// On state change, break up the block: |
|
// (this allows us to consider each block individually for try-finally reconstruction) |
|
newBlock = SplitBlock(newBlock, oldInst); |
|
// We keep the state-changing instruction around (as first instruction of the new block) |
|
// for reconstructing the try-finallys. |
|
} else { |
|
newBlock.Instructions.Add(new InvalidExpression("Assigned non-constant to iterator.state field") { |
|
ILRange = oldInst.ILRange |
|
}); |
|
continue; // don't copy over this instruction, but continue with the basic block |
|
} |
|
} else if (field.MemberDefinition.Equals(currentField)) { |
|
// create yield return |
|
newBlock.Instructions.Add(new YieldReturn(value) { ILRange = oldInst.ILRange }); |
|
ConvertBranchAfterYieldReturn(newBlock, oldBlock, oldInst.ChildIndex); |
|
break; // we're done with this basic block |
|
} |
|
} else if (oldInst is Call call && call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis() |
|
&& finallyMethodToStateRange.ContainsKey((IMethod)call.Method.MemberDefinition)) |
|
{ |
|
// Break up the basic block on a call to a finally method |
|
// (this allows us to consider each block individually for try-finally reconstruction) |
|
newBlock = SplitBlock(newBlock, oldInst); |
|
} |
|
// copy over the instruction to the new block |
|
newBlock.Instructions.Add(oldInst); |
|
UpdateBranchTargets(oldInst); |
|
} |
|
} |
|
|
|
// Insert new artificial block as entry point, and jump to state 0. |
|
// This causes the method to start directly at the first user code, |
|
// and the whole compiler-generated state-dispatching logic becomes unreachable code |
|
// and gets deleted. |
|
newBody.Blocks.Insert(0, new Block { |
|
Instructions = { MakeGoTo(0) } |
|
}); |
|
return newBody; |
|
|
|
void ConvertBranchAfterYieldReturn(Block newBlock, Block oldBlock, int i) |
|
{ |
|
if (!(oldBlock.Instructions[i + 1].MatchStFld(out var target, out var field, out var value) |
|
&& target.MatchLdThis() |
|
&& field.MemberDefinition == stateField |
|
&& value.MatchLdcI4(out int newState))) { |
|
newBlock.Instructions.Add(new InvalidBranch("Unable to find new state assignment for yield return")); |
|
return; |
|
} |
|
if (!(oldBlock.Instructions[i + 2].MatchReturn(out var retVal) |
|
&& retVal.MatchLdcI4(1))) { |
|
newBlock.Instructions.Add(new InvalidBranch("Unable to find 'return true' for yield return")); |
|
return; |
|
} |
|
newBlock.Instructions.Add(MakeGoTo(newState)); |
|
} |
|
|
|
Block SplitBlock(Block newBlock, ILInstruction oldInst) |
|
{ |
|
if (newBlock.Instructions.Count > 0) { |
|
var newBlock2 = new Block(); |
|
newBlock2.ILRange = new Interval(oldInst.ILRange.Start, oldInst.ILRange.Start); |
|
newBody.Blocks.Add(newBlock2); |
|
newBlock.Instructions.Add(new Branch(newBlock2)); |
|
newBlock = newBlock2; |
|
} |
|
return newBlock; |
|
} |
|
|
|
ILInstruction MakeGoTo(int v) |
|
{ |
|
Block targetBlock = null; |
|
foreach (var (block, stateSet) in blockStateSets) { |
|
if (stateSet.Contains(v)) |
|
targetBlock = block; |
|
} |
|
if (targetBlock != null) |
|
return new Branch(newBody.Blocks[targetBlock.ChildIndex]); |
|
else |
|
return new InvalidBranch("Could not find block for state " + v); |
|
} |
|
|
|
void UpdateBranchTargets(ILInstruction inst) |
|
{ |
|
switch (inst) { |
|
case Branch branch: |
|
if (branch.TargetContainer == oldBody) { |
|
branch.TargetBlock = newBody.Blocks[branch.TargetBlock.ChildIndex]; |
|
} |
|
break; |
|
case Leave leave: |
|
if (leave.TargetContainer == oldBody) { |
|
leave.TargetContainer = newBody; |
|
} |
|
break; |
|
case Return ret: |
|
ILInstruction value = ret.Value; |
|
if (value.MatchLdLoc(out var v) && v.IsSingleDefinition |
|
&& v.StoreInstructions.SingleOrDefault() is StLoc stloc) |
|
{ |
|
returnStores.Add(stloc); |
|
value = stloc.Value; |
|
} |
|
if (value.MatchLdcI4(0)) { |
|
// yield break |
|
ret.ReplaceWith(new Leave(newBody) { ILRange = ret.ILRange }); |
|
} else { |
|
ret.ReplaceWith(new InvalidBranch("Unexpected return in MoveNext()") { ILRange = ret.ILRange }); |
|
} |
|
break; |
|
} |
|
foreach (var child in inst.Children) { |
|
UpdateBranchTargets(child); |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
#region TranslateFieldsToLocalAccess |
|
/// <summary> |
|
/// Translates all field accesses in `function` to local variable accesses. |
|
/// </summary> |
|
internal static void TranslateFieldsToLocalAccess(ILFunction function, ILInstruction inst, Dictionary<IField, ILVariable> fieldToVariableMap) |
|
{ |
|
if (inst is LdFlda ldflda && ldflda.Target.MatchLdThis()) { |
|
var fieldDef = (IField)ldflda.Field.MemberDefinition; |
|
if (!fieldToVariableMap.TryGetValue(fieldDef, out var v)) { |
|
string name = null; |
|
if (!string.IsNullOrEmpty(fieldDef.Name) && fieldDef.Name[0] == '<') { |
|
int pos = fieldDef.Name.IndexOf('>'); |
|
if (pos > 1) |
|
name = fieldDef.Name.Substring(1, pos - 1); |
|
} |
|
v = function.RegisterVariable(VariableKind.Local, ldflda.Field.ReturnType, name); |
|
fieldToVariableMap.Add(fieldDef, v); |
|
} |
|
inst.ReplaceWith(new LdLoca(v)); |
|
} else if (inst.MatchLdThis()) { |
|
inst.ReplaceWith(new InvalidExpression("iterator") { ExpectedResultType = inst.ResultType }); |
|
} else { |
|
foreach (var child in inst.Children) { |
|
TranslateFieldsToLocalAccess(function, child, fieldToVariableMap); |
|
} |
|
} |
|
} |
|
#endregion |
|
|
|
#region DecompileFinallyBlocks |
|
void DecompileFinallyBlocks() |
|
{ |
|
foreach (var method in finallyMethodToStateRange.Keys) { |
|
var function = CreateILAst((MethodDefinition)context.TypeSystem.GetCecil(method)); |
|
var body = (BlockContainer)function.Body; |
|
var newState = GetNewState(body.EntryPoint); |
|
if (newState != null) { |
|
body.EntryPoint.Instructions.RemoveAt(0); |
|
} |
|
function.ReleaseRef(); // make body reusable outside of function |
|
decompiledFinallyMethods.Add(method, (newState, body)); |
|
} |
|
} |
|
#endregion |
|
|
|
#region Reconstruct try-finally blocks |
|
|
|
/// <summary> |
|
/// Reconstruct try-finally blocks. |
|
/// * The stateChanges (iterator._state = N;) tell us when to open a try-finally block |
|
/// * The calls to the finally method tell us when to leave the try block. |
|
/// |
|
/// There might be multiple stateChanges for a given try-finally block, e.g. |
|
/// both the original entry point, and the target when leaving a nested block. |
|
/// In proper C# code, the entry point of the try-finally will dominate all other code |
|
/// in the try-block, so we can use dominance to find the proper entry point. |
|
/// |
|
/// Precondition: the blocks in newBody are topologically sorted. |
|
/// </summary> |
|
void ReconstructTryFinallyBlocks(BlockContainer newBody) |
|
{ |
|
context.Stepper.Step("Reconstuct try-finally blocks"); |
|
var blockState = new int[newBody.Blocks.Count]; |
|
blockState[0] = -1; |
|
var stateToContainer = new Dictionary<int, BlockContainer>(); |
|
stateToContainer.Add(-1, newBody); |
|
// First, analyse the newBody: for each block, determine the active state number. |
|
foreach (var block in newBody.Blocks) { |
|
int oldState = blockState[block.ChildIndex]; |
|
BlockContainer container; // new container for the block |
|
if (GetNewState(block) is int newState) { |
|
// OK, state change |
|
// Remove the state-changing instruction |
|
block.Instructions.RemoveAt(0); |
|
|
|
if (!stateToContainer.TryGetValue(newState, out container)) { |
|
// First time we see this state. |
|
// This means we just found the entry point of a try block. |
|
CreateTryBlock(block, newState); |
|
// CreateTryBlock() wraps the contents of 'block' with a TryFinally. |
|
// We thus need to put the block (which now contains the whole TryFinally) |
|
// into the parent container. |
|
// Assuming a state transition never enters more than one state at once, |
|
// we can use stateToContainer[oldState] as parent. |
|
container = stateToContainer[oldState]; |
|
} |
|
} else { |
|
// Because newBody is topologically sorted we because we just removed unreachable code, |
|
// we can assume that blockState[] was already set for this block. |
|
newState = oldState; |
|
container = stateToContainer[oldState]; |
|
} |
|
if (container != newBody) { |
|
// Move the block into the container. |
|
container.Blocks.Add(block); |
|
// Keep the stale reference in newBody.Blocks for now, to avoid |
|
// changing the ChildIndex of the other blocks while we use it |
|
// to index the blockState array. |
|
} |
|
#if DEBUG |
|
block.Instructions.Insert(0, new Nop { Comment = "state == " + newState }); |
|
#endif |
|
// Propagate newState to successor blocks |
|
foreach (var branch in block.Descendants.OfType<Branch>()) { |
|
if (branch.TargetBlock.Parent == newBody) { |
|
Debug.Assert(blockState[branch.TargetBlock.ChildIndex] == newState || blockState[branch.TargetBlock.ChildIndex] == 0); |
|
blockState[branch.TargetBlock.ChildIndex] = newState; |
|
} |
|
} |
|
} |
|
newBody.Blocks.RemoveAll(b => b.Parent != newBody); |
|
|
|
void CreateTryBlock(Block block, int state) |
|
{ |
|
var finallyMethod = FindFinallyMethod(state); |
|
Debug.Assert(finallyMethod != null); |
|
// remove the method so that it doesn't get cause ambiguity when processing nested try-finally blocks |
|
finallyMethodToStateRange.Remove(finallyMethod); |
|
|
|
var tryBlock = new Block(); |
|
tryBlock.ILRange = block.ILRange; |
|
tryBlock.Instructions.AddRange(block.Instructions); |
|
var tryBlockContainer = new BlockContainer(); |
|
tryBlockContainer.Blocks.Add(tryBlock); |
|
stateToContainer.Add(state, tryBlockContainer); |
|
|
|
ILInstruction finallyBlock; |
|
if (decompiledFinallyMethods.TryGetValue(finallyMethod, out var decompiledMethod)) { |
|
finallyBlock = decompiledMethod.body; |
|
} else { |
|
finallyBlock = new InvalidBranch("Missing decompiledFinallyMethod"); |
|
} |
|
|
|
block.Instructions.Clear(); |
|
block.Instructions.Add(new TryFinally(tryBlockContainer, finallyBlock)); |
|
} |
|
|
|
IMethod FindFinallyMethod(int state) |
|
{ |
|
IMethod foundMethod = null; |
|
foreach (var (method, stateRange) in finallyMethodToStateRange) { |
|
if (stateRange.Contains(state)) { |
|
if (foundMethod == null) |
|
foundMethod = method; |
|
else |
|
Debug.Fail("Ambiguous finally method for state " + state); |
|
} |
|
} |
|
return foundMethod; |
|
} |
|
} |
|
|
|
// Gets the state that is transitioned to at the start of the block |
|
int? GetNewState(Block block) |
|
{ |
|
if (block.Instructions[0].MatchStFld(out var target, out var field, out var value) |
|
&& target.MatchLdThis() |
|
&& field.MemberDefinition.Equals(stateField) |
|
&& value.MatchLdcI4(out int newState)) |
|
{ |
|
return newState; |
|
} else if (block.Instructions[0] is Call call |
|
&& call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis() |
|
&& decompiledFinallyMethods.TryGetValue((IMethod)call.Method.MemberDefinition, out var finallyMethod)) |
|
{ |
|
return finallyMethod.outerState; |
|
} |
|
return null; |
|
} |
|
#endregion |
|
} |
|
}
|
|
|