Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 82 additions & 20 deletions src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1943,7 +1943,11 @@ private static void GetInvocationArgumentsForEscape(
parameters[argsToParamsOpt.IsDefault ? argIndex : argsToParamsOpt[argIndex]] :
null;

if (mixableArguments is not null && isMixableParameter(parameter))
if (mixableArguments is not null
&& isMixableParameter(parameter)
// assume any expression variable is a valid mixing destination,
// since we will infer a legal val-escape for it (if it doesn't already have a narrower one).
&& isMixableArgument(argument))
{
mixableArguments.Add(new MixableDestination(parameter, argument));
}
Expand All @@ -1968,6 +1972,9 @@ parameter is not null &&
parameter.Type.IsRefLikeType &&
parameter.RefKind.IsWritableReference();

static bool isMixableArgument(BoundExpression argument) =>
argument is not (BoundDeconstructValuePlaceholder or BoundLocal { DeclarationKind: not BoundLocalDeclarationKind.None });

static EscapeArgument getReceiver(Symbol symbol, BoundExpression receiver)
{
if (symbol is FunctionPointerMethodSymbol)
Expand Down Expand Up @@ -2195,6 +2202,30 @@ private bool UseUpdatedEscapeRulesForInvocation(Symbol symbol)
return method?.UseUpdatedEscapeRules == true;
}

private static bool ShouldInferDeclarationExpressionValEscape(BoundExpression argument, [NotNullWhen(true)] out SourceLocalSymbol? localSymbol)
{
var symbol = argument switch
{
BoundDeconstructValuePlaceholder p => p.ExpressionSymbol,
BoundLocal { DeclarationKind: not BoundLocalDeclarationKind.None } l => l.ExpressionSymbol,
_ => null
};
if (symbol is SourceLocalSymbol { ValEscapeScope: CallingMethodScope } local)
Copy link
Member Author

@RikkiGibson RikkiGibson Oct 6, 2022

Choose a reason for hiding this comment

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

note that we really would prefer to use a sentinel ValEscapeScope in cases like these (e.g. with value 1 << 32). I attempted to do this but it started to really churn and wanted to be expedient for the purposes of getting this fix in.

{
localSymbol = local;
return true;
}
else
{
// No need to infer a val escape for a global variable.
// These are only used in top-level statements in scripting mode,
// and since they are class fields, their scope is always CallingMethod.
Debug.Assert(symbol is null or SourceLocalSymbol or GlobalExpressionVariable);
localSymbol = null;
return false;
}
}

/// <summary>
/// Validates whether the invocation is valid per no-mixing rules.
/// Returns <see langword="false"/> when it is not valid and produces diagnostics (possibly more than one recursively) that helps to figure the reason.
Expand Down Expand Up @@ -2239,7 +2270,7 @@ private bool CheckInvocationArgMixing(
var escapeArguments = ArrayBuilder<EscapeArgument>.GetInstance();
GetInvocationArgumentsForEscape(
symbol,
receiver: null, // receiver handled explicitly below
receiverOpt,
parameters,
argsOpt,
argRefKindsOpt: default,
Expand All @@ -2252,43 +2283,51 @@ private bool CheckInvocationArgMixing(
{
foreach (var (_, argument, refKind) in escapeArguments)
{
if (ShouldInferDeclarationExpressionValEscape(argument, out _))
Copy link
Member Author

Choose a reason for hiding this comment

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

I think we also wanted to change this to use 'isMixableArgument'.

I think the behavior/factoring for the current release is going to be fine, we just want to make sure to iron these kinds of things out when we refactor in 17.5.

{
// assume any expression variable is a valid mixing destination,
// since we will infer a legal val-escape for it (if it doesn't already have a narrower one).
continue;
}

if (refKind.IsWritableReference() && argument.Type?.IsRefLikeType == true)
{
escapeTo = Math.Min(escapeTo, GetValEscape(argument, scopeOfTheContainingExpression));
}
}

if (escapeTo == scopeOfTheContainingExpression)
{
// cannot fail. common case.
return true;
}
var hasMixingError = false;

// track the widest scope that arguments could safely escape to.
// use this scope as the inferred STE of declaration expressions.
var inferredDestinationValEscape = CallingMethodScope;
foreach (var (parameter, argument, _) in escapeArguments)
{
var valid = CheckValEscape(argument.Syntax, argument, scopeOfTheContainingExpression, escapeTo, false, diagnostics);

if (!valid)
// in the old rules, we assume that refs cannot escape into ref struct variables.
// e.g. in `dest = M(ref arg)`, we assume `ref arg` will not escape into `dest`, but `arg` might.
inferredDestinationValEscape = Math.Max(inferredDestinationValEscape, GetValEscape(argument, scopeOfTheContainingExpression));
if (!hasMixingError && !CheckValEscape(argument.Syntax, argument, scopeOfTheContainingExpression, escapeTo, false, diagnostics))
{
string parameterName = GetInvocationParameterName(parameter);
Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, symbol, parameterName);
return false;
hasMixingError = true;
}
}

foreach (var (_, argument, _) in escapeArguments)
{
if (ShouldInferDeclarationExpressionValEscape(argument, out var localSymbol))
{
localSymbol.SetValEscape(inferredDestinationValEscape);
}
}
Comment on lines +2317 to 2323
Copy link
Member

Choose a reason for hiding this comment

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

This code is the same between new and updated rules. Should it be factored otu?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it is just beneath the bar of factoring out.


return !hasMixingError;
}
finally
{
escapeArguments.Free();
}

// check val escape of receiver if ref-like
if (receiverOpt?.Type?.IsRefLikeType == true)
{
// Should we also report ErrorCode.ERR_CallArgMixing if CheckValEscape() fails?
return CheckValEscape(receiverOpt.Syntax, receiverOpt, scopeOfTheContainingExpression, escapeTo, false, diagnostics);
}

return true;
}

private bool CheckInvocationArgMixingWithUpdatedRules(
Expand Down Expand Up @@ -2352,9 +2391,32 @@ private bool CheckInvocationArgMixingWithUpdatedRules(
}
}

inferDeclarationExpressionValEscape();

mixableArguments.Free();
escapeValues.Free();
return valid;

void inferDeclarationExpressionValEscape()
{
// find the widest scope that arguments could safely escape to.
// use this scope as the inferred STE of declaration expressions.
var inferredDestinationValEscape = CallingMethodScope;
foreach (var (_, fromArg, _, isRefEscape) in escapeValues)
{
inferredDestinationValEscape = Math.Max(inferredDestinationValEscape, isRefEscape
? GetRefEscape(fromArg, scopeOfTheContainingExpression)
: GetValEscape(fromArg, scopeOfTheContainingExpression));
}

foreach (var (_, fromArg, _, _) in escapeValues)
{
if (ShouldInferDeclarationExpressionValEscape(fromArg, out var localSymbol))
{
localSymbol.SetValEscape(inferredDestinationValEscape);
}
}
}
}

private static bool IsReceiverRefReadOnly(Symbol methodOrPropertySymbol) => methodOrPropertySymbol switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ internal virtual void SetRefEscape(uint value)

internal virtual void SetValEscape(uint value)
{
// either we should be setting the val escape for the first time,
// or not contradicting what was set before.
Debug.Assert(
_valEscapeScope == Binder.CallingMethodScope
|| _valEscapeScope == value);
_valEscapeScope = value;
}

Expand Down
Loading