Skip to content

Part 1 of support for C# 12 InlineArray #3467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

siegfriedpammer
Copy link
Member

No description provided.

@siegfriedpammer
Copy link
Member Author

@ds5678 this PR covers the basics of InlineArrays... before we can continue with params collections, we will have to add minimal support for collection expressions:

  • Only use collection expression syntax, if the array type is an inline array type (target-typed to span) or a single-element span

Would you be interested in taking this on?

See some experiments here: https://sharplab.io/#v2:CYLg1APgAgTAjAWAFBQAwAIpwHQCUCuAdgC4CWAtgKbYDCA9uQA6kA2lATgMocBupAxpQDOAbmRQAzJhjoa6AN7J0y9AG0AkoRalClAILt2AQwCeACgAsASgC6SlZPRDi7fP2LoAsic4u3xe2VFJBVQ9EZ2Uh4jYkp0HQ8AfTEQlQBfJUDMKSwMAAUjY3IhTkYjQk1tXQNjcysFLNCoAHZMOFRsACk6HTMAIgAadEHhoxG+gCNx/j6rFNCMkKzHXPRS8voWNndSOgrCUjIjbQAvDjN64LCHVtyunsJ+oZHVPrHnqeeZmzmsxdDllJvL5XO50AAxOh0C5ZK7XdAAegRmFaqjgQxgQwkQwsNhE6AA7nR8CxgIQAOQeInsADWQwmlH4RnwQjiOiqcUKtSE6EKcUIdA8fX4dC2jLIewAtJQAB4RYRCXaEdAiwjOUFkCZsPqNFTAvxgrmmdAAXnQwEoADNmSxiPN4UaTKpUDZTeg4Pbro60a6zTBPWFvTBfegJAHQt6JCGLOGbrzDKYA4s0kA

using System;
using System.Runtime.CompilerServices;
public class C {
    [InlineArray(4)]
    public struct MyStruct
    {
        private int _;
    }
 
    public string ParamsSpanInlineArray() {
        return string.Join(", ", "a", "b", "c");
    }
 
    public string SpanCollectionInitializer() {
        return string.Join(", ", ["a", "b", "c"]);
    }
    
    public MyStruct Foo()
    {
        // return [1, 2, 3, 4]; wouldn't work, because inline arrays are not "collection-expression constructible"
        MyStruct array = default;
        array[0] = 1;
        array[1] = 2;
        array[2] = 3;
        array[3] = 4;
        return array;
    }
}

@ds5678
Copy link
Contributor

ds5678 commented May 5, 2025

Would you be interested in taking this on?

Sure, I can work on this.

@@ -435,6 +435,29 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm, isDelegateInvocation: true));
}

if (settings.InlineArrays && method.DeclaringType.FullName == "<PrivateImplementationDetails>")
{
var unwrappedTarget = argumentList.Arguments[0].Expression;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argumentList.Arguments might be empty here.

case "InlineArrayAsReadOnlySpan":
var arrayResolveResult = unwrappedTarget.GetResolveResult();
unwrappedTarget.RemoveAnnotations<ResolveResult>();
return unwrappedTarget.Detach().WithRR(new InlineArrayResolveResult(arrayResolveResult, method.ReturnType));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make sense. A ResolveResult describes the semantics of a C# expression we have generated. If the C# expression doesn't change, it doesn't make sense to swap out the associated ResolveResult?

Copy link
Member

@dgrunwald dgrunwald May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that our conversion logic needs to consider all possible conversions (for overload resolution), not just the conversion actually picked in the IL program (that wouldn't allow us to detect if we're about to emit C# that would pick the wrong overload).

So InterpolatedStringResolveResult makes sense (it's associated with a special syntactic form of an argument expression); but InlineArrayResolveResult doesn't (there's no syntactic difference between an argument expression that is converted to span, and one that is passed as-is without conversion).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should we then represent the implicit conversion from an arbitrary struct type with the InlineArrayAttribute to (ReadOnly)Span<T>?

Given

[InlineArray(4)] struct MyArray { private int elem; }

MyArray GetArray() => default;

int GetIndex() => 0;

int item = GetArray()[GetIndex()];

the last expression is compiled down to (pseudo code, with some extra inlining):

int num = <PrivateImplementationDetails>.InlineArrayAsReadOnlySpan<MyArray, int>(GetArray(), 4)[GetIndex()];

what the transform does is: extract the first argument of InlineArrayAsReadOnlySpan swap the ResolveResult in order to account for the implicit conversion to ReadOnlySpan<int> and replace the call to InlineArrayAsReadOnlySpan with the first argument.

without the extra ResolveResult, we would get int num = ((ReadOnlySpan<int>)GetArray())[GetIndex()];.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your case is a bit of a red herring: the return value of GetArray() cannot be converted to ReadOnlySpan<int> (neither with an explicit cast nor with an implicit conversion), because this conversion only allows lvalues.
We need to treat that particular call combination (conversion to span followed by indexing) as a special case, maybe even introducing a special ILAst instruction for this combination.
For thinking about the conversion modelling, the indexing special case is actively misleading. There's it's more useful to consider Foo(some_array), where Foo has multiple overloads (some of which involve the conversion).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In particular for conversion modeling you also you need to think about cases where the IL does not perform the conversion to span, but the C# code might unless we add explicit casts.
For example: in this example the explicit cast to (object) is required because the overload would otherwise be ambiguous. If our conversion machinery does not consider the span conversion to be possible, OR would think that the cast is not required, and we generate invalid code. Thus our conversion machinery needs to support span conversions in general, not just for a special resolve result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants