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,
+ }
+}