Browse Source

Merge pull request #1986 from icsharpcode/ranges

Implement support for C# 8.0 indexing and slices (Ranges)
pull/2016/head
Siegfried Pammer 6 years ago committed by GitHub
parent
commit
5cba0ee631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 8
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 281
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs
  4. 3
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  5. 48
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs
  6. 17
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  7. 3
      ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
  8. 142
      ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertParenthesesVisitor.cs
  9. 10
      ICSharpCode.Decompiler/CSharp/Syntax/Expressions/BinaryOperatorExpression.cs
  10. 8
      ICSharpCode.Decompiler/CSharp/Syntax/Expressions/UnaryOperatorExpression.cs
  11. 7
      ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
  12. 20
      ICSharpCode.Decompiler/DecompilerSettings.cs
  13. 2
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  14. 5
      ICSharpCode.Decompiler/IL/Instructions.cs
  15. 12
      ICSharpCode.Decompiler/IL/Instructions.tt
  16. 2
      ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
  17. 9
      ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
  18. 596
      ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs
  19. 15
      ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs
  20. 137
      ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs
  21. 8
      ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs
  22. 9
      ILSpy/Properties/Resources.Designer.cs
  23. 3
      ILSpy/Properties/Resources.resx

1
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -90,6 +90,7 @@
<Compile Include="DisassemblerPrettyTestRunner.cs" /> <Compile Include="DisassemblerPrettyTestRunner.cs" />
<Compile Include="TestCases\Correctness\StringConcat.cs" /> <Compile Include="TestCases\Correctness\StringConcat.cs" />
<Compile Include="TestCases\ILPretty\Issue1681.cs" /> <Compile Include="TestCases\ILPretty\Issue1681.cs" />
<None Include="TestCases\Pretty\IndexRangeTest.cs" />
<None Include="TestCases\ILPretty\Issue1918.cs" /> <None Include="TestCases\ILPretty\Issue1918.cs" />
<Compile Include="TestCases\ILPretty\Issue1922.cs" /> <Compile Include="TestCases\ILPretty\Issue1922.cs" />
<Compile Include="TestCases\Ugly\NoLocalFunctions.cs" /> <Compile Include="TestCases\Ugly\NoLocalFunctions.cs" />

8
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -89,7 +89,13 @@ namespace ICSharpCode.Decompiler.Tests
RunForLibrary(); RunForLibrary();
RunForLibrary(asmOptions: AssemblerOptions.UseDebug); RunForLibrary(asmOptions: AssemblerOptions.UseDebug);
} }
[Test]
public void IndexRangeTest([ValueSource(nameof(dotnetCoreOnlyOptions))] CompilerOptions cscOptions)
{
RunForLibrary(cscOptions: cscOptions);
}
[Test] [Test]
public void InlineAssignmentTest([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) public void InlineAssignmentTest([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{ {

281
ICSharpCode.Decompiler.Tests/TestCases/Pretty/IndexRangeTest.cs

@ -0,0 +1,281 @@
// Copyright (c) 2020 Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class CustomList
{
public int Count => 0;
public int this[int index] => 0;
public CustomList Slice(int start, int length)
{
return this;
}
}
internal class CustomList2
{
public int Count => 0;
public int this[int index] => 0;
public int this[Index index] => 0;
public CustomList2 this[Range range] => this;
public CustomList2 Slice(int start, int length)
{
return this;
}
}
internal class IndexRangeTest
{
public static int[] GetArray()
{
throw null;
}
public static List<int> GetList()
{
throw null;
}
public static Span<int> GetSpan()
{
throw null;
}
public static string GetString()
{
throw null;
}
public static Index GetIndex(int i = 0)
{
return i;
}
public static Range GetRange(int i = 0)
{
return i..^i;
}
public static int GetInt(int i = 0)
{
return i;
}
public static Range[] SeveralRanges()
{
// Some of these are semantically identical, but we can still distinguish them in the IL code:
return new Range[14] {
..,
0..,
^0..,
GetInt(1)..,
^GetInt(2)..,
..0,
..^0,
..GetInt(3),
..^GetInt(4),
0..^0,
^0..0,
0..0,
GetInt(5)..GetInt(6),
0..(GetInt(7) + GetInt(8))
};
}
public static void UseIndex()
{
Console.WriteLine(GetArray()[GetIndex()]);
Console.WriteLine(GetList()[GetIndex()]);
Console.WriteLine(GetSpan()[GetIndex()]);
Console.WriteLine(GetString()[GetIndex()]);
Console.WriteLine(new CustomList()[GetIndex()]);
Console.WriteLine(new CustomList2()[GetIndex()]);
}
public static void UseIndexFromEnd()
{
Console.WriteLine(GetArray()[^GetInt()]);
Console.WriteLine(GetList()[^GetInt()]);
Console.WriteLine(GetSpan()[^GetInt()]);
Console.WriteLine(GetString()[^GetInt()]);
Console.WriteLine(new CustomList()[^GetInt()]);
Console.WriteLine(new CustomList2()[^GetInt()]);
}
public static void UseIndexForWrite()
{
GetArray()[GetIndex()] = GetInt();
GetList()[GetIndex()] = GetInt();
GetSpan()[GetIndex()] = GetInt();
}
private static void UseRef(ref int i)
{
}
public static void UseIndexForRef()
{
UseRef(ref GetArray()[GetIndex()]);
UseRef(ref GetArray()[^GetInt()]);
UseRef(ref GetSpan()[GetIndex()]);
UseRef(ref GetSpan()[^GetInt()]);
}
public static void UseRange()
{
Console.WriteLine(GetArray()[GetRange()]);
//Console.WriteLine(GetList()[GetRange()]); // fails to compile
Console.WriteLine(GetSpan()[GetRange()].ToString());
Console.WriteLine(GetString()[GetRange()]);
Console.WriteLine(new CustomList()[GetRange()]);
Console.WriteLine(new CustomList2()[GetRange()]);
}
public static void UseNewRangeFromIndex()
{
Console.WriteLine(GetArray()[GetIndex(1)..GetIndex(2)]);
//Console.WriteLine(GetList()[GetIndex(1)..GetIndex(2)]); // fails to compile
Console.WriteLine(GetSpan()[GetIndex(1)..GetIndex(2)].ToString());
Console.WriteLine(GetString()[GetIndex(1)..GetIndex(2)]);
Console.WriteLine(new CustomList()[GetIndex(1)..GetIndex(2)]);
Console.WriteLine(new CustomList2()[GetIndex(1)..GetIndex(2)]);
}
public static void UseNewRangeFromIntegers_BothFromStart()
{
Console.WriteLine(GetArray()[GetInt(1)..GetInt(2)]);
//Console.WriteLine(GetList()[GetInt()..GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[GetInt(1)..GetInt(2)].ToString());
Console.WriteLine(GetString()[GetInt(1)..GetInt(2)]);
Console.WriteLine(new CustomList()[GetInt(1)..GetInt(2)]);
Console.WriteLine(new CustomList2()[GetInt(1)..GetInt(2)]);
}
public static void UseNewRangeFromIntegers_BothFromEnd()
{
Console.WriteLine(GetArray()[^GetInt(1)..^GetInt(2)]);
//Console.WriteLine(GetList()[^GetInt()..^GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[^GetInt(1)..^GetInt(2)].ToString());
Console.WriteLine(GetString()[^GetInt(1)..^GetInt(2)]);
Console.WriteLine(new CustomList()[^GetInt(1)..^GetInt(2)]);
Console.WriteLine(new CustomList2()[^GetInt(1)..^GetInt(2)]);
}
public static void UseNewRangeFromIntegers_FromStartAndEnd()
{
Console.WriteLine(GetArray()[GetInt(1)..^GetInt(2)]);
//Console.WriteLine(GetList()[GetInt()..^GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[GetInt(1)..^GetInt(2)].ToString());
Console.WriteLine(GetString()[GetInt(1)..^GetInt(2)]);
Console.WriteLine(new CustomList()[GetInt(1)..^GetInt(2)]);
Console.WriteLine(new CustomList2()[GetInt(1)..^GetInt(2)]);
}
public static void UseNewRangeFromIntegers_FromEndAndStart()
{
Console.WriteLine(GetArray()[^GetInt(1)..GetInt(2)]);
//Console.WriteLine(GetList()[^GetInt()..GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[^GetInt(1)..GetInt(2)].ToString());
Console.WriteLine(GetString()[^GetInt(1)..GetInt(2)]);
Console.WriteLine(new CustomList()[^GetInt(1)..GetInt(2)]);
Console.WriteLine(new CustomList2()[^GetInt(1)..GetInt(2)]);
}
public static void UseNewRangeFromIntegers_OnlyEndPoint()
{
Console.WriteLine(GetArray()[..GetInt(2)]);
//Console.WriteLine(GetList()[..GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[..GetInt(2)].ToString());
Console.WriteLine(GetString()[..GetInt(2)]);
Console.WriteLine(new CustomList()[..GetInt(2)]);
Console.WriteLine(new CustomList2()[..GetInt(2)]);
}
public static void UseNewRangeFromIntegers_OnlyEndPoint_FromEnd()
{
Console.WriteLine(GetArray()[..^GetInt(2)]);
//Console.WriteLine(GetList()[..^GetInt()]); // fails to compile
Console.WriteLine(GetSpan()[..^GetInt(2)].ToString());
Console.WriteLine(GetString()[..^GetInt(2)]);
Console.WriteLine(new CustomList()[..^GetInt(2)]);
Console.WriteLine(new CustomList2()[..^GetInt(2)]);
}
public static void UseNewRangeFromIntegers_OnlyStartPoint()
{
Console.WriteLine(GetArray()[GetInt(1)..]);
//Console.WriteLine(GetList()[GetInt()..]); // fails to compile
Console.WriteLine(GetSpan()[GetInt(1)..].ToString());
Console.WriteLine(GetString()[GetInt(1)..]);
Console.WriteLine(new CustomList()[GetInt(1)..]);
Console.WriteLine(new CustomList2()[GetInt(1)..]);
}
public static void UseNewRangeFromIntegers_OnlyStartPoint_FromEnd()
{
Console.WriteLine(GetArray()[^GetInt(1)..]);
//Console.WriteLine(GetList()[^GetInt()..]); // fails to compile
Console.WriteLine(GetSpan()[^GetInt(1)..].ToString());
Console.WriteLine(GetString()[^GetInt(1)..]);
Console.WriteLine(new CustomList()[^GetInt(1)..]);
Console.WriteLine(new CustomList2()[^GetInt(1)..]);
}
public static void UseConstantRange()
{
// Fortunately the C# compiler doesn't optimize
// "str.Length - 2 - 1" here, so the normal pattern applies.
Console.WriteLine(GetString()[1..2]);
Console.WriteLine(GetString()[1..^1]);
Console.WriteLine(GetString()[^2..^1]);
Console.WriteLine(GetString()[..1]);
Console.WriteLine(GetString()[..^1]);
Console.WriteLine(GetString()[1..]);
Console.WriteLine(GetString()[^1..]);
}
public static void UseWholeRange()
{
Console.WriteLine(GetArray()[..]);
//Console.WriteLine(GetList()[..]); // fails to compile
Console.WriteLine(GetSpan()[..].ToString());
Console.WriteLine(GetString()[..]);
Console.WriteLine(new CustomList()[..]);
Console.WriteLine(new CustomList2()[..]);
}
public static void UseIndexForIntIndexerWhenIndexIndexerIsAvailable()
{
// Same code as the compiler emits for CustomList,
// but here we can't translate it back to `customList[GetIndex()]`
// because that would call a different overload.
CustomList2 customList = new CustomList2();
int count = customList.Count;
int offset = GetIndex().GetOffset(count);
Console.WriteLine(customList[offset]);
}
public static void UseSliceWhenRangeIndexerIsAvailable()
{
// Same code as the compiler emits for CustomList,
// but here we can't translate it back to `customList[GetIndex()]`
// because that would call a different overload.
CustomList2 customList = new CustomList2();
int count = customList.Count;
Range range = GetRange();
int offset = range.Start.GetOffset(count);
int length = range.End.GetOffset(count) - offset;
Console.WriteLine(customList.Slice(offset, length));
}
}
}

3
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -150,7 +150,8 @@ namespace ICSharpCode.Decompiler.CSharp
new TransformCollectionAndObjectInitializers(), new TransformCollectionAndObjectInitializers(),
new TransformExpressionTrees(), new TransformExpressionTrees(),
new NamedArgumentTransform(), new NamedArgumentTransform(),
new UserDefinedLogicTransform() new UserDefinedLogicTransform(),
new IndexRangeTransform()
), ),
} }
}, },

48
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -252,6 +252,12 @@ namespace ICSharpCode.Decompiler.CSharp
argumentList.ExpectedParameters = method.Parameters.ToArray(); argumentList.ExpectedParameters = method.Parameters.ToArray();
} }
if (settings.Ranges) {
if (HandleRangeConstruction(out var result, callOpCode, method, target, argumentList)) {
return result;
}
}
if (callOpCode == OpCode.NewObj) { if (callOpCode == OpCode.NewObj) {
return HandleConstructorCall(expectedTargetDetails, target.ResolveResult, method, argumentList); return HandleConstructorCall(expectedTargetDetails, target.ResolveResult, method, argumentList);
} }
@ -1494,5 +1500,47 @@ namespace ICSharpCode.Decompiler.CSharp
return Build(call.OpCode, call.Method, arguments, argumentToParameterMap, call.ConstrainedTo) return Build(call.OpCode, call.Method, arguments, argumentToParameterMap, call.ConstrainedTo)
.WithILInstruction(call).WithILInstruction(block); .WithILInstruction(call).WithILInstruction(block);
} }
private bool HandleRangeConstruction(out ExpressionWithResolveResult result, OpCode callOpCode, IMethod method, TranslatedExpression target, ArgumentList argumentList)
{
result = default;
if (argumentList.ArgumentNames != null) {
return false; // range syntax doesn't support named arguments
}
if (method.DeclaringType.IsKnownType(KnownTypeCode.Range)) {
if (callOpCode == OpCode.NewObj && argumentList.Length == 2) {
result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, argumentList.Arguments[1])
.WithRR(new MemberResolveResult(null, method));
return true;
} else if (callOpCode == OpCode.Call && method.Name == "get_All" && argumentList.Length == 0) {
result = new BinaryOperatorExpression(Expression.Null, BinaryOperatorType.Range, Expression.Null)
.WithRR(new MemberResolveResult(null, method.AccessorOwner ?? method));
return true;
} else if (callOpCode == OpCode.Call && method.Name == "StartAt" && argumentList.Length == 1) {
result = new BinaryOperatorExpression(argumentList.Arguments[0], BinaryOperatorType.Range, Expression.Null)
.WithRR(new MemberResolveResult(null, method));
return true;
} else if (callOpCode == OpCode.Call && method.Name == "EndAt" && argumentList.Length == 1) {
result = new BinaryOperatorExpression(Expression.Null, BinaryOperatorType.Range, argumentList.Arguments[0])
.WithRR(new MemberResolveResult(null, method));
return true;
}
} else if (callOpCode == OpCode.NewObj && method.DeclaringType.IsKnownType(KnownTypeCode.Index)) {
if (argumentList.Length != 2)
return false;
if (!(argumentList.Arguments[1].Expression is PrimitiveExpression pe && pe.Value is true))
return false;
result = new UnaryOperatorExpression(UnaryOperatorType.IndexFromEnd, argumentList.Arguments[0])
.WithRR(new MemberResolveResult(null, method));
return true;
} else if (method is SyntheticRangeIndexAccessor rangeIndexAccessor && rangeIndexAccessor.IsSlicing) {
// For slicing the method is called Slice()/Substring(), but we still need to output indexer notation.
// So special-case range-based slicing here.
result = new IndexerExpression(target, argumentList.Arguments.Select(a => a.Expression))
.WithRR(new MemberResolveResult(target.ResolveResult, method));
return true;
}
return false;
}
} }
} }

17
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -2244,14 +2244,23 @@ namespace ICSharpCode.Decompiler.CSharp
arrayType = new ArrayType(compilation, inst.Type, inst.Indices.Count); arrayType = new ArrayType(compilation, inst.Type, inst.Indices.Count);
arrayExpr = arrayExpr.ConvertTo(arrayType, this); arrayExpr = arrayExpr.ConvertTo(arrayType, this);
} }
TranslatedExpression expr = new IndexerExpression( IndexerExpression indexerExpr;
arrayExpr, inst.Indices.Select(i => TranslateArrayIndex(i).Expression) if (inst.WithSystemIndex) {
).WithILInstruction(inst).WithRR(new ResolveResult(arrayType.ElementType)); 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) return new DirectionExpression(FieldDirection.Ref, expr)
.WithoutILInstruction().WithRR(new ByReferenceResolveResult(expr.Type, ReferenceKind.Ref)); .WithoutILInstruction().WithRR(new ByReferenceResolveResult(expr.Type, ReferenceKind.Ref));
} }
TranslatedExpression TranslateArrayIndex(ILInstruction i) TranslatedExpression TranslateArrayIndex(ILInstruction i, bool expectSystemIndex = false)
{ {
var input = Translate(i); var input = Translate(i);
KnownTypeCode targetType; KnownTypeCode targetType;

3
ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs

@ -696,6 +696,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
case BinaryOperatorType.NullCoalescing: case BinaryOperatorType.NullCoalescing:
spacePolicy = true; spacePolicy = true;
break; break;
case BinaryOperatorType.Range:
spacePolicy = false;
break;
default: default:
throw new NotSupportedException("Invalid value for BinaryOperatorType"); throw new NotSupportedException("Invalid value for BinaryOperatorType");
} }

142
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 // 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 // software and associated documentation files (the "Software"), to deal in the Software
@ -36,25 +36,39 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
/// </summary> /// </summary>
public bool InsertParenthesesForReadability { get; set; } public bool InsertParenthesesForReadability { get; set; }
const int Primary = 17; enum PrecedenceLevel
const int NullableRewrap = 16; {
const int QueryOrLambda = 15; // Higher integer value = higher precedence.
const int Unary = 14; Assignment,
const int RelationalAndTypeTesting = 10; Conditional, // ?:
const int Equality = 9; NullCoalescing, // ??
const int Conditional = 2; ConditionalOr, // ||
const int Assignment = 1; ConditionalAnd, // &&
BitwiseOr, // |
ExclusiveOr, // binary ^
BitwiseAnd, // binary &
Equality, // == !=
RelationalAndTypeTesting, // < <= > >= is
Shift, // << >>
Additive, // binary + -
Multiplicative, // * / %
Range, // ..
Unary,
QueryOrLambda,
NullableRewrap,
Primary
}
/// <summary> /// <summary>
/// Gets the row number in the C# 4.0 spec operator precedence table. /// Gets the row number in the C# 4.0 spec operator precedence table.
/// </summary> /// </summary>
static int GetPrecedence(Expression expr) static PrecedenceLevel GetPrecedence(Expression expr)
{ {
// Note: the operator precedence table on MSDN is incorrect // Note: the operator precedence table on MSDN is incorrect
if (expr is QueryExpression) { if (expr is QueryExpression) {
// Not part of the table in the C# spec, but we need to ensure that queries within // Not part of the table in the C# spec, but we need to ensure that queries within
// primary expressions get parenthesized. // primary expressions get parenthesized.
return QueryOrLambda; return PrecedenceLevel.QueryOrLambda;
} }
if (expr is UnaryOperatorExpression uoe) { if (expr is UnaryOperatorExpression uoe) {
switch (uoe.Operator) { switch (uoe.Operator) {
@ -62,81 +76,83 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
case UnaryOperatorType.PostIncrement: case UnaryOperatorType.PostIncrement:
case UnaryOperatorType.NullConditional: case UnaryOperatorType.NullConditional:
case UnaryOperatorType.SuppressNullableWarning: case UnaryOperatorType.SuppressNullableWarning:
return Primary; return PrecedenceLevel.Primary;
case UnaryOperatorType.NullConditionalRewrap: case UnaryOperatorType.NullConditionalRewrap:
return NullableRewrap; return PrecedenceLevel.NullableRewrap;
case UnaryOperatorType.IsTrue: case UnaryOperatorType.IsTrue:
return Conditional; return PrecedenceLevel.Conditional;
default: default:
return Unary; return PrecedenceLevel.Unary;
} }
} }
if (expr is CastExpression) if (expr is CastExpression)
return Unary; return PrecedenceLevel.Unary;
if (expr is PrimitiveExpression primitive) { if (expr is PrimitiveExpression primitive) {
var value = primitive.Value; var value = primitive.Value;
if (value is int i && i < 0) if (value is int i && i < 0)
return Unary; return PrecedenceLevel.Unary;
if (value is long l && l < 0) if (value is long l && l < 0)
return Unary; return PrecedenceLevel.Unary;
if (value is float f && f < 0) if (value is float f && f < 0)
return Unary; return PrecedenceLevel.Unary;
if (value is double d && d < 0) if (value is double d && d < 0)
return Unary; return PrecedenceLevel.Unary;
if (value is decimal de && de < 0) if (value is decimal de && de < 0)
return Unary; return PrecedenceLevel.Unary;
return PrecedenceLevel.Primary;
} }
BinaryOperatorExpression boe = expr as BinaryOperatorExpression; if (expr is BinaryOperatorExpression boe) {
if (boe != null) {
switch (boe.Operator) { switch (boe.Operator) {
case BinaryOperatorType.Range:
return PrecedenceLevel.Range;
case BinaryOperatorType.Multiply: case BinaryOperatorType.Multiply:
case BinaryOperatorType.Divide: case BinaryOperatorType.Divide:
case BinaryOperatorType.Modulus: case BinaryOperatorType.Modulus:
return 13; // multiplicative return PrecedenceLevel.Multiplicative;
case BinaryOperatorType.Add: case BinaryOperatorType.Add:
case BinaryOperatorType.Subtract: case BinaryOperatorType.Subtract:
return 12; // additive return PrecedenceLevel.Additive;
case BinaryOperatorType.ShiftLeft: case BinaryOperatorType.ShiftLeft:
case BinaryOperatorType.ShiftRight: case BinaryOperatorType.ShiftRight:
return 11; return PrecedenceLevel.Shift;
case BinaryOperatorType.GreaterThan: case BinaryOperatorType.GreaterThan:
case BinaryOperatorType.GreaterThanOrEqual: case BinaryOperatorType.GreaterThanOrEqual:
case BinaryOperatorType.LessThan: case BinaryOperatorType.LessThan:
case BinaryOperatorType.LessThanOrEqual: case BinaryOperatorType.LessThanOrEqual:
return RelationalAndTypeTesting; return PrecedenceLevel.RelationalAndTypeTesting;
case BinaryOperatorType.Equality: case BinaryOperatorType.Equality:
case BinaryOperatorType.InEquality: case BinaryOperatorType.InEquality:
return Equality; return PrecedenceLevel.Equality;
case BinaryOperatorType.BitwiseAnd: case BinaryOperatorType.BitwiseAnd:
return 8; return PrecedenceLevel.BitwiseAnd;
case BinaryOperatorType.ExclusiveOr: case BinaryOperatorType.ExclusiveOr:
return 7; return PrecedenceLevel.ExclusiveOr;
case BinaryOperatorType.BitwiseOr: case BinaryOperatorType.BitwiseOr:
return 6; return PrecedenceLevel.BitwiseOr;
case BinaryOperatorType.ConditionalAnd: case BinaryOperatorType.ConditionalAnd:
return 5; return PrecedenceLevel.ConditionalAnd;
case BinaryOperatorType.ConditionalOr: case BinaryOperatorType.ConditionalOr:
return 4; return PrecedenceLevel.ConditionalOr;
case BinaryOperatorType.NullCoalescing: case BinaryOperatorType.NullCoalescing:
return 3; return PrecedenceLevel.NullCoalescing;
default: default:
throw new NotSupportedException("Invalid value for BinaryOperatorType"); throw new NotSupportedException("Invalid value for BinaryOperatorType");
} }
} }
if (expr is IsExpression || expr is AsExpression) if (expr is IsExpression || expr is AsExpression)
return RelationalAndTypeTesting; return PrecedenceLevel.RelationalAndTypeTesting;
if (expr is ConditionalExpression || expr is DirectionExpression) if (expr is ConditionalExpression || expr is DirectionExpression)
return Conditional; return PrecedenceLevel.Conditional;
if (expr is AssignmentExpression || expr is LambdaExpression) if (expr is AssignmentExpression || expr is LambdaExpression)
return Assignment; return PrecedenceLevel.Assignment;
// anything else: primary expression // anything else: primary expression
return Primary; return PrecedenceLevel.Primary;
} }
/// <summary> /// <summary>
/// Parenthesizes the expression if it does not have the minimum required precedence. /// Parenthesizes the expression if it does not have the minimum required precedence.
/// </summary> /// </summary>
static void ParenthesizeIfRequired(Expression expr, int minimumPrecedence) static void ParenthesizeIfRequired(Expression expr, PrecedenceLevel minimumPrecedence)
{ {
if (GetPrecedence(expr) < minimumPrecedence) { if (GetPrecedence(expr) < minimumPrecedence) {
Parenthesize(expr); Parenthesize(expr);
@ -151,25 +167,25 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
// Primary expressions // Primary expressions
public override void VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression) public override void VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression)
{ {
ParenthesizeIfRequired(memberReferenceExpression.Target, Primary); ParenthesizeIfRequired(memberReferenceExpression.Target, PrecedenceLevel.Primary);
base.VisitMemberReferenceExpression(memberReferenceExpression); base.VisitMemberReferenceExpression(memberReferenceExpression);
} }
public override void VisitPointerReferenceExpression(PointerReferenceExpression pointerReferenceExpression) public override void VisitPointerReferenceExpression(PointerReferenceExpression pointerReferenceExpression)
{ {
ParenthesizeIfRequired(pointerReferenceExpression.Target, Primary); ParenthesizeIfRequired(pointerReferenceExpression.Target, PrecedenceLevel.Primary);
base.VisitPointerReferenceExpression(pointerReferenceExpression); base.VisitPointerReferenceExpression(pointerReferenceExpression);
} }
public override void VisitInvocationExpression(InvocationExpression invocationExpression) public override void VisitInvocationExpression(InvocationExpression invocationExpression)
{ {
ParenthesizeIfRequired(invocationExpression.Target, Primary); ParenthesizeIfRequired(invocationExpression.Target, PrecedenceLevel.Primary);
base.VisitInvocationExpression(invocationExpression); base.VisitInvocationExpression(invocationExpression);
} }
public override void VisitIndexerExpression(IndexerExpression indexerExpression) public override void VisitIndexerExpression(IndexerExpression indexerExpression)
{ {
ParenthesizeIfRequired(indexerExpression.Target, Primary); ParenthesizeIfRequired(indexerExpression.Target, PrecedenceLevel.Primary);
ArrayCreateExpression ace = indexerExpression.Target as ArrayCreateExpression; ArrayCreateExpression ace = indexerExpression.Target as ArrayCreateExpression;
if (ace != null && (InsertParenthesesForReadability || ace.Initializer.IsNull)) { if (ace != null && (InsertParenthesesForReadability || ace.Initializer.IsNull)) {
// require parentheses for "(new int[1])[0]" // 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. // Even in readability mode, don't parenthesize casts of casts.
if (!(castExpression.Expression is CastExpression)) { 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 // 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. // "(int)-1" is fine, but "(A)-b" is not a cast.
@ -255,14 +271,14 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
// Binary Operators // Binary Operators
public override void VisitBinaryOperatorExpression(BinaryOperatorExpression binaryOperatorExpression) public override void VisitBinaryOperatorExpression(BinaryOperatorExpression binaryOperatorExpression)
{ {
int precedence = GetPrecedence(binaryOperatorExpression); PrecedenceLevel precedence = GetPrecedence(binaryOperatorExpression);
if (binaryOperatorExpression.Operator == BinaryOperatorType.NullCoalescing) { if (binaryOperatorExpression.Operator == BinaryOperatorType.NullCoalescing) {
if (InsertParenthesesForReadability) { if (InsertParenthesesForReadability) {
ParenthesizeIfRequired(binaryOperatorExpression.Left, NullableRewrap); ParenthesizeIfRequired(binaryOperatorExpression.Left, PrecedenceLevel.NullableRewrap);
if (GetBinaryOperatorType(binaryOperatorExpression.Right) == BinaryOperatorType.NullCoalescing) { if (GetBinaryOperatorType(binaryOperatorExpression.Right) == BinaryOperatorType.NullCoalescing) {
ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence); ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence);
} else { } else {
ParenthesizeIfRequired(binaryOperatorExpression.Right, NullableRewrap); ParenthesizeIfRequired(binaryOperatorExpression.Right, PrecedenceLevel.NullableRewrap);
} }
} else { } else {
// ?? is right-associative // ?? is right-associative
@ -270,10 +286,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence); ParenthesizeIfRequired(binaryOperatorExpression.Right, precedence);
} }
} else { } else {
if (InsertParenthesesForReadability && precedence < Equality) { if (InsertParenthesesForReadability && precedence < PrecedenceLevel.Equality) {
// In readable mode, boost the priority of the left-hand side if the operator // 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. // 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) { if (GetBinaryOperatorType(binaryOperatorExpression.Left) == binaryOperatorExpression.Operator) {
ParenthesizeIfRequired(binaryOperatorExpression.Left, precedence); ParenthesizeIfRequired(binaryOperatorExpression.Left, precedence);
} else { } else {
@ -309,9 +325,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
{ {
if (InsertParenthesesForReadability) { if (InsertParenthesesForReadability) {
// few people know the precedence of 'is', so always put parentheses in nice-looking mode. // 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 { } else {
ParenthesizeIfRequired(isExpression.Expression, RelationalAndTypeTesting); ParenthesizeIfRequired(isExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting);
} }
base.VisitIsExpression(isExpression); base.VisitIsExpression(isExpression);
} }
@ -320,9 +336,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
{ {
if (InsertParenthesesForReadability) { if (InsertParenthesesForReadability) {
// few people know the precedence of 'as', so always put parentheses in nice-looking mode. // 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 { } else {
ParenthesizeIfRequired(asExpression.Expression, RelationalAndTypeTesting); ParenthesizeIfRequired(asExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting);
} }
base.VisitAsExpression(asExpression); base.VisitAsExpression(asExpression);
} }
@ -341,13 +357,13 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
// Only ((a ? b : c) ? d : e) strictly needs the additional parentheses // Only ((a ? b : c) ? d : e) strictly needs the additional parentheses
if (InsertParenthesesForReadability && !IsConditionalRefExpression(conditionalExpression)) { if (InsertParenthesesForReadability && !IsConditionalRefExpression(conditionalExpression)) {
// Precedence of ?: can be confusing; so always put parentheses in nice-looking mode. // Precedence of ?: can be confusing; so always put parentheses in nice-looking mode.
ParenthesizeIfRequired(conditionalExpression.Condition, NullableRewrap); ParenthesizeIfRequired(conditionalExpression.Condition, PrecedenceLevel.NullableRewrap);
ParenthesizeIfRequired(conditionalExpression.TrueExpression, NullableRewrap); ParenthesizeIfRequired(conditionalExpression.TrueExpression, PrecedenceLevel.NullableRewrap);
ParenthesizeIfRequired(conditionalExpression.FalseExpression, NullableRewrap); ParenthesizeIfRequired(conditionalExpression.FalseExpression, PrecedenceLevel.NullableRewrap);
} else { } else {
ParenthesizeIfRequired(conditionalExpression.Condition, Conditional + 1); ParenthesizeIfRequired(conditionalExpression.Condition, PrecedenceLevel.Conditional + 1);
ParenthesizeIfRequired(conditionalExpression.TrueExpression, Conditional); ParenthesizeIfRequired(conditionalExpression.TrueExpression, PrecedenceLevel.Conditional);
ParenthesizeIfRequired(conditionalExpression.FalseExpression, Conditional); ParenthesizeIfRequired(conditionalExpression.FalseExpression, PrecedenceLevel.Conditional);
} }
base.VisitConditionalExpression(conditionalExpression); base.VisitConditionalExpression(conditionalExpression);
} }
@ -361,11 +377,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
public override void VisitAssignmentExpression(AssignmentExpression assignmentExpression) public override void VisitAssignmentExpression(AssignmentExpression assignmentExpression)
{ {
// assignment is right-associative // assignment is right-associative
ParenthesizeIfRequired(assignmentExpression.Left, Assignment + 1); ParenthesizeIfRequired(assignmentExpression.Left, PrecedenceLevel.Assignment + 1);
if (InsertParenthesesForReadability && !(assignmentExpression.Right is DirectionExpression)) { if (InsertParenthesesForReadability && !(assignmentExpression.Right is DirectionExpression)) {
ParenthesizeIfRequired(assignmentExpression.Right, RelationalAndTypeTesting + 1); ParenthesizeIfRequired(assignmentExpression.Right, PrecedenceLevel.RelationalAndTypeTesting + 1);
} else { } else {
ParenthesizeIfRequired(assignmentExpression.Right, Assignment); ParenthesizeIfRequired(assignmentExpression.Right, PrecedenceLevel.Assignment);
} }
base.VisitAssignmentExpression(assignmentExpression); base.VisitAssignmentExpression(assignmentExpression);
} }
@ -394,7 +410,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
public override void VisitNamedExpression (NamedExpression namedExpression) public override void VisitNamedExpression (NamedExpression namedExpression)
{ {
if (InsertParenthesesForReadability) { if (InsertParenthesesForReadability) {
ParenthesizeIfRequired(namedExpression.Expression, RelationalAndTypeTesting + 1); ParenthesizeIfRequired(namedExpression.Expression, PrecedenceLevel.RelationalAndTypeTesting + 1);
} }
base.VisitNamedExpression (namedExpression); base.VisitNamedExpression (namedExpression);
} }

10
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 ShiftLeftRole = new TokenRole ("<<");
public readonly static TokenRole ShiftRightRole = new TokenRole (">>"); public readonly static TokenRole ShiftRightRole = new TokenRole (">>");
public readonly static TokenRole NullCoalescingRole = new TokenRole ("??"); public readonly static TokenRole NullCoalescingRole = new TokenRole ("??");
public readonly static TokenRole RangeRole = new TokenRole ("..");
public readonly static Role<Expression> LeftRole = new Role<Expression>("Left", Expression.Null); public readonly static Role<Expression> LeftRole = new Role<Expression>("Left", Expression.Null);
public readonly static Role<Expression> RightRole = new Role<Expression>("Right", Expression.Null); public readonly static Role<Expression> RightRole = new Role<Expression>("Right", Expression.Null);
@ -151,6 +152,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return ShiftRightRole; return ShiftRightRole;
case BinaryOperatorType.NullCoalescing: case BinaryOperatorType.NullCoalescing:
return NullCoalescingRole; return NullCoalescingRole;
case BinaryOperatorType.Range:
return RangeRole;
default: default:
throw new NotSupportedException("Invalid value for BinaryOperatorType"); throw new NotSupportedException("Invalid value for BinaryOperatorType");
} }
@ -197,6 +200,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return ExpressionType.RightShift; return ExpressionType.RightShift;
case BinaryOperatorType.NullCoalescing: case BinaryOperatorType.NullCoalescing:
return ExpressionType.Coalesce; return ExpressionType.Coalesce;
case BinaryOperatorType.Range:
return ExpressionType.Extension;
default: default:
throw new NotSupportedException("Invalid value for BinaryOperatorType"); throw new NotSupportedException("Invalid value for BinaryOperatorType");
} }
@ -255,6 +260,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
ShiftRight, ShiftRight,
/// <summary>left ?? right</summary> /// <summary>left ?? right</summary>
NullCoalescing NullCoalescing,
/// <summary>left .. right</summary>
/// <remarks>left and right are optional = may be Expression.Null</remarks>
Range
} }
} }

8
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 AwaitRole = new TokenRole ("await");
public readonly static TokenRole NullConditionalRole = new TokenRole ("?"); public readonly static TokenRole NullConditionalRole = new TokenRole ("?");
public readonly static TokenRole SuppressNullableWarningRole = new TokenRole ("!"); public readonly static TokenRole SuppressNullableWarningRole = new TokenRole ("!");
public readonly static TokenRole IndexFromEndRole = new TokenRole ("^");
public UnaryOperatorExpression() public UnaryOperatorExpression()
{ {
@ -122,6 +123,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return null; // no syntax return null; // no syntax
case UnaryOperatorType.SuppressNullableWarning: case UnaryOperatorType.SuppressNullableWarning:
return SuppressNullableWarningRole; return SuppressNullableWarningRole;
case UnaryOperatorType.IndexFromEnd:
return IndexFromEndRole;
default: default:
throw new NotSupportedException("Invalid value for UnaryOperatorType"); throw new NotSupportedException("Invalid value for UnaryOperatorType");
} }
@ -150,6 +153,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
case UnaryOperatorType.AddressOf: case UnaryOperatorType.AddressOf:
case UnaryOperatorType.Await: case UnaryOperatorType.Await:
case UnaryOperatorType.SuppressNullableWarning: case UnaryOperatorType.SuppressNullableWarning:
case UnaryOperatorType.IndexFromEnd:
return ExpressionType.Extension; return ExpressionType.Extension;
default: default:
throw new NotSupportedException("Invalid value for UnaryOperatorType"); throw new NotSupportedException("Invalid value for UnaryOperatorType");
@ -206,5 +210,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// C# 8 postfix ! operator (dammit operator) /// C# 8 postfix ! operator (dammit operator)
/// </summary> /// </summary>
SuppressNullableWarning, SuppressNullableWarning,
/// <summary>
/// C# 8 prefix ^ operator
/// </summary>
IndexFromEnd,
} }
} }

7
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()))); invocationExpression.ReplaceWith(new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First())));
} }
break; 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); BinaryOperatorType? bop = GetBinaryOperatorTypeFromMetadataName(method.Name);

20
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -112,12 +112,13 @@ namespace ICSharpCode.Decompiler
asyncUsingAndForEachStatement = false; asyncUsingAndForEachStatement = false;
asyncEnumerator = false; asyncEnumerator = false;
staticLocalFunctions = false; staticLocalFunctions = false;
ranges = false;
} }
} }
public CSharp.LanguageVersion GetMinimumRequiredVersion() public CSharp.LanguageVersion GetMinimumRequiredVersion()
{ {
if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions) if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement || staticLocalFunctions || ranges)
return CSharp.LanguageVersion.CSharp8_0; return CSharp.LanguageVersion.CSharp8_0;
if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers || patternBasedFixedStatement) if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers || patternBasedFixedStatement)
return CSharp.LanguageVersion.CSharp7_3; return CSharp.LanguageVersion.CSharp7_3;
@ -1100,6 +1101,23 @@ namespace ICSharpCode.Decompiler
} }
} }
bool ranges = true;
/// <summary>
/// Gets/Sets whether C# 8.0 static local functions should be transformed.
/// </summary>
[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; bool nullableReferenceTypes = true;
/// <summary> /// <summary>

2
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -64,6 +64,7 @@
<Compile Include="CSharp\OutputVisitor\GenericGrammarAmbiguityVisitor.cs" /> <Compile Include="CSharp\OutputVisitor\GenericGrammarAmbiguityVisitor.cs" />
<Compile Include="CSharp\RequiredNamespaceCollector.cs" /> <Compile Include="CSharp\RequiredNamespaceCollector.cs" />
<Compile Include="CSharp\SequencePointBuilder.cs" /> <Compile Include="CSharp\SequencePointBuilder.cs" />
<Compile Include="IL\Transforms\IndexRangeTransform.cs" />
<Compile Include="CSharp\TranslatedStatement.cs" /> <Compile Include="CSharp\TranslatedStatement.cs" />
<Compile Include="DebugInfo\KnownGuids.cs" /> <Compile Include="DebugInfo\KnownGuids.cs" />
<Compile Include="Disassembler\DisassemblerSignatureTypeProvider.cs" /> <Compile Include="Disassembler\DisassemblerSignatureTypeProvider.cs" />
@ -384,6 +385,7 @@
<Compile Include="TypeSystem\Implementation\MetadataTypeDefinition.cs" /> <Compile Include="TypeSystem\Implementation\MetadataTypeDefinition.cs" />
<Compile Include="TypeSystem\Implementation\MetadataTypeParameter.cs" /> <Compile Include="TypeSystem\Implementation\MetadataTypeParameter.cs" />
<Compile Include="TypeSystem\Implementation\SpecializedParameter.cs" /> <Compile Include="TypeSystem\Implementation\SpecializedParameter.cs" />
<Compile Include="TypeSystem\Implementation\SyntheticRangeIndexer.cs" />
<Compile Include="TypeSystem\Implementation\ThreeState.cs" /> <Compile Include="TypeSystem\Implementation\ThreeState.cs" />
<Compile Include="TypeSystem\MetadataModule.cs" /> <Compile Include="TypeSystem\MetadataModule.cs" />
<Compile Include="TypeSystem\ModifiedType.cs" /> <Compile Include="TypeSystem\ModifiedType.cs" />

5
ICSharpCode.Decompiler/IL/Instructions.cs

@ -4713,6 +4713,7 @@ namespace ICSharpCode.Decompiler.IL
clone.Indices.AddRange(this.Indices.Select(arg => (ILInstruction)arg.Clone())); clone.Indices.AddRange(this.Indices.Select(arg => (ILInstruction)arg.Clone()));
return clone; return clone;
} }
public bool WithSystemIndex;
public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced public bool DelayExceptions; // NullReferenceException/IndexOutOfBoundsException only occurs when the reference is dereferenced
public override StackType ResultType { get { return StackType.Ref; } } public override StackType ResultType { get { return StackType.Ref; } }
/// <summary>Gets whether the 'readonly' prefix was applied to this instruction.</summary> /// <summary>Gets whether the 'readonly' prefix was applied to this instruction.</summary>
@ -4729,6 +4730,8 @@ namespace ICSharpCode.Decompiler.IL
public override void WriteTo(ITextOutput output, ILAstWritingOptions options) public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
{ {
WriteILRange(output, options); WriteILRange(output, options);
if (WithSystemIndex)
output.Write("withsystemindex.");
if (DelayExceptions) if (DelayExceptions)
output.Write("delayex."); output.Write("delayex.");
if (IsReadOnly) if (IsReadOnly)
@ -4759,7 +4762,7 @@ namespace ICSharpCode.Decompiler.IL
protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match) protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match)
{ {
var o = other as LdElema; 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;
} }
} }
} }

12
ICSharpCode.Decompiler/IL/Instructions.tt

@ -278,6 +278,7 @@
CustomClassName("LdLen"), CustomArguments(("array", new[] { "O" })), CustomConstructor, CustomWriteTo, MayThrow), CustomClassName("LdLen"), CustomArguments(("array", new[] { "O" })), CustomConstructor, CustomWriteTo, MayThrow),
new OpCode("ldelema", "Load address of array element.", new OpCode("ldelema", "Load address of array element.",
CustomClassName("LdElema"), HasTypeOperand, CustomChildren(new [] { new ArgumentInfo("array"), new ArgumentInfo("indices") { IsCollection = true } }, true), CustomClassName("LdElema"), HasTypeOperand, CustomChildren(new [] { new ArgumentInfo("array"), new ArgumentInfo("indices") { IsCollection = true } }, true),
BoolFlag("WithSystemIndex"),
MayThrowIfNotDelayed, ResultType("Ref"), SupportsReadonlyPrefix), MayThrowIfNotDelayed, ResultType("Ref"), SupportsReadonlyPrefix),
new OpCode("get.pinnable.reference", "Retrieves a pinnable reference for the input object." + Environment.NewLine 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 + "The input must be an object reference (O)." + Environment.NewLine
@ -1105,6 +1106,17 @@ protected override void Disconnected()
opCode.WriteOperand.Add("member.WriteTo(output);"); opCode.WriteOperand.Add("member.WriteTo(output);");
}; };
// Adds a member of type bool to the instruction.
static Action<OpCode> 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. // LoadConstant trait: the instruction loads a compile-time constant. Implies NoArguments.
static Action<OpCode> LoadConstant(string operandType) static Action<OpCode> LoadConstant(string operandType)
{ {

2
ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs

@ -208,6 +208,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{ {
base.VisitLdElema(inst); base.VisitLdElema(inst);
CleanUpArrayIndices(inst.Indices); CleanUpArrayIndices(inst.Indices);
if (IndexRangeTransform.HandleLdElema(inst, context))
return;
} }
protected internal override void VisitNewArr(NewArr inst) protected internal override void VisitNewArr(NewArr inst)

9
ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

@ -21,6 +21,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
namespace ICSharpCode.Decompiler.IL.Transforms namespace ICSharpCode.Decompiler.IL.Transforms
{ {
@ -469,6 +470,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (parent.SlotInfo == CompoundAssignmentInstruction.TargetSlot) { if (parent.SlotInfo == CompoundAssignmentInstruction.TargetSlot) {
return true; return true;
} }
if (((CallInstruction)parent).Method is SyntheticRangeIndexAccessor) {
return true;
}
break;
case OpCode.LdElema:
if (((LdElema)parent).WithSystemIndex) {
return true;
}
break; break;
} }
// decide based on the top-level target instruction into which we are inlining: // decide based on the top-level target instruction into which we are inlining:

596
ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs

@ -0,0 +1,596 @@
// Copyright (c) 2020 Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Diagnostics;
using System.Linq;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
namespace ICSharpCode.Decompiler.IL.Transforms
{
/// <summary>
/// Transform for the C# 8 System.Index / System.Range feature
/// </summary>
class IndexRangeTransform : IStatementTransform
{
/// <summary>
/// Called by expression transforms.
/// Handles the `array[System.Index]` cases.
/// </summary>
public static bool HandleLdElema(LdElema ldelema, ILTransformContext context)
{
if (!context.Settings.Ranges)
return false;
if (!ldelema.Array.MatchLdLoc(out ILVariable array))
return false;
if (ldelema.Indices.Count != 1)
return false; // the index/range feature doesn't support multi-dimensional arrays
var index = ldelema.Indices[0];
if (index is CallInstruction call && call.Method.Name == "GetOffset" && call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index)) {
// ldelema T(ldloc array, call GetOffset(..., ldlen.i4(ldloc array)))
// -> withsystemindex.ldelema T(ldloc array, ...)
if (call.Arguments.Count != 2)
return false;
if (!(call.Arguments[1].MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array)))
return false;
context.Step("ldelema with System.Index", ldelema);
foreach (var node in call.Arguments[1].Descendants)
ldelema.AddILRange(node);
ldelema.AddILRange(call);
ldelema.WithSystemIndex = true;
// The method call had a `ref System.Index` argument for the this pointer, but we want a `System.Index` by-value.
ldelema.Indices[0] = new LdObj(call.Arguments[0], call.Method.DeclaringType);
return true;
} else if (index is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub && !bni.IsLifted && !bni.CheckForOverflow) {
// ldelema T(ldloc array, binary.sub.i4(ldlen.i4(ldloc array), ...))
// -> withsystemindex.ldelema T(ldloc array, newobj System.Index(..., fromEnd: true))
if (!(bni.Left.MatchLdLen(StackType.I4, out var arrayLoad) && arrayLoad.MatchLdLoc(array)))
return false;
var indexMethods = new IndexMethods(context.TypeSystem);
if (!indexMethods.IsValid)
return false; // don't use System.Index if not supported by the target framework
context.Step("ldelema indexed from end", ldelema);
foreach (var node in bni.Left.Descendants)
ldelema.AddILRange(node);
ldelema.AddILRange(bni);
ldelema.WithSystemIndex = true;
ldelema.Indices[0] = MakeIndex(IndexKind.FromEnd, bni.Right, indexMethods);
return true;
}
return false;
}
class IndexMethods
{
public readonly IMethod IndexCtor;
public readonly IMethod IndexImplicitConv;
public readonly IMethod RangeCtor;
public IType IndexType => IndexCtor?.DeclaringType;
public IType RangeType => RangeCtor?.DeclaringType;
public bool IsValid => IndexCtor != null && IndexImplicitConv != null && RangeCtor != null;
public readonly IMethod RangeStartAt;
public readonly IMethod RangeEndAt;
public readonly IMethod RangeGetAll;
public IndexMethods(ICompilation compilation)
{
var indexType = compilation.FindType(KnownTypeCode.Index);
foreach (var ctor in indexType.GetConstructors(m => m.Parameters.Count == 2)) {
if (ctor.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)
&& ctor.Parameters[1].Type.IsKnownType(KnownTypeCode.Boolean)) {
this.IndexCtor = ctor;
}
}
foreach (var op in indexType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit")) {
if (op.Parameters.Count == 1 && op.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32)) {
this.IndexImplicitConv = op;
}
}
var rangeType = compilation.FindType(KnownTypeCode.Range);
foreach (var ctor in rangeType.GetConstructors(m => m.Parameters.Count == 2)) {
if (ctor.Parameters[0].Type.IsKnownType(KnownTypeCode.Index)
&& ctor.Parameters[1].Type.IsKnownType(KnownTypeCode.Index)) {
this.RangeCtor = ctor;
}
}
foreach (var m in rangeType.GetMethods(m => m.Parameters.Count == 1)) {
if (m.Parameters.Count == 1 && m.Parameters[0].Type.IsKnownType(KnownTypeCode.Index)) {
if (m.Name == "StartAt")
this.RangeStartAt = m;
else if (m.Name == "EndAt")
this.RangeEndAt = m;
}
}
foreach (var p in rangeType.GetProperties(p => p.IsStatic && p.Name == "All")) {
this.RangeGetAll = p.Getter;
}
}
}
void IStatementTransform.Run(Block block, int pos, StatementTransformContext context)
{
if (!context.Settings.Ranges)
return;
int startPos = pos;
ILVariable containerVar = null;
// The container length access may be a separate instruction, or it may be inline with the variable's use
if (MatchContainerLengthStore(block.Instructions[pos], out ILVariable containerLengthVar, ref containerVar)) {
// stloc containerLengthVar(call get_Length/get_Count(ldloc container))
pos++;
} else {
// Reset if MatchContainerLengthStore only had a partial match. MatchGetOffset() will then set `containerVar`.
containerLengthVar = null;
containerVar = null;
}
if (block.Instructions[pos].MatchStLoc(out var rangeVar, out var rangeVarInit) && rangeVar.Type.IsKnownType(KnownTypeCode.Range)) {
// stloc rangeVar(rangeVarInit)
pos++;
} else {
rangeVar = null;
rangeVarInit = null;
}
// stloc startOffsetVar(call GetOffset(startIndexLoad, ldloc length))
if (!block.Instructions[pos].MatchStLoc(out ILVariable startOffsetVar, out ILInstruction startOffsetVarInit))
return;
if (!(startOffsetVar.IsSingleDefinition && startOffsetVar.StackType == StackType.I4))
return;
var startIndexKind = MatchGetOffset(startOffsetVarInit, out ILInstruction startIndexLoad, containerLengthVar, ref containerVar);
pos++;
if (startOffsetVar.LoadCount == 1) {
TransformIndexing();
} else if (startOffsetVar.LoadCount == 2) {
// might be slicing: startOffset is used once for the slice length calculation, and once for the Slice() method call
TransformSlicing();
}
void TransformIndexing()
{
// complex_expr(call get_Item(ldloc container, ldloc startOffsetVar))
if (rangeVar != null)
return;
if (!(startOffsetVar.LoadInstructions.Single().Parent is CallInstruction call))
return;
if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter && call.Arguments.Count == 2) {
if (call.Method.AccessorOwner?.SymbolKind != SymbolKind.Indexer)
return;
if (call.Method.Parameters.Count != 1)
return;
} else if (call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Setter && call.Arguments.Count == 3) {
if (call.Method.AccessorOwner?.SymbolKind != SymbolKind.Indexer)
return;
if (call.Method.Parameters.Count != 2)
return;
} else if (IsSlicingMethod(call.Method)) {
TransformSlicing(sliceLengthWasMisdetectedAsStartOffset: true);
return;
} else {
return;
}
if (startIndexKind == IndexKind.FromStart) {
// FromStart is only relevant for slicing; indexing from the start does not involve System.Index at all.
return;
}
if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind)) {
return;
}
// startOffsetVar might be used deep inside a complex statement, ensure we can inline up to that point:
for (int i = startPos; i < pos; i++) {
if (!ILInlining.CanInlineInto(block.Instructions[pos], startOffsetVar, block.Instructions[i]))
return;
}
if (!call.Method.Parameters[0].Type.IsKnownType(KnownTypeCode.Int32))
return;
if (!MatchContainerVar(call.Arguments[0], ref containerVar))
return;
if (!call.Arguments[1].MatchLdLoc(startOffsetVar))
return;
var specialMethods = new IndexMethods(context.TypeSystem);
if (!specialMethods.IsValid)
return;
if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: false))
return;
context.Step($"{call.Method.Name} indexed with {startIndexKind}", call);
var newMethod = new SyntheticRangeIndexAccessor(call.Method, specialMethods.IndexType, slicing: false);
var newCall = CallInstruction.Create(call.OpCode, newMethod);
newCall.ConstrainedTo = call.ConstrainedTo;
newCall.ILStackWasEmpty = call.ILStackWasEmpty;
newCall.Arguments.Add(call.Arguments[0]);
newCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods));
newCall.Arguments.AddRange(call.Arguments.Skip(2));
newCall.AddILRange(call);
for (int i = startPos; i < pos; i++) {
newCall.AddILRange(block.Instructions[i]);
}
call.ReplaceWith(newCall);
block.Instructions.RemoveRange(startPos, pos - startPos);
}
void TransformSlicing(bool sliceLengthWasMisdetectedAsStartOffset = false)
{
ILVariable sliceLengthVar;
ILInstruction sliceLengthVarInit;
if (sliceLengthWasMisdetectedAsStartOffset) {
// Special case: when slicing without a start point, the slice length calculation is mis-detected as the start offset,
// and since it only has a single use, we end in TransformIndexing(), which then calls TransformSlicing
// on this code path.
sliceLengthVar = startOffsetVar;
sliceLengthVarInit = ((StLoc)sliceLengthVar.StoreInstructions.Single()).Value;
startOffsetVar = null;
startIndexLoad = new LdcI4(0);
startIndexKind = IndexKind.TheStart;
} else {
// stloc containerLengthVar(call get_Length(ldloc containerVar))
// stloc startOffset(call GetOffset(startIndexLoad, ldloc length))
// -- we are here --
// stloc sliceLengthVar(binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset))
// complex_expr(call Slice(ldloc containerVar, ldloc startOffset, ldloc sliceLength))
if (!block.Instructions[pos].MatchStLoc(out sliceLengthVar, out sliceLengthVarInit))
return;
pos++;
}
if (!(sliceLengthVar.IsSingleDefinition && sliceLengthVar.LoadCount == 1))
return;
if (!MatchSliceLength(sliceLengthVarInit, out IndexKind endIndexKind, out ILInstruction endIndexLoad, containerLengthVar, ref containerVar, startOffsetVar))
return;
if (!CheckContainerLengthVariableUseCount(containerLengthVar, startIndexKind, endIndexKind)) {
return;
}
if (rangeVar != null) {
if (!MatchIndexFromRange(startIndexKind, startIndexLoad, rangeVar, "get_Start"))
return;
if (!MatchIndexFromRange(endIndexKind, endIndexLoad, rangeVar, "get_End"))
return;
}
if (!(sliceLengthVar.LoadInstructions.Single().Parent is CallInstruction call))
return;
if (!IsSlicingMethod(call.Method))
return;
if (call.Arguments.Count != 3)
return;
if (!MatchContainerVar(call.Arguments[0], ref containerVar))
return;
if (startOffsetVar == null) {
Debug.Assert(startIndexKind == IndexKind.TheStart);
if (!call.Arguments[1].MatchLdcI4(0))
return;
} else {
if (!call.Arguments[1].MatchLdLoc(startOffsetVar))
return;
}
if (!call.Arguments[2].MatchLdLoc(sliceLengthVar))
return;
if (!CSharpWillGenerateIndexer(call.Method.DeclaringType, slicing: true))
return;
if (startIndexKind == IndexKind.FromStart && endIndexKind == IndexKind.FromStart) {
// It's possible we actually have a startIndex/endIndex that involves the container length,
// but we couldn't detect it yet because the statement initializing the containerLengthVar is
// not yet part of the region to be transformed.
// If we transform now, we'd end up with:
// int length = span.Length;
// Console.WriteLine(span[(length - GetInt(1))..(length - GetInt(2))].ToString());
// which is correct but unnecessarily complex.
// So we peek ahead at the next instruction to be transformed:
if (startPos > 0 && MatchContainerLengthStore(block.Instructions[startPos - 1], out _, ref containerVar)) {
// Looks like the transform would be able do to a better job including that previous instruction,
// so let's avoid transforming just yet.
return;
}
// Something similar happens with the rangeVar:
if (startPos > 0 && block.Instructions[startPos - 1] is StLoc stloc && stloc.Variable.Type.IsKnownType(KnownTypeCode.Range)) {
return;
}
}
var specialMethods = new IndexMethods(context.TypeSystem);
if (!specialMethods.IsValid)
return;
context.Step($"{call.Method.Name} sliced with {startIndexKind}..{endIndexKind}", call);
var newMethod = new SyntheticRangeIndexAccessor(call.Method, specialMethods.RangeType, slicing: true);
var newCall = CallInstruction.Create(call.OpCode, newMethod);
newCall.ConstrainedTo = call.ConstrainedTo;
newCall.ILStackWasEmpty = call.ILStackWasEmpty;
newCall.Arguments.Add(call.Arguments[0]);
if (rangeVar != null) {
newCall.Arguments.Add(rangeVarInit);
} else if (startIndexKind == IndexKind.TheStart && endIndexKind == IndexKind.TheEnd && specialMethods.RangeGetAll != null) {
newCall.Arguments.Add(new Call(specialMethods.RangeGetAll));
} else if (startIndexKind == IndexKind.TheStart && specialMethods.RangeEndAt != null) {
var rangeCtorCall = new Call(specialMethods.RangeEndAt);
rangeCtorCall.Arguments.Add(MakeIndex(endIndexKind, endIndexLoad, specialMethods));
newCall.Arguments.Add(rangeCtorCall);
} else if (endIndexKind == IndexKind.TheEnd && specialMethods.RangeStartAt != null) {
var rangeCtorCall = new Call(specialMethods.RangeStartAt);
rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods));
newCall.Arguments.Add(rangeCtorCall);
} else {
var rangeCtorCall = new NewObj(specialMethods.RangeCtor);
rangeCtorCall.Arguments.Add(MakeIndex(startIndexKind, startIndexLoad, specialMethods));
rangeCtorCall.Arguments.Add(MakeIndex(endIndexKind, endIndexLoad, specialMethods));
newCall.Arguments.Add(rangeCtorCall);
}
newCall.AddILRange(call);
for (int i = startPos; i < pos; i++) {
newCall.AddILRange(block.Instructions[i]);
}
call.ReplaceWith(newCall);
block.Instructions.RemoveRange(startPos, pos - startPos);
}
}
static bool IsSlicingMethod(IMethod method)
{
if (method.IsExtensionMethod)
return false;
if (method.Parameters.Count != 2)
return false;
if (!method.Parameters.All(p => p.Type.IsKnownType(KnownTypeCode.Int32)))
return false;
return method.Name == "Slice"
|| (method.Name == "Substring" && method.DeclaringType.IsKnownType(KnownTypeCode.String));
}
/// <summary>
/// Check that the number of uses of the containerLengthVar variable matches those expected in the pattern.
/// </summary>
private bool CheckContainerLengthVariableUseCount(ILVariable containerLengthVar, IndexKind startIndexKind, IndexKind endIndexKind = IndexKind.FromStart)
{
int expectedUses = 0;
if (startIndexKind != IndexKind.FromStart && startIndexKind != IndexKind.TheStart)
expectedUses += 1;
if (endIndexKind != IndexKind.FromStart && endIndexKind != IndexKind.TheStart)
expectedUses += 1;
if (containerLengthVar != null) {
return containerLengthVar.LoadCount == expectedUses;
} else {
return expectedUses <= 1; // can have one inline use
}
}
/// <summary>
/// Matches 'addressof System.Index(call get_Start/get_End(ldloca rangeVar))'
/// </summary>
static bool MatchIndexFromRange(IndexKind indexKind, ILInstruction indexLoad, ILVariable rangeVar, string accessorName)
{
if (indexKind != IndexKind.RefSystemIndex)
return false;
if (!(indexLoad is AddressOf addressOf))
return false;
if (!addressOf.Type.IsKnownType(KnownTypeCode.Index))
return false;
if (!(addressOf.Value is Call call))
return false;
if (call.Method.Name != accessorName)
return false;
if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Range))
return false;
if (call.Arguments.Count != 1)
return false;
return call.Arguments[0].MatchLdLoca(rangeVar);
}
static ILInstruction MakeIndex(IndexKind indexKind, ILInstruction indexLoad, IndexMethods specialMethods)
{
if (indexKind == IndexKind.RefSystemIndex) {
// stloc containerLengthVar(call get_Length/get_Count(ldloc container))
// stloc startOffsetVar(call GetOffset(startIndexLoad, ldloc length))
// complex_expr(call get_Item(ldloc container, ldloc startOffsetVar))
// -->
// complex_expr(call get_Item(ldloc container, ldobj startIndexLoad))
return new LdObj(indexLoad, specialMethods.IndexType);
} else if (indexKind == IndexKind.FromEnd || indexKind == IndexKind.TheEnd) {
// stloc offsetVar(binary.sub.i4(call get_Length/get_Count(ldloc container), startIndexLoad))
// complex_expr(call get_Item(ldloc container, ldloc startOffsetVar))
// -->
// complex_expr(call get_Item(ldloc container, newobj System.Index(startIndexLoad, fromEnd: true)))
return new NewObj(specialMethods.IndexCtor) { Arguments = { indexLoad, new LdcI4(1) } };
} else {
Debug.Assert(indexKind == IndexKind.FromStart || indexKind == IndexKind.TheStart);
return new Call(specialMethods.IndexImplicitConv) { Arguments = { indexLoad } };
}
}
/// <summary>
/// Gets whether the C# compiler will call `container[int]` when using `container[Index]`.
/// </summary>
private bool CSharpWillGenerateIndexer(IType declaringType, bool slicing)
{
bool foundInt32Overload = false;
bool foundIndexOverload = false;
bool foundRangeOverload = false;
bool foundCountProperty = false;
foreach (var prop in declaringType.GetProperties(p => p.IsIndexer || (p.Name == "Length" || p.Name == "Count"))) {
if (prop.IsIndexer && prop.Parameters.Count == 1) {
var p = prop.Parameters[0];
if (p.Type.IsKnownType(KnownTypeCode.Int32)) {
foundInt32Overload = true;
} else if (p.Type.IsKnownType(KnownTypeCode.Index)) {
foundIndexOverload = true;
} else if (p.Type.IsKnownType(KnownTypeCode.Range)) {
foundRangeOverload = true;
}
} else if (prop.Name == "Length" || prop.Name == "Count") {
foundCountProperty = true;
}
}
if (slicing) {
return /* foundSlicingMethod && */ foundCountProperty && !foundRangeOverload;
} else {
return foundInt32Overload && foundCountProperty && !foundIndexOverload;
}
}
/// <summary>
/// Matches the instruction:
/// stloc containerLengthVar(call get_Length/get_Count(ldloc containerVar))
/// </summary>
static bool MatchContainerLengthStore(ILInstruction inst, out ILVariable lengthVar, ref ILVariable containerVar)
{
if (!inst.MatchStLoc(out lengthVar, out var init))
return false;
if (!(lengthVar.IsSingleDefinition && lengthVar.StackType == StackType.I4))
return false;
return MatchContainerLength(init, null, ref containerVar);
}
/// <summary>
/// If lengthVar is non-null, matches 'ldloc lengthVar'.
///
/// Otherwise, matches the instruction:
/// call get_Length/get_Count(ldloc containerVar)
/// </summary>
static bool MatchContainerLength(ILInstruction init, ILVariable lengthVar, ref ILVariable containerVar)
{
if (lengthVar != null) {
Debug.Assert(containerVar != null);
return init.MatchLdLoc(lengthVar);
}
if (!(init is CallInstruction call))
return false;
if (call.ResultType != StackType.I4)
return false;
if (!(call.Method.IsAccessor && call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter))
return false;
if (!(call.Method.AccessorOwner is IProperty lengthProp))
return false;
if (lengthProp.Name == "Length") {
// OK, Length is preferred
} else if (lengthProp.Name == "Count") {
// Also works, but only if the type doesn't have "Length"
if (lengthProp.DeclaringType.GetProperties(p => p.Name == "Length").Any())
return false;
}
if (!lengthProp.ReturnType.IsKnownType(KnownTypeCode.Int32))
return false;
if (lengthProp.IsVirtual && call.OpCode != OpCode.CallVirt)
return false;
if (call.Arguments.Count != 1)
return false;
return MatchContainerVar(call.Arguments[0], ref containerVar);
}
static bool MatchContainerVar(ILInstruction inst, ref ILVariable containerVar)
{
if (containerVar != null) {
return inst.MatchLdLoc(containerVar) || inst.MatchLdLoca(containerVar);
} else {
return inst.MatchLdLoc(out containerVar) || inst.MatchLdLoca(out containerVar);
}
}
enum IndexKind
{
/// <summary>
/// indexLoad is an integer, from the start of the container
/// </summary>
FromStart,
/// <summary>
/// indexLoad is loading the address of a System.Index struct
/// </summary>
RefSystemIndex,
/// <summary>
/// indexLoad is an integer, from the end of the container
/// </summary>
FromEnd,
/// <summary>
/// Always equivalent to `0`, used for the start-index when slicing without a startpoint `a[..end]`
/// </summary>
TheStart,
/// <summary>
/// Always equivalent to `^0`, used for the end-index when slicing without an endpoint `a[start..]`
/// </summary>
TheEnd,
}
/// <summary>
/// Matches an instruction computing an offset:
/// call System.Index.GetOffset(indexLoad, ldloc containerLengthVar)
/// or
/// binary.sub.i4(ldloc containerLengthVar, indexLoad)
///
/// Anything else not matching these patterns is interpreted as an `int` expression from the start of the container.
/// </summary>
static IndexKind MatchGetOffset(ILInstruction inst, out ILInstruction indexLoad,
ILVariable containerLengthVar, ref ILVariable containerVar)
{
indexLoad = inst;
if (MatchContainerLength(inst, containerLengthVar, ref containerVar)) {
indexLoad = new LdcI4(0);
return IndexKind.TheEnd;
} else if (inst is CallInstruction call) {
// call System.Index.GetOffset(indexLoad, ldloc containerLengthVar)
if (call.Method.Name != "GetOffset")
return IndexKind.FromStart;
if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.Index))
return IndexKind.FromStart;
if (call.Arguments.Count != 2)
return IndexKind.FromStart;
if (!MatchContainerLength(call.Arguments[1], containerLengthVar, ref containerVar))
return IndexKind.FromStart;
indexLoad = call.Arguments[0];
return IndexKind.RefSystemIndex;
} else if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) {
if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted)
return IndexKind.FromStart;
// binary.sub.i4(ldloc containerLengthVar, indexLoad)
if (!MatchContainerLength(bni.Left, containerLengthVar, ref containerVar))
return IndexKind.FromStart;
indexLoad = bni.Right;
return IndexKind.FromEnd;
} else {
return IndexKind.FromStart;
}
}
/// <summary>
/// Matches an instruction computing a slice length:
/// binary.sub.i4(call GetOffset(endIndexLoad, ldloc length), ldloc startOffset))
/// </summary>
static bool MatchSliceLength(ILInstruction inst, out IndexKind endIndexKind, out ILInstruction endIndexLoad, ILVariable containerLengthVar, ref ILVariable containerVar, ILVariable startOffsetVar)
{
endIndexKind = default;
endIndexLoad = default;
if (inst is BinaryNumericInstruction bni && bni.Operator == BinaryNumericOperator.Sub) {
if (bni.CheckForOverflow || bni.ResultType != StackType.I4 || bni.IsLifted)
return false;
if (startOffsetVar == null) {
// When slicing without explicit start point: `a[..endIndex]`
if (!bni.Right.MatchLdcI4(0))
return false;
} else {
if (!bni.Right.MatchLdLoc(startOffsetVar))
return false;
}
endIndexKind = MatchGetOffset(bni.Left, out endIndexLoad, containerLengthVar, ref containerVar);
return true;
} else if (startOffsetVar == null) {
// When slicing without explicit start point: `a[..endIndex]`, the compiler doesn't always emit the "- 0".
endIndexKind = MatchGetOffset(inst, out endIndexLoad, containerLengthVar, ref containerVar);
return true;
} else {
return false;
}
}
}
}

15
ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs

@ -114,7 +114,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
value = ldFlda.Target; value = ldFlda.Target;
} }
if (value.OpCode != OpCode.LdLoca) { 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; return AddressUse.Unknown;
} }
foreach (var load in stloc.Variable.LoadInstructions) { foreach (var load in stloc.Variable.LoadInstructions) {
@ -132,14 +133,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// Address is passed to method. // Address is passed to method.
// We'll assume the method only uses the address locally, // We'll assume the method only uses the address locally,
// unless we can see an address being returned from the method: // unless we can see an address being returned from the method:
if (call is NewObj) { IType returnType = (call is NewObj) ? call.Method.DeclaringType : call.Method.ReturnType;
if (call.Method.DeclaringType.IsByRefLike) { 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; return AddressUse.Unknown;
}
} else {
if (call.Method.ReturnType.IsByRefLike) {
return AddressUse.Unknown;
}
} }
foreach (var p in call.Method.Parameters) { foreach (var p in call.Method.Parameters) {
// catch "out Span<int>" and similar // catch "out Span<int>" and similar

137
ICSharpCode.Decompiler/TypeSystem/Implementation/SyntheticRangeIndexer.cs

@ -0,0 +1,137 @@
// Copyright (c) 2020 Daniel Grunwald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using ICSharpCode.Decompiler.Util;
using System.Reflection;
using System.Reflection.Metadata;
using System.Diagnostics;
using System.Linq;
namespace ICSharpCode.Decompiler.TypeSystem.Implementation
{
/// <summary>
/// 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.
/// </summary>
class SyntheticRangeIndexAccessor : IMethod
{
/// <summary>
/// The underlying method: `get_Item(int)`, `set_Item(int, T)` or `Slice(int, int)`.
/// </summary>
readonly IMethod underlyingMethod;
readonly IType indexOrRangeType;
readonly IReadOnlyList<IParameter> parameters;
readonly bool slicing;
public SyntheticRangeIndexAccessor(IMethod underlyingMethod, IType indexOrRangeType, bool slicing)
{
Debug.Assert(underlyingMethod != null);
Debug.Assert(indexOrRangeType != null);
this.underlyingMethod = underlyingMethod;
this.indexOrRangeType = indexOrRangeType;
this.slicing = slicing;
var parameters = new List<IParameter>();
parameters.Add(new DefaultParameter(indexOrRangeType, ""));
if (slicing) {
Debug.Assert(underlyingMethod.Parameters.Count == 2);
} else {
parameters.AddRange(underlyingMethod.Parameters.Skip(1));
}
this.parameters = parameters;
}
public bool IsSlicing => slicing;
bool IMethod.ReturnTypeIsRefReadOnly => underlyingMethod.ReturnTypeIsRefReadOnly;
bool IMethod.ThisIsRefReadOnly => underlyingMethod.ThisIsRefReadOnly;
IReadOnlyList<ITypeParameter> IMethod.TypeParameters => EmptyList<ITypeParameter>.Instance;
IReadOnlyList<IType> IMethod.TypeArguments => EmptyList<IType>.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<IParameter> IParameterizedMember.Parameters => parameters;
IMember IMember.MemberDefinition => underlyingMethod.MemberDefinition;
IType IMember.ReturnType => underlyingMethod.ReturnType;
IEnumerable<IMember> IMember.ExplicitlyImplementedInterfaceMembers => EmptyList<IMember>.Instance;
bool IMember.IsExplicitInterfaceImplementation => false;
bool IMember.IsVirtual => underlyingMethod.IsVirtual;
bool IMember.IsOverride => underlyingMethod.IsOverride;
bool IMember.IsOverridable => underlyingMethod.IsOverridable;
TypeParameterSubstitution IMember.Substitution => underlyingMethod.Substitution;
EntityHandle IEntity.MetadataToken => underlyingMethod.MetadataToken;
public string Name => underlyingMethod.Name;
IType IEntity.DeclaringType => underlyingMethod.DeclaringType;
ITypeDefinition IEntity.DeclaringTypeDefinition => underlyingMethod.DeclaringTypeDefinition;
IModule IEntity.ParentModule => underlyingMethod.ParentModule;
Accessibility IEntity.Accessibility => underlyingMethod.Accessibility;
bool IEntity.IsStatic => underlyingMethod.IsStatic;
bool IEntity.IsAbstract => underlyingMethod.IsAbstract;
bool IEntity.IsSealed => underlyingMethod.IsSealed;
SymbolKind ISymbol.SymbolKind => SymbolKind.Method;
ICompilation ICompilationProvider.Compilation => underlyingMethod.Compilation;
string INamedElement.FullName => underlyingMethod.FullName;
string INamedElement.ReflectionName => underlyingMethod.ReflectionName;
string INamedElement.Namespace => underlyingMethod.Namespace;
public override bool Equals(object obj)
{
return obj is SyntheticRangeIndexAccessor g
&& this.underlyingMethod.Equals(g.underlyingMethod)
&& this.indexOrRangeType.Equals(g.indexOrRangeType)
&& this.slicing == g.slicing;
}
public override int GetHashCode()
{
return underlyingMethod.GetHashCode() ^ indexOrRangeType.GetHashCode();
}
bool IMember.Equals(IMember obj, TypeVisitor typeNormalization)
{
return obj is SyntheticRangeIndexAccessor g
&& this.underlyingMethod.Equals(g.underlyingMethod, typeNormalization)
&& this.indexOrRangeType.AcceptVisitor(typeNormalization).Equals(g.indexOrRangeType.AcceptVisitor(typeNormalization));
}
IEnumerable<IAttribute> IEntity.GetAttributes() => underlyingMethod.GetAttributes();
IEnumerable<IAttribute> IMethod.GetReturnTypeAttributes() => underlyingMethod.GetReturnTypeAttributes();
IMethod IMethod.Specialize(TypeParameterSubstitution substitution)
{
return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing);
}
IMember IMember.Specialize(TypeParameterSubstitution substitution)
{
return new SyntheticRangeIndexAccessor(underlyingMethod.Specialize(substitution), indexOrRangeType, slicing);
}
}
}

8
ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs

@ -147,6 +147,10 @@ namespace ICSharpCode.Decompiler.TypeSystem
IAsyncEnumerableOfT, IAsyncEnumerableOfT,
/// <summary><c>System.Collections.Generic.IAsyncEnumerator{T}</c></summary> /// <summary><c>System.Collections.Generic.IAsyncEnumerator{T}</c></summary>
IAsyncEnumeratorOfT, IAsyncEnumeratorOfT,
/// <summary><c>System.Index</c></summary>
Index,
/// <summary><c>System.Range</c></summary>
Range
} }
/// <summary> /// <summary>
@ -155,7 +159,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
[Serializable] [Serializable]
public sealed class KnownTypeReference : ITypeReference 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] { static readonly KnownTypeReference[] knownTypeReferences = new KnownTypeReference[KnownTypeCodeCount] {
null, // None null, // None
@ -218,6 +222,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
new KnownTypeReference(KnownTypeCode.Unsafe, TypeKind.Class, "System.Runtime.CompilerServices", "Unsafe", 0), 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.IAsyncEnumerableOfT, TypeKind.Interface, "System.Collections.Generic", "IAsyncEnumerable", 1),
new KnownTypeReference(KnownTypeCode.IAsyncEnumeratorOfT, TypeKind.Interface, "System.Collections.Generic", "IAsyncEnumerator", 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),
}; };
/// <summary> /// <summary>

9
ILSpy/Properties/Resources.Designer.cs generated

@ -938,6 +938,15 @@ namespace ICSharpCode.ILSpy.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Ranges.
/// </summary>
public static string DecompilerSettings_Ranges {
get {
return ResourceManager.GetString("DecompilerSettings.Ranges", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Read-only methods. /// Looks up a localized string similar to Read-only methods.
/// </summary> /// </summary>

3
ILSpy/Properties/Resources.resx

@ -852,6 +852,9 @@ Do you want to continue?</value>
Do you want to continue?</value> Do you want to continue?</value>
</data> </data>
<data name="DecompilerSettings.Ranges" xml:space="preserve">
<value>Ranges</value>
</data>
<data name="AddPreconfiguredList" xml:space="preserve"> <data name="AddPreconfiguredList" xml:space="preserve">
<value>Add preconfigured list...</value> <value>Add preconfigured list...</value>
</data> </data>

Loading…
Cancel
Save