From 79f7a188b0946295c89c62df53c3e0b77c1d2440 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 4 May 2025 08:54:45 +0200 Subject: [PATCH] Add support for C# 12 inline array expressions --- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../PrettyTestRunner.cs | 6 + .../TestCases/Pretty/InlineArrayTests.cs | 138 +++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 17 ++ .../CSharp/ExpressionBuilder.cs | 22 ++ .../CSharp/Resolver/CSharpConversions.cs | 2 +- ICSharpCode.Decompiler/DecompilerSettings.cs | 21 +- .../ICSharpCode.Decompiler.csproj | 1 + ICSharpCode.Decompiler/IL/Instructions.cs | 149 +++++++++++ ICSharpCode.Decompiler/IL/Instructions.tt | 3 + .../IL/Transforms/ExpressionTransforms.cs | 5 + .../IL/Transforms/ILInlining.cs | 23 ++ .../IL/Transforms/InlineArrayTransform.cs | 234 ++++++++++++++++++ .../Semantics/Conversion.cs | 5 + 14 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs create mode 100644 ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 1a0d83379..110b3bb36 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -140,6 +140,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 25c5e0191..12020c28b 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -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 configureDecompiler = null) { await Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, configureDecompiler); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs new file mode 100644 index 000000000..79b800c39 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs @@ -0,0 +1,138 @@ +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 + { + private T elem; + } + + public byte Byte0() + { + return GetByte16()[0]; + } + + public byte GenericByte0() + { + return GetGeneric()[0]; + } + + public byte Byte5() + { + return GetByte16()[5]; + } + + public byte GenericByte5() + { + return GetGeneric()[5]; + } + + public byte ByteN() + { + return GetByte16()[GetIndex()]; + } + + public byte GenericByteN() + { + return GetGeneric()[GetIndex()]; + } + + public byte Byte0(Byte16 array, byte value) + { + return array[0] = value; + } + + public byte GenericByte0(Generic16 array, byte value) + { + return array[0] = value; + } + + public byte Byte5(Byte16 array, byte value) + { + return array[5] = value; + } + + public byte GenericByte5(Generic16 array, byte value) + { + return array[5] = value; + } + + public byte ByteN(Byte16 array, byte value) + { + return array[GetIndex()] = value; + } + + public byte GenericByteN(Generic16 array, byte value) + { + return array[GetIndex()] = value; + } + + 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)buffer); + Byte16 buffer2 = GetByte16(); + Receiver((ReadOnlySpan)buffer2); + Byte16 buffer3 = GetByte16(); + ReceiverSpan((Span)buffer3); + Byte16 buffer4 = GetByte16(); + ReceiverReadOnlySpan((ReadOnlySpan)buffer4); + } + + public Byte16 GetByte16() + { + return default(Byte16); + } + + public Generic16 GetGeneric() + { + return default(Generic16); + } + + public int GetIndex() + { + return 0; + } + + public void Receiver(Span span) + { + } + + public void Receiver(ReadOnlySpan span) + { + } + + public void Receiver(Byte16 span) + { + } + + public void Receiver(object span) + { + } + + public void ReceiverSpan(Span span) + { + } + + public void ReceiverReadOnlySpan(ReadOnlySpan span) + { + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index b8b6680e5..298adf7c4 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -463,6 +463,23 @@ namespace ICSharpCode.Decompiler.CSharp return HandleImplicitConversion(method, argumentList.Arguments[0]); } + if (settings.InlineArrays + && method is { DeclaringType.FullName: "", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" } + && argumentList.Length == 2) + { + argumentList.CheckNoNamedOrOptionalArguments(); + var argument = argumentList.Arguments[0]; + var targetType = method.ReturnType; + 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); + } + 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) diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 13ec88dcc..43fd7bb78 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -3130,6 +3130,28 @@ 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 = GetInlineArrayElementType(inst.Type); + 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)); + + IType GetInlineArrayElementType(IType arrayType) + { + return arrayType?.GetFields(f => !f.IsStatic).SingleOrDefault()?.Type ?? SpecialType.UnknownType; + } + } + TranslatedExpression TranslateArrayIndex(ILInstruction i) { var input = Translate(i); diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs index 6b19a4762..f190a554f 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs @@ -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; diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 0ccfec2fa..bbf7a0055 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -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 } } + bool inlineArrays = true; + + /// + /// Gets/Sets whether C# 12.0 inline array uses should be transformed. + /// + [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; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 5e02ca705..7e2e19341 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -107,6 +107,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 4f3ef843d..4094f9c9e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -191,6 +191,8 @@ namespace ICSharpCode.Decompiler.IL LdLen, /// Load address of array element. LdElema, + /// Load address of inline array element. + LdElemaInlineArray, /// 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 } } namespace ICSharpCode.Decompiler.IL +{ + /// Load address of inline array element. + 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(this, 1); + this.Indices.AddRange(indices); + } + IType type; + /// Returns the type operand. + 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 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(clone, 1); + clone.Indices.AddRange(this.Indices.Select(arg => (ILInstruction)arg.Clone())); + return clone; + } + public override StackType ResultType { get { return StackType.Ref; } } + /// Gets whether the 'readonly' prefix was applied to this instruction. + 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(ILVisitor visitor) + { + return visitor.VisitLdElemaInlineArray(this); + } + public override T AcceptVisitor(ILVisitor 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 { /// Retrieves a pinnable reference for the input object. /// The input must be an object reference (O). @@ -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 { 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 { 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 "sizeof", "ldlen", "ldelema", + "ldelema.inlinearray", "get.pinnable.reference", "string.to.int", "expression.tree.cast", @@ -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; diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 6f42205af..8c2a5bf5e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -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 diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 57b475694..90907922b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -277,6 +277,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms protected internal override void VisitCall(Call inst) { + if (context.Settings.InlineArrays && InlineArrayTransform.RunOnExpression(inst, context)) + { + return; + } + if (NullableLiftingTransform.MatchGetValueOrDefault(inst, out var nullableValue, out var fallback) && SemanticHelper.IsPure(fallback.Flags)) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 2b92f6cc5..af52fd58b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -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 // 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 } } + 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 == "" + && 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) diff --git a/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs new file mode 100644 index 000000000..8631b09eb --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs @@ -0,0 +1,234 @@ +// 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; + } + + /// + /// Matches call get_Item(addressof System.(ReadOnly)Span[[T]](call InlineArrayAs(ReadOnly)Span(addr, length)), index) + /// + 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]) + 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]) + return false; + + type = inlineArrayType; + addr = addrInst; + index = indexInst; + + return true; + } + else + { + return false; + } + } + + /// + /// Matches call InlineArrayElementRef(ReadOnly)(addr, index) + /// + 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, var indexInst]) + return false; + + if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRef", out var inlineArrayType)) + { + isReadOnly = false; + type = inlineArrayType; + addr = addrInst; + index = indexInst; + return true; + } + + if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRefReadOnly", out inlineArrayType)) + { + isReadOnly = true; + type = inlineArrayType; + addr = addrInst; + index = indexInst; + return true; + } + + return false; + } + + 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: "", 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; + } + } +} diff --git a/ICSharpCode.Decompiler/Semantics/Conversion.cs b/ICSharpCode.Decompiler/Semantics/Conversion.cs index a92735b4f..4bcc1d766 100644 --- a/ICSharpCode.Decompiler/Semantics/Conversion.cs +++ b/ICSharpCode.Decompiler/Semantics/Conversion.cs @@ -87,6 +87,11 @@ namespace ICSharpCode.Decompiler.Semantics /// public static readonly Conversion ThrowExpressionConversion = new BuiltinConversion(true, 11); + /// + /// C# 12 inline array implicitly being converted to or . + /// + 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)