Skip to content
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

Collection expressions: use inline arrays for collection expressions with span target types #69412

Merged
merged 13 commits into from
Aug 24, 2023

Conversation

cston
Copy link
Member

@cston cston commented Aug 5, 2023

Use inline arrays for the code generated for collection expressions with Span<T> or ReadOnlySpan<T> target types; fall back to arrays on platforms that do not support inline arrays.

See collection-expressions.md#ref-safety.

Relates to test plan #66418

@cston cston marked this pull request as ready for review August 6, 2023 05:37
@cston cston requested a review from a team as a code owner August 6, 2023 05:37
@jaredpar jaredpar added this to the 17.8 milestone Aug 7, 2023
@jaredpar jaredpar added New Feature - Collection Expressions and removed untriaged Issues and PRs which have not yet been triaged by a lead labels Aug 9, 2023
@@ -618,6 +619,41 @@ public override BoundNode VisitRefTypeOperator(BoundRefTypeOperator node)
return node.Update(operand, getTypeFromHandle, type);
}

private BoundStatement? RewriteFieldOrPropertyInitializer(BoundStatement initializer)
{
var previousLocals = _additionalLocals;
Copy link
Member

@jcouv jcouv Aug 11, 2023

Choose a reason for hiding this comment

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

I didn't understand, why introduce this new _additionalLocals machinery, which affects every single block, when one can easily insert a new block during lowering of specific nodes when additional locals are needed? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

The inline array temporary needs to be added in the enclosing scope (so the lifetime of the temporary matches that scope) rather than being created in a new nested block.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Thanks much for the explanation. Consider adding a comment on the declaration of _additionalLocals (allows adding locals without inserting new blocks/scopes)

{
return statement;
}
return new BoundBlock(
Copy link
Member

@jcouv jcouv Aug 15, 2023

Choose a reason for hiding this comment

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

I'm a little worried about the fact that a Visit method besides VisitBlock has to handle additional locals.
How do we know whether any other Visit methods (current or future) also needs to handle that? Is there anything we could do to help remembering to handle? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't thought of an easy way to assert that _additionalLocals is always at the expected scope. If there are Visit methods that represent blocks but are not setting _additionalLocals, then there two possibilities. If the block is a top-level block, we should fail the assert in VisitArrayOrSpanCollectionExpression() that _additionalLocals is { }. Otherwise, _additionalLocals will represent the containing block of the actual block. And for the latter case, that should just mean that the inline array temporaries will have longer scope than is strictly necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding assert to LocalRewriter.VisitCollectionExpression() that _additionalLocals is { } to increase the chance of catching other cases.

Debug.Assert(elementType.Equals(spanType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0], TypeCompareKind.AllIgnoreOptions));

if (collectionTypeKind == CollectionExpressionTypeKind.ReadOnlySpan &&
ShouldUseRuntimeHelpersCreateSpan(node, elementType.Type))
Copy link
Member

@jcouv jcouv Aug 15, 2023

Choose a reason for hiding this comment

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

I'm not sure what all the requirements to be able to emit as a blob are. Is there any case where emitting as a blob could fail (thus fall back to a plain array instantiation) but we could emit as an inline array below? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

If the collection is a candidate to be emitted in the assembly data section - that is, if it is a ReadOnlySpan<T> where T is one of the expected primitive types, and the collection contains constant values only - then it will be emitted as global data. The only condition that should affect the generated code is whether the particular RuntimeHelpers.CreateSpan() optimization is available, but the result is still global data. See CollectionExpressionTests.RuntimeHelpers_CreateSpan for instance.

if (elements.Length > 0
&& !elements.Any(i => i is BoundCollectionExpressionSpreadElement)
&& _compilation.Assembly.RuntimeSupportsInlineArrayTypes
&& (!constructMethod.ReturnType.IsRefLikeType || constructMethod.Parameters[0].EffectiveScope == ScopedKind.ScopedValue))
Copy link
Member

@jcouv jcouv Aug 15, 2023

Choose a reason for hiding this comment

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

There used to be conditions related to ref-ness. Are those no longer needed? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

In lowering, we only need to check that the collection expression is targeting a span, has a known length, and is not empty, and the runtime supports inline arrays. If those conditions are met, we'll use an inline array. Any checks for ref safety are handled in ref safety pass before lowering.

Construct(ImmutableArray.Create(TypeWithAnnotations.Create(inlineArrayType), elementType));
return _factory.Call(
// or
// ... InlineArrayAsSpan<<>y__InlineArrayN<ElementType>, ElementType>(ref tmp, N)
Copy link
Member

Choose a reason for hiding this comment

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

I understand this is an implementation detail, but should this be mentioned in the spec?

Copy link
Member Author

Choose a reason for hiding this comment

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

should this be mentioned in the spec?

Which part are you referring to? That we are using an inline array?

@jcouv
Copy link
Member

jcouv commented Aug 16, 2023

                static void Create1() { {{spanType}}<int> s = []; s.Report(); }

nit: I didn't follow. What's the benefit of moving the .Report() call inside the methods? #Closed


In reply to: 1679779399


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:2675 in f3c7566. [](commit_id = f3c7566, deletion_comment = False)

@cston
Copy link
Member Author

cston commented Aug 16, 2023

                static void Create1() { {{spanType}}<int> s = []; s.Report(); }

With this change, a ref safety error will be reported returning the span instance since the collection expression has local scope.


In reply to: 1679779399


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:2675 in f3c7566. [](commit_id = f3c7566, deletion_comment = False)

@jcouv
Copy link
Member

jcouv commented Aug 16, 2023

            if (targetFramework == TargetFramework.Net80)

nit: should we be using a rolling target framework, instead of locking on .NET 8? However, it looks like we don't have a turn-key TargetFramework for that...


In reply to: 1679797271


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:10198 in f3c7566. [](commit_id = f3c7566, deletion_comment = False)

@cston
Copy link
Member Author

cston commented Aug 16, 2023

            if (targetFramework == TargetFramework.Net80)

Yes, we could use the latest target framework here instead, if we had such a definition.


In reply to: 1679797271


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:10198 in f3c7566. [](commit_id = f3c7566, deletion_comment = False)

@cston cston requested review from RikkiGibson and a team August 16, 2023 23:24
@RikkiGibson RikkiGibson self-assigned this Aug 17, 2023
Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

LGTM overall, had some comments and only skimmed the tests.

case CollectionExpressionTypeKind.CollectionBuilder:
// For a ref struct type with a builder method, the scope of the collection
// expression is the scope of an invocation of the builder method with the
// collection expression as the span argument.
Copy link
Contributor

Choose a reason for hiding this comment

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

pseudocode might be more clear than prose here. e.g. RS rs = [x, y, z] is equivalent to RS rs = RSFactory.Create((ReadOnlySpan<...>)[x, y, z])

if (constructMethod is not { Parameters: [{ RefKind: RefKind.None } parameter] })
{
// Unexpected construct method. Restrict the collection to local scope.
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

we expect an error to be reported elsewhere for this?

Copy link
Member Author

Choose a reason for hiding this comment

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

We should have filtered out the unexpected collection builder method in Binder.GetCollectionBuilderMethod(). The case here should not be hit.

{
var collectionTypeKind = ConversionsBase.GetCollectionExpressionTypeKind(_compilation, expr.Type, out var elementType);

switch (collectionTypeKind)
Copy link
Contributor

Choose a reason for hiding this comment

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

does this handle spreads correctly? Maybe we never expect to get any of the below TypeKinds when spreads are present? Might be worth asserting.

Copy link
Member Author

Choose a reason for hiding this comment

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

A collection expression targeting a span is considered local scope, even if the collection includes spreads. See CollectionExpressionTests.RefSafety_Return_02 for example.

@@ -3466,6 +3466,17 @@ static WellKnownMembers()
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32,
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32,

// System_ReadOnlySpan_T__op_Implicit_ReadOnlySpan_T_Array
Copy link
Contributor

Choose a reason for hiding this comment

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

I wasn't sure why we want this symbol in addition to ReadOnlySpan<T>..ctor(T[])

Copy link
Member Author

Choose a reason for hiding this comment

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

VisitStatementSubList(builder, node.Statements);

var additionalLocals = TemporaryArray<LocalSymbol>.Empty;
var previousLocals = _additionalLocals;
Copy link
Contributor

Choose a reason for hiding this comment

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

A comment about what we are trying to do might be appropriate, e.g. when _additionalLocals is null here, it means we are entering the outermost block of the current function. Symbols inserted to _additionalLocals by child statements should be put in this block.

""";
var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80);
comp.VerifyEmitDiagnostics(
// (7,26): error CS9203: A collection expression of type 'Span<int>' cannot be used in this context because it may be exposed outside of the current scope.
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps the diagnostic message should say something like exposed outside of the current function.


string sourceB = $$"""
using System.Collections.Generic;
{{(useUnsafe ? "unsafe" : "")}} class Program
Copy link
Contributor

Choose a reason for hiding this comment

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

I wasn't sure why we expected presence or absence of unsafe to affect the test.

Copy link
Member Author

Choose a reason for hiding this comment

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

unsafe shouldn't affect the test, but since we have some ref safety diagnostics that vary based on unsafe, it seemed useful to include an explicit test.

}
else
{
verifier.VerifyIL("Program.ThreeItems<T>", """
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have a tracking issue for adding an "unexpected heap alloc" warning here?

Copy link
Member Author

Choose a reason for hiding this comment

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

{
// A non-empty collection expression with span type may be stored
// on the stack. In those cases the expression may have local scope.
if (expr.Type?.IsRefLikeType == true && expr.Elements.Length > 0)
Copy link
Member

@jcouv jcouv Aug 23, 2023

Choose a reason for hiding this comment

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

nit: consider inverting if to flatten method #Resolved

@jcouv
Copy link
Member

jcouv commented Aug 23, 2023

                    ReadOnlySpan<int> s = [1, 2, 3];

nit: could do away with ceremony (just a top-level statement is enough)


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:12990 in 7feae25. [](commit_id = 7feae25, deletion_comment = False)

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (iteration 9)

{
public static T[] ToArray(ReadOnlySpan<T> s) => s.ToArray();
}
record class B<T>(T x, T y, T z) : A<T>(ToArray([x, y, z]));
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it feels like the test is not verifying the value of what is actually passed to the base constructor.

Copy link
Contributor

@RikkiGibson RikkiGibson Aug 24, 2023

Choose a reason for hiding this comment

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

nevermind, we can trust ToArray to give an equivalent collection as the span. I just didn't have my records brain switched on when I reviewed this.

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

Successfully merging this pull request may close these issues.

6 participants