-
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
Avoid reusing temps whose refs might be captured #76009
base: main
Are you sure you want to change the base?
Conversation
// At the same time, these expression locals might be lifted to the containing block | ||
// (to avoid reusing them if they might be captured by a ref struct). | ||
// Then we use this map to keep track of the redeclared locals. | ||
private Dictionary<LocalDefinition, LocalDefinition>? _redeclaredLocals; |
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.
It maps from the old LocalDefinition to the new LocalDefinition (which is redeclaring/shadowing the old one). Does that make sense?
I will try to explain it in the comment. Thanks.
@@ -887,7 +887,7 @@ public BoundCall Call(BoundExpression? receiver, MethodSymbol method, ImmutableA | |||
return new BoundCall( | |||
Syntax, receiver, initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown, method, args, | |||
argumentNamesOpt: default(ImmutableArray<String?>), argumentRefKindsOpt: refKinds, isDelegateCall: false, expanded: false, invokedAsExtensionMethod: false, | |||
argsToParamsOpt: ImmutableArray<int>.Empty, defaultArguments: default(BitVector), resultKind: LookupResultKind.Viable, type: method.ReturnType) | |||
argsToParamsOpt: default, defaultArguments: default(BitVector), resultKind: LookupResultKind.Viable, type: method.ReturnType) |
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.
Because the utility Binder.GetCorrespondingParameter
that's used in this PR as part of CodeGenerator.MightEscapeTemporaryRefs
would fail on an assert - it expects argsToParamsOpt
to be either default or matching the number of arguments:
Debug.Assert(argumentOrdinal < argsToParamsOpt.Length); |
Which seems like a reasonable invariant which this callsite was violating.
@@ -673,6 +673,8 @@ private void EmitBlock(BoundBlock block) | |||
{ | |||
EmitUninstrumentedBlock(block); | |||
} | |||
|
|||
ReleaseBlockTemps(); |
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 cannot assume that this block necessary maps to one syntactically present in source
Why cannot we assume that? To me it seems we don't synthesize BoundBlocks.
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.
Why cannot we assume that? To me it seems we don't synthesize BoundBlocks.
Because we shouldn't be assuming that. There is nothing wrong in introducing a bound block.
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 is nothing wrong in introducing a bound block.
This comment suggests otherwise:
BoundBlock specify SCOPE (visibility) of a variable. |
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 comment suggests otherwise: ...
I am failing to guess what part of the comment you find relevant. Could you quote that part, etc.?
For example, SyntheticBoundNodeFactory
has a bunch of helpers to synthesize blocks and they are used for one reason or the other. There is also a way to synthesize a block manually. So, even if you haven't found an example, it doesn't mean there isn't one already, or that one won't be introduced in the future.
@@ -64,6 +64,23 @@ public override bool Equals(object? obj) | |||
// maps local identities to locals. | |||
private Dictionary<ILocalSymbolInternal, LocalDefinition>? _localMap; | |||
|
|||
// The lowered tree might define the same local symbol | |||
// in multiple sequences that are part of one expression, for example: |
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.
in multiple sequences that are part of one expression, for example:
This doesn't sound right. I think we should fix this, a local symbol should belong to exactly one scope in the bound tree. I assume this is a pre-existing condition, i.e. it is not introduced by this change. If so, I would prefer the fix to go into a separate PR.
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.
Sounds good, I will look into that.
Just to confirm: I think this "invalid" tree shape is introduced somewhere in lowering simply by reusing the same node twice, e.g., something like _factory.Call(arguments: [node, node])
, and that node
happens to contain a BoundSequence which then causes the local to be declared twice in the tree. So are you saying we should never do that (reuse the same node) and if we do it somewhere, it's a bug (hopefully a rare one)?
|
||
for (var arg = 0; arg < arguments.Length; arg++) | ||
{ | ||
var parameter = Binder.GetCorrespondingParameter( |
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.
Fixes #67435.
The idea is to have a "heuristic" to detect whether a method might capture the references passed to it. If such method call is detected, and a temporary reference is being emitted, we lift the temp to live for the whole block instead of just the expression.
The block lifetime is enough - ref safety analysis already checks refs to rvalues cannot escape blocks.
The heuristic is implemented by
CodeGenerator.MightEscapeTemporaryRefs
. It runs on the lowered nodes (because it's the emit layer which decides to emit a temporary). It might have false positives (some calls likeM(rvalue, out _)
might be marked by the heuristic as dangerous but they are not), but it shouldn't have false negatives.Without a heuristic, we would need to avoid reusing many more temps, which would be a regression (at least in IL size). But perhaps that's negligible and it would be better to avoid this complexity? I'm not sure.