-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Conversation
ef70352
to
e72459d
Compare
…with span target types
7a0d691
to
cd306e0
Compare
@@ -618,6 +619,41 @@ public override BoundNode VisitRefTypeOperator(BoundRefTypeOperator node) | |||
return node.Update(operand, getTypeFromHandle, type); | |||
} | |||
|
|||
private BoundStatement? RewriteFieldOrPropertyInitializer(BoundStatement initializer) | |||
{ | |||
var previousLocals = _additionalLocals; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
nit: I didn't follow. What's the benefit of moving the In reply to: 1679779399 Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:2675 in f3c7566. [](commit_id = f3c7566, deletion_comment = False) |
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) |
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) |
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) |
There was a problem hiding this 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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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[])
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>", """ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this 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])); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Use inline arrays for the code generated for collection expressions with
Span<T>
orReadOnlySpan<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