/// <summary>
        /// Reduces the expression node to a simpler expression.
        /// </summary>
        /// <returns>The reduced expression.</returns>
        public override Expression Reduce()
        {
            var operand = Operand;

            var temps = new List<ParameterExpression>();
            var stmts = new List<Expression>();


            //
            // NB: We can't check IsPure with the readOnly parameter set to true, because the user conversions may assign to a variable.
            //     To mitigate this, we'd have to check all element conversions to make sure they don't assign to the variable, which is
            //     very unlikely, though a hand-written conversion lambda could have such a side-effect. One could argue that a side-
            //     effecting conversion that mutates the parent tuple yields undefined behavior.
            //

            if (!Helpers.IsPure(Operand))
            {
                var operandVariable = Expression.Parameter(Operand.Type, "__t");
                
                temps.Add(operandVariable);
                stmts.Add(Expression.Assign(operandVariable, Operand));

                operand = operandVariable;
            }

            Expression res;

            if (operand.Type.IsNullableType())
            {
                var nonNullOperand = Helpers.MakeNullableGetValue(operand); // NB: Use of Nullable<T>.Value to ensure proper exception is thrown if null.
                var nonNullOperandVariable = Expression.Parameter(nonNullOperand.Type, "__nonNull");

                var args = GetConversions(nonNullOperandVariable);

                if (Type.IsNullableType())
                {
                    //
                    // T? -> U?
                    //
                    // var t = operand;
                    //
                    // if (t.HasValue)
                    // {
                    //    var v = t.Value;
                    //    return (U?)new U(...);
                    // }
                    // else
                    // {
                    //    return default(U?);
                    // }
                    //

                    var hasValueTest = Helpers.MakeNullableHasValue(operand);

                    var nullValue = Expression.Default(Type);

                    var convertedTuple = Expression.Convert(CSharpExpression.TupleLiteral(Type.GetNonNullableType(), args, argumentNames: null), Type);
                    var nonNullValue = Expression.Block(new[] { nonNullOperandVariable }, Expression.Assign(nonNullOperandVariable, nonNullOperand), convertedTuple);

                    res = Expression.Condition(hasValueTest, nonNullValue, nullValue);
                }
                else
                {
                    //
                    // T? -> U
                    //
                    // var t = operand;
                    // var v = operand.Value; // NB: May throw
                    // return new U(...);

                    temps.Add(nonNullOperandVariable);
                    stmts.Add(Expression.Assign(nonNullOperandVariable, nonNullOperand));

                    res = CSharpExpression.TupleLiteral(Type, args, argumentNames: null);
                }
            }
            else
            {
                var args = GetConversions(operand);

                var targetType = Type.GetNonNullableType();

                var convertedTuple = CSharpExpression.TupleLiteral(targetType, args, argumentNames: null);

                if (Type.IsNullableType())
                {
                    //
                    // T -> U?
                    //
                    // var t = operand;
                    // return (U?)new U(...);

                    res = Expression.Convert(convertedTuple, Type);
                }
                else
                {
                    //
                    // T -> U
                    //
                    // var t = operand;
                    // return new U(...);
                    //

                    res = convertedTuple;
                }
            }

            stmts.Add(res);

            return Helpers.Comma(temps, stmts);

            List<Expression> GetConversions(Expression operand)
            {
                var n = ElementConversions.Count;

                var args = new List<Expression>(n);

                for (int i = 0; i < n; i++)
                {
                    var conversion = ElementConversions[i];

                    var item = Helpers.GetTupleItemAccess(operand, i);

                    var conversionParameter = conversion.Parameters[0];

                    if (conversion.Body is UnaryExpression { Operand: var unaryOperand } unary && unaryOperand == conversionParameter && IsConvert(unary.NodeType))
                    {
                        args.Add(unary.Update(item));
                    }
Example #2
0
        /// <summary>
        /// Creates a recursive pattern that can perform positional matching and/or matching on properties.
        /// </summary>
        /// <param name="info">Type information about the pattern.</param>
        /// <param name="type">The type to check for.</param>
        /// <param name="deconstructMethod">The method used to deconstruct an object into its components for use with positional subpatterns.</param>
        /// <param name="deconstruction">The positional subpatterns to apply to the components of the object.</param>
        /// <param name="properties">The property subpatterns to apply.</param>
        /// <returns>A <see cref="RecursiveCSharpPattern" /> representing a recursive pattern.</returns>
        public static RecursiveCSharpPattern Recursive(CSharpObjectPatternInfo info, Type type, MethodInfo deconstructMethod, IEnumerable <PositionalCSharpSubpattern> deconstruction, IEnumerable <PropertyCSharpSubpattern> properties)
        {
            if (info == null)
            {
                // NB: We could arguably attempt to infer the type from the other parameters but that's a bit too implicit.

                RequiresNotNull(type, nameof(type));

                info = ObjectPatternInfo(PatternInfo(typeof(object), type), variable: null);
            }
            else
            {
                if (info.Variable != null)
                {
                    RequiresCompatiblePatternTypes(info.Info.NarrowedType, info.Variable.Type);
                }
            }

            var narrowedType             = info.Info.NarrowedType;
            var deconstructionCollection = deconstruction.ToReadOnly();
            var propertiesCollection     = properties.ToReadOnly();

            var objParam = Expression.Parameter(narrowedType); // NB: Utility to utilize some expression factories to expedite some checks.

            // REVIEW: Reordering of positional matches with names (for tuple elements or deconstruction out parameters) is not supported.

            validateType();
            validatePositional();
            validateProperties();

            return(new RecursiveCSharpPattern(info, type, deconstructMethod, deconstructionCollection, propertiesCollection));

            void validateType()
            {
                if (type != null)
                {
                    ValidatePatternType(type);

                    var variableType = info.Variable?.Type;

                    if (variableType != null)
                    {
                        RequiresCompatiblePatternTypes(type, variableType);

                        if (variableType != narrowedType)
                        {
                            throw Error.CannotAssignPatternResultToVariable(variableType, narrowedType);
                        }
                    }
                }
            }

            void validatePositional()
            {
                if (deconstructionCollection.Count > 0)
                {
                    foreach (var positionalPattern in deconstructionCollection)
                    {
                        RequiresNotNull(positionalPattern, nameof(deconstruction));
                    }

                    if (deconstructMethod != null)
                    {
                        validateWithDeconstructMethod();
                    }
                    else if (IsTupleType(narrowedType))
                    {
                        validateWithTupleType();
                    }
                    else
                    {
                        throw Error.InvalidPositionalPattern();
                    }

                    void validateWithDeconstructMethod()
                    {
                        ValidateMethodInfo(deconstructMethod);

                        if (deconstructMethod.ReturnType != typeof(void))
                        {
                            throw Error.DeconstructShouldReturnVoid(deconstructMethod);
                        }

                        var parameters = deconstructMethod.GetParametersCached();

                        if (deconstructMethod.IsStatic)
                        {
                            if (parameters.Length == 0)
                            {
                                throw Error.DeconstructExtensionMethodMissingThis(deconstructMethod);
                            }

                            ValidateOneArgument(deconstructMethod, ExpressionType.Call, objParam, parameters[0]);

                            parameters = parameters.RemoveFirst();
                        }
                        else
                        {
                            ValidateCallInstanceType(narrowedType, deconstructMethod);
                        }

                        var arity = parameters.Length;

                        checkArity(arity);

                        var parameterToPattern = new Dictionary <ParameterInfo, PositionalCSharpSubpattern>();

                        foreach (var positionalPattern in deconstructionCollection)
                        {
                            if (positionalPattern.Field != null)
                            {
                                throw Error.PositionalPatternWithDeconstructMethodCannotSpecifyField();
                            }

                            var parameter = positionalPattern.Parameter;

                            if (parameter != null)
                            {
                                if (parameter.Member != deconstructMethod)
                                {
                                    throw Error.PositionalPatternParameterIsNotDeclaredOnDeconstructMethod(parameter, deconstructMethod);
                                }

                                if (parameterToPattern.ContainsKey(parameter))
                                {
                                    throw Error.PositionalPatternParameterShouldOnlyBeUsedOnce(parameter);
                                }

                                parameterToPattern.Add(parameter, positionalPattern);
                            }
                        }

                        var bindByParameter = parameterToPattern.Count > 0;

                        if (bindByParameter && parameterToPattern.Count != arity)
                        {
                            throw Error.PositionalPatternWithDeconstructMethodShouldSpecifyAllParameters();
                        }

                        PositionalCSharpSubpattern getPositionalPattern(ParameterInfo parameter, int index) => bindByParameter
                            ? parameterToPattern[parameter]
                            : deconstructionCollection[index];

                        for (var i = 0; i < arity; i++)
                        {
                            var parameter = parameters[i];

                            if (!parameter.IsOut)
                            {
                                throw Error.DeconstructParameterShouldBeOut(parameter, deconstructMethod);
                            }

                            var pattern       = getPositionalPattern(parameter, i).Pattern;
                            var parameterType = parameter.ParameterType.GetElementType();

                            // REVIEW: Can we loosen the type checking here (assignment compatibility) or trigger ChangeType?
                            RequiresCompatiblePatternTypes(pattern.InputType, parameterType);
                        }
                    }

                    void validateWithTupleType()
                    {
                        var arity = GetTupleArity(narrowedType);

                        checkArity(arity);

                        var byIndexCount   = 0;
                        var indexToPattern = new PositionalCSharpSubpattern[arity];

                        foreach (var positionalPattern in deconstructionCollection)
                        {
                            if (positionalPattern.Parameter != null)
                            {
                                throw Error.PositionalPatternWithTupleCannotSpecifyParameter();
                            }

                            if (positionalPattern.Field != null)
                            {
                                var index = positionalPattern.Field.Index;

                                if (index < 0 || index >= arity)
                                {
                                    throw Error.PositionalPatternTupleIndexOutOfRange(index, arity);
                                }

                                if (indexToPattern[index] != null)
                                {
                                    throw Error.PositionalPatternTupleIndexShouldOnlyBeUsedOnce(index);
                                }

                                indexToPattern[index] = positionalPattern;
                                byIndexCount++;
                            }
                        }

                        var bindByIndex = byIndexCount > 0;

                        if (bindByIndex && byIndexCount != arity)
                        {
                            throw Error.PositionalPatternWithTupleShouldSpecifyAllIndices();
                        }

                        PositionalCSharpSubpattern getPositionalPattern(int index) => bindByIndex
                            ? indexToPattern[index]
                            : deconstructionCollection[index];

                        var elementTypes = GetTupleComponentTypes(narrowedType).ToReadOnly();

                        for (var i = 0; i < arity; i++)
                        {
                            var elementType = elementTypes[i];

                            var pattern = getPositionalPattern(i).Pattern;

                            // REVIEW: Can we loosen the type checking here (assignment compatibility) or trigger ChangeType?
                            RequiresCompatiblePatternTypes(pattern.InputType, elementType);
                        }
                    }

                    void checkArity(int arity)
                    {
                        if (arity != deconstructionCollection.Count)
                        {
                            throw Error.InvalidPositionalPatternCount(narrowedType);
                        }
                    }
                }
            }

            void validateProperties()
            {
                foreach (var propertyPattern in propertiesCollection)
                {
                    RequiresNotNull(propertyPattern, nameof(properties));

                    var member = findTopMostMember(propertyPattern.Member);

                    if (member.Member != null)
                    {
                        // TODO: Inline the check.
                        _ = Expression.MakeMemberAccess(objParam, member.Member);
                    }
                    else
                    {
                        // TODO: Inline the check.
                        _ = Helpers.GetTupleItemAccess(objParam, member.TupleField.Index);
                    }
                }