-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Add ReturnOnly scope #64090
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
Add ReturnOnly scope #64090
Changes from 3 commits
ec524e6
29839c6
dca30f5
e582e59
d8a761e
d0749e5
95e8f68
efb9aa8
33438c0
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 |
|---|---|---|
|
|
@@ -22,17 +22,20 @@ internal partial class Binder | |
| /// <summary> | ||
| /// For the purpose of escape verification we operate with the depth of local scopes. | ||
| /// The depth is a uint, with smaller number representing shallower/wider scopes. | ||
| /// The 0 and 1 are special scopes - | ||
| /// 0 is the "external" or "return" scope that is outside of the containing method/lambda. | ||
| /// If something can escape to scope 0, it can escape to any scope in a given method or can be returned. | ||
| /// 1 is the "parameter" or "top" scope that is just inside the containing method/lambda. | ||
| /// 0, 1 and 2 are special scopes - | ||
| /// 0 is the "calling method" scope that is outside of the containing method/lambda. | ||
| /// If something can escape to scope 0, it can escape to any scope in a given method through a ref parameter or return. | ||
| /// 1 is the "return-only" scope that is outside of the containing method/lambda. | ||
| /// If something can escape to scope 1, it can escape to any scope in a given method or can be returned, but it can't escape through a ref parameter. | ||
| /// 2 is the "current method" scope that is just inside the containing method/lambda. | ||
| /// If something can escape to scope 1, it can escape to any scope in a given method, but cannot be returned. | ||
| /// n + 1 corresponds to scopes immediately inside a scope of depth n. | ||
| /// Since sibling scopes do not intersect and a value cannot escape from one to another without | ||
| /// escaping to a wider scope, we can use simple depth numbering without ambiguity. | ||
| /// </summary> | ||
| internal const uint ExternalScope = 0; | ||
| internal const uint TopLevelScope = 1; | ||
| internal const uint ReturnOnlyScope = 1; | ||
| internal const uint TopLevelScope = 2; | ||
|
|
||
| // Some value kinds are semantically the same and the only distinction is how errors are reported | ||
| // for those purposes we reserve lowest 2 bits | ||
|
|
@@ -719,7 +722,7 @@ private static bool CheckLocalRefEscape(SyntaxNode node, BoundLocal local, uint | |
| return true; | ||
| } | ||
|
|
||
| if (escapeTo == Binder.ExternalScope) | ||
| if (escapeTo is Binder.ExternalScope or Binder.ReturnOnlyScope) | ||
| { | ||
| if (localSymbol.RefKind == RefKind.None) | ||
| { | ||
|
|
@@ -794,21 +797,17 @@ private uint GetParameterValEscape(ParameterSymbol parameter) | |
|
|
||
| private uint GetParameterRefEscape(ParameterSymbol parameter) | ||
| { | ||
| if (UseUpdatedEscapeRules) | ||
| { | ||
| return parameter.RefKind is RefKind.None || parameter.EffectiveScope != DeclarationScope.Unscoped ? Binder.TopLevelScope : Binder.ExternalScope; | ||
| } | ||
| else | ||
| return parameter switch | ||
| { | ||
| // byval parameters can escape to method's top level. Others can escape further. | ||
| // NOTE: "method" here means nearest containing method, lambda or local function. | ||
| return parameter.RefKind == RefKind.None ? Binder.TopLevelScope : Binder.ExternalScope; | ||
| } | ||
| { RefKind: RefKind.None } or { EffectiveScope: not DeclarationScope.Unscoped } => Binder.TopLevelScope, | ||
| { EffectiveScope: DeclarationScope.Unscoped, Type.IsRefLikeType: true } => Binder.ReturnOnlyScope, | ||
|
Contributor
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.
Member
Author
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. Yes. The state machine won't test the property again in this path, though. It feels like it might be good to either:
Member
Author
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 went with removing. |
||
| _ => Binder.ExternalScope | ||
| }; | ||
| } | ||
|
|
||
| private bool CheckParameterValEscape(SyntaxNode node, BoundParameter parameter, uint escapeTo, BindingDiagnosticBag diagnostics) | ||
| { | ||
| Debug.Assert(escapeTo == Binder.ExternalScope); | ||
| Debug.Assert(escapeTo is Binder.ExternalScope or Binder.ReturnOnlyScope); | ||
| if (UseUpdatedEscapeRules) | ||
| { | ||
| var parameterSymbol = parameter.ParameterSymbol; | ||
|
|
@@ -826,22 +825,32 @@ private bool CheckParameterValEscape(SyntaxNode node, BoundParameter parameter, | |
| } | ||
| } | ||
|
|
||
| private bool CheckParameterRefEscape(SyntaxNode node, BoundParameter parameter, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics) | ||
| private bool CheckParameterRefEscape(SyntaxNode node, BoundExpression parameter, ParameterSymbol parameterSymbol, uint escapeTo, bool checkingReceiver, BindingDiagnosticBag diagnostics) | ||
| { | ||
| var parameterSymbol = parameter.ParameterSymbol; | ||
|
|
||
| if (GetParameterRefEscape(parameterSymbol) > escapeTo) | ||
| var refSafeToEscape = GetParameterRefEscape(parameterSymbol); | ||
| if (refSafeToEscape > escapeTo) | ||
| { | ||
| var isRefScoped = parameterSymbol.EffectiveScope == DeclarationScope.RefScoped; | ||
| Debug.Assert(parameterSymbol.RefKind == RefKind.None || isRefScoped); | ||
| if (checkingReceiver) | ||
| Debug.Assert(parameterSymbol.RefKind == RefKind.None || isRefScoped || refSafeToEscape == Binder.ReturnOnlyScope); | ||
|
|
||
| if (parameter is BoundThisReference) | ||
| { | ||
| Error(diagnostics, isRefScoped ? ErrorCode.ERR_RefReturnScopedParameter2 : ErrorCode.ERR_RefReturnParameter2, parameter.Syntax, parameterSymbol.Name); | ||
| Error(diagnostics, ErrorCode.ERR_RefReturnStructThis, node); | ||
| return false; | ||
| } | ||
| else | ||
|
|
||
| #pragma warning disable format | ||
| var (errorCode, syntax) = (checkingReceiver, isRefScoped, refSafeToEscape) switch | ||
| { | ||
| Error(diagnostics, isRefScoped ? ErrorCode.ERR_RefReturnScopedParameter : ErrorCode.ERR_RefReturnParameter, node, parameterSymbol.Name); | ||
| } | ||
| (checkingReceiver: true, isRefScoped: true, _) => (ErrorCode.ERR_RefReturnScopedParameter2, parameter.Syntax), | ||
| (checkingReceiver: true, isRefScoped: false, Binder.ReturnOnlyScope) => (ErrorCode.ERR_RefReturnOnlyParameter, parameter.Syntax), | ||
| (checkingReceiver: true, isRefScoped: false, _) => (ErrorCode.ERR_RefReturnParameter2, parameter.Syntax), | ||
| (checkingReceiver: false, isRefScoped: true, _) => (ErrorCode.ERR_RefReturnScopedParameter, node), | ||
| (checkingReceiver: false, isRefScoped: false, Binder.ReturnOnlyScope) => (ErrorCode.ERR_RefReturnOnlyParameter, node), | ||
| (checkingReceiver: false, isRefScoped: false, _) => (ErrorCode.ERR_RefReturnParameter, node) | ||
| }; | ||
| #pragma warning restore format | ||
| Error(diagnostics, errorCode, syntax, parameterSymbol.Name); | ||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -2362,7 +2371,7 @@ private static ErrorCode GetStandardLvalueError(BindValueKind kind) | |
|
|
||
| private static ErrorCode GetStandardRValueRefEscapeError(uint escapeTo) | ||
| { | ||
| if (escapeTo == Binder.ExternalScope) | ||
| if (escapeTo is Binder.ExternalScope or Binder.ReturnOnlyScope) | ||
| { | ||
| return ErrorCode.ERR_RefReturnLvalueExpected; | ||
| } | ||
|
|
@@ -2524,28 +2533,9 @@ internal uint GetRefEscape(BoundExpression expr, uint scopeOfTheContainingExpres | |
| return ((BoundLocal)expr).LocalSymbol.RefEscapeScope; | ||
|
|
||
| case BoundKind.ThisReference: | ||
| Debug.Assert(this.ContainingMember() is MethodSymbol { ThisParameter: not null }); | ||
|
|
||
| var thisref = (BoundThisReference)expr; | ||
|
|
||
| // "this" is an RValue, unless in a struct. | ||
| if (!thisref.Type.IsValueType) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| if (UseUpdatedEscapeRules) | ||
| { | ||
| if (this.ContainingMember() is MethodSymbol { ThisParameter: var thisParameter } && | ||
| thisParameter.EffectiveScope == DeclarationScope.Unscoped) | ||
| { | ||
| return Binder.ExternalScope; | ||
| } | ||
| } | ||
|
|
||
| //"this" is not returnable by reference in a struct. | ||
| // can ref escape to any other level | ||
| return Binder.TopLevelScope; | ||
| var thisParam = ((MethodSymbol)this.ContainingMember()).ThisParameter; | ||
| Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything)); | ||
| return GetParameterRefEscape(thisParam); | ||
|
|
||
| case BoundKind.ConditionalOperator: | ||
| var conditional = (BoundConditionalOperator)expr; | ||
|
|
@@ -2761,7 +2751,7 @@ internal bool CheckRefEscape(SyntaxNode node, BoundExpression expr, uint escapeF | |
| case BoundKind.RefValueOperator: | ||
| // The undocumented __refvalue(tr, T) expression results in an lvalue of type T. | ||
| // for compat reasons it is not ref-returnable (since TypedReference is not val-returnable) | ||
| if (escapeTo == Binder.ExternalScope) | ||
| if (escapeTo is Binder.ExternalScope or Binder.ReturnOnlyScope) | ||
| { | ||
| break; | ||
| } | ||
|
|
@@ -2782,41 +2772,16 @@ internal bool CheckRefEscape(SyntaxNode node, BoundExpression expr, uint escapeF | |
|
|
||
| case BoundKind.Parameter: | ||
| var parameter = (BoundParameter)expr; | ||
| return CheckParameterRefEscape(node, parameter, escapeTo, checkingReceiver, diagnostics); | ||
| return CheckParameterRefEscape(node, parameter, parameter.ParameterSymbol, escapeTo, checkingReceiver, diagnostics); | ||
|
|
||
| case BoundKind.Local: | ||
| var local = (BoundLocal)expr; | ||
| return CheckLocalRefEscape(node, local, escapeTo, checkingReceiver, diagnostics); | ||
|
|
||
| case BoundKind.ThisReference: | ||
| Debug.Assert(this.ContainingMember() is MethodSymbol { ThisParameter: not null }); | ||
|
|
||
| var thisref = (BoundThisReference)expr; | ||
|
|
||
| // "this" is an RValue, unless in a struct. | ||
| if (!thisref.Type.IsValueType) | ||
| { | ||
| break; | ||
| } | ||
|
|
||
| //"this" is not returnable by reference in a struct. | ||
| if (escapeTo == Binder.ExternalScope) | ||
| { | ||
| if (UseUpdatedEscapeRules) | ||
| { | ||
| if (this.ContainingMember() is MethodSymbol { ThisParameter: var thisParameter } && | ||
| thisParameter.EffectiveScope == DeclarationScope.Unscoped) | ||
| { | ||
| // can ref escape to any other level | ||
| return true; | ||
| } | ||
| } | ||
| Error(diagnostics, ErrorCode.ERR_RefReturnStructThis, node); | ||
| return false; | ||
| } | ||
|
|
||
| // can ref escape to any other level | ||
| return true; | ||
| var thisParam = ((MethodSymbol)this.ContainingMember()).ThisParameter; | ||
| Debug.Assert(thisParam.Type.Equals(((BoundThisReference)expr).Type, TypeCompareKind.ConsiderEverything)); | ||
| return CheckParameterRefEscape(node, expr, thisParam, escapeTo, checkingReceiver, diagnostics); | ||
|
|
||
| case BoundKind.ConditionalOperator: | ||
| var conditional = (BoundConditionalOperator)expr; | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 suspect that more places where this check is used can be simplified. If the ParameterSymbol APIs behave appropriately depending on which escape rules are in use, then doing these checks separately could be redundant.