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).