Browse Source

Merge pull request #3467 from icsharpcode/feature/inlinearrays

Part 1 of support for C# 12 InlineArray
master
Siegfried Pammer 5 days ago committed by GitHub
parent
commit
83c34d4e53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  3. 32
      ICSharpCode.Decompiler.Tests/TestCases/Correctness/Conversions.cs
  4. 155
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs
  5. 36
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs
  6. 17
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  7. 12
      ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
  8. 21
      ICSharpCode.Decompiler/DecompilerSettings.cs
  9. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  10. 149
      ICSharpCode.Decompiler/IL/Instructions.cs
  11. 3
      ICSharpCode.Decompiler/IL/Instructions.tt
  12. 5
      ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
  13. 23
      ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
  14. 245
      ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs
  15. 24
      ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs
  16. 5
      ICSharpCode.Decompiler/Semantics/Conversion.cs
  17. 7
      ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
  18. 26
      ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

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

@ -140,6 +140,7 @@ @@ -140,6 +140,7 @@
<Compile Include="TestCases\ILPretty\MonoFixed.cs" />
<Compile Include="TestCases\Pretty\Comparisons.cs" />
<Compile Include="TestCases\Pretty\GloballyQualifiedTypeInStringInterpolation.cs" />
<Compile Include="TestCases\Pretty\InlineArrayTests.cs" />
<Compile Include="TestCases\Pretty\Issue3406.cs" />
<Compile Include="TestCases\Pretty\Issue3439.cs" />
<Compile Include="TestCases\Pretty\Issue3442.cs" />

6
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -755,6 +755,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -755,6 +755,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task InlineArrayTests([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action<DecompilerSettings> configureDecompiler = null)
{
await Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, configureDecompiler);

32
ICSharpCode.Decompiler.Tests/TestCases/Correctness/Conversions.cs

@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
// #include "../../../ICSharpCode.Decompiler/Util/CSharpPrimitiveCast.cs"
using System;
using System.Runtime.CompilerServices;
using ICSharpCode.Decompiler.Util;
@ -111,6 +112,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness @@ -111,6 +112,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
Console.WriteLine(ReadZeroTerminatedString("Hello World!".Length));
C1.Test();
#if ROSLYN2 && !NET40
C3.Run();
#endif
}
static void RunTest(bool checkForOverflow)
@ -199,4 +203,32 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness @@ -199,4 +203,32 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
return new C1();
}
}
#if ROSLYN2 && !NET40
class C3
{
[InlineArray(4)] struct MyArray { private int elem; }
static void Foo(object o)
{
Console.WriteLine("Foo(object) called");
}
static void Foo(ReadOnlySpan<int> o)
{
Console.WriteLine("Foo(ReadOnlySpan<int>) called");
}
static void Test(MyArray arr)
{
Foo((object)arr);
}
public static void Run()
{
Console.WriteLine("C3.Run() called");
Test(default);
}
}
#endif
}

155
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
using System;
using System.Runtime.CompilerServices;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
public class InlineArrayTests
{
[InlineArray(16)]
public struct Byte16
{
private byte elem;
}
[InlineArray(16)]
public struct Generic16<T>
{
private T elem;
}
public byte Byte0()
{
return GetByte16()[0];
}
public byte GenericByte0()
{
return GetGeneric<byte>()[0];
}
public byte Byte5()
{
return GetByte16()[5];
}
public byte GenericByte5()
{
return GetGeneric<byte>()[5];
}
public byte ByteN()
{
return GetByte16()[GetIndex()];
}
public byte GenericByteN()
{
return GetGeneric<byte>()[GetIndex()];
}
public byte Byte0(Byte16 array, byte value)
{
return array[0] = value;
}
public byte GenericByte0(Generic16<byte> array, byte value)
{
return array[0] = value;
}
public byte Byte5(Byte16 array, byte value)
{
return array[5] = value;
}
public byte GenericByte5(Generic16<byte> array, byte value)
{
return array[5] = value;
}
public byte ByteN(Byte16 array, byte value)
{
return array[GetIndex()] = value;
}
public byte GenericByteN(Generic16<byte> array, byte value)
{
return array[GetIndex()] = value;
}
public void Slice(Byte16 array)
{
Receiver(array[..8]);
Receiver((ReadOnlySpan<byte>)array[..8]);
ReceiverSpan(array[..8]);
ReceiverReadOnlySpan(array[..8]);
}
// TODO
//public void Slice(Byte16 array, int end)
//{
// Receiver(array[..end]);
// Receiver((ReadOnlySpan<byte>)array[..end]);
// ReceiverSpan(array[..end]);
// ReceiverReadOnlySpan(array[..end]);
//}
public byte VariableSplitting(Byte16 array, byte value)
{
return array[GetIndex()] = (array[GetIndex() + 1] = value);
}
public void OverloadResolution()
{
Receiver(GetByte16());
Receiver((object)GetByte16());
Byte16 buffer = GetByte16();
Receiver((Span<byte>)buffer);
Byte16 buffer2 = GetByte16();
Receiver((ReadOnlySpan<byte>)buffer2);
Byte16 buffer3 = GetByte16();
ReceiverSpan(buffer3);
Byte16 buffer4 = GetByte16();
ReceiverReadOnlySpan(buffer4);
}
public Byte16 GetByte16()
{
return default(Byte16);
}
public Generic16<T> GetGeneric<T>()
{
return default(Generic16<T>);
}
public int GetIndex()
{
return 0;
}
public void Receiver(Span<byte> span)
{
}
public void Receiver(ReadOnlySpan<byte> span)
{
}
public void Receiver(Byte16 span)
{
}
public void Receiver(object span)
{
}
public void ReceiverSpan(Span<byte> span)
{
}
public void ReceiverReadOnlySpan(ReadOnlySpan<byte> span)
{
}
}
}

36
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -463,6 +463,42 @@ namespace ICSharpCode.Decompiler.CSharp @@ -463,6 +463,42 @@ namespace ICSharpCode.Decompiler.CSharp
return HandleImplicitConversion(method, argumentList.Arguments[0]);
}
if (settings.InlineArrays
&& method is { DeclaringType.FullName: "<PrivateImplementationDetails>", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" }
&& argumentList.Length == 2)
{
argumentList.CheckNoNamedOrOptionalArguments();
var arrayType = method.TypeArguments[0];
var arrayLength = arrayType.GetInlineArrayLength();
var arrayElementType = arrayType.GetInlineArrayElementType();
var argument = argumentList.Arguments[0];
var spanLengthExpr = argumentList.Arguments[1];
var targetType = method.ReturnType;
var spanType = typeSystem.FindType(KnownTypeCode.SpanOfT);
if (argument.Expression is DirectionExpression { FieldDirection: FieldDirection.In or FieldDirection.Ref, Expression: var lvalueExpr })
{
// `(TargetType)(in arg)` is invalid syntax.
// Also, `f(in arg)` is invalid when there's an implicit conversion involved.
argument = argument.UnwrapChild(lvalueExpr);
}
if (spanLengthExpr.ResolveResult.ConstantValue is int spanLength && spanLength <= arrayLength)
{
if (spanLength < arrayLength)
{
argument = new IndexerExpression(argument.Expression, new BinaryOperatorExpression {
Operator = BinaryOperatorType.Range,
Right = spanLengthExpr.Expression
}).WithRR(new ResolveResult(new ParameterizedType(spanType, arrayElementType))).WithoutILInstruction();
if (targetType.IsKnownType(KnownTypeCode.SpanOfT))
{
return argument;
}
}
return new CastExpression(expressionBuilder.ConvertType(targetType), argument.Expression)
.WithRR(new ConversionResolveResult(targetType, argument.ResolveResult, Conversion.InlineArrayConversion));
}
}
if (settings.LiftNullables && method.Name == "GetValueOrDefault"
&& method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT)
&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean)

17
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -3130,6 +3130,23 @@ namespace ICSharpCode.Decompiler.CSharp @@ -3130,6 +3130,23 @@ namespace ICSharpCode.Decompiler.CSharp
.WithoutILInstruction().WithRR(new ByReferenceResolveResult(expr.ResolveResult, ReferenceKind.Ref));
}
protected internal override TranslatedExpression VisitLdElemaInlineArray(LdElemaInlineArray inst, TranslationContext context)
{
TranslatedExpression arrayExpr = TranslateTarget(
inst.Array,
nonVirtualInvocation: true,
memberStatic: false,
memberDeclaringType: inst.Type
);
var inlineArrayElementType = inst.Type.GetInlineArrayElementType();
IndexerExpression indexerExpr = new IndexerExpression(
arrayExpr, inst.Indices.Select(i => TranslateArrayIndex(i).Expression)
);
TranslatedExpression expr = indexerExpr.WithILInstruction(inst).WithRR(new ResolveResult(inlineArrayElementType));
return new DirectionExpression(FieldDirection.Ref, expr)
.WithoutILInstruction().WithRR(new ByReferenceResolveResult(expr.ResolveResult, ReferenceKind.Ref));
}
TranslatedExpression TranslateArrayIndex(ILInstruction i)
{
var input = Translate(i);

12
ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs

@ -142,7 +142,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -142,7 +142,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
if (c != Conversion.None)
return c;
}
if (resolveResult is InterpolatedStringResolveResult isrr)
if (resolveResult is InterpolatedStringResolveResult)
{
if (toType.IsKnownType(KnownTypeCode.IFormattable) || toType.IsKnownType(KnownTypeCode.FormattableString))
return Conversion.ImplicitInterpolatedStringConversion;
@ -230,12 +230,20 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -230,12 +230,20 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
if (c != Conversion.None)
return c;
}
if ((toType.IsKnownType(KnownTypeCode.SpanOfT) || toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
&& fromType.IsInlineArrayType())
{
var elementType = fromType.GetInlineArrayElementType();
var spanElementType = toType.TypeArguments[0];
if (IdentityConversion(elementType, spanElementType))
return Conversion.InlineArrayConversion;
}
return Conversion.None;
}
/// <summary>
/// Gets whether the type 'fromType' is convertible to 'toType'
/// using one of the conversions allowed when satisying constraints (§4.4.4)
/// using one of the conversions allowed when satisfying constraints (§4.4.4)
/// </summary>
public bool IsConstraintConvertible(IType fromType, IType toType)
{

21
ICSharpCode.Decompiler/DecompilerSettings.cs

@ -164,12 +164,13 @@ namespace ICSharpCode.Decompiler @@ -164,12 +164,13 @@ namespace ICSharpCode.Decompiler
{
refReadOnlyParameters = false;
usePrimaryConstructorSyntaxForNonRecordTypes = false;
inlineArrays = false;
}
}
public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
if (refReadOnlyParameters || usePrimaryConstructorSyntaxForNonRecordTypes)
if (refReadOnlyParameters || usePrimaryConstructorSyntaxForNonRecordTypes || inlineArrays)
return CSharp.LanguageVersion.CSharp12_0;
if (scopedRef || requiredMembers || numericIntPtr || utf8StringLiterals || unsignedRightShift || checkedOperators)
return CSharp.LanguageVersion.CSharp11_0;
@ -2053,6 +2054,24 @@ namespace ICSharpCode.Decompiler @@ -2053,6 +2054,24 @@ namespace ICSharpCode.Decompiler
}
}
bool inlineArrays = true;
/// <summary>
/// Gets/Sets whether C# 12.0 inline array uses should be transformed.
/// </summary>
[Category("C# 12.0 / VS 2022.8")]
[Description("DecompilerSettings.InlineArrays")]
public bool InlineArrays {
get { return inlineArrays; }
set {
if (inlineArrays != value)
{
inlineArrays = value;
OnPropertyChanged();
}
}
}
bool separateLocalVariableDeclarations = false;
/// <summary>

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -107,6 +107,7 @@ @@ -107,6 +107,7 @@
<Compile Include="DecompilationProgress.cs" />
<Compile Include="Disassembler\IEntityProcessor.cs" />
<Compile Include="Disassembler\SortByNameProcessor.cs" />
<Compile Include="IL\Transforms\InlineArrayTransform.cs" />
<Compile Include="IL\Transforms\RemoveUnconstrainedGenericReferenceTypeCheck.cs" />
<Compile Include="Metadata\MetadataFile.cs" />
<Compile Include="Metadata\ModuleReferenceMetadata.cs" />

149
ICSharpCode.Decompiler/IL/Instructions.cs

@ -191,6 +191,8 @@ namespace ICSharpCode.Decompiler.IL @@ -191,6 +191,8 @@ namespace ICSharpCode.Decompiler.IL
LdLen,
/// <summary>Load address of array element.</summary>
LdElema,
/// <summary>Load address of inline array element.</summary>
LdElemaInlineArray,
/// <summary>Retrieves a pinnable reference for the input object.
/// The input must be an object reference (O).
/// If the input is an array/string, evaluates to a reference to the first element/character, or to a null reference if the array is null or empty.
@ -4998,6 +5000,127 @@ namespace ICSharpCode.Decompiler.IL @@ -4998,6 +5000,127 @@ namespace ICSharpCode.Decompiler.IL
}
}
namespace ICSharpCode.Decompiler.IL
{
/// <summary>Load address of inline array element.</summary>
public sealed partial class LdElemaInlineArray : ILInstruction
{
public LdElemaInlineArray(IType type, ILInstruction array, params ILInstruction[] indices) : base(OpCode.LdElemaInlineArray)
{
this.type = type;
this.Array = array;
this.Indices = new InstructionCollection<ILInstruction>(this, 1);
this.Indices.AddRange(indices);
}
IType type;
/// <summary>Returns the type operand.</summary>
public IType Type {
get { return type; }
set { type = value; InvalidateFlags(); }
}
public static readonly SlotInfo ArraySlot = new SlotInfo("Array", canInlineInto: true);
ILInstruction array = null!;
public ILInstruction Array {
get { return this.array; }
set {
ValidateChild(value);
SetChildInstruction(ref this.array, value, 0);
}
}
public static readonly SlotInfo IndicesSlot = new SlotInfo("Indices", canInlineInto: true);
public InstructionCollection<ILInstruction> Indices { get; private set; }
protected sealed override int GetChildCount()
{
return 1 + Indices.Count;
}
protected sealed override ILInstruction GetChild(int index)
{
switch (index)
{
case 0:
return this.array;
default:
return this.Indices[index - 1];
}
}
protected sealed override void SetChild(int index, ILInstruction value)
{
switch (index)
{
case 0:
this.Array = value;
break;
default:
this.Indices[index - 1] = (ILInstruction)value;
break;
}
}
protected sealed override SlotInfo GetChildSlot(int index)
{
switch (index)
{
case 0:
return ArraySlot;
default:
return IndicesSlot;
}
}
public sealed override ILInstruction Clone()
{
var clone = (LdElemaInlineArray)ShallowClone();
clone.Array = this.array.Clone();
clone.Indices = new InstructionCollection<ILInstruction>(clone, 1);
clone.Indices.AddRange(this.Indices.Select(arg => (ILInstruction)arg.Clone()));
return clone;
}
public override StackType ResultType { get { return StackType.Ref; } }
/// <summary>Gets whether the 'readonly' prefix was applied to this instruction.</summary>
public bool IsReadOnly { get; set; }
protected override InstructionFlags ComputeFlags()
{
return array.Flags | Indices.Aggregate(InstructionFlags.None, (f, arg) => f | arg.Flags) | InstructionFlags.MayThrow;
}
public override InstructionFlags DirectFlags {
get {
return InstructionFlags.MayThrow;
}
}
public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
{
WriteILRange(output, options);
if (IsReadOnly)
output.Write("readonly.");
output.Write(OpCode);
output.Write(' ');
type.WriteTo(output);
output.Write('(');
this.array.WriteTo(output, options);
foreach (var indices in Indices)
{
output.Write(", ");
indices.WriteTo(output, options);
}
output.Write(')');
}
public override void AcceptVisitor(ILVisitor visitor)
{
visitor.VisitLdElemaInlineArray(this);
}
public override T AcceptVisitor<T>(ILVisitor<T> visitor)
{
return visitor.VisitLdElemaInlineArray(this);
}
public override T AcceptVisitor<C, T>(ILVisitor<C, T> visitor, C context)
{
return visitor.VisitLdElemaInlineArray(this, context);
}
protected internal override bool PerformMatch(ILInstruction? other, ref Patterns.Match match)
{
var o = other as LdElemaInlineArray;
return o != null && type.Equals(o.type) && this.array.PerformMatch(o.array, ref match) && Patterns.ListMatch.DoMatch(this.Indices, o.Indices, ref match) && IsReadOnly == o.IsReadOnly;
}
}
}
namespace ICSharpCode.Decompiler.IL
{
/// <summary>Retrieves a pinnable reference for the input object.
/// The input must be an object reference (O).
@ -7235,6 +7358,10 @@ namespace ICSharpCode.Decompiler.IL @@ -7235,6 +7358,10 @@ namespace ICSharpCode.Decompiler.IL
{
Default(inst);
}
protected internal virtual void VisitLdElemaInlineArray(LdElemaInlineArray inst)
{
Default(inst);
}
protected internal virtual void VisitGetPinnableReference(GetPinnableReference inst)
{
Default(inst);
@ -7641,6 +7768,10 @@ namespace ICSharpCode.Decompiler.IL @@ -7641,6 +7768,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst);
}
protected internal virtual T VisitLdElemaInlineArray(LdElemaInlineArray inst)
{
return Default(inst);
}
protected internal virtual T VisitGetPinnableReference(GetPinnableReference inst)
{
return Default(inst);
@ -8047,6 +8178,10 @@ namespace ICSharpCode.Decompiler.IL @@ -8047,6 +8178,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst, context);
}
protected internal virtual T VisitLdElemaInlineArray(LdElemaInlineArray inst, C context)
{
return Default(inst, context);
}
protected internal virtual T VisitGetPinnableReference(GetPinnableReference inst, C context)
{
return Default(inst, context);
@ -8223,6 +8358,7 @@ namespace ICSharpCode.Decompiler.IL @@ -8223,6 +8358,7 @@ namespace ICSharpCode.Decompiler.IL
"sizeof",
"ldlen",
"ldelema",
"ldelema.inlinearray",
"get.pinnable.reference",
"string.to.int",
"expression.tree.cast",
@ -8849,6 +8985,19 @@ namespace ICSharpCode.Decompiler.IL @@ -8849,6 +8985,19 @@ namespace ICSharpCode.Decompiler.IL
array = default(ILInstruction);
return false;
}
public bool MatchLdElemaInlineArray([NotNullWhen(true)] out IType? type, [NotNullWhen(true)] out ILInstruction? array)
{
var inst = this as LdElemaInlineArray;
if (inst != null)
{
type = inst.Type;
array = inst.Array;
return true;
}
type = default(IType);
array = default(ILInstruction);
return false;
}
public bool MatchGetPinnableReference([NotNullWhen(true)] out ILInstruction? argument, out IMethod? method)
{
var inst = this as GetPinnableReference;

3
ICSharpCode.Decompiler/IL/Instructions.tt

@ -290,6 +290,9 @@ @@ -290,6 +290,9 @@
CustomClassName("LdElema"), HasTypeOperand, CustomChildren(new [] { new ArgumentInfo("array"), new ArgumentInfo("indices") { IsCollection = true } }, true),
BoolFlag("WithSystemIndex"),
MayThrowIfNotDelayed, ResultType("Ref"), SupportsReadonlyPrefix),
new OpCode("ldelema.inlinearray", "Load address of inline array element.",
CustomClassName("LdElemaInlineArray"), HasTypeOperand, CustomChildren(new [] { new ArgumentInfo("array"), new ArgumentInfo("indices") { IsCollection = true } }, true),
MayThrow, 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
+ "If the input is an array/string, evaluates to a reference to the first element/character, or to a null reference if the array is null or empty." + Environment.NewLine

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

@ -293,9 +293,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -293,9 +293,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
context.Step("TransformRuntimeHelpersCreateSpanInitialization: single-dim", inst);
inst.ReplaceWith(replacement2);
replacement2.AcceptVisitor(this);
return;
}
base.VisitCall(inst);
if (context.Settings.InlineArrays && InlineArrayTransform.RunOnExpression(inst, context))
{
return;
}
TransformAssignment.HandleCompoundAssign(inst, context);
}

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

@ -333,6 +333,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -333,6 +333,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
throw new InvalidOperationException("invalid expression classification");
}
}
else if (loadInst.Parent is LdElemaInlineArray)
{
return true;
}
else if (IsPassedToReadOnlySpanOfCharCtor(loadInst))
{
// Always inlining is possible here, because it's an 'in' or 'ref readonly' parameter
@ -344,6 +348,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -344,6 +348,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// already inlined.
return true;
}
if (IsPassedToInlineArrayAsSpan(loadInst))
{
// Inlining is not allowed
return false;
}
else if (IsPassedToInParameter(loadInst))
{
if (options.HasFlag(InliningOptions.Aggressive))
@ -377,6 +386,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -377,6 +386,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
private static bool IsPassedToInlineArrayAsSpan(LdLoca loadInst)
{
if (loadInst.Parent is not Call call)
return false;
var method = call.Method;
var declaringType = method.DeclaringType;
return declaringType.ReflectionName == "<PrivateImplementationDetails>"
&& method.Name is "InlineArrayAsReadOnlySpan" or "InlineArrayAsSpan"
&& method.Parameters is [var arg, var length]
&& (method.ReturnType.IsKnownType(KnownTypeCode.SpanOfT) || method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
&& arg.Type is ByReferenceType
&& length.Type.IsKnownType(KnownTypeCode.Int32);
}
internal static bool MethodRequiresCopyForReadonlyLValue(IMethod method, IType constrainedTo = null)
{
if (method == null)

245
ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs

@ -0,0 +1,245 @@ @@ -0,0 +1,245 @@
// Copyright (c) 2025 Siegfried Pammer
//
// 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.
#nullable enable
using System.Diagnostics.CodeAnalysis;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.Decompiler.IL.Transforms
{
static class InlineArrayTransform
{
internal static bool RunOnExpression(Call inst, StatementTransformContext context)
{
if (MatchSpanIndexerWithInlineArrayAsSpan(inst, out var type, out var addr, out var index, out bool isReadOnly))
{
if (isReadOnly)
{
context.Step("call get_Item(addressof System.ReadOnlySpan{T}(call InlineArrayAsReadOnlySpan(addr)), index) -> readonly.ldelema.inlinearray(addr, index)", inst);
}
else
{
context.Step("call get_Item(addressof System.Span{T}(call InlineArrayAsSpan(addr)), index) -> ldelema.inlinearray(addr, index)", inst);
}
inst.ReplaceWith(new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst));
return true;
}
if (MatchInlineArrayElementRef(inst, out type, out addr, out index, out isReadOnly))
{
if (isReadOnly)
{
context.Step("call InlineArrayElementRefReadOnly(addr, index) -> readonly.ldelema.inlinearray(addr, index)", inst);
}
else
{
context.Step("call InlineArrayElementRef(addr, index) -> ldelema.inlinearray(addr, index)", inst);
}
inst.ReplaceWith(new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst));
return true;
}
if (MatchInlineArrayFirstElementRef(inst, out type, out addr, out isReadOnly))
{
if (isReadOnly)
{
context.Step("call InlineArrayFirstElementRefReadOnly(addr) -> readonly.ldelema.inlinearray(addr, ldc.i4 0)", inst);
}
else
{
context.Step("call InlineArrayFirstElementRef(addr) -> ldelema.inlinearray(addr, ldc.i4 0)", inst);
}
inst.ReplaceWith(new LdElemaInlineArray(type, addr, new LdcI4(0)) { IsReadOnly = isReadOnly }.WithILRange(inst));
return true;
}
return false;
}
/// <summary>
/// Matches call get_Item(addressof System.(ReadOnly)Span[[T]](call InlineArrayAs(ReadOnly)Span(addr, length)), index)
/// </summary>
static bool MatchSpanIndexerWithInlineArrayAsSpan(Call inst, [NotNullWhen(true)] out IType? type, [NotNullWhen(true)] out ILInstruction? addr, [NotNullWhen(true)] out ILInstruction? index, out bool isReadOnly)
{
isReadOnly = false;
type = null;
addr = null;
index = null;
if (MatchSpanGetItem(inst.Method, "ReadOnlySpan"))
{
isReadOnly = true;
if (inst.Arguments is not [AddressOf { Value: Call targetInst, Type: var typeInfo }, var indexInst])
return false;
if (!MatchInlineArrayHelper(targetInst.Method, "InlineArrayAsReadOnlySpan", out var inlineArrayType))
return false;
if (targetInst.Arguments is not [var addrInst, LdcI4 { Value: var length }])
return false;
if (length < 0 || length > inlineArrayType.GetInlineArrayLength())
return false;
type = inlineArrayType;
addr = addrInst;
index = indexInst;
return true;
}
else if (MatchSpanGetItem(inst.Method, "Span"))
{
if (inst.Arguments is not [AddressOf { Value: Call targetInst, Type: var typeInfo }, var indexInst])
return false;
if (!MatchInlineArrayHelper(targetInst.Method, "InlineArrayAsSpan", out var inlineArrayType))
return false;
if (targetInst.Arguments is not [var addrInst, LdcI4 { Value: var length }])
return false;
if (length < 0 || length > inlineArrayType.GetInlineArrayLength())
return false;
type = inlineArrayType;
addr = addrInst;
index = indexInst;
return true;
}
else
{
return false;
}
}
/// <summary>
/// Matches call InlineArrayElementRef(ReadOnly)(addr, index)
/// </summary>
static bool MatchInlineArrayElementRef(Call inst, [NotNullWhen(true)] out IType? type, [NotNullWhen(true)] out ILInstruction? addr, [NotNullWhen(true)] out ILInstruction? index, out bool isReadOnly)
{
type = null;
addr = null;
index = null;
isReadOnly = false;
if (inst.Arguments is not [var addrInst, LdcI4 { Value: var indexValue } indexInst])
return false;
addr = addrInst;
index = indexInst;
if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRef", out var inlineArrayType))
{
isReadOnly = false;
type = inlineArrayType;
}
else if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRefReadOnly", out inlineArrayType))
{
isReadOnly = true;
type = inlineArrayType;
}
else
{
return false;
}
if (indexValue < 0 || indexValue >= inlineArrayType.GetInlineArrayLength())
{
return false;
}
return true;
}
private static bool MatchInlineArrayFirstElementRef(Call inst, [NotNullWhen(true)] out IType? type, [NotNullWhen(true)] out ILInstruction? addr, out bool isReadOnly)
{
type = null;
addr = null;
isReadOnly = false;
if (inst.Arguments is not [var addrInst])
return false;
if (MatchInlineArrayHelper(inst.Method, "InlineArrayFirstElementRef", out var inlineArrayType))
{
isReadOnly = false;
type = inlineArrayType;
addr = addrInst;
return true;
}
if (MatchInlineArrayHelper(inst.Method, "InlineArrayFirstElementRefReadOnly", out inlineArrayType))
{
isReadOnly = true;
type = inlineArrayType;
addr = addrInst;
return true;
}
return false;
}
static bool MatchSpanGetItem(IMethod method, string typeName)
{
return method is {
IsStatic: false,
Name: "get_Item",
DeclaringType: { Namespace: "System", Name: string name, TypeParameterCount: 1, DeclaringType: null }
} && typeName == name;
}
static bool MatchInlineArrayHelper(IMethod method, string methodName, [NotNullWhen(true)] out IType? inlineArrayType)
{
inlineArrayType = null;
if (method is not {
IsStatic: true, Name: var name,
DeclaringType: { FullName: "<PrivateImplementationDetails>", TypeParameterCount: 0 },
TypeArguments: [var bufferType, _],
Parameters: var parameters
})
{
return false;
}
if (methodName != name)
return false;
if (methodName.Contains("FirstElement"))
{
if (parameters is not [{ Type: ByReferenceType { ElementType: var type } }])
return false;
if (!type.Equals(bufferType))
return false;
}
else
{
if (parameters is not [{ Type: ByReferenceType { ElementType: var type } }, { Type: var lengthOrIndexParameterType }])
return false;
if (!type.Equals(bufferType) || !lengthOrIndexParameterType.IsKnownType(KnownTypeCode.Int32))
return false;
}
inlineArrayType = bufferType;
return true;
}
}
}

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

@ -151,11 +151,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -151,11 +151,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
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;
// We exclude Span<T>.Item[int index] and ReadOnlySpan<T>.Item[int index], because it is known that this
// or members of this cannot be returned by the method.
if (!IsSpanOfTIndexerAccessor(call.Method))
{
// 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;
}
}
foreach (var p in call.Method.Parameters)
{
// catch "out Span<int>" and similar
@ -174,6 +180,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -174,6 +180,16 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return AddressUse.Immediate;
}
static bool IsSpanOfTIndexerAccessor(IMethod method)
{
var declaringType = method.DeclaringType;
if (!declaringType.IsKnownType(KnownTypeCode.SpanOfT)
&& !declaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
return false;
return method.AccessorOwner is IProperty { IsIndexer: true, Name: "Item", Parameters: [var param], ReturnType: ByReferenceType { ElementType: var rt } }
&& param.Type.IsKnownType(KnownTypeCode.Int32) && rt.Equals(declaringType.TypeArguments[0]);
}
/// <summary>
/// Given 'ldloc ref_local' and 'ldloca target; stloc ref_local', returns the ldloca.
/// This function must return a non-null LdLoca for every use of a SupportedRefLocal.

5
ICSharpCode.Decompiler/Semantics/Conversion.cs

@ -87,6 +87,11 @@ namespace ICSharpCode.Decompiler.Semantics @@ -87,6 +87,11 @@ namespace ICSharpCode.Decompiler.Semantics
/// </summary>
public static readonly Conversion ThrowExpressionConversion = new BuiltinConversion(true, 11);
/// <summary>
/// C# 12 inline array implicitly being converted to <see cref="System.Span{T}"/> or <see cref="System.ReadOnlySpan{T}"/>.
/// </summary>
public static readonly Conversion InlineArrayConversion = new BuiltinConversion(true, 12);
public static Conversion UserDefinedConversion(IMethod operatorMethod, bool isImplicit, Conversion conversionBeforeUserDefinedOperator, Conversion conversionAfterUserDefinedOperator, bool isLifted = false, bool isAmbiguous = false)
{
if (operatorMethod == null)

7
ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs

@ -112,11 +112,14 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -112,11 +112,14 @@ namespace ICSharpCode.Decompiler.TypeSystem
// C# 11 attributes:
RequiredAttribute,
// C# 12 attributes:
InlineArray,
}
public static class KnownAttributes
{
internal const int Count = (int)KnownAttribute.RequiredAttribute + 1;
internal const int Count = (int)KnownAttribute.InlineArray + 1;
static readonly TopLevelTypeName[] typeNames = new TopLevelTypeName[Count]{
default,
@ -186,6 +189,8 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -186,6 +189,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
new TopLevelTypeName("System.Runtime.CompilerServices", "PreserveBaseOverridesAttribute"),
// C# 11 attributes:
new TopLevelTypeName("System.Runtime.CompilerServices", "RequiredMemberAttribute"),
// C# 12 attributes:
new TopLevelTypeName("System.Runtime.CompilerServices", "InlineArrayAttribute"),
};
public static ref readonly TopLevelTypeName GetTypeName(this KnownAttribute attr)

26
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

@ -306,6 +306,32 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -306,6 +306,32 @@ namespace ICSharpCode.Decompiler.TypeSystem
}
}
public static bool IsInlineArrayType(this IType type)
{
if (type.Kind != TypeKind.Struct)
return false;
var td = type.GetDefinition();
if (td == null)
return false;
return td.HasAttribute(KnownAttribute.InlineArray);
}
public static int? GetInlineArrayLength(this IType type)
{
if (type.Kind != TypeKind.Struct)
return null;
var td = type.GetDefinition();
if (td == null)
return null;
var attr = td.GetAttribute(KnownAttribute.InlineArray);
return attr?.FixedArguments.FirstOrDefault().Value as int?;
}
public static IType GetInlineArrayElementType(this IType arrayType)
{
return arrayType?.GetFields(f => !f.IsStatic).SingleOrDefault()?.Type ?? SpecialType.UnknownType;
}
/// <summary>
/// Gets whether the type is the specified known type.
/// For generic known types, this returns true for any parameterization of the type (and also for the definition itself).

Loading…
Cancel
Save