Skip to content
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

Merged
merged 20 commits into from
Jun 23, 2022
Merged

Ref fields escape analysis #61195

merged 20 commits into from
Jun 23, 2022

Conversation

cston
Copy link
Member

@cston cston commented May 9, 2022

Proposal: low-level-struct-improvements.md
Test plan: #59194

Included:

  • Add rules for ref fields (see spec)
  • Update rules for ref re-assignment (see spec)
  • Add rules for scoped variables (see spec)
  • this parameter of struct is implicitly scoped ref
  • out parameters are implicitly scoped out
  • Update rules for method invocation (see spec)
  • Update rules for arguments must match (see spec)
  • Apply updated rules with -langversion:preview or when runtime contains ref fields feature flag

@cston cston force-pushed the ref-scope branch 12 times, most recently from 9b115a6 to c315004 Compare May 16, 2022 23:37
@cston cston force-pushed the ref-scope branch 3 times, most recently from ac3a4fa to 09e43f1 Compare May 18, 2022 21:26
@cston cston force-pushed the ref-scope branch 2 times, most recently from 899e9af to 28f99b6 Compare June 3, 2022 13:25
@cston cston marked this pull request as ready for review June 3, 2022 19:36
@cston cston requested a review from a team as a code owner June 3, 2022 19:36
@RikkiGibson RikkiGibson self-assigned this Jun 3, 2022
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide resolved
@@ -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)
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 3, 2022

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

Copy link
Member Author

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);
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 3, 2022

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),
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 3, 2022

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

Copy link
Member Author

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}""" + $"""
Copy link
Contributor

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)
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 3, 2022

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

Copy link
Member Author

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.

@RikkiGibson

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.
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 6, 2022

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

Copy link
Member Author

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 a ref struct then ref-safe-to-escape of all ref 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
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 6, 2022

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

Copy link
Member Author

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
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 6, 2022

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

Copy link
Member Author

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(
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 6, 2022

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;
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 7, 2022

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

src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs Outdated Show resolved Hide 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;
Copy link
Contributor

@RikkiGibson RikkiGibson Jun 7, 2022

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

Copy link
Member Author

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().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +1549 to 1550
ParameterSymbol? unmatchedInParameter = TryGetUnmatchedInParameterAndFreeMatchedArgs(parameters, ref inParametersMatchedWithArgs);

Copy link
Contributor

@RikkiGibson RikkiGibson Jun 7, 2022

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

Copy link
Member Author

@cston cston Jun 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cston
Copy link
Member Author

cston commented Jun 20, 2022

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@cston
Copy link
Member Author

cston commented Jun 20, 2022

@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;
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope = DeclarationScope.RefScoped;

We allow to override it with an attribute? #Closed

Copy link
Member Author

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,
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true

Can we make this stronger? I.e. param.RefKind != RefKind.Out? #Closed

@@ -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)));
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeclarationScope.Unscoped

Can we make this stronger and verify that parameter is not out? #Closed

@@ -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)) &&
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeclarationScope.Unscoped

Similar question as above #Closed

{
return _refEscapeScope;
}
return Binder.TopLevelScope;
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return Binder.TopLevelScope;

It looks like the table above talks about two different scopes, but we always return the same value. What am I missing? #Resolved

Copy link
Member Author

@cston cston Jun 22, 2022

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.

Copy link
Contributor

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
Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (commit 20)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants