diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index f5f4337e8..b409376e0 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -90,6 +90,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 66456641c..7a6cc9aa0 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -89,7 +89,13 @@ namespace ICSharpCode.Decompiler.Tests RunForLibrary(); RunForLibrary(asmOptions: AssemblerOptions.UseDebug); } - + + [Test] + public void IndexRangeTest([ValueSource(nameof(dotnetCoreOnlyOptions))] CompilerOptions cscOptions) + { + RunForLibrary(cscOptions: cscOptions); + } + [Test] public void InlineAssignmentTest([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs new file mode 100644 index 000000000..c4c7f6a6c --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -0,0 +1,281 @@ +// 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 System; +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class CustomList + { + public int Count => 0; + public int this[int index] => 0; + + public CustomList Slice(int start, int length) + { + return this; + } + } + + internal class CustomList2 + { + public int Count => 0; + public int this[int index] => 0; + public int this[Index index] => 0; + public CustomList2 this[Range range] => this; + + public CustomList2 Slice(int start, int length) + { + return this; + } + } + + internal class IndexRangeTest + { + public static int[] GetArray() + { + throw null; + } + 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) + { + return i; + } + public static Range GetRange(int i = 0) + { + return i..^i; + } + public static int GetInt(int i = 0) + { + return i; + } + public static Range[] SeveralRanges() + { + // Some of these are semantically identical, but we can still distinguish them in the IL code: + return new Range[14] { + .., + 0.., + ^0.., + GetInt(1).., + ^GetInt(2).., + ..0, + ..^0, + ..GetInt(3), + ..^GetInt(4), + 0..^0, + ^0..0, + 0..0, + GetInt(5)..GetInt(6), + 0..(GetInt(7) + GetInt(8)) + }; + } + + public static void UseIndex() + { + Console.WriteLine(GetArray()[GetIndex()]); + Console.WriteLine(GetList()[GetIndex()]); + Console.WriteLine(GetSpan()[GetIndex()]); + Console.WriteLine(GetString()[GetIndex()]); + Console.WriteLine(new CustomList()[GetIndex()]); + Console.WriteLine(new CustomList2()[GetIndex()]); + } + + public static void UseIndexFromEnd() + { + Console.WriteLine(GetArray()[^GetInt()]); + Console.WriteLine(GetList()[^GetInt()]); + Console.WriteLine(GetSpan()[^GetInt()]); + Console.WriteLine(GetString()[^GetInt()]); + Console.WriteLine(new CustomList()[^GetInt()]); + Console.WriteLine(new CustomList2()[^GetInt()]); + } + + public static void UseIndexForWrite() + { + GetArray()[GetIndex()] = GetInt(); + GetList()[GetIndex()] = GetInt(); + GetSpan()[GetIndex()] = GetInt(); + } + + private static void UseRef(ref int i) + { + } + + public static void UseIndexForRef() + { + UseRef(ref GetArray()[GetIndex()]); + UseRef(ref GetArray()[^GetInt()]); + UseRef(ref GetSpan()[GetIndex()]); + UseRef(ref GetSpan()[^GetInt()]); + } + + public static void UseRange() + { + Console.WriteLine(GetArray()[GetRange()]); + //Console.WriteLine(GetList()[GetRange()]); // fails to compile + Console.WriteLine(GetSpan()[GetRange()].ToString()); + Console.WriteLine(GetString()[GetRange()]); + Console.WriteLine(new CustomList()[GetRange()]); + Console.WriteLine(new CustomList2()[GetRange()]); + } + public static void UseNewRangeFromIndex() + { + Console.WriteLine(GetArray()[GetIndex(1)..GetIndex(2)]); + //Console.WriteLine(GetList()[GetIndex(1)..GetIndex(2)]); // fails to compile + Console.WriteLine(GetSpan()[GetIndex(1)..GetIndex(2)].ToString()); + Console.WriteLine(GetString()[GetIndex(1)..GetIndex(2)]); + Console.WriteLine(new CustomList()[GetIndex(1)..GetIndex(2)]); + Console.WriteLine(new CustomList2()[GetIndex(1)..GetIndex(2)]); + } + public static void UseNewRangeFromIntegers_BothFromStart() + { + Console.WriteLine(GetArray()[GetInt(1)..GetInt(2)]); + //Console.WriteLine(GetList()[GetInt()..GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[GetInt(1)..GetInt(2)].ToString()); + Console.WriteLine(GetString()[GetInt(1)..GetInt(2)]); + Console.WriteLine(new CustomList()[GetInt(1)..GetInt(2)]); + Console.WriteLine(new CustomList2()[GetInt(1)..GetInt(2)]); + } + public static void UseNewRangeFromIntegers_BothFromEnd() + { + Console.WriteLine(GetArray()[^GetInt(1)..^GetInt(2)]); + //Console.WriteLine(GetList()[^GetInt()..^GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[^GetInt(1)..^GetInt(2)].ToString()); + Console.WriteLine(GetString()[^GetInt(1)..^GetInt(2)]); + Console.WriteLine(new CustomList()[^GetInt(1)..^GetInt(2)]); + Console.WriteLine(new CustomList2()[^GetInt(1)..^GetInt(2)]); + } + public static void UseNewRangeFromIntegers_FromStartAndEnd() + { + Console.WriteLine(GetArray()[GetInt(1)..^GetInt(2)]); + //Console.WriteLine(GetList()[GetInt()..^GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[GetInt(1)..^GetInt(2)].ToString()); + Console.WriteLine(GetString()[GetInt(1)..^GetInt(2)]); + Console.WriteLine(new CustomList()[GetInt(1)..^GetInt(2)]); + Console.WriteLine(new CustomList2()[GetInt(1)..^GetInt(2)]); + } + public static void UseNewRangeFromIntegers_FromEndAndStart() + { + Console.WriteLine(GetArray()[^GetInt(1)..GetInt(2)]); + //Console.WriteLine(GetList()[^GetInt()..GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[^GetInt(1)..GetInt(2)].ToString()); + Console.WriteLine(GetString()[^GetInt(1)..GetInt(2)]); + Console.WriteLine(new CustomList()[^GetInt(1)..GetInt(2)]); + Console.WriteLine(new CustomList2()[^GetInt(1)..GetInt(2)]); + } + + public static void UseNewRangeFromIntegers_OnlyEndPoint() + { + Console.WriteLine(GetArray()[..GetInt(2)]); + //Console.WriteLine(GetList()[..GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[..GetInt(2)].ToString()); + Console.WriteLine(GetString()[..GetInt(2)]); + Console.WriteLine(new CustomList()[..GetInt(2)]); + Console.WriteLine(new CustomList2()[..GetInt(2)]); + } + + public static void UseNewRangeFromIntegers_OnlyEndPoint_FromEnd() + { + Console.WriteLine(GetArray()[..^GetInt(2)]); + //Console.WriteLine(GetList()[..^GetInt()]); // fails to compile + Console.WriteLine(GetSpan()[..^GetInt(2)].ToString()); + Console.WriteLine(GetString()[..^GetInt(2)]); + Console.WriteLine(new CustomList()[..^GetInt(2)]); + Console.WriteLine(new CustomList2()[..^GetInt(2)]); + } + + public static void UseNewRangeFromIntegers_OnlyStartPoint() + { + Console.WriteLine(GetArray()[GetInt(1)..]); + //Console.WriteLine(GetList()[GetInt()..]); // fails to compile + Console.WriteLine(GetSpan()[GetInt(1)..].ToString()); + Console.WriteLine(GetString()[GetInt(1)..]); + Console.WriteLine(new CustomList()[GetInt(1)..]); + Console.WriteLine(new CustomList2()[GetInt(1)..]); + } + + public static void UseNewRangeFromIntegers_OnlyStartPoint_FromEnd() + { + Console.WriteLine(GetArray()[^GetInt(1)..]); + //Console.WriteLine(GetList()[^GetInt()..]); // fails to compile + Console.WriteLine(GetSpan()[^GetInt(1)..].ToString()); + Console.WriteLine(GetString()[^GetInt(1)..]); + Console.WriteLine(new CustomList()[^GetInt(1)..]); + Console.WriteLine(new CustomList2()[^GetInt(1)..]); + } + + public static void UseConstantRange() + { + // Fortunately the C# compiler doesn't optimize + // "str.Length - 2 - 1" here, so the normal pattern applies. + Console.WriteLine(GetString()[1..2]); + Console.WriteLine(GetString()[1..^1]); + Console.WriteLine(GetString()[^2..^1]); + + Console.WriteLine(GetString()[..1]); + Console.WriteLine(GetString()[..^1]); + Console.WriteLine(GetString()[1..]); + Console.WriteLine(GetString()[^1..]); + } + + public static void UseWholeRange() + { + Console.WriteLine(GetArray()[..]); + //Console.WriteLine(GetList()[..]); // fails to compile + Console.WriteLine(GetSpan()[..].ToString()); + Console.WriteLine(GetString()[..]); + Console.WriteLine(new CustomList()[..]); + Console.WriteLine(new CustomList2()[..]); + } + + 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. + CustomList2 customList = new CustomList2(); + int count = customList.Count; + int offset = GetIndex().GetOffset(count); + Console.WriteLine(customList[offset]); + } + + 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. + CustomList2 customList = new CustomList2(); + int count = customList.Count; + Range range = GetRange(); + int offset = range.Start.GetOffset(count); + int length = range.End.GetOffset(count) - offset; + Console.WriteLine(customList.Slice(offset, length)); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index d5d9ce9d9..3fdf9f0ad 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -150,7 +150,8 @@ namespace ICSharpCode.Decompiler.CSharp new TransformCollectionAndObjectInitializers(), new TransformExpressionTrees(), new NamedArgumentTransform(), - new UserDefinedLogicTransform() + new UserDefinedLogicTransform(), + new IndexRangeTransform() ), } }, diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 267a757fe..68a3daf8e 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -252,6 +252,12 @@ namespace ICSharpCode.Decompiler.CSharp argumentList.ExpectedParameters = method.Parameters.ToArray(); } + if (settings.Ranges) { + if (HandleRangeConstruction(out var result, callOpCode, method, target, argumentList)) { + return result; + } + } + if (callOpCode == OpCode.NewObj) { return HandleConstructorCall(expectedTargetDetails, target.ResolveResult, method, argumentList); } @@ -1494,5 +1500,47 @@ namespace ICSharpCode.Decompiler.CSharp return Build(call.OpCode, call.Method, arguments, argumentToParameterMap, call.ConstrainedTo) .WithILInstruction(call).WithILInstruction(block); } + + private bool HandleRangeConstruction(out ExpressionWithResolveResult result, OpCode callOpCode, IMethod method, TranslatedExpression target, ArgumentList argumentList) + { + result = default; + if (argumentList.ArgumentNames != null) { + return false; // range syntax doesn't support named arguments + } + if (method.DeclaringType.IsKnownType(KnownTypeCode.Range)) { + if (callOpCode == OpCode.NewObj && argumentList.Length == 2) { + result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, argumentList.Arguments[1]) + .WithRR(new MemberResolveResult(null, method)); + return true; + } else if (callOpCode == OpCode.Call && method.Name == "get_All" && argumentList.Length == 0) { + result = new BinaryOperatorExpression(Expression.Null, BinaryOperatorType.Range, Expression.Null) + .WithRR(new MemberResolveResult(null, method.AccessorOwner ?? method)); + return true; + } else if (callOpCode == OpCode.Call && method.Name == "StartAt" && argumentList.Length == 1) { + result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, Expression.Null) + .WithRR(new MemberResolveResult(null, method)); + return true; + } else if (callOpCode == OpCode.Call && method.Name == "EndAt" && argumentList.Length == 1) { + result = new BinaryOperatorExpression(Expression.Null, BinaryOperatorType.Range, argumentList.Arguments[0]) + .WithRR(new MemberResolveResult(null, method)); + return true; + } + } else if (callOpCode == OpCode.NewObj && method.DeclaringType.IsKnownType(KnownTypeCode.Index)) { + if (argumentList.Length != 2) + return false; + if (!(argumentList.Arguments[1].Expression is PrimitiveExpression pe && pe.Value is true)) + return false; + result = new UnaryOperatorExpression(UnaryOperatorType.IndexFromEnd, argumentList.Arguments[0]) + .WithRR(new MemberResolveResult(null, method)); + return true; + } else if (method is SyntheticRangeIndexAccessor rangeIndexAccessor && rangeIndexAccessor.IsSlicing) { + // For slicing the method is called Slice()/Substring(), but we still need to output indexer notation. + // So special-case range-based slicing here. + result = new IndexerExpression(target, argumentList.Arguments.Select(a => a.Expression)) + .WithRR(new MemberResolveResult(target.ResolveResult, method)); + return true; + } + return false; + } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 31f9fa0f1..7c6660e50 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2244,14 +2244,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/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 4eea34baa..1d3f99617 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -696,6 +696,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor case BinaryOperatorType.NullCoalescing: spacePolicy = true; break; + case BinaryOperatorType.Range: + spacePolicy = false; + break; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs index 3af55614f..4791b3127 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team +// Copyright (c) 2010-2020 AlphaSierraPapa for the SharpDevelop Team // // 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 @@ -36,25 +36,39 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor /// public bool InsertParenthesesForReadability { get; set; } - const int Primary = 17; - const int NullableRewrap = 16; - const int QueryOrLambda = 15; - const int Unary = 14; - const int RelationalAndTypeTesting = 10; - const int Equality = 9; - const int Conditional = 2; - const int Assignment = 1; + enum PrecedenceLevel + { + // Higher integer value = higher precedence. + Assignment, + Conditional, // ?: + NullCoalescing, // ?? + ConditionalOr, // || + ConditionalAnd, // && + BitwiseOr, // | + ExclusiveOr, // binary ^ + BitwiseAnd, // binary & + Equality, // == != + RelationalAndTypeTesting, // < <= > >= is + Shift, // << >> + Additive, // binary + - + Multiplicative, // * / % + Range, // .. + Unary, + QueryOrLambda, + NullableRewrap, + Primary + } /// /// Gets the row number in the C# 4.0 spec operator precedence table. /// - static int GetPrecedence(Expression expr) + static PrecedenceLevel GetPrecedence(Expression expr) { // Note: the operator precedence table on MSDN is incorrect if (expr is QueryExpression) { // Not part of the table in the C# spec, but we need to ensure that queries within // primary expressions get parenthesized. - return QueryOrLambda; + return PrecedenceLevel.QueryOrLambda; } if (expr is UnaryOperatorExpression uoe) { switch (uoe.Operator) { @@ -62,81 +76,83 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor case UnaryOperatorType.PostIncrement: case UnaryOperatorType.NullConditional: case UnaryOperatorType.SuppressNullableWarning: - return Primary; + return PrecedenceLevel.Primary; case UnaryOperatorType.NullConditionalRewrap: - return NullableRewrap; + return PrecedenceLevel.NullableRewrap; case UnaryOperatorType.IsTrue: - return Conditional; + return PrecedenceLevel.Conditional; default: - return Unary; + return PrecedenceLevel.Unary; } } if (expr is CastExpression) - return Unary; + return PrecedenceLevel.Unary; if (expr is PrimitiveExpression primitive) { var value = primitive.Value; if (value is int i && i < 0) - return Unary; + return PrecedenceLevel.Unary; if (value is long l && l < 0) - return Unary; + return PrecedenceLevel.Unary; if (value is float f && f < 0) - return Unary; + return PrecedenceLevel.Unary; if (value is double d && d < 0) - return Unary; + return PrecedenceLevel.Unary; if (value is decimal de && de < 0) - return Unary; + return PrecedenceLevel.Unary; + return PrecedenceLevel.Primary; } - BinaryOperatorExpression boe = expr as BinaryOperatorExpression; - if (boe != null) { + if (expr is BinaryOperatorExpression boe) { switch (boe.Operator) { + case BinaryOperatorType.Range: + return PrecedenceLevel.Range; case BinaryOperatorType.Multiply: case BinaryOperatorType.Divide: case BinaryOperatorType.Modulus: - return 13; // multiplicative + return PrecedenceLevel.Multiplicative; case BinaryOperatorType.Add: case BinaryOperatorType.Subtract: - return 12; // additive + return PrecedenceLevel.Additive; case BinaryOperatorType.ShiftLeft: case BinaryOperatorType.ShiftRight: - return 11; + return PrecedenceLevel.Shift; case BinaryOperatorType.GreaterThan: case BinaryOperatorType.GreaterThanOrEqual: case BinaryOperatorType.LessThan: case BinaryOperatorType.LessThanOrEqual: - return RelationalAndTypeTesting; + return PrecedenceLevel.RelationalAndTypeTesting; case BinaryOperatorType.Equality: case BinaryOperatorType.InEquality: - return Equality; + return PrecedenceLevel.Equality; case BinaryOperatorType.BitwiseAnd: - return 8; + return PrecedenceLevel.BitwiseAnd; case BinaryOperatorType.ExclusiveOr: - return 7; + return PrecedenceLevel.ExclusiveOr; case BinaryOperatorType.BitwiseOr: - return 6; + return PrecedenceLevel.BitwiseOr; case BinaryOperatorType.ConditionalAnd: - return 5; + return PrecedenceLevel.ConditionalAnd; case BinaryOperatorType.ConditionalOr: - return 4; + return PrecedenceLevel.ConditionalOr; case BinaryOperatorType.NullCoalescing: - return 3; + return PrecedenceLevel.NullCoalescing; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } } if (expr is IsExpression || expr is AsExpression) - return RelationalAndTypeTesting; + return PrecedenceLevel.RelationalAndTypeTesting; if (expr is ConditionalExpression || expr is DirectionExpression) - return Conditional; + return PrecedenceLevel.Conditional; if (expr is AssignmentExpression || expr is LambdaExpression) - return Assignment; + return PrecedenceLevel.Assignment; // anything else: primary expression - return Primary; + return PrecedenceLevel.Primary; } /// /// Parenthesizes the expression if it does not have the minimum required precedence. /// - static void ParenthesizeIfRequired(Expression expr, int minimumPrecedence) + static void ParenthesizeIfRequired(Expression expr, PrecedenceLevel minimumPrecedence) { if (GetPrecedence(expr) < minimumPrecedence) { Parenthesize(expr); @@ -151,25 +167,25 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor // Primary expressions public override void VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression) { - ParenthesizeIfRequired(memberReferenceExpression.Target, Primary); + ParenthesizeIfRequired(memberReferenceExpression.Target, PrecedenceLevel.Primary); base.VisitMemberReferenceExpression(memberReferenceExpression); } public override void VisitPointerReferenceExpression(PointerReferenceExpression pointerReferenceExpression) { - ParenthesizeIfRequired(pointerReferenceExpression.Target, Primary); + ParenthesizeIfRequired(pointerReferenceExpression.Target, PrecedenceLevel.Primary); base.VisitPointerReferenceExpression(pointerReferenceExpression); } public override void VisitInvocationExpression(InvocationExpression invocationExpression) { - ParenthesizeIfRequired(invocationExpression.Target, Primary); + ParenthesizeIfRequired(invocationExpression.Target, PrecedenceLevel.Primary); base.VisitInvocationExpression(invocationExpression); } public override void VisitIndexerExpression(IndexerExpression indexerExpression) { - ParenthesizeIfRequired(indexerExpression.Target, Primary); + ParenthesizeIfRequired(indexerExpression.Target, PrecedenceLevel.Primary); ArrayCreateExpression ace = indexerExpression.Target as ArrayCreateExpression; if (ace != null && (InsertParenthesesForReadability || ace.Initializer.IsNull)) { // require parentheses for "(new int[1])[0]" @@ -192,7 +208,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { // Even in readability mode, don't parenthesize casts of casts. if (!(castExpression.Expression is CastExpression)) { - ParenthesizeIfRequired(castExpression.Expression, InsertParenthesesForReadability ? NullableRewrap : Unary); + ParenthesizeIfRequired(castExpression.Expression, InsertParenthesesForReadability ? PrecedenceLevel.NullableRewrap : PrecedenceLevel.Unary); } // There's a nasty issue in the C# grammar: cast expressions including certain operators are ambiguous in some cases // "(int)-1" is fine, but "(A)-b" is not a cast. @@ -255,14 +271,14 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor // Binary Operators public override void VisitBinaryOperatorExpression(BinaryOperatorExpression binaryOperatorExpression) { - int precedence = GetPrecedence(binaryOperatorExpression); + PrecedenceLevel precedence = GetPrecedence(binaryOperatorExpression); if (binaryOperatorExpression.Operator == BinaryOperatorType.NullCoalescing) { if (InsertParenthesesForReadability) { - ParenthesizeIfRequired(binaryOperatorExpression.Left, NullableRewrap); + ParenthesizeIfRequired(binaryOperatorExpression.Left, PrecedenceLevel.NullableRewrap); if (GetBinaryOperatorType(binaryOperatorExpression.Right) == BinaryOperatorType.NullCoalescing) { ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence); } else { - ParenthesizeIfRequired(binaryOperatorExpression.Right, NullableRewrap); + ParenthesizeIfRequired(binaryOperatorExpression.Right, PrecedenceLevel.NullableRewrap); } } else { // ?? is right-associative @@ -270,10 +286,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence); } } else { - if (InsertParenthesesForReadability && precedence < Equality) { + if (InsertParenthesesForReadability && precedence < PrecedenceLevel.Equality) { // In readable mode, boost the priority of the left-hand side if the operator // there isn't the same as the operator on this expression. - int boostTo = IsBitwise(binaryOperatorExpression.Operator) ? Unary : Equality; + PrecedenceLevel boostTo = IsBitwise(binaryOperatorExpression.Operator) ? PrecedenceLevel.Unary : PrecedenceLevel.Equality; if (GetBinaryOperatorType(binaryOperatorExpression.Left) == binaryOperatorExpression.Operator) { ParenthesizeIfRequired(binaryOperatorExpression.Left, precedence); } else { @@ -309,9 +325,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { if (InsertParenthesesForReadability) { // few people know the precedence of 'is', so always put parentheses in nice-looking mode. - ParenthesizeIfRequired(isExpression.Expression, NullableRewrap); + ParenthesizeIfRequired(isExpression.Expression, PrecedenceLevel.NullableRewrap); } else { - ParenthesizeIfRequired(isExpression.Expression, RelationalAndTypeTesting); + ParenthesizeIfRequired(isExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting); } base.VisitIsExpression(isExpression); } @@ -320,9 +336,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor { if (InsertParenthesesForReadability) { // few people know the precedence of 'as', so always put parentheses in nice-looking mode. - ParenthesizeIfRequired(asExpression.Expression, NullableRewrap); + ParenthesizeIfRequired(asExpression.Expression, PrecedenceLevel.NullableRewrap); } else { - ParenthesizeIfRequired(asExpression.Expression, RelationalAndTypeTesting); + ParenthesizeIfRequired(asExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting); } base.VisitAsExpression(asExpression); } @@ -341,13 +357,13 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor // Only ((a ? b : c) ? d : e) strictly needs the additional parentheses if (InsertParenthesesForReadability && !IsConditionalRefExpression(conditionalExpression)) { // Precedence of ?: can be confusing; so always put parentheses in nice-looking mode. - ParenthesizeIfRequired(conditionalExpression.Condition, NullableRewrap); - ParenthesizeIfRequired(conditionalExpression.TrueExpression, NullableRewrap); - ParenthesizeIfRequired(conditionalExpression.FalseExpression, NullableRewrap); + ParenthesizeIfRequired(conditionalExpression.Condition, PrecedenceLevel.NullableRewrap); + ParenthesizeIfRequired(conditionalExpression.TrueExpression, PrecedenceLevel.NullableRewrap); + ParenthesizeIfRequired(conditionalExpression.FalseExpression, PrecedenceLevel.NullableRewrap); } else { - ParenthesizeIfRequired(conditionalExpression.Condition, Conditional + 1); - ParenthesizeIfRequired(conditionalExpression.TrueExpression, Conditional); - ParenthesizeIfRequired(conditionalExpression.FalseExpression, Conditional); + ParenthesizeIfRequired(conditionalExpression.Condition, PrecedenceLevel.Conditional + 1); + ParenthesizeIfRequired(conditionalExpression.TrueExpression, PrecedenceLevel.Conditional); + ParenthesizeIfRequired(conditionalExpression.FalseExpression, PrecedenceLevel.Conditional); } base.VisitConditionalExpression(conditionalExpression); } @@ -361,11 +377,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor public override void VisitAssignmentExpression(AssignmentExpression assignmentExpression) { // assignment is right-associative - ParenthesizeIfRequired(assignmentExpression.Left, Assignment + 1); + ParenthesizeIfRequired(assignmentExpression.Left, PrecedenceLevel.Assignment + 1); if (InsertParenthesesForReadability && !(assignmentExpression.Right is DirectionExpression)) { - ParenthesizeIfRequired(assignmentExpression.Right, RelationalAndTypeTesting + 1); + ParenthesizeIfRequired(assignmentExpression.Right, PrecedenceLevel.RelationalAndTypeTesting + 1); } else { - ParenthesizeIfRequired(assignmentExpression.Right, Assignment); + ParenthesizeIfRequired(assignmentExpression.Right, PrecedenceLevel.Assignment); } base.VisitAssignmentExpression(assignmentExpression); } @@ -394,7 +410,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor public override void VisitNamedExpression (NamedExpression namedExpression) { if (InsertParenthesesForReadability) { - ParenthesizeIfRequired(namedExpression.Expression, RelationalAndTypeTesting + 1); + ParenthesizeIfRequired(namedExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting + 1); } base.VisitNamedExpression (namedExpression); } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs index a7eb4d6db..b6358f56a 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs @@ -54,6 +54,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public readonly static TokenRole ShiftLeftRole = new TokenRole ("<<"); public readonly static TokenRole ShiftRightRole = new TokenRole (">>"); public readonly static TokenRole NullCoalescingRole = new TokenRole ("??"); + public readonly static TokenRole RangeRole = new TokenRole (".."); public readonly static Role LeftRole = new Role("Left", Expression.Null); public readonly static Role RightRole = new Role("Right", Expression.Null); @@ -151,6 +152,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return ShiftRightRole; case BinaryOperatorType.NullCoalescing: return NullCoalescingRole; + case BinaryOperatorType.Range: + return RangeRole; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } @@ -197,6 +200,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return ExpressionType.RightShift; case BinaryOperatorType.NullCoalescing: return ExpressionType.Coalesce; + case BinaryOperatorType.Range: + return ExpressionType.Extension; default: throw new NotSupportedException("Invalid value for BinaryOperatorType"); } @@ -255,6 +260,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax ShiftRight, /// left ?? right - NullCoalescing + NullCoalescing, + /// left .. right + /// left and right are optional = may be Expression.Null + Range } } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs index 31e9bd9f1..eb29f2d65 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs @@ -45,6 +45,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax public readonly static TokenRole AwaitRole = new TokenRole ("await"); public readonly static TokenRole NullConditionalRole = new TokenRole ("?"); public readonly static TokenRole SuppressNullableWarningRole = new TokenRole ("!"); + public readonly static TokenRole IndexFromEndRole = new TokenRole ("^"); public UnaryOperatorExpression() { @@ -122,6 +123,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return null; // no syntax case UnaryOperatorType.SuppressNullableWarning: return SuppressNullableWarningRole; + case UnaryOperatorType.IndexFromEnd: + return IndexFromEndRole; default: throw new NotSupportedException("Invalid value for UnaryOperatorType"); } @@ -150,6 +153,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax case UnaryOperatorType.AddressOf: case UnaryOperatorType.Await: case UnaryOperatorType.SuppressNullableWarning: + case UnaryOperatorType.IndexFromEnd: return ExpressionType.Extension; default: throw new NotSupportedException("Invalid value for UnaryOperatorType"); @@ -206,5 +210,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax /// C# 8 postfix ! operator (dammit operator) /// SuppressNullableWarning, + /// + /// C# 8 prefix ^ operator + /// + IndexFromEnd, } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs index f031778fd..e50eb7134 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs @@ -125,6 +125,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms invocationExpression.ReplaceWith(new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First()))); } break; + case "System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray": + if (arguments.Length == 2 && context.Settings.Ranges) { + var slicing = new IndexerExpression(arguments[0].Detach(), arguments[1].Detach()); + slicing.CopyAnnotationsFrom(invocationExpression); + invocationExpression.ReplaceWith(slicing); + } + break; } BinaryOperatorType? bop = GetBinaryOperatorTypeFromMetadataName(method.Name); diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 95eb3fcb7..18efc74a0 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -112,12 +112,13 @@ namespace ICSharpCode.Decompiler asyncUsingAndForEachStatement = false; asyncEnumerator = false; staticLocalFunctions = false; + ranges = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions) + if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions || ranges) return CSharp.LanguageVersion.CSharp8_0; if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers || patternBasedFixedStatement) return CSharp.LanguageVersion.CSharp7_3; @@ -1100,6 +1101,23 @@ namespace ICSharpCode.Decompiler } } + bool ranges = true; + + /// + /// Gets/Sets whether C# 8.0 static local functions should be transformed. + /// + [Category("C# 8.0 / VS 2019")] + [Description("DecompilerSettings.Ranges")] + public bool Ranges { + get { return ranges; } + set { + if (ranges != value) { + ranges = value; + OnPropertyChanged(); + } + } + } + bool nullableReferenceTypes = true; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index d21930dbb..3d735ec42 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -64,6 +64,7 @@ + @@ -384,6 +385,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 a011b0051..97c17d1a8 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -208,6 +208,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..ca20dbbb7 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -21,6 +21,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.TypeSystem.Implementation; namespace ICSharpCode.Decompiler.IL.Transforms { @@ -469,6 +470,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (parent.SlotInfo == CompoundAssignmentInstruction.TargetSlot) { return true; } + if (((CallInstruction)parent).Method is SyntheticRangeIndexAccessor) { + 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: diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs new file mode 100644 index 000000000..2140c5416 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -0,0 +1,596 @@ +// 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 System; +using System.Diagnostics; +using System.Linq; +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 : IStatementTransform + { + /// + /// 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 indexMethods = new IndexMethods(context.TypeSystem); + if (!indexMethods.IsValid) + 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] = MakeIndex(IndexKind.FromEnd, bni.Right, indexMethods); + return true; + } + + return false; + } + + class IndexMethods + { + public readonly IMethod IndexCtor; + public readonly IMethod IndexImplicitConv; + public readonly IMethod RangeCtor; + public IType IndexType => IndexCtor?.DeclaringType; + public IType RangeType => RangeCtor?.DeclaringType; + public bool IsValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null; + + public readonly IMethod RangeStartAt; + public readonly IMethod RangeEndAt; + public readonly IMethod RangeGetAll; + + public IndexMethods(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)) { + this.IndexCtor = ctor; + } + } + foreach (var op in indexType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit")) { + if (op.Parameters.Count == 1 && op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) { + this.IndexImplicitConv = op; + } + } + var rangeType = compilation.FindType(KnownTypeCode.Range); + foreach (var ctor in rangeType.GetConstructors(m => m.Parameters.Count == 2)) { + if (ctor.Parameters[0].Type.IsKnownType(KnownTypeCode.Index) + && ctor.Parameters[1].Type.IsKnownType(KnownTypeCode.Index)) { + this.RangeCtor = ctor; + } + } + foreach (var m in rangeType.GetMethods(m => m.Parameters.Count == 1)) { + if (m.Parameters.Count == 1 && m.Parameters[0].Type.IsKnownType(KnownTypeCode.Index)) { + if (m.Name == "StartAt") + this.RangeStartAt = m; + else if (m.Name == "EndAt") + this.RangeEndAt = m; + } + } + foreach (var p in rangeType.GetProperties(p => p.IsStatic && p.Name == "All")) { + this.RangeGetAll = p.Getter; + } + } + } + + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) + { + if (!context.Settings.Ranges) + return; + int startPos = pos; + ILVariable containerVar = null; + // The container length access may be a separate instruction, or it may be inline with the variable's use + if (MatchContainerLengthStore(block.Instructions[pos], out ILVariable containerLengthVar, ref containerVar)) { + // stloc containerLengthVar(call get_Length/get_Count(ldloc container)) + pos++; + } else { + // Reset if MatchContainerLengthStore only had a partial match. MatchGetOffset() will then set `containerVar`. + containerLengthVar = null; + containerVar = null; + } + if (block.Instructions[pos].MatchStLoc(out var rangeVar, out var rangeVarInit) && rangeVar.Type.IsKnownType(KnownTypeCode.Range)) { + // stloc rangeVar(rangeVarInit) + pos++; + } else { + rangeVar = null; + rangeVarInit = null; + } + // stloc startOffsetVar(call GetOffset(startIndexLoad, ldloc length)) + if (!block.Instructions[pos].MatchStLoc(out ILVariable startOffsetVar, out ILInstruction startOffsetVarInit)) + return; + if (!(startOffsetVar.IsSingleDefinition && startOffsetVar.StackType == StackType.I4)) + return; + var startIndexKind = MatchGetOffset(startOffsetVarInit, out ILInstruction startIndexLoad, containerLengthVar, ref containerVar); + pos++; + if (startOffsetVar.LoadCount == 1) { + TransformIndexing(); + } else if (startOffsetVar.LoadCount == 2) { + // might be slicing: startOffset is used once for the slice length calculation, and once for the Slice() method call + TransformSlicing(); + } + + void TransformIndexing() + { + // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) + + if (rangeVar != null) + return; + if (!(startOffsetVar.LoadInstructions.Single().Parent is CallInstruction call)) + return; + if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter && call.Arguments.Count == 2) { + if (call.Method.AccessorOwner?.SymbolKind != SymbolKind.Indexer) + return; + if (call.Method.Parameters.Count != 1) + return; + } else if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Setter && call.Arguments.Count == 3) { + if (call.Method.AccessorOwner?.SymbolKind != SymbolKind.Indexer) + return; + if (call.Method.Parameters.Count != 2) + return; + } else if (IsSlicingMethod(call.Method)) { + TransformSlicing(sliceLengthWasMisdetectedAsStartOffset: true); + return; + } else { + return; + } + if (startIndexKind == IndexKind.FromStart) { + // FromStart is only relevant for slicing; indexing from the start does not involve System.Index at all. + return; + } + if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind)) { + return; + } + // startOffsetVar might be used deep inside a complex statement, ensure we can inline up to that point: + for (int i = startPos; i < pos; i++) { + if (!ILInlining.CanInlineInto(block.Instructions[pos], startOffsetVar, block.Instructions[i])) + return; + } + if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) + return; + if (!MatchContainerVar(call.Arguments[0], ref containerVar)) + return; + if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) + return; + var specialMethods = new IndexMethods(context.TypeSystem); + if (!specialMethods.IsValid) + return; + if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false)) + return; + + context.Step($"{call.Method.Name} indexed with {startIndexKind}", call); + var newMethod = new SyntheticRangeIndexAccessor(call.Method, specialMethods.IndexType, slicing: false); + var newCall = CallInstruction.Create(call.OpCode, newMethod); + newCall.ConstrainedTo = call.ConstrainedTo; + newCall.ILStackWasEmpty = call.ILStackWasEmpty; + newCall.Arguments.Add(call.Arguments[0]); + newCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods)); + newCall.Arguments.AddRange(call.Arguments.Skip(2)); + newCall.AddILRange(call); + for (int i = startPos; i < pos; i++) { + newCall.AddILRange(block.Instructions[i]); + } + call.ReplaceWith(newCall); + block.Instructions.RemoveRange(startPos, pos - startPos); + } + + void TransformSlicing(bool sliceLengthWasMisdetectedAsStartOffset = false) + { + ILVariable sliceLengthVar; + ILInstruction sliceLengthVarInit; + if (sliceLengthWasMisdetectedAsStartOffset) { + // Special case: when slicing without a start point, the slice length calculation is mis-detected as the start offset, + // and since it only has a single use, we end in TransformIndexing(), which then calls TransformSlicing + // on this code path. + sliceLengthVar = startOffsetVar; + sliceLengthVarInit = ((StLoc)sliceLengthVar.StoreInstructions.Single()).Value; + startOffsetVar = null; + startIndexLoad = new LdcI4(0); + startIndexKind = IndexKind.TheStart; + } else { + // stloc containerLengthVar(call get_Length(ldloc containerVar)) + // stloc startOffset(call GetOffset(startIndexLoad, ldloc length)) + // -- we are here -- + // stloc sliceLengthVar(binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset)) + // complex_expr(call Slice(ldloc containerVar, ldloc startOffset, ldloc sliceLength)) + + if (!block.Instructions[pos].MatchStLoc(out sliceLengthVar, out sliceLengthVarInit)) + return; + pos++; + } + + if (!(sliceLengthVar.IsSingleDefinition && sliceLengthVar.LoadCount == 1)) + return; + if (!MatchSliceLength(sliceLengthVarInit, out IndexKind endIndexKind, out ILInstruction endIndexLoad, containerLengthVar, ref containerVar, startOffsetVar)) + return; + if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind, endIndexKind)) { + return; + } + if (rangeVar != null) { + if (!MatchIndexFromRange(startIndexKind, startIndexLoad, rangeVar, "get_Start")) + return; + if (!MatchIndexFromRange(endIndexKind, endIndexLoad, rangeVar, "get_End")) + return; + } + if (!(sliceLengthVar.LoadInstructions.Single().Parent is CallInstruction call)) + return; + if (!IsSlicingMethod(call.Method)) + return; + if (call.Arguments.Count != 3) + return; + if (!MatchContainerVar(call.Arguments[0], ref containerVar)) + return; + if (startOffsetVar == null) { + Debug.Assert(startIndexKind == IndexKind.TheStart); + if (!call.Arguments[1].MatchLdcI4(0)) + return; + } else { + if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) + return; + } + if (!call.Arguments[2].MatchLdLoc(sliceLengthVar)) + return; + if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: true)) + return; + if (startIndexKind == IndexKind.FromStart && endIndexKind == IndexKind.FromStart) { + // It's possible we actually have a startIndex/endIndex that involves the container length, + // but we couldn't detect it yet because the statement initializing the containerLengthVar is + // not yet part of the region to be transformed. + // If we transform now, we'd end up with: + // int length = span.Length; + // Console.WriteLine(span[(length - GetInt(1))..(length - GetInt(2))].ToString()); + // which is correct but unnecessarily complex. + // So we peek ahead at the next instruction to be transformed: + if (startPos > 0 && MatchContainerLengthStore(block.Instructions[startPos - 1], out _, ref containerVar)) { + // Looks like the transform would be able do to a better job including that previous instruction, + // so let's avoid transforming just yet. + return; + } + // Something similar happens with the rangeVar: + if (startPos > 0 && block.Instructions[startPos - 1] is StLoc stloc && stloc.Variable.Type.IsKnownType(KnownTypeCode.Range)) { + return; + } + } + var specialMethods = new IndexMethods(context.TypeSystem); + if (!specialMethods.IsValid) + return; + + context.Step($"{call.Method.Name} sliced with {startIndexKind}..{endIndexKind}", call); + var newMethod = new SyntheticRangeIndexAccessor(call.Method, specialMethods.RangeType, slicing: true); + var newCall = CallInstruction.Create(call.OpCode, newMethod); + newCall.ConstrainedTo = call.ConstrainedTo; + newCall.ILStackWasEmpty = call.ILStackWasEmpty; + newCall.Arguments.Add(call.Arguments[0]); + if (rangeVar != null) { + newCall.Arguments.Add(rangeVarInit); + } else if (startIndexKind == IndexKind.TheStart && endIndexKind == IndexKind.TheEnd && specialMethods.RangeGetAll != null) { + newCall.Arguments.Add(new Call(specialMethods.RangeGetAll)); + } else if (startIndexKind == IndexKind.TheStart && specialMethods.RangeEndAt != null) { + var rangeCtorCall = new Call(specialMethods.RangeEndAt); + rangeCtorCall.Arguments.Add(MakeIndex(endIndexKind, endIndexLoad, specialMethods)); + newCall.Arguments.Add(rangeCtorCall); + } else if (endIndexKind == IndexKind.TheEnd && specialMethods.RangeStartAt != null) { + var rangeCtorCall = new Call(specialMethods.RangeStartAt); + rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods)); + newCall.Arguments.Add(rangeCtorCall); + } else { + var rangeCtorCall = new NewObj(specialMethods.RangeCtor); + rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods)); + rangeCtorCall.Arguments.Add(MakeIndex(endIndexKind, endIndexLoad, specialMethods)); + newCall.Arguments.Add(rangeCtorCall); + } + newCall.AddILRange(call); + for (int i = startPos; i < pos; i++) { + newCall.AddILRange(block.Instructions[i]); + } + call.ReplaceWith(newCall); + block.Instructions.RemoveRange(startPos, pos - startPos); + } + } + + static bool IsSlicingMethod(IMethod method) + { + if (method.IsExtensionMethod) + return false; + if (method.Parameters.Count != 2) + return false; + if (!method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32))) + return false; + return method.Name == "Slice" + || (method.Name == "Substring" && method.DeclaringType.IsKnownType(KnownTypeCode.String)); + } + + /// + /// Check that the number of uses of the containerLengthVar variable matches those expected in the pattern. + /// + private bool CheckContainerLengthVariableUseCount(ILVariable containerLengthVar, IndexKind startIndexKind, IndexKind endIndexKind = IndexKind.FromStart) + { + int expectedUses = 0; + if (startIndexKind != IndexKind.FromStart && startIndexKind != IndexKind.TheStart) + expectedUses += 1; + if (endIndexKind != IndexKind.FromStart && endIndexKind != IndexKind.TheStart) + expectedUses += 1; + if (containerLengthVar != null) { + return containerLengthVar.LoadCount == expectedUses; + } else { + return expectedUses <= 1; // can have one inline use + } + } + + /// + /// Matches 'addressof System.Index(call get_Start/get_End(ldloca rangeVar))' + /// + static bool MatchIndexFromRange(IndexKind indexKind, ILInstruction indexLoad, ILVariable rangeVar, string accessorName) + { + if (indexKind != IndexKind.RefSystemIndex) + return false; + if (!(indexLoad is AddressOf addressOf)) + return false; + if (!addressOf.Type.IsKnownType(KnownTypeCode.Index)) + return false; + if (!(addressOf.Value is Call call)) + return false; + if (call.Method.Name != accessorName) + return false; + if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Range)) + return false; + if (call.Arguments.Count != 1) + return false; + return call.Arguments[0].MatchLdLoca(rangeVar); + } + + static ILInstruction MakeIndex(IndexKind indexKind, ILInstruction indexLoad, IndexMethods specialMethods) + { + if (indexKind == IndexKind.RefSystemIndex) { + // stloc containerLengthVar(call get_Length/get_Count(ldloc container)) + // stloc startOffsetVar(call GetOffset(startIndexLoad, ldloc length)) + // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) + // --> + // complex_expr(call get_Item(ldloc container, ldobj startIndexLoad)) + return new LdObj(indexLoad, specialMethods.IndexType); + } else if (indexKind == IndexKind.FromEnd || indexKind == IndexKind.TheEnd) { + // stloc offsetVar(binary.sub.i4(call get_Length/get_Count(ldloc container), startIndexLoad)) + // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) + // --> + // complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true))) + return new NewObj(specialMethods.IndexCtor) { Arguments = { indexLoad, new LdcI4(1) } }; + } else { + Debug.Assert(indexKind == IndexKind.FromStart || indexKind == IndexKind.TheStart); + return new Call(specialMethods.IndexImplicitConv) { Arguments = { indexLoad } }; + } + } + + /// + /// Gets whether the C# compiler will call `container[int]` when using `container[Index]`. + /// + private bool CSharpWillGenerateIndexer(IType declaringType, bool slicing) + { + bool foundInt32Overload = false; + bool foundIndexOverload = false; + bool foundRangeOverload = false; + bool foundCountProperty = false; + foreach (var prop in declaringType.GetProperties(p => p.IsIndexer || (p.Name == "Length" || p.Name == "Count"))) { + if (prop.IsIndexer && prop.Parameters.Count == 1) { + var p = prop.Parameters[0]; + if (p.Type.IsKnownType(KnownTypeCode.Int32)) { + foundInt32Overload = true; + } else if (p.Type.IsKnownType(KnownTypeCode.Index)) { + foundIndexOverload = true; + } else if (p.Type.IsKnownType(KnownTypeCode.Range)) { + foundRangeOverload = true; + } + } else if (prop.Name == "Length" || prop.Name == "Count") { + foundCountProperty = true; + } + } + if (slicing) { + return /* foundSlicingMethod && */ foundCountProperty && !foundRangeOverload; + } else { + return foundInt32Overload && foundCountProperty && !foundIndexOverload; + } + } + + /// + /// Matches the instruction: + /// stloc containerLengthVar(call get_Length/get_Count(ldloc containerVar)) + /// + static bool MatchContainerLengthStore(ILInstruction inst, out ILVariable lengthVar, ref ILVariable containerVar) + { + if (!inst.MatchStLoc(out lengthVar, out var init)) + return false; + if (!(lengthVar.IsSingleDefinition && lengthVar.StackType == StackType.I4)) + return false; + return MatchContainerLength(init, null, ref containerVar); + } + + /// + /// If lengthVar is non-null, matches 'ldloc lengthVar'. + /// + /// Otherwise, matches the instruction: + /// call get_Length/get_Count(ldloc containerVar) + /// + static bool MatchContainerLength(ILInstruction init, ILVariable lengthVar, ref ILVariable containerVar) + { + if (lengthVar != null) { + Debug.Assert(containerVar != null); + return init.MatchLdLoc(lengthVar); + } + if (!(init is CallInstruction call)) + return false; + if (call.ResultType != StackType.I4) + return false; + if (!(call.Method.IsAccessor && call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter)) + return false; + if (!(call.Method.AccessorOwner is IProperty lengthProp)) + return false; + if (lengthProp.Name == "Length") { + // OK, Length is preferred + } else if (lengthProp.Name == "Count") { + // Also works, but only if the type doesn't have "Length" + if (lengthProp.DeclaringType.GetProperties(p => p.Name == "Length").Any()) + return false; + } + if (!lengthProp.ReturnType.IsKnownType(KnownTypeCode.Int32)) + return false; + if (lengthProp.IsVirtual && call.OpCode != OpCode.CallVirt) + return false; + if (call.Arguments.Count != 1) + return false; + return MatchContainerVar(call.Arguments[0], ref containerVar); + } + + static bool MatchContainerVar(ILInstruction inst, ref ILVariable containerVar) + { + if (containerVar != null) { + return inst.MatchLdLoc(containerVar) || inst.MatchLdLoca(containerVar); + } else { + return inst.MatchLdLoc(out containerVar) || inst.MatchLdLoca(out containerVar); + } + } + + enum IndexKind + { + /// + /// indexLoad is an integer, from the start of the container + /// + FromStart, + /// + /// indexLoad is loading the address of a System.Index struct + /// + RefSystemIndex, + /// + /// indexLoad is an integer, from the end of the container + /// + FromEnd, + /// + /// Always equivalent to `0`, used for the start-index when slicing without a startpoint `a[..end]` + /// + TheStart, + /// + /// Always equivalent to `^0`, used for the end-index when slicing without an endpoint `a[start..]` + /// + TheEnd, + } + + /// + /// Matches an instruction computing an offset: + /// call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) + /// or + /// binary.sub.i4(ldloc containerLengthVar, indexLoad) + /// + /// Anything else not matching these patterns is interpreted as an `int` expression from the start of the container. + /// + static IndexKind MatchGetOffset(ILInstruction inst, out ILInstruction indexLoad, + ILVariable containerLengthVar, ref ILVariable containerVar) + { + indexLoad = inst; + if (MatchContainerLength(inst, containerLengthVar, ref containerVar)) { + indexLoad = new LdcI4(0); + return IndexKind.TheEnd; + } else if (inst is CallInstruction call) { + // call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) + if (call.Method.Name != "GetOffset") + return IndexKind.FromStart; + if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) + return IndexKind.FromStart; + if (call.Arguments.Count != 2) + return IndexKind.FromStart; + if (!MatchContainerLength(call.Arguments[1], containerLengthVar, ref containerVar)) + return IndexKind.FromStart; + indexLoad = call.Arguments[0]; + return IndexKind.RefSystemIndex; + } else if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { + if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) + return IndexKind.FromStart; + // binary.sub.i4(ldloc containerLengthVar, indexLoad) + if (!MatchContainerLength(bni.Left, containerLengthVar, ref containerVar)) + return IndexKind.FromStart; + indexLoad = bni.Right; + return IndexKind.FromEnd; + } else { + return IndexKind.FromStart; + } + } + + /// + /// Matches an instruction computing a slice length: + /// binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset)) + /// + static bool MatchSliceLength(ILInstruction inst, out IndexKind endIndexKind, out ILInstruction endIndexLoad, ILVariable containerLengthVar, ref ILVariable containerVar, ILVariable startOffsetVar) + { + endIndexKind = default; + endIndexLoad = default; + if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { + if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) + return false; + if (startOffsetVar == null) { + // When slicing without explicit start point: `a[..endIndex]` + if (!bni.Right.MatchLdcI4(0)) + return false; + } else { + if (!bni.Right.MatchLdLoc(startOffsetVar)) + return false; + } + endIndexKind = MatchGetOffset(bni.Left, out endIndexLoad, containerLengthVar, ref containerVar); + return true; + } else if (startOffsetVar == null) { + // When slicing without explicit start point: `a[..endIndex]`, the compiler doesn't always emit the "- 0". + endIndexKind = MatchGetOffset(inst, out endIndexLoad, containerLengthVar, ref containerVar); + return true; + } else { + return false; + } + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs index 850f50fc1..5f7473fb1 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs @@ -114,7 +114,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms value = ldFlda.Target; } if (value.OpCode != OpCode.LdLoca) { - // GroupStores.HandleLoad() only detects ref-locals when they are directly initialized with ldloca + // GroupStores only handles ref-locals correctly when they are supported by GetAddressLoadForRefLocalUse(), + // which only works for ldflda*(ldloca) return AddressUse.Unknown; } foreach (var load in stloc.Variable.LoadInstructions) { @@ -132,14 +133,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms // Address is passed to method. // We'll assume the method only uses the address locally, // unless we can see an address being returned from the method: - if (call is NewObj) { - if (call.Method.DeclaringType.IsByRefLike) { + IType returnType = (call is NewObj) ? call.Method.DeclaringType : call.Method.ReturnType; + if (returnType.IsByRefLike) { + // If the address is returned from the method, it check whether it's consumed immediately. + // This can still be fine, as long as we also check the consumer's other arguments for 'stloc targetVar'. + if (DetermineAddressUse(call, targetVar) != AddressUse.Immediate) return AddressUse.Unknown; - } - } else { - if (call.Method.ReturnType.IsByRefLike) { - return AddressUse.Unknown; - } } foreach (var p in call.Method.Parameters) { // catch "out Span" and similar diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs new file mode 100644 index 000000000..9d79e1b79 --- /dev/null +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs @@ -0,0 +1,137 @@ +// 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 System.Collections.Generic; +using ICSharpCode.Decompiler.Util; +using System.Reflection; +using System.Reflection.Metadata; +using System.Diagnostics; +using System.Linq; + +namespace ICSharpCode.Decompiler.TypeSystem.Implementation +{ + /// + /// Synthetic method representing a compiler-generated indexer + /// with the signature 'get_Item(System.Index)' or 'get_Item(System.Range)'. + /// Can also be a setter. + /// Used for the "Implicit Index support"/"Implicit Range support" for the C# 8 ranges feature. + /// + class SyntheticRangeIndexAccessor : IMethod + { + /// + /// The underlying method: `get_Item(int)`, `set_Item(int, T)` or `Slice(int, int)`. + /// + readonly IMethod underlyingMethod; + readonly IType indexOrRangeType; + readonly IReadOnlyList parameters; + readonly bool slicing; + + public SyntheticRangeIndexAccessor(IMethod underlyingMethod, IType indexOrRangeType, bool slicing) + { + Debug.Assert(underlyingMethod != null); + Debug.Assert(indexOrRangeType != null); + this.underlyingMethod = underlyingMethod; + this.indexOrRangeType = indexOrRangeType; + this.slicing = slicing; + var parameters = new List(); + parameters.Add(new DefaultParameter(indexOrRangeType, "")); + if (slicing) { + Debug.Assert(underlyingMethod.Parameters.Count == 2); + } else { + parameters.AddRange(underlyingMethod.Parameters.Skip(1)); + } + this.parameters = parameters; + } + + public bool IsSlicing => slicing; + + bool IMethod.ReturnTypeIsRefReadOnly => underlyingMethod.ReturnTypeIsRefReadOnly; + bool IMethod.ThisIsRefReadOnly => underlyingMethod.ThisIsRefReadOnly; + + IReadOnlyList IMethod.TypeParameters => EmptyList.Instance; + IReadOnlyList IMethod.TypeArguments => EmptyList.Instance; + + bool IMethod.IsExtensionMethod => false; + bool IMethod.IsLocalFunction => false; + bool IMethod.IsConstructor => false; + bool IMethod.IsDestructor => false; + bool IMethod.IsOperator => false; + bool IMethod.HasBody => underlyingMethod.HasBody; + bool IMethod.IsAccessor => underlyingMethod.IsAccessor; + IMember IMethod.AccessorOwner => underlyingMethod.AccessorOwner; + MethodSemanticsAttributes IMethod.AccessorKind => underlyingMethod.AccessorKind; + IMethod IMethod.ReducedFrom => underlyingMethod.ReducedFrom; + IReadOnlyList IParameterizedMember.Parameters => parameters; + IMember IMember.MemberDefinition => underlyingMethod.MemberDefinition; + IType IMember.ReturnType => underlyingMethod.ReturnType; + IEnumerable IMember.ExplicitlyImplementedInterfaceMembers => EmptyList.Instance; + bool IMember.IsExplicitInterfaceImplementation => false; + bool IMember.IsVirtual => underlyingMethod.IsVirtual; + bool IMember.IsOverride => underlyingMethod.IsOverride; + bool IMember.IsOverridable => underlyingMethod.IsOverridable; + TypeParameterSubstitution IMember.Substitution => underlyingMethod.Substitution; + EntityHandle IEntity.MetadataToken => underlyingMethod.MetadataToken; + public string Name => underlyingMethod.Name; + IType IEntity.DeclaringType => underlyingMethod.DeclaringType; + ITypeDefinition IEntity.DeclaringTypeDefinition => underlyingMethod.DeclaringTypeDefinition; + IModule IEntity.ParentModule => underlyingMethod.ParentModule; + Accessibility IEntity.Accessibility => underlyingMethod.Accessibility; + bool IEntity.IsStatic => underlyingMethod.IsStatic; + bool IEntity.IsAbstract => underlyingMethod.IsAbstract; + bool IEntity.IsSealed => underlyingMethod.IsSealed; + SymbolKind ISymbol.SymbolKind => SymbolKind.Method; + ICompilation ICompilationProvider.Compilation => underlyingMethod.Compilation; + string INamedElement.FullName => underlyingMethod.FullName; + string INamedElement.ReflectionName => underlyingMethod.ReflectionName; + string INamedElement.Namespace => underlyingMethod.Namespace; + + public override bool Equals(object obj) + { + return obj is SyntheticRangeIndexAccessor g + && this.underlyingMethod.Equals(g.underlyingMethod) + && this.indexOrRangeType.Equals(g.indexOrRangeType) + && this.slicing == g.slicing; + } + + public override int GetHashCode() + { + return underlyingMethod.GetHashCode() ^ indexOrRangeType.GetHashCode(); + } + + bool IMember.Equals(IMember obj, TypeVisitor typeNormalization) + { + return obj is SyntheticRangeIndexAccessor g + && this.underlyingMethod.Equals(g.underlyingMethod, typeNormalization) + && this.indexOrRangeType.AcceptVisitor(typeNormalization).Equals(g.indexOrRangeType.AcceptVisitor(typeNormalization)); + } + + IEnumerable IEntity.GetAttributes() => underlyingMethod.GetAttributes(); + + IEnumerable IMethod.GetReturnTypeAttributes() => underlyingMethod.GetReturnTypeAttributes(); + + IMethod IMethod.Specialize(TypeParameterSubstitution substitution) + { + return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing); + } + + IMember IMember.Specialize(TypeParameterSubstitution substitution) + { + return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing); + } + } +} diff --git a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs index 805f95b14..a6981f2b2 100644 --- a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs +++ b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs @@ -147,6 +147,10 @@ namespace ICSharpCode.Decompiler.TypeSystem IAsyncEnumerableOfT, /// System.Collections.Generic.IAsyncEnumerator{T} IAsyncEnumeratorOfT, + /// System.Index + Index, + /// System.Range + Range } /// @@ -155,7 +159,7 @@ namespace ICSharpCode.Decompiler.TypeSystem [Serializable] public sealed class KnownTypeReference : ITypeReference { - internal const int KnownTypeCodeCount = (int)KnownTypeCode.IAsyncEnumeratorOfT + 1; + internal const int KnownTypeCodeCount = (int)KnownTypeCode.Range + 1; static readonly KnownTypeReference[] knownTypeReferences = new KnownTypeReference[KnownTypeCodeCount] { null, // None @@ -218,6 +222,8 @@ namespace ICSharpCode.Decompiler.TypeSystem new KnownTypeReference(KnownTypeCode.Unsafe, TypeKind.Class, "System.Runtime.CompilerServices", "Unsafe", 0), new KnownTypeReference(KnownTypeCode.IAsyncEnumerableOfT, TypeKind.Interface, "System.Collections.Generic", "IAsyncEnumerable", 1), new KnownTypeReference(KnownTypeCode.IAsyncEnumeratorOfT, TypeKind.Interface, "System.Collections.Generic", "IAsyncEnumerator", 1), + new KnownTypeReference(KnownTypeCode.Index, TypeKind.Struct, "System", "Index", 0), + new KnownTypeReference(KnownTypeCode.Range, TypeKind.Struct, "System", "Range", 0), }; /// diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 00e102b8d..10b87ca5c 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -938,6 +938,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Ranges. + /// + public static string DecompilerSettings_Ranges { + get { + return ResourceManager.GetString("DecompilerSettings.Ranges", resourceCulture); + } + } + /// /// Looks up a localized string similar to Read-only methods. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 45e1d4246..d89524790 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -852,6 +852,9 @@ Do you want to continue? Do you want to continue? + + Ranges + Add preconfigured list...