From fe84ea9730eade4e832573701158d6257fa3d387 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Tue, 20 Dec 2016 01:50:54 +0100 Subject: [PATCH] WIP: yield return decompiler --- .../CSharp/CSharpDecompiler.cs | 19 +- .../CSharp/ExpressionBuilder.cs | 22 +- .../CSharp/StatementBuilder.cs | 20 +- .../CSharp/WholeProjectDecompiler.cs | 2 +- ICSharpCode.Decompiler/DecompilerSettings.cs | 2 +- .../FlowAnalysis/DataFlowVisitor.cs | 10 +- .../FlowAnalysis/Dominance.cs | 2 +- .../ICSharpCode.Decompiler.csproj | 7 +- ICSharpCode.Decompiler/IL/BlockBuilder.cs | 2 +- .../IL/ControlFlow/ConditionDetection.cs | 1 + .../IL/ControlFlow/DetectPinnedRegions.cs | 13 +- .../IL/ControlFlow/LoopDetection.cs | 2 +- .../IL/ControlFlow/StateRangeAnalysis.cs | 195 ++++++ .../IL/ControlFlow/SymbolicExecution.cs | 161 +++++ .../IL/ControlFlow/YieldReturnDecompiler.cs | 585 ++++++++++++++++++ ICSharpCode.Decompiler/IL/ILReader.cs | 12 +- ICSharpCode.Decompiler/IL/Instructions.cs | 211 ++++++- ICSharpCode.Decompiler/IL/Instructions.tt | 16 +- .../IL/Instructions/ILFunction.cs | 13 + .../IL/Instructions/ILInstruction.cs | 6 +- .../IL/Instructions/PatternMatching.cs | 50 +- .../IL/Instructions/SimpleInstruction.cs | 43 +- .../CachedDelegateInitialization.cs | 9 +- .../IL/Transforms/LoopingTransform.cs | 36 +- .../Tests/RoundtripAssembly.cs | 2 +- .../Tests/TestCases/Pretty/YieldReturn.cs | 45 +- .../TypeSystem/CecilLoader.cs | 8 +- .../DefaultUnresolvedAttribute.cs | 2 +- .../TypeSystem/TypeSystemExtensions.cs | 27 + .../Util/CollectionExtensions.cs | 6 + .../Util/ExtensionMethods.cs | 6 - ICSharpCode.Decompiler/Util/LongSet.cs | 52 +- ILSpy/ILSpyTraceListener.cs | 12 +- ILSpy/XmlDoc/XmlDocLoader.cs | 4 +- 34 files changed, 1469 insertions(+), 134 deletions(-) create mode 100644 ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs create mode 100644 ICSharpCode.Decompiler/IL/ControlFlow/SymbolicExecution.cs create mode 100644 ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index f799ef1a4..0f867ff45 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -48,6 +48,18 @@ namespace ICSharpCode.Decompiler.CSharp List ilTransforms = GetILTransforms(); + /// + /// Pre-yield/await transforms. + /// + internal static List EarlyILTransforms() + { + return new List { + new ControlFlowSimplification(), + new SplitVariables(), + new ILInlining(), + }; + } + public static List GetILTransforms() { return new List { @@ -57,6 +69,7 @@ namespace ICSharpCode.Decompiler.CSharp new SplitVariables(), new ILInlining(), new DetectPinnedRegions(), // must run after inlining but before non-critical control flow transforms + new YieldReturnDecompiler(), // must run after inlining but before loop detection new DetectExitPoints(), new BlockILTransform { PostOrderTransforms = { @@ -161,8 +174,8 @@ namespace ICSharpCode.Decompiler.CSharp if (type.DeclaringType != null) { if (settings.AnonymousMethods && IsClosureType(type)) return true; -// if (settings.YieldReturn && YieldReturnDecompiler.IsCompilerGeneratorEnumerator(type)) -// return true; + if (settings.YieldReturn && YieldReturnDecompiler.IsCompilerGeneratorEnumerator(type)) + return true; // if (settings.AsyncAwait && AsyncDecompiler.IsCompilerGeneratedStateMachine(type)) // return true; } else if (type.IsCompilerGenerated()) { @@ -616,7 +629,7 @@ namespace ICSharpCode.Decompiler.CSharp } AddDefinesForConditionalAttributes(function); - var statementBuilder = new StatementBuilder(specializingTypeSystem, decompilationContext, method); + var statementBuilder = new StatementBuilder(specializingTypeSystem, decompilationContext, method, function); entityDecl.AddChild(statementBuilder.ConvertAsBlock(function.Body), Roles.Body); } diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index e739a5075..0dd9f3672 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -942,7 +942,7 @@ namespace ICSharpCode.Decompiler.CSharp AnonymousMethodExpression ame = new AnonymousMethodExpression(); ame.Parameters.AddRange(MakeParameters(method, function)); ame.HasParameterList = true; - StatementBuilder builder = new StatementBuilder(typeSystem.GetSpecializingTypeSystem(new SimpleTypeResolveContext(method)), this.decompilationContext, method); + StatementBuilder builder = new StatementBuilder(typeSystem.GetSpecializingTypeSystem(new SimpleTypeResolveContext(method)), this.decompilationContext, method, function); var body = builder.ConvertAsBlock(function.Body); bool isLambda = false; bool isMultiLineLambda = false; @@ -1548,10 +1548,10 @@ namespace ICSharpCode.Decompiler.CSharp .WithILInstruction(inst) .WithRR(new ByReferenceResolveResult(value.ResolveResult, false)); } - - protected internal override TranslatedExpression VisitInvalidInstruction(InvalidInstruction inst, TranslationContext context) + + protected internal override TranslatedExpression VisitInvalidBranch(InvalidBranch inst, TranslationContext context) { - string message = "Invalid IL"; + string message = "Error"; if (inst.ILRange.Start != 0) { message += $" near IL_{inst.ILRange.Start:x4}"; } @@ -1560,7 +1560,19 @@ namespace ICSharpCode.Decompiler.CSharp } return ErrorExpression(message); } - + + protected internal override TranslatedExpression VisitInvalidExpression(InvalidExpression inst, TranslationContext context) + { + string message = "Error"; + if (inst.ILRange.Start != 0) { + message += $" near IL_{inst.ILRange.Start:x4}"; + } + if (!string.IsNullOrEmpty(inst.Message)) { + message += ": " + inst.Message; + } + return ErrorExpression(message); + } + protected override TranslatedExpression Default(ILInstruction inst, TranslationContext context) { return ErrorExpression("OpCode not supported: " + inst.OpCode); diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 66b4641b7..e3c5b5dc9 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -31,12 +31,14 @@ namespace ICSharpCode.Decompiler.CSharp class StatementBuilder : ILVisitor { internal readonly ExpressionBuilder exprBuilder; + readonly ILFunction currentFunction; readonly IMethod currentMethod; - public StatementBuilder(IDecompilerTypeSystem typeSystem, ITypeResolveContext decompilationContext, IMethod currentMethod) + public StatementBuilder(IDecompilerTypeSystem typeSystem, ITypeResolveContext decompilationContext, IMethod currentMethod, ILFunction currentFunction) { Debug.Assert(typeSystem != null && decompilationContext != null && currentMethod != null); this.exprBuilder = new ExpressionBuilder(typeSystem, decompilationContext); + this.currentFunction = currentFunction; this.currentMethod = currentMethod; } @@ -146,8 +148,12 @@ namespace ICSharpCode.Decompiler.CSharp { if (inst.TargetContainer == breakTarget) return new BreakStatement(); - if (inst.TargetContainer.SlotInfo == ILFunction.BodySlot) - return new ReturnStatement(); + if (inst.TargetContainer.SlotInfo == ILFunction.BodySlot) { + if (currentFunction.IsIterator) + return new YieldBreakStatement(); + else + return new ReturnStatement(); + } string label; if (!endContainerLabels.TryGetValue(inst.TargetContainer, out label)) { label = "end_" + inst.TargetLabel; @@ -173,6 +179,14 @@ namespace ICSharpCode.Decompiler.CSharp return new ReturnStatement(exprBuilder.Translate(inst.ReturnValue).ConvertTo(currentMethod.ReturnType, exprBuilder)); } + protected internal override Statement VisitYieldReturn(YieldReturn inst) + { + var elementType = currentMethod.ReturnType.GetElementTypeFromIEnumerable(currentMethod.Compilation, true, out var isGeneric); + return new YieldReturnStatement { + Expression = exprBuilder.Translate(inst.Value).ConvertTo(elementType, exprBuilder) + }; + } + TryCatchStatement MakeTryCatch(ILInstruction tryBlock) { var tryBlockConverted = Convert(tryBlock); diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index 73b56baad..ede55c7c5 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -198,7 +198,7 @@ namespace ICSharpCode.Decompiler.CSharp var asm = module.AssemblyResolver.Resolve(r); if (!IsGacAssembly(r, asm)) { if (asm != null) { - w.WriteElementString("HintPath", asm.MainModule.FullyQualifiedName); + w.WriteElementString("HintPath", asm.MainModule.FileName); } } w.WriteEndElement(); diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 091026c7a..8ad9da01a 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -57,7 +57,7 @@ namespace ICSharpCode.Decompiler } } - bool yieldReturn = true; + bool yieldReturn = false; /// /// Decompile enumerators. diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs index 65ee4e295..cc9046710 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs @@ -271,7 +271,6 @@ namespace ICSharpCode.Decompiler.FlowAnalysis #if DEBUG DebugPoint(debugOutputState, inst); #endif - } /// @@ -447,7 +446,7 @@ namespace ICSharpCode.Decompiler.FlowAnalysis MarkUnreachable(); } - protected internal override void VisitInvalidInstruction(InvalidInstruction inst) + protected internal override void VisitInvalidBranch(InvalidBranch inst) { MarkUnreachable(); } @@ -579,5 +578,12 @@ namespace ICSharpCode.Decompiler.FlowAnalysis state = afterSections; DebugEndPoint(inst); } + + protected internal override void VisitYieldReturn(YieldReturn inst) + { + DebugStartPoint(inst); + inst.Value.AcceptVisitor(this); + DebugEndPoint(inst); + } } } diff --git a/ICSharpCode.Decompiler/FlowAnalysis/Dominance.cs b/ICSharpCode.Decompiler/FlowAnalysis/Dominance.cs index a6b91e50f..bc7c90fa7 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/Dominance.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/Dominance.cs @@ -140,7 +140,7 @@ namespace ICSharpCode.Decompiler.FlowAnalysis if (j.IsReachable && (j.Predecessors.Count >= 2 || (j.Predecessors.Count >= 1 && j.ImmediateDominator == null))) { // Add j to frontier of all predecessors and their dominators up to j's immediate dominator. foreach (var p in j.Predecessors) { - for (var runner = p; runner != j.ImmediateDominator && runner != j; runner = runner.ImmediateDominator) { + for (var runner = p; runner != j.ImmediateDominator && runner != j && runner != null; runner = runner.ImmediateDominator) { nonEmpty.Set(runner.UserIndex); } } diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 906752754..0696fc373 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -280,8 +280,11 @@ + + + @@ -547,9 +550,7 @@ - - - + Documentation\XML Documentation.html diff --git a/ICSharpCode.Decompiler/IL/BlockBuilder.cs b/ICSharpCode.Decompiler/IL/BlockBuilder.cs index bfa9d81af..929f9876d 100644 --- a/ICSharpCode.Decompiler/IL/BlockBuilder.cs +++ b/ICSharpCode.Decompiler/IL/BlockBuilder.cs @@ -187,7 +187,7 @@ namespace ICSharpCode.Decompiler.IL foreach (var block in container.Blocks) { ConnectBranches(block); if (block.Instructions.Count == 0 || !block.Instructions.Last().HasFlag(InstructionFlags.EndPointUnreachable)) { - block.Instructions.Add(new InvalidInstruction("Unexpected end of block")); + block.Instructions.Add(new InvalidBranch("Unexpected end of block")); } } containerStack.Pop(); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs index ee84fda70..54c8edb2a 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs @@ -79,6 +79,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow if (IsUsableBranchToChild(cfgNode, exitInst)) { // "...; goto usableblock;" // -> embed target block in this block + context.Step("Inline target block of unconditional branch", exitInst); var targetBlock = ((Branch)exitInst).TargetBlock; Debug.Assert(exitInst == block.Instructions.Last()); block.Instructions.RemoveAt(block.Instructions.Count - 1); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/DetectPinnedRegions.cs b/ICSharpCode.Decompiler/IL/ControlFlow/DetectPinnedRegions.cs index 8f8b94035..e7c839996 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/DetectPinnedRegions.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/DetectPinnedRegions.cs @@ -397,20 +397,21 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow void ReplacePinnedVar(ILVariable oldVar, ILVariable newVar, ILInstruction inst) { - Conv conv = inst as Conv; - if (conv != null && conv.Kind == ConversionKind.StopGCTracking && conv.Argument.MatchLdLoc(oldVar)) { + if (inst is Conv conv && conv.Kind == ConversionKind.StopGCTracking && conv.Argument.MatchLdLoc(oldVar)) { // conv ref->i (ldloc oldVar) // => ldloc newVar conv.AddILRange(conv.Argument.ILRange); conv.ReplaceWith(new LdLoc(newVar) { ILRange = conv.ILRange }); return; } - var iwvo = inst as IInstructionWithVariableOperand; - if (iwvo != null && iwvo.Variable == oldVar) { + if (inst is IInstructionWithVariableOperand iwvo && iwvo.Variable == oldVar) { iwvo.Variable = newVar; - if (inst is StLoc && oldVar.Type.Kind == TypeKind.ByReference) { - ((StLoc)inst).Value = new Conv(((StLoc)inst).Value, PrimitiveType.I, false, Sign.None); + if (inst is StLoc stloc && oldVar.Type.Kind == TypeKind.ByReference) { + stloc.Value = new Conv(stloc.Value, PrimitiveType.I, false, Sign.None); } + } else if (inst.MatchLdStr(out var val) && val == "Is this ILSpy?") { + inst.ReplaceWith(new LdStr("This is ILSpy!")); // easter egg ;) + return; } foreach (var child in inst.Children) { ReplacePinnedVar(oldVar, newVar, child); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs b/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs index a59304048..362d831f9 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs @@ -96,7 +96,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Debug.Assert(loop[0] == h); foreach (var node in loop) { node.Visited = false; // reset visited flag so that we can find outer loops - Debug.Assert(h.Dominates(node), "The loop body must be dominated by the loop head"); + Debug.Assert(h.Dominates(node) || !node.IsReachable, "The loop body must be dominated by the loop head"); } ConstructLoop(loop, exitPoint); } diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs b/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs new file mode 100644 index 000000000..ded58e325 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/ControlFlow/StateRangeAnalysis.cs @@ -0,0 +1,195 @@ +// Copyright (c) 2012 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.TypeSystem; +using ICSharpCode.Decompiler.Util; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.IL.ControlFlow +{ + enum StateRangeAnalysisMode + { + IteratorMoveNext, + IteratorDispose, + AsyncMoveNext + } + + /// + /// Symbolically executes code to determine which blocks are reachable for which values + /// of the 'state' field. + /// + /// + /// Assumption: there are no loops/backward jumps + /// We 'run' the code, with "state" being a symbolic variable + /// so it can form expressions like "state + x" (when there's a sub instruction) + /// + /// For each block, we maintain the set of values for state for which the block is reachable. + /// This is (int.MinValue, int.MaxValue) for the first instruction. + /// These ranges are propagated depending on the conditional jumps performed by the code. + /// + class StateRangeAnalysis + { + readonly StateRangeAnalysisMode mode; + readonly IField stateField; + readonly SymbolicEvaluationContext evalContext; + + readonly Dictionary ranges = new Dictionary(); + readonly internal Dictionary finallyMethodToStateRange; // used only for IteratorDispose + + public StateRangeAnalysis(StateRangeAnalysisMode mode, IField stateField, ILVariable cachedStateVar = null) + { + this.mode = mode; + this.stateField = stateField; + if (mode == StateRangeAnalysisMode.IteratorDispose) { + finallyMethodToStateRange = new Dictionary(); + } + + evalContext = new SymbolicEvaluationContext(stateField); + if (cachedStateVar != null) + evalContext.AddStateVariable(cachedStateVar); + } + + /// + /// Assign state ranges for all blocks within 'inst'. + /// + /// + /// The set of states for which the exit point of the instruction is reached. + /// This must be a subset of the input set. + /// + /// Returns an empty set for unsupported instructions. + /// + public LongSet AssignStateRanges(ILInstruction inst, LongSet stateRange) + { + switch (inst) { + case BlockContainer blockContainer: + AddStateRange(blockContainer.EntryPoint, stateRange); + foreach (var block in blockContainer.Blocks) { + // We assume that there are no jumps to blocks already processed. + // TODO: is SortBlocks() guaranteeing this, even if the user code has loops? + if (ranges.TryGetValue(block, out stateRange)) { + AssignStateRanges(block, stateRange); + } + } + // Since we don't track 'leave' edges, we can only conservatively + // return LongSet.Empty. + return LongSet.Empty; + case Block block: + foreach (var instInBlock in block.Instructions) { + if (stateRange.IsEmpty) + break; + var oldStateRange = stateRange; + stateRange = AssignStateRanges(instInBlock, stateRange); + // End-point can only be reachable in a subset of the states where the start-point is reachable. + Debug.Assert(stateRange.IsSubsetOf(oldStateRange)); + // If the end-point is unreachable, it must be reachable in no states. + Debug.Assert(stateRange.IsEmpty || !instInBlock.HasFlag(InstructionFlags.EndPointUnreachable)); + } + return stateRange; + case TryFinally tryFinally: + var afterTry = AssignStateRanges(tryFinally.TryBlock, stateRange); + // really finally should start with 'stateRange.UnionWith(afterTry)', but that's + // equal to 'stateRange'. + Debug.Assert(afterTry.IsSubsetOf(stateRange)); + var afterFinally = AssignStateRanges(tryFinally.FinallyBlock, stateRange); + return afterTry.IntersectWith(afterFinally); + case SwitchInstruction switchInst: + SymbolicValue val = evalContext.Eval(switchInst.Value); + if (val.Type != SymbolicValueType.State) + goto default; + List allSectionLabels = new List(); + List exitIntervals = new List(); + foreach (var section in switchInst.Sections) { + // switch (state + Constant) + // matches 'case VALUE:' + // iff (state + Constant == value) + // iff (state == value - Constant) + var effectiveLabels = section.Labels.AddOffset(unchecked(-val.Constant)); + allSectionLabels.AddRange(effectiveLabels.Intervals); + var result = AssignStateRanges(section.Body, stateRange.IntersectWith(effectiveLabels)); + exitIntervals.AddRange(result.Intervals); + } + var defaultSectionLabels = stateRange.ExceptWith(new LongSet(allSectionLabels)); + exitIntervals.AddRange(AssignStateRanges(switchInst.DefaultBody, defaultSectionLabels).Intervals); + // exitIntervals = union of exits of all sections + return new LongSet(exitIntervals); + case IfInstruction ifInst: + val = evalContext.Eval(ifInst.Condition).AsBool(); + LongSet trueRanges; + LongSet falseRanges; + if (val.Type == SymbolicValueType.StateEquals) { + trueRanges = stateRange.IntersectWith(new LongSet(val.Constant)); + falseRanges = stateRange.ExceptWith(new LongSet(val.Constant)); + } else if (val.Type == SymbolicValueType.StateInEquals) { + trueRanges = stateRange.ExceptWith(new LongSet(val.Constant)); + falseRanges = stateRange.IntersectWith(new LongSet(val.Constant)); + } else { + goto default; + } + var afterTrue = AssignStateRanges(ifInst.TrueInst, trueRanges); + var afterFalse = AssignStateRanges(ifInst.FalseInst, falseRanges); + return afterTrue.UnionWith(afterFalse); + case Branch br: + AddStateRange(br.TargetBlock, stateRange); + return LongSet.Empty; + case Nop nop: + return stateRange; + case StLoc stloc when stloc.Variable.IsSingleDefinition: + val = evalContext.Eval(stloc.Value); + if (val.Type == SymbolicValueType.State && val.Constant == 0) { + evalContext.AddStateVariable(stloc.Variable); + return stateRange; + } else { + goto default; // user code + } + case Call call when mode == StateRangeAnalysisMode.IteratorDispose: + // Call to finally method. + // Usually these are in finally blocks, but sometimes (e.g. foreach over array), + // the C# compiler puts the call to a finally method outside the try-finally block. + finallyMethodToStateRange.Add((IMethod)call.Method.MemberDefinition, stateRange); + return LongSet.Empty; // return Empty since we executed user code (the finally method) + default: + // User code - abort analysis + if (mode == StateRangeAnalysisMode.IteratorDispose && inst.OpCode != OpCode.Return) { + throw new SymbolicAnalysisFailedException("Unexpected instruction in Iterator.Dispose()"); + } + return LongSet.Empty; + } + } + + private void AddStateRange(Block block, LongSet stateRange) + { + if (ranges.TryGetValue(block, out var existingRange)) + ranges[block] = stateRange.UnionWith(existingRange); + else + ranges.Add(block, stateRange); + } + + public IEnumerable<(Block, LongSet)> GetBlockStateSetMapping(BlockContainer container) + { + foreach (var block in container.Blocks) { + if (ranges.TryGetValue(block, out var stateSet)) + yield return (block, stateSet); + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/SymbolicExecution.cs b/ICSharpCode.Decompiler/IL/ControlFlow/SymbolicExecution.cs new file mode 100644 index 000000000..c7721f2df --- /dev/null +++ b/ICSharpCode.Decompiler/IL/ControlFlow/SymbolicExecution.cs @@ -0,0 +1,161 @@ +// 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.TypeSystem; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ICSharpCode.Decompiler.IL.ControlFlow +{ + /// + /// This exception is thrown when we find something else than we expect from the C# compiler. + /// This aborts the analysis and makes the whole transform fail. + /// + class SymbolicAnalysisFailedException : Exception + { + public SymbolicAnalysisFailedException() { } + public SymbolicAnalysisFailedException(string message) : base(message) { } + } + + enum SymbolicValueType + { + /// + /// Unknown value + /// + Unknown, + /// + /// int: Constant (result of ldc.i4) + /// + IntegerConstant, + /// + /// int: State + Constant + /// + State, + /// + /// This pointer (result of ldarg.0) + /// + This, + /// + /// bool: State == Constant + /// + StateEquals, + /// + /// bool: State != Constant + /// + StateInEquals + } + + struct SymbolicValue + { + public readonly int Constant; + public readonly SymbolicValueType Type; + + public SymbolicValue(SymbolicValueType type, int constant = 0) + { + this.Type = type; + this.Constant = constant; + } + + public SymbolicValue AsBool() + { + if (Type == SymbolicValueType.State) { + // convert state integer to bool: + // if (state + c) = if (state + c != 0) = if (state != -c) + return new SymbolicValue(SymbolicValueType.StateInEquals, unchecked(-Constant)); + } + return this; + } + public override string ToString() + { + return string.Format("[SymbolicValue {0}: {1}]", this.Type, this.Constant); + } + } + + class SymbolicEvaluationContext + { + readonly IField stateField; + readonly List stateVariables = new List(); + + public SymbolicEvaluationContext(IField stateField) + { + this.stateField = stateField; + } + + public void AddStateVariable(ILVariable v) + { + if (!stateVariables.Contains(v)) + stateVariables.Add(v); + } + + static readonly SymbolicValue Failed = new SymbolicValue(SymbolicValueType.Unknown); + + public SymbolicValue Eval(ILInstruction inst) + { + if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub && !bni.CheckForOverflow) { + var left = Eval(bni.Left); + var right = Eval(bni.Right); + if (left.Type != SymbolicValueType.State && left.Type != SymbolicValueType.IntegerConstant) + return Failed; + if (right.Type != SymbolicValueType.IntegerConstant) + return Failed; + return new SymbolicValue(left.Type, unchecked(left.Constant - right.Constant)); + } else if (inst.MatchLdFld(out var target, out var field)) { + if (Eval(target).Type != SymbolicValueType.This) + return Failed; + if (field.MemberDefinition != stateField) + return Failed; + return new SymbolicValue(SymbolicValueType.State); + } else if (inst.MatchLdLoc(out var loadedVariable)) { + if (stateVariables.Contains(loadedVariable)) + return new SymbolicValue(SymbolicValueType.State); + else if (loadedVariable.Kind == VariableKind.Parameter && loadedVariable.Index < 0) + return new SymbolicValue(SymbolicValueType.This); + else + return Failed; + } else if (inst.MatchLdcI4(out var value)) { + return new SymbolicValue(SymbolicValueType.IntegerConstant, value); + } else if (inst is Comp comp) { + var left = Eval(comp.Left); + var right = Eval(comp.Right); + if (left.Type != SymbolicValueType.State || right.Type != SymbolicValueType.IntegerConstant) + return Failed; + // bool: (state + left.Constant == right.Constant) + // bool: (state == right.Constant - left.Constant) + if (comp.Kind == ComparisonKind.Equality) + return new SymbolicValue(SymbolicValueType.StateEquals, unchecked(right.Constant - left.Constant)); + else if (comp.Kind == ComparisonKind.Inequality) + return new SymbolicValue(SymbolicValueType.StateInEquals, unchecked(right.Constant - left.Constant)); + else + return Failed; + } else if (inst is LogicNot logicNot) { + SymbolicValue val = Eval(logicNot.Argument).AsBool(); + if (val.Type == SymbolicValueType.StateEquals) + return new SymbolicValue(SymbolicValueType.StateInEquals, val.Constant); + else if (val.Type == SymbolicValueType.StateInEquals) + return new SymbolicValue(SymbolicValueType.StateEquals, val.Constant); + else + return Failed; + } else { + return Failed; + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs new file mode 100644 index 000000000..f4ef21c70 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs @@ -0,0 +1,585 @@ +// 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.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; + + /// The type that contains the function being decompiled. + TypeDefinition currentType; + + /// The compiler-generated enumerator class. + /// Set in MatchEnumeratorCreationPattern() + TypeDefinition enumeratorType; + + /// The constructor of the compiler-generated enumerator class. + /// Set in MatchEnumeratorCreationPattern() + MethodDefinition enumeratorCtor; + + /// The dispose method of the compiler-generated enumerator class. + /// Set in ConstructExceptionTable() + MethodDefinition disposeMethod; + + /// The field in the compiler-generated class holding the current state of the state machine + /// Set in AnalyzeCtor() + IField stateField; + + /// The backing field of the 'Current' property in the compiler-generated class + /// Set in AnalyzeCurrentProperty() + IField currentField; + + /// Maps the fields of the compiler-generated class to the original parameters. + /// Set in MatchEnumeratorCreationPattern() and ResolveIEnumerableIEnumeratorFieldMapping() + readonly Dictionary fieldToParameterMap = new Dictionary(); + + /// 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. + /// Set in ConstructExceptionTable() + Dictionary finallyMethodToStateRange; + + /// + /// List of blocks that change the iterator state on block entry. + /// + readonly List<(int state, Block block)> stateChanges = new List<(int state, Block block)>(); + + #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.stateChanges.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().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); + + context.Step("Reconstruct try-finally blocks", function); + ReconstructTryFinallyBlocks(newBody); + + context.Step("Translate fields to local accesses", function); + TranslateFieldsToLocalAccess(function, function, fieldToParameterMap); + + // 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; + } + } + + /// + /// Matches the body of a method as a single basic block. + /// + 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; + } + + /// + /// Matches the newobj instruction that creates an instance of the compiler-generated enumerator helper class. + /// + 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()) + /// + /// Looks at the enumerator's ctor and figures out which of the fields holds the state. + /// + 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"); + } + + /// + /// Creates ILAst for the specified method, optimized up to before the 'YieldReturn' step. + /// + 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()) + /// + /// Looks at the enumerator's get_Current method and figures out which of the fields holds the current value. + /// + 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()) { + 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; + } + + /// + /// 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(); + /// + 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 + var ssaDefs = new Dictionary(); + + 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 (if necessary): + 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; + } +#if DEBUG + newBlock.Instructions.Add(new Nop { Comment = "iterator._state = " + newState }); +#endif + stateChanges.Add((newState, newBlock)); + } 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.MatchReturn(out value)) { + if (value.MatchLdLoc(out var v)) { + ssaDefs.TryGetValue(v, out value); + } + if (value.MatchLdcI4(0)) { + // yield break + newBlock.Instructions.Add(new Leave(newBody) { ILRange = oldInst.ILRange }); + } else { + newBlock.Instructions.Add(new InvalidBranch("Unexpected return in MoveNext()")); + } + break; // we're done with this basic block + } else if (oldInst.MatchStLoc(out var v, out value) && v.IsSingleDefinition) { + ssaDefs.Add(v, value); + } + // copy over the instruction to the new block + UpdateBranchTargets(oldInst); + newBlock.Instructions.Add(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)); + } + + 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; + } + foreach (var child in inst.Children) { + UpdateBranchTargets(child); + } + } + } + #endregion + + #region TranslateFieldsToLocalAccess + /// + /// Translates all field accesses in `function` to local variable accesses. + /// + internal static void TranslateFieldsToLocalAccess(ILFunction function, ILInstruction inst, Dictionary 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 Reconstruct try-finally blocks + + private void ReconstructTryFinallyBlocks(BlockContainer newBody) + { + // TODO + } + + #endregion + } +} diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 90125829d..27fd2aea3 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -335,7 +335,7 @@ namespace ICSharpCode.Decompiler.IL ILInstruction DecodeInstruction() { if (nextInstructionIndex >= body.Instructions.Count) - return new InvalidInstruction("Unexpected end of body"); + return new InvalidBranch("Unexpected end of body"); var cecilInst = body.Instructions[nextInstructionIndex++]; currentInstruction = cecilInst; switch (cecilInst.OpCode.Code) { @@ -792,7 +792,7 @@ namespace ICSharpCode.Decompiler.IL case Cil.Code.Unbox_Any: return Push(new UnboxAny(Pop(), ReadAndDecodeTypeReference())); default: - return new InvalidInstruction("Unknown opcode: " + cecilInst.OpCode.ToString()); + return new InvalidBranch("Unknown opcode: " + cecilInst.OpCode.ToString()); } } @@ -869,7 +869,7 @@ namespace ICSharpCode.Decompiler.IL ILInstruction Peek() { if (currentStack.IsEmpty) { - return new InvalidInstruction("Stack underflow") { ILRange = GetCurrentInstructionInterval() }; + return new InvalidExpression("Stack underflow") { ILRange = GetCurrentInstructionInterval() }; } return new LdLoc(currentStack.Peek()); } @@ -877,7 +877,7 @@ namespace ICSharpCode.Decompiler.IL ILInstruction Pop() { if (currentStack.IsEmpty) { - return new InvalidInstruction("Stack underflow") { ILRange = GetCurrentInstructionInterval() }; + return new InvalidExpression("Stack underflow") { ILRange = GetCurrentInstructionInterval() }; } ILVariable v; currentStack = currentStack.Pop(out v); @@ -892,8 +892,8 @@ namespace ICSharpCode.Decompiler.IL inst = new Conv(inst, PrimitiveType.I, false, Sign.None); } else if (expectedType == StackType.Ref && inst.ResultType == StackType.I) { // implicitly start GC tracking - } else if (inst is InvalidInstruction) { - ((InvalidInstruction)inst).ExpectedResultType = expectedType; + } else if (inst is InvalidExpression) { + ((InvalidExpression)inst).ExpectedResultType = expectedType; } else { Warn($"Expected {expectedType}, but got {inst.ResultType}"); } diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index d4c5a0923..020c1baa9 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -30,7 +30,9 @@ namespace ICSharpCode.Decompiler.IL public enum OpCode { /// Represents invalid IL. Semantically, this instruction is considered to throw some kind of exception. - InvalidInstruction, + InvalidBranch, + /// Represents invalid IL. Semantically, this instruction is considered to produce some kind of value. + InvalidExpression, /// No operation. Takes 0 arguments and returns void. Nop, /// A container of IL blocks. @@ -156,6 +158,8 @@ namespace ICSharpCode.Decompiler.IL RefAnyType, /// Push the address stored in a typed reference. RefAnyValue, + /// Yield an element from an iterator. + YieldReturn, /// Matches any node AnyNode, } @@ -448,36 +452,73 @@ namespace ICSharpCode.Decompiler.IL.Patterns namespace ICSharpCode.Decompiler.IL { /// Represents invalid IL. Semantically, this instruction is considered to throw some kind of exception. - public sealed partial class InvalidInstruction : SimpleInstruction + public sealed partial class InvalidBranch : SimpleInstruction { - public InvalidInstruction() : base(OpCode.InvalidInstruction) + public InvalidBranch() : base(OpCode.InvalidBranch) { } protected override InstructionFlags ComputeFlags() { - return InstructionFlags.MayThrow | InstructionFlags.EndPointUnreachable; + return InstructionFlags.MayThrow | InstructionFlags.SideEffect | InstructionFlags.EndPointUnreachable; } public override InstructionFlags DirectFlags { get { - return InstructionFlags.MayThrow | InstructionFlags.EndPointUnreachable; + return InstructionFlags.MayThrow | InstructionFlags.SideEffect | InstructionFlags.EndPointUnreachable; } } public override void AcceptVisitor(ILVisitor visitor) { - visitor.VisitInvalidInstruction(this); + visitor.VisitInvalidBranch(this); } public override T AcceptVisitor(ILVisitor visitor) { - return visitor.VisitInvalidInstruction(this); + return visitor.VisitInvalidBranch(this); } public override T AcceptVisitor(ILVisitor visitor, C context) { - return visitor.VisitInvalidInstruction(this, context); + return visitor.VisitInvalidBranch(this, context); } protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) { - var o = other as InvalidInstruction; + var o = other as InvalidBranch; + return o != null; + } + } +} +namespace ICSharpCode.Decompiler.IL +{ + /// Represents invalid IL. Semantically, this instruction is considered to produce some kind of value. + public sealed partial class InvalidExpression : SimpleInstruction + { + public InvalidExpression() : base(OpCode.InvalidExpression) + { + } + + protected override InstructionFlags ComputeFlags() + { + return InstructionFlags.MayThrow | InstructionFlags.SideEffect; + } + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.MayThrow | InstructionFlags.SideEffect; + } + } + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitInvalidExpression(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitInvalidExpression(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitInvalidExpression(this, context); + } + protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) + { + var o = other as InvalidExpression; return o != null; } } @@ -2442,7 +2483,7 @@ namespace ICSharpCode.Decompiler.IL clone.Target = this.target.Clone(); return clone; } - public bool DelayExceptions; + public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced readonly IField field; /// Returns the field operand. public IField Field { get { return field; } } @@ -3406,7 +3447,7 @@ namespace ICSharpCode.Decompiler.IL clone.Indices.AddRange(this.Indices.Select(arg => arg.Clone())); return clone; } - public bool DelayExceptions; + public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced public override StackType ResultType { get { return StackType.Ref; } } /// Gets whether the 'readonly' prefix was applied to this instruction. public bool IsReadOnly { get; set; } @@ -3665,6 +3706,98 @@ namespace ICSharpCode.Decompiler.IL } } } +namespace ICSharpCode.Decompiler.IL +{ + /// Yield an element from an iterator. + public sealed partial class YieldReturn : ILInstruction + { + public YieldReturn(ILInstruction value) : base(OpCode.YieldReturn) + { + this.Value = value; + } + public static readonly SlotInfo ValueSlot = new SlotInfo("Value", canInlineInto: true); + ILInstruction value; + public ILInstruction Value { + get { return this.value; } + set { + ValidateChild(value); + SetChildInstruction(ref this.value, value, 0); + } + } + protected sealed override int GetChildCount() + { + return 1; + } + protected sealed override ILInstruction GetChild(int index) + { + switch (index) { + case 0: + return this.value; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override void SetChild(int index, ILInstruction value) + { + switch (index) { + case 0: + this.Value = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override SlotInfo GetChildSlot(int index) + { + switch (index) { + case 0: + return ValueSlot; + default: + throw new IndexOutOfRangeException(); + } + } + public sealed override ILInstruction Clone() + { + var clone = (YieldReturn)ShallowClone(); + clone.Value = this.value.Clone(); + return clone; + } + public override StackType ResultType { get { return StackType.Void; } } + protected override InstructionFlags ComputeFlags() + { + return InstructionFlags.MayBranch | InstructionFlags.SideEffect | value.Flags; + } + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.MayBranch | InstructionFlags.SideEffect; + } + } + public override void WriteTo(ITextOutput output) + { + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output); + output.Write(')'); + } + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitYieldReturn(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitYieldReturn(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitYieldReturn(this, context); + } + protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) + { + var o = other as YieldReturn; + return o != null && this.value.PerformMatch(o.value, ref match); + } + } +} namespace ICSharpCode.Decompiler.IL.Patterns { /// Matches any node @@ -3719,7 +3852,11 @@ namespace ICSharpCode.Decompiler.IL /// Called by Visit*() methods that were not overridden protected abstract void Default(ILInstruction inst); - protected internal virtual void VisitInvalidInstruction(InvalidInstruction inst) + protected internal virtual void VisitInvalidBranch(InvalidBranch inst) + { + Default(inst); + } + protected internal virtual void VisitInvalidExpression(InvalidExpression inst) { Default(inst); } @@ -3971,6 +4108,10 @@ namespace ICSharpCode.Decompiler.IL { Default(inst); } + protected internal virtual void VisitYieldReturn(YieldReturn inst) + { + Default(inst); + } } /// @@ -3981,7 +4122,11 @@ namespace ICSharpCode.Decompiler.IL /// Called by Visit*() methods that were not overridden protected abstract T Default(ILInstruction inst); - protected internal virtual T VisitInvalidInstruction(InvalidInstruction inst) + protected internal virtual T VisitInvalidBranch(InvalidBranch inst) + { + return Default(inst); + } + protected internal virtual T VisitInvalidExpression(InvalidExpression inst) { return Default(inst); } @@ -4233,6 +4378,10 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst); } + protected internal virtual T VisitYieldReturn(YieldReturn inst) + { + return Default(inst); + } } /// @@ -4243,7 +4392,11 @@ namespace ICSharpCode.Decompiler.IL /// Called by Visit*() methods that were not overridden protected abstract T Default(ILInstruction inst, C context); - protected internal virtual T VisitInvalidInstruction(InvalidInstruction inst, C context) + protected internal virtual T VisitInvalidBranch(InvalidBranch inst, C context) + { + return Default(inst, context); + } + protected internal virtual T VisitInvalidExpression(InvalidExpression inst, C context) { return Default(inst, context); } @@ -4495,12 +4648,17 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst, context); } + protected internal virtual T VisitYieldReturn(YieldReturn inst, C context) + { + return Default(inst, context); + } } partial class InstructionOutputExtensions { static readonly string[] originalOpCodeNames = { - "invalid", + "invalid.branch", + "invalid.expr", "nop", "ILFunction", "BlockContainer", @@ -4563,15 +4721,24 @@ namespace ICSharpCode.Decompiler.IL "mkrefany", "refanytype", "refanyval", + "yield.return", "AnyNode", }; } partial class ILInstruction { - public bool MatchInvalidInstruction() + public bool MatchInvalidBranch() + { + var inst = this as InvalidBranch; + if (inst != null) { + return true; + } + return false; + } + public bool MatchInvalidExpression() { - var inst = this as InvalidInstruction; + var inst = this as InvalidExpression; if (inst != null) { return true; } @@ -5023,6 +5190,16 @@ namespace ICSharpCode.Decompiler.IL type = default(IType); return false; } + public bool MatchYieldReturn(out ILInstruction value) + { + var inst = this as YieldReturn; + if (inst != null) { + value = inst.Value; + return true; + } + value = default(ILInstruction); + return false; + } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 0e170f37e..1a8756175 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -37,10 +37,12 @@ }; OpCode[] opCodes = { - new OpCode("invalid", "Represents invalid IL. Semantically, this instruction is considered to throw some kind of exception.", - CustomClassName("InvalidInstruction"), NoArguments, MayThrow, HasFlag("InstructionFlags.EndPointUnreachable")), + new OpCode("invalid.branch", "Represents invalid IL. Semantically, this instruction is considered to throw some kind of exception.", + CustomClassName("InvalidBranch"), NoArguments, MayThrow, SideEffect, HasFlag("InstructionFlags.EndPointUnreachable")), + new OpCode("invalid.expr", "Represents invalid IL. Semantically, this instruction is considered to produce some kind of value.", + CustomClassName("InvalidExpression"), NoArguments, MayThrow, SideEffect), new OpCode("nop", "No operation. Takes 0 arguments and returns void.", - VoidResult, NoArguments), + VoidResult, NoArguments, CustomWriteTo), new OpCode("ILFunction", "A container of IL blocks.", CustomChildren(new [] { new ChildInfo("body") @@ -211,6 +213,12 @@ new OpCode("refanyval", "Push the address stored in a typed reference.", CustomClassName("RefAnyValue"), Unary, HasTypeOperand, MayThrow, ResultType("Ref")), + new OpCode("yield.return", "Yield an element from an iterator.", + MayBranch, // yield return may end up returning if the consumer disposes the iterator without + SideEffect, // consumer can have arbitrary side effects while we're yielding + CustomArguments("value"), VoidResult), + // note: "yield break" is always represented using a "leave" instruction + // patterns new OpCode("AnyNode", "Matches any node", Pattern, CustomArguments(), CustomConstructor), }; @@ -552,7 +560,7 @@ namespace ICSharpCode.Decompiler.IL static Action ControlFlow = HasFlag("InstructionFlags.ControlFlow"); static Action MayThrowIfNotDelayed = HasFlag("(DelayExceptions ? InstructionFlags.None : InstructionFlags.MayThrow)") + (opCode => { - opCode.Members.Add("public bool DelayExceptions;"); + opCode.Members.Add("public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced"); }); static Action BaseClass(string name) diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs index f38f3af93..52a10555b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs @@ -34,6 +34,15 @@ namespace ICSharpCode.Decompiler.IL public readonly MethodDefinition Method; public readonly ILVariableCollection Variables; + /// + /// Gets whether this function is a decompiled iterator (is using yield). + /// This flag gets set by the YieldReturnDecompiler. + /// + /// If set, the 'return' instruction has the semantics of 'yield break;' + /// instead of a normal return. + /// + public bool IsIterator; + public ILFunction(MethodDefinition method, ILInstruction body) : base(OpCode.ILFunction) { this.Body = body; @@ -65,6 +74,10 @@ namespace ICSharpCode.Decompiler.IL output.WriteLine(" {"); output.Indent(); + if (IsIterator) { + output.WriteLine(".iterator"); + } + output.MarkFoldStart(Variables.Count + " variable(s)", true); foreach (var variable in Variables) { variable.WriteDefinitionTo(output); diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs index f46f85c00..4c2dde78e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs @@ -312,7 +312,7 @@ namespace ICSharpCode.Decompiler.IL readonly int end; int pos; - public ChildrenEnumerator(ILInstruction inst) + internal ChildrenEnumerator(ILInstruction inst) { Debug.Assert(inst != null); this.inst = inst; @@ -443,10 +443,10 @@ namespace ICSharpCode.Decompiler.IL /// /// /// This property returns true if the ILInstruction is reachable from the root node - /// of the ILAst; it does make use of the Parent field so the considerations + /// of the ILAst; it does not make use of the Parent field so the considerations /// about orphaned nodes and stale positions don't apply. /// - protected bool IsConnected { + protected internal bool IsConnected { get { return refCount > 0; } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs index 491e6bb16..ee24dfa8d 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs @@ -111,6 +111,17 @@ namespace ICSharpCode.Decompiler.IL var inst = this as Leave; return inst != null && inst.TargetContainer == targetContainer; } + + public bool MatchReturn(out ILInstruction returnValue) + { + if (this is Return ret) { + returnValue = ret.ReturnValue; + return returnValue != null; + } else { + returnValue = null; + return false; + } + } public bool MatchIfInstruction(out ILInstruction condition, out ILInstruction trueInst, out ILInstruction falseInst) { @@ -216,28 +227,27 @@ namespace ICSharpCode.Decompiler.IL public bool MatchLdsFld(IField field) { - LdsFlda ldsflda = (this as LdObj)?.Target as LdsFlda; - if (ldsflda != null) { + if (this is LdObj ldobj && ldobj.Target is LdsFlda ldsflda) { return field.Equals(ldsflda.Field); } return false; } - public bool MatchLdFld(out IField field) + public bool MatchLdFld(out ILInstruction target, out IField field) { - LdFlda ldflda = (this as LdObj)?.Target as LdFlda; - if (ldflda != null) { + if (this is LdObj ldobj && ldobj.Target is LdFlda ldflda) { + target = ldflda.Target; field = ldflda.Field; return true; } + target = null; field = null; return false; } public bool MatchLdsFld(out IField field) { - LdsFlda ldsflda = (this as LdObj)?.Target as LdsFlda; - if (ldsflda != null) { + if (this is LdObj ldobj && ldobj.Target is LdsFlda ldsflda) { field = ldsflda.Field; return true; } @@ -245,31 +255,29 @@ namespace ICSharpCode.Decompiler.IL return false; } - public bool MatchStsFld(out ILInstruction value, out IField field) - { - var stobj = this as StObj; - LdsFlda ldsflda = stobj?.Target as LdsFlda; - if (ldsflda != null) { - value = stobj.Value; + public bool MatchStsFld(out IField field, out ILInstruction value) + { + if (this is StObj stobj && stobj.Target is LdsFlda ldsflda) { field = ldsflda.Field; + value = stobj.Value; return true; } - value = null; field = null; + value = null; return false; } - public bool MatchStFld(out ILInstruction value, out IField field) - { - var stobj = this as StObj; - LdFlda ldflda = stobj?.Target as LdFlda; - if (ldflda != null) { - value = stobj.Value; + public bool MatchStFld(out ILInstruction target, out IField field, out ILInstruction value) + { + if (this is StObj stobj && stobj.Target is LdFlda ldflda) { + target = ldflda.Target; field = ldflda.Field; + value = stobj.Value; return true; } - value = null; + target = null; field = null; + value = null; return false; } diff --git a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs index 457be97ef..572d5d702 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs @@ -31,12 +31,25 @@ namespace ICSharpCode.Decompiler.IL } } - partial class InvalidInstruction : SimpleInstruction + partial class Nop + { + public string Comment; + + public override void WriteTo(ITextOutput output) + { + output.Write(OpCode); + if (!string.IsNullOrEmpty(Comment)) { + output.Write(" // " + Comment); + } + } + } + + partial class InvalidBranch : SimpleInstruction { public string Message; public StackType ExpectedResultType = StackType.Unknown; - public InvalidInstruction(string message) : this() + public InvalidBranch(string message) : this() { this.Message = message; } @@ -55,4 +68,30 @@ namespace ICSharpCode.Decompiler.IL } } } + + partial class InvalidExpression : SimpleInstruction + { + public string Message; + public StackType ExpectedResultType = StackType.Unknown; + + public InvalidExpression(string message) : this() + { + this.Message = message; + } + + public override StackType ResultType + { + get { return ExpectedResultType; } + } + + public override void WriteTo(ITextOutput output) + { + output.Write(OpCode); + if (!string.IsNullOrEmpty(Message)) { + output.Write('('); + output.Write(Message); + output.Write(')'); + } + } + } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs index b9cb27850..1ab5ed5dd 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs @@ -67,7 +67,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms var storeInst = trueInst.Instructions[0]; if (!inst.Condition.MatchCompEquals(out ILInstruction left, out ILInstruction right) || !left.MatchLdsFld(out IField field) || !right.MatchLdNull()) return false; - if (!storeInst.MatchStsFld(out ILInstruction value, out IField field2) || !field.Equals(field2) || !field.IsCompilerGeneratedOrIsInCompilerGeneratedClass()) + if (!storeInst.MatchStsFld(out IField field2, out ILInstruction value) || !field.Equals(field2) || !field.IsCompilerGeneratedOrIsInCompilerGeneratedClass()) return false; if (!DelegateConstruction.IsDelegateConstruction(value as NewObj, true)) return false; @@ -106,7 +106,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms // the optional field store was moved into storeInst by inline assignment: if (!(value is NewObj)) { IField field, field2; - if (value.MatchStsFld(out value2, out field)) { + if (value.MatchStsFld(out field, out value2)) { if (!(value2 is NewObj) || !field.IsCompilerGeneratedOrIsInCompilerGeneratedClass()) return false; var storeBeforeIf = inst.Parent.Children.ElementAtOrDefault(inst.ChildIndex - 1) as StLoc; @@ -114,11 +114,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; value = value2; hasFieldStore = true; - } else if (value.MatchStFld(out value2, out field)) { + } else if (value.MatchStFld(out var target, out field, out value2)) { + // TODO: shouldn't we test 'target'? if (!(value2 is NewObj) || !field.IsCompilerGeneratedOrIsInCompilerGeneratedClass()) return false; var storeBeforeIf = inst.Parent.Children.ElementAtOrDefault(inst.ChildIndex - 1) as StLoc; - if (storeBeforeIf == null || storeBeforeIf.Variable != v || !storeBeforeIf.Value.MatchLdFld(out field2) || !field.Equals(field2)) + if (storeBeforeIf == null || storeBeforeIf.Variable != v || !storeBeforeIf.Value.MatchLdFld(out var target2, out field2) || !field.Equals(field2)) return false; value = value2; hasFieldStore = true; diff --git a/ICSharpCode.Decompiler/IL/Transforms/LoopingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/LoopingTransform.cs index f216cb49b..ba2c307a3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LoopingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LoopingTransform.cs @@ -16,45 +16,19 @@ // 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; namespace ICSharpCode.Decompiler.IL.Transforms { - /// - /// Repeats the child transforms until the ILAst no longer changes. - /// - public class LoopingTransform : IILTransform - { - readonly IILTransform[] children; - - public LoopingTransform(params IILTransform[] children) - { - this.children = children; - } - - public void Run(ILFunction function, ILTransformContext context) - { - do { - function.ResetDirty(); - function.RunTransforms(children, context); - if (function.IsDirty) - context.Step("Function is dirty; running another loop iteration.", function); - } while (function.IsDirty); - } - - public IReadOnlyCollection Transforms - { - get { return children; } - } - } - /// /// Repeats the child transforms until the ILAst no longer changes. /// public class LoopingBlockTransform : IBlockTransform { readonly IBlockTransform[] children; - + bool running; + public LoopingBlockTransform(params IBlockTransform[] children) { this.children = children; @@ -62,12 +36,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms public void Run(Block block, BlockTransformContext context) { + if (running) + throw new InvalidOperationException("LoopingBlockTransform already running. Transforms (and the CSharpDecompiler) are neither neither thread-safe nor re-entrant."); + running = true; do { block.ResetDirty(); block.RunTransforms(children, context); if (block.IsDirty) context.Step("Block is dirty; running another loop iteration.", block); } while (block.IsDirty); + running = false; } public IReadOnlyCollection Transforms { diff --git a/ICSharpCode.Decompiler/Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler/Tests/RoundtripAssembly.cs index 35919b2ec..1b07d879d 100644 --- a/ICSharpCode.Decompiler/Tests/RoundtripAssembly.cs +++ b/ICSharpCode.Decompiler/Tests/RoundtripAssembly.cs @@ -251,7 +251,7 @@ namespace ICSharpCode.Decompiler.Tests { if (asm == null) return false; - return !localAssemblies.Contains(asm.MainModule.FullyQualifiedName); + return !localAssemblies.Contains(asm.MainModule.FileName); } } diff --git a/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs b/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs index 5fe898780..0bc27cd13 100644 --- a/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs +++ b/ICSharpCode.Decompiler/Tests/TestCases/Pretty/YieldReturn.cs @@ -21,8 +21,10 @@ using System.Collections.Generic; namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { - public static class YieldReturn + public class YieldReturn { + int fieldOnThis; + public static IEnumerable SimpleYieldReturn() { yield return "A"; @@ -30,6 +32,25 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty yield return "C"; } + public static IEnumerator SimpleYieldReturnEnumerator() + { + yield return "A"; + yield return "B"; + yield return "C"; + } + + public IEnumerable YieldReturnParameters(int p) + { + yield return p; + yield return fieldOnThis; + } + + public IEnumerator YieldReturnParametersEnumerator(int p) + { + yield return p; + yield return fieldOnThis; + } + public static IEnumerable YieldReturnInLoop() { for (int i = 0; i < 100; i++) { @@ -147,5 +168,27 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty yield return 'b'; yield return 'c'; } + + + public static IEnumerable ExceptionHandling() + { + yield return 'a'; + try { + Console.WriteLine("1 - try"); + } catch (Exception) { + Console.WriteLine("1 - catch"); + } + yield return 'b'; + try { + try { + Console.WriteLine("2 - try"); + } finally { + Console.WriteLine("2 - finally"); + } + yield return 'c'; + } finally { + Console.WriteLine("outer finally"); + } + } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/TypeSystem/CecilLoader.cs b/ICSharpCode.Decompiler/TypeSystem/CecilLoader.cs index 322c7e5f7..513550af8 100644 --- a/ICSharpCode.Decompiler/TypeSystem/CecilLoader.cs +++ b/ICSharpCode.Decompiler/TypeSystem/CecilLoader.cs @@ -176,9 +176,9 @@ namespace ICSharpCode.Decompiler.TypeSystem moduleAttributes = interningProvider.InternList(moduleAttributes); this.currentAssembly = new DefaultUnresolvedAssembly(assemblyDefinition != null ? assemblyDefinition.Name.FullName : moduleDefinition.Name); - currentAssembly.Location = moduleDefinition.FullyQualifiedName; - ExtensionMethods.AddRange(currentAssembly.AssemblyAttributes, assemblyAttributes); - ExtensionMethods.AddRange(currentAssembly.ModuleAttributes, assemblyAttributes); + currentAssembly.Location = moduleDefinition.FileName; + currentAssembly.AssemblyAttributes.AddRange(assemblyAttributes); + currentAssembly.ModuleAttributes.AddRange(assemblyAttributes); // Register type forwarders: foreach (ExportedType type in moduleDefinition.ExportedTypes) { @@ -831,7 +831,7 @@ namespace ICSharpCode.Decompiler.TypeSystem return; // https://github.com/icsharpcode/SharpDevelop/issues/284 } var blobSecDecl = new UnresolvedSecurityDeclarationBlob((int)secDecl.Action, blob); - ExtensionMethods.AddRange(targetCollection, blobSecDecl.UnresolvedAttributes); + targetCollection.AddRange(blobSecDecl.UnresolvedAttributes); } #endregion #endregion diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultUnresolvedAttribute.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultUnresolvedAttribute.cs index b17ce3795..024197767 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultUnresolvedAttribute.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultUnresolvedAttribute.cs @@ -47,7 +47,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation if (attributeType == null) throw new ArgumentNullException("attributeType"); this.attributeType = attributeType; - ExtensionMethods.AddRange(this.ConstructorParameterTypes, constructorParameterTypes); + this.ConstructorParameterTypes.AddRange(constructorParameterTypes); } protected override void FreezeInternal() diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index 106f837da..fab533a31 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -787,5 +787,32 @@ namespace ICSharpCode.Decompiler.TypeSystem return null; } #endregion + + public static IType GetElementTypeFromIEnumerable(this IType collectionType, ICompilation compilation, bool allowIEnumerator, out bool? isGeneric) + { + bool foundNonGenericIEnumerable = false; + foreach (IType baseType in collectionType.GetAllBaseTypes()) { + ITypeDefinition baseTypeDef = baseType.GetDefinition(); + if (baseTypeDef != null) { + KnownTypeCode typeCode = baseTypeDef.KnownTypeCode; + if (typeCode == KnownTypeCode.IEnumerableOfT || (allowIEnumerator && typeCode == KnownTypeCode.IEnumeratorOfT)) { + ParameterizedType pt = baseType as ParameterizedType; + if (pt != null) { + isGeneric = true; + return pt.GetTypeArgument(0); + } + } + if (typeCode == KnownTypeCode.IEnumerable || (allowIEnumerator && typeCode == KnownTypeCode.IEnumerator)) + foundNonGenericIEnumerable = true; + } + } + // System.Collections.IEnumerable found in type hierarchy -> Object is element type. + if (foundNonGenericIEnumerable) { + isGeneric = false; + return compilation.FindType(KnownTypeCode.Object); + } + isGeneric = null; + return SpecialType.UnknownType; + } } } diff --git a/ICSharpCode.Decompiler/Util/CollectionExtensions.cs b/ICSharpCode.Decompiler/Util/CollectionExtensions.cs index d79536b7f..b4ba4dc54 100644 --- a/ICSharpCode.Decompiler/Util/CollectionExtensions.cs +++ b/ICSharpCode.Decompiler/Util/CollectionExtensions.cs @@ -5,6 +5,12 @@ namespace ICSharpCode.Decompiler.Util { static class CollectionExtensions { + public static void Deconstruct(this KeyValuePair pair, out K key, out V value) + { + key = pair.Key; + value = pair.Value; + } + public static HashSet ToHashSet(this IEnumerable input) { return new HashSet(input); diff --git a/ICSharpCode.Decompiler/Util/ExtensionMethods.cs b/ICSharpCode.Decompiler/Util/ExtensionMethods.cs index 5da0357bb..a5e754140 100644 --- a/ICSharpCode.Decompiler/Util/ExtensionMethods.cs +++ b/ICSharpCode.Decompiler/Util/ExtensionMethods.cs @@ -26,12 +26,6 @@ namespace ICSharpCode.Decompiler.Util /// static class ExtensionMethods { - public static void AddRange(this ICollection target, IEnumerable input) - { - foreach (T item in input) - target.Add(item); - } - public static Predicate And(this Predicate filter1, Predicate filter2) { if (filter1 == null) diff --git a/ICSharpCode.Decompiler/Util/LongSet.cs b/ICSharpCode.Decompiler/Util/LongSet.cs index c26e3828b..17790360e 100644 --- a/ICSharpCode.Decompiler/Util/LongSet.cs +++ b/ICSharpCode.Decompiler/Util/LongSet.cs @@ -29,12 +29,21 @@ namespace ICSharpCode.Decompiler.Util /// public struct LongSet : IEquatable { + /// + /// The intervals in this set of longs. + /// + /// + /// Invariant: the intervals in this array are non-empty, non-overlapping, non-touching, and sorted. + /// + /// This invariant ensures every LongSet is always in a normalized representation. + /// public readonly ImmutableArray Intervals; - - public LongSet(ImmutableArray intervals) + + private LongSet(ImmutableArray intervals) { this.Intervals = intervals; #if DEBUG + // Check invariant long minValue = long.MinValue; for (int i = 0; i < intervals.Length; i++) { Debug.Assert(!intervals[i].IsEmpty); @@ -57,7 +66,7 @@ namespace ICSharpCode.Decompiler.Util : this(ImmutableArray.Create(LongInterval.Inclusive(value, value))) { } - + /// /// Create a new LongSet that contains the values from the interval. /// @@ -66,6 +75,14 @@ namespace ICSharpCode.Decompiler.Util { } + /// + /// Creates a new LongSet the contains the values from the specified intervals. + /// + public LongSet(IEnumerable intervals) + : this(MergeOverlapping(intervals.Where(i => !i.IsEmpty).OrderBy(i => i.Start)).ToImmutableArray()) + { + } + /// /// The empty LongSet. /// @@ -233,7 +250,34 @@ namespace ICSharpCode.Decompiler.Util } return new LongSet(newIntervals.ToImmutableArray()); } - + + /// + /// Gets whether this set is a subset of other, or equal. + /// + public bool IsSubsetOf(LongSet other) + { + // TODO: optimize IsSubsetOf -- there's no need to build a temporary set + return this.UnionWith(other).SetEquals(other); + } + + /// + /// Gets whether this set is a superset of other, or equal. + /// + public bool IsSupersetOf(LongSet other) + { + return other.IsSubsetOf(this); + } + + public bool IsProperSubsetOf(LongSet other) + { + return IsSubsetOf(other) && !SetEquals(other); + } + + public bool IsProperSupersetOf(LongSet other) + { + return IsSupersetOf(other) && !SetEquals(other); + } + public bool Contains(long val) { int index = upper_bound(val); diff --git a/ILSpy/ILSpyTraceListener.cs b/ILSpy/ILSpyTraceListener.cs index 3009419cc..96274ba52 100644 --- a/ILSpy/ILSpyTraceListener.cs +++ b/ILSpy/ILSpyTraceListener.cs @@ -21,6 +21,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using ICSharpCode.ILSpy.Controls; +using System.Linq; namespace ICSharpCode.ILSpy { @@ -49,12 +50,19 @@ namespace ICSharpCode.ILSpy public override void Fail(string message, string detailMessage) { base.Fail(message, detailMessage); // let base class write the assert to the debug console + string topFrame = ""; string stackTrace = ""; try { stackTrace = new StackTrace(true).ToString(); + var frames = stackTrace.Split('\r', '\n') + .Where(f => f.Length > 0) + .SkipWhile(f => f.Contains("ILSpyTraceListener") || f.Contains("System.Diagnostics")) + .ToList(); + topFrame = frames[0]; + stackTrace = string.Join(Environment.NewLine, frames); } catch { } lock (ignoredStacks) { - if (ignoredStacks.Contains(stackTrace)) + if (ignoredStacks.Contains(topFrame)) return; if (dialogIsOpen) return; @@ -76,7 +84,7 @@ namespace ICSharpCode.ILSpy } else if (result == 2) { // ignore } else if (result == 3) { lock (ignoredStacks) { - ignoredStacks.Add(stackTrace); + ignoredStacks.Add(topFrame); } } } diff --git a/ILSpy/XmlDoc/XmlDocLoader.cs b/ILSpy/XmlDoc/XmlDocLoader.cs index 4e2d74367..e81a79230 100644 --- a/ILSpy/XmlDoc/XmlDocLoader.cs +++ b/ILSpy/XmlDoc/XmlDocLoader.cs @@ -54,9 +54,9 @@ namespace ICSharpCode.ILSpy.XmlDoc lock (cache) { XmlDocumentationProvider xmlDoc; if (!cache.TryGetValue(module, out xmlDoc)) { - string xmlDocFile = LookupLocalizedXmlDoc(module.FullyQualifiedName); + string xmlDocFile = LookupLocalizedXmlDoc(module.FileName); if (xmlDocFile == null) { - xmlDocFile = FindXmlDocumentation(Path.GetFileName(module.FullyQualifiedName), module.Runtime); + xmlDocFile = FindXmlDocumentation(Path.GetFileName(module.FileName), module.Runtime); } if (xmlDocFile != null) { xmlDoc = new XmlDocumentationProvider(xmlDocFile);