From 2b0180d76a11dd1dd6961aee751563cf0c970c06 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Tue, 1 Apr 2025 15:39:31 +0200 Subject: [PATCH 1/8] Extensions: ref safety analysis --- .../Portable/Binder/Binder.ValueChecks.cs | 53 ++- .../Test/Emit3/Semantics/ExtensionTests.cs | 317 ++++++++++++++++++ 2 files changed, 354 insertions(+), 16 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 1bf76dd56b12..071d89eb507b 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.CodeGen; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -2253,7 +2252,8 @@ private bool CheckInvocationEscapeWithUpdatedRules( // For consistency with C#10 implementation, we don't report an additional error // for the receiver. (In both implementations, the call to Check*Escape() above // will have reported a specific escape error for the receiver though.) - if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && symbol is not SignatureOnlyMethodSymbol) + bool argumentIsReceiver = (object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) == receiver; + if ((!argumentIsReceiver || param?.IsExtensionParameter() == true) && symbol is not SignatureOnlyMethodSymbol) { ReportInvocationEscapeError(syntax, symbol, param, checkingReceiver, diagnostics); } @@ -2289,25 +2289,46 @@ private void GetInvocationArgumentsForEscape( { if (receiver is { }) { - Debug.Assert(receiver.Type is { }); - Debug.Assert(receiverIsSubjectToCloning != ThreeState.Unknown); - var method = methodInfo.Method; - if (receiverIsSubjectToCloning == ThreeState.True) + MethodSymbol? method = methodInfo.Method; + if (method?.GetIsNewExtensionMember() == true) { - Debug.Assert(receiver is not BoundValuePlaceholderBase && method is not null && receiver.Type?.IsReferenceType == false); + // Analyze the receiver as an argument + var parameter = method.ContainingType.ExtensionParameter; + + 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(receiver)) + { + mixableArguments.Add(new MixableDestination(parameter, receiver)); + } + + var refKind = parameter?.RefKind ?? RefKind.None; + + escapeArguments.Add(new EscapeArgument(parameter, receiver, refKind)); + } + else + { + Debug.Assert(receiver.Type is { }); + Debug.Assert(receiverIsSubjectToCloning != ThreeState.Unknown); + if (receiverIsSubjectToCloning == ThreeState.True) + { + Debug.Assert(receiver is not BoundValuePlaceholderBase && method is not null && receiver.Type?.IsReferenceType == false); #if DEBUG - AssertVisited(receiver); + AssertVisited(receiver); #endif - // Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration - receiver = new BoundCapturedReceiverPlaceholder(receiver.Syntax, receiver, _localScopeDepth, receiver.Type).MakeCompilerGenerated(); - } + // Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration + receiver = new BoundCapturedReceiverPlaceholder(receiver.Syntax, receiver, _localScopeDepth, receiver.Type).MakeCompilerGenerated(); + } - var tuple = getReceiver(methodInfo, receiver); - escapeArguments.Add(tuple); + var tuple = getReceiver(methodInfo, receiver); + escapeArguments.Add(tuple); - if (mixableArguments is not null && isMixableParameter(tuple.Parameter)) - { - mixableArguments.Add(new MixableDestination(tuple.Parameter, receiver)); + if (mixableArguments is not null && isMixableParameter(tuple.Parameter)) + { + mixableArguments.Add(new MixableDestination(tuple.Parameter, receiver)); + } } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 8d7496e27f05..4233463ff76b 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -32640,4 +32640,321 @@ static class E comp.VerifyEmitDiagnostics(); CompileAndVerify(comp, expectedOutput: "ran ran"); } + + [Fact] + public void RefAnalysis_Invocation_01() + { + string source = """ +class C +{ + ref int M2() + { + int i = 0; + return ref i.M(); + } + + ref int M3() + { + int i = 0; + return ref E.M(ref i); + } +} + +static class E +{ + extension(ref int i) + { + public ref int M() => ref i; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,20): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // return ref i.M(); + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(6, 20), + // (6,20): error CS8347: Cannot use a result of 'E.extension(ref int).M()' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref i.M(); + Diagnostic(ErrorCode.ERR_EscapeCall, "i.M()").WithArguments("E.extension(ref int).M()", "i").WithLocation(6, 20), + // (12,20): error CS8347: Cannot use a result of 'E.M(ref int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref E.M(ref i); + Diagnostic(ErrorCode.ERR_EscapeCall, "E.M(ref i)").WithArguments("E.M(ref int)", "i").WithLocation(12, 20), + // (12,28): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // return ref E.M(ref i); + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(12, 28)); + } + + [Fact] + public void RefAnalysis_Invocation_02() + { + string source = """ +class C +{ + ref int M2() + { + int i = 0; + ref int ri = ref i; + return ref ri.M(); + } + + ref int M3() + { + int i = 0; + ref int ri = ref i; + return ref E.M(ref ri); + } +} + +static class E +{ + extension(ref int i) + { + public ref int M() => ref i; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,20): error CS8157: Cannot return 'ri' by reference because it was initialized to a value that cannot be returned by reference + // return ref ri.M(); + Diagnostic(ErrorCode.ERR_RefReturnNonreturnableLocal, "ri").WithArguments("ri").WithLocation(7, 20), + // (7,20): error CS8347: Cannot use a result of 'E.extension(ref int).M()' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref ri.M(); + Diagnostic(ErrorCode.ERR_EscapeCall, "ri.M()").WithArguments("E.extension(ref int).M()", "i").WithLocation(7, 20), + // (14,20): error CS8347: Cannot use a result of 'E.M(ref int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref E.M(ref ri); + Diagnostic(ErrorCode.ERR_EscapeCall, "E.M(ref ri)").WithArguments("E.M(ref int)", "i").WithLocation(14, 20), + // (14,28): error CS8157: Cannot return 'ri' by reference because it was initialized to a value that cannot be returned by reference + // return ref E.M(ref ri); + Diagnostic(ErrorCode.ERR_RefReturnNonreturnableLocal, "ri").WithArguments("ri").WithLocation(14, 28)); + } + + [Fact] + public void RefAnalysis_Invocation_03() + { + string source = """ +class C +{ + RS MA(RS rs) => rs.M1(); + RS MB(RS rs) => E.M1(rs); + + ref RS MC(ref RS rs) => ref rs.M2(); + ref RS MD(ref RS rs) => ref E.M2(ref rs); +} + +static class E +{ + extension(RS rs) + { + public RS M1() => rs; + } + extension(ref RS rs) + { + public ref RS M2() => ref rs; + } +} + +ref struct RS { } +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void RefAnalysis_Invocation_04() + { + string source = """ +class C +{ + ref int MA(ref int a) + { + int b = 42; + return ref a.M(ref b); // 1 + } + ref int MB(ref int a) + { + int b = 42; + return ref b.M(ref a); // 2 + } + ref int MC(ref int a) + { + int b = 42; + return ref E.M(ref a, ref b); // 3 + } +} + +static class E +{ + extension(ref int i) + { + public ref int M(ref int j) => throw null; + } +} + +ref struct RS { } +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,20): error CS8347: Cannot use a result of 'E.extension(ref int).M(ref int)' in this context because it may expose variables referenced by parameter 'j' outside of their declaration scope + // return ref a.M(ref b); // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "a.M(ref b)").WithArguments("E.extension(ref int).M(ref int)", "j").WithLocation(6, 20), + // (6,28): error CS8168: Cannot return local 'b' by reference because it is not a ref local + // return ref a.M(ref b); // 1 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "b").WithArguments("b").WithLocation(6, 28), + // (11,20): error CS8168: Cannot return local 'b' by reference because it is not a ref local + // return ref b.M(ref a); // 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "b").WithArguments("b").WithLocation(11, 20), + // (11,20): error CS8347: Cannot use a result of 'E.extension(ref int).M(ref int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref b.M(ref a); // 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "b.M(ref a)").WithArguments("E.extension(ref int).M(ref int)", "i").WithLocation(11, 20), + // (16,20): error CS8347: Cannot use a result of 'E.M(ref int, ref int)' in this context because it may expose variables referenced by parameter 'j' outside of their declaration scope + // return ref E.M(ref a, ref b); // 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "E.M(ref a, ref b)").WithArguments("E.M(ref int, ref int)", "j").WithLocation(16, 20), + // (16,35): error CS8168: Cannot return local 'b' by reference because it is not a ref local + // return ref E.M(ref a, ref b); // 3 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "b").WithArguments("b").WithLocation(16, 35)); + } + + [Fact] + public void RefAnalysis_Invocation_05() + { + var text = """ +class Program +{ + void Test1() + { + S1 rOuter = default; + + System.Span inner = stackalloc int[1]; + S1 rInner = MayWrap(ref inner); + + rOuter.MayAssign(ref rOuter); + rInner.MayAssign(ref rInner); + + rOuter.MayAssign(ref rInner); // 1 + rInner.MayAssign(ref rOuter); // 2 + } + + static S1 MayWrap(ref System.Span arg) => default; +} + +ref struct S1 { } + +static class E +{ + extension(ref S1 arg1) + { + public void MayAssign(ref S1 arg2) { arg1 = arg2; } + } +} +"""; + var comp = CreateCompilation(text, targetFramework: TargetFramework.Net90); + comp.VerifyDiagnostics( + // (13,30): error CS8352: Cannot use variable 'rInner' in this context because it may expose referenced variables outside of their declaration scope + // rOuter.MayAssign(ref rInner); // 1 + Diagnostic(ErrorCode.ERR_EscapeVariable, "rInner").WithArguments("rInner").WithLocation(13, 30), + // (13,9): error CS8350: This combination of arguments to 'E.extension(ref S1).MayAssign(ref S1)' is disallowed because it may expose variables referenced by parameter 'arg2' outside of their declaration scope + // rOuter.MayAssign(ref rInner); // 1 + Diagnostic(ErrorCode.ERR_CallArgMixing, "rOuter.MayAssign(ref rInner)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg2").WithLocation(13, 9), + // (14,9): error CS8352: Cannot use variable 'rInner' in this context because it may expose referenced variables outside of their declaration scope + // rInner.MayAssign(ref rOuter); // 2 + Diagnostic(ErrorCode.ERR_EscapeVariable, "rInner").WithArguments("rInner").WithLocation(14, 9), + // (14,9): error CS8350: This combination of arguments to 'E.extension(ref S1).MayAssign(ref S1)' is disallowed because it may expose variables referenced by parameter 'arg1' outside of their declaration scope + // rInner.MayAssign(ref rOuter); // 2 + Diagnostic(ErrorCode.ERR_CallArgMixing, "rInner.MayAssign(ref rOuter)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg1").WithLocation(14, 9)); + } + + [Fact] + public void RefAnalysis_PropertyAccess_01() + { + string source = """ +class C +{ + ref int M2() + { + int i = 0; + return ref i.P; + } + + ref int M3() + { + int i = 0; + return ref E.get_P(ref i); + } +} + +static class E +{ + extension(ref int i) + { + public ref int P => ref i; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,20): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // return ref i.P; + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(6, 20), + // (6,20): error CS8347: Cannot use a result of 'E.extension(ref int).P' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref i.P; + Diagnostic(ErrorCode.ERR_EscapeCall, "i.P").WithArguments("E.extension(ref int).P", "i").WithLocation(6, 20), + // (12,20): error CS8347: Cannot use a result of 'E.get_P(ref int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref E.get_P(ref i); + Diagnostic(ErrorCode.ERR_EscapeCall, "E.get_P(ref i)").WithArguments("E.get_P(ref int)", "i").WithLocation(12, 20), + // (12,32): error CS8168: Cannot return local 'i' by reference because it is not a ref local + // return ref E.get_P(ref i); + Diagnostic(ErrorCode.ERR_RefReturnLocal, "i").WithArguments("i").WithLocation(12, 32)); + } + + [Fact] + public void RefAnalysis_PropertyAccess_02() + { + string source = """ +class C +{ + ref int M2() + { + int i = 0; + ref int ri = ref i; + return ref ri.P; + } + + ref int M3() + { + int i = 0; + ref int ri = ref i; + return ref E.get_P(ref ri); + } +} + +static class E +{ + extension(ref int i) + { + public ref int P => ref i; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,20): error CS8157: Cannot return 'ri' by reference because it was initialized to a value that cannot be returned by reference + // return ref ri.P; + Diagnostic(ErrorCode.ERR_RefReturnNonreturnableLocal, "ri").WithArguments("ri").WithLocation(7, 20), + // (7,20): error CS8347: Cannot use a result of 'E.extension(ref int).P' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref ri.P; + Diagnostic(ErrorCode.ERR_EscapeCall, "ri.P").WithArguments("E.extension(ref int).P", "i").WithLocation(7, 20), + // (14,20): error CS8347: Cannot use a result of 'E.get_P(ref int)' in this context because it may expose variables referenced by parameter 'i' outside of their declaration scope + // return ref E.get_P(ref ri); + Diagnostic(ErrorCode.ERR_EscapeCall, "E.get_P(ref ri)").WithArguments("E.get_P(ref int)", "i").WithLocation(14, 20), + // (14,32): error CS8157: Cannot return 'ri' by reference because it was initialized to a value that cannot be returned by reference + // return ref E.get_P(ref ri); + Diagnostic(ErrorCode.ERR_RefReturnNonreturnableLocal, "ri").WithArguments("ri").WithLocation(14, 32)); + } } From 4e6ab4a26fc7eda9bf2b6cf82c627265b94cf7f0 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Thu, 3 Apr 2025 19:35:34 +0200 Subject: [PATCH 2/8] Revert code change --- .../Portable/Binder/Binder.ValueChecks.cs | 53 ++++++------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 071d89eb507b..1bf76dd56b12 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.CodeGen; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -2252,8 +2253,7 @@ private bool CheckInvocationEscapeWithUpdatedRules( // For consistency with C#10 implementation, we don't report an additional error // for the receiver. (In both implementations, the call to Check*Escape() above // will have reported a specific escape error for the receiver though.) - bool argumentIsReceiver = (object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) == receiver; - if ((!argumentIsReceiver || param?.IsExtensionParameter() == true) && symbol is not SignatureOnlyMethodSymbol) + if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && symbol is not SignatureOnlyMethodSymbol) { ReportInvocationEscapeError(syntax, symbol, param, checkingReceiver, diagnostics); } @@ -2289,46 +2289,25 @@ private void GetInvocationArgumentsForEscape( { if (receiver is { }) { - MethodSymbol? method = methodInfo.Method; - if (method?.GetIsNewExtensionMember() == true) + Debug.Assert(receiver.Type is { }); + Debug.Assert(receiverIsSubjectToCloning != ThreeState.Unknown); + var method = methodInfo.Method; + if (receiverIsSubjectToCloning == ThreeState.True) { - // Analyze the receiver as an argument - var parameter = method.ContainingType.ExtensionParameter; - - 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(receiver)) - { - mixableArguments.Add(new MixableDestination(parameter, receiver)); - } - - var refKind = parameter?.RefKind ?? RefKind.None; - - escapeArguments.Add(new EscapeArgument(parameter, receiver, refKind)); - } - else - { - Debug.Assert(receiver.Type is { }); - Debug.Assert(receiverIsSubjectToCloning != ThreeState.Unknown); - if (receiverIsSubjectToCloning == ThreeState.True) - { - Debug.Assert(receiver is not BoundValuePlaceholderBase && method is not null && receiver.Type?.IsReferenceType == false); + Debug.Assert(receiver is not BoundValuePlaceholderBase && method is not null && receiver.Type?.IsReferenceType == false); #if DEBUG - AssertVisited(receiver); + AssertVisited(receiver); #endif - // Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration - receiver = new BoundCapturedReceiverPlaceholder(receiver.Syntax, receiver, _localScopeDepth, receiver.Type).MakeCompilerGenerated(); - } + // Equivalent to a non-ref local with the underlying receiver as an initializer provided at declaration + receiver = new BoundCapturedReceiverPlaceholder(receiver.Syntax, receiver, _localScopeDepth, receiver.Type).MakeCompilerGenerated(); + } - var tuple = getReceiver(methodInfo, receiver); - escapeArguments.Add(tuple); + var tuple = getReceiver(methodInfo, receiver); + escapeArguments.Add(tuple); - if (mixableArguments is not null && isMixableParameter(tuple.Parameter)) - { - mixableArguments.Add(new MixableDestination(tuple.Parameter, receiver)); - } + if (mixableArguments is not null && isMixableParameter(tuple.Parameter)) + { + mixableArguments.Add(new MixableDestination(tuple.Parameter, receiver)); } } From eb391b988cf83f345173eda66c036b23509ebc9f Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Thu, 3 Apr 2025 21:52:55 +0200 Subject: [PATCH 3/8] Alternate approach --- .../Portable/Binder/Binder.ValueChecks.cs | 123 +++++++++++++++--- .../Test/Emit3/Semantics/ExtensionTests.cs | 47 ++++++- 2 files changed, 152 insertions(+), 18 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 1bf76dd56b12..cc934373d494 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -85,6 +85,34 @@ internal static MethodInfo Create(PropertySymbol property, AccessorKind accessor internal static MethodInfo Create(BoundIndexerAccess expr) => Create(expr.Indexer, expr.AccessorKind); + internal MethodInfo ReplaceWithExtensionImplementation(out bool wasError) + { + var method = replace(Method); + var setMethod = replace(SetMethod); + Symbol symbol = ReferenceEquals(Symbol, Method) && method is not null ? method : Symbol; + + wasError = (Method is not null && method is null) || (SetMethod is not null && setMethod is null); + + return new MethodInfo(symbol, method, setMethod); + + static MethodSymbol? replace(MethodSymbol? method) + { + if (method is null) + { + return null; + } + + if (method.OriginalDefinition.TryGetCorrespondingExtensionImplementationMethod() is MethodSymbol implementationMethod) + { + return implementationMethod.AsMember(method.ContainingSymbol.ContainingType). + ConstructIfGeneric(method.ContainingType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Concat(method.TypeArgumentsWithAnnotations)); + } + + // Tracked by https://github.com/dotnet/roslyn/issues/76130 : Test this code path + return null; + } + } + public override string? ToString() => Method?.ToString(); } @@ -1971,9 +1999,12 @@ bool isRefEscape Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt)); #endif + MethodInfo localMethodInfo = methodInfo; + ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + if (methodInfo.UseUpdatedEscapeRules) { - return GetInvocationEscapeWithUpdatedRules(methodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, isRefEscape); + return GetInvocationEscapeWithUpdatedRules(localMethodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, isRefEscape); } // SPEC: (also applies to the CheckInvocationEscape counterpart) @@ -1991,7 +2022,7 @@ bool isRefEscape SafeContext escapeScope = SafeContext.CallingMethod; var escapeValues = ArrayBuilder.GetInstance(); GetEscapeValuesForOldRules( - in methodInfo, + in localMethodInfo, // Receiver handled explicitly below receiver: null, receiverIsSubjectToCloning: ThreeState.Unknown, @@ -2036,7 +2067,7 @@ bool isRefEscape } // check receiver if ref-like - if (methodInfo.Method?.RequiresInstanceReceiver == true && receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) + if (localMethodInfo.Method?.RequiresInstanceReceiver == true && receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) { escapeScope = escapeScope.Intersect(GetValEscape(receiver, localScopeDepth)); } @@ -2100,6 +2131,57 @@ private SafeContext GetInvocationEscapeWithUpdatedRules( return escapeScope; } + private void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo methodInfo, ref ImmutableArray parameters, + ref BoundExpression? receiver, ref ImmutableArray argsOpt, ref ImmutableArray argRefKindsOpt, + ref ImmutableArray argsToParamsOpt) + { + Symbol? symbol = methodInfo.Symbol; + if (symbol?.GetIsNewExtensionMember() != true) + { + return; + } + + var extensionParameter = symbol.ContainingType.ExtensionParameter; + Debug.Assert(extensionParameter is not null); + Debug.Assert(receiver is not null); + methodInfo = methodInfo.ReplaceWithExtensionImplementation(out bool wasError); + if (wasError) + { + return; + } + + parameters = parameters.IsDefault ? [extensionParameter] : [extensionParameter, .. parameters]; + argsOpt = argsOpt.IsDefault ? [receiver] : [receiver, .. argsOpt]; + receiver = null; + + if (argRefKindsOpt.IsDefault) + { + if (extensionParameter.RefKind == RefKind.Ref) + { + var argRefKindsBuilder = ArrayBuilder.GetInstance(argsOpt.Length + 1, fillWithValue: RefKind.None); + argRefKindsBuilder[0] = RefKind.Ref; + argRefKindsOpt = argRefKindsBuilder.ToImmutableAndFree(); + } + } + else + { + var receiverRefKind = extensionParameter.RefKind == RefKind.Ref ? RefKind.Ref : RefKind.None; + argRefKindsOpt = [receiverRefKind, .. argRefKindsOpt]; + } + + if (!argsToParamsOpt.IsDefault) + { + var argsToParamsBuilder = ArrayBuilder.GetInstance(argsToParamsOpt.Length + 1); + argsToParamsBuilder.Add(0); + for (int i = 0; i < argsToParamsOpt.Length; i++) + { + argsToParamsBuilder.Add(argsToParamsOpt[i] + 1); + } + + argsToParamsOpt = argsToParamsBuilder.ToImmutableAndFree(); + } + } + /// /// Validates whether given invocation can allow its results to escape from level to level. /// The result indicates whether the escape is possible. @@ -2128,9 +2210,12 @@ bool isRefEscape Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt)); #endif + MethodInfo localMethodInfo = methodInfo; + ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + if (methodInfo.UseUpdatedEscapeRules) { - return CheckInvocationEscapeWithUpdatedRules(syntax, methodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, checkingReceiver, escapeFrom, escapeTo, diagnostics, isRefEscape); + return CheckInvocationEscapeWithUpdatedRules(syntax, localMethodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, checkingReceiver, escapeFrom, escapeTo, diagnostics, isRefEscape, methodInfo.Symbol); } // SPEC: @@ -2139,7 +2224,7 @@ bool isRefEscape // o no ref or out argument(excluding the receiver and arguments of ref-like types) may have a narrower ref-safe-to-escape than E1; and // o no argument(including the receiver) may have a narrower safe-to-escape than E1. - var symbol = methodInfo.Symbol; + var symbol = localMethodInfo.Symbol; if (!symbol.RequiresInstanceReceiver()) { // ignore receiver when symbol is static @@ -2148,7 +2233,7 @@ bool isRefEscape var escapeArguments = ArrayBuilder.GetInstance(); GetInvocationArgumentsForEscape( - methodInfo, + localMethodInfo, receiver: null, // receiver handled explicitly below receiverIsSubjectToCloning: ThreeState.Unknown, parameters, @@ -2180,7 +2265,7 @@ bool isRefEscape { if (symbol is not SignatureOnlyMethodSymbol) { - ReportInvocationEscapeError(syntax, symbol, parameter, checkingReceiver, diagnostics); + ReportInvocationEscapeError(syntax, methodInfo.Symbol, parameter, checkingReceiver, diagnostics); } return false; @@ -2214,7 +2299,8 @@ private bool CheckInvocationEscapeWithUpdatedRules( SafeContext escapeFrom, SafeContext escapeTo, BindingDiagnosticBag diagnostics, - bool isRefEscape) + bool isRefEscape, + Symbol symbolForReporting) { bool result = true; @@ -2231,7 +2317,6 @@ private bool CheckInvocationEscapeWithUpdatedRules( ignoreArglistRefKinds: true, // https://github.com/dotnet/roslyn/issues/63325: for compatibility with C#10 implementation. argsAndParamsAll); - var symbol = methodInfo.Symbol; var returnsRefToRefStruct = methodInfo.ReturnsRefToRefStruct; foreach (var (param, argument, _, isArgumentRefEscape) in argsAndParamsAll) { @@ -2253,9 +2338,9 @@ private bool CheckInvocationEscapeWithUpdatedRules( // For consistency with C#10 implementation, we don't report an additional error // for the receiver. (In both implementations, the call to Check*Escape() above // will have reported a specific escape error for the receiver though.) - if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && symbol is not SignatureOnlyMethodSymbol) + if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && (Symbol?)methodInfo.Symbol is not SignatureOnlyMethodSymbol) { - ReportInvocationEscapeError(syntax, symbol, param, checkingReceiver, diagnostics); + ReportInvocationEscapeError(syntax, symbolForReporting, param, checkingReceiver, diagnostics); } result = false; break; @@ -2778,9 +2863,12 @@ private bool CheckInvocationArgMixing( SafeContext localScopeDepth, BindingDiagnosticBag diagnostics) { + MethodInfo localMethodInfo = methodInfo; + ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiverOpt, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + if (methodInfo.UseUpdatedEscapeRules) { - return CheckInvocationArgMixingWithUpdatedRules(syntax, methodInfo, receiverOpt, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, diagnostics); + return CheckInvocationArgMixingWithUpdatedRules(syntax, localMethodInfo, receiverOpt, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, diagnostics, methodInfo.Symbol); } // SPEC: @@ -2788,7 +2876,7 @@ private bool CheckInvocationArgMixing( // - If there is a ref or out argument of a ref struct type (including the receiver), with safe-to-escape E1, then // - no argument (including the receiver) may have a narrower safe-to-escape than E1. - var symbol = methodInfo.Symbol; + var symbol = localMethodInfo.Symbol; if (!symbol.RequiresInstanceReceiver()) { // ignore receiver when symbol is static @@ -2801,7 +2889,7 @@ private bool CheckInvocationArgMixing( // collect all writeable ref-like arguments, including receiver var escapeArguments = ArrayBuilder.GetInstance(); GetInvocationArgumentsForEscape( - methodInfo, + localMethodInfo, receiverOpt, receiverIsSubjectToCloning, parameters, @@ -2845,7 +2933,7 @@ private bool CheckInvocationArgMixing( if (!hasMixingError && !CheckValEscape(argument.Syntax, argument, localScopeDepth, escapeTo, false, diagnostics)) { string parameterName = GetInvocationParameterName(parameter); - Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, symbol, parameterName); + Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, methodInfo.Symbol, parameterName); hasMixingError = true; } } @@ -2876,7 +2964,8 @@ private bool CheckInvocationArgMixingWithUpdatedRules( ImmutableArray argRefKindsOpt, ImmutableArray argsToParamsOpt, SafeContext localScopeDepth, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + Symbol symbolForReporting) { var mixableArguments = ArrayBuilder.GetInstance(); var escapeValues = ArrayBuilder.GetInstance(); @@ -2913,7 +3002,7 @@ private bool CheckInvocationArgMixingWithUpdatedRules( if (!valid) { string parameterName = GetInvocationParameterName(fromParameter); - Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, methodInfo.Symbol, parameterName); + Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, symbolForReporting, parameterName); break; } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 4233463ff76b..0b9e25f34512 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -32837,6 +32837,7 @@ void Test1() rOuter.MayAssign(ref rInner); // 1 rInner.MayAssign(ref rOuter); // 2 + rInner.MayAssign(arg2: ref rOuter); // 3 } static S1 MayWrap(ref System.Span arg) => default; @@ -32865,7 +32866,51 @@ static class E Diagnostic(ErrorCode.ERR_EscapeVariable, "rInner").WithArguments("rInner").WithLocation(14, 9), // (14,9): error CS8350: This combination of arguments to 'E.extension(ref S1).MayAssign(ref S1)' is disallowed because it may expose variables referenced by parameter 'arg1' outside of their declaration scope // rInner.MayAssign(ref rOuter); // 2 - Diagnostic(ErrorCode.ERR_CallArgMixing, "rInner.MayAssign(ref rOuter)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg1").WithLocation(14, 9)); + Diagnostic(ErrorCode.ERR_CallArgMixing, "rInner.MayAssign(ref rOuter)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg1").WithLocation(14, 9), + // (15,9): error CS8352: Cannot use variable 'rInner' in this context because it may expose referenced variables outside of their declaration scope + // rInner.MayAssign(arg2: ref rOuter); // 3 + Diagnostic(ErrorCode.ERR_EscapeVariable, "rInner").WithArguments("rInner").WithLocation(15, 9), + // (15,9): error CS8350: This combination of arguments to 'E.extension(ref S1).MayAssign(ref S1)' is disallowed because it may expose variables referenced by parameter 'arg1' outside of their declaration scope + // rInner.MayAssign(arg2: ref rOuter); // 3 + Diagnostic(ErrorCode.ERR_CallArgMixing, "rInner.MayAssign(arg2: ref rOuter)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg1").WithLocation(15, 9)); + } + + [Fact] + public void RefAnalysis_Invocation_06() + { + string source = """ +class C +{ + void M2(ref int j) + { + int i = 0; + j = ref i.M(); + } + + void M3(ref int j) + { + int i = 0; + j = ref E.M(ref i); + } +} + +static class E +{ + extension(ref int i) + { + public ref int M() => ref i; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,9): error CS8374: Cannot ref-assign 'i.M()' to 'j' because 'i.M()' has a narrower escape scope than 'j'. + // j = ref i.M(); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M()").WithArguments("j", "i.M()").WithLocation(6, 9), + // (12,9): error CS8374: Cannot ref-assign 'E.M(ref i)' to 'j' because 'E.M(ref i)' has a narrower escape scope than 'j'. + // j = ref E.M(ref i); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref E.M(ref i)").WithArguments("j", "E.M(ref i)").WithLocation(12, 9)); } [Fact] From 7cdc808496e41e41569932bee53729a6fc829592 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Fri, 4 Apr 2025 08:34:08 +0200 Subject: [PATCH 4/8] Comment --- src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index cc934373d494..79aaf95e6b6e 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -93,6 +93,7 @@ internal MethodInfo ReplaceWithExtensionImplementation(out bool wasError) wasError = (Method is not null && method is null) || (SetMethod is not null && setMethod is null); + // Tracked by https://github.com/dotnet/roslyn/issues/76130 : Test with indexers (ie. "method") and in compound assignment (ie. "setMethod") return new MethodInfo(symbol, method, setMethod); static MethodSymbol? replace(MethodSymbol? method) From 87979ccac64afff6081f62697e7231824e82ef8c Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Sat, 5 Apr 2025 11:50:13 +0200 Subject: [PATCH 5/8] Address feedback --- .../Portable/Binder/Binder.ValueChecks.cs | 19 +++------- .../Test/Emit3/Semantics/ExtensionTests.cs | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 79aaf95e6b6e..ab10e6dcb0d1 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -2132,7 +2132,7 @@ private SafeContext GetInvocationEscapeWithUpdatedRules( return escapeScope; } - private void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo methodInfo, ref ImmutableArray parameters, + private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo methodInfo, ref ImmutableArray parameters, ref BoundExpression? receiver, ref ImmutableArray argsOpt, ref ImmutableArray argRefKindsOpt, ref ImmutableArray argsToParamsOpt) { @@ -2145,29 +2145,20 @@ private void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo methodInf var extensionParameter = symbol.ContainingType.ExtensionParameter; Debug.Assert(extensionParameter is not null); Debug.Assert(receiver is not null); - methodInfo = methodInfo.ReplaceWithExtensionImplementation(out bool wasError); + MethodInfo replacedMethodInfo = methodInfo.ReplaceWithExtensionImplementation(out bool wasError); if (wasError) { return; } + methodInfo = replacedMethodInfo; parameters = parameters.IsDefault ? [extensionParameter] : [extensionParameter, .. parameters]; argsOpt = argsOpt.IsDefault ? [receiver] : [receiver, .. argsOpt]; receiver = null; - if (argRefKindsOpt.IsDefault) + if (!argRefKindsOpt.IsDefault) { - if (extensionParameter.RefKind == RefKind.Ref) - { - var argRefKindsBuilder = ArrayBuilder.GetInstance(argsOpt.Length + 1, fillWithValue: RefKind.None); - argRefKindsBuilder[0] = RefKind.Ref; - argRefKindsOpt = argRefKindsBuilder.ToImmutableAndFree(); - } - } - else - { - var receiverRefKind = extensionParameter.RefKind == RefKind.Ref ? RefKind.Ref : RefKind.None; - argRefKindsOpt = [receiverRefKind, .. argRefKindsOpt]; + argRefKindsOpt = [RefKind.None, .. argRefKindsOpt]; } if (!argsToParamsOpt.IsDefault) diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 0b9e25f34512..17e113b9941d 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -32913,6 +32913,44 @@ static class E Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref E.M(ref i)").WithArguments("j", "E.M(ref i)").WithLocation(12, 9)); } + [Fact] + public void RefAnalysis_Invocation_07() + { + string source = """ +class C +{ + void MA(ref readonly int j) + { + int i = 0; + j = ref i.M(); + j = ref E.M(ref i); + j = ref i.M2(); + } +} + +static class E +{ + extension(ref readonly int i) + { + public ref readonly int M() => ref i; + } + public static ref readonly int M2(this ref readonly int i) => ref i; +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,9): error CS8374: Cannot ref-assign 'i.M()' to 'j' because 'i.M()' has a narrower escape scope than 'j'. + // j = ref i.M(); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M()").WithArguments("j", "i.M()").WithLocation(6, 9), + // (7,9): error CS8374: Cannot ref-assign 'E.M(ref i)' to 'j' because 'E.M(ref i)' has a narrower escape scope than 'j'. + // j = ref E.M(ref i); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref E.M(ref i)").WithArguments("j", "E.M(ref i)").WithLocation(7, 9), + // (8,9): error CS8374: Cannot ref-assign 'i.M2()' to 'j' because 'i.M2()' has a narrower escape scope than 'j'. + // j = ref i.M2(); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M2()").WithArguments("j", "i.M2()").WithLocation(8, 9)); + } + [Fact] public void RefAnalysis_PropertyAccess_01() { From a49e05225d9172d48e1c808c5bb1683b0befa7fb Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 7 Apr 2025 09:09:17 +0200 Subject: [PATCH 6/8] Skip if static. Use new parameters for clarity --- .../Portable/Binder/Binder.ValueChecks.cs | 9 +- .../Test/Emit3/Semantics/ExtensionTests.cs | 163 ++++++++++++++++++ 2 files changed, 167 insertions(+), 5 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index ab10e6dcb0d1..14fb275b9d9e 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -2137,14 +2137,11 @@ private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo me ref ImmutableArray argsToParamsOpt) { Symbol? symbol = methodInfo.Symbol; - if (symbol?.GetIsNewExtensionMember() != true) + if (symbol?.GetIsNewExtensionMember() != true || symbol.IsStatic) { return; } - var extensionParameter = symbol.ContainingType.ExtensionParameter; - Debug.Assert(extensionParameter is not null); - Debug.Assert(receiver is not null); MethodInfo replacedMethodInfo = methodInfo.ReplaceWithExtensionImplementation(out bool wasError); if (wasError) { @@ -2152,7 +2149,9 @@ private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo me } methodInfo = replacedMethodInfo; - parameters = parameters.IsDefault ? [extensionParameter] : [extensionParameter, .. parameters]; + + Debug.Assert(receiver is not null); + parameters = methodInfo.Symbol.GetParameters(); argsOpt = argsOpt.IsDefault ? [receiver] : [receiver, .. argsOpt]; receiver = null; diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index 17e113b9941d..4f4fced32472 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -32875,6 +32875,37 @@ static class E Diagnostic(ErrorCode.ERR_CallArgMixing, "rInner.MayAssign(arg2: ref rOuter)").WithArguments("E.extension(ref S1).MayAssign(ref S1)", "arg1").WithLocation(15, 9)); } + [Fact] + public void RefAnalysis_Invocation_05_Static() + { + var text = """ +class Program +{ + void Test1() + { + System.Span inner = stackalloc int[1]; + S1 rInner = MayWrap(ref inner); + + S1.MayAssign(ref rInner); + } + + static S1 MayWrap(ref System.Span arg) => default; +} + +ref struct S1 { } + +static class E +{ + extension(ref S1 arg1) + { + public static void MayAssign(ref S1 arg2) { } + } +} +"""; + var comp = CreateCompilation(text, targetFramework: TargetFramework.Net90); + comp.VerifyDiagnostics(); + } + [Fact] public void RefAnalysis_Invocation_06() { @@ -32951,6 +32982,138 @@ static class E Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M2()").WithArguments("j", "i.M2()").WithLocation(8, 9)); } + [Fact] + public void RefAnalysis_Invocation_08() + { + string source = """ +class C +{ + void MA(ref readonly int j, ref int k) + { + int i = 0; + j = ref i.M(ref k); + j = ref E.M(ref i, ref k); + j = ref i.M2(ref k); + } +} + +static class E +{ + extension(ref readonly int i) + { + public ref readonly int M(ref int k) => ref i; + } + public static ref readonly int M2(this ref readonly int i, ref int k) => ref i; +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,9): error CS8374: Cannot ref-assign 'i.M(ref k)' to 'j' because 'i.M(ref k)' has a narrower escape scope than 'j'. + // j = ref i.M(ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M(ref k)").WithArguments("j", "i.M(ref k)").WithLocation(6, 9), + // (7,9): error CS8374: Cannot ref-assign 'E.M(ref i, ref k)' to 'j' because 'E.M(ref i, ref k)' has a narrower escape scope than 'j'. + // j = ref E.M(ref i, ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref E.M(ref i, ref k)").WithArguments("j", "E.M(ref i, ref k)").WithLocation(7, 9), + // (8,9): error CS8374: Cannot ref-assign 'i.M2(ref k)' to 'j' because 'i.M2(ref k)' has a narrower escape scope than 'j'. + // j = ref i.M2(ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M2(ref k)").WithArguments("j", "i.M2(ref k)").WithLocation(8, 9)); + } + + [Fact] + public void RefAnalysis_Invocation_09() + { + string source = """ +class C +{ + void MA(ref int j, ref int k) + { + int i = 0; + j = ref i.M(ref k); + j = ref E.M(ref i, ref k); + j = ref i.M2(ref k); + } +} + +static class E +{ + extension(ref int i) + { + public ref int M(ref int k) => ref i; + } + public static ref int M2(this ref int i, ref int k) => ref i; +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (6,9): error CS8374: Cannot ref-assign 'i.M(ref k)' to 'j' because 'i.M(ref k)' has a narrower escape scope than 'j'. + // j = ref i.M(ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M(ref k)").WithArguments("j", "i.M(ref k)").WithLocation(6, 9), + // (7,9): error CS8374: Cannot ref-assign 'E.M(ref i, ref k)' to 'j' because 'E.M(ref i, ref k)' has a narrower escape scope than 'j'. + // j = ref E.M(ref i, ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref E.M(ref i, ref k)").WithArguments("j", "E.M(ref i, ref k)").WithLocation(7, 9), + // (8,9): error CS8374: Cannot ref-assign 'i.M2(ref k)' to 'j' because 'i.M2(ref k)' has a narrower escape scope than 'j'. + // j = ref i.M2(ref k); + Diagnostic(ErrorCode.ERR_RefAssignNarrower, "j = ref i.M2(ref k)").WithArguments("j", "i.M2(ref k)").WithLocation(8, 9)); + } + + [Fact] + public void RefAnalysis_Invocation_10() + { + var source = """ +class Program +{ + static ref int G8A() + { + int t = default; + int u = default; + return ref t.F8(out u); // 1 + } + static ref int G8A_2() + { + int t = default; + int u = default; + return ref E.F8(t, out u); // 2 + } + static ref int G8A_3() + { + int t = default; + int u = default; + return ref E.F8(in t, out u); // 3 + } +} +static class E +{ + extension(in int t) + { + public ref int F8(out int u) => throw null; + } +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,20): error CS8168: Cannot return local 't' by reference because it is not a ref local + // return ref t.F8(out u); // 1 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "t").WithArguments("t").WithLocation(7, 20), + // (7,20): error CS8347: Cannot use a result of 'E.extension(in int).F8(out int)' in this context because it may expose variables referenced by parameter 't' outside of their declaration scope + // return ref t.F8(out u); // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "t.F8(out u)").WithArguments("E.extension(in int).F8(out int)", "t").WithLocation(7, 20), + // (13,20): error CS8347: Cannot use a result of 'E.F8(in int, out int)' in this context because it may expose variables referenced by parameter 't' outside of their declaration scope + // return ref E.F8(t, out u); // 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "E.F8(t, out u)").WithArguments("E.F8(in int, out int)", "t").WithLocation(13, 20), + // (13,25): error CS8168: Cannot return local 't' by reference because it is not a ref local + // return ref E.F8(t, out u); // 2 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "t").WithArguments("t").WithLocation(13, 25), + // (19,20): error CS8347: Cannot use a result of 'E.F8(in int, out int)' in this context because it may expose variables referenced by parameter 't' outside of their declaration scope + // return ref E.F8(in t, out u); // 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "E.F8(in t, out u)").WithArguments("E.F8(in int, out int)", "t").WithLocation(19, 20), + // (19,28): error CS8168: Cannot return local 't' by reference because it is not a ref local + // return ref E.F8(in t, out u); // 3 + Diagnostic(ErrorCode.ERR_RefReturnLocal, "t").WithArguments("t").WithLocation(19, 28)); + } + [Fact] public void RefAnalysis_PropertyAccess_01() { From 56934ed59ae743f8746a0cbb7d165a5959707c94 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 7 Apr 2025 10:46:07 +0200 Subject: [PATCH 7/8] Re-use parameters --- src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 14fb275b9d9e..f98a8c6d3c3a 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -2150,8 +2150,11 @@ private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo me methodInfo = replacedMethodInfo; + var extensionParameter = symbol.ContainingType.ExtensionParameter; + Debug.Assert(extensionParameter is not null); + parameters = parameters.IsDefault ? [extensionParameter] : [extensionParameter, .. parameters]; + Debug.Assert(receiver is not null); - parameters = methodInfo.Symbol.GetParameters(); argsOpt = argsOpt.IsDefault ? [receiver] : [receiver, .. argsOpt]; receiver = null; From e40d3f8f62b1d12419ff4c7d605212a68d2a3c63 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 7 Apr 2025 23:41:49 +0200 Subject: [PATCH 8/8] Address feedback --- .../Portable/Binder/Binder.ValueChecks.cs | 393 +++++++++--------- 1 file changed, 206 insertions(+), 187 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index f98a8c6d3c3a..f6f377cf562e 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -2000,80 +2000,84 @@ bool isRefEscape Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt)); #endif - MethodInfo localMethodInfo = methodInfo; - ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + MethodInfo localMethodInfo = ReplaceWithExtensionImplementationIfNeeded(methodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); if (methodInfo.UseUpdatedEscapeRules) { return GetInvocationEscapeWithUpdatedRules(localMethodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, isRefEscape); } - // SPEC: (also applies to the CheckInvocationEscape counterpart) - // - // An lvalue resulting from a ref-returning method invocation e1.M(e2, ...) is ref-safe - to - escape the smallest of the following scopes: - //• The entire enclosing method - //• the ref-safe-to-escape of all ref/out/in argument expressions(excluding the receiver) - //• the safe-to - escape of all argument expressions(including the receiver) - // - // An rvalue resulting from a method invocation e1.M(e2, ...) is safe - to - escape from the smallest of the following scopes: - //• The entire enclosing method - //• the safe-to-escape of all argument expressions(including the receiver) - // - - SafeContext escapeScope = SafeContext.CallingMethod; - var escapeValues = ArrayBuilder.GetInstance(); - GetEscapeValuesForOldRules( - in localMethodInfo, - // Receiver handled explicitly below - receiver: null, - receiverIsSubjectToCloning: ThreeState.Unknown, - parameters, - argsOpt, - argRefKindsOpt, - argsToParamsOpt, - // ref kinds of varargs are not interesting here. - // __refvalue is not ref-returnable, so ref varargs can't come back from a call - ignoreArglistRefKinds: true, - mixableArguments: null, - escapeValues); + return getInvocationEscapeWithOldRules(localMethodInfo, parameters, receiver, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, isRefEscape); - try + SafeContext getInvocationEscapeWithOldRules(MethodInfo methodInfo, ImmutableArray parameters, BoundExpression? receiver, ImmutableArray argsOpt, ImmutableArray argRefKindsOpt, ImmutableArray argsToParamsOpt, SafeContext localScopeDepth, bool isRefEscape) { - foreach (var (parameter, argument, _, argumentIsRefEscape) in escapeValues) + // SPEC: (also applies to the CheckInvocationEscape counterpart) + // + // An lvalue resulting from a ref-returning method invocation e1.M(e2, ...) is ref-safe - to - escape the smallest of the following scopes: + //• The entire enclosing method + //• the ref-safe-to-escape of all ref/out/in argument expressions(excluding the receiver) + //• the safe-to - escape of all argument expressions(including the receiver) + // + // An rvalue resulting from a method invocation e1.M(e2, ...) is safe - to - escape from the smallest of the following scopes: + //• The entire enclosing method + //• the safe-to-escape of all argument expressions(including the receiver) + // + + SafeContext escapeScope = SafeContext.CallingMethod; + var escapeValues = ArrayBuilder.GetInstance(); + GetEscapeValuesForOldRules( + in methodInfo, + // Receiver handled explicitly below + receiver: null, + receiverIsSubjectToCloning: ThreeState.Unknown, + parameters, + argsOpt, + argRefKindsOpt, + argsToParamsOpt, + // ref kinds of varargs are not interesting here. + // __refvalue is not ref-returnable, so ref varargs can't come back from a call + ignoreArglistRefKinds: true, + mixableArguments: null, + escapeValues); + + try { - // ref escape scope is the narrowest of - // - ref escape of all byref arguments - // - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result) - // - // val escape scope is the narrowest of - // - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes) - SafeContext argumentEscape = (isRefEscape, argumentIsRefEscape) switch - { - (true, true) => GetRefEscape(argument, localScopeDepth), - (false, false) => GetValEscape(argument, localScopeDepth), - _ => escapeScope - }; + foreach (var (parameter, argument, _, argumentIsRefEscape) in escapeValues) + { + // ref escape scope is the narrowest of + // - ref escape of all byref arguments + // - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result) + // + // val escape scope is the narrowest of + // - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes) + SafeContext argumentEscape = (isRefEscape, argumentIsRefEscape) switch + { + (true, true) => GetRefEscape(argument, localScopeDepth), + (false, false) => GetValEscape(argument, localScopeDepth), + _ => escapeScope + }; - escapeScope = escapeScope.Intersect(argumentEscape); - if (localScopeDepth.IsConvertibleTo(escapeScope)) - { - // can't get any worse - return escapeScope; + escapeScope = escapeScope.Intersect(argumentEscape); + if (localScopeDepth.IsConvertibleTo(escapeScope)) + { + // can't get any worse + return escapeScope; + } } } - } - finally - { - escapeValues.Free(); - } + finally + { + escapeValues.Free(); + } - // check receiver if ref-like - if (localMethodInfo.Method?.RequiresInstanceReceiver == true && receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) - { - escapeScope = escapeScope.Intersect(GetValEscape(receiver, localScopeDepth)); - } + // check receiver if ref-like + if (methodInfo.Method?.RequiresInstanceReceiver == true && receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) + { + escapeScope = escapeScope.Intersect(GetValEscape(receiver, localScopeDepth)); + } - return escapeScope; + return escapeScope; + } } private SafeContext GetInvocationEscapeWithUpdatedRules( @@ -2132,24 +2136,22 @@ private SafeContext GetInvocationEscapeWithUpdatedRules( return escapeScope; } - private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo methodInfo, ref ImmutableArray parameters, + private static MethodInfo ReplaceWithExtensionImplementationIfNeeded(MethodInfo methodInfo, ref ImmutableArray parameters, ref BoundExpression? receiver, ref ImmutableArray argsOpt, ref ImmutableArray argRefKindsOpt, ref ImmutableArray argsToParamsOpt) { Symbol? symbol = methodInfo.Symbol; if (symbol?.GetIsNewExtensionMember() != true || symbol.IsStatic) { - return; + return methodInfo; } MethodInfo replacedMethodInfo = methodInfo.ReplaceWithExtensionImplementation(out bool wasError); if (wasError) { - return; + return methodInfo; } - methodInfo = replacedMethodInfo; - var extensionParameter = symbol.ContainingType.ExtensionParameter; Debug.Assert(extensionParameter is not null); parameters = parameters.IsDefault ? [extensionParameter] : [extensionParameter, .. parameters]; @@ -2174,6 +2176,8 @@ private static void ReplaceWithExtensionImplementationIfNeeded(ref MethodInfo me argsToParamsOpt = argsToParamsBuilder.ToImmutableAndFree(); } + + return replacedMethodInfo; } /// @@ -2204,80 +2208,88 @@ bool isRefEscape Debug.Assert(AllParametersConsideredInEscapeAnalysisHaveArguments(argsOpt, parameters, argsToParamsOpt)); #endif - MethodInfo localMethodInfo = methodInfo; - ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + MethodInfo localMethodInfo = ReplaceWithExtensionImplementationIfNeeded(methodInfo, ref parameters, ref receiver, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); if (methodInfo.UseUpdatedEscapeRules) { return CheckInvocationEscapeWithUpdatedRules(syntax, localMethodInfo, receiver, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, checkingReceiver, escapeFrom, escapeTo, diagnostics, isRefEscape, methodInfo.Symbol); } - // SPEC: - // In a method invocation, the following constraints apply: - //• If there is a ref or out argument to a ref struct type (including the receiver), with safe-to-escape E1, then - // o no ref or out argument(excluding the receiver and arguments of ref-like types) may have a narrower ref-safe-to-escape than E1; and - // o no argument(including the receiver) may have a narrower safe-to-escape than E1. + return checkInvocationEscapeWithOldRules(syntax, localMethodInfo, ref receiver, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, checkingReceiver, escapeFrom, escapeTo, diagnostics, isRefEscape, methodInfo.Symbol); - var symbol = localMethodInfo.Symbol; - if (!symbol.RequiresInstanceReceiver()) + bool checkInvocationEscapeWithOldRules(SyntaxNode syntax, MethodInfo methodInfo, + ref BoundExpression? receiver, ImmutableArray parameters, ImmutableArray argsOpt, + ImmutableArray argRefKindsOpt, ImmutableArray argsToParamsOpt, + bool checkingReceiver, SafeContext escapeFrom, SafeContext escapeTo, + BindingDiagnosticBag diagnostics, bool isRefEscape, Symbol symbolForReporting) { - // ignore receiver when symbol is static - receiver = null; - } + // SPEC: + // In a method invocation, the following constraints apply: + //• If there is a ref or out argument to a ref struct type (including the receiver), with safe-to-escape E1, then + // o no ref or out argument(excluding the receiver and arguments of ref-like types) may have a narrower ref-safe-to-escape than E1; and + // o no argument(including the receiver) may have a narrower safe-to-escape than E1. - var escapeArguments = ArrayBuilder.GetInstance(); - GetInvocationArgumentsForEscape( - localMethodInfo, - receiver: null, // receiver handled explicitly below - receiverIsSubjectToCloning: ThreeState.Unknown, - parameters, - argsOpt, - argRefKindsOpt, - argsToParamsOpt, - // ref kinds of varargs are not interesting here. - // __refvalue is not ref-returnable, so ref varargs can't come back from a call - ignoreArglistRefKinds: true, - mixableArguments: null, - escapeArguments); - - try - { - foreach (var (parameter, argument, effectiveRefKind) in escapeArguments) + var symbol = methodInfo.Symbol; + if (!symbol.RequiresInstanceReceiver()) { - // ref escape scope is the narrowest of - // - ref escape of all byref arguments - // - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result) - // - // val escape scope is the narrowest of - // - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes) + // ignore receiver when symbol is static + receiver = null; + } - var valid = effectiveRefKind != RefKind.None && isRefEscape ? - CheckRefEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics) : - CheckValEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics); + var escapeArguments = ArrayBuilder.GetInstance(); + GetInvocationArgumentsForEscape( + methodInfo, + receiver: null, // receiver handled explicitly below + receiverIsSubjectToCloning: ThreeState.Unknown, + parameters, + argsOpt, + argRefKindsOpt, + argsToParamsOpt, + // ref kinds of varargs are not interesting here. + // __refvalue is not ref-returnable, so ref varargs can't come back from a call + ignoreArglistRefKinds: true, + mixableArguments: null, + escapeArguments); - if (!valid) + try + { + foreach (var (parameter, argument, effectiveRefKind) in escapeArguments) { - if (symbol is not SignatureOnlyMethodSymbol) + // ref escape scope is the narrowest of + // - ref escape of all byref arguments + // - val escape of all byval arguments (ref-like values can be unwrapped into refs, so treat val escape of values as possible ref escape of the result) + // + // val escape scope is the narrowest of + // - val escape of all byval arguments (refs cannot be wrapped into values, so their ref escape is irrelevant, only use val escapes) + + var valid = effectiveRefKind != RefKind.None && isRefEscape ? + CheckRefEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics) : + CheckValEscape(argument.Syntax, argument, escapeFrom, escapeTo, false, diagnostics); + + if (!valid) { - ReportInvocationEscapeError(syntax, methodInfo.Symbol, parameter, checkingReceiver, diagnostics); - } + if (symbol is not SignatureOnlyMethodSymbol) + { + ReportInvocationEscapeError(syntax, methodInfo.Symbol, parameter, checkingReceiver, diagnostics); + } - return false; + return false; + } } } - } - finally - { - escapeArguments.Free(); - } + finally + { + escapeArguments.Free(); + } - // check receiver if ref-like - if (receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) - { - return CheckValEscape(receiver.Syntax, receiver, escapeFrom, escapeTo, false, diagnostics); - } + // check receiver if ref-like + if (receiver?.Type?.IsRefLikeOrAllowsRefLikeType() == true) + { + return CheckValEscape(receiver.Syntax, receiver, escapeFrom, escapeTo, false, diagnostics); + } - return true; + return true; + } } private bool CheckInvocationEscapeWithUpdatedRules( @@ -2332,7 +2344,7 @@ private bool CheckInvocationEscapeWithUpdatedRules( // For consistency with C#10 implementation, we don't report an additional error // for the receiver. (In both implementations, the call to Check*Escape() above // will have reported a specific escape error for the receiver though.) - if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && (Symbol?)methodInfo.Symbol is not SignatureOnlyMethodSymbol) + if ((object)((argument as BoundCapturedReceiverPlaceholder)?.Receiver ?? argument) != receiver && methodInfo.Symbol is not SignatureOnlyMethodSymbol) { ReportInvocationEscapeError(syntax, symbolForReporting, param, checkingReceiver, diagnostics); } @@ -2857,94 +2869,101 @@ private bool CheckInvocationArgMixing( SafeContext localScopeDepth, BindingDiagnosticBag diagnostics) { - MethodInfo localMethodInfo = methodInfo; - ReplaceWithExtensionImplementationIfNeeded(ref localMethodInfo, ref parameters, ref receiverOpt, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); + MethodInfo localMethodInfo = ReplaceWithExtensionImplementationIfNeeded(methodInfo, ref parameters, ref receiverOpt, ref argsOpt, ref argRefKindsOpt, ref argsToParamsOpt); if (methodInfo.UseUpdatedEscapeRules) { return CheckInvocationArgMixingWithUpdatedRules(syntax, localMethodInfo, receiverOpt, receiverIsSubjectToCloning, parameters, argsOpt, argRefKindsOpt, argsToParamsOpt, localScopeDepth, diagnostics, methodInfo.Symbol); } - // SPEC: - // In a method invocation, the following constraints apply: - // - If there is a ref or out argument of a ref struct type (including the receiver), with safe-to-escape E1, then - // - no argument (including the receiver) may have a narrower safe-to-escape than E1. + return checkInvocationArgMixingWithOldRules(syntax, localMethodInfo, ref receiverOpt, receiverIsSubjectToCloning, parameters, argsOpt, argsToParamsOpt, localScopeDepth, diagnostics, methodInfo.Symbol); - var symbol = localMethodInfo.Symbol; - if (!symbol.RequiresInstanceReceiver()) + bool checkInvocationArgMixingWithOldRules(SyntaxNode syntax, MethodInfo methodInfo, + ref BoundExpression? receiverOpt, ThreeState receiverIsSubjectToCloning, ImmutableArray parameters, + ImmutableArray argsOpt, ImmutableArray argsToParamsOpt, + SafeContext localScopeDepth, BindingDiagnosticBag diagnostics, Symbol symbolForReporting) { - // ignore receiver when symbol is static - receiverOpt = null; - } + // SPEC: + // In a method invocation, the following constraints apply: + // - If there is a ref or out argument of a ref struct type (including the receiver), with safe-to-escape E1, then + // - no argument (including the receiver) may have a narrower safe-to-escape than E1. - // widest possible escape via writeable ref-like receiver or ref/out argument. - SafeContext escapeTo = localScopeDepth; + var symbol = methodInfo.Symbol; + if (!symbol.RequiresInstanceReceiver()) + { + // ignore receiver when symbol is static + receiverOpt = null; + } - // collect all writeable ref-like arguments, including receiver - var escapeArguments = ArrayBuilder.GetInstance(); - GetInvocationArgumentsForEscape( - localMethodInfo, - receiverOpt, - receiverIsSubjectToCloning, - parameters, - argsOpt, - argRefKindsOpt: default, - argsToParamsOpt, - ignoreArglistRefKinds: false, - mixableArguments: null, - escapeArguments); + // widest possible escape via writeable ref-like receiver or ref/out argument. + SafeContext escapeTo = localScopeDepth; - try - { - foreach (var (_, argument, refKind) in escapeArguments) + // collect all writeable ref-like arguments, including receiver + var escapeArguments = ArrayBuilder.GetInstance(); + GetInvocationArgumentsForEscape( + methodInfo, + receiverOpt, + receiverIsSubjectToCloning, + parameters, + argsOpt, + argRefKindsOpt: default, + argsToParamsOpt, + ignoreArglistRefKinds: false, + mixableArguments: null, + escapeArguments); + + try { - if (ShouldInferDeclarationExpressionValEscape(argument, out _)) + foreach (var (_, argument, refKind) in escapeArguments) { - // Any variable from a declaration expression is a valid mixing destination as we - // infer a legal value escape for it. It does not contribute input as it's declared - // at this point (functions like an `out` in the new escape rules) - continue; - } + if (ShouldInferDeclarationExpressionValEscape(argument, out _)) + { + // Any variable from a declaration expression is a valid mixing destination as we + // infer a legal value escape for it. It does not contribute input as it's declared + // at this point (functions like an `out` in the new escape rules) + continue; + } - if (refKind.IsWritableReference() - && !argument.IsDiscardExpression() - && argument.Type?.IsRefLikeOrAllowsRefLikeType() == true) - { - escapeTo = escapeTo.Union(GetValEscape(argument, localScopeDepth)); + if (refKind.IsWritableReference() + && !argument.IsDiscardExpression() + && argument.Type?.IsRefLikeOrAllowsRefLikeType() == true) + { + escapeTo = escapeTo.Union(GetValEscape(argument, localScopeDepth)); + } } - } - var hasMixingError = false; + 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 = SafeContext.CallingMethod; - foreach (var (parameter, argument, _) in escapeArguments) - { - // 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 = inferredDestinationValEscape.Intersect(GetValEscape(argument, localScopeDepth)); - if (!hasMixingError && !CheckValEscape(argument.Syntax, argument, localScopeDepth, escapeTo, false, diagnostics)) - { - string parameterName = GetInvocationParameterName(parameter); - Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, methodInfo.Symbol, parameterName); - hasMixingError = true; + // track the widest scope that arguments could safely escape to. + // use this scope as the inferred STE of declaration expressions. + var inferredDestinationValEscape = SafeContext.CallingMethod; + foreach (var (parameter, argument, _) in escapeArguments) + { + // 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 = inferredDestinationValEscape.Intersect(GetValEscape(argument, localScopeDepth)); + if (!hasMixingError && !CheckValEscape(argument.Syntax, argument, localScopeDepth, escapeTo, false, diagnostics)) + { + string parameterName = GetInvocationParameterName(parameter); + Error(diagnostics, ErrorCode.ERR_CallArgMixing, syntax, symbolForReporting, parameterName); + hasMixingError = true; + } } - } - foreach (var (_, argument, _) in escapeArguments) - { - if (ShouldInferDeclarationExpressionValEscape(argument, out var localSymbol)) + foreach (var (_, argument, _) in escapeArguments) { - SetLocalScopes(localSymbol, refEscapeScope: _localScopeDepth, valEscapeScope: inferredDestinationValEscape); + if (ShouldInferDeclarationExpressionValEscape(argument, out var localSymbol)) + { + SetLocalScopes(localSymbol, refEscapeScope: _localScopeDepth, valEscapeScope: inferredDestinationValEscape); + } } - } - return !hasMixingError; - } - finally - { - escapeArguments.Free(); + return !hasMixingError; + } + finally + { + escapeArguments.Free(); + } } }