From dc6e094a307d3e4a59a48fdb5a3a12c91cbf9e38 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 08:26:18 +0200 Subject: [PATCH] Add support for indexing arrays using System.Index --- .../TestCases/Pretty/IndexRangeTest.cs | 52 ++++++++--- .../CSharp/ExpressionBuilder.cs | 17 +++- .../ICSharpCode.Decompiler.csproj | 1 + ICSharpCode.Decompiler/IL/Instructions.cs | 5 +- ICSharpCode.Decompiler/IL/Instructions.tt | 12 +++ .../IL/Transforms/ExpressionTransforms.cs | 2 + .../IL/Transforms/ILInlining.cs | 5 ++ .../IL/Transforms/IndexRangeTransform.cs | 89 +++++++++++++++++++ 8 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index bb47378ba..8ec33c1b7 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -29,20 +29,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty internal class IndexRangeTest { - public static string[] GetArray() - { + public static int[] GetArray() + { throw null; - } - public static List GetList() - { + } + public static List GetList() + { throw null; } public static Span GetSpan() - { + { throw null; } public static string GetString() - { + { throw null; } public static Index GetIndex(int i = 0) @@ -60,8 +60,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseIndex() { -#if TODO Console.WriteLine(GetArray()[GetIndex()]); +#if TODO Console.WriteLine(GetList()[GetIndex()]); Console.WriteLine(GetSpan()[GetIndex()]); Console.WriteLine(GetString()[GetIndex()]); @@ -69,6 +69,38 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty #endif Console.WriteLine(new CustomList2()[GetIndex()]); } + + public static void UseIndexFromEnd() + { + Console.WriteLine(GetArray()[^GetInt()]); +#if TODO + Console.WriteLine(GetList()[^GetInt()]); + Console.WriteLine(GetSpan()[^GetInt()]); + Console.WriteLine(GetString()[^GetInt()]); + Console.WriteLine(new CustomList()[^GetInt()]); +#endif + Console.WriteLine(new CustomList2()[^GetInt()]); + } + + public static void UseIndexForWrite() + { + GetArray()[GetIndex()] = GetInt(); +#if TODO + GetList()[GetIndex()] = GetInt(); + GetSpan()[GetIndex()] = GetInt(); +#endif + } + + private static void UseRef(ref int i) + { + } + + public static void UseIndexForRef() + { + UseRef(ref GetArray()[GetIndex()]); + UseRef(ref GetArray()[^GetInt()]); + } + public static void UseRange() { #if TODO @@ -173,7 +205,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseIndexForIntIndexerWhenIndexIndexerIsAvailable() - { + { // Same code as the compiler emits for CustomList, // but here we can't translate it back to `customList[GetIndex()]` // because that would call a different overload. @@ -184,7 +216,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseSliceWhenRangeIndexerIsAvailable() - { + { // Same code as the compiler emits for CustomList, // but here we can't translate it back to `customList[GetIndex()]` // because that would call a different overload. diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index ffec61113..37b58782a 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2238,14 +2238,23 @@ namespace ICSharpCode.Decompiler.CSharp arrayType = new ArrayType(compilation, inst.Type, inst.Indices.Count); arrayExpr = arrayExpr.ConvertTo(arrayType, this); } - TranslatedExpression expr = new IndexerExpression( - arrayExpr, inst.Indices.Select(i => TranslateArrayIndex(i).Expression) - ).WithILInstruction(inst).WithRR(new ResolveResult(arrayType.ElementType)); + IndexerExpression indexerExpr; + if (inst.WithSystemIndex) { + var systemIndex = compilation.FindType(KnownTypeCode.Index); + indexerExpr = new IndexerExpression( + arrayExpr, inst.Indices.Select(i => Translate(i, typeHint: systemIndex).ConvertTo(systemIndex, this).Expression) + ); + } else { + indexerExpr = new IndexerExpression( + arrayExpr, inst.Indices.Select(i => TranslateArrayIndex(i).Expression) + ); + } + TranslatedExpression expr = indexerExpr.WithILInstruction(inst).WithRR(new ResolveResult(arrayType.ElementType)); return new DirectionExpression(FieldDirection.Ref, expr) .WithoutILInstruction().WithRR(new ByReferenceResolveResult(expr.Type, ReferenceKind.Ref)); } - TranslatedExpression TranslateArrayIndex(ILInstruction i) + TranslatedExpression TranslateArrayIndex(ILInstruction i, bool expectSystemIndex = false) { var input = Translate(i); KnownTypeCode targetType; diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index a28fea867..75cb72258 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -64,6 +64,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 1db516ca1..7fd43ab93 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -4713,6 +4713,7 @@ namespace ICSharpCode.Decompiler.IL clone.Indices.AddRange(this.Indices.Select(arg => (ILInstruction)arg.Clone())); return clone; } + public bool WithSystemIndex; public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced public override StackType ResultType { get { return StackType.Ref; } } /// Gets whether the 'readonly' prefix was applied to this instruction. @@ -4729,6 +4730,8 @@ namespace ICSharpCode.Decompiler.IL public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); + if (WithSystemIndex) + output.Write("withsystemindex."); if (DelayExceptions) output.Write("delayex."); if (IsReadOnly) @@ -4759,7 +4762,7 @@ namespace ICSharpCode.Decompiler.IL protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) { var o = other as LdElema; - return o != null && type.Equals(o.type) && this.array.PerformMatch(o.array, ref match) && Patterns.ListMatch.DoMatch(this.Indices, o.Indices, ref match) && DelayExceptions == o.DelayExceptions && IsReadOnly == o.IsReadOnly; + return o != null && type.Equals(o.type) && this.array.PerformMatch(o.array, ref match) && Patterns.ListMatch.DoMatch(this.Indices, o.Indices, ref match) && this.WithSystemIndex == o.WithSystemIndex && DelayExceptions == o.DelayExceptions && IsReadOnly == o.IsReadOnly; } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index e18ce8d8e..69c2640eb 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -278,6 +278,7 @@ CustomClassName("LdLen"), CustomArguments(("array", new[] { "O" })), CustomConstructor, CustomWriteTo, MayThrow), new OpCode("ldelema", "Load address of array element.", CustomClassName("LdElema"), HasTypeOperand, CustomChildren(new [] { new ArgumentInfo("array"), new ArgumentInfo("indices") { IsCollection = true } }, true), + BoolFlag("WithSystemIndex"), MayThrowIfNotDelayed, ResultType("Ref"), SupportsReadonlyPrefix), new OpCode("get.pinnable.reference", "Retrieves a pinnable reference for the input object." + Environment.NewLine + "The input must be an object reference (O)." + Environment.NewLine @@ -1105,6 +1106,17 @@ protected override void Disconnected() opCode.WriteOperand.Add("member.WriteTo(output);"); }; + // Adds a member of type bool to the instruction. + static Action BoolFlag(string flagName) + { + return opCode => { + opCode.PerformMatchConditions.Add($"this.{flagName} == o.{flagName}"); + opCode.Members.Add($"public bool {flagName};"); + opCode.GenerateWriteTo = true; + opCode.WriteOpCodePrefix.Add($"if ({flagName}){Environment.NewLine}\toutput.Write(\"{flagName.ToLowerInvariant()}.\");"); + }; + } + // LoadConstant trait: the instruction loads a compile-time constant. Implies NoArguments. static Action LoadConstant(string operandType) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 061ee7721..bfc95c390 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -206,6 +206,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms { base.VisitLdElema(inst); CleanUpArrayIndices(inst.Indices); + if (IndexRangeTransform.HandleLdElema(inst, context)) + return; } protected internal override void VisitNewArr(NewArr inst) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 95fd133ba..448ecbb6c 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -470,6 +470,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } break; + case OpCode.LdElema: + if (((LdElema)parent).WithSystemIndex) { + return true; + } + break; } // decide based on the top-level target instruction into which we are inlining: switch (next.OpCode) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs new file mode 100644 index 000000000..ed3140bf5 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -0,0 +1,89 @@ +// Copyright (c) 2020 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 ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.TypeSystem.Implementation; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + /// + /// Transform for the C# 8 System.Index / System.Range feature + /// + class IndexRangeTransform + { + /// + /// Called by expression transforms. + /// Handles the `array[System.Index]` cases. + /// + public static bool HandleLdElema(LdElema ldelema, ILTransformContext context) + { + if (!context.Settings.Ranges) + return false; + if (!ldelema.Array.MatchLdLoc(out ILVariable array)) + return false; + if (ldelema.Indices.Count != 1) + return false; // the index/range feature doesn't support multi-dimensional arrays + var index = ldelema.Indices[0]; + if (index is CallInstruction call && call.Method.Name == "GetOffset" && call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) { + // ldelema T(ldloc array, call GetOffset(..., ldlen.i4(ldloc array))) + // -> withsystemindex.ldelema T(ldloc array, ...) + if (call.Arguments.Count != 2) + return false; + if (!(call.Arguments[1].MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array))) + return false; + context.Step("ldelema with System.Index", ldelema); + foreach (var node in call.Arguments[1].Descendants) + ldelema.AddILRange(node); + ldelema.AddILRange(call); + ldelema.WithSystemIndex = true; + // The method call had a `ref System.Index` argument for the this pointer, but we want a `System.Index` by-value. + ldelema.Indices[0] = new LdObj(call.Arguments[0], call.Method.DeclaringType); + return true; + } else if (index is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub && !bni.IsLifted && !bni.CheckForOverflow) { + // ldelema T(ldloc array, binary.sub.i4(ldlen.i4(ldloc array), ...)) + // -> withsystemindex.ldelema T(ldloc array, newobj System.Index(..., fromEnd: true)) + if (!(bni.Left.MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array))) + return false; + var indexCtor = FindIndexConstructor(context.TypeSystem); + if (indexCtor == null) + return false; // don't use System.Index if not supported by the target framework + context.Step("ldelema indexed from end", ldelema); + foreach (var node in bni.Left.Descendants) + ldelema.AddILRange(node); + ldelema.AddILRange(bni); + ldelema.WithSystemIndex = true; + ldelema.Indices[0] = new NewObj(indexCtor) { Arguments = { bni.Right, new LdcI4(1) } }; + return true; + } + + return false; + } + + static IMethod FindIndexConstructor(ICompilation compilation) + { + var indexType = compilation.FindType(KnownTypeCode.Index); + foreach (var ctor in indexType.GetConstructors(m => m.Parameters.Count == 2)) { + if (ctor.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32) + && ctor.Parameters[1].Type.IsKnownType(KnownTypeCode.Boolean)) { + return ctor; + } + } + return null; + } + } +}