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/Correctness/Conversions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Conversions.cs index 01b76929d..59e94ed4b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Conversions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/Conversions.cs @@ -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 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 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 o) + { + Console.WriteLine("Foo(ReadOnlySpan) called"); + } + + static void Test(MyArray arr) + { + Foo((object)arr); + } + + public static void Run() + { + Console.WriteLine("C3.Run() called"); + Test(default); + } + } +#endif } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs new file mode 100644 index 000000000..ea7a74338 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs @@ -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 + { + 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 void Slice(Byte16 array) + { + Receiver(array[..8]); + Receiver((ReadOnlySpan)array[..8]); + ReceiverSpan(array[..8]); + ReceiverReadOnlySpan(array[..8]); + } + + // TODO + //public void Slice(Byte16 array, int end) + //{ + // Receiver(array[..end]); + // Receiver((ReadOnlySpan)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)buffer); + Byte16 buffer2 = GetByte16(); + Receiver((ReadOnlySpan)buffer2); + Byte16 buffer3 = GetByte16(); + ReceiverSpan(buffer3); + Byte16 buffer4 = GetByte16(); + ReceiverReadOnlySpan(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..058eff4e3 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -463,6 +463,42 @@ 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 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) diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 13ec88dcc..4fc23581c 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -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); diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs index 6b19a4762..6acbeaa49 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; @@ -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; } /// /// 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) /// public bool IsConstraintConvertible(IType fromType, IType toType) { 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..7d36fb9e2 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -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); } 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..3bb7b0427 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs @@ -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; + } + + /// + /// 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 { 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; + } + } + + /// + /// 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, 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: "", 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/IL/Transforms/SplitVariables.cs b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs index 4699f9f57..b6f336697 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs @@ -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.Item[int index] and ReadOnlySpan.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" and similar @@ -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]); + } + /// /// 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. 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) diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 59d2c19eb..a9e59e0d9 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -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 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) diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index d4009bb31..6b9dcb352 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -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; + } + /// /// 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).