// 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 System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Mono.Cecil; namespace ICSharpCode.Decompiler.ILAst { /// /// Performs inlining transformations. /// public class ILInlining { readonly ILBlock method; internal Dictionary numStloc = new Dictionary(); internal Dictionary numLdloc = new Dictionary(); internal Dictionary numLdloca = new Dictionary(); public ILInlining(ILBlock method) { this.method = method; AnalyzeMethod(); } void AnalyzeMethod() { numStloc.Clear(); numLdloc.Clear(); numLdloca.Clear(); // Analyse the whole method AnalyzeNode(method); } /// /// For each variable reference, adds to the num* dicts. /// Direction will be 1 for analysis, and -1 when removing a node from analysis. /// void AnalyzeNode(ILNode node, int direction = 1) { ILExpression expr = node as ILExpression; if (expr != null) { ILVariable locVar = expr.Operand as ILVariable; if (locVar != null) { if (expr.Code == ILCode.Stloc) { numStloc[locVar] = numStloc.GetOrDefault(locVar) + direction; } else if (expr.Code == ILCode.Ldloc) { numLdloc[locVar] = numLdloc.GetOrDefault(locVar) + direction; } else if (expr.Code == ILCode.Ldloca) { numLdloca[locVar] = numLdloca.GetOrDefault(locVar) + direction; } else { throw new NotSupportedException(expr.Code.ToString()); } } foreach (ILExpression child in expr.Arguments) AnalyzeNode(child, direction); } else { var catchBlock = node as ILTryCatchBlock.CatchBlock; if (catchBlock != null && catchBlock.ExceptionVariable != null) { numStloc[catchBlock.ExceptionVariable] = numStloc.GetOrDefault(catchBlock.ExceptionVariable) + direction; } foreach (ILNode child in node.GetChildren()) AnalyzeNode(child, direction); } } public bool InlineAllVariables() { bool modified = false; ILInlining i = new ILInlining(method); foreach (ILBlock block in method.GetSelfAndChildrenRecursive()) modified |= i.InlineAllInBlock(block); return modified; } public bool InlineAllInBlock(ILBlock block) { bool modified = false; List body = block.Body; if (block is ILTryCatchBlock.CatchBlock && body.Count > 1) { ILVariable v = ((ILTryCatchBlock.CatchBlock)block).ExceptionVariable; if (v != null && v.IsGenerated) { if (numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 1 && numLdloc.GetOrDefault(v) == 1) { ILVariable v2; ILExpression ldException; if (body[0].Match(ILCode.Stloc, out v2, out ldException) && ldException.MatchLdloc(v)) { body.RemoveAt(0); ((ILTryCatchBlock.CatchBlock)block).ExceptionVariable = v2; modified = true; } } } } for(int i = 0; i < body.Count - 1;) { ILVariable locVar; ILExpression expr; if (body[i].Match(ILCode.Stloc, out locVar, out expr) && InlineOneIfPossible(block.Body, i, aggressive: false)) { modified = true; i = Math.Max(0, i - 1); // Go back one step } else { i++; } } foreach(ILBasicBlock bb in body.OfType()) { modified |= InlineAllInBasicBlock(bb); } return modified; } public bool InlineAllInBasicBlock(ILBasicBlock bb) { bool modified = false; List body = bb.Body; for(int i = 0; i < body.Count;) { ILVariable locVar; ILExpression expr; if (body[i].Match(ILCode.Stloc, out locVar, out expr) && InlineOneIfPossible(bb.Body, i, aggressive: false)) { modified = true; i = Math.Max(0, i - 1); // Go back one step } else { i++; } } return modified; } /// /// Inlines instructions before pos into block.Body[pos]. /// /// The number of instructions that were inlined. public int InlineInto(List body, int pos, bool aggressive) { if (pos >= body.Count) return 0; int count = 0; while (--pos >= 0) { ILExpression expr = body[pos] as ILExpression; if (expr == null || expr.Code != ILCode.Stloc) break; if (InlineOneIfPossible(body, pos, aggressive)) count++; else break; } return count; } /// /// Aggressively inlines the stloc instruction at block.Body[pos] into the next instruction, if possible. /// If inlining was possible; we will continue to inline (non-aggressively) into the the combined instruction. /// /// /// After the operation, pos will point to the new combined instruction. /// public bool InlineIfPossible(List body, ref int pos) { if (InlineOneIfPossible(body, pos, true)) { pos -= InlineInto(body, pos, false); return true; } return false; } /// /// Inlines the stloc instruction at block.Body[pos] into the next instruction, if possible. /// public bool InlineOneIfPossible(List body, int pos, bool aggressive) { ILVariable v; ILExpression inlinedExpression; if (body[pos].Match(ILCode.Stloc, out v, out inlinedExpression) && !v.IsPinned) { if (InlineIfPossible(v, inlinedExpression, body.ElementAtOrDefault(pos+1), aggressive)) { // Assign the ranges of the stloc instruction: inlinedExpression.ILRanges.AddRange(((ILExpression)body[pos]).ILRanges); // Remove the stloc instruction: body.RemoveAt(pos); return true; } else if (numLdloc.GetOrDefault(v) == 0 && numLdloca.GetOrDefault(v) == 0) { // The variable is never loaded if (inlinedExpression.HasNoSideEffects()) { // Remove completely AnalyzeNode(body[pos], -1); body.RemoveAt(pos); return true; } else if (inlinedExpression.CanBeExpressionStatement() && v.IsGenerated) { // Assign the ranges of the stloc instruction: inlinedExpression.ILRanges.AddRange(((ILExpression)body[pos]).ILRanges); // Remove the stloc, but keep the inner expression body[pos] = inlinedExpression; return true; } } } return false; } /// /// Inlines 'expr' into 'next', if possible. /// bool InlineIfPossible(ILVariable v, ILExpression inlinedExpression, ILNode next, bool aggressive) { // ensure the variable is accessed only a single time if (numStloc.GetOrDefault(v) != 1) return false; int ldloc = numLdloc.GetOrDefault(v); if (ldloc > 1 || ldloc + numLdloca.GetOrDefault(v) != 1) return false; if (next is ILCondition) next = ((ILCondition)next).Condition; else if (next is ILWhileLoop) next = ((ILWhileLoop)next).Condition; ILExpression parent; int pos; if (FindLoadInNext(next as ILExpression, v, inlinedExpression, out parent, out pos) == true) { if (ldloc == 0) { if (!IsGeneratedValueTypeTemporary((ILExpression)next, parent, pos, v, inlinedExpression)) return false; } else { if (!aggressive && !v.IsGenerated && !NonAggressiveInlineInto((ILExpression)next, parent, inlinedExpression)) return false; } // Assign the ranges of the ldloc instruction: inlinedExpression.ILRanges.AddRange(parent.Arguments[pos].ILRanges); if (ldloc == 0) { // it was an ldloca instruction, so we need to use the pseudo-opcode 'addressof' so that the types // comes out correctly parent.Arguments[pos] = new ILExpression(ILCode.AddressOf, null, inlinedExpression); } else { parent.Arguments[pos] = inlinedExpression; } return true; } return false; } /// /// Is this a temporary variable generated by the C# compiler for instance method calls on value type values /// /// The next top-level expression /// The direct parent of the load within 'next' /// Index of the load within 'parent' /// The variable being inlined. /// The expression being inlined bool IsGeneratedValueTypeTemporary(ILExpression next, ILExpression parent, int pos, ILVariable v, ILExpression inlinedExpression) { if (pos == 0 && v.Type != null && v.Type.IsValueType) { // Inlining a value type variable is allowed only if the resulting code will maintain the semantics // that the method is operating on a copy. // Thus, we have to disallow inlining of other locals, fields, array elements, dereferenced pointers switch (inlinedExpression.Code) { case ILCode.Ldloc: case ILCode.Stloc: case ILCode.CompoundAssignment: case ILCode.Ldelem_Any: case ILCode.Ldelem_I: case ILCode.Ldelem_I1: case ILCode.Ldelem_I2: case ILCode.Ldelem_I4: case ILCode.Ldelem_I8: case ILCode.Ldelem_R4: case ILCode.Ldelem_R8: case ILCode.Ldelem_Ref: case ILCode.Ldelem_U1: case ILCode.Ldelem_U2: case ILCode.Ldelem_U4: case ILCode.Ldobj: case ILCode.Ldind_Ref: return false; case ILCode.Ldfld: case ILCode.Stfld: case ILCode.Ldsfld: case ILCode.Stsfld: // allow inlining field access only if it's a readonly field FieldDefinition f = ((FieldReference)inlinedExpression.Operand).Resolve(); if (!(f != null && f.IsInitOnly)) return false; break; case ILCode.Call: case ILCode.CallGetter: // inlining runs both before and after IntroducePropertyAccessInstructions, // so we have to handle both 'call' and 'callgetter' MethodReference mr = (MethodReference)inlinedExpression.Operand; // ensure that it's not an multi-dimensional array getter if (mr.DeclaringType is ArrayType) return false; goto case ILCode.Callvirt; case ILCode.Callvirt: case ILCode.CallvirtGetter: // don't inline foreach loop variables: mr = (MethodReference)inlinedExpression.Operand; if (mr.Name == "get_Current" && mr.HasThis) return false; break; case ILCode.Castclass: case ILCode.Unbox_Any: // These are valid, but might occur as part of a foreach loop variable. ILExpression arg = inlinedExpression.Arguments[0]; if (arg.Code == ILCode.CallGetter || arg.Code == ILCode.CallvirtGetter || arg.Code == ILCode.Call || arg.Code == ILCode.Callvirt) { mr = (MethodReference)arg.Operand; if (mr.Name == "get_Current" && mr.HasThis) return false; // looks like a foreach loop variable, so don't inline it } break; } // inline the compiler-generated variable that are used when accessing a member on a value type: switch (parent.Code) { case ILCode.Call: case ILCode.CallGetter: case ILCode.CallSetter: case ILCode.Callvirt: case ILCode.CallvirtGetter: case ILCode.CallvirtSetter: MethodReference mr = (MethodReference)parent.Operand; return mr.HasThis; case ILCode.Stfld: case ILCode.Ldfld: case ILCode.Ldflda: case ILCode.Await: return true; } } return false; } /// /// Determines whether a variable should be inlined in non-aggressive mode, even though it is not a generated variable. /// /// The next top-level expression /// The direct parent of the load within 'next' /// The expression being inlined bool NonAggressiveInlineInto(ILExpression next, ILExpression parent, ILExpression inlinedExpression) { if (inlinedExpression.Code == ILCode.DefaultValue) return true; switch (next.Code) { case ILCode.Ret: case ILCode.Brtrue: return parent == next; case ILCode.Switch: return parent == next || (parent.Code == ILCode.Sub && parent == next.Arguments[0]); default: return false; } } /// /// Gets whether 'expressionBeingMoved' can be inlined into 'expr'. /// public bool CanInlineInto(ILExpression expr, ILVariable v, ILExpression expressionBeingMoved) { ILExpression parent; int pos; return FindLoadInNext(expr, v, expressionBeingMoved, out parent, out pos) == true; } /// /// Finds the position to inline to. /// /// true = found; false = cannot continue search; null = not found bool? FindLoadInNext(ILExpression expr, ILVariable v, ILExpression expressionBeingMoved, out ILExpression parent, out int pos) { parent = null; pos = 0; if (expr == null) return false; for (int i = 0; i < expr.Arguments.Count; i++) { // Stop when seeing an opcode that does not guarantee that its operands will be evaluated. // Inlining in that case might result in the inlined expresion not being evaluted. if (i == 1 && (expr.Code == ILCode.LogicAnd || expr.Code == ILCode.LogicOr || expr.Code == ILCode.TernaryOp || expr.Code == ILCode.NullCoalescing)) return false; ILExpression arg = expr.Arguments[i]; if ((arg.Code == ILCode.Ldloc || arg.Code == ILCode.Ldloca) && arg.Operand == v) { parent = expr; pos = i; return true; } bool? r = FindLoadInNext(arg, v, expressionBeingMoved, out parent, out pos); if (r != null) return r; } if (IsSafeForInlineOver(expr, expressionBeingMoved)) return null; // continue searching else return false; // abort, inlining not possible } /// /// Determines whether it is safe to move 'expressionBeingMoved' past 'expr' /// bool IsSafeForInlineOver(ILExpression expr, ILExpression expressionBeingMoved) { switch (expr.Code) { case ILCode.Ldloc: ILVariable loadedVar = (ILVariable)expr.Operand; if (numLdloca.GetOrDefault(loadedVar) != 0) { // abort, inlining is not possible return false; } foreach (ILExpression potentialStore in expressionBeingMoved.GetSelfAndChildrenRecursive()) { if (potentialStore.Code == ILCode.Stloc && potentialStore.Operand == loadedVar) return false; } // the expression is loading a non-forbidden variable return true; case ILCode.Ldloca: case ILCode.Ldflda: case ILCode.Ldsflda: case ILCode.Ldelema: case ILCode.AddressOf: case ILCode.ValueOf: case ILCode.NullableOf: // address-loading instructions are safe if their arguments are safe foreach (ILExpression arg in expr.Arguments) { if (!IsSafeForInlineOver(arg, expressionBeingMoved)) return false; } return true; default: // instructions with no side-effects are safe (except for Ldloc and Ldloca which are handled separately) return expr.HasNoSideEffects(); } } /// /// Runs a very simple form of copy propagation. /// Copy propagation is used in two cases: /// 1) assignments from arguments to local variables /// If the target variable is assigned to only once (so always is that argument) and the argument is never changed (no ldarga/starg), /// then we can replace the variable with the argument. /// 2) assignments of address-loading instructions to local variables /// public void CopyPropagation() { foreach (ILBlock block in method.GetSelfAndChildrenRecursive()) { for (int i = 0; i < block.Body.Count; i++) { ILVariable v; ILExpression copiedExpr; if (block.Body[i].Match(ILCode.Stloc, out v, out copiedExpr) && !v.IsParameter && numStloc.GetOrDefault(v) == 1 && numLdloca.GetOrDefault(v) == 0 && CanPerformCopyPropagation(copiedExpr, v)) { // un-inline the arguments of the ldArg instruction ILVariable[] uninlinedArgs = new ILVariable[copiedExpr.Arguments.Count]; for (int j = 0; j < uninlinedArgs.Length; j++) { uninlinedArgs[j] = new ILVariable { IsGenerated = true, Name = v.Name + "_cp_" + j }; block.Body.Insert(i++, new ILExpression(ILCode.Stloc, uninlinedArgs[j], copiedExpr.Arguments[j])); } // perform copy propagation: foreach (var expr in method.GetSelfAndChildrenRecursive()) { if (expr.Code == ILCode.Ldloc && expr.Operand == v) { expr.Code = copiedExpr.Code; expr.Operand = copiedExpr.Operand; for (int j = 0; j < uninlinedArgs.Length; j++) { expr.Arguments.Add(new ILExpression(ILCode.Ldloc, uninlinedArgs[j])); } } } block.Body.RemoveAt(i); if (uninlinedArgs.Length > 0) { // if we un-inlined stuff; we need to update the usage counters AnalyzeMethod(); } InlineInto(block.Body, i, aggressive: false); // maybe inlining gets possible after the removal of block.Body[i] i -= uninlinedArgs.Length + 1; } } } } bool CanPerformCopyPropagation(ILExpression expr, ILVariable copyVariable) { switch (expr.Code) { case ILCode.Ldloca: case ILCode.Ldelema: case ILCode.Ldflda: case ILCode.Ldsflda: // All address-loading instructions always return the same value for a given operand/argument combination, // so they can be safely copied. return true; case ILCode.Ldloc: ILVariable v = (ILVariable)expr.Operand; if (v.IsParameter) { // Parameters can be copied only if they aren't assigned to (directly or indirectly via ldarga) return numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 0; } else { // Variables are be copied only if both they and the target copy variable are generated, // and if the variable has only a single assignment return v.IsGenerated && copyVariable.IsGenerated && numLdloca.GetOrDefault(v) == 0 && numStloc.GetOrDefault(v) == 1; } default: return false; } } } }