-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Support declaring and binding to methods with params Span<T>
#59123
Conversation
…o ReadOnlySpan<T>
// PROTOTYPE: Test use-site diagnostics. | ||
var useSiteInfo = new CompoundUseSiteInfo<AssemblySymbol>(_diagnostics, _compilation.Assembly); | ||
var conversion = _compilation.Conversions.ClassifyImplicitConversionFromType(spanType, paramArrayType, ref useSiteInfo); | ||
conversion.MarkUnderlyingConversionsCheckedRecursive(); |
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.
return CreateParamArrayArgument(syntax, paramArrayType, arrayArgs, _compilation, this); | ||
if (paramArrayType.IsSZArray()) | ||
{ | ||
return CreateParamArrayArgument(syntax, paramArrayType, arrayArgs, _compilation, 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.
System_Span_T__get_Item, | ||
System_Span_T__get_Length, | ||
System_Span_T__op_Implicit_SpanReadOnlySpan, |
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 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.
Yes, as we discussed offline, we'll use the well-known member for conversion.
var parameter = ((BoundParameter)expr).ParameterSymbol; | ||
// params Span<T> value cannot be returned since that would | ||
// prevent sharing repeated allocations at the call-site. | ||
if (parameter.IsParams && !parameter.Type.IsSZArray()) |
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 I remember correctly, it used to be that this property on its own wasn't sufficient for a parameter to be treated as a "real" params
parameter. The type and position was important as well. Also, even if parameter is a "real" params, it doesn't mean that Span<T>
was created by compiler, a user-created span could be passed. Another thing to consider is the fact, that removing params
is not a binary breaking change, but it looks like it is going to disable the safety check. Perhaps, it would be better to move the safety check to the consumer, where we know exactly what is going on rather than doing this at the definition site? #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.
Updated check for params
and added a PROTOTYPE comment to move the error reporting to the caller.
{ | ||
Debug.Assert((object)final == final.ContainingSymbol.GetParameters().Last()); | ||
return final.IsParams && ((ParameterSymbol)final.OriginalDefinition).Type.IsSZArray(); | ||
return final.IsParams && final.Type.IsParamsType(Compilation); |
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 (type is NamedTypeSymbol { Arity: 1 } namedType) | ||
{ | ||
return namedType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[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.
@@ -1638,9 +1638,9 @@ private TypeSymbol GetParameterType(ParameterSymbol parameter, MemberAnalysisRes | |||
{ | |||
var type = parameter.Type; | |||
if (result.Kind == MemberResolutionKind.ApplicableInExpandedForm && | |||
parameter.IsParams && type.IsSZArray()) | |||
parameter.IsParams && type.IsParamsType(Compilation)) |
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 (type is NamedTypeSymbol { Arity: 1 } namedType) | ||
{ | ||
var constructedFrom = namedType.ConstructedFrom; |
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.
@@ -507,15 +506,23 @@ private static void CheckParameterModifiers(BaseParameterSyntax parameter, Bindi | |||
// error CS1100: Method '{0}' has a parameter modifier 'this' which is not on the first parameter | |||
diagnostics.Add(ErrorCode.ERR_BadThisParam, thisKeyword.GetLocation(), owner.Name); | |||
} | |||
else if (parameter.IsParams && owner.IsOperator()) | |||
else if (parameter.IsParams) |
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 refactoring doesn't look equivalent. It is disabling the following checks even if no errors reported. Consider moving this if
to the end of the chain, or restructuring the code to perform the following checks unless an error is reported (perhaps ignoring the language version error). #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.
I believe the following checks are not applicable for a params
parameter if no errors are reported here, although that may not be the case in the future, so I've moved this block to the end.
// for 'stackalloc' in LocalRewriter.VisitStackAllocArrayCreationBase().) | ||
|
||
// For now, simply allocate the array on the heap: new Span<T>(new T[length]) | ||
var intType = _compilation.GetSpecialType(SpecialType.System_Int32); |
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 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 use helper that reports diagnostics
This is marked as resolved, but I do not see any changes around reporting diagnostics.
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.
Removed.
|
||
// For now, simply allocate the array on the heap: new Span<T>(new T[length]) | ||
var intType = _compilation.GetSpecialType(SpecialType.System_Int32); | ||
var array = new BoundArrayCreation( |
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 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.
CreateParamArrayArgument()
creates an array and initializes the values. Ideally, the Span<T>
here will be created through a BCL helper method so initialization will likely be handled separately from allocation.
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 Span here will be created through a BCL helper method so initialization will likely be handled separately from allocation.
When and if this happens, then we will adjust implementation accordingly. At the moment, however, I see no good reason adding all this complexity when we have a perfectly good reusable helper.
// ... | ||
// temp[n - 1] = arrayArgs[n - 1]; | ||
// temp | ||
for (int i = 0; i < arrayArgs.Length; i++) |
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 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.
To allow for allocation to be handled by a runtime helper method.
{ | ||
assignment = _factory.AssignmentExpression( | ||
_factory.Call(temp, spanGetItem, MakeLiteral(syntax, ConstantValue.Create(i), intType, this)), | ||
_factory.Convert(elementType, arrayArgs[i])); |
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.
sideEffects.Add(assignment); | ||
} | ||
|
||
var expr = MakeConversionNode(syntax, temp, conversion, temp.Type, @checked: 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.
Done with review pass (commit 2), tests are not looked at. |
@cston It looks like there are legitimate test failures. |
// prevent sharing repeated allocations at the call-site. | ||
if (parameter.IsParams && | ||
parameter.Ordinal == parameter.ContainingSymbol.GetParameterCount() - 1 && | ||
!parameter.Type.IsSZArray()) |
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 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 think in error situations the type could be neither and the error will be unexpected and confusing.
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.
Added PROTOTYPE comment.
PropertySymbol p => p.GetOwnOrInheritedGetMethod() ?? p.GetOwnOrInheritedSetMethod(), | ||
_ => throw ExceptionUtilities.UnexpectedValue(methodOrIndexer), | ||
}; | ||
return CreateParamArrayArgument(syntax, parameterType, arrayArgs, compilation, method, BindingDiagnosticBag.Discarded); |
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.
PropertySymbol p => p.GetOwnOrInheritedGetMethod() ?? p.GetOwnOrInheritedSetMethod(), | ||
_ => throw ExceptionUtilities.UnexpectedValue(methodOrIndexer), | ||
}; | ||
return CreateParamArrayArgument(syntax, parameterType, arrayArgs, compilation, method, BindingDiagnosticBag.Discarded); |
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.
Children(4): | ||
ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Span<System.Int32>, IsImplicit) (Syntax: 'F(1, 2)') | ||
Left: | ||
ILocalReferenceOperation: (OperationKind.LocalReference, Type: System.Span<System.Int32>, IsImplicit) (Syntax: 'F(1, 2)') |
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.
null | ||
Arguments(1): | ||
IArgumentOperation (ArgumentKind.ParamArray, Matching Parameter: args) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'F(1, 2)') | ||
IOperation: (OperationKind.None, Type: System.Span<System.Int32>, IsImplicit) (Syntax: 'F(1, 2)') |
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.
Done with review pass (commit 6) |
/azp run |
Azure Pipelines successfully started running 4 pipeline(s). |
(byte)(MemberFlags.PropertyGet), // Flags | ||
(byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Span_T - WellKnownType.ExtSentinel), // DeclaringTypeId | ||
0, // Arity | ||
0, // Method Signature | ||
(byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, // Return Type | ||
|
||
// System_ReadOnlySpan__ctor | ||
// System_Span_T__op_Implicit_SpanReadOnlySpan | ||
(byte)(MemberFlags.Method | MemberFlags.Static), // Flags |
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.
|
||
static BoundExpression createBadExpression(SyntaxNode syntax, TypeSymbol type) | ||
{ | ||
return new BoundBadExpression(syntax, LookupResultKind.NotReferencable, ImmutableArray<Symbol?>.Empty, ImmutableArray<BoundExpression>.Empty, 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.
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 PROTOTYPE comment will be fine for now, I think.
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 (commit 7), modulo a couple of suggested PROTOTYPE comments
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 (commit 8), assuming CI is passing
// than on the definition. That would allow calling this method with an explicit Span<T> | ||
// without an error. That's also important because changing the method defintion between | ||
// params and non-params is not a binary breaking change. | ||
Error(diagnostics, ErrorCode.ERR_EscapeParamsSpan, node, expr.Syntax); |
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.
Added a note to test plan. Per offline conversation, we need to figure out safe escape rules first.
Would it be okay to remove this error in the meantime? #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.
Removed.
var m1Original = m1.LeastOverriddenMember.OriginalDefinition.GetParameters(); | ||
var m2Original = m2.LeastOverriddenMember.OriginalDefinition.GetParameters(); | ||
|
||
// Prefer params Span<T> or ReadOnlySpan<T> over params 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.
The spec says there's an order of preference: ReadOnlySpan<T>
first, then Span<T>
, T[]
and IEnumerable<T>
#Closed
using Roslyn.Test.Utilities; | ||
using Xunit; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp.UnitTests |
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 file-scoped namespace while you can ;-) #ByDesign
compilation, | ||
diagnostics); | ||
|
||
var spanConstructor = (MethodSymbol)Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Span_T__ctorArray, diagnostics, syntax.Location); |
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 adding nullability annotation to GetWellKnownTypeMember
and enabling around the new code #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.
This file is nullable enabled, but I've left GetWellKnownTypeMember()
as is, since that's probably beyond the scope of this PR.
// A.F2("span", 3); | ||
Diagnostic(ErrorCode.ERR_FeatureInPreview, @"A.F2(""span"", 3)").WithArguments("params Span<T>").WithLocation(8, 9)); | ||
|
||
compB = CreateCompilation(sourceB, references: new[] { refA }, targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); |
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 use RegularNext (instead of preview) in tests that use Regular10 or CSharp10 #Resolved
|
||
if (paramArrayType.IsReadOnlySpanType(compilation)) | ||
{ | ||
var spanToReadOnlySpanOperator = (MethodSymbol)Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Span_T__op_Implicit_SpanReadOnlySpan, diagnostics, syntax.Location); |
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.
Consider adding test with missing newly-added well-known members System_Span_T__op_Implicit_SpanReadOnlySpan and System_Span_T__ctorArray
Never mind, found it: MissingSpanConstructor
#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.
Done with review pass (iteration 8)
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 10)
Tests pass locally but the feature branch is not up to date for CI. |
@cston confirmed all the tests are passing locally. The CI machines changed out from under us durin the lifetime of this PR. The next merge from main should fix. Force merging now (it's a feature branch) and next merge from main should show green else we'll dig into the failures |
Proposal: params-span.md
Initial feature changes:
params Span<T>
andparams ReadOnlySpan<T>
params Span<T>
orparams ReadOnlySpan<T>
-langversion
errorsparams Span<T>
orparams ReadOnlySpan<T>
overparams T[]
in overload resolution; treatparams Span<T>
andparams ReadOnlySpan<T>
equallyTest plan #57049