From cb790c6d3881dfd0401181110312dcee3d0fe321 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Sat, 3 May 2025 13:29:19 +0200 Subject: [PATCH 1/6] Allow variable splitting of compiler-generated temporaries for Span<T>/ROS<T> --- .../IL/Transforms/SplitVariables.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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<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 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. From 79f7a188b0946295c89c62df53c3e0b77c1d2440 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Sun, 4 May 2025 08:54:45 +0200 Subject: [PATCH 2/6] 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 @@ <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" /> 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<DecompilerSettings> 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<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 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((Span<byte>)buffer3); + Byte16 buffer4 = GetByte16(); + ReceiverReadOnlySpan((ReadOnlySpan<byte>)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) + { + } + } +} 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: "<PrivateImplementationDetails>", 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; + + /// <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> 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 @@ <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" /> 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, /// <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 } } 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 { 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 == "<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) 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; + } + + /// <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]) + 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; + } + } + + /// <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, 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: "<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; + } + } +} 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 /// </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) From 667036c54ea8bc3f5f23e43bd5f4813b340f8c99 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Fri, 30 May 2025 20:05:42 +0200 Subject: [PATCH 3/6] Add support for InlineArrayConversion --- .../TestCases/Correctness/Conversions.cs | 32 +++++++++++++++++++ .../TestCases/Pretty/InlineArrayTests.cs | 4 +-- .../CSharp/Resolver/CSharpConversions.cs | 10 +++++- .../Implementation/KnownAttributes.cs | 7 +++- .../TypeSystem/TypeSystemExtensions.cs | 10 ++++++ 5 files changed, 59 insertions(+), 4 deletions(-) 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<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 } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs index 79b800c39..886947d31 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs @@ -91,9 +91,9 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty Byte16 buffer2 = GetByte16(); Receiver((ReadOnlySpan<byte>)buffer2); Byte16 buffer3 = GetByte16(); - ReceiverSpan((Span<byte>)buffer3); + ReceiverSpan(buffer3); Byte16 buffer4 = GetByte16(); - ReceiverReadOnlySpan((ReadOnlySpan<byte>)buffer4); + ReceiverReadOnlySpan(buffer4); } public Byte16 GetByte16() diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs index f190a554f..0d1789ea7 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs @@ -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 @field = fromType.GetFields(f => !f.IsStatic, GetMemberOptions.IgnoreInheritedMembers).FirstOrDefault(); + var spanElementType = toType.TypeArguments[0]; + if (field != null && IdentityConversion(field.ReturnType, 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) { 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..54cfaf9a4 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -306,6 +306,16 @@ 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); + } + /// <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). From 101ddf878396229d789ccb7b456c62a6fee69aa0 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Thu, 5 Jun 2025 21:57:53 +0200 Subject: [PATCH 4/6] Add support for constant slices of InlineArrays --- .../TestCases/Pretty/InlineArrayTests.cs | 17 +++++++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 21 ++++++++++++++++++- .../CSharp/ExpressionBuilder.cs | 7 +------ .../CSharp/Resolver/CSharpConversions.cs | 4 ++-- .../TypeSystem/TypeSystemExtensions.cs | 16 ++++++++++++++ 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs index 886947d31..ea7a74338 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs @@ -77,6 +77,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty 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); diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 298adf7c4..058eff4e3 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -468,16 +468,35 @@ namespace ICSharpCode.Decompiler.CSharp && 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); } - return new CastExpression(expressionBuilder.ConvertType(targetType), argument.Expression) + 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" diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 43fd7bb78..4fc23581c 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -3138,18 +3138,13 @@ namespace ICSharpCode.Decompiler.CSharp memberStatic: false, memberDeclaringType: inst.Type ); - var inlineArrayElementType = GetInlineArrayElementType(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)); - - IType GetInlineArrayElementType(IType arrayType) - { - return arrayType?.GetFields(f => !f.IsStatic).SingleOrDefault()?.Type ?? SpecialType.UnknownType; - } } TranslatedExpression TranslateArrayIndex(ILInstruction i) diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs index 0d1789ea7..6acbeaa49 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs @@ -233,9 +233,9 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver if ((toType.IsKnownType(KnownTypeCode.SpanOfT) || toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)) && fromType.IsInlineArrayType()) { - var @field = fromType.GetFields(f => !f.IsStatic, GetMemberOptions.IgnoreInheritedMembers).FirstOrDefault(); + var elementType = fromType.GetInlineArrayElementType(); var spanElementType = toType.TypeArguments[0]; - if (field != null && IdentityConversion(field.ReturnType, spanElementType)) + if (IdentityConversion(elementType, spanElementType)) return Conversion.InlineArrayConversion; } return Conversion.None; diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index 54cfaf9a4..6b9dcb352 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -316,6 +316,22 @@ namespace ICSharpCode.Decompiler.TypeSystem 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). From 47dd905ad026ab4360cbcc572e713ecf50c6f6e2 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Fri, 6 Jun 2025 19:46:10 +0200 Subject: [PATCH 5/6] Make InlineArrayTransform post-order --- .../IL/Transforms/ExpressionTransforms.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 90907922b..7d36fb9e2 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -277,11 +277,6 @@ 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)) { @@ -298,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); } From 298c247355eeafb687b7412b209e219d6235913e Mon Sep 17 00:00:00 2001 From: Siegfried Pammer <siegfriedpammer@gmail.com> Date: Sun, 15 Jun 2025 18:16:19 +0200 Subject: [PATCH 6/6] InlineArrayTransform: Add more bounds checking --- .../IL/Transforms/InlineArrayTransform.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs index 8631b09eb..3bb7b0427 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs @@ -93,7 +93,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!MatchInlineArrayHelper(targetInst.Method, "InlineArrayAsReadOnlySpan", out var inlineArrayType)) return false; - if (targetInst.Arguments is not [var addrInst, LdcI4]) + if (targetInst.Arguments is not [var addrInst, LdcI4 { Value: var length }]) + return false; + + if (length < 0 || length > inlineArrayType.GetInlineArrayLength()) return false; type = inlineArrayType; @@ -110,7 +113,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!MatchInlineArrayHelper(targetInst.Method, "InlineArrayAsSpan", out var inlineArrayType)) return false; - if (targetInst.Arguments is not [var addrInst, LdcI4]) + if (targetInst.Arguments is not [var addrInst, LdcI4 { Value: var length }]) + return false; + + if (length < 0 || length > inlineArrayType.GetInlineArrayLength()) return false; type = inlineArrayType; @@ -135,28 +141,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms index = null; isReadOnly = false; - if (inst.Arguments is not [var addrInst, var indexInst]) + 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; - addr = addrInst; - index = indexInst; - return true; } - - if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRefReadOnly", out inlineArrayType)) + else if (MatchInlineArrayHelper(inst.Method, "InlineArrayElementRefReadOnly", out inlineArrayType)) { isReadOnly = true; type = inlineArrayType; - addr = addrInst; - index = indexInst; - return true; + } + else + { + return false; } - 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)