-
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
Changes from 7 commits
cd306e0
a2acbc8
7fce767
f7f1fa8
9803f2e
eda04a9
f3c7566
1031821
7feae25
9ee705d
09355c9
377194c
4d29bb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3852,6 +3852,13 @@ internal uint GetValEscape(BoundExpression expr, uint scopeOfTheContainingExpres | |
return GetInterpolatedStringHandlerConversionEscapeScope(conversion.Operand, scopeOfTheContainingExpression); | ||
} | ||
|
||
if (conversion.ConversionKind == ConversionKind.CollectionExpression) | ||
{ | ||
return HasLocalScope((BoundCollectionExpression)conversion.Operand) | ||
? scopeOfTheContainingExpression | ||
: CallingMethodScope; | ||
} | ||
|
||
if (conversion.Conversion.IsInlineArray) | ||
{ | ||
ImmutableArray<BoundExpression> arguments; | ||
|
@@ -3959,9 +3966,6 @@ internal uint GetValEscape(BoundExpression expr, uint scopeOfTheContainingExpres | |
var switchExpr = (BoundSwitchExpression)expr; | ||
return GetValEscape(switchExpr.SwitchArms.SelectAsArray(a => a.Value), scopeOfTheContainingExpression); | ||
|
||
case BoundKind.CollectionExpression: | ||
return CallingMethodScope; | ||
|
||
default: | ||
// in error situations some unexpected nodes could make here | ||
// returning "scopeOfTheContainingExpression" seems safer than throwing. | ||
|
@@ -3971,6 +3975,51 @@ internal uint GetValEscape(BoundExpression expr, uint scopeOfTheContainingExpres | |
} | ||
} | ||
|
||
#nullable enable | ||
private bool HasLocalScope(BoundCollectionExpression expr) | ||
{ | ||
// 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) | ||
{ | ||
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 commentThe 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 commentThe 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 |
||
{ | ||
case CollectionExpressionTypeKind.ReadOnlySpan: | ||
Debug.Assert(elementType is { }); | ||
return !LocalRewriter.ShouldUseRuntimeHelpersCreateSpan(expr, elementType); | ||
case CollectionExpressionTypeKind.Span: | ||
return true; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. pseudocode might be more clear than prose here. e.g. |
||
var constructMethod = expr.CollectionBuilderMethod; | ||
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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. We should have filtered out the unexpected collection builder method in |
||
} | ||
Debug.Assert(constructMethod.ReturnType.Equals(expr.Type, TypeCompareKind.AllIgnoreOptions)); | ||
Debug.Assert(parameter.Type.OriginalDefinition.Equals(_compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions)); | ||
if (parameter.EffectiveScope == ScopedKind.ScopedValue) | ||
{ | ||
return false; | ||
} | ||
if (LocalRewriter.ShouldUseRuntimeHelpersCreateSpan(expr, ((NamedTypeSymbol)parameter.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0].Type)) | ||
{ | ||
return false; | ||
} | ||
return true; | ||
default: | ||
throw ExceptionUtilities.UnexpectedValue(collectionTypeKind); // ref struct collection type with unexpected type kind | ||
} | ||
} | ||
return false; | ||
} | ||
#nullable disable | ||
|
||
private uint GetTupleValEscape(ImmutableArray<BoundExpression> elements, uint scopeOfTheContainingExpression) | ||
{ | ||
uint narrowestScope = scopeOfTheContainingExpression; | ||
|
@@ -4374,6 +4423,16 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint escapeF | |
return CheckInterpolatedStringHandlerConversionEscape(conversion.Operand, escapeFrom, escapeTo, diagnostics); | ||
} | ||
|
||
if (conversion.ConversionKind == ConversionKind.CollectionExpression) | ||
{ | ||
if (HasLocalScope((BoundCollectionExpression)conversion.Operand) && escapeTo < _localScopeDepth) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_CollectionExpressionEscape, node, expr.Type); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
if (conversion.Conversion.IsInlineArray) | ||
{ | ||
ImmutableArray<BoundExpression> arguments; | ||
|
@@ -4488,9 +4547,6 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint escapeF | |
|
||
return true; | ||
|
||
case BoundKind.CollectionExpression: | ||
return true; | ||
|
||
default: | ||
// in error situations some unexpected nodes could make here | ||
// returning "false" seems safer than throwing. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,7 @@ internal sealed partial class LocalRewriter : BoundTreeRewriterWithStackGuard | |
private readonly int _topLevelMethodOrdinal; | ||
private DelegateCacheRewriter? _lazyDelegateCacheRewriter; | ||
private bool _inExpressionLambda; | ||
private ArrayBuilder<LocalSymbol>? _additionalLocals; | ||
|
||
/// <summary> | ||
/// The original body of the current lambda or local function body, or null if not currently lowering a lambda. | ||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. I didn't understand, why introduce this new There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 = ArrayBuilder<LocalSymbol>.GetInstance(); | ||
|
||
try | ||
{ | ||
if (initializer.Kind == BoundKind.Block) | ||
{ | ||
var block = (BoundBlock)initializer; | ||
|
||
var statement = RewriteExpressionStatement((BoundExpressionStatement)block.Statements.Single(), suppressInstrumentation: true); | ||
Debug.Assert(statement is { }); | ||
return block.Update(block.Locals.AddRange(_additionalLocals), block.LocalFunctions, block.HasUnsafeModifier, block.Instrumentation, ImmutableArray.Create(statement)); | ||
} | ||
else | ||
{ | ||
var statement = RewriteExpressionStatement((BoundExpressionStatement)initializer, suppressInstrumentation: true); | ||
if (statement is null || _additionalLocals.Count == 0) | ||
{ | ||
return statement; | ||
} | ||
return new BoundBlock( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little worried about the fact that a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't thought of an easy way to assert that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding assert to |
||
statement.Syntax, | ||
_additionalLocals.ToImmutable(), | ||
ImmutableArray.Create(statement)); | ||
} | ||
} | ||
finally | ||
{ | ||
_additionalLocals.Free(); | ||
_additionalLocals = previousLocals; | ||
} | ||
} | ||
|
||
public override BoundNode VisitTypeOrInstanceInitializers(BoundTypeOrInstanceInitializers node) | ||
{ | ||
ImmutableArray<BoundStatement> originalStatements = node.Statements; | ||
|
@@ -626,18 +662,7 @@ public override BoundNode VisitTypeOrInstanceInitializers(BoundTypeOrInstanceIni | |
{ | ||
if (IsFieldOrPropertyInitializer(initializer)) | ||
{ | ||
if (initializer.Kind == BoundKind.Block) | ||
{ | ||
var block = (BoundBlock)initializer; | ||
|
||
var statement = RewriteExpressionStatement((BoundExpressionStatement)block.Statements.Single(), suppressInstrumentation: true); | ||
Debug.Assert(statement is { }); | ||
statements.Add(block.Update(block.Locals, block.LocalFunctions, block.HasUnsafeModifier, block.Instrumentation, ImmutableArray.Create(statement))); | ||
} | ||
else | ||
{ | ||
statements.Add(RewriteExpressionStatement((BoundExpressionStatement)initializer, suppressInstrumentation: true)); | ||
} | ||
statements.Add(RewriteFieldOrPropertyInitializer(initializer)); | ||
} | ||
else | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,6 @@ | |
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Diagnostics; | ||
using System.Collections.Immutable; | ||
using Microsoft.CodeAnalysis.CSharp.Symbols; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
|
@@ -20,26 +19,37 @@ public override BoundNode VisitBlock(BoundBlock node) | |
} | ||
|
||
var builder = ArrayBuilder<BoundStatement>.GetInstance(); | ||
VisitStatementSubList(builder, node.Statements); | ||
var previousLocals = _additionalLocals; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = ArrayBuilder<LocalSymbol>.GetInstance(); | ||
|
||
var additionalLocals = TemporaryArray<LocalSymbol>.Empty; | ||
|
||
BoundBlockInstrumentation? instrumentation = null; | ||
if (Instrument) | ||
try | ||
{ | ||
Instrumenter.InstrumentBlock(node, this, ref additionalLocals, out var prologue, out var epilogue, out instrumentation); | ||
if (prologue != null) | ||
{ | ||
builder.Insert(0, prologue); | ||
} | ||
VisitStatementSubList(builder, node.Statements); | ||
|
||
var additionalLocals = TemporaryArray<LocalSymbol>.Empty; | ||
|
||
if (epilogue != null) | ||
BoundBlockInstrumentation? instrumentation = null; | ||
if (Instrument) | ||
{ | ||
builder.Add(epilogue); | ||
Instrumenter.InstrumentBlock(node, this, ref additionalLocals, out var prologue, out var epilogue, out instrumentation); | ||
if (prologue != null) | ||
{ | ||
builder.Insert(0, prologue); | ||
} | ||
|
||
if (epilogue != null) | ||
{ | ||
builder.Add(epilogue); | ||
} | ||
} | ||
} | ||
|
||
return new BoundBlock(node.Syntax, node.Locals.AddRange(additionalLocals), node.LocalFunctions, node.HasUnsafeModifier, instrumentation, builder.ToImmutableAndFree(), node.HasErrors); | ||
return new BoundBlock(node.Syntax, node.Locals.AddRange(_additionalLocals).AddRange(additionalLocals), node.LocalFunctions, node.HasUnsafeModifier, instrumentation, builder.ToImmutableAndFree(), node.HasErrors); | ||
} | ||
finally | ||
{ | ||
_additionalLocals.Free(); | ||
_additionalLocals = previousLocals; | ||
} | ||
} | ||
|
||
/// <summary> | ||
|
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