diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index db7f0e6..f5d4d50 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -1016,6 +1016,12 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task ExtensionMethods([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task CS9_ExtensionGetEnumerator([ValueSource(nameof(roslyn3OrNewerWithNet40Options))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs new file mode 100644 index 0000000..af03ca0 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -0,0 +1,571 @@ +#nullable enable +// Copyright (c) 2014-2016 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.TypeSystem; +namespace ICSharpCode.Decompiler.IL +{ + /// + /// A block consists of a list of IL instructions. + /// + /// + /// Note: if execution reaches the end of the instruction list, + /// the FinalInstruction (which is not part of the list) will be executed. + /// The block returns returns the result value of the FinalInstruction. + /// For blocks returning void, the FinalInstruction will usually be 'nop'. + /// + /// + /// There are three different uses for blocks: + /// 1) Blocks in block containers. Used as targets for Branch instructions. + /// 2) Blocks to group a bunch of statements, e.g. the TrueInst of an IfInstruction. + /// 3) Inline blocks that evaluate to a value, e.g. for array initializers. + /// + partial class Block : ILInstruction + { + public static readonly SlotInfo InstructionSlot = new SlotInfo("Instruction", isCollection: true); + public static readonly SlotInfo FinalInstructionSlot = new SlotInfo("FinalInstruction"); + + public readonly BlockKind Kind; + public readonly InstructionCollection Instructions; + ILInstruction finalInstruction = null!; + + /// + /// For blocks in a block container, this field holds + /// the number of incoming control flow edges to this block. + /// + /// + /// This variable is automatically updated when adding/removing branch instructions from the ILAst, + /// or when adding the block as an entry point to a BlockContainer. + /// + public int IncomingEdgeCount { get; internal set; } + + /// + /// A 'final instruction' that gets executed after the Instructions collection. + /// Provides the return value for the block. + /// + /// + /// Blocks in containers must have 'Nop' as a final instruction. + /// + /// Note that the FinalInstruction is included in Block.Children, + /// but not in Block.Instructions! + /// + public ILInstruction FinalInstruction { + get { + return finalInstruction; + } + set { + ValidateChild(value); + SetChildInstruction(ref finalInstruction, value, Instructions.Count); + } + } + + protected internal override void InstructionCollectionUpdateComplete() + { + base.InstructionCollectionUpdateComplete(); + if (finalInstruction.Parent == this) + finalInstruction.ChildIndex = Instructions.Count; + } + + public Block(BlockKind kind = BlockKind.ControlFlow) : base(OpCode.Block) + { + this.Kind = kind; + this.Instructions = new InstructionCollection(this, 0); + this.FinalInstruction = new Nop(); + } + + public override ILInstruction Clone() + { + Block clone = new Block(Kind); + clone.AddILRange(this); + clone.Instructions.AddRange(this.Instructions.Select(inst => inst.Clone())); + clone.FinalInstruction = this.FinalInstruction.Clone(); + return clone; + } + + /// + /// The load of the initializer target an ArrayInitializer block evaluates to. The block + /// may end in the implicit conversion of that array to Span<T>/ReadOnlySpan<T>, + /// which is returned in ; everything else about the block is + /// the same in both shapes. + /// + internal static LdLoc? MatchArrayInitializerFinal(ILInstruction finalInstruction, out IMethod? conversion) + { + conversion = null; + if (finalInstruction is CallInstruction { Arguments.Count: 1 } call + && call.Method.IsOperator && call.Method.Name == "op_Implicit" + && (call.Method.ReturnType.IsKnownType(KnownTypeCode.SpanOfT) + || call.Method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))) + { + conversion = call.Method; + finalInstruction = call.Arguments[0]; + } + return finalInstruction as LdLoc; + } + + internal override void CheckInvariant(ILPhase phase, ICompilation compilation) + { + base.CheckInvariant(phase, compilation); + for (int i = 0; i < Instructions.Count - 1; i++) + { + // only the last instruction may have an unreachable endpoint + Debug.Assert(!Instructions[i].HasFlag(InstructionFlags.EndPointUnreachable)); + } + switch (this.Kind) + { + case BlockKind.ControlFlow: + Debug.Assert(finalInstruction.OpCode == OpCode.Nop); + break; + 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; + DebugAssert(stloc != null, "Instructions in CallWithNamedArgs must be assignments"); + DebugAssert(stloc.Variable.Kind == VariableKind.NamedArgument); + DebugAssert(stloc.Variable.IsSingleDefinition && stloc.Variable.LoadCount == 1); + DebugAssert(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; + case BlockKind.ArrayInitializer: + var final = MatchArrayInitializerFinal(finalInstruction, out _); + Debug.Assert(final != null && final.Variable.IsSingleDefinition && final.Variable.Kind == VariableKind.InitializerTarget); + IType? type = null; + Debug.Assert(Instructions[0].MatchStLoc(final!.Variable, out var init) && init.MatchNewArr(out type)); + for (int i = 1; i < Instructions.Count; i++) + { + DebugAssert(Instructions[i].MatchStObj(out ILInstruction? target, out _, out var t) && type != null && type.Equals(t)); + DebugAssert(target.MatchLdElema(out t, out ILInstruction? array) && type.Equals(t)); + DebugAssert(array.MatchLdLoc(out ILVariable? v) && v == final.Variable); + } + break; + case BlockKind.CollectionInitializer: + case BlockKind.ObjectInitializer: + var final2 = finalInstruction as LdLoc; + Debug.Assert(final2 != null); + var initVar2 = final2!.Variable; + Debug.Assert(initVar2.StoreCount == 1 && initVar2.Kind == VariableKind.InitializerTarget); + IType? type2 = null; + bool condition = Instructions[0].MatchStLoc(final2.Variable, out var init2); + Debug.Assert(condition); + Debug.Assert(init2 is NewObj + || init2 is DefaultValue + || (init2 is CallInstruction c && c.Method.FullNameIs("System.Activator", "CreateInstance") && c.Method.TypeArguments.Count == 1) + || (init2 is Block named && named.Kind == BlockKind.CallWithNamedArgs)); + switch (init2) + { + case NewObj newObj: + type2 = newObj.Method.DeclaringType; + break; + case DefaultValue defaultValue: + type2 = defaultValue.Type; + break; + case Block callWithNamedArgs when callWithNamedArgs.Kind == BlockKind.CallWithNamedArgs: + type2 = ((CallInstruction)callWithNamedArgs.FinalInstruction).Method.ReturnType; + break; + case CallInstruction ci2 when TransformCollectionAndObjectInitializers.IsRecordCloneMethodCall(ci2): + type2 = ci2.Method.DeclaringType; + break; + case Call c2 when c2.Method.FullNameIs("System.Activator", "CreateInstance") && c2.Method.TypeArguments.Count == 1: + type2 = c2.Method.TypeArguments[0]; + break; + default: + Debug.Assert(false); + break; + } + for (int i = 1; i < Instructions.Count; i++) + { + if (Instructions[i] is StLoc { Variable: var v }) + { + foreach (var inst in v.LoadInstructions) + { + Debug.Assert(inst.IsDescendantOf(this)); + } + foreach (var inst in v.AddressInstructions) + { + Debug.Assert(inst.IsDescendantOf(this)); + } + foreach (ILInstruction inst in v.StoreInstructions) + { + Debug.Assert(inst.IsDescendantOf(this)); + } + } + else + { + Debug.Assert(AccessPathElement.GetAccessPath(Instructions[i], type2).Kind != AccessPathKind.Invalid); + } + } + break; + case BlockKind.DeconstructionConversions: + Debug.Assert(this.SlotInfo == DeconstructInstruction.ConversionsSlot); + break; + case BlockKind.DeconstructionAssignments: + Debug.Assert(this.SlotInfo == DeconstructInstruction.AssignmentsSlot); + break; + case BlockKind.InterpolatedString: + Debug.Assert(FinalInstruction is Call { Method: { Name: "ToStringAndClear" }, Arguments: { Count: 1 } }); + var interpolInit = Instructions[0] as StLoc; + DebugAssert(interpolInit != null + && interpolInit.Variable.Kind == VariableKind.InitializerTarget + && interpolInit.Variable.AddressCount == Instructions.Count + && interpolInit.Variable.StoreCount == 1); + for (int i = 1; i < Instructions.Count; i++) + { + Call? inst = Instructions[i] as Call; + DebugAssert(inst != null); + DebugAssert(inst.Arguments.Count >= 1 && inst.Arguments[0].MatchLdLoca(interpolInit.Variable)); + } + break; + } + } + + public override StackType ResultType { + get { + return finalInstruction.ResultType; + } + } + + public override IType InferType(ICompilation compilation) + { + return finalInstruction.InferType(compilation); + } + + internal override bool CanInlineIntoSlot(int childIndex, ILInstruction expressionBeingMoved) + { + switch (Kind) + { + case BlockKind.ControlFlow when Parent is BlockContainer: + case BlockKind.ArrayInitializer: + case BlockKind.CollectionInitializer: + case BlockKind.ObjectInitializer: + case BlockKind.CallInlineAssign: + // Allow inlining into the first instruction of the block + return childIndex == 0; + default: + return false; + } + } + + /// + /// Gets the name of this block. + /// + public string Label { + get { return Disassembler.DisassemblerHelpers.OffsetToString(this.StartILOffset); } + } + + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) + { + WriteILRange(output, options); + output.Write("Block "); + output.WriteLocalReference(Label, this, isDefinition: true); + if (Kind != BlockKind.ControlFlow) + output.Write($" ({Kind})"); + if (Parent is BlockContainer) + output.Write(" (incoming: {0})", IncomingEdgeCount); + output.Write(' '); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + int index = 0; + foreach (var inst in Instructions) + { + if (options.ShowChildIndexInBlock) + { + output.Write("[" + index + "] "); + index++; + } + inst.WriteTo(output, options); + output.WriteLine(); + } + if (finalInstruction.OpCode != OpCode.Nop) + { + output.Write("final: "); + finalInstruction.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.Write("}"); + output.MarkFoldEnd(); + } + + protected override int GetChildCount() + { + return Instructions.Count + 1; + } + + protected override ILInstruction GetChild(int index) + { + if (index == Instructions.Count) + return finalInstruction; + return Instructions[index]; + } + + protected override void SetChild(int index, ILInstruction value) + { + if (index == Instructions.Count) + FinalInstruction = value; + else + Instructions[index] = value; + } + + protected override SlotInfo GetChildSlot(int index) + { + if (index == Instructions.Count) + return FinalInstructionSlot; + else + return InstructionSlot; + } + + protected override InstructionFlags ComputeFlags() + { + var flags = InstructionFlags.None; + foreach (var inst in Instructions) + { + flags |= inst.Flags; + } + flags |= FinalInstruction.Flags; + return flags; + } + + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.None; + } + } + + /// + /// Deletes this block from its parent container. + /// This may cause the indices of other blocks in that container to change. + /// + /// It is an error to call this method on blocks that are not directly within a container. + /// It is also an error to call this method on the entry-point block. + /// + public void Remove() + { + Debug.Assert(ChildIndex > 0); + var container = (BlockContainer)Parent!; + Debug.Assert(container.Blocks[ChildIndex] == this); + container.Blocks.SwapRemoveAt(ChildIndex); + } + + /// + /// Apply a list of transforms to this function. + /// + public void RunTransforms(IEnumerable transforms, BlockTransformContext context) + { + this.CheckInvariant(ILPhase.Normal, context.TypeSystem); + foreach (var transform in transforms) + { + context.CancellationToken.ThrowIfCancellationRequested(); + Debug.Assert(context.IndexOfFirstAlreadyTransformedInstruction <= this.Instructions.Count); + context.StepStartGroup(transform.GetType().Name); + transform.Run(this, context); + this.CheckInvariant(ILPhase.Normal, context.TypeSystem); + context.StepEndGroup(); + } + } + + /// + /// Gets the predecessor of the given instruction. + /// Returns null if inst.Parent is not a block. + /// + public static ILInstruction? GetPredecessor(ILInstruction inst) + { + if (inst.Parent is Block block && inst.ChildIndex > 0) + { + return block.Instructions[inst.ChildIndex - 1]; + } + else + { + return null; + } + } + + /// + /// If inst is a block consisting of a single instruction, returns that instruction. + /// Otherwise, returns the input instruction. + /// + [return: NotNullIfNotNull("inst")] + public static ILInstruction? Unwrap(ILInstruction? inst) + { + if (inst is Block block) + { + if (block.Instructions.Count == 1 && block.finalInstruction.MatchNop()) + return block.Instructions[0]; + } + return inst; + } + + /// + /// Gets the closest parent Block. + /// Returns null, if the instruction is not a descendant of a Block. + /// + public static Block? FindClosestBlock(ILInstruction? inst) + { + var curr = inst; + while (curr != null) + { + if (curr is Block b) + return b; + curr = curr.Parent; + } + return null; + } + + /// + /// Gets the closest ancestor that is child of a control-flow (top-level) Block. + /// Returns null, if the instruction is not a descendant of a Block. + /// + public static ILInstruction? GetContainingStatement(ILInstruction inst) + { + var curr = inst; + while (curr != null) + { + if (curr.Parent is Block { Kind: BlockKind.ControlFlow }) + return curr; + curr = curr.Parent; + } + return null; + } + + public bool MatchInlineAssignBlock([NotNullWhen(true)] out CallInstruction? call, [NotNullWhen(true)] out ILInstruction? value) + { + call = null; + value = null; + if (this.Kind != BlockKind.CallInlineAssign) + return false; + if (this.Instructions.Count != 1) + return false; + call = this.Instructions[0] as CallInstruction; + if (call == null || call.Arguments.Count == 0) + return false; + if (!call.Arguments.Last().MatchStLoc(out var tmp, out value)) + return false; + if (!(tmp.IsSingleDefinition && tmp.LoadCount == 1)) + return false; + return this.FinalInstruction.MatchLdLoc(tmp); + } + + public bool MatchIfAtEndOfBlock([NotNullWhen(true)] out ILInstruction? condition, [NotNullWhen(true)] out ILInstruction? trueInst, [NotNullWhen(true)] out ILInstruction? falseInst) + { + condition = null; + trueInst = null; + falseInst = null; + if (Instructions.Count < 2) + return false; + if (Instructions[Instructions.Count - 2].MatchIfInstruction(out condition, out trueInst)) + { + // Swap trueInst<>falseInst for every logic.not in the condition. + falseInst = Instructions.Last(); + while (condition.MatchLogicNot(out var arg)) + { + condition = arg; + (trueInst, falseInst) = (falseInst, trueInst); + } + return true; + } + return false; + } + } + + public enum BlockKind + { + /// + /// Block is used for control flow. + /// All blocks in block containers must have this type. + /// Control flow blocks cannot evaluate to a value (FinalInstruction must be Nop). + /// + ControlFlow, + /// + /// Block is used for array initializers, e.g. `new int[] { expr1, expr2 }`. + /// + ArrayInitializer, + CollectionInitializer, + ObjectInitializer, + StackAllocInitializer, + /// + /// Block is used for using the result of a property setter inline. + /// Example: Use(this.Property = value); + /// This is only for inline assignments to property or indexers; other inline assignments work + /// by using the result value of the stloc/stobj instructions. + /// + /// Constructed by TransformAssignment. + /// Can be deconstructed using Block.MatchInlineAssignBlock(). + /// + /// + /// Block { + /// call setter(..., stloc s(...)) + /// final: ldloc s + /// } + /// + 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, + /// + /// + /// + DeconstructionConversions, + /// + /// + /// + DeconstructionAssignments, + WithInitializer, + /// + /// String interpolation using DefaultInterpolatedStringHandler. + /// + /// + /// Block { + /// stloc I_0 = newobj DefaultInterpolatedStringHandler(...) + /// call AppendXXX(I_0, ...) + /// ... + /// final: call ToStringAndClear(ldloc I_0) + /// } + /// + InterpolatedString, + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs new file mode 100644 index 0000000..6c0c98c --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -0,0 +1,993 @@ +// Copyright (c) 2011-2017 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.TypeSystem.Implementation; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + [Flags] + public enum InliningOptions + { + None = 0, + Aggressive = 1, + IntroduceNamedArguments = 2, + FindDeconstruction = 4, + AllowChangingOrderOfEvaluationForExceptions = 8, + AllowInliningOfLdloca = 0x10 + } + + /// + /// Performs inlining transformations. + /// + public class ILInlining : IILTransform, IBlockTransform, IStatementTransform + { + internal InliningOptions options; + + public void Run(ILFunction function, ILTransformContext context) + { + foreach (var block in function.Descendants.OfType()) + { + InlineAllInBlock(function, block, this.options, context); + } + function.Variables.RemoveDead(); + } + + public void Run(Block block, BlockTransformContext context) + { + InlineAllInBlock(context.Function, block, this.options, context); + } + + public void Run(Block block, int pos, StatementTransformContext context) + { + var options = this.options | OptionsForBlock(block, pos, context); + while (InlineOneIfPossible(block, pos, options, context: context)) + { + // repeat inlining until nothing changes. + } + } + + internal static InliningOptions OptionsForBlock(Block block, int pos, ILTransformContext context) + { + InliningOptions options = InliningOptions.None; + if (context.Settings.AggressiveInlining || IsCatchWhenBlock(block)) + { + options |= InliningOptions.Aggressive; + } + else + { + var function = block.Ancestors.OfType().FirstOrDefault(); + var inst = block.Instructions[pos]; + if (IsInConstructorInitializer(function, inst) || PreferExpressionsOverStatements(function)) + options |= InliningOptions.Aggressive; + } + if (!context.Settings.UseRefLocalsForAccurateOrderOfEvaluation) + { + options |= InliningOptions.AllowChangingOrderOfEvaluationForExceptions; + } + return options; + } + + static bool PreferExpressionsOverStatements(ILFunction function) + { + switch (function.Kind) + { + case ILFunctionKind.Delegate: + return function.Parameters.Any(p => CSharp.CSharpDecompiler.IsTransparentIdentifier(p.Name)); + case ILFunctionKind.ExpressionTree: + return true; + default: + return false; + } + } + + public static bool InlineAllInBlock(ILFunction function, Block block, InliningOptions options, ILTransformContext context) + { + bool modified = false; + var instructions = block.Instructions; + for (int i = instructions.Count - 1; i >= 0; i--) + { + if (instructions[i] is StLoc inst) + { + if (InlineOneIfPossible(block, i, options, context)) + { + modified = true; + continue; + } + } + } + return modified; + } + + internal static bool IsInConstructorInitializer(ILFunction function, ILInstruction inst) + { + int ctorCallStart = function.ChainedConstructorCallILOffset; + if (inst.EndILOffset > ctorCallStart) + return false; + var topLevelInst = inst.Ancestors.LastOrDefault(instr => instr.Parent is Block); + if (topLevelInst == null) + return false; + return topLevelInst.EndILOffset <= ctorCallStart; + } + + internal static bool IsCatchWhenBlock(Block block) + { + var container = BlockContainer.FindClosestContainer(block); + return container?.Parent is TryCatchHandler handler + && handler.Filter == container; + } + + /// + /// Inlines instructions before pos into block.Instructions[pos]. + /// + /// The number of instructions that were inlined. + public static int InlineInto(Block block, int pos, InliningOptions options, ILTransformContext context) + { + if (pos >= block.Instructions.Count) + return 0; + int count = 0; + while (--pos >= 0) + { + if (InlineOneIfPossible(block, pos, options, context)) + count++; + else + break; + } + return count; + } + + /// + /// Aggressively inlines the stloc instruction at block.Body[pos] into the next instruction, if possible. + /// + public static bool InlineIfPossible(Block block, int pos, ILTransformContext context) + { + return InlineOneIfPossible(block, pos, InliningOptions.Aggressive, context); + } + + /// + /// Inlines the stloc instruction at block.Instructions[pos] into the next instruction, if possible. + /// + public static bool InlineOneIfPossible(Block block, int pos, InliningOptions options, ILTransformContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + if (block.Instructions[pos] is not StLoc stloc) + return false; + if (!VariableCanBeUsedForInlining(stloc.Variable)) + return false; + // TODO: inlining of small integer types might be semantically incorrect, + // but we can't avoid it this easily without breaking lots of tests. + //if (v.Type.IsSmallIntegerType()) + // return false; // stloc might perform implicit truncation + return InlineOne(stloc, options, context); + } + + public static bool VariableCanBeUsedForInlining(ILVariable v) + { + if (v.Kind == VariableKind.PinnedLocal) + return false; + // ensure the variable is accessed only a single time + if (v.StoreCount != 1) + return false; + if (v.LoadCount + v.AddressCount != 1) + return false; + return true; + } + + /// + /// Inlines the stloc instruction at block.Instructions[pos] into the next instruction. + /// + /// Note that this method does not check whether 'v' has only one use; + /// the caller is expected to validate whether inlining 'v' has any effects on other uses of 'v'. + /// + public static bool InlineOne(StLoc stloc, InliningOptions options, ILTransformContext context) + { + ILVariable v = stloc.Variable; + Block block = (Block)stloc.Parent; + int pos = stloc.ChildIndex; + if (DoInline(v, stloc.Value, block.Instructions.ElementAtOrDefault(pos + 1), options, context)) + { + // Assign the ranges of the stloc instruction: + stloc.Value.AddILRange(stloc); + // Remove the stloc instruction: + Debug.Assert(block.Instructions[pos] == stloc); + block.Instructions.RemoveAt(pos); + return true; + } + else if (v.LoadCount == 0 && v.AddressCount == 0) + { + // The variable is never loaded + if (SemanticHelper.IsPure(stloc.Value.Flags)) + { + // Remove completely if the instruction has no effects + // (except for reading locals) + context.Step("Remove dead store without side effects", stloc); + block.Instructions.RemoveAt(pos); + return true; + } + else if (v.Kind == VariableKind.StackSlot) + { + context.Step("Remove dead store, but keep expression", stloc); + // Assign the ranges of the stloc instruction: + stloc.Value.AddILRange(stloc); + // Remove the stloc, but keep the inner expression + var keptExpression = stloc.Value; + stloc.ReplaceWith(keptExpression); + context.EndStep(keptExpression); + return true; + } + } + return false; + } + + /// + /// Inlines 'expr' into 'next', if possible. + /// + /// Note that this method does not check whether 'v' has only one use; + /// the caller is expected to validate whether inlining 'v' has any effects on other uses of 'v'. + /// + static bool DoInline(ILVariable v, ILInstruction inlinedExpression, ILInstruction next, InliningOptions options, ILTransformContext context) + { + var r = FindLoadInNext(next, v, inlinedExpression, options); + if (r.Type == FindResultType.Found || r.Type == FindResultType.NamedArgument) + { + var loadInst = r.LoadInst; + // A stackalloc whose result is a pointer (rather than a Span) is only valid C# as + // the initializer of a pointer-typed local. Inlining it into any expression position + // retypes it as Span, which breaks every pointer use (dereference, a pointer-typed + // argument, a reinterpreting cast). Moving it into a local store keeps it a local + // initializer, and the Span/ReadOnlySpan constructor is the one position where + // reinterpreting it as a span is exactly the point; everything else must not inline. + if (inlinedExpression.ResultType == StackType.I + && (inlinedExpression is LocAlloc || inlinedExpression is Block { Kind: BlockKind.StackAllocInitializer }) + && loadInst.Parent is not StLoc + && !IsStackAllocSpanConstructorArgument(loadInst)) + { + return false; + } + if (loadInst.OpCode == OpCode.LdLoca) + { + if (!IsGeneratedTemporaryForAddressOf((LdLoca)loadInst, v, inlinedExpression, options)) + return false; + } + else + { + Debug.Assert(loadInst.OpCode == OpCode.LdLoc); + bool aggressive = (options & InliningOptions.Aggressive) != 0; + if (!aggressive && v.Kind != VariableKind.StackSlot + && !NonAggressiveInlineInto(next, r, inlinedExpression, v)) + { + return false; + } + } + + if (r.Type == FindResultType.NamedArgument) + { + NamedArgumentTransform.IntroduceNamedArgument(r.CallArgument, context); + // Now that the argument is evaluated early, we can inline as usual + } + + context.Step($"Inline variable '{v.Name}'", inlinedExpression); + // Assign the ranges of the ldloc instruction: + inlinedExpression.AddILRange(loadInst); + + ILInstruction inlinedResult; + if (loadInst.OpCode == OpCode.LdLoca) + { + // it was an ldloca instruction, so we need to use the pseudo-opcode 'addressof' + // to preserve the semantics of the compiler-generated temporary + Debug.Assert(((LdLoca)loadInst).Variable == v); + inlinedResult = new AddressOf(inlinedExpression, v.Type); + loadInst.ReplaceWith(inlinedResult); + } + else + { + inlinedResult = inlinedExpression; + loadInst.ReplaceWith(inlinedExpression); + } + context.EndStep(inlinedResult); + return true; + } + return false; + } + + /// + /// Returns true if is the pointer argument of a + /// new Span<T>(pointer, length) / new ReadOnlySpan<T>(pointer, length) + /// constructor, i.e. the one position where a pointer-typed stackalloc may be inlined + /// (TransformSpanTCtorContainingStackAlloc turns it into a span stackalloc afterwards). + /// + static bool IsStackAllocSpanConstructorArgument(ILInstruction loadInst) + { + return loadInst.Parent is NewObj newObj + && newObj.Arguments.Count == 2 + && newObj.Arguments[0] == loadInst + && (newObj.Method.DeclaringType.IsKnownType(KnownTypeCode.SpanOfT) + || newObj.Method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)); + } + + /// + /// Is this a temporary variable generated by the C# compiler for instance method calls on value type values + /// + /// The load instruction (a descendant within 'next') + /// The variable being inlined. + static bool IsGeneratedTemporaryForAddressOf(LdLoca loadInst, ILVariable v, ILInstruction inlinedExpression, InliningOptions options) + { + Debug.Assert(loadInst.Variable == v); + if (!options.HasFlag(InliningOptions.AllowInliningOfLdloca)) + { + return false; // inlining of ldloca is not allowed in the early inlining stage + } + // 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 ensure we're operating on an r-value. + // Additionally, we cannot inline in cases where the C# compiler prohibits the direct use + // of the rvalue (e.g. M(ref (MyStruct)obj); is invalid). + if (IsUsedAsThisPointerInCall(loadInst, out var method, out var constrainedTo)) + { + if (options.HasFlag(InliningOptions.Aggressive)) + { + // Inlining might be required in ctor initializers (see #2714). + // expressionBuilder.VisitAddressOf will handle creating the copy for us. + return true; + } + + switch (ClassifyExpression(inlinedExpression)) + { + case ExpressionClassification.RValue: + // For struct method calls on rvalues, the C# compiler always generates temporaries. + return true; + case ExpressionClassification.MutableLValue: + // For struct method calls on mutable lvalues, the C# compiler never generates temporaries. + return false; + case ExpressionClassification.ReadonlyLValue: + // For struct method calls on readonly lvalues, the C# compiler + // only generates a temporary if it isn't a "readonly struct" + return MethodRequiresCopyForReadonlyLValue(method, constrainedTo); + default: + throw new InvalidOperationException("invalid expression classification"); + } + } + else if (loadInst.Parent is LdElemaInlineArray) + { + return true; + } + else if (IsPassedToReadOnlySpanCtor(loadInst)) + { + // Always inlining is possible here, because it's an 'in' or 'ref readonly' parameter + // and the C# compiler allows calling it with an rvalue, even though that might produce + // a warning. Note that we don't need to check the expression classification, because + // expressionBuilder.VisitAddressOf will handle creating the copy for us. + // This is necessary, because there are compiler-generated uses of this ctor when + // passing a single-element array to a params ROS parameter and our following transforms + // assume the value is already inlined. + return true; + } + else if (IsPassedToInlineArrayAsSpan(loadInst)) + { + // Inlining is not allowed: + // .InlineArrayAsReadOnlySpan(GetInlineArray()) is invalid C# code + return false; + } + else if (IsPassedToInParameter(loadInst)) + { + if (options.HasFlag(InliningOptions.Aggressive)) + { + // Inlining might be required in ctor initializers (see #2714). + // expressionBuilder.VisitAddressOf will handle creating the copy for us. + return true; + } + + switch (ClassifyExpression(inlinedExpression)) + { + case ExpressionClassification.RValue: + // For rvalues passed to in parameters, the C# compiler generates a temporary. + return true; + case ExpressionClassification.MutableLValue: + case ExpressionClassification.ReadonlyLValue: + // For lvalues passed to in parameters, the C# compiler never generates temporaries. + return false; + default: + throw new InvalidOperationException("invalid expression classification"); + } + } + else if (IsUsedAsThisPointerInFieldRead(loadInst)) + { + // mcs generated temporaries for field reads on rvalues (#1555) + return ClassifyExpression(inlinedExpression) == ExpressionClassification.RValue; + } + else + { + return false; + } + } + + private static bool IsPassedToInlineArrayAsSpan(LdLoca loadInst) + { + if (loadInst.Parent is not Call call) + return false; + var method = call.Method; + var declaringType = method.DeclaringType; + return declaringType.ReflectionName == "" + && method.Name is "InlineArrayAsReadOnlySpan" or "InlineArrayAsSpan" + && method.Parameters is [var arg, var length] + && (method.ReturnType.IsKnownType(KnownTypeCode.SpanOfT) || method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)) + && arg.Type is ByReferenceType + && length.Type.IsKnownType(KnownTypeCode.Int32); + } + + internal static bool MethodRequiresCopyForReadonlyLValue(IMethod method, IType constrainedTo = null) + { + if (method == null) + return true; + var type = constrainedTo ?? method.DeclaringType; + if (type.IsReferenceType == true) + return false; // reference types are never implicitly copied + if (method.ThisIsRefReadOnly) + return false; // no copies for calls on readonly structs + return true; + } + + internal static bool IsUsedAsThisPointerInCall(LdLoca ldloca) + { + return IsUsedAsThisPointerInCall(ldloca, out _, out _); + } + + static bool IsUsedAsThisPointerInCall(LdLoca ldloca, out IMethod method, out IType constrainedType) + { + method = null; + constrainedType = null; + if (ldloca.Variable.Type.IsReferenceType ?? false) + return false; + ILInstruction inst = ldloca; + if (inst.Parent is LdObjIfRef) + { + inst = inst.Parent; + } + while (inst.Parent is LdFlda ldflda) + { + inst = ldflda; + } + if (inst.ChildIndex != 0) + return false; + switch (inst.Parent.OpCode) + { + case OpCode.Call: + case OpCode.CallVirt: + var callInst = (CallInstruction)inst.Parent; + method = callInst.Method; + constrainedType = callInst.ConstrainedTo; + if (method.IsAccessor) + { + if (method.AccessorKind == MethodSemanticsAttributes.Getter) + { + // C# doesn't allow property compound assignments on temporary structs + return !(inst.Parent.Parent is CompoundAssignmentInstruction cai + && cai.TargetKind == CompoundTargetKind.Property + && cai.Target == inst.Parent); + } + else + { + // C# doesn't allow calling setters on temporary structs + return false; + } + } + return !method.IsStatic; + case OpCode.Await: + method = ((Await)inst.Parent).GetAwaiterMethod; + return true; + case OpCode.NullableUnwrap: + return ((NullableUnwrap)inst.Parent).RefInput; + case OpCode.MatchInstruction: + method = ((MatchInstruction)inst.Parent).Method; + return true; + default: + return false; + } + } + + static bool IsUsedAsThisPointerInFieldRead(LdLoca ldloca) + { + if (ldloca.Variable.Type.IsReferenceType ?? false) + return false; + ILInstruction inst = ldloca; + while (inst.Parent is LdFlda ldflda) + { + inst = ldflda; + } + return inst != ldloca && inst.Parent is LdObj; + } + + internal static bool IsPassedToInParameter(LdLoca ldloca) + { + if (ldloca.Parent is not CallInstruction call) + { + return false; + } + return call.GetParameter(ldloca.ChildIndex)?.ReferenceKind is ReferenceKind.In; + } + + static bool IsPassedToReadOnlySpanCtor(LdLoca ldloca) + { + if (ldloca.Parent is not NewObj call) + { + return false; + } + var method = call.Method; + return method.IsConstructor + && method.Parameters.Count == 1 + && method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT); + } + + internal static bool IsReadOnlySpanCharCtor(IMethod method) + { + return method.IsConstructor + && method.Parameters.Count == 1 + && method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) + && method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Char) + && method.Parameters[0].Type is ByReferenceType brt + && brt.ElementType.IsKnownType(KnownTypeCode.Char); + } + + /// + /// Gets whether the instruction, when converted into C#, turns into an l-value that can + /// be used to mutate a value-type. + /// If this function returns false, the C# compiler would introduce a temporary copy + /// when calling a method on a value-type (and any mutations performed by the method will be lost) + /// + internal static ExpressionClassification ClassifyExpression(ILInstruction inst) + { + switch (inst.OpCode) + { + case OpCode.LdLoc: + case OpCode.StLoc: + ILVariable v = ((IInstructionWithVariableOperand)inst).Variable; + if (v.IsRefReadOnly + || v.Kind == VariableKind.ForeachLocal + || v.Kind == VariableKind.UsingLocal) + { + return ExpressionClassification.ReadonlyLValue; + } + else + { + return ExpressionClassification.MutableLValue; + } + case OpCode.LdObj: + // ldobj typically refers to a storage location, + // but readonly fields are an exception. + if (IsReadonlyReference(((LdObj)inst).Target)) + { + return ExpressionClassification.ReadonlyLValue; + } + else + { + return ExpressionClassification.MutableLValue; + } + case OpCode.StObj: + // stobj is the same as ldobj. + if (IsReadonlyReference(((StObj)inst).Target)) + { + return ExpressionClassification.ReadonlyLValue; + } + else + { + return ExpressionClassification.MutableLValue; + } + case OpCode.Call: + var m = ((CallInstruction)inst).Method; + // multi-dimensional array getters are lvalues, + // everything else is an rvalue. + if (m.DeclaringType.Kind == TypeKind.Array) + { + return ExpressionClassification.MutableLValue; + } + else + { + return ExpressionClassification.RValue; + } + default: + return ExpressionClassification.RValue; // most instructions result in an rvalue + } + } + + /// + /// Gets whether the ILInstruction will turn into a C# expresion that is considered readonly by the C# compiler. + /// + internal static bool IsReadonlyReference(ILInstruction addr) + { + switch (addr) + { + case LdFlda ldflda: + return ldflda.Field.IsReadOnly + || (ldflda.Field.DeclaringType.Kind == TypeKind.Struct && IsReadonlyReference(ldflda.Target)); + case LdsFlda ldsflda: + return ldsflda.Field.IsReadOnly; + case LdLoc ldloc: + return ldloc.Variable.IsRefReadOnly; + case Call call: + return call.Method.ReturnTypeIsRefReadOnly; + case CallVirt call: + return call.Method.ReturnTypeIsRefReadOnly; + case CallIndirect calli: + return calli.FunctionPointerType.ReturnIsRefReadOnly; + case AddressOf _: + // C# doesn't allow mutation of value-type temporaries + return true; + default: + if (addr.MatchLdFld(out _, out var field)) + return field.ReturnTypeIsRefReadOnly; + 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 variable being eliminated by inlining. + /// The expression being inlined + static bool NonAggressiveInlineInto(ILInstruction next, FindResult findResult, ILInstruction inlinedExpression, ILVariable v) + { + if (findResult.Type == FindResultType.NamedArgument) + { + var originalStore = (StLoc)inlinedExpression.Parent; + return !originalStore.ILStackWasEmpty; + } + Debug.Assert(findResult.Type == FindResultType.Found); + + var loadInst = findResult.LoadInst; + Debug.Assert(loadInst.IsDescendantOf(next)); + + // decide based on the source expression being inlined + switch (inlinedExpression.OpCode) + { + case OpCode.DefaultValue: + case OpCode.StObj: + case OpCode.NumericCompoundAssign: + case OpCode.UserDefinedCompoundAssign: + case OpCode.Await: + case OpCode.SwitchInstruction: + return true; + case OpCode.LdLoc: + if (v.StateMachineField == null && ((LdLoc)inlinedExpression).Variable.StateMachineField != null) + { + // Roslyn likes to put the result of fetching a state machine field into a temporary variable, + // so inline more aggressively in such cases. + return true; + } + break; + } + if (inlinedExpression.ResultType == StackType.Ref) + { + // VB likes to use ref locals for compound assignment + // (the C# compiler uses ref stack slots instead). + // We want to avoid unnecessary ref locals, so we'll always inline them if possible. + return true; + } + + var parent = loadInst.Parent; + if (NullableLiftingTransform.MatchNullableCtor(parent, out _, out _)) + { + // inline into nullable ctor call in lifted operator + parent = parent.Parent; + } + if (parent is ILiftableInstruction liftable && liftable.IsLifted) + { + return true; // inline into lifted operators + } + // decide based on the new parent into which we are inlining: + switch (parent.OpCode) + { + case OpCode.NullCoalescingInstruction: + if (NullableType.IsNullable(v.Type)) + return true; // inline nullables into ?? operator + break; + case OpCode.NullableUnwrap: + return true; // inline into ?. operator + case OpCode.UserDefinedLogicOperator: + case OpCode.DynamicLogicOperatorInstruction: + return true; // inline into (left slot of) user-defined && or || operator + case OpCode.DynamicGetMemberInstruction: + case OpCode.DynamicGetIndexInstruction: + if (parent.Parent.OpCode == OpCode.DynamicCompoundAssign) + return true; // inline into dynamic compound assignments + break; + case OpCode.DynamicCompoundAssign: + return true; + case OpCode.GetPinnableReference: + case OpCode.LocAllocSpan: + return true; // inline size-expressions into localloc.span + case OpCode.Call: + case OpCode.CallVirt: + // Aggressive inline into property/indexer getter calls for compound assignment calls + // (The compiler generates locals for these because it doesn't want to evalute the args twice for getter+setter) + if (parent.SlotInfo == CompoundAssignmentInstruction.TargetSlot) + { + return true; + } + if (((CallInstruction)parent).Method is SyntheticRangeIndexAccessor) + { + return true; + } + break; + case OpCode.CallIndirect when loadInst.SlotInfo == CallIndirect.FunctionPointerSlot: + return true; + case OpCode.LdElema: + if (((LdElema)parent).WithSystemIndex) + { + return true; + } + break; + case OpCode.Leave: + case OpCode.YieldReturn: + return true; + case OpCode.SwitchInstruction: + // Preserve type info on switch instruction, if we're inlining a local variable into the switch-value slot. + if (v.Kind != VariableKind.StackSlot && loadInst.SlotInfo == SwitchInstruction.ValueSlot) + { + ((SwitchInstruction)parent).Type ??= v.Type; + } + return true; + //case OpCode.BinaryNumericInstruction when parent.SlotInfo == SwitchInstruction.ValueSlot: + case OpCode.StringToInt when parent.SlotInfo == SwitchInstruction.ValueSlot: + return true; + case OpCode.MatchInstruction: + var match = (MatchInstruction)parent; + if (match.IsDeconstructTuple + || (match.CheckType && match.Variable.Type.IsReferenceType != true)) + { + return true; + } + break; + } + // decide based on the top-level target instruction into which we are inlining: + switch (next.OpCode) + { + case OpCode.IfInstruction: + while (parent.MatchLogicNot(out _)) + { + parent = parent.Parent; + } + return parent == next; + default: + return false; + } + } + + /// + /// Gets whether 'expressionBeingMoved' can be inlined into 'expr'. + /// + public static bool CanInlineInto(ILInstruction expr, ILVariable v, ILInstruction expressionBeingMoved) + { + return FindLoadInNext(expr, v, expressionBeingMoved, InliningOptions.None).Type == FindResultType.Found; + } + + internal enum FindResultType + { + /// + /// 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. + /// Only used with . + /// + NamedArgument, + /// + /// Found a deconstruction. + /// Only used with . + /// + Deconstruction, + } + + internal readonly struct FindResult + { + public readonly FindResultType Type; + public readonly ILInstruction LoadInst; // ldloc or ldloca instruction that loads the variable to be inlined + public readonly ILInstruction CallArgument; // argument of call that needs to be promoted to a named argument + + private FindResult(FindResultType type, ILInstruction loadInst, ILInstruction callArg) + { + this.Type = type; + this.LoadInst = loadInst; + this.CallArgument = callArg; + } + + public static readonly FindResult Stop = new FindResult(FindResultType.Stop, null, null); + public static readonly FindResult Continue = new FindResult(FindResultType.Continue, null, null); + + public static FindResult Found(ILInstruction loadInst) + { + Debug.Assert(loadInst.OpCode == OpCode.LdLoc || loadInst.OpCode == OpCode.LdLoca); + return new FindResult(FindResultType.Found, loadInst, null); + } + + public static FindResult NamedArgument(ILInstruction loadInst, ILInstruction callArg) + { + Debug.Assert(loadInst.OpCode == OpCode.LdLoc || loadInst.OpCode == OpCode.LdLoca); + Debug.Assert(callArg.Parent is CallInstruction); + return new FindResult(FindResultType.NamedArgument, loadInst, callArg); + } + + public static FindResult Deconstruction(DeconstructInstruction deconstruction) + { + return new FindResult(FindResultType.Deconstruction, deconstruction, null); + } + } + + /// + /// Finds the position to inline to. + /// + /// true = found; false = cannot continue search; null = not found + internal static FindResult FindLoadInNext(ILInstruction expr, ILVariable v, ILInstruction expressionBeingMoved, InliningOptions options) + { + if (expr == null) + return FindResult.Stop; + if (expr.MatchLdLoc(v) || expr.MatchLdLoca(v)) + { + // Match found, we can inline unless there are slot restrictions. + if (expr.Parent.SatisfiesSlotRestrictionForInlining(expr.ChildIndex, expressionBeingMoved)) + { + return FindResult.Found(expr); + } + // We cannot inline because the targets slot restrictions are not satisfied + if ((options & InliningOptions.AllowChangingOrderOfEvaluationForExceptions) != 0 && expr.SlotInfo == StObj.TargetSlot) + { + // Special case: inlining will change code semantics, + // but we accept that so that we can avoid a ref local. + if (expressionBeingMoved is LdFlda ldflda) + ldflda.DelayExceptions = true; + else if (expressionBeingMoved is LdElema ldelema) + ldelema.DelayExceptions = true; + return FindResult.Found(expr); + } + return FindResult.Stop; + } + else if (expr is Block { Kind: BlockKind.CallWithNamedArgs } block) + { + return NamedArgumentTransform.CanExtendNamedArgument(block, v, expressionBeingMoved); + } + else if (options.HasFlag(InliningOptions.FindDeconstruction) && expr is DeconstructInstruction di) + { + return FindResult.Deconstruction(di); + } + foreach (var child in expr.Children) + { + if (!expr.CanInlineIntoSlot(child.ChildIndex, expressionBeingMoved)) + return FindResult.Stop; + + // Recursively try to find the load instruction + FindResult r = FindLoadInNext(child, v, expressionBeingMoved, options); + if (r.Type != FindResultType.Continue) + { + if (r.Type == FindResultType.Stop && (options & InliningOptions.IntroduceNamedArguments) != 0 && expr is CallInstruction call) + return NamedArgumentTransform.CanIntroduceNamedArgument(call, child, v, expressionBeingMoved); + return r; + } + } + if (IsSafeForInlineOver(expr, expressionBeingMoved)) + return FindResult.Continue; // continue searching + else + return FindResult.Stop; // abort, inlining not possible + } + + /// + /// Determines whether it is safe to move 'expressionBeingMoved' past 'expr' + /// + static bool IsSafeForInlineOver(ILInstruction expr, ILInstruction expressionBeingMoved) + { + return SemanticHelper.MayReorder(expressionBeingMoved, expr); + } + + /// + /// Finds the first call instruction within the instructions that were inlined into inst. + /// + internal static CallInstruction FindFirstInlinedCall(ILInstruction inst) + { + foreach (var child in inst.Children) + { + if (!child.SlotInfo.CanInlineInto) + break; + var call = FindFirstInlinedCall(child); + if (call != null) + { + return call; + } + } + return inst as CallInstruction; + } + + /// + /// Gets whether 'expressionBeingMoved' can be moved from somewhere before 'stmt' to become the replacement of 'targetLoad'. + /// + public static bool CanMoveInto(ILInstruction expressionBeingMoved, ILInstruction stmt, ILInstruction targetLoad) + { + Debug.Assert(targetLoad.IsDescendantOf(stmt)); + for (ILInstruction inst = targetLoad; inst != stmt; inst = inst.Parent) + { + if (!inst.Parent.CanInlineIntoSlot(inst.ChildIndex, expressionBeingMoved)) + return false; + // Check whether re-ordering with predecessors is valid: + int childIndex = inst.ChildIndex; + for (int i = 0; i < childIndex; ++i) + { + ILInstruction predecessor = inst.Parent.Children[i]; + if (!IsSafeForInlineOver(predecessor, expressionBeingMoved)) + return false; + } + } + return true; + } + + /// + /// Gets whether 'expressionBeingMoved' can be moved from somewhere before 'stmt' to become the replacement of 'targetLoad'. + /// + public static bool CanMoveIntoCallVirt(ILInstruction expressionBeingMoved, ILInstruction stmt, CallVirt nestedCallVirt, ILInstruction targetLoad) + { + Debug.Assert(targetLoad.IsDescendantOf(stmt) && nestedCallVirt.IsDescendantOf(stmt)); + ILInstruction thisArg = nestedCallVirt.Arguments[0]; + Debug.Assert(thisArg is LdObjIfRef); + for (ILInstruction inst = targetLoad; inst != stmt; inst = inst.Parent) + { + if (!inst.Parent.CanInlineIntoSlot(inst.ChildIndex, expressionBeingMoved)) + return false; + // Check whether re-ordering with predecessors is valid: + int childIndex = inst.ChildIndex; + for (int i = 0; i < childIndex; ++i) + { + ILInstruction predecessor = inst.Parent.Children[i]; + if (predecessor != thisArg && !IsSafeForInlineOver(predecessor, expressionBeingMoved)) + return false; + } + } + return true; + } + + /// + /// Gets whether arg can be un-inlined out of stmt. + /// + /// + internal static bool CanUninline(ILInstruction arg, ILInstruction stmt) + { + // moving into and moving out-of are equivalent + return CanMoveInto(arg, stmt, arg); + } + } + + internal enum ExpressionClassification + { + RValue, + MutableLValue, + ReadonlyLValue, + } +}