#nullable enable // Copyright (c) 2014 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.Collections.Generic; using System.Diagnostics; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.IL { public enum VariableKind { /// <summary> /// A local variable. /// </summary> Local, /// <summary> /// A pinned local variable (not associated with a pinned region) /// </summary> PinnedLocal, /// <summary> /// A pinned local variable (associated with a pinned region) /// </summary> PinnedRegionLocal, /// <summary> /// A local variable used as using-resource variable. /// </summary> UsingLocal, /// <summary> /// A local variable used as foreach variable. /// </summary> ForeachLocal, /// <summary> /// A local variable used inside an array, collection or /// object initializer block to denote the object being initialized. /// </summary> InitializerTarget, /// <summary> /// A parameter. /// </summary> Parameter, /// <summary> /// Variable created for exception handler. /// </summary> ExceptionStackSlot, /// <summary> /// Local variable used in a catch block. /// </summary> ExceptionLocal, /// <summary> /// Variable created from stack slot. /// </summary> StackSlot, /// <summary> /// Variable in BlockKind.CallWithNamedArgs /// </summary> NamedArgument, /// <summary> /// Local variable that holds the display class used for lambdas within this function. /// </summary> DisplayClassLocal, /// <summary> /// Local variable declared within a pattern match. /// </summary> PatternLocal, /// <summary> /// Temporary variable declared in a deconstruction init section. /// </summary> DeconstructionInitTemporary, } static class VariableKindExtensions { public static bool IsThis(this ILVariable v) { return v.Kind == VariableKind.Parameter && v.Index < 0; } public static bool IsLocal(this VariableKind kind) { switch (kind) { case VariableKind.Local: case VariableKind.ExceptionLocal: case VariableKind.ForeachLocal: case VariableKind.UsingLocal: case VariableKind.PatternLocal: case VariableKind.PinnedLocal: case VariableKind.PinnedRegionLocal: case VariableKind.DisplayClassLocal: return true; default: return false; } } } [DebuggerDisplay("{Name} : {Type}")] public class ILVariable { VariableKind kind; public VariableKind Kind { get { return kind; } internal set { if (kind == VariableKind.Parameter) throw new InvalidOperationException("Kind=Parameter cannot be changed!"); if (Index != null && value.IsLocal() && !kind.IsLocal()) { // For variables, Index has different meaning than for stack slots, // so we need to reset it to null. // StackSlot -> ForeachLocal can happen sometimes (e.g. PST.TransformForeachOnArray) Index = null; } kind = value; } } public readonly StackType StackType; IType type; public IType Type { get { return type; } internal set { if (value.GetStackType() != StackType) throw new ArgumentException($"Expected stack-type: {StackType} may not be changed. Found: {value.GetStackType()}"); type = value; } } /// <summary> /// This variable is either a C# 7 'in' parameter or must be declared as 'ref readonly'. /// </summary> public bool IsRefReadOnly { get; internal set; } /// <summary> /// The index of the local variable or parameter (depending on Kind) /// /// For VariableKinds with "Local" in the name: /// * if non-null, the Index refers to the LocalVariableSignature. /// * index may be null for variables that used to be fields (captured by lambda/async) /// For Parameters, the Index refers to the method's list of parameters. /// The special "this" parameter has index -1. /// For ExceptionStackSlot, the index is the IL offset of the exception handler. /// For other kinds, the index has no meaning, and is usually null. /// </summary> public int? Index { get; private set; } [Conditional("DEBUG")] internal void CheckInvariant() { switch (kind) { case VariableKind.Local: case VariableKind.ForeachLocal: case VariableKind.PatternLocal: case VariableKind.PinnedLocal: case VariableKind.PinnedRegionLocal: case VariableKind.UsingLocal: case VariableKind.ExceptionLocal: case VariableKind.DisplayClassLocal: // in range of LocalVariableSignature Debug.Assert(Index == null || Index >= 0); break; case VariableKind.Parameter: // -1 for the "this" parameter Debug.Assert(Index >= -1); Debug.Assert(Function == null || Index < Function.Parameters.Count); break; case VariableKind.ExceptionStackSlot: Debug.Assert(Index >= 0); break; } } public string? Name { get; set; } public bool HasGeneratedName { get; set; } /// <summary> /// Gets the function in which this variable is declared. /// </summary> /// <remarks> /// This property is set automatically when the variable is added to the <c>ILFunction.Variables</c> collection. /// </remarks> public ILFunction? Function { get; internal set; } /// <summary> /// Gets the block container in which this variable is captured. /// For captured variables declared inside the loop, the capture scope is the BlockContainer of the loop. /// For captured variables declared outside of the loop, the capture scope is the BlockContainer of the parent function. /// </summary> /// <remarks> /// This property returns null for variables that are not captured. /// </remarks> public BlockContainer? CaptureScope { get; internal set; } /// <summary> /// Gets the index of this variable within the <c>Function.Variables</c> collection. /// </summary> /// <remarks> /// This property is set automatically when the variable is added to the <c>VariableScope.Variables</c> collection. /// It may change if an item with a lower index is removed from the collection. /// </remarks> public int IndexInFunction { get; internal set; } /// <summary> /// Number of ldloc instructions referencing this variable. /// </summary> /// <remarks> /// This variable is automatically updated when adding/removing ldloc instructions from the ILAst. /// </remarks> public int LoadCount => LoadInstructions.Count; readonly List<LdLoc> loadInstructions = new List<LdLoc>(); /// <summary> /// List of ldloc instructions referencing this variable. /// </summary> /// <remarks> /// This list is automatically updated when adding/removing ldloc instructions from the ILAst. /// </remarks> public IReadOnlyList<LdLoc> LoadInstructions => loadInstructions; /// <summary> /// Number of store instructions referencing this variable, /// plus 1 if HasInitialValue. /// /// Stores are: /// <list type="item"> /// <item>stloc</item> /// <item>TryCatchHandler (assigning the exception variable)</item> /// <item>PinnedRegion (assigning the pointer variable)</item> /// <item>initial values (<see cref="UsesInitialValue"/>)</item> /// </list> /// </summary> /// <remarks> /// This variable is automatically updated when adding/removing stores instructions from the ILAst. /// </remarks> public int StoreCount => (usesInitialValue ? 1 : 0) + StoreInstructions.Count; readonly List<IStoreInstruction> storeInstructions = new List<IStoreInstruction>(); /// <summary> /// List of store instructions referencing this variable. /// /// Stores are: /// <list type="item"> /// <item>stloc</item> /// <item>TryCatchHandler (assigning the exception variable)</item> /// <item>PinnedRegion (assigning the pointer variable)</item> /// <item>initial values (<see cref="UsesInitialValue"/>) -- however, there is no instruction for /// the initial value, so it is not contained in the store list.</item> /// </list> /// </summary> /// <remarks> /// This list is automatically updated when adding/removing stores instructions from the ILAst. /// </remarks> public IReadOnlyList<IStoreInstruction> StoreInstructions => storeInstructions; /// <summary> /// Number of ldloca instructions referencing this variable. /// </summary> /// <remarks> /// This variable is automatically updated when adding/removing ldloca instructions from the ILAst. /// </remarks> public int AddressCount => AddressInstructions.Count; readonly List<LdLoca> addressInstructions = new List<LdLoca>(); /// <summary> /// List of ldloca instructions referencing this variable. /// </summary> /// <remarks> /// This list is automatically updated when adding/removing ldloca instructions from the ILAst. /// </remarks> public IReadOnlyList<LdLoca> AddressInstructions => addressInstructions; internal void AddLoadInstruction(LdLoc inst) => inst.IndexInLoadInstructionList = AddInstruction(loadInstructions, inst); internal void AddStoreInstruction(IStoreInstruction inst) => inst.IndexInStoreInstructionList = AddInstruction(storeInstructions, inst); internal void AddAddressInstruction(LdLoca inst) => inst.IndexInAddressInstructionList = AddInstruction(addressInstructions, inst); internal void RemoveLoadInstruction(LdLoc inst) => RemoveInstruction(loadInstructions, inst.IndexInLoadInstructionList, inst); internal void RemoveStoreInstruction(IStoreInstruction inst) => RemoveInstruction(storeInstructions, inst.IndexInStoreInstructionList, inst); internal void RemoveAddressInstruction(LdLoca inst) => RemoveInstruction(addressInstructions, inst.IndexInAddressInstructionList, inst); int AddInstruction<T>(List<T> list, T inst) where T : class, IInstructionWithVariableOperand { list.Add(inst); return list.Count - 1; } void RemoveInstruction<T>(List<T> list, int index, T? inst) where T : class, IInstructionWithVariableOperand { Debug.Assert(list[index] == inst); int indexToMove = list.Count - 1; list[index] = list[indexToMove]; list[index].IndexInVariableInstructionMapping = index; list.RemoveAt(indexToMove); } bool initialValueIsInitialized; /// <summary> /// Gets/Sets whether the variable's initial value is initialized. /// This is always <c>true</c> for parameters (incl. <c>this</c>). /// /// Normal variables have an initial value if the function uses ".locals init". /// </summary> public bool InitialValueIsInitialized { get { return initialValueIsInitialized; } set { if (Kind == VariableKind.Parameter && !value) throw new InvalidOperationException("Cannot remove InitialValueIsInitialized from parameters"); initialValueIsInitialized = value; } } bool usesInitialValue; /// <summary> /// Gets/Sets whether the initial value of the variable is used. /// This is always <c>true</c> for parameters (incl. <c>this</c>). /// /// Normal variables use the initial value, if no explicit initialization is done. /// </summary> /// <remarks> /// The following table shows the relationship between <see cref="InitialValueIsInitialized"/> /// and <see cref="UsesInitialValue"/>. /// <list type="table"> /// <listheader> /// <term><see cref="InitialValueIsInitialized"/></term> /// <term><see cref="UsesInitialValue"/></term> /// <term>Meaning</term> /// </listheader> /// <item> /// <term><see langword="true" /></term> /// <term><see langword="true" /></term> /// <term>This variable's initial value is zero-initialized (<c>.locals init</c>) and the initial value is used. /// From C#'s point of view a the value <c>default(T)</c> is assigned at the site of declaration.</term> /// </item> /// <item> /// <term><see langword="true" /></term> /// <term><see langword="false" /></term> /// <term>This variable's initial value is zero-initialized (<c>.locals init</c>) and the initial value is not used. /// From C#'s point of view no implicit initialization occurs, because the code assigns a value /// explicitly, before the variable is first read.</term> /// </item> /// <item> /// <term><see langword="false" /></term> /// <term><see langword="true" /></term> /// <term>This variable's initial value is uninitialized (<c>.locals</c> without <c>init</c>) and the /// initial value is used. /// From C#'s point of view a call to <code>System.Runtime.CompilerServices.Unsafe.SkipInit(out T)</code> /// is generated after the declaration.</term> /// </item> /// <item> /// <term><see langword="false" /></term> /// <term><see langword="false" /></term> /// <term>This variable's initial value is uninitialized (<c>.locals</c> without <c>init</c>) and the /// initial value is not used. /// From C#'s point of view no implicit initialization occurs, because the code assigns a value /// explicitly, before the variable is first read.</term> /// </item> /// </list> /// </remarks> public bool UsesInitialValue { get { return usesInitialValue; } set { if (Kind == VariableKind.Parameter && !value) throw new InvalidOperationException("Cannot remove UsesInitialValue from parameters"); usesInitialValue = value; } } [Obsolete("Use 'UsesInitialValue' instead.")] public bool HasInitialValue { get => UsesInitialValue; set => UsesInitialValue = value; } /// <summary> /// Gets whether the variable is in SSA form: /// There is exactly 1 store, and every load sees the value from that store. /// </summary> /// <remarks> /// Note: the single store is not necessary a store instruction, it might also /// be the use of the implicit initial value. /// For example: for parameters, IsSingleDefinition will only return true if /// the parameter is never assigned to within the function. /// </remarks> public bool IsSingleDefinition { get { return StoreCount == 1 && AddressCount == 0; } } /// <summary> /// Gets whether the variable is dead - unused. /// </summary> public bool IsDead { get { return StoreInstructions.Count == 0 && LoadCount == 0 && AddressCount == 0; } } /// <summary> /// The field which was converted to a local variable. /// Set when the variable is from a 'yield return' or 'async' state machine. /// </summary> public IField? StateMachineField; /// <summary> /// If enabled, remove dead stores to this variable as if the "Remove dead code" option is enabled. /// </summary> internal bool RemoveIfRedundant; public ILVariable(VariableKind kind, IType type, int? index = null) { if (type == null) throw new ArgumentNullException(nameof(type)); this.Kind = kind; this.type = type; this.StackType = type.GetStackType(); this.Index = index; if (kind == VariableKind.Parameter) { this.InitialValueIsInitialized = true; this.UsesInitialValue = true; } CheckInvariant(); } public ILVariable(VariableKind kind, IType type, StackType stackType, int? index = null) { if (type == null) throw new ArgumentNullException(nameof(type)); this.Kind = kind; this.type = type; this.StackType = stackType; this.Index = index; if (kind == VariableKind.Parameter) { this.InitialValueIsInitialized = true; this.UsesInitialValue = true; } CheckInvariant(); } public override string? ToString() { return Name; } internal void WriteDefinitionTo(ITextOutput output) { if (IsRefReadOnly) { output.Write("readonly "); } switch (Kind) { case VariableKind.Local: output.Write("local "); break; case VariableKind.PinnedLocal: output.Write("pinned local "); break; case VariableKind.PinnedRegionLocal: output.Write("PinnedRegion local "); break; case VariableKind.Parameter: output.Write("param "); break; case VariableKind.ExceptionLocal: output.Write("exception local "); break; case VariableKind.ExceptionStackSlot: output.Write("exception stack "); break; case VariableKind.StackSlot: output.Write("stack "); break; case VariableKind.InitializerTarget: output.Write("initializer "); break; case VariableKind.ForeachLocal: output.Write("foreach "); break; case VariableKind.UsingLocal: output.Write("using "); break; case VariableKind.NamedArgument: output.Write("named_arg "); break; case VariableKind.DisplayClassLocal: output.Write("display_class local "); break; case VariableKind.PatternLocal: output.Write("pattern local "); break; case VariableKind.DeconstructionInitTemporary: output.Write("deconstruction init temporary "); break; default: throw new ArgumentOutOfRangeException(); } output.WriteLocalReference(this.Name, this, isDefinition: true); output.Write(" : "); Type.WriteTo(output); output.Write('('); if (Kind == VariableKind.Parameter || Kind == VariableKind.Local || Kind == VariableKind.PinnedLocal || Kind == VariableKind.PinnedRegionLocal) { output.Write("Index={0}, ", Index); } output.Write("LoadCount={0}, AddressCount={1}, StoreCount={2})", LoadCount, AddressCount, StoreCount); if (Kind != VariableKind.Parameter) { if (initialValueIsInitialized) { output.Write(" init"); } else { output.Write(" uninit"); } if (usesInitialValue) { output.Write(" used"); } else { output.Write(" unused"); } } if (CaptureScope != null) { output.Write(" captured in "); output.WriteLocalReference(CaptureScope.EntryPoint?.Label, CaptureScope); } if (StateMachineField != null) { output.Write(" from state-machine"); } } internal void WriteTo(ITextOutput output) { output.WriteLocalReference(this.Name, this); } /// <summary> /// Gets whether this variable occurs within the specified instruction. /// </summary> internal bool IsUsedWithin(ILInstruction inst) { if (inst is IInstructionWithVariableOperand iwvo && iwvo.Variable == this) { return true; } foreach (var child in inst.Children) { if (IsUsedWithin(child)) return true; } return false; } } public interface IInstructionWithVariableOperand { ILVariable Variable { get; set; } int IndexInVariableInstructionMapping { get; set; } } public interface IStoreInstruction : IInstructionWithVariableOperand { int IndexInStoreInstructionList { get; set; } } interface ILoadInstruction : IInstructionWithVariableOperand { int IndexInLoadInstructionList { get; set; } } interface IAddressInstruction : IInstructionWithVariableOperand { int IndexInAddressInstructionList { get; set; } } public class ILVariableEqualityComparer : IEqualityComparer<ILVariable> { public static readonly ILVariableEqualityComparer Instance = new ILVariableEqualityComparer(); public bool Equals(ILVariable? x, ILVariable? y) { if (x == y) return true; if (x == null || y == null) return false; if (x.Kind == VariableKind.StackSlot || y.Kind == VariableKind.StackSlot) return false; if (x.Kind == VariableKind.PatternLocal || y.Kind == VariableKind.PatternLocal) return false; if (!(x.Function == y.Function && x.Kind == y.Kind)) return false; if (x.Index != null) return x.Index == y.Index; else if (x.StateMachineField != null) return x.StateMachineField.Equals(y.StateMachineField); else return false; } public int GetHashCode(ILVariable obj) { if (obj.Kind is VariableKind.StackSlot or VariableKind.PatternLocal) return obj.GetHashCode(); if (obj.Index != null) return (obj.Function, obj.Kind, obj.Index).GetHashCode(); if (obj.StateMachineField != null) return (obj.Function, obj.Kind, obj.StateMachineField).GetHashCode(); return obj.GetHashCode(); } } }