From 4846feb640bbac2e57766950c1a23ab73d8bc709 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 04:03:06 +0200 Subject: [PATCH 01/10] Add support for C# 8 range syntax. This initial commit only handles the trivial case where an Index or Range object is constructed. The TODO portions of the test case show there are plenty of cases where where the C# compiler emits more complex code patterns that will require ILAst transforms. --- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../PrettyTestRunner.cs | 8 +- .../TestCases/Pretty/IndexRangeTest.cs | 199 ++++++++++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 42 ++++ .../OutputVisitor/CSharpOutputVisitor.cs | 3 + .../OutputVisitor/InsertParenthesesVisitor.cs | 142 +++++++------ .../Expressions/BinaryOperatorExpression.cs | 10 +- .../Expressions/UnaryOperatorExpression.cs | 8 + ICSharpCode.Decompiler/DecompilerSettings.cs | 20 +- .../TypeSystem/KnownTypeReference.cs | 8 +- ILSpy/Properties/Resources.Designer.cs | 9 + ILSpy/Properties/Resources.resx | 3 + 12 files changed, 386 insertions(+), 67 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 053e73191..abf931437 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..bb47378ba --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -0,0 +1,199 @@ +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 string[] 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 void UseIndex() + { +#if TODO + Console.WriteLine(GetArray()[GetIndex()]); + Console.WriteLine(GetList()[GetIndex()]); + Console.WriteLine(GetSpan()[GetIndex()]); + Console.WriteLine(GetString()[GetIndex()]); + Console.WriteLine(new CustomList()[GetIndex()]); +#endif + Console.WriteLine(new CustomList2()[GetIndex()]); + } + public static void UseRange() + { +#if TODO + 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()]); +#endif + Console.WriteLine(new CustomList2()[GetRange()]); + } + public static void UseNewRangeFromIndex() + { +#if TODO + Console.WriteLine(GetArray()[GetIndex()..GetIndex()]); + //Console.WriteLine(GetList()[GetIndex()..GetIndex()]); // fails to compile + Console.WriteLine(GetSpan()[GetIndex()..GetIndex()].ToString()); + Console.WriteLine(GetString()[GetIndex()..GetIndex()]); + Console.WriteLine(new CustomList()[GetIndex()..GetIndex()]); +#endif + Console.WriteLine(new CustomList2()[GetIndex()..GetIndex()]); + } + public static void UseNewRangeFromIntegers_BothFromStart() + { +#if TODO + 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)]); +#endif + Console.WriteLine(new CustomList2()[GetInt(1)..GetInt(2)]); + } + public static void UseNewRangeFromIntegers_BothFromEnd() + { +#if TODO + 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)]); +#endif + Console.WriteLine(new CustomList2()[^GetInt(1)..^GetInt(2)]); + } + public static void UseNewRangeFromIntegers_FromStartAndEnd() + { +#if TODO + 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)]); +#endif + Console.WriteLine(new CustomList2()[GetInt(1)..^GetInt(2)]); + } + public static void UseNewRangeFromIntegers_FromEndAndStart() + { +#if TODO + 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)]); +#endif + Console.WriteLine(new CustomList2()[^GetInt(1)..GetInt(2)]); + } + + public static void UseNewRangeFromIntegers_OnlyEndPoint() + { +#if TODO + 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)]); +#endif + Console.WriteLine(new CustomList2()[..GetInt(2)]); + } + + public static void UseNewRangeFromIntegers_OnlyStartPoint() + { +#if TODO + 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)..]); +#endif + Console.WriteLine(new CustomList2()[GetInt(1)..]); + } + + public static void UseWholeRange() + { +#if TODO + Console.WriteLine(GetArray()[..]); + //Console.WriteLine(GetList()[..]); // fails to compile + Console.WriteLine(GetSpan()[..].ToString()); + Console.WriteLine(GetString()[..]); + Console.WriteLine(new CustomList()[..]); +#endif + 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/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 267a757fe..ba8aaf82e 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, argumentList)) { + return result; + } + } + if (callOpCode == OpCode.NewObj) { return HandleConstructorCall(expectedTargetDetails, target.ResolveResult, method, argumentList); } @@ -1494,5 +1500,41 @@ 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, 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 ResolveResult(method.DeclaringType)); + 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 ResolveResult(method.DeclaringType)); + 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 ResolveResult(method.DeclaringType)); + 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 ResolveResult(method.DeclaringType)); + 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 ResolveResult(method.DeclaringType)); + return true; + } + return false; + } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index faa67d21c..6e1e9650f 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/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/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 267bf70dd..2da49f2d0 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -920,6 +920,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 de2f7a957..426065be5 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -852,4 +852,7 @@ Do you want to continue? Do you want to continue? + + Ranges + \ No newline at end of file From dc6e094a307d3e4a59a48fdb5a3a12c91cbf9e38 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 08:26:18 +0200 Subject: [PATCH 02/10] 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; + } + } +} From 748c54a1c159f8d15064e059d53e7237317dc7f0 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 08:29:48 +0200 Subject: [PATCH 03/10] Support array slicing. --- .../TestCases/Pretty/IndexRangeTest.cs | 16 ++++++++-------- .../ReplaceMethodCallsWithOperators.cs | 7 +++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index 8ec33c1b7..96c86a951 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -103,8 +103,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseRange() { -#if TODO Console.WriteLine(GetArray()[GetRange()]); +#if TODO //Console.WriteLine(GetList()[GetRange()]); // fails to compile Console.WriteLine(GetSpan()[GetRange()].ToString()); Console.WriteLine(GetString()[GetRange()]); @@ -114,8 +114,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseNewRangeFromIndex() { -#if TODO Console.WriteLine(GetArray()[GetIndex()..GetIndex()]); +#if TODO //Console.WriteLine(GetList()[GetIndex()..GetIndex()]); // fails to compile Console.WriteLine(GetSpan()[GetIndex()..GetIndex()].ToString()); Console.WriteLine(GetString()[GetIndex()..GetIndex()]); @@ -125,8 +125,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseNewRangeFromIntegers_BothFromStart() { -#if TODO Console.WriteLine(GetArray()[GetInt(1)..GetInt(2)]); +#if TODO //Console.WriteLine(GetList()[GetInt()..GetInt()]); // fails to compile Console.WriteLine(GetSpan()[GetInt(1)..GetInt(2)].ToString()); Console.WriteLine(GetString()[GetInt(1)..GetInt(2)]); @@ -136,8 +136,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseNewRangeFromIntegers_BothFromEnd() { -#if TODO Console.WriteLine(GetArray()[^GetInt(1)..^GetInt(2)]); +#if TODO //Console.WriteLine(GetList()[^GetInt()..^GetInt()]); // fails to compile Console.WriteLine(GetSpan()[^GetInt(1)..^GetInt(2)].ToString()); Console.WriteLine(GetString()[^GetInt(1)..^GetInt(2)]); @@ -158,8 +158,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseNewRangeFromIntegers_FromEndAndStart() { -#if TODO Console.WriteLine(GetArray()[^GetInt(1)..GetInt(2)]); +#if TODO //Console.WriteLine(GetList()[^GetInt()..GetInt()]); // fails to compile Console.WriteLine(GetSpan()[^GetInt(1)..GetInt(2)].ToString()); Console.WriteLine(GetString()[^GetInt(1)..GetInt(2)]); @@ -170,8 +170,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseNewRangeFromIntegers_OnlyEndPoint() { -#if TODO Console.WriteLine(GetArray()[..GetInt(2)]); +#if TODO //Console.WriteLine(GetList()[..GetInt()]); // fails to compile Console.WriteLine(GetSpan()[..GetInt(2)].ToString()); Console.WriteLine(GetString()[..GetInt(2)]); @@ -182,8 +182,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseNewRangeFromIntegers_OnlyStartPoint() { -#if TODO Console.WriteLine(GetArray()[GetInt(1)..]); +#if TODO //Console.WriteLine(GetList()[GetInt()..]); // fails to compile Console.WriteLine(GetSpan()[GetInt(1)..].ToString()); Console.WriteLine(GetString()[GetInt(1)..]); @@ -194,8 +194,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseWholeRange() { -#if TODO Console.WriteLine(GetArray()[..]); +#if TODO //Console.WriteLine(GetList()[..]); // fails to compile Console.WriteLine(GetSpan()[..].ToString()); Console.WriteLine(GetString()[..]); 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); From 12226c5f90330b336f8b79d3f60e0dd9c3f37f1a Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 10:58:01 +0200 Subject: [PATCH 04/10] Add support for indexing a container with a System.Index instance. --- .../TestCases/Pretty/IndexRangeTest.cs | 8 +- .../CSharp/CSharpDecompiler.cs | 3 +- .../ICSharpCode.Decompiler.csproj | 1 + .../IL/Transforms/ILInlining.cs | 4 + .../IL/Transforms/IndexRangeTransform.cs | 149 +++++++++++++++++- .../Implementation/SyntheticRangeIndexer.cs | 128 +++++++++++++++ 6 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index 96c86a951..27df19e3c 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -61,12 +61,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseIndex() { Console.WriteLine(GetArray()[GetIndex()]); -#if TODO Console.WriteLine(GetList()[GetIndex()]); Console.WriteLine(GetSpan()[GetIndex()]); Console.WriteLine(GetString()[GetIndex()]); Console.WriteLine(new CustomList()[GetIndex()]); -#endif Console.WriteLine(new CustomList2()[GetIndex()]); } @@ -85,10 +83,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseIndexForWrite() { GetArray()[GetIndex()] = GetInt(); -#if TODO GetList()[GetIndex()] = GetInt(); GetSpan()[GetIndex()] = GetInt(); -#endif } private static void UseRef(ref int i) @@ -99,6 +95,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { UseRef(ref GetArray()[GetIndex()]); UseRef(ref GetArray()[^GetInt()]); + UseRef(ref GetSpan()[GetIndex()]); +#if TODO + UseRef(ref GetSpan()[^GetInt()]); +#endif } public static void UseRange() diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 53b10fa6f..6e7625c57 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/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 75cb72258..d3b5ed919 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -385,6 +385,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 448ecbb6c..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,9 @@ 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) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index ed3140bf5..98b81d3f2 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -16,6 +16,10 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; +using System.Linq; +using ICSharpCode.Decompiler.CSharp.Resolver; +using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; @@ -24,7 +28,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// Transform for the C# 8 System.Index / System.Range feature /// - class IndexRangeTransform + class IndexRangeTransform : IStatementTransform { /// /// Called by expression transforms. @@ -85,5 +89,148 @@ namespace ICSharpCode.Decompiler.IL.Transforms } return null; } + + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) + { + int startPos = pos; + if (!MatchContainerLengthStore(block.Instructions[pos], out ILVariable containerLengthVar, out ILVariable containerVar)) + return; + pos++; + if (!MatchGetOffsetFromIndex(block.Instructions[pos], out ILVariable startOffsetVar, out ILInstruction startIndexLdloca, containerLengthVar)) + return; + pos++; + if (startOffsetVar.LoadCount == 1) { + // complex_expr(call get_Item(ldloc container, ldloc offset)) + + // 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 (!(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 { + return; + } + if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) + return; + if (!call.Arguments[0].MatchLdLoc(containerVar) && !call.Arguments[0].MatchLdLoca(containerVar)) + return; + if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) + return; + var indexType = context.TypeSystem.FindType(KnownTypeCode.Index); + if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, indexType, context)) + return; + + // stloc length(call get_Length/ get_Count(ldloc container)) + // stloc offset(call GetOffset(..., ldloc length)) + // complex_expr(call get_Item(ldloc container, ldloc offset)) + // --> + // complex_expr(call get_Item(ldloc container, ...)) + context.Step($"{call.Method.Name} indexed with System.Index", call); + var newMethod = new SyntheticRangeIndexAccessor(call.Method, indexType); + var newCall = CallInstruction.Create(call.OpCode, newMethod); + newCall.ConstrainedTo = call.ConstrainedTo; + newCall.ILStackWasEmpty = call.ILStackWasEmpty; + newCall.Arguments.Add(call.Arguments[0]); + newCall.Arguments.Add(new LdObj(startIndexLdloca, indexType)); + 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); + } + } + + /// + /// Gets whether the C# compiler will call `container[int]` when using `container[Index]`. + /// + private bool CSharpWillGenerateIndexer(IType declaringType, IType indexType, ILTransformContext context) + { + bool foundInt32Overload = false; + bool foundIndexOverload = 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 (prop.Name == "Length" || prop.Name=="Count") { + foundCountProperty = true; + } + } + return foundInt32Overload && foundCountProperty && !foundIndexOverload; + } + + /// + /// Matches the instruction: + /// stloc containerLengthVar(call get_Length/get_Count(ldloc containerVar)) + /// + static bool MatchContainerLengthStore(ILInstruction inst, out ILVariable lengthVar, out ILVariable containerVar) + { + containerVar = null; + if (!inst.MatchStLoc(out lengthVar, out var init)) + return false; + if (!(lengthVar.IsSingleDefinition && lengthVar.StackType == StackType.I4)) + return false; + if (!(init is CallInstruction call)) + 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 call.Arguments[0].MatchLdLoc(out containerVar) || call.Arguments[0].MatchLdLoca(out containerVar); + } + + /// + /// Matches the instruction: + /// stloc offsetVar(call System.Index.GetOffset(indexLdloca, ldloc containerLengthVar)) + /// + static bool MatchGetOffsetFromIndex(ILInstruction inst, out ILVariable offsetVar, out ILInstruction indexLdloca, ILVariable containerLengthVar) + { + indexLdloca = null; + if (!inst.MatchStLoc(out offsetVar, out var offsetValue)) + return false; + if (!(offsetVar.IsSingleDefinition && offsetVar.StackType == StackType.I4)) + return false; + if (!(offsetValue is CallInstruction call)) + return false; + if (call.Method.Name != "GetOffset") + return false; + if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) + return false; + if (call.Arguments.Count != 2) + return false; + indexLdloca = call.Arguments[0]; + return call.Arguments[1].MatchLdLoc(containerLengthVar); + } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs new file mode 100644 index 000000000..5ea2e8f10 --- /dev/null +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs @@ -0,0 +1,128 @@ +// 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; + + public SyntheticRangeIndexAccessor(IMethod underlyingMethod, IType indexOrRangeType) + { + Debug.Assert(underlyingMethod != null); + Debug.Assert(indexOrRangeType != null); + this.underlyingMethod = underlyingMethod; + this.indexOrRangeType = indexOrRangeType; + var parameters = new List(); + parameters.Add(new DefaultParameter(indexOrRangeType, "")); + parameters.AddRange(underlyingMethod.Parameters.Skip(1)); + this.parameters = parameters; + } + + 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); + } + + 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); + } + + IMember IMember.Specialize(TypeParameterSubstitution substitution) + { + return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType); + } + } +} From 060830dd6460afbd21a4f6731ee2995bc8f20fc6 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 11:08:31 +0200 Subject: [PATCH 05/10] Variable splitting: support cases where a ref is passed through a ref-returning method, and then used. From `IndexRangeTest.UseIndexForRef`: ``` stloc V_4(call GetSpan()) stloc S_19(ldloca V_4) stloc V_2(call get_Length(ldloc S_19)) stloc V_3(call GetOffset(addressof System.Index(call GetIndex(ldc.i4 0)), ldloc V_2)) call UseRef(call get_Item(ldloc S_19, ldloc V_3)) stloc V_4(call GetSpan()) stloc S_30(ldloca V_4) stloc V_2(binary.sub.i4(call get_Length(ldloc S_30), call GetInt(ldc.i4 0))) call UseRef(call get_Item(ldloc S_30, ldloc V_2)) ``` Due to `Span.get_Item` being a ref-return, it's possible that `ref V_4` is returned and passed into the `UseRef` method (this can't actually happen given Span's implementation, but it's a possible implementation of the get_Item type signature). But we still need to split `V_4` -- it's a compiler-generated variable and needs to be inlined. I think we can do this relatively simply by continuing to go up the ancestor instructions when we hit a ref-returning call. The recursive `DetermineAddressUse` call will check that there are no stores to `V_4` between the `get_Item` call and the point where the returned reference is used. Thus we still ensure that we don't split a variable while there is a live reference to it. --- .../IL/Transforms/SplitVariables.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 From dc38355e1204c0da6cf9d3a9bae8c136814361e8 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 11:55:32 +0200 Subject: [PATCH 06/10] Support `list[^idx]`. Here the C# compiler does not actually create a `System.Index` instance, but instead compiles to `list[list.Count - idx]`. --- .../TestCases/Pretty/IndexRangeTest.cs | 4 - .../IL/Transforms/IndexRangeTransform.cs | 133 +++++++++++++----- 2 files changed, 101 insertions(+), 36 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index 27df19e3c..9233680a3 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -71,12 +71,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty 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()]); } @@ -96,9 +94,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty UseRef(ref GetArray()[GetIndex()]); UseRef(ref GetArray()[^GetInt()]); UseRef(ref GetSpan()[GetIndex()]); -#if TODO UseRef(ref GetSpan()[^GetInt()]); -#endif } public static void UseRange() diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index 98b81d3f2..46e69000b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.Semantics; @@ -93,15 +94,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) { int startPos = pos; - if (!MatchContainerLengthStore(block.Instructions[pos], out ILVariable containerLengthVar, out ILVariable containerVar)) - return; + // 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, out ILVariable containerVar)) { + pos++; + } else { + // Reset if MatchContainerLengthStore only had a partial match. MatchGetOffset() will then set `containerVar`. + containerLengthVar = null; + containerVar = null; + } + var startIndexKind = MatchGetOffset(block.Instructions[pos], out ILVariable startOffsetVar, out ILInstruction startIndexLoad, containerLengthVar, ref containerVar); pos++; - if (!MatchGetOffsetFromIndex(block.Instructions[pos], out ILVariable startOffsetVar, out ILInstruction startIndexLdloca, containerLengthVar)) + if (startIndexKind == IndexKind.None) return; - pos++; if (startOffsetVar.LoadCount == 1) { - // complex_expr(call get_Item(ldloc container, ldloc offset)) - + // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) + // 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])) @@ -129,21 +136,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) return; var indexType = context.TypeSystem.FindType(KnownTypeCode.Index); - if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, indexType, context)) + var indexCtor = FindIndexConstructor(context.TypeSystem); + if (indexCtor == null) + return; + if (!CSharpWillGenerateIndexer(call.Method.DeclaringType)) return; - // stloc length(call get_Length/ get_Count(ldloc container)) - // stloc offset(call GetOffset(..., ldloc length)) - // complex_expr(call get_Item(ldloc container, ldloc offset)) - // --> - // complex_expr(call get_Item(ldloc container, ...)) - context.Step($"{call.Method.Name} indexed with System.Index", call); + context.Step($"{call.Method.Name} indexed with {startIndexKind}", call); var newMethod = new SyntheticRangeIndexAccessor(call.Method, indexType); var newCall = CallInstruction.Create(call.OpCode, newMethod); newCall.ConstrainedTo = call.ConstrainedTo; newCall.ILStackWasEmpty = call.ILStackWasEmpty; newCall.Arguments.Add(call.Arguments[0]); - newCall.Arguments.Add(new LdObj(startIndexLdloca, indexType)); + if (startIndexKind == IndexKind.RefSystemIndex) { + // stloc length(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)) + newCall.Arguments.Add(new LdObj(startIndexLoad, indexType)); + } else { + // stloc offsetVar(binary.sub.i4(ldloc containerLengthVar, startIndexLoad)) + // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) + // --> + // complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true))) + Debug.Assert(startIndexKind == IndexKind.FromEnd); + newCall.Arguments.Add(new NewObj(indexCtor) { Arguments = { startIndexLoad, new LdcI4(1) } }); + } newCall.Arguments.AddRange(call.Arguments.Skip(2)); newCall.AddILRange(call); for (int i = startPos; i < pos; i++) { @@ -157,7 +176,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// Gets whether the C# compiler will call `container[int]` when using `container[Index]`. /// - private bool CSharpWillGenerateIndexer(IType declaringType, IType indexType, ILTransformContext context) + private bool CSharpWillGenerateIndexer(IType declaringType) { bool foundInt32Overload = false; bool foundIndexOverload = false; @@ -188,8 +207,25 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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)) @@ -207,30 +243,63 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (call.Arguments.Count != 1) return false; - return call.Arguments[0].MatchLdLoc(out containerVar) || call.Arguments[0].MatchLdLoca(out containerVar); + if (containerVar != null) { + return call.Arguments[0].MatchLdLoc(containerVar) || call.Arguments[0].MatchLdLoca(containerVar); + } else { + return call.Arguments[0].MatchLdLoc(out containerVar) || call.Arguments[0].MatchLdLoca(out containerVar); + } + } + + enum IndexKind + { + None, + /// + /// indexLoad is loading the address of a System.Index struct + /// + RefSystemIndex, + /// + /// indexLoad is an integer, from the end of the container + /// + FromEnd } /// - /// Matches the instruction: - /// stloc offsetVar(call System.Index.GetOffset(indexLdloca, ldloc containerLengthVar)) + /// Matches an instruction computing an offset: + /// stloc offsetVar(call System.Index.GetOffset(indexLoad, ldloc containerLengthVar)) + /// or + /// stloc offsetVar(binary.sub.i4(ldloc containerLengthVar, indexLoad)) /// - static bool MatchGetOffsetFromIndex(ILInstruction inst, out ILVariable offsetVar, out ILInstruction indexLdloca, ILVariable containerLengthVar) + static IndexKind MatchGetOffset(ILInstruction inst, out ILVariable offsetVar, out ILInstruction indexLoad, + ILVariable containerLengthVar, ref ILVariable containerVar) { - indexLdloca = null; + indexLoad = null; if (!inst.MatchStLoc(out offsetVar, out var offsetValue)) - return false; + return IndexKind.None; if (!(offsetVar.IsSingleDefinition && offsetVar.StackType == StackType.I4)) - return false; - if (!(offsetValue is CallInstruction call)) - return false; - if (call.Method.Name != "GetOffset") - return false; - if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) - return false; - if (call.Arguments.Count != 2) - return false; - indexLdloca = call.Arguments[0]; - return call.Arguments[1].MatchLdLoc(containerLengthVar); + return IndexKind.None; + if (offsetValue is CallInstruction call) { + // call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) + if (call.Method.Name != "GetOffset") + return IndexKind.None; + if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) + return IndexKind.None; + if (call.Arguments.Count != 2) + return IndexKind.None; + if (!MatchContainerLength(call.Arguments[1], containerLengthVar, ref containerVar)) + return IndexKind.None; + indexLoad = call.Arguments[0]; + return IndexKind.RefSystemIndex; + } else if (offsetValue is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { + if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) + return IndexKind.None; + // binary.sub.i4(ldloc containerLengthVar, indexLoad) + if (!MatchContainerLength(bni.Left, containerLengthVar, ref containerVar)) + return IndexKind.None; + indexLoad = bni.Right; + return IndexKind.FromEnd; + } else { + return IndexKind.None; + } } } } From dd54dbc144d4e1e423b028851a3c24c5fef12fb7 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 12:10:27 +0200 Subject: [PATCH 07/10] Disable IndexRangeTransform if the "C# 8 ranges" setting is disabled. --- ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index 46e69000b..3a91f0d1d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -16,11 +16,8 @@ // 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.CSharp.Resolver; -using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; @@ -93,6 +90,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) { + if (!context.Settings.Ranges) + return; int startPos = pos; // 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, out ILVariable containerVar)) { From aed358b5a09863ea9e263a50277278ebc057b0f8 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 12:38:17 +0200 Subject: [PATCH 08/10] Use more precise ResolveResult, so that Range/Index operators are hyperlinked. --- ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index ba8aaf82e..5619f736b 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1510,19 +1510,19 @@ namespace ICSharpCode.Decompiler.CSharp 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 ResolveResult(method.DeclaringType)); + .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 ResolveResult(method.DeclaringType)); + .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 ResolveResult(method.DeclaringType)); + .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 ResolveResult(method.DeclaringType)); + .WithRR(new MemberResolveResult(null, method)); return true; } } else if (callOpCode == OpCode.NewObj && method.DeclaringType.IsKnownType(KnownTypeCode.Index)) { @@ -1531,7 +1531,7 @@ namespace ICSharpCode.Decompiler.CSharp if (!(argumentList.Arguments[1].Expression is PrimitiveExpression pe && pe.Value is true)) return false; result = new UnaryOperatorExpression(UnaryOperatorType.IndexFromEnd, argumentList.Arguments[0]) - .WithRR(new ResolveResult(method.DeclaringType)); + .WithRR(new MemberResolveResult(null, method)); return true; } return false; From 0dd75d68526f7f19845d81198c571d9ab9ed6872 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 14:34:16 +0200 Subject: [PATCH 09/10] Add support for slicing using C# 8 ranges. --- .../TestCases/Pretty/IndexRangeTest.cs | 42 ++- ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 10 +- .../IL/Transforms/IndexRangeTransform.cs | 341 +++++++++++++++--- .../Implementation/SyntheticRangeIndexer.cs | 19 +- 4 files changed, 328 insertions(+), 84 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index 9233680a3..442e9fa72 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -1,4 +1,22 @@ -using System; +// 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 @@ -110,57 +128,47 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } public static void UseNewRangeFromIndex() { - Console.WriteLine(GetArray()[GetIndex()..GetIndex()]); -#if TODO - //Console.WriteLine(GetList()[GetIndex()..GetIndex()]); // fails to compile - Console.WriteLine(GetSpan()[GetIndex()..GetIndex()].ToString()); - Console.WriteLine(GetString()[GetIndex()..GetIndex()]); - Console.WriteLine(new CustomList()[GetIndex()..GetIndex()]); -#endif - Console.WriteLine(new CustomList2()[GetIndex()..GetIndex()]); + 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)]); -#if TODO //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)]); -#endif Console.WriteLine(new CustomList2()[GetInt(1)..GetInt(2)]); } public static void UseNewRangeFromIntegers_BothFromEnd() { Console.WriteLine(GetArray()[^GetInt(1)..^GetInt(2)]); -#if TODO //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)]); -#endif Console.WriteLine(new CustomList2()[^GetInt(1)..^GetInt(2)]); } public static void UseNewRangeFromIntegers_FromStartAndEnd() { -#if TODO 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)]); -#endif Console.WriteLine(new CustomList2()[GetInt(1)..^GetInt(2)]); } public static void UseNewRangeFromIntegers_FromEndAndStart() { Console.WriteLine(GetArray()[^GetInt(1)..GetInt(2)]); -#if TODO //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)]); -#endif Console.WriteLine(new CustomList2()[^GetInt(1)..GetInt(2)]); } diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 5619f736b..68a3daf8e 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -253,7 +253,7 @@ namespace ICSharpCode.Decompiler.CSharp } if (settings.Ranges) { - if (HandleRangeConstruction(out var result, callOpCode, method, argumentList)) { + if (HandleRangeConstruction(out var result, callOpCode, method, target, argumentList)) { return result; } } @@ -1501,7 +1501,7 @@ namespace ICSharpCode.Decompiler.CSharp .WithILInstruction(call).WithILInstruction(block); } - private bool HandleRangeConstruction(out ExpressionWithResolveResult result, OpCode callOpCode, IMethod method, ArgumentList argumentList) + private bool HandleRangeConstruction(out ExpressionWithResolveResult result, OpCode callOpCode, IMethod method, TranslatedExpression target, ArgumentList argumentList) { result = default; if (argumentList.ArgumentNames != null) { @@ -1533,6 +1533,12 @@ namespace ICSharpCode.Decompiler.CSharp 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; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index 3a91f0d1d..72bf4cd6a 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -16,6 +16,7 @@ // 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; @@ -61,31 +62,52 @@ namespace ICSharpCode.Decompiler.IL.Transforms // -> 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) + var indexMethods = new IndexMethods(context.TypeSystem); + if (!indexMethods.AllValid) 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) } }; + ldelema.Indices[0] = MakeIndex(IndexKind.FromEnd, bni.Right, indexMethods); return true; } return false; } - static IMethod FindIndexConstructor(ICompilation compilation) + class IndexMethods { - 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; + public readonly IMethod IndexCtor; + public readonly IMethod IndexImplicitConv; + public readonly IMethod RangeCtor; + public IType IndexType => IndexCtor?.DeclaringType; + public IType RangeType => RangeCtor?.DeclaringType; + public bool AllValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null; + + 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[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; + } } } - return null; } void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) @@ -93,21 +115,50 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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, out ILVariable containerVar)) { + 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; } - var startIndexKind = MatchGetOffset(block.Instructions[pos], out ILVariable startOffsetVar, out ILInstruction startIndexLoad, containerLengthVar, ref containerVar); - pos++; - if (startIndexKind == IndexKind.None) + 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 (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])) @@ -130,39 +181,117 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) return; - if (!call.Arguments[0].MatchLdLoc(containerVar) && !call.Arguments[0].MatchLdLoca(containerVar)) + if (!MatchContainerVar(call.Arguments[0], ref containerVar)) return; if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) return; - var indexType = context.TypeSystem.FindType(KnownTypeCode.Index); - var indexCtor = FindIndexConstructor(context.TypeSystem); - if (indexCtor == null) + var specialMethods = new IndexMethods(context.TypeSystem); + if (!specialMethods.AllValid) return; - if (!CSharpWillGenerateIndexer(call.Method.DeclaringType)) + if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false)) return; context.Step($"{call.Method.Name} indexed with {startIndexKind}", call); - var newMethod = new SyntheticRangeIndexAccessor(call.Method, indexType); + 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]); - if (startIndexKind == IndexKind.RefSystemIndex) { - // stloc length(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)) - newCall.Arguments.Add(new LdObj(startIndexLoad, indexType)); + 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() + { + // 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 var sliceLengthVar, out var 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 (call.Method.Name == "Slice") { + // OK, custom class slicing + } else if (call.Method.Name == "Substring" && call.Method.DeclaringType.IsKnownType(KnownTypeCode.String)) { + // OK, string slicing } else { - // stloc offsetVar(binary.sub.i4(ldloc containerLengthVar, startIndexLoad)) - // complex_expr(call get_Item(ldloc container, ldloc startOffsetVar)) - // --> - // complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true))) - Debug.Assert(startIndexKind == IndexKind.FromEnd); - newCall.Arguments.Add(new NewObj(indexCtor) { Arguments = { startIndexLoad, new LdcI4(1) } }); + return; + } + if (call.Method.IsExtensionMethod) + return; + if (call.Method.Parameters.Count != 2) + return; + if (!call.Method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32))) + return; + if (call.Arguments.Count != 3) + return; + if (!MatchContainerVar(call.Arguments[0], ref containerVar)) + return; + 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.AllValid) + 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 { + 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.Arguments.AddRange(call.Arguments.Skip(2)); newCall.AddILRange(call); for (int i = startPos; i < pos; i++) { newCall.AddILRange(block.Instructions[i]); @@ -172,13 +301,74 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + /// + /// 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) + expectedUses += 1; + if (endIndexKind != IndexKind.FromStart) + 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) { + // 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); + 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) + 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) { @@ -187,21 +377,26 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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") { + } else if (prop.Name == "Length" || prop.Name == "Count") { foundCountProperty = true; } } - return foundInt32Overload && foundCountProperty && !foundIndexOverload; + 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, out ILVariable containerVar) + static bool MatchContainerLengthStore(ILInstruction inst, out ILVariable lengthVar, ref ILVariable containerVar) { - containerVar = null; if (!inst.MatchStLoc(out lengthVar, out var init)) return false; if (!(lengthVar.IsSingleDefinition && lengthVar.StackType == StackType.I4)) @@ -242,16 +437,24 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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 call.Arguments[0].MatchLdLoc(containerVar) || call.Arguments[0].MatchLdLoca(containerVar); + return inst.MatchLdLoc(containerVar) || inst.MatchLdLoca(containerVar); } else { - return call.Arguments[0].MatchLdLoc(out containerVar) || call.Arguments[0].MatchLdLoca(out containerVar); + return inst.MatchLdLoc(out containerVar) || inst.MatchLdLoca(out containerVar); } } enum IndexKind { - None, + /// + /// indexLoad is an integer, from the start of the container + /// + FromStart, /// /// indexLoad is loading the address of a System.Index struct /// @@ -264,40 +467,58 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// Matches an instruction computing an offset: - /// stloc offsetVar(call System.Index.GetOffset(indexLoad, ldloc containerLengthVar)) + /// call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) /// or - /// stloc offsetVar(binary.sub.i4(ldloc containerLengthVar, indexLoad)) + /// 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 ILVariable offsetVar, out ILInstruction indexLoad, + static IndexKind MatchGetOffset(ILInstruction inst, out ILInstruction indexLoad, ILVariable containerLengthVar, ref ILVariable containerVar) { - indexLoad = null; - if (!inst.MatchStLoc(out offsetVar, out var offsetValue)) - return IndexKind.None; - if (!(offsetVar.IsSingleDefinition && offsetVar.StackType == StackType.I4)) - return IndexKind.None; - if (offsetValue is CallInstruction call) { + indexLoad = inst; + if (inst is CallInstruction call) { // call System.Index.GetOffset(indexLoad, ldloc containerLengthVar) if (call.Method.Name != "GetOffset") - return IndexKind.None; + return IndexKind.FromStart; if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) - return IndexKind.None; + return IndexKind.FromStart; if (call.Arguments.Count != 2) - return IndexKind.None; + return IndexKind.FromStart; if (!MatchContainerLength(call.Arguments[1], containerLengthVar, ref containerVar)) - return IndexKind.None; + return IndexKind.FromStart; indexLoad = call.Arguments[0]; return IndexKind.RefSystemIndex; - } else if (offsetValue is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { + } else if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) - return IndexKind.None; + return IndexKind.FromStart; // binary.sub.i4(ldloc containerLengthVar, indexLoad) if (!MatchContainerLength(bni.Left, containerLengthVar, ref containerVar)) - return IndexKind.None; + return IndexKind.FromStart; indexLoad = bni.Right; return IndexKind.FromEnd; } else { - return IndexKind.None; + 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 (!bni.Right.MatchLdLoc(startOffsetVar)) + return false; + endIndexKind = MatchGetOffset(bni.Left, out endIndexLoad, containerLengthVar, ref containerVar); + return true; + } else { + return false; } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs index 5ea2e8f10..9d79e1b79 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs @@ -39,19 +39,27 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation readonly IMethod underlyingMethod; readonly IType indexOrRangeType; readonly IReadOnlyList parameters; + readonly bool slicing; - public SyntheticRangeIndexAccessor(IMethod underlyingMethod, IType indexOrRangeType) + 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, "")); - parameters.AddRange(underlyingMethod.Parameters.Skip(1)); + 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; @@ -96,7 +104,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation { return obj is SyntheticRangeIndexAccessor g && this.underlyingMethod.Equals(g.underlyingMethod) - && this.indexOrRangeType.Equals(g.indexOrRangeType); + && this.indexOrRangeType.Equals(g.indexOrRangeType) + && this.slicing == g.slicing; } public override int GetHashCode() @@ -117,12 +126,12 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IMethod IMethod.Specialize(TypeParameterSubstitution substitution) { - return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType); + return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing); } IMember IMember.Specialize(TypeParameterSubstitution substitution) { - return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType); + return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing); } } } From 1926756cfa3f56ece61581fb7fa53b30fe46f33d Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 19 Apr 2020 19:51:12 +0200 Subject: [PATCH 10/10] Handle the special cases where the range does not have a start or endpoint. --- .../TestCases/Pretty/IndexRangeTest.cs | 62 ++++++- .../IL/Transforms/IndexRangeTransform.cs | 167 +++++++++++++----- 2 files changed, 173 insertions(+), 56 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs index 442e9fa72..c4c7f6a6c 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs @@ -75,6 +75,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { 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() { @@ -118,12 +138,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseRange() { Console.WriteLine(GetArray()[GetRange()]); -#if TODO //Console.WriteLine(GetList()[GetRange()]); // fails to compile Console.WriteLine(GetSpan()[GetRange()].ToString()); Console.WriteLine(GetString()[GetRange()]); Console.WriteLine(new CustomList()[GetRange()]); -#endif Console.WriteLine(new CustomList2()[GetRange()]); } public static void UseNewRangeFromIndex() @@ -175,36 +193,64 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static void UseNewRangeFromIntegers_OnlyEndPoint() { Console.WriteLine(GetArray()[..GetInt(2)]); -#if TODO //Console.WriteLine(GetList()[..GetInt()]); // fails to compile Console.WriteLine(GetSpan()[..GetInt(2)].ToString()); Console.WriteLine(GetString()[..GetInt(2)]); Console.WriteLine(new CustomList()[..GetInt(2)]); -#endif 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)..]); -#if TODO //Console.WriteLine(GetList()[GetInt()..]); // fails to compile Console.WriteLine(GetSpan()[GetInt(1)..].ToString()); Console.WriteLine(GetString()[GetInt(1)..]); Console.WriteLine(new CustomList()[GetInt(1)..]); -#endif 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()[..]); -#if TODO //Console.WriteLine(GetList()[..]); // fails to compile Console.WriteLine(GetSpan()[..].ToString()); Console.WriteLine(GetString()[..]); Console.WriteLine(new CustomList()[..]); -#endif Console.WriteLine(new CustomList2()[..]); } diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index 72bf4cd6a..2140c5416 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -63,7 +63,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!(bni.Left.MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array))) return false; var indexMethods = new IndexMethods(context.TypeSystem); - if (!indexMethods.AllValid) + 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) @@ -84,7 +84,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms public readonly IMethod RangeCtor; public IType IndexType => IndexCtor?.DeclaringType; public IType RangeType => RangeCtor?.DeclaringType; - public bool AllValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null; + 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) { @@ -96,7 +100,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } foreach (var op in indexType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit")) { - if (op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) { + if (op.Parameters.Count == 1 && op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) { this.IndexImplicitConv = op; } } @@ -107,6 +111,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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; + } } } @@ -152,18 +167,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (rangeVar != null) 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 (!(startOffsetVar.LoadInstructions.Single().Parent is CallInstruction call)) return; if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter && call.Arguments.Count == 2) { @@ -176,9 +179,24 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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)) @@ -186,7 +204,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) return; var specialMethods = new IndexMethods(context.TypeSystem); - if (!specialMethods.AllValid) + if (!specialMethods.IsValid) return; if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false)) return; @@ -207,16 +225,31 @@ namespace ICSharpCode.Decompiler.IL.Transforms block.Instructions.RemoveRange(startPos, pos - startPos); } - void TransformSlicing() + void TransformSlicing(bool sliceLengthWasMisdetectedAsStartOffset = false) { - // 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 var sliceLengthVar, out var sliceLengthVarInit)) - return; - pos++; + 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)) @@ -232,25 +265,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms } if (!(sliceLengthVar.LoadInstructions.Single().Parent is CallInstruction call)) return; - if (call.Method.Name == "Slice") { - // OK, custom class slicing - } else if (call.Method.Name == "Substring" && call.Method.DeclaringType.IsKnownType(KnownTypeCode.String)) { - // OK, string slicing - } else { - return; - } - if (call.Method.IsExtensionMethod) - return; - if (call.Method.Parameters.Count != 2) - return; - if (!call.Method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32))) + if (!IsSlicingMethod(call.Method)) return; if (call.Arguments.Count != 3) return; if (!MatchContainerVar(call.Arguments[0], ref containerVar)) return; - if (!call.Arguments[1].MatchLdLoc(startOffsetVar)) - 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)) @@ -275,7 +303,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } var specialMethods = new IndexMethods(context.TypeSystem); - if (!specialMethods.AllValid) + if (!specialMethods.IsValid) return; context.Step($"{call.Method.Name} sliced with {startIndexKind}..{endIndexKind}", call); @@ -286,6 +314,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms 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)); @@ -301,15 +339,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + 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) + if (startIndexKind != IndexKind.FromStart && startIndexKind != IndexKind.TheStart) expectedUses += 1; - if (endIndexKind != IndexKind.FromStart) + if (endIndexKind != IndexKind.FromStart && endIndexKind != IndexKind.TheStart) expectedUses += 1; if (containerLengthVar != null) { return containerLengthVar.LoadCount == expectedUses; @@ -349,14 +399,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms // --> // complex_expr(call get_Item(ldloc container, ldobj startIndexLoad)) return new LdObj(indexLoad, specialMethods.IndexType); - } else if (indexKind == IndexKind.FromEnd) { + } 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); + Debug.Assert(indexKind == IndexKind.FromStart || indexKind == IndexKind.TheStart); return new Call(specialMethods.IndexImplicitConv) { Arguments = { indexLoad } }; } } @@ -462,7 +512,15 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// /// indexLoad is an integer, from the end of the container /// - FromEnd + 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, } /// @@ -477,7 +535,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms ILVariable containerLengthVar, ref ILVariable containerVar) { indexLoad = inst; - if (inst is CallInstruction call) { + 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; @@ -513,10 +574,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) { if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted) return false; - if (!bni.Right.MatchLdLoc(startOffsetVar)) - 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; }