Ref fields escape analysis#61195
Conversation
9b115a6 to
c315004
Compare
ac3a4fa to
09e43f1
Compare
899e9af to
28f99b6
Compare
| } | ||
|
|
||
| internal static uint GetBroadestValEscape(BoundTupleExpression expr, uint scopeOfTheContainingExpression) | ||
| internal uint GetBroadestValEscape(BoundTupleExpression expr, uint scopeOfTheContainingExpression) |
There was a problem hiding this comment.
nit: should we settle on one of "widest" or "broadest" and use the term throughout? #ByDesign
There was a problem hiding this comment.
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.
Should we determine this value at the time we initialize _refEscapeScope? Similarly with _valEscapeScope. #Resolved
| 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.
Maybe these diagnostics are referring to the lowered form of the interpolated string here? #Resolved
There was a problem hiding this comment.
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).
| // 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.
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.
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.
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.
| // 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.
Could you please explain why this is invalid? #Resolved
There was a problem hiding this comment.
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 structthen ref-safe-to-escape of allrefarguments
I've added a comment.
| 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.
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.
it seems that the general risk is that references to variables within the method
MayAssign1may escape into the variables referenced in its__arglist
Yes, that's my understanding.
| @@ -3344,11 +3474,12 @@ public ref struct S | |||
| "; | |||
| // Tracking issue: https://github.com/dotnet/roslyn/issues/22361 | |||
There was a problem hiding this comment.
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.
The change below was incorrect and reverted, so this tracking issue still applies.
| } | ||
| "; | ||
| CreateCompilationWithMscorlibAndSpan(text, parseOptions: TestOptions.RegularPreview).VerifyDiagnostics( | ||
| CreateCompilationWithMscorlibAndSpan(text, parseOptions: TestOptions.Regular10).VerifyDiagnostics( |
There was a problem hiding this comment.
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.
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
| uint escapeScope = Binder.ExternalScope; | ||
|
|
||
| ArrayBuilder<bool> inParametersMatchedWithArgs = null; | ||
| ArrayBuilder<bool>? inParametersMatchedWithArgs = null; |
There was a problem hiding this comment.
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.
Yes, I've added a PROTOTYPE comment to TryGetUnmatchedInParameterAndFreeMatchedArgs().
| ParameterSymbol? unmatchedInParameter = TryGetUnmatchedInParameterAndFreeMatchedArgs(parameters, ref inParametersMatchedWithArgs); | ||
|
|
There was a problem hiding this comment.
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
|
/azp run |
|
Azure Pipelines successfully started running 4 pipeline(s). |
|
@RikkiGibson, @AlekseyTs, thanks for the detailed reviews. I've responded to all feedback. |
| if (inOutFlags == ParameterAttributes.Out) | ||
| { | ||
| refKind = RefKind.Out; | ||
| scope = DeclarationScope.RefScoped; |
There was a problem hiding this comment.
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, |
| { | ||
| 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))); |
| static bool isValidTypeArgument(AnonymousTypeField field) | ||
| { | ||
| return field.Scope == DeclarationScope.Unscoped && | ||
| return (field.Scope == DeclarationScope.Unscoped || (field.Scope == DeclarationScope.RefScoped && field.RefKind == RefKind.Out)) && |
| { | ||
| return _refEscapeScope; | ||
| } | ||
| return Binder.TopLevelScope; |
There was a problem hiding this comment.
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.
for unscoped values, we return ref-safe-to-escape from the initializer.
Perhaps the table above should reflect that and the spec as well.
| { | ||
| // 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.
nit: we could assert !ParameterHelpers.RequiresLIfetimeAnnotationAttribute() here.
| { | ||
| diagnostics.Add(ErrorCode.ERR_ThisInBadContext, thisKeyword.GetLocation()); | ||
| } | ||
| if (refKind == RefKind.Out && scope == DeclarationScope.Unscoped) |
There was a problem hiding this comment.
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.
Proposal: low-level-struct-improvements.md
Test plan: #59194
Included:
reffields (see spec)refre-assignment (see spec)scopedvariables (see spec)thisparameter ofstructis implicitlyscoped refoutparameters are implicitlyscoped out-langversion:previewor when runtime containsref fieldsfeature flag