diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 1792b1787..f17ace897 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -717,6 +717,8 @@ namespace ICSharpCode.Decompiler.CSharp protected internal override Statement VisitBlock(Block block) { + if (block.Kind != BlockKind.ControlFlow) + return Default(block); // Block without container BlockStatement blockStatement = new BlockStatement(); foreach (var inst in block.Instructions) { diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 416e8aa7e..570fdf096 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -60,6 +60,7 @@ namespace ICSharpCode.Decompiler } if (languageVersion < CSharp.LanguageVersion.CSharp4) { dynamic = false; + namedArguments = false; // * named and optional arguments (not supported yet) } if (languageVersion < CSharp.LanguageVersion.CSharp5) { @@ -635,6 +636,21 @@ namespace ICSharpCode.Decompiler } } + bool namedArguments = false; + + /// + /// Gets/Sets whether named arguments should be used. + /// + public bool NamedArguments { + get { return namedArguments; } + set { + if (namedArguments != value) { + namedArguments = value; + OnPropertyChanged(); + } + } + } + #region Options to aid VB decompilation bool assumeArrayLengthFitsIntoInt32 = true; diff --git a/ICSharpCode.Decompiler/IL/InstructionFlags.cs b/ICSharpCode.Decompiler/IL/InstructionFlags.cs index 1fcd6ee6d..bf0ddd517 100644 --- a/ICSharpCode.Decompiler/IL/InstructionFlags.cs +++ b/ICSharpCode.Decompiler/IL/InstructionFlags.cs @@ -70,7 +70,7 @@ namespace ICSharpCode.Decompiler.IL /// The instruction contains some kind of internal control flow. /// /// - /// If this flag is not set, the all descendants of the instruction are fully evaluated (modulo MayThrow/MayBranch) + /// If this flag is not set, all descendants of the instruction are fully evaluated (modulo MayThrow/MayBranch) /// in left-to-right pre-order. /// /// Note that branch instructions don't have this flag set, because their control flow is not internal diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index c3d5838fc..dc08bbb55 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -114,6 +114,21 @@ namespace ICSharpCode.Decompiler.IL case BlockKind.CallInlineAssign: Debug.Assert(MatchInlineAssignBlock(out _, out _)); break; + case BlockKind.CallWithNamedArgs: + Debug.Assert(finalInstruction is CallInstruction); + foreach (var inst in Instructions) { + var stloc = inst as StLoc; + Debug.Assert(stloc != null, "Instructions in CallWithNamedArgs must be assignments"); + Debug.Assert(stloc.Variable.IsSingleDefinition && stloc.Variable.LoadCount == 1); + Debug.Assert(stloc.Variable.LoadInstructions.Single().Parent == finalInstruction); + } + var call = (CallInstruction)finalInstruction; + if (call.IsInstanceCall) { + // special case: with instance calls, Instructions[0] must be for the this parameter + ILVariable v = ((StLoc)Instructions[0]).Variable; + Debug.Assert(call.Arguments[0].MatchLdLoc(v)); + } + break; } } @@ -311,6 +326,23 @@ namespace ICSharpCode.Decompiler.IL /// final: ldloc s /// } /// - CallInlineAssign + CallInlineAssign, + /// + /// Call using named arguments. + /// + /// + /// Each instruction is assigning to a new local. + /// The final instruction is a call. + /// The locals for this block have exactly one store and + /// exactly one load, which must be an immediate argument to the call. + /// + /// + /// Block { + /// stloc arg0 = ... + /// stloc arg1 = ... + /// final: call M(..., arg1, arg0, ...) + /// } + /// + CallWithNamedArgs, } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs index 8eff8c227..81344b8ce 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs @@ -65,6 +65,13 @@ namespace ICSharpCode.Decompiler.IL this.Arguments = new InstructionCollection(this, 0); } + /// + /// Gets whether this is an instance call (i.e. whether the first argument is the 'this' pointer). + /// + public bool IsInstanceCall { + get { return !(Method.IsStatic || OpCode == OpCode.NewObj); } + } + /// /// Gets the parameter for the argument with the specified index. /// Returns null for the this parameter. diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 6fa0e1283..9ab415d4e 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -190,8 +190,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// static bool DoInline(ILVariable v, ILInstruction inlinedExpression, ILInstruction next, bool aggressive, ILTransformContext context) { - ILInstruction loadInst; - if (FindLoadInNext(next, v, inlinedExpression, out loadInst) == true) { + var r = FindLoadInNext(next, v, inlinedExpression, out var loadInst); + if (r == FindResult.Found) { if (loadInst.OpCode == OpCode.LdLoca) { if (!IsGeneratedValueTypeTemporary(next, (LdLoca)loadInst, v, inlinedExpression)) return false; @@ -213,6 +213,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms loadInst.ReplaceWith(inlinedExpression); } return true; + } else if (r == FindResult.NamedArgument && context.Settings.NamedArguments) { + Debug.Assert(loadInst.OpCode == OpCode.LdLoc); + //if (!aggressive && v.Kind != VariableKind.StackSlot && !NonAggressiveInlineInto(next, loadInst, inlinedExpression, v)) + // return false; + context.Step($"Introduce named argument '{v.Name}'", inlinedExpression); + var call = (CallInstruction)loadInst.Parent; + if (!(call.Parent is Block namedArgBlock) || namedArgBlock.Kind != BlockKind.CallWithNamedArgs) { + // create namedArgBlock: + namedArgBlock = new Block(BlockKind.CallWithNamedArgs); + call.ReplaceWith(namedArgBlock); + namedArgBlock.FinalInstruction = call; + if (call.IsInstanceCall) { + IType thisVarType = call.Method.DeclaringType; + if (CallInstruction.ExpectedTypeForThisPointer(thisVarType) == StackType.Ref) { + thisVarType = new ByReferenceType(thisVarType); + } + var function = call.Ancestors.OfType().First(); + var thisArgVar = function.RegisterVariable(VariableKind.StackSlot, thisVarType, "this_arg"); + namedArgBlock.Instructions.Add(new StLoc(thisArgVar, call.Arguments[0])); + call.Arguments[0] = new LdLoc(thisArgVar); + } + } + namedArgBlock.Instructions.Insert(call.IsInstanceCall ? 1 : 0, new StLoc(v, inlinedExpression)); + return true; } return false; } @@ -351,59 +375,149 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// public static bool CanInlineInto(ILInstruction expr, ILVariable v, ILInstruction expressionBeingMoved) { - ILInstruction loadInst; - return FindLoadInNext(expr, v, expressionBeingMoved, out loadInst) == true; + return FindLoadInNext(expr, v, expressionBeingMoved, out _) == FindResult.Found; + } + + enum FindResult + { + /// + /// Found a load; inlining is possible. + /// + Found, + /// + /// Load not found and re-ordering not possible. Stop the search. + /// + Stop, + /// + /// Load not found, but the expressionBeingMoved can be re-ordered with regards to the + /// tested expression, so we may continue searching for the matching load. + /// + Continue, + /// + /// Found a load in call, but re-ordering not possible with regards to the + /// other call arguments. + /// Inlining is not possible, but we might convert the call to named arguments. + /// + NamedArgument, } /// /// Finds the position to inline to. /// /// true = found; false = cannot continue search; null = not found - static bool? FindLoadInNext(ILInstruction expr, ILVariable v, ILInstruction expressionBeingMoved, out ILInstruction loadInst) + static FindResult FindLoadInNext(ILInstruction expr, ILVariable v, ILInstruction expressionBeingMoved, out ILInstruction loadInst) { loadInst = null; if (expr == null) - return false; + return FindResult.Stop; if (expr.MatchLdLoc(v) || expr.MatchLdLoca(v)) { // Match found, we can inline loadInst = expr; - return true; - } else if (expr is Block block && block.Instructions.Count > 0) { - // Inlining into inline-blocks? only for some block types, and only into the first instruction. + return FindResult.Found; + } else if (expr is Block block) { + // Inlining into inline-blocks? switch (block.Kind) { case BlockKind.ArrayInitializer: case BlockKind.CollectionInitializer: case BlockKind.ObjectInitializer: - return FindLoadInNext(block.Instructions[0], v, expressionBeingMoved, out loadInst) ?? false; + // Allow inlining into the first instruction of the block + if (block.Instructions.Count == 0) + return FindResult.Stop; + return NoContinue(FindLoadInNext(block.Instructions[0], v, expressionBeingMoved, out loadInst)); // If FindLoadInNext() returns null, we still can't continue searching // because we can't inline over the remainder of the block. + case BlockKind.CallWithNamedArgs: + return CanExtendNamedArgument(block, v, expressionBeingMoved, out loadInst); default: - return false; + return FindResult.Stop; } } else if (expr is BlockContainer container && container.EntryPoint.IncomingEdgeCount == 1) { // Possibly a switch-container, allow inlining into the switch instruction: - return FindLoadInNext(container.EntryPoint.Instructions[0], v, expressionBeingMoved, out loadInst) ?? false; + return NoContinue(FindLoadInNext(container.EntryPoint.Instructions[0], v, expressionBeingMoved, out loadInst)); // If FindLoadInNext() returns null, we still can't continue searching // because we can't inline over the remainder of the blockcontainer. } else if (expr is NullableRewrap) { // Inlining into nullable.rewrap is OK unless the expression being inlined // contains a nullable.wrap that isn't being re-wrapped within the expression being inlined. if (expressionBeingMoved.HasFlag(InstructionFlags.MayUnwrapNull)) - return false; + return FindResult.Stop; } foreach (var child in expr.Children) { if (!child.SlotInfo.CanInlineInto) - return false; + return FindResult.Stop; // Recursively try to find the load instruction - bool? r = FindLoadInNext(child, v, expressionBeingMoved, out loadInst); - if (r != null) + FindResult r = FindLoadInNext(child, v, expressionBeingMoved, out loadInst); + if (r != FindResult.Continue) { + if (r == FindResult.Stop && expr is CallInstruction call) + return CanIntroduceNamedArgument(call, child, v, out loadInst); return r; + } } if (IsSafeForInlineOver(expr, expressionBeingMoved)) - return null; // continue searching + return FindResult.Continue; // continue searching + else + return FindResult.Stop; // abort, inlining not possible + } + + private static FindResult NoContinue(FindResult findResult) + { + if (findResult == FindResult.Continue) + return FindResult.Stop; else - return false; // abort, inlining not possible + return findResult; + } + + private static FindResult CanIntroduceNamedArgument(CallInstruction call, ILInstruction child, ILVariable v, out ILInstruction loadInst) + { + loadInst = null; + Debug.Assert(child.Parent == call); + if (call.IsInstanceCall && child.ChildIndex == 0) + return FindResult.Stop; // cannot use named arg to move expressionBeingMoved before this pointer + if (call.Method.IsOperator || call.Method.IsAccessor) + return FindResult.Stop; // cannot use named arg for operators or accessors + for (int i = child.ChildIndex; i < call.Arguments.Count; i++) { + if (call.Arguments[i] is LdLoc ldloc && ldloc.Variable == v) { + loadInst = ldloc; + return FindResult.NamedArgument; + } + } + return FindResult.Stop; + } + + private static FindResult CanExtendNamedArgument(Block block, ILVariable v, ILInstruction expressionBeingMoved, out ILInstruction loadInst) + { + Debug.Assert(block.Kind == BlockKind.CallWithNamedArgs); + var firstArg = ((StLoc)block.Instructions[0]).Value; + var r = FindLoadInNext(firstArg, v, expressionBeingMoved, out loadInst); + if (r == FindResult.Found || r == FindResult.NamedArgument) { + return r; // OK, inline into first instruction of block + } + var call = (CallInstruction)block.FinalInstruction; + if (call.IsInstanceCall) { + // For instance calls, block.Instructions[0] is the argument + // for the 'this' pointer. We can only insert at position 1. + if (r == FindResult.Stop) { + // error: can't move expressionBeingMoved after block.Instructions[0] + return FindResult.Stop; + } + // Because we always ensure block.Instructions[0] is the 'this' argument, + // it's possible that the place we actually need to inline into + // is within block.Instructions[1]: + if (block.Instructions.Count > 1) { + r = FindLoadInNext(block.Instructions[1], v, expressionBeingMoved, out loadInst); + if (r == FindResult.Found || r == FindResult.NamedArgument) { + return r; // OK, inline into block.Instructions[1] + } + } + } + foreach (var arg in call.Arguments) { + if (arg.MatchLdLoc(v)) { + loadInst = arg; + return FindResult.NamedArgument; + } + } + return FindResult.Stop; } ///