File: Microsoft\CSharp\RuntimeBinder\BinderHelper.cs
Web Access
Project: src\src\libraries\Microsoft.CSharp\src\Microsoft.CSharp.csproj (Microsoft.CSharp)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Numerics.Hashing;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.CSharp.RuntimeBinder.Errors;
 
namespace Microsoft.CSharp.RuntimeBinder
{
    internal static class BinderHelper
    {
        private static MethodInfo s_DoubleIsNaN;
        private static MethodInfo s_SingleIsNaN;
 
        [RequiresUnreferencedCode(Binder.TrimmerWarning)]
        internal static DynamicMetaObject Bind(
                ICSharpBinder action,
                RuntimeBinder binder,
                DynamicMetaObject[] args,
                IEnumerable<CSharpArgumentInfo> arginfos,
                DynamicMetaObject onBindingError)
        {
            Expression[] parameters = new Expression[args.Length];
            BindingRestrictions restrictions = BindingRestrictions.Empty;
            ICSharpInvokeOrInvokeMemberBinder callPayload = action as ICSharpInvokeOrInvokeMemberBinder;
            ParameterExpression tempForIncrement = null;
            IEnumerator<CSharpArgumentInfo> arginfosEnum = (arginfos ?? Array.Empty<CSharpArgumentInfo>()).GetEnumerator();
 
            for (int index = 0; index < args.Length; ++index)
            {
                DynamicMetaObject o = args[index];
                // Our contract with the DLR is such that we will not enter a bind unless we have
                // values for the meta-objects involved.
 
                Debug.Assert(o.HasValue);
 
                CSharpArgumentInfo info = arginfosEnum.MoveNext() ? arginfosEnum.Current : null;
 
                if (index == 0 && IsIncrementOrDecrementActionOnLocal(action))
                {
                    // We have an inc or a dec operation. Insert the temp local instead.
                    //
                    // We need to do this because for value types, the object will come
                    // in boxed, and we'd need to unbox it to get the original type in order
                    // to increment. The only way to do that is to create a new temporary.
                    object value = o.Value;
                    tempForIncrement = Expression.Variable(value != null ? value.GetType() : typeof(object), "t0");
                    parameters[0] = tempForIncrement;
                }
                else
                {
                    parameters[index] = o.Expression;
                }
 
                BindingRestrictions r = DeduceArgumentRestriction(index, callPayload, o, info);
                restrictions = restrictions.Merge(r);
 
                // Here we check the argument info. If the argument info shows that the current argument
                // is a literal constant, then we also add an instance restriction on the value of
                // the constant.
                if (info != null && info.LiteralConstant)
                {
                    if (o.Value is double && double.IsNaN((double)o.Value))
                    {
                        MethodInfo isNaN = s_DoubleIsNaN ??= typeof(double).GetMethod("IsNaN");
                        Expression e = Expression.Call(null, isNaN, o.Expression);
                        restrictions = restrictions.Merge(BindingRestrictions.GetExpressionRestriction(e));
                    }
                    else if (o.Value is float && float.IsNaN((float)o.Value))
                    {
                        MethodInfo isNaN = s_SingleIsNaN ??= typeof(float).GetMethod("IsNaN");
                        Expression e = Expression.Call(null, isNaN, o.Expression);
                        restrictions = restrictions.Merge(BindingRestrictions.GetExpressionRestriction(e));
                    }
                    else
                    {
                        Expression e = Expression.Equal(o.Expression, Expression.Constant(o.Value, o.Expression.Type));
                        r = BindingRestrictions.GetExpressionRestriction(e);
                        restrictions = restrictions.Merge(r);
                    }
                }
            }
 
            // Get the bound expression.
            try
            {
                Expression expression = binder.Bind(action, parameters, args, out DynamicMetaObject deferredBinding);
 
                if (deferredBinding != null)
                {
                    expression = ConvertResult(deferredBinding.Expression, action);
                    restrictions = deferredBinding.Restrictions.Merge(restrictions);
                    return new DynamicMetaObject(expression, restrictions);
                }
 
                if (tempForIncrement != null)
                {
                    // If we have a ++ or -- payload, we need to do some temp rewriting.
                    // We rewrite to the following:
                    //
                    // temp = (type)o;
                    // temp++;
                    // o = temp;
                    // return o;
 
                    DynamicMetaObject arg0 = args[0];
 
                    expression = Expression.Block(
                        new[] { tempForIncrement },
                        Expression.Assign(tempForIncrement, Expression.Convert(arg0.Expression, arg0.Value.GetType())),
                        expression,
                        Expression.Assign(arg0.Expression, Expression.Convert(tempForIncrement, arg0.Expression.Type)));
                }
 
                expression = ConvertResult(expression, action);
 
                return new DynamicMetaObject(expression, restrictions);
            }
            catch (RuntimeBinderException e)
            {
                if (onBindingError != null)
                {
                    return onBindingError;
                }
 
                return new DynamicMetaObject(
                    Expression.Throw(
                        Expression.New(
                            typeof(RuntimeBinderException).GetConstructor(new Type[] { typeof(string) }),
                            Expression.Constant(e.Message)
                        ),
                        GetTypeForErrorMetaObject(action, args)
                    ),
                    restrictions
                );
            }
        }
 
        public static void ValidateBindArgument(DynamicMetaObject argument, string paramName)
        {
            ArgumentNullException.ThrowIfNull(argument, paramName);
            if (!argument.HasValue)
            {
                throw Error.DynamicArgumentNeedsValue(paramName);
            }
        }
 
        public static void ValidateBindArgument(DynamicMetaObject[] arguments, string paramName)
        {
            if (arguments != null) // null is treated as empty, so not invalid
            {
                for (int i = 0; i < arguments.Length; ++i)
                {
                    ValidateBindArgument(arguments[i], $"{paramName}[{i}]");
                }
            }
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static bool IsTypeOfStaticCall(
            int parameterIndex,
            ICSharpInvokeOrInvokeMemberBinder callPayload)
        {
            return parameterIndex == 0 && callPayload != null && callPayload.StaticCall;
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static bool IsComObject(object obj)
        {
            return obj != null && Marshal.IsComObject(obj);
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static bool IsDynamicallyTypedRuntimeProxy(DynamicMetaObject argument, CSharpArgumentInfo info)
        {
            // This detects situations where, although the argument has a value with
            // a given type, that type is insufficient to determine, statically, the
            // set of reference conversions that are going to exist at bind time for
            // different values. For instance, one __ComObject may allow a conversion
            // to IFoo while another does not.
 
            bool isDynamicObject =
                info != null &&
                !info.UseCompileTimeType &&
                IsComObject(argument.Value);
 
            return isDynamicObject;
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static BindingRestrictions DeduceArgumentRestriction(
            int parameterIndex,
            ICSharpInvokeOrInvokeMemberBinder callPayload,
            DynamicMetaObject argument,
            CSharpArgumentInfo info)
        {
            // Here we deduce what predicates the DLR can apply to future calls in order to
            // determine whether to use the previously-computed-and-cached delegate, or
            // whether we need to bind the site again. Ideally we would like the
            // predicate to be as broad as is possible; if we can re-use analysis based
            // solely on the type of the argument, that is preferable to re-using analysis
            // based on object identity with a previously-analyzed argument.
 
            // The times when we need to restrict re-use to a particular instance, rather
            // than its type, are:
            //
            // * if the argument is a null reference then we have no type information.
            //
            // * if we are making a static call then the first argument is
            //   going to be a Type object. In this scenario we should always check
            //   for a specific Type object rather than restricting to the Type type.
            //
            // * if the argument was dynamic at compile time and it is a dynamic proxy
            //   object that the runtime manages, such as COM RCWs and transparent
            //   proxies.
            //
            // ** there is also a case for constant values (such as literals) to use
            //    something like value restrictions, and that is accomplished in Bind().
 
            bool useValueRestriction =
                argument.Value == null ||
                IsTypeOfStaticCall(parameterIndex, callPayload) ||
                IsDynamicallyTypedRuntimeProxy(argument, info);
 
            return useValueRestriction ?
                    BindingRestrictions.GetInstanceRestriction(argument.Expression, argument.Value) :
                    BindingRestrictions.GetTypeRestriction(argument.Expression, argument.RuntimeType);
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static Expression ConvertResult(Expression binding, ICSharpBinder action)
        {
            // Need to handle the following cases:
            //   (1) Call to a constructor: no conversions.
            //   (2) Call to a void-returning method: return null iff result is discarded.
            //   (3) Call to a value-type returning method: box to object.
            //
            // In all other cases, binding.Type should be equivalent or
            // reference assignable to resultType.
 
            // No conversions needed for , the call site has the correct type.
            if (action is CSharpInvokeConstructorBinder)
            {
                return binding;
            }
 
            if (binding.Type == typeof(void))
            {
                if (action is ICSharpInvokeOrInvokeMemberBinder invoke && invoke.ResultDiscarded)
                {
                    Debug.Assert(action.ReturnType == typeof(object));
                    return Expression.Block(binding, Expression.Default(action.ReturnType));
                }
 
                throw Error.BindToVoidMethodButExpectResult();
            }
 
            if (binding.Type.IsValueType && !action.ReturnType.IsValueType)
            {
                Debug.Assert(action.ReturnType == typeof(object));
                return Expression.Convert(binding, action.ReturnType);
            }
 
            return binding;
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static Type GetTypeForErrorMetaObject(ICSharpBinder action, DynamicMetaObject[] args)
        {
            // This is similar to ConvertResult but has fewer things to worry about.
 
            if (action is CSharpInvokeConstructorBinder)
            {
                Debug.Assert(args != null);
                Debug.Assert(args.Length != 0);
                Debug.Assert(args[0].Value is Type);
                return args[0].Value as Type;
            }
 
            return action.ReturnType;
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        private static bool IsIncrementOrDecrementActionOnLocal(ICSharpBinder action) =>
            action is CSharpUnaryOperationBinder operatorPayload
            && (operatorPayload.Operation == ExpressionType.Increment || operatorPayload.Operation == ExpressionType.Decrement);
 
        /////////////////////////////////////////////////////////////////////////////////
 
        internal static T[] Cons<T>(T sourceHead, T[] sourceTail)
        {
            if (sourceTail?.Length != 0)
            {
                T[] array = new T[sourceTail.Length + 1];
                array[0] = sourceHead;
                sourceTail.CopyTo(array, 1);
                return array;
            }
 
            return new[] { sourceHead };
        }
 
        internal static T[] Cons<T>(T sourceHead, T[] sourceMiddle, T sourceLast)
        {
            if (sourceMiddle?.Length != 0)
            {
                T[] array = new T[sourceMiddle.Length + 2];
                array[0] = sourceHead;
                array[array.Length - 1] = sourceLast;
                sourceMiddle.CopyTo(array, 1);
                return array;
            }
 
            return new[] { sourceHead, sourceLast };
        }
 
        /////////////////////////////////////////////////////////////////////////////////
 
        internal static T[] ToArray<T>(IEnumerable<T> source) => source == null ? Array.Empty<T>() : source.ToArray();
 
        /////////////////////////////////////////////////////////////////////////////////
 
        internal static CallInfo CreateCallInfo(ref IEnumerable<CSharpArgumentInfo> argInfos, int discard)
        {
            // This function converts the C# Binder's notion of argument information to the
            // DLR's notion. The DLR counts arguments differently than C#. Here are some
            // examples:
 
            // Expression      Binder                     C# ArgInfos   DLR CallInfo
            //
            // d.M(1, 2, 3);   CSharpInvokeMemberBinder   4             3
            // d(1, 2, 3);     CSharpInvokeBinder         4             3
            // d[1, 2] = 3;    CSharpSetIndexBinder       4             2
            // d[1, 2, 3]      CSharpGetIndexBinder       4             3
            //
            // The "discard" parameter tells this function how many of the C# arg infos it
            // should not count as DLR arguments.
 
            int argCount = 0;
            List<string> argNames = new List<string>();
            CSharpArgumentInfo[] infoArray = ToArray(argInfos);
            argInfos = infoArray; // Write back the array to allow single enumeration.
            foreach (CSharpArgumentInfo info in infoArray)
            {
                if (info.NamedArgument)
                {
                    argNames.Add(info.Name);
                }
                ++argCount;
            }
 
            Debug.Assert(discard <= argCount);
            Debug.Assert(argNames.Count <= argCount - discard);
 
            return new CallInfo(argCount - discard, argNames);
        }
 
        internal static string GetCLROperatorName(this ExpressionType p) =>
            p switch
            {
 
                // Binary Operators
                ExpressionType.Add => SpecialNames.CLR_Add,
                ExpressionType.Subtract => SpecialNames.CLR_Subtract,
                ExpressionType.Multiply => SpecialNames.CLR_Multiply,
                ExpressionType.Divide => SpecialNames.CLR_Division,
                ExpressionType.Modulo => SpecialNames.CLR_Modulus,
                ExpressionType.LeftShift => SpecialNames.CLR_LShift,
                ExpressionType.RightShift => SpecialNames.CLR_RShift,
                ExpressionType.LessThan => SpecialNames.CLR_LT,
                ExpressionType.GreaterThan => SpecialNames.CLR_GT,
                ExpressionType.LessThanOrEqual => SpecialNames.CLR_LTE,
                ExpressionType.GreaterThanOrEqual => SpecialNames.CLR_GTE,
                ExpressionType.Equal => SpecialNames.CLR_Equality,
                ExpressionType.NotEqual => SpecialNames.CLR_Inequality,
                ExpressionType.And => SpecialNames.CLR_BitwiseAnd,
                ExpressionType.ExclusiveOr => SpecialNames.CLR_ExclusiveOr,
                ExpressionType.Or => SpecialNames.CLR_BitwiseOr,
 
                // "op_LogicalNot";
                ExpressionType.AddAssign => SpecialNames.CLR_InPlaceAdd,
                ExpressionType.SubtractAssign => SpecialNames.CLR_InPlaceSubtract,
                ExpressionType.MultiplyAssign => SpecialNames.CLR_InPlaceMultiply,
                ExpressionType.DivideAssign => SpecialNames.CLR_InPlaceDivide,
                ExpressionType.ModuloAssign => SpecialNames.CLR_InPlaceModulus,
                ExpressionType.AndAssign => SpecialNames.CLR_InPlaceBitwiseAnd,
                ExpressionType.ExclusiveOrAssign => SpecialNames.CLR_InPlaceExclusiveOr,
                ExpressionType.OrAssign => SpecialNames.CLR_InPlaceBitwiseOr,
                ExpressionType.LeftShiftAssign => SpecialNames.CLR_InPlaceLShift,
                ExpressionType.RightShiftAssign => SpecialNames.CLR_InPlaceRShift,
 
                // Unary Operators
                ExpressionType.Negate => SpecialNames.CLR_UnaryNegation,
                ExpressionType.UnaryPlus => SpecialNames.CLR_UnaryPlus,
                ExpressionType.Not => SpecialNames.CLR_LogicalNot,
                ExpressionType.OnesComplement => SpecialNames.CLR_OnesComplement,
                ExpressionType.IsTrue => SpecialNames.CLR_True,
                ExpressionType.IsFalse => SpecialNames.CLR_False,
 
                ExpressionType.Increment => SpecialNames.CLR_Increment,
                ExpressionType.Decrement => SpecialNames.CLR_Decrement,
 
                _ => null,
            };
 
        internal static int AddArgHashes(int hash, Type[] typeArguments, CSharpArgumentInfo[] argInfos)
        {
            foreach (var typeArg in typeArguments)
            {
                hash = HashHelpers.Combine(hash, typeArg.GetHashCode());
            }
 
            return AddArgHashes(hash, argInfos);
        }
 
        internal static int AddArgHashes(int hash, CSharpArgumentInfo[] argInfos)
        {
            foreach (var argInfo in argInfos)
            {
                hash = HashHelpers.Combine(hash, (int)argInfo.Flags);
                var argName = argInfo.Name;
                if (!string.IsNullOrEmpty(argName))
                {
                    hash = HashHelpers.Combine(hash, argName.GetHashCode());
                }
            }
 
            return hash;
        }
 
        internal static bool CompareArgInfos(Type[] typeArgs, Type[] otherTypeArgs, CSharpArgumentInfo[] argInfos, CSharpArgumentInfo[] otherArgInfos)
        {
            for (int i = 0; i < typeArgs.Length; i++)
            {
                if (typeArgs[i] != otherTypeArgs[i])
                {
                    return false;
                }
            }
 
            return CompareArgInfos(argInfos, otherArgInfos);
        }
 
        internal static bool CompareArgInfos(CSharpArgumentInfo[] argInfos, CSharpArgumentInfo[] otherArgInfos)
        {
            for (int i = 0; i < argInfos.Length; i++)
            {
                var argInfo = argInfos[i];
                var otherArgInfo = otherArgInfos[i];
 
                if (argInfo.Flags != otherArgInfo.Flags ||
                    argInfo.Name != otherArgInfo.Name)
                {
                    return false;
                }
            }
 
            return true;
        }
 
#if !ENABLECOMBINDER
        internal static void ThrowIfUsingDynamicCom(DynamicMetaObject target)
        {
            if (target.LimitType.IsCOMObject)
            {
                throw ErrorHandling.Error(ErrorCode.ERR_DynamicBindingComUnsupported);
            }
        }
#endif
    }
}