-
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
Ref fields escape analysis #61195
Ref fields escape analysis #61195
Conversation
9b115a6
to
c315004
Compare
ac3a4fa
to
09e43f1
Compare
899e9af
to
28f99b6
Compare
@@ -2560,7 +2734,7 @@ internal static bool CheckRefEscape(SyntaxNode node, BoundExpression expr, uint | |||
return false; | |||
} | |||
|
|||
internal static uint GetBroadestValEscape(BoundTupleExpression expr, uint scopeOfTheContainingExpression) | |||
internal uint GetBroadestValEscape(BoundTupleExpression expr, uint scopeOfTheContainingExpression) |
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: should we settle on one of "widest" or "broadest" and use the term throughout? #ByDesign
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.
Will leave this as is since it wasn't part of the PR.
uint limit = _scope == DeclarationScope.Unscoped && _refKind != RefKind.None ? | ||
Binder.ExternalScope : | ||
Binder.TopLevelScope; | ||
return Math.Max(_refEscapeScope, limit); |
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 we determine this value at the time we initialize _refEscapeScope
? Similarly with _valEscapeScope
. #Resolved
comp.VerifyDiagnostics( | ||
// (17,9): error CS8350: This combination of arguments to 'CustomHandler.M2(ref S1, ref CustomHandler)' is disallowed because it may expose variables referenced by parameter 'handler' outside of their declaration scope | ||
// M2(ref s1, $"{s}"); | ||
Diagnostic(ErrorCode.ERR_CallArgMixing, @"M2(ref s1, " + expression + @")").WithArguments("CustomHandler.M2(ref S1, ref CustomHandler)", "handler").WithLocation(17, 9), | ||
// (17,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference | ||
// M2(ref s1, $"{s}"); | ||
Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "s1").WithLocation(17, 16), |
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.
Maybe these diagnostics are referring to the lowered form of the interpolated string here? #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.
Yes, these additional errors are reported from CheckInvocationEscape()
for CustomHandler.CustomHandler(System.Int32 literalLength, System.Int32 formattedCount, ref S1 s1)
, from the interpolated string handler conversion data, when checking the safe-to-escape of arguments in CheckInvocationArgMixing()
for CustomHandler.M2(ref S1 s1, ref CustomHandler handler)
.
@@ -11098,6 +11098,12 @@ public static void M(ref S1 s1) | |||
// (17,9): error CS8350: This combination of arguments to 'CustomHandler.M2(ref S1, ref CustomHandler)' is disallowed because it may expose variables referenced by parameter 'handler' outside of their declaration scope | |||
// M2(ref s1, $"{s}"); | |||
Diagnostic(ErrorCode.ERR_CallArgMixing, @"M2(ref s1, " + expression + @")").WithArguments("CustomHandler.M2(ref S1, ref CustomHandler)", "handler").WithLocation(17, 9), | |||
// (17,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference | |||
// M2(ref s1, $"""{s}""" + $""" |
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.
It would be nice if the diagnostics could be improved here separately of this feature. I didn't find this message to be clear. It looks like the message is referring to code which is synthesized during binding.
CreateCompilationWithMscorlibAndSpan(text).VerifyDiagnostics( | ||
// (18,20): error CS8166: Cannot return a parameter by reference 'x' because it is not a ref or out parameter | ||
// return ref x; | ||
Diagnostic(ErrorCode.ERR_RefReturnParameter, "x").WithArguments("x").WithLocation(18, 20) |
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.
You already mentioned this offline, but we will need to improve the diagnostic here. I suggest using a message like Cannot return parameter '{0}' by reference because it is not a ref parameter
. Then possibly a separate message for scoped ref
. #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.
Yes, I have several PROTOTYPE comments for ERR_RefReturnParameter
when it is reported for an out
parameter. I agree, it seems reasonable to report "because it is not a ref parameter" only.
This comment was marked as resolved.
This comment was marked as resolved.
// (19,33): error CS8526: Cannot use local 'inner' in this context because it may expose referenced variables outside of their declaration scope | ||
// x = MayWrap(ref inner); | ||
Diagnostic(ErrorCode.ERR_EscapeLocal, "inner").WithArguments("inner").WithLocation(19, 33), | ||
// (19,21): error CS8521: Cannot use a result of 'Program.MayWrap(ref Span<int>)' in this context because it may expose variables referenced by parameter 'arg' outside of their declaration scope | ||
// x = MayWrap(ref inner); | ||
Diagnostic(ErrorCode.ERR_EscapeCall, "MayWrap(ref inner)").WithArguments("Program.MayWrap(ref System.Span<int>)", "arg").WithLocation(19, 21) | ||
); | ||
// x = MayWrap(ref outer); is invalid in C#11. |
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.
Could you please explain why this is invalid? #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.
This is a breaking change:
An rvalue resulting from a method invocation
e1.M(e2, ...)
is safe-to-escape from the smallest of the following scopes:
...
3. When the return is aref struct
then ref-safe-to-escape of allref
arguments
I've added a comment.
@@ -1367,6 +1452,30 @@ static S1 MayWrap(ref Span<int> arg) | |||
// MayAssign1(__arglist(ref inner, ref rOuter)); | |||
Diagnostic(ErrorCode.ERR_CallArgMixing, "MayAssign1(__arglist(ref inner, ref rOuter))").WithArguments("Program.MayAssign1(__arglist)", "__arglist").WithLocation(23, 9) | |||
); | |||
|
|||
// C#11 reports errors for arg2 = MayWrap(ref arg1); because a method invocation that |
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 I grasp this one either but it seems that the general risk is that references to variables within the method MayAssign1
may escape into the variables referenced in its __arglist
. Is that right? #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.
it seems that the general risk is that references to variables within the method
MayAssign1
may escape into the variables referenced in its__arglist
Yes, that's my understanding.
@@ -3344,11 +3474,12 @@ public void M(ref S global) | |||
"; | |||
// Tracking issue: https://github.com/dotnet/roslyn/issues/22361 |
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.
It looks like we resolved this issue by making it disallowed for a reference to local1
to leak into s
in M(out S s)
? That's the only reason I can see why we would allow global = local2
here. Consider deleting the comment here and making a note that we resolved the issue as part of this feature implementation. #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.
The change below was incorrect and reverted, so this tracking issue still applies.
@@ -7968,7 +7968,7 @@ static S1 MayWrap(ref Span<int> arg) | |||
} | |||
} | |||
"; | |||
CreateCompilationWithMscorlibAndSpan(text, parseOptions: TestOptions.RegularPreview).VerifyDiagnostics( | |||
CreateCompilationWithMscorlibAndSpan(text, parseOptions: TestOptions.Regular10).VerifyDiagnostics( |
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 also testing LangVersion.Preview here. #Resolved
{ | ||
if (UseUpdatedEscapeRules) | ||
{ | ||
return parameter.RefKind is RefKind.None or RefKind.Out || parameter.Scope != DeclarationScope.Unscoped ? Binder.TopLevelScope : Binder.ExternalScope; |
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.
It feels like we shouldn't have to check here if this happens to be an out
parameter to decide if this is implicitly scoped. The parameter.Scope
property should just give the right answer. #Resolved
@@ -1350,7 +1489,7 @@ bool isRefEscape | |||
//by default it is safe to escape | |||
uint escapeScope = Binder.ExternalScope; | |||
|
|||
ArrayBuilder<bool> inParametersMatchedWithArgs = null; | |||
ArrayBuilder<bool>? inParametersMatchedWithArgs = null; |
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: not necessary to handle in this PR, but this should perhaps be a BitVector perhaps be deleted #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.
Yes, I've added a PROTOTYPE comment to TryGetUnmatchedInParameterAndFreeMatchedArgs()
.
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.
ParameterSymbol? unmatchedInParameter = TryGetUnmatchedInParameterAndFreeMatchedArgs(parameters, ref inParametersMatchedWithArgs); | ||
|
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 generate optional arguments during binding now, so does this method of detecting unmatched in parameters actually do anything? Can we delete the whole mechanism for detecting unmatched in parameters? #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.
/azp run |
Azure Pipelines successfully started running 4 pipeline(s). |
@RikkiGibson, @AlekseyTs, thanks for the detailed reviews. I've responded to all feedback. |
@@ -270,6 +270,7 @@ public bool TryGetFlowAnalysisAnnotations(out FlowAnalysisAnnotations value) | |||
if (inOutFlags == ParameterAttributes.Out) | |||
{ | |||
refKind = RefKind.Out; | |||
scope = DeclarationScope.RefScoped; |
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, that's possible, with an explicit attribute from metadata.
Debug.Assert((param as Symbols.PublicModel.ParameterSymbol)?.GetSymbol<ParameterSymbol>().Scope switch | ||
{ | ||
null => true, | ||
DeclarationScope.Unscoped => 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.
@@ -191,7 +191,7 @@ private NamedTypeSymbol ConstructAnonymousDelegateImplementationSymbol(Anonymous | |||
if (allValidTypeArguments(typeDescr)) | |||
{ | |||
var fields = typeDescr.Fields; | |||
Debug.Assert(fields.All(f => f.Scope == DeclarationScope.Unscoped)); | |||
Debug.Assert(fields.All(f => f.Scope == DeclarationScope.Unscoped || (f.Scope == DeclarationScope.RefScoped && f.RefKind == RefKind.Out))); |
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.
@@ -260,7 +260,7 @@ static bool allValidTypeArguments(AnonymousTypeDescriptor typeDescr) | |||
|
|||
static bool isValidTypeArgument(AnonymousTypeField field) | |||
{ | |||
return field.Scope == DeclarationScope.Unscoped && | |||
return (field.Scope == DeclarationScope.Unscoped || (field.Scope == DeclarationScope.RefScoped && field.RefKind == RefKind.Out)) && |
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 _refEscapeScope; | ||
} | ||
return Binder.TopLevelScope; |
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.
For all scoped values, the ref-safe-to-escape value is the current method; for unscoped values, we return ref-safe-to-escape from the initializer.
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.
for unscoped values, we return ref-safe-to-escape from the initializer.
Perhaps the table above should reflect that and the spec as well.
@@ -612,7 +612,13 @@ void visitFunctionPointerSignature(IMethodSymbol symbol) | |||
foreach (var param in symbol.Parameters) | |||
{ | |||
// https://github.com/dotnet/roslyn/issues/61647: Use public API. | |||
Debug.Assert((param as Symbols.PublicModel.ParameterSymbol)?.GetSymbol<ParameterSymbol>().Scope is null or DeclarationScope.Unscoped); | |||
Debug.Assert((param as Symbols.PublicModel.ParameterSymbol)?.GetSymbol<ParameterSymbol>().Scope switch |
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: we could assert !ParameterHelpers.RequiresLIfetimeAnnotationAttribute()
here.
@@ -145,6 +145,10 @@ internal static class ParameterHelpers | |||
{ | |||
diagnostics.Add(ErrorCode.ERR_ThisInBadContext, thisKeyword.GetLocation()); | |||
} | |||
if (refKind == RefKind.Out && scope == DeclarationScope.Unscoped) |
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 logic seems repeated in several places. It's simple logic so maybe it doesn't matter for feature merge. But we could have something like GetDefaultLifetime(RefKind)
and then RequiresLifetimeAnnotation would be implemented as GetDefaultLifetime(param.RefKind) != param.Scope
. It's not urgent or blocking to do this, so feel free to WontFix it or to open this as a design debt issue.
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 20)
Proposal: low-level-struct-improvements.md
Test plan: #59194
Included:
ref
fields (see spec)ref
re-assignment (see spec)scoped
variables (see spec)this
parameter ofstruct
is implicitlyscoped ref
out
parameters are implicitlyscoped out
-langversion:preview
or when runtime containsref fields
feature flag