|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Operations;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.CSharp.CodeGen;
namespace Microsoft.CodeAnalysis.CSharp
{
internal sealed partial class LocalRewriter
{
public override BoundNode VisitDynamicInvocation(BoundDynamicInvocation node)
{
return VisitDynamicInvocation(node, resultDiscarded: false);
}
public BoundExpression VisitDynamicInvocation(BoundDynamicInvocation node, bool resultDiscarded)
{
// Dynamic can't have created handler conversions because we don't know target types.
AssertNoImplicitInterpolatedStringHandlerConversions(node.Arguments);
var loweredArguments = VisitList(node.Arguments);
bool hasImplicitReceiver;
BoundExpression loweredReceiver;
ImmutableArray<TypeWithAnnotations> typeArguments;
string name;
switch (node.Expression.Kind)
{
case BoundKind.MethodGroup:
// method invocation
BoundMethodGroup methodGroup = (BoundMethodGroup)node.Expression;
typeArguments = methodGroup.TypeArgumentsOpt;
name = methodGroup.Name;
hasImplicitReceiver = (methodGroup.Flags & BoundMethodGroupFlags.HasImplicitReceiver) != 0;
// Should have been eliminated during binding of dynamic invocation:
Debug.Assert(methodGroup.ReceiverOpt == null || methodGroup.ReceiverOpt.Kind != BoundKind.TypeOrValueExpression);
if (methodGroup.ReceiverOpt == null)
{
// Calling a static method defined on an outer class via its simple name.
NamedTypeSymbol firstContainer = node.ApplicableMethods.First().ContainingType;
Debug.Assert(node.ApplicableMethods.All(m => !m.RequiresInstanceReceiver && TypeSymbol.Equals(m.ContainingType, firstContainer, TypeCompareKind.ConsiderEverything2)));
loweredReceiver = new BoundTypeExpression(node.Syntax, null, firstContainer);
}
else if (hasImplicitReceiver && _factory.TopLevelMethod is { RequiresInstanceReceiver: false })
{
// Calling a static method defined on the current class via its simple name.
Debug.Assert(_factory.CurrentType is { });
loweredReceiver = new BoundTypeExpression(node.Syntax, null, _factory.CurrentType);
}
else
{
loweredReceiver = VisitExpression(methodGroup.ReceiverOpt);
}
// If we are calling a method on a NoPIA type, we need to embed all methods/properties
// with the matching name of this dynamic invocation.
EmbedIfNeedTo(loweredReceiver, methodGroup.Methods, node.Syntax);
break;
case BoundKind.DynamicMemberAccess:
// method invocation
var memberAccess = (BoundDynamicMemberAccess)node.Expression;
name = memberAccess.Name;
typeArguments = memberAccess.TypeArgumentsOpt;
loweredReceiver = VisitExpression(memberAccess.Receiver);
hasImplicitReceiver = false;
break;
default:
// delegate invocation
var loweredExpression = VisitExpression(node.Expression);
return _dynamicFactory.MakeDynamicInvocation(loweredExpression, loweredArguments, node.ArgumentNamesOpt, node.ArgumentRefKindsOpt, resultDiscarded).ToExpression();
}
Debug.Assert(loweredReceiver != null);
return _dynamicFactory.MakeDynamicMemberInvocation(
name,
loweredReceiver,
typeArguments,
loweredArguments,
node.ArgumentNamesOpt,
node.ArgumentRefKindsOpt,
hasImplicitReceiver,
resultDiscarded).ToExpression();
}
private void EmbedIfNeedTo(BoundExpression receiver, ImmutableArray<MethodSymbol> methods, SyntaxNode syntaxNode)
{
// If we are calling a method on a NoPIA type, we need to embed all methods/properties
// with the matching name of this dynamic invocation.
var module = this.EmitModule;
if (module != null && receiver != null && receiver.Type is { })
{
var assembly = receiver.Type.ContainingAssembly;
if ((object)assembly != null && assembly.IsLinked)
{
foreach (var m in methods)
{
module.EmbeddedTypesManagerOpt.EmbedMethodIfNeedTo(m.OriginalDefinition.GetCciAdapter(), syntaxNode, _diagnostics.DiagnosticBag);
}
}
}
}
private void EmbedIfNeedTo(BoundExpression receiver, ImmutableArray<PropertySymbol> properties, SyntaxNode syntaxNode)
{
// If we are calling a method on a NoPIA type, we need to embed all methods/properties
// with the matching name of this dynamic invocation.
var module = this.EmitModule;
if (module != null && receiver is { Type: { } })
{
var assembly = receiver.Type.ContainingAssembly;
if ((object)assembly != null && assembly.IsLinked)
{
foreach (var p in properties)
{
module.EmbeddedTypesManagerOpt.EmbedPropertyIfNeedTo(p.OriginalDefinition.GetCciAdapter(), syntaxNode, _diagnostics.DiagnosticBag);
}
}
}
}
private void InterceptCallAndAdjustArguments(
ref MethodSymbol method,
ref BoundExpression? receiverOpt,
ref ImmutableArray<BoundExpression> arguments,
ref ImmutableArray<RefKind> argumentRefKindsOpt,
ref ArrayBuilder<LocalSymbol> temps,
bool invokedAsExtensionMethod,
Syntax.SimpleNameSyntax? nameSyntax)
{
if (this._compilation.TryGetInterceptor(nameSyntax) is not var (attributeLocation, interceptor))
{
// The call was not intercepted.
return;
}
Debug.Assert(nameSyntax != null);
Debug.Assert(interceptor.IsDefinition);
Debug.Assert(!interceptor.ContainingType.IsGenericType);
if (interceptor.Arity != 0)
{
var typeArgumentsBuilder = ArrayBuilder<TypeWithAnnotations>.GetInstance();
method.ContainingType.GetAllTypeArgumentsNoUseSiteDiagnostics(typeArgumentsBuilder);
typeArgumentsBuilder.AddRange(method.TypeArgumentsWithAnnotations);
var netArity = typeArgumentsBuilder.Count;
if (netArity == 0)
{
this._diagnostics.Add(ErrorCode.ERR_InterceptorCannotBeGeneric, attributeLocation, interceptor, method);
typeArgumentsBuilder.Free();
return;
}
else if (interceptor.Arity != netArity)
{
this._diagnostics.Add(ErrorCode.ERR_InterceptorArityNotCompatible, attributeLocation, interceptor, netArity, method);
typeArgumentsBuilder.Free();
return;
}
interceptor = interceptor.Construct(typeArgumentsBuilder.ToImmutableAndFree());
if (!interceptor.CheckConstraints(new ConstraintsHelper.CheckConstraintsArgs(this._compilation, this._compilation.Conversions, includeNullability: true, attributeLocation, this._diagnostics)))
{
return;
}
}
if (method.MethodKind is not MethodKind.Ordinary)
{
this._diagnostics.Add(ErrorCode.ERR_InterceptableMethodMustBeOrdinary, attributeLocation, nameSyntax.Identifier.ValueText);
return;
}
var containingMethod = this._factory.CurrentFunction;
Debug.Assert(containingMethod is not null);
var useSiteInfo = this.GetNewCompoundUseSiteInfo();
var isAccessible = AccessCheck.IsSymbolAccessible(interceptor, containingMethod.ContainingType, ref useSiteInfo);
this._diagnostics.Add(attributeLocation, useSiteInfo);
if (!isAccessible)
{
this._diagnostics.Add(ErrorCode.ERR_InterceptorNotAccessible, attributeLocation, interceptor, containingMethod);
return;
}
// When the original call is to an instance method, and the interceptor is an extension method,
// we need to take special care to intercept with the extension method as though it is being called in reduced form.
Debug.Assert(receiverOpt is not BoundTypeExpression || method.IsStatic);
var needToReduce = receiverOpt is not (null or BoundTypeExpression) && interceptor.IsExtensionMethod;
var symbolForCompare = needToReduce ? ReducedExtensionMethodSymbol.Create(interceptor, receiverOpt!.Type, _compilation, out _) : interceptor;
if (!MemberSignatureComparer.InterceptorsComparer.Equals(method, symbolForCompare))
{
this._diagnostics.Add(ErrorCode.ERR_InterceptorSignatureMismatch, attributeLocation, method, interceptor);
return;
}
_ = SourceMemberContainerTypeSymbol.CheckValidNullableMethodOverride(
_compilation,
method,
symbolForCompare,
_diagnostics,
static (diagnostics, method, interceptor, topLevel, attributeLocation) =>
{
diagnostics.Add(ErrorCode.WRN_NullabilityMismatchInReturnTypeOnInterceptor, attributeLocation, method);
},
static (diagnostics, method, interceptor, implementingParameter, blameAttributes, attributeLocation) =>
{
diagnostics.Add(ErrorCode.WRN_NullabilityMismatchInParameterTypeOnInterceptor, attributeLocation, new FormattedSymbol(implementingParameter, SymbolDisplayFormat.ShortFormat), method);
},
extraArgument: attributeLocation);
if (!MemberSignatureComparer.InterceptorsStrictComparer.Equals(method, symbolForCompare))
{
this._diagnostics.Add(ErrorCode.WRN_InterceptorSignatureMismatch, attributeLocation, method, interceptor);
}
method.TryGetThisParameter(out var methodThisParameter);
var interceptorThisParameterForCompare = needToReduce ? interceptor.Parameters[0] :
interceptor.TryGetThisParameter(out var interceptorThisParameter) ? interceptorThisParameter : null;
switch (methodThisParameter, interceptorThisParameterForCompare)
{
case (not null, null):
case (not null, not null) when !methodThisParameter.Type.Equals(interceptorThisParameterForCompare.Type, TypeCompareKind.ObliviousNullableModifierMatchesAny)
|| methodThisParameter.RefKind != interceptorThisParameterForCompare.RefKind:
this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, attributeLocation, methodThisParameter, method);
return;
case (null, not null):
this._diagnostics.Add(ErrorCode.ERR_InterceptorMustNotHaveThisParameter, attributeLocation, method);
return;
default:
break;
}
if (invokedAsExtensionMethod && interceptor.IsStatic && !interceptor.IsExtensionMethod)
{
// Special case when intercepting an extension method call in reduced form with a non-extension.
this._diagnostics.Add(ErrorCode.ERR_InterceptorMustHaveMatchingThisParameter, attributeLocation, method.Parameters[0], method);
return;
}
if (SourceMemberContainerTypeSymbol.CheckValidScopedOverride(
method,
symbolForCompare,
this._diagnostics,
static (diagnostics, method, symbolForCompare, implementingParameter, blameAttributes, attributeLocation) =>
{
diagnostics.Add(ErrorCode.ERR_InterceptorScopedMismatch, attributeLocation, method, symbolForCompare);
},
extraArgument: attributeLocation,
allowVariance: true,
// Since we've already reduced 'symbolForCompare', we compare as though it is not an extension.
invokedAsExtensionMethod: false))
{
return;
}
if (needToReduce)
{
Debug.Assert(methodThisParameter is not null);
Debug.Assert(receiverOpt?.Type is not null);
// Usually we expect the receiver to already be converted to the this parameter type.
// However, in the case of a non-reference type receiver, where the this parameter is some base reference type,
// for example a struct type and System.ValueType respectively, we need to convert the receiver to parameter type,
// because we can't use the same `.constrained` calling pattern here which we would have used for an instance method receiver.
Debug.Assert(receiverOpt.Type.Equals(interceptor.Parameters[0].Type, TypeCompareKind.AllIgnoreOptions)
|| (!receiverOpt.Type.IsReferenceType && interceptor.Parameters[0].Type.IsReferenceType));
receiverOpt = MakeConversionNode(receiverOpt, interceptor.Parameters[0].Type, @checked: false, markAsChecked: true);
var thisRefKind = methodThisParameter.RefKind;
// Instance call receivers can be implicitly captured to temps in the emit layer, but not static call arguments
// Therefore we may need to explicitly store the receiver to temp here.
if (thisRefKind != RefKind.None
&& !Binder.HasHome(
receiverOpt,
thisRefKind == RefKind.Ref ? Binder.AddressKind.Writeable : Binder.AddressKind.ReadOnlyStrict,
_factory.CurrentFunction,
peVerifyCompatEnabled: false,
stackLocalsOpt: null))
{
var receiverTemp = _factory.StoreToTemp(receiverOpt, out var assignmentToTemp);
temps.Add(receiverTemp.LocalSymbol);
receiverOpt = _factory.Sequence(locals: [], sideEffects: [assignmentToTemp], receiverTemp);
}
arguments = arguments.Insert(0, receiverOpt);
receiverOpt = null;
// CodeGenerator.EmitArguments requires that we have a fully-filled-out argumentRefKindsOpt for any ref/in/out arguments.
if (argumentRefKindsOpt.IsDefault && thisRefKind != RefKind.None)
{
argumentRefKindsOpt = method.Parameters.SelectAsArray(static param => param.RefKind);
}
if (!argumentRefKindsOpt.IsDefault)
{
argumentRefKindsOpt = argumentRefKindsOpt.Insert(0, thisRefKind);
}
}
method = interceptor;
return;
}
public override BoundNode VisitCall(BoundCall node)
{
Debug.Assert(node != null);
BoundExpression rewrittenCall;
if (tryGetReceiver(node, out BoundCall? receiver1))
{
// Handle long call chain of both instance and extension method invocations.
var calls = ArrayBuilder<BoundCall>.GetInstance();
calls.Push(node);
node = receiver1;
while (tryGetReceiver(node, out BoundCall? receiver2))
{
calls.Push(node);
node = receiver2;
}
// Rewrite the receiver
BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt);
do
{
rewrittenCall = visitArgumentsAndFinishRewrite(node, rewrittenReceiver);
rewrittenReceiver = rewrittenCall;
}
while (calls.TryPop(out node!));
calls.Free();
}
else
{
// Rewrite the receiver
BoundExpression? rewrittenReceiver = VisitExpression(node.ReceiverOpt);
rewrittenCall = visitArgumentsAndFinishRewrite(node, rewrittenReceiver);
}
return rewrittenCall;
// Gets the instance or extension invocation receiver if any.
static bool tryGetReceiver(BoundCall node, [MaybeNullWhen(returnValue: false)] out BoundCall receiver)
{
if (node.ReceiverOpt is BoundCall instanceReceiver)
{
receiver = instanceReceiver;
return true;
}
if (node.InvokedAsExtensionMethod && node.Arguments is [BoundCall extensionReceiver, ..])
{
Debug.Assert(node.ReceiverOpt is null);
receiver = extensionReceiver;
return true;
}
receiver = null;
return false;
}
BoundExpression visitArgumentsAndFinishRewrite(BoundCall node, BoundExpression? rewrittenReceiver)
{
MethodSymbol method = node.Method;
ImmutableArray<int> argsToParamsOpt = node.ArgsToParamsOpt;
ImmutableArray<RefKind> argRefKindsOpt = node.ArgumentRefKindsOpt;
ImmutableArray<BoundExpression> arguments = node.Arguments;
bool invokedAsExtensionMethod = node.InvokedAsExtensionMethod;
// Rewritten receiver can be actually the first argument of an extension invocation.
BoundExpression? firstRewrittenArgument = null;
if (rewrittenReceiver is not null && node.ReceiverOpt is null)
{
Debug.Assert(invokedAsExtensionMethod && !arguments.IsEmpty);
firstRewrittenArgument = rewrittenReceiver;
rewrittenReceiver = null;
}
ArrayBuilder<LocalSymbol>? temps = null;
var rewrittenArguments = VisitArgumentsAndCaptureReceiverIfNeeded(
ref rewrittenReceiver,
captureReceiverMode: ReceiverCaptureMode.Default,
arguments,
method,
argsToParamsOpt,
argRefKindsOpt,
storesOpt: null,
ref temps,
firstRewrittenArgument: firstRewrittenArgument);
rewrittenArguments = MakeArguments(
rewrittenArguments,
method,
node.Expanded,
argsToParamsOpt,
ref argRefKindsOpt,
ref temps,
invokedAsExtensionMethod);
InterceptCallAndAdjustArguments(ref method, ref rewrittenReceiver, ref rewrittenArguments, ref argRefKindsOpt, ref temps, invokedAsExtensionMethod, node.InterceptableNameSyntax);
if (Instrument)
{
Instrumenter.InterceptCallAndAdjustArguments(ref method, ref rewrittenReceiver, ref rewrittenArguments, ref argRefKindsOpt);
}
var rewrittenCall = MakeCall(node, node.Syntax, rewrittenReceiver, method, rewrittenArguments, argRefKindsOpt, node.ResultKind, temps.ToImmutableAndFree());
if (Instrument)
{
rewrittenCall = Instrumenter.InstrumentCall(node, rewrittenCall);
}
return rewrittenCall;
}
}
private BoundExpression MakeCall(
BoundCall? node,
SyntaxNode syntax,
BoundExpression? rewrittenReceiver,
MethodSymbol method,
ImmutableArray<BoundExpression> rewrittenArguments,
ImmutableArray<RefKind> argumentRefKinds,
LookupResultKind resultKind,
ImmutableArray<LocalSymbol> temps)
{
BoundExpression rewrittenBoundCall;
if (method.IsStatic &&
method.ContainingType.IsObjectType() &&
!_inExpressionLambda &&
(object)method == (object)_compilation.GetSpecialTypeMember(SpecialMember.System_Object__ReferenceEquals))
{
Debug.Assert(rewrittenArguments.Length == 2);
// ECMA - 335
// I.8.2.5.1 Identity
// ...
// Identity is implemented on System.Object via the ReferenceEquals method.
rewrittenBoundCall = new BoundBinaryOperator(
syntax,
BinaryOperatorKind.ObjectEqual,
null,
methodOpt: null,
constrainedToTypeOpt: null,
resultKind,
rewrittenArguments[0],
rewrittenArguments[1],
method.ReturnType);
}
else if (node == null)
{
rewrittenBoundCall = new BoundCall(
syntax,
rewrittenReceiver,
initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
method,
rewrittenArguments,
argumentNamesOpt: default(ImmutableArray<string?>),
argumentRefKinds,
isDelegateCall: false,
expanded: false,
invokedAsExtensionMethod: false,
argsToParamsOpt: default(ImmutableArray<int>),
defaultArguments: default(BitVector),
resultKind: resultKind,
type: method.ReturnType);
}
else
{
rewrittenBoundCall = node.Update(
rewrittenReceiver,
initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
method,
rewrittenArguments,
argumentNamesOpt: default(ImmutableArray<string?>),
argumentRefKinds,
node.IsDelegateCall,
expanded: false,
invokedAsExtensionMethod: false,
argsToParamsOpt: default(ImmutableArray<int>),
defaultArguments: default(BitVector),
node.ResultKind,
method.ReturnType);
}
Debug.Assert(rewrittenBoundCall.Type is not null);
if (!temps.IsDefaultOrEmpty)
{
return new BoundSequence(
syntax,
locals: temps,
sideEffects: ImmutableArray<BoundExpression>.Empty,
value: rewrittenBoundCall,
type: rewrittenBoundCall.Type);
}
return rewrittenBoundCall;
}
private BoundExpression MakeCall(SyntaxNode syntax, BoundExpression? rewrittenReceiver, MethodSymbol method, ImmutableArray<BoundExpression> rewrittenArguments)
{
return MakeCall(
node: null,
syntax: syntax,
rewrittenReceiver: rewrittenReceiver,
method: method,
rewrittenArguments: rewrittenArguments,
argumentRefKinds: default(ImmutableArray<RefKind>),
resultKind: LookupResultKind.Viable,
temps: default);
}
private static bool IsSafeForReordering(BoundExpression expression, RefKind kind)
{
// To be safe for reordering an expression must not cause any observable side effect *or
// observe any side effect*. Accessing a local by value, for example, is possibly not
// safe for reordering because reading a local can give a different result if reordered
// with respect to a write elsewhere.
var current = expression;
while (true)
{
if (current.ConstantValueOpt != null)
{
return true;
}
switch (current.Kind)
{
default:
return false;
case BoundKind.Parameter:
Debug.Assert(!IsCapturedPrimaryConstructorParameter(expression));
goto case BoundKind.Local;
case BoundKind.Local:
// A ref to a local variable or formal parameter is safe to reorder; it
// never has a side effect or consumes one.
return kind != RefKind.None;
case BoundKind.PassByCopy:
return IsSafeForReordering(((BoundPassByCopy)current).Expression, kind);
case BoundKind.Conversion:
{
BoundConversion conv = (BoundConversion)current;
switch (conv.ConversionKind)
{
case ConversionKind.AnonymousFunction:
case ConversionKind.ImplicitConstant:
case ConversionKind.MethodGroup:
case ConversionKind.NullLiteral:
case ConversionKind.DefaultLiteral:
return true;
case ConversionKind.Boxing:
case ConversionKind.ImplicitDynamic:
case ConversionKind.ExplicitDynamic:
case ConversionKind.ExplicitEnumeration:
case ConversionKind.ExplicitNullable:
case ConversionKind.ExplicitNumeric:
case ConversionKind.ExplicitReference:
case ConversionKind.Identity:
case ConversionKind.ImplicitEnumeration:
case ConversionKind.ImplicitNullable:
case ConversionKind.ImplicitNumeric:
case ConversionKind.ImplicitReference:
case ConversionKind.Unboxing:
case ConversionKind.ExplicitPointerToInteger:
case ConversionKind.ExplicitPointerToPointer:
case ConversionKind.ImplicitPointerToVoid:
case ConversionKind.ImplicitNullToPointer:
case ConversionKind.ExplicitIntegerToPointer:
current = conv.Operand;
break;
case ConversionKind.ExplicitUserDefined:
case ConversionKind.ImplicitUserDefined:
// expression trees rewrite this later.
// it is a kind of user defined conversions on IntPtr and in some cases can fail
case ConversionKind.IntPtr:
case ConversionKind.ImplicitThrow:
return false;
default:
// when this assert is hit, examine whether such conversion kind is
// 1) actually expected to get this far
// 2) figure if it is possibly not producing or consuming any sideeffects (rare case)
// 3) add a case for it
Debug.Assert(false, "Unexpected conversion kind" + conv.ConversionKind);
// it is safe to assume that conversion is not reorderable
return false;
}
break;
}
}
}
}
internal static bool IsCapturedPrimaryConstructorParameter(BoundExpression expression)
{
return expression is BoundParameter { ParameterSymbol: { ContainingSymbol: SynthesizedPrimaryConstructor primaryCtor } parameter } &&
primaryCtor.GetCapturedParameters().ContainsKey(parameter);
}
private enum ReceiverCaptureMode
{
/// <summary>
/// No special capture of the receiver, unless arguments need to refer to it.
/// For example, in case of a string interpolation handler.
/// </summary>
Default = 0,
/// <summary>
/// Used for a regular indexer compound assignment rewrite.
/// Everything is going to be in a single setter call with a getter call inside its value argument.
/// Only receiver and the indexes can be evaluated prior to evaluating the setter call.
/// </summary>
CompoundAssignment,
/// <summary>
/// Used for situations when additional arbitrary side-effects are possibly involved.
/// Think about deconstruction, etc.
/// </summary>
UseTwiceComplex
}
/// <summary>
/// Visits all arguments of a method, doing any necessary rewriting for interpolated string handler conversions that
/// might be present in the arguments and creating temps for any discard parameters.
/// </summary>
private ImmutableArray<BoundExpression> VisitArgumentsAndCaptureReceiverIfNeeded(
[NotNullIfNotNull(nameof(rewrittenReceiver))] ref BoundExpression? rewrittenReceiver,
ReceiverCaptureMode captureReceiverMode,
ImmutableArray<BoundExpression> arguments,
Symbol methodOrIndexer,
ImmutableArray<int> argsToParamsOpt,
ImmutableArray<RefKind> argumentRefKindsOpt,
ArrayBuilder<BoundExpression>? storesOpt,
ref ArrayBuilder<LocalSymbol>? tempsOpt,
BoundExpression? firstRewrittenArgument = null)
{
Debug.Assert(argumentRefKindsOpt.IsDefault || argumentRefKindsOpt.Length == arguments.Length);
var requiresInstanceReceiver = methodOrIndexer.RequiresInstanceReceiver() && methodOrIndexer is not MethodSymbol { MethodKind: MethodKind.Constructor } and not FunctionPointerMethodSymbol;
Debug.Assert(!requiresInstanceReceiver || rewrittenReceiver != null || _inExpressionLambda);
Debug.Assert(captureReceiverMode == ReceiverCaptureMode.Default || (requiresInstanceReceiver && rewrittenReceiver != null && storesOpt is object));
BoundLocal? receiverTemp = null;
BoundAssignmentOperator? assignmentToTemp = null;
if (captureReceiverMode != ReceiverCaptureMode.Default ||
(requiresInstanceReceiver && arguments.Any(a => usesReceiver(a))))
{
Debug.Assert(!_inExpressionLambda);
Debug.Assert(rewrittenReceiver is object);
Debug.Assert(rewrittenReceiver.Type is { });
RefKind refKind;
if (captureReceiverMode != ReceiverCaptureMode.Default)
{
// SPEC VIOLATION: It is not very clear when receiver of constrained callvirt is dereferenced - when pushed (in lexical order),
// SPEC VIOLATION: or when actual call is executed. The actual behavior seems to be implementation specific in different JITs.
// SPEC VIOLATION: To not depend on that, the right thing to do here is to store the value of the variable
// SPEC VIOLATION: when variable has reference type (regular temp), and store variable's location when it has a value type. (ref temp)
// SPEC VIOLATION: in a case of unconstrained generic type parameter a runtime test (default(T) == null) would be needed
// SPEC VIOLATION: However, for compatibility with Dev12 we will continue treating all generic type parameters, constrained or not,
// SPEC VIOLATION: as value types.
refKind = rewrittenReceiver.Type.IsValueType || rewrittenReceiver.Type.Kind == SymbolKind.TypeParameter ? RefKind.Ref : RefKind.None;
}
else
{
if (rewrittenReceiver.Type.IsReferenceType)
{
refKind = RefKind.None;
}
else
{
refKind = rewrittenReceiver.GetRefKind();
if (refKind == RefKind.None &&
Binder.HasHome(rewrittenReceiver,
Binder.AddressKind.Constrained,
_factory.CurrentFunction,
peVerifyCompatEnabled: false,
stackLocalsOpt: null))
{
refKind = RefKind.Ref;
}
}
}
receiverTemp = _factory.StoreToTemp(rewrittenReceiver, out assignmentToTemp, refKind);
tempsOpt ??= ArrayBuilder<LocalSymbol>.GetInstance();
tempsOpt.Add(receiverTemp.LocalSymbol);
}
ImmutableArray<BoundExpression> rewrittenArguments;
if (arguments.IsEmpty)
{
rewrittenArguments = arguments;
}
else
{
var argumentsAssignedToTemp = BitVector.Null;
var visitedArgumentsBuilder = ArrayBuilder<BoundExpression>.GetInstance(arguments.Length);
var parameters = methodOrIndexer.GetParameters();
#if DEBUG
var saveTempsOpt = tempsOpt;
#endif
for (int i = 0; i < arguments.Length; i++)
{
var argument = arguments[i];
if (argument is BoundDiscardExpression discard)
{
ensureTempTrackingSetup(ref tempsOpt, ref argumentsAssignedToTemp);
visitedArgumentsBuilder.Add(_factory.MakeTempForDiscard(discard, tempsOpt));
argumentsAssignedToTemp[i] = true;
continue;
}
ImmutableArray<BoundInterpolatedStringArgumentPlaceholder> argumentPlaceholders = addInterpolationPlaceholderReplacements(
parameters,
visitedArgumentsBuilder,
i,
receiverTemp,
ref tempsOpt,
ref argumentsAssignedToTemp);
visitedArgumentsBuilder.Add(i == 0 && firstRewrittenArgument is not null
? firstRewrittenArgument
: VisitExpression(argument));
foreach (var placeholder in argumentPlaceholders)
{
// We didn't set this one up, so we can't remove it.
if (placeholder.ArgumentIndex == BoundInterpolatedStringArgumentPlaceholder.TrailingConstructorValidityParameter)
{
continue;
}
RemovePlaceholderReplacement(placeholder);
}
}
#if DEBUG
Debug.Assert(saveTempsOpt is object || tempsOpt?.Count is null or > 0);
#endif
rewrittenArguments = visitedArgumentsBuilder.ToImmutableAndFree();
}
if (receiverTemp is object)
{
Debug.Assert(assignmentToTemp is object);
Debug.Assert(tempsOpt is object);
BoundAssignmentOperator? extraRefInitialization = null;
if (receiverTemp.LocalSymbol.IsRef &&
CodeGenerator.IsPossibleReferenceTypeReceiverOfConstrainedCall(receiverTemp) &&
!CodeGenerator.ReceiverIsKnownToReferToTempIfReferenceType(receiverTemp) &&
(captureReceiverMode == ReceiverCaptureMode.UseTwiceComplex ||
!CodeGenerator.IsSafeToDereferenceReceiverRefAfterEvaluatingArguments(rewrittenArguments)))
{
ReferToTempIfReferenceTypeReceiver(receiverTemp, ref assignmentToTemp, out extraRefInitialization, tempsOpt);
}
if (storesOpt is object)
{
if (extraRefInitialization is object)
{
storesOpt.Add(extraRefInitialization);
}
storesOpt.Add(assignmentToTemp);
rewrittenReceiver = receiverTemp;
}
else
{
rewrittenReceiver = _factory.Sequence(
ImmutableArray<LocalSymbol>.Empty,
extraRefInitialization is object ? ImmutableArray.Create<BoundExpression>(extraRefInitialization, assignmentToTemp) : ImmutableArray.Create<BoundExpression>(assignmentToTemp),
receiverTemp);
}
}
return rewrittenArguments;
void ensureTempTrackingSetup([NotNull] ref ArrayBuilder<LocalSymbol>? tempsOpt, ref BitVector positionsAssignedToTemp)
{
tempsOpt ??= ArrayBuilder<LocalSymbol>.GetInstance();
if (positionsAssignedToTemp.IsNull)
{
positionsAssignedToTemp = BitVector.Create(arguments.Length);
}
}
ImmutableArray<BoundInterpolatedStringArgumentPlaceholder> addInterpolationPlaceholderReplacements(
ImmutableArray<ParameterSymbol> parameters,
ArrayBuilder<BoundExpression> visitedArgumentsBuilder,
int argumentIndex,
BoundLocal? receiverTemp,
ref ArrayBuilder<LocalSymbol>? tempsOpt,
ref BitVector argumentsAssignedToTemp)
{
var argument = arguments[argumentIndex];
if (argument is BoundConversion { ConversionKind: ConversionKind.InterpolatedStringHandler, Operand: BoundInterpolatedString or BoundBinaryOperator } conversion)
{
// Handler conversions are not supported in expression lambdas.
Debug.Assert(!_inExpressionLambda);
var interpolationData = conversion.Operand.GetInterpolatedStringHandlerData();
if (interpolationData.ArgumentPlaceholders.Length > (interpolationData.HasTrailingHandlerValidityParameter ? 1 : 0))
{
Debug.Assert(!((BoundConversion)argument).ExplicitCastInCode);
// We have an interpolated string handler conversion that needs context from the surrounding arguments. We need to store
// all arguments up to and including the last argument needed by this interpolated string conversion into temps, in order
// to ensure we're keeping lexical ordering of side effects.
ensureTempTrackingSetup(ref tempsOpt, ref argumentsAssignedToTemp);
Debug.Assert(!argumentsAssignedToTemp.IsNull);
foreach (var placeholder in interpolationData.ArgumentPlaceholders)
{
// Replace each needed placeholder with a sequence of store and evaluate the temp.
var argIndex = placeholder.ArgumentIndex;
Debug.Assert(argIndex < argumentIndex);
BoundLocal local;
switch (argIndex)
{
case BoundInterpolatedStringArgumentPlaceholder.InstanceParameter:
Debug.Assert(usesReceiver(argument));
Debug.Assert(requiresInstanceReceiver);
Debug.Assert(receiverTemp is object);
local = receiverTemp;
break;
case >= 0 when argumentsAssignedToTemp[argIndex]:
local = visitedArgumentsBuilder[argIndex] switch
{
BoundSequence { Value: BoundLocal l } => l,
BoundLocal l => l, // Can happen for discard arguments
var u => throw ExceptionUtilities.UnexpectedValue(u.Kind)
};
break;
case >= 0:
Debug.Assert(visitedArgumentsBuilder[argIndex] != null);
var paramIndex = argsToParamsOpt.IsDefault ? argIndex : argsToParamsOpt[argIndex];
RefKind argRefKind = argumentRefKindsOpt.RefKinds(argIndex);
RefKind paramRefKind = parameters[paramIndex].RefKind;
var visitedArgument = visitedArgumentsBuilder[argIndex];
local = _factory.StoreToTemp(visitedArgument, out var store, refKind: paramRefKind is RefKind.In or RefKind.RefReadOnlyParameter ? RefKind.In : argRefKind);
tempsOpt.Add(local.LocalSymbol);
visitedArgumentsBuilder[argIndex] = _factory.Sequence(ImmutableArray<LocalSymbol>.Empty, ImmutableArray.Create<BoundExpression>(store), local);
argumentsAssignedToTemp[argIndex] = true;
break;
case BoundInterpolatedStringArgumentPlaceholder.TrailingConstructorValidityParameter:
// Visiting the interpolated string itself will allocate the temp for this one.
continue;
default:
throw ExceptionUtilities.UnexpectedValue(argIndex);
}
AddPlaceholderReplacement(placeholder, local);
}
return interpolationData.ArgumentPlaceholders;
}
}
return ImmutableArray<BoundInterpolatedStringArgumentPlaceholder>.Empty;
}
static bool usesReceiver(BoundExpression argument)
{
if (argument is BoundConversion { ConversionKind: ConversionKind.InterpolatedStringHandler, Operand: BoundInterpolatedString or BoundBinaryOperator } conversion)
{
var interpolationData = conversion.Operand.GetInterpolatedStringHandlerData();
if (interpolationData.ArgumentPlaceholders.Length > (interpolationData.HasTrailingHandlerValidityParameter ? 1 : 0))
{
Debug.Assert(!((BoundConversion)argument).ExplicitCastInCode);
foreach (var placeholder in interpolationData.ArgumentPlaceholders)
{
if (placeholder.ArgumentIndex == BoundInterpolatedStringArgumentPlaceholder.InstanceParameter)
{
return true;
}
}
}
}
return false;
}
}
private void ReferToTempIfReferenceTypeReceiver(BoundLocal receiverTemp, ref BoundAssignmentOperator assignmentToTemp, out BoundAssignmentOperator? extraRefInitialization, ArrayBuilder<LocalSymbol> temps)
{
Debug.Assert(assignmentToTemp.IsRef);
var receiverType = receiverTemp.Type;
Debug.Assert(receiverType is object);
// A case where T is actually a class must be handled specially.
// Taking a reference to a class instance is fragile because the value behind the
// reference might change while arguments are evaluated. However, the call should be
// performed on the instance that is behind reference at the time we push the
// reference to the stack. So, for a class we need to emit a reference to a temporary
// location, rather than to the original location
BoundLocal cache = _factory.Local(_factory.SynthesizedLocal(receiverType));
temps.Add(cache.LocalSymbol);
if (!receiverType.IsReferenceType)
{
// Store receiver ref to a different ref local - intermediate ref
var intermediateRef = _factory.Local(_factory.SynthesizedLocal(receiverType, refKind: RefKind.Ref));
temps.Add(intermediateRef.LocalSymbol);
extraRefInitialization = assignmentToTemp.Update(intermediateRef, assignmentToTemp.Right, assignmentToTemp.IsRef, assignmentToTemp.Type);
// `receiverTemp` initialization is adjusted as follows:
// If we are dealing with a value type, use value of the intermediate ref.
// Otherwise, use an address of a temp where we store the underlying reference type instance.
assignmentToTemp =
assignmentToTemp.Update(
assignmentToTemp.Left,
#pragma warning disable format
new BoundComplexConditionalReceiver(receiverTemp.Syntax,
intermediateRef,
_factory.Sequence(new BoundExpression[] { _factory.AssignmentExpression(cache, intermediateRef) }, cache),
receiverType) { WasCompilerGenerated = true },
#pragma warning restore format
assignmentToTemp.IsRef,
assignmentToTemp.Type);
// SpillSequenceSpiller should be able to recognize this node in order to handle its spilling.
Debug.Assert(SpillSequenceSpiller.IsComplexConditionalInitializationOfReceiverRef(assignmentToTemp, out _, out _, out _, out _));
}
else
{
extraRefInitialization = null;
// We are dealing with a reference type. We can simply copy the instance into a temp and
// use its address instead.
assignmentToTemp =
assignmentToTemp.Update(
assignmentToTemp.Left,
_factory.Sequence(new BoundExpression[] { _factory.AssignmentExpression(cache, assignmentToTemp.Right) }, cache),
assignmentToTemp.IsRef,
assignmentToTemp.Type);
}
((SynthesizedLocal)receiverTemp.LocalSymbol).SetIsKnownToReferToTempIfReferenceType();
Debug.Assert(CodeGenerator.ReceiverIsKnownToReferToTempIfReferenceType(receiverTemp));
}
/// <summary>
/// Rewrites arguments of an invocation according to the receiving method or indexer.
/// It is assumed that each argument has already been lowered, but we may need
/// additional rewriting for the arguments, such as re-ordering
/// arguments based on <paramref name="argsToParamsOpt"/> map, etc.
/// </summary>
private ImmutableArray<BoundExpression> MakeArguments(
ImmutableArray<BoundExpression> rewrittenArguments,
Symbol methodOrIndexer,
bool expanded,
ImmutableArray<int> argsToParamsOpt,
ref ImmutableArray<RefKind> argumentRefKindsOpt,
[NotNull] ref ArrayBuilder<LocalSymbol>? temps,
bool invokedAsExtensionMethod = false)
{
// We need to do a fancy rewrite under the following circumstances:
// (1) there were named arguments that reordered the arguments; we might
// have to generate temporaries to ensure that the arguments are
// evaluated in source code order, not the actual call order.
//
// If none of those are the case then we can just take an early out.
Debug.Assert(rewrittenArguments.All(arg => arg is not BoundDiscardExpression), "Discards should have been substituted by VisitArguments");
temps ??= ArrayBuilder<LocalSymbol>.GetInstance();
ImmutableArray<ParameterSymbol> parameters = methodOrIndexer.GetParameters();
BoundExpression? optimized;
Debug.Assert(expanded ? rewrittenArguments.Length == parameters.Length : rewrittenArguments.Length >= parameters.Length);
Debug.Assert(rewrittenArguments.Count(a => a.IsParamsArrayOrCollection) <= (expanded ? 1 : 0));
if (CanSkipRewriting(rewrittenArguments, methodOrIndexer, argsToParamsOpt, invokedAsExtensionMethod, false, out var isComReceiver))
{
argumentRefKindsOpt = GetEffectiveArgumentRefKinds(argumentRefKindsOpt, parameters);
if (expanded && TryOptimizeParamsArray(rewrittenArguments[rewrittenArguments.Length - 1], out optimized))
{
return rewrittenArguments.SetItem(rewrittenArguments.Length - 1, optimized);
}
return rewrittenArguments;
}
// We have:
// * a list of arguments, already converted to their proper types,
// in source code order. Some optional arguments might be missing.
// * a map showing which parameter each argument corresponds to. If
// this is null, then the argument to parameter mapping is one-to-one.
// * the ref kind of each argument, in source code order. That is, whether
// the argument was marked as ref, out, or value (neither).
// * a method symbol.
// * whether the call is expanded or normal form.
// We rewrite the call so that:
// * if in its expanded form, we create the params array.
// * if the call requires reordering of arguments because of named arguments, temporaries are generated as needed
// Doing this transformation can move around refness in interesting ways. For example, consider
//
// A().M(y : ref B()[C()], x : out D());
//
// This will be created as a call with receiver A(), symbol M, argument list ( B()[C()], D() ),
// name list ( y, x ) and ref list ( ref, out ). We can rewrite this into temporaries:
//
// A().M(
// seq ( ref int temp_y = ref B()[C()], out D() ),
// temp_y );
//
// Now we have a call with receiver A(), symbol M, argument list as shown, no name list,
// and ref list ( out, value ). We do not want to pass a *ref* to temp_y; the temporary
// storage is not the thing being ref'd! We want to pass the *value* of temp_y, which
// *contains* a reference.
// We attempt to minimize the number of temporaries required. Arguments which neither
// produce nor observe a side effect can be placed into their proper position without
// recourse to a temporary. For example:
//
// Where(predicate: x=>x.Length!=0, sequence: S())
//
// can be rewritten without any temporaries because the conversion from lambda to
// delegate does not produce any side effect that could be observed by S().
//
// By contrast:
//
// Goo(z: this.p, y: this.Q(), x: (object)10)
//
// The boxing of 10 can be reordered, but the fetch of this.p has to happen before the
// call to this.Q() because the call could change the value of this.p.
//
// We start by binding everything that is not obviously reorderable as a temporary, and
// then run an optimizer to remove unnecessary temporaries.
BoundExpression[] actualArguments = new BoundExpression[parameters.Length]; // The actual arguments that will be passed; one actual argument per formal parameter.
ArrayBuilder<BoundAssignmentOperator> storesToTemps = ArrayBuilder<BoundAssignmentOperator>.GetInstance(rewrittenArguments.Length);
ArrayBuilder<RefKind> refKinds = ArrayBuilder<RefKind>.GetInstance(parameters.Length, RefKind.None);
// Step one: Store everything that is non-trivial into a temporary; record the
// stores in storesToTemps and make the actual argument a reference to the temp.
BuildStoresToTemps(
expanded,
argsToParamsOpt,
parameters,
argumentRefKindsOpt,
rewrittenArguments,
forceLambdaSpilling: false, // lambda conversions can be re-ordered in calls without side affects
actualArguments,
refKinds,
storesToTemps);
// all the formal arguments are now in place.
// Optimize away unnecessary temporaries.
// Necessary temporaries have their store instructions merged into the appropriate
// argument expression.
OptimizeTemporaries(actualArguments, storesToTemps, temps);
storesToTemps.Free();
if (expanded && TryOptimizeParamsArray(actualArguments[actualArguments.Length - 1], out optimized))
{
actualArguments[actualArguments.Length - 1] = optimized;
}
if (isComReceiver)
{
RewriteArgumentsForComCall(parameters, actualArguments, refKinds, temps);
}
// * The refkind map is now filled out to match the arguments.
// * The list of parameter names is now null because the arguments have been reordered.
// * The args-to-params map is now null because every argument exactly matches its parameter.
// * The call is no longer in its expanded form.
argumentRefKindsOpt = GetRefKindsOrNull(refKinds);
refKinds.Free();
Debug.Assert(actualArguments.All(static arg => arg is not null));
return actualArguments.AsImmutableOrNull();
}
private bool TryOptimizeParamsArray(BoundExpression possibleParamsArray, [NotNullWhen(true)] out BoundExpression? optimized)
{
if (possibleParamsArray.IsParamsArrayOrCollection && !_inExpressionLambda && ((BoundArrayCreation)possibleParamsArray).Bounds is [BoundLiteral { ConstantValueOpt.Value: 0 }])
{
optimized = CreateArrayEmptyCallIfAvailable(possibleParamsArray.Syntax, ((ArrayTypeSymbol)possibleParamsArray.Type!).ElementType);
if (optimized is { })
{
return true;
}
}
optimized = null;
return false;
}
/// <summary>
/// Patch refKinds for arguments that match 'in', 'ref', or 'ref readonly' parameters to have effective RefKind.
/// For the purpose of further analysis we will mark the arguments as -
/// - In if was originally passed as None and matches an 'in' or 'ref readonly' parameter
/// - StrictIn if was originally passed as In or Ref and matches an 'in' or 'ref readonly' parameter
/// - Ref if the argument is an interpolated string literal subject to an interpolated string handler conversion. No other types
/// are patched here.
/// Here and in the layers after the lowering we only care about None/notNone differences for the arguments
/// Except for async stack spilling which needs to know whether arguments were originally passed as "In" and must obey "no copying" rule.
/// </summary>
private static ImmutableArray<RefKind> GetEffectiveArgumentRefKinds(ImmutableArray<RefKind> argumentRefKindsOpt, ImmutableArray<ParameterSymbol> parameters)
{
ArrayBuilder<RefKind>? refKindsBuilder = null;
for (int i = 0; i < parameters.Length; i++)
{
var paramRefKind = parameters[i].RefKind;
if (paramRefKind is RefKind.In or RefKind.RefReadOnlyParameter)
{
var argRefKind = argumentRefKindsOpt.IsDefault ? RefKind.None : argumentRefKindsOpt[i];
fillRefKindsBuilder(argumentRefKindsOpt, parameters, ref refKindsBuilder);
refKindsBuilder[i] = argRefKind == RefKind.None ? RefKind.In : RefKindExtensions.StrictIn;
}
else if (paramRefKind == RefKind.Ref)
{
var argRefKind = argumentRefKindsOpt.IsDefault ? RefKind.None : argumentRefKindsOpt[i];
if (argRefKind == RefKind.None)
{
// Interpolated strings used as interpolated string handlers are allowed to match ref parameters without `ref`
Debug.Assert(parameters[i].Type is NamedTypeSymbol { IsInterpolatedStringHandlerType: true, IsValueType: true });
fillRefKindsBuilder(argumentRefKindsOpt, parameters, ref refKindsBuilder);
refKindsBuilder[i] = RefKind.Ref;
}
}
}
if (refKindsBuilder != null)
{
argumentRefKindsOpt = refKindsBuilder.ToImmutableAndFree();
}
// NOTE: we may have more arguments than parameters in a case of arglist. That is ok.
Debug.Assert(argumentRefKindsOpt.IsDefault || argumentRefKindsOpt.Length >= parameters.Length);
return argumentRefKindsOpt;
static void fillRefKindsBuilder(ImmutableArray<RefKind> argumentRefKindsOpt, ImmutableArray<ParameterSymbol> parameters, [NotNull] ref ArrayBuilder<RefKind>? refKindsBuilder)
{
if (refKindsBuilder == null)
{
if (!argumentRefKindsOpt.IsDefault)
{
Debug.Assert(!argumentRefKindsOpt.IsEmpty);
refKindsBuilder = ArrayBuilder<RefKind>.GetInstance(parameters.Length);
refKindsBuilder.AddRange(argumentRefKindsOpt);
}
else
{
refKindsBuilder = ArrayBuilder<RefKind>.GetInstance(parameters.Length, fillWithValue: RefKind.None);
}
}
}
}
// temporariesBuilder will be null when factory is null.
internal static bool CanSkipRewriting(
ImmutableArray<BoundExpression> rewrittenArguments,
Symbol methodOrIndexer,
ImmutableArray<int> argsToParamsOpt,
bool invokedAsExtensionMethod,
bool ignoreComReceiver,
out bool isComReceiver)
{
isComReceiver = false;
// An applicable "vararg" method could not possibly be applicable in its expanded
// form, and cannot possibly have named arguments or used optional parameters,
// because the __arglist() argument has to be positional and in the last position.
if (methodOrIndexer.GetIsVararg())
{
Debug.Assert(rewrittenArguments.Length == methodOrIndexer.GetParameterCount() + 1);
Debug.Assert(argsToParamsOpt.IsDefault);
return true;
}
if (!ignoreComReceiver)
{
var receiverNamedType = invokedAsExtensionMethod ?
((MethodSymbol)methodOrIndexer).Parameters[0].Type as NamedTypeSymbol :
methodOrIndexer.ContainingType;
isComReceiver = receiverNamedType is { IsComImport: true };
}
return rewrittenArguments.Length == methodOrIndexer.GetParameterCount() &&
argsToParamsOpt.IsDefault &&
!isComReceiver;
}
private static ImmutableArray<RefKind> GetRefKindsOrNull(ArrayBuilder<RefKind> refKinds)
{
foreach (var refKind in refKinds)
{
if (refKind != RefKind.None)
{
return refKinds.ToImmutable();
}
}
return default(ImmutableArray<RefKind>);
}
private delegate BoundExpression ParamsArrayElementRewriter<TArg>(BoundExpression element, ref TArg arg);
private static BoundExpression RewriteParamsArray<TArg>(BoundExpression paramsArray, ParamsArrayElementRewriter<TArg> elementRewriter, ref TArg arg)
{
Debug.Assert(paramsArray.IsParamsArrayOrCollection);
if (paramsArray is BoundArrayCreation { Bounds: [BoundLiteral] bounds, InitializerOpt: BoundArrayInitialization { Initializers: var elements } initialization } creation)
{
ArrayBuilder<BoundExpression>? elementsBuilder = null;
for (int i = 0; i < elements.Length; i++)
{
var element = elements[i];
var replacement = elementRewriter(element, ref arg);
if (element != replacement)
{
if (elementsBuilder == null)
{
elementsBuilder = ArrayBuilder<BoundExpression>.GetInstance(elements.Length);
elementsBuilder.AddRange(elements, i);
}
elementsBuilder.Add(replacement);
}
else if (elementsBuilder is { })
{
elementsBuilder.Add(replacement);
}
}
if (elementsBuilder is { })
{
return creation.Update(bounds, initialization.Update(elementsBuilder.ToImmutableAndFree()), creation.Type);
}
else
{
return creation;
}
}
else
{
throw ExceptionUtilities.Unreachable();
}
}
// This fills in the arguments, refKinds and storesToTemps arrays.
private void BuildStoresToTemps(
bool expanded,
ImmutableArray<int> argsToParamsOpt,
ImmutableArray<ParameterSymbol> parameters,
ImmutableArray<RefKind> argumentRefKinds,
ImmutableArray<BoundExpression> rewrittenArguments,
bool forceLambdaSpilling,
/* out */ BoundExpression[] arguments,
/* out */ ArrayBuilder<RefKind> refKinds,
/* out */ ArrayBuilder<BoundAssignmentOperator> storesToTemps)
{
Debug.Assert(refKinds.Count == arguments.Length);
Debug.Assert(storesToTemps.Count == 0);
Debug.Assert(rewrittenArguments.Length == parameters.Length);
Debug.Assert(rewrittenArguments.Count(a => a.IsParamsArrayOrCollection) <= (expanded ? 1 : 0));
for (int a = 0; a < rewrittenArguments.Length; ++a)
{
BoundExpression argument = rewrittenArguments[a];
int p = (!argsToParamsOpt.IsDefault) ? argsToParamsOpt[a] : a;
RefKind argRefKind = argumentRefKinds.RefKinds(a);
RefKind paramRefKind = parameters[p].RefKind;
Debug.Assert(arguments[p] == null);
if (argument.IsParamsArrayOrCollection)
{
Debug.Assert(expanded);
Debug.Assert(p == parameters.Length - 1);
Debug.Assert(argRefKind == RefKind.None);
refKinds[p] = argRefKind;
if (a == rewrittenArguments.Length - 1)
{
arguments[p] = argument;
}
else
{
// Storing the array creation instead changes IL for
// Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen.CodeGenTests.NamedParamsOptimizationAndParams002
// unit test.
(LocalRewriter rewriter, bool forceLambdaSpilling, ArrayBuilder<BoundAssignmentOperator> storesToTemps) arg = (rewriter: this, forceLambdaSpilling, storesToTemps);
arguments[p] = RewriteParamsArray(
argument,
static (BoundExpression element, ref (LocalRewriter rewriter, bool forceLambdaSpilling, ArrayBuilder<BoundAssignmentOperator> storesToTemps) arg) =>
arg.rewriter.StoreArgumentToTempIfNecessary(arg.forceLambdaSpilling, arg.storesToTemps, element, RefKind.None, RefKind.None),
ref arg);
Debug.Assert(arguments[p].IsParamsArrayOrCollection);
}
continue;
}
arguments[p] = StoreArgumentToTempIfNecessary(forceLambdaSpilling, storesToTemps, argument, argRefKind, paramRefKind);
// Patch refKinds for arguments that match 'in' or 'ref readonly' parameters to have effective RefKind
// For the purpose of further analysis we will mark the arguments as -
// - In if was originally passed as None and matches an 'in' or 'ref readonly' parameter
// - StrictIn if was originally passed as In or Ref and matches an 'in' or 'ref readonly' parameter
// Here and in the layers after the lowering we only care about None/notNone differences for the arguments
// Except for async stack spilling which needs to know whether arguments were originally passed as "In" and must obey "no copying" rule.
if (paramRefKind is RefKind.In or RefKind.RefReadOnlyParameter)
{
Debug.Assert(argRefKind is RefKind.None or RefKind.In or RefKind.Ref);
argRefKind = argRefKind == RefKind.None ? RefKind.In : RefKindExtensions.StrictIn;
}
refKinds[p] = argRefKind;
}
return;
}
private BoundExpression StoreArgumentToTempIfNecessary(bool forceLambdaSpilling, ArrayBuilder<BoundAssignmentOperator> storesToTemps, BoundExpression argument, RefKind argRefKind, RefKind paramRefKind)
{
if ((!forceLambdaSpilling || !isLambdaConversion(argument)) &&
IsSafeForReordering(argument, argRefKind))
{
return argument;
}
else
{
var temp = _factory.StoreToTemp(
argument,
out BoundAssignmentOperator assignment,
refKind: paramRefKind is RefKind.In or RefKind.RefReadOnlyParameter
? (argRefKind == RefKind.None ? RefKind.In : RefKindExtensions.StrictIn)
: argRefKind);
storesToTemps.Add(assignment);
return temp;
}
bool isLambdaConversion(BoundExpression expr)
=> expr is BoundConversion conv && conv.ConversionKind == ConversionKind.AnonymousFunction;
}
private BoundExpression CreateEmptyArray(SyntaxNode syntax, ArrayTypeSymbol arrayType)
{
BoundExpression? arrayEmpty = CreateArrayEmptyCallIfAvailable(syntax, arrayType.ElementType);
if (arrayEmpty is { })
{
return arrayEmpty;
}
// new T[0]
return new BoundArrayCreation(
syntax,
ImmutableArray.Create<BoundExpression>(
new BoundLiteral(
syntax,
ConstantValue.Create(0),
_compilation.GetSpecialType(SpecialType.System_Int32))),
initializerOpt: null,
arrayType)
{ WasCompilerGenerated = true };
}
private BoundExpression? CreateArrayEmptyCallIfAvailable(SyntaxNode syntax, TypeSymbol elementType)
{
if (elementType.IsPointerOrFunctionPointer())
{
// Pointer types cannot be used as type arguments.
return null;
}
MethodSymbol? arrayEmpty = _compilation.GetSpecialTypeMember(SpecialMember.System_Array__Empty) as MethodSymbol;
if (arrayEmpty is null) // will be null if Array.Empty<T> doesn't exist in reference assemblies
{
return null;
}
_diagnostics.ReportUseSite(arrayEmpty, syntax);
// return an invocation of "Array.Empty<T>()"
arrayEmpty = arrayEmpty.Construct(ImmutableArray.Create(elementType));
return new BoundCall(
syntax,
receiverOpt: null,
initialBindingReceiverIsSubjectToCloning: ThreeState.Unknown,
arrayEmpty,
ImmutableArray<BoundExpression>.Empty,
default(ImmutableArray<string?>),
default(ImmutableArray<RefKind>),
isDelegateCall: false,
expanded: false,
invokedAsExtensionMethod: false,
argsToParamsOpt: default(ImmutableArray<int>),
defaultArguments: default(BitVector),
resultKind: LookupResultKind.Viable,
type: arrayEmpty.ReturnType);
}
private static void OptimizeTemporaries(
BoundExpression[] arguments,
ArrayBuilder<BoundAssignmentOperator> storesToTemps,
ArrayBuilder<LocalSymbol> temporariesBuilder)
{
Debug.Assert(arguments != null);
Debug.Assert(storesToTemps != null);
Debug.Assert(temporariesBuilder != null);
if (storesToTemps.Count > 0)
{
int tempsNeeded = MergeArgumentsAndSideEffects(arguments, storesToTemps);
if (tempsNeeded > 0)
{
foreach (BoundAssignmentOperator s in storesToTemps)
{
if (s != null)
{
temporariesBuilder.Add(((BoundLocal)s.Left).LocalSymbol);
}
}
}
}
}
/// <summary>
/// Process tempStores and add them as side-effects to arguments where needed. The return
/// value tells how many temps are actually needed. For unnecessary temps the corresponding
/// temp store will be cleared.
/// </summary>
private static int MergeArgumentsAndSideEffects(
BoundExpression[] arguments,
ArrayBuilder<BoundAssignmentOperator> tempStores)
{
Debug.Assert(arguments != null);
Debug.Assert(tempStores != null);
int tempsRemainedInUse = tempStores.Count;
// Suppose we've got temporaries: t0 = A(), t1 = B(), t2 = C(), t4 = D(), t5 = E()
// and arguments: t0, t2, t1, t4, 10, t5
//
// We wish to produce arguments list: A(), SEQ(t1=B(), C()), t1, D(), 10, E()
//
// Our algorithm essentially finds temp stores that must happen before given argument
// load, and if there are any they become side effects of the given load.
// Stores immediately followed by loads of the same thing can be eliminated.
//
// Constraints:
// Stores must happen before corresponding loads.
// Stores cannot move relative to other stores. If arg was movable it would not need a temp.
int firstUnclaimedStore = 0;
for (int a = 0; a < arguments.Length; ++a)
{
var argument = arguments[a];
if (argument.IsParamsArrayOrCollection)
{
(ArrayBuilder<BoundAssignmentOperator> tempStores, int tempsRemainedInUse, int firstUnclaimedStore) arg = (tempStores, tempsRemainedInUse, firstUnclaimedStore);
arguments[a] = RewriteParamsArray(
argument,
static (BoundExpression element, ref (ArrayBuilder<BoundAssignmentOperator> tempStores, int tempsRemainedInUse, int firstUnclaimedStore) arg) =>
mergeArgumentAndSideEffect(element, arg.tempStores, ref arg.tempsRemainedInUse, ref arg.firstUnclaimedStore),
ref arg);
tempsRemainedInUse = arg.tempsRemainedInUse;
firstUnclaimedStore = arg.firstUnclaimedStore;
}
else
{
arguments[a] = mergeArgumentAndSideEffect(argument, tempStores, ref tempsRemainedInUse, ref firstUnclaimedStore);
}
}
Debug.Assert(firstUnclaimedStore == tempStores.Count, "not all side-effects were claimed");
return tempsRemainedInUse;
static BoundExpression mergeArgumentAndSideEffect(BoundExpression argument, ArrayBuilder<BoundAssignmentOperator> tempStores, ref int tempsRemainedInUse, ref int firstUnclaimedStore)
{
// if argument is a load, search for corresponding store. if store is found, extract
// the actual expression we were storing and add it as an argument - this one does
// not need a temp. if there are any unclaimed stores before the found one, add them
// as side effects that precede this arg, they cannot happen later.
// NOTE: missing optional parameters are not filled yet and therefore nulls - no need to do anything for them
if (argument.Kind == BoundKind.Local)
{
var correspondingStore = -1;
for (int i = firstUnclaimedStore; i < tempStores.Count; i++)
{
if (tempStores[i].Left == argument)
{
correspondingStore = i;
break;
}
}
// store found?
if (correspondingStore != -1)
{
var value = tempStores[correspondingStore].Right;
Debug.Assert(value.Type is { });
// the matched store will not need to go into side-effects, only ones before it will
// remove the store to signal that we are not using its temp.
tempStores[correspondingStore] = null!;
tempsRemainedInUse--;
// no need for side-effects?
// just combine store and load
if (correspondingStore == firstUnclaimedStore)
{
argument = value;
}
else
{
var sideeffects = new BoundExpression[correspondingStore - firstUnclaimedStore];
for (int s = 0; s < sideeffects.Length; s++)
{
sideeffects[s] = tempStores[firstUnclaimedStore + s];
}
argument = new BoundSequence(
value.Syntax,
// this sequence does not own locals. Note that temps that
// we use for the rewrite are stored in one arg and loaded
// in another so they must live in a scope above.
ImmutableArray<LocalSymbol>.Empty,
sideeffects.AsImmutableOrNull(),
value,
value.Type);
}
firstUnclaimedStore = correspondingStore + 1;
}
}
return argument;
}
}
// Omit ref feature for COM interop: We can pass arguments by value for ref parameters if we are calling a method/property on an instance of a COM imported type.
// We should have ignored the 'ref' on the parameter during overload resolution for the given method call.
// If we had any ref omitted argument for the given call, we create a temporary local and
// replace the argument with the following BoundSequence: { side-effects: { temp = argument }, value = { ref temp } }
// NOTE: The temporary local must be scoped to live across the entire BoundCall node,
// otherwise the codegen optimizer might re-use the same temporary for multiple ref-omitted arguments for this call.
private void RewriteArgumentsForComCall(
ImmutableArray<ParameterSymbol> parameters,
BoundExpression[] actualArguments, //already re-ordered to match parameters
ArrayBuilder<RefKind> argsRefKindsBuilder,
ArrayBuilder<LocalSymbol> temporariesBuilder)
{
Debug.Assert(actualArguments != null);
Debug.Assert(actualArguments.Length == parameters.Length);
Debug.Assert(argsRefKindsBuilder != null);
Debug.Assert(argsRefKindsBuilder.Count == parameters.Length);
var argsCount = actualArguments.Length;
for (int argIndex = 0; argIndex < argsCount; ++argIndex)
{
RefKind paramRefKind = parameters[argIndex].RefKind;
RefKind argRefKind = argsRefKindsBuilder[argIndex];
// Rewrite only if the argument was passed with no ref/out and the
// parameter was declared ref.
if (argRefKind != RefKind.None || paramRefKind != RefKind.Ref)
{
continue;
}
var argument = actualArguments[argIndex];
if (argument.Kind == BoundKind.Local)
{
var localRefKind = ((BoundLocal)argument).LocalSymbol.RefKind;
if (localRefKind == RefKind.Ref)
{
// Already passing an address from the ref local.
continue;
}
Debug.Assert(localRefKind == RefKind.None);
}
BoundAssignmentOperator boundAssignmentToTemp;
BoundLocal boundTemp = _factory.StoreToTemp(argument, out boundAssignmentToTemp);
actualArguments[argIndex] = new BoundSequence(
argument.Syntax,
locals: ImmutableArray<LocalSymbol>.Empty,
sideEffects: ImmutableArray.Create<BoundExpression>(boundAssignmentToTemp),
value: boundTemp,
type: boundTemp.Type);
argsRefKindsBuilder[argIndex] = RefKind.Ref;
temporariesBuilder.Add(boundTemp.LocalSymbol);
}
}
public override BoundNode VisitDynamicMemberAccess(BoundDynamicMemberAccess node)
{
// InvokeMember operation:
if (node.Invoked)
{
return node;
}
// GetMember operation:
Debug.Assert(node.TypeArgumentsOpt.IsDefault);
var loweredReceiver = VisitExpression(node.Receiver);
return _dynamicFactory.MakeDynamicGetMember(loweredReceiver, node.Name, node.Indexed).ToExpression();
}
}
}
|