protected static PrimitiveExpression BinaryOp(
            int index, int length, PrimitiveExpression leftOperand, PrimitiveExpression rightOperand,
            Func <long, long, long> longOperation, Func <BigInteger, BigInteger, BigInteger> bigOperation,
            Func <decimal, decimal, decimal> decimalOperation
            )
        {
            Debug.Assert(leftOperand.Type == rightOperand.Type);

            if (leftOperand.Type == PrimitiveType.IntegerLong)
            {
                // try natively long
                try
                {
                    return(new PrimitiveExpression(
                               index, length, longOperation(leftOperand.LongValue, rightOperand.LongValue)
                               ));
                }
                catch (OverflowException)
                {
                }

                // try promoting to BigInteger
                try
                {
                    return(new PrimitiveExpression(
                               index, length, bigOperation(leftOperand.LongValue, rightOperand.LongValue)
                               ));
                }
                catch (OverflowException)
                {
                }

                // last attempt: decimal
                return(new PrimitiveExpression(
                           index, length, decimalOperation(leftOperand.LongValue, rightOperand.LongValue)
                           ));
            }
            else if (leftOperand.Type == PrimitiveType.IntegerBig)
            {
                // try natively BigInteger
                try
                {
                    return(new PrimitiveExpression(
                               index, length, bigOperation(leftOperand.BigIntegerValue, rightOperand.BigIntegerValue)
                               ));
                }
                catch (OverflowException)
                {
                }

                // last attempt: decimal
                return(new PrimitiveExpression(
                           index, length,
                           decimalOperation(
                               checked ((decimal)leftOperand.BigIntegerValue),
                               checked ((decimal)rightOperand.BigIntegerValue)
                               )
                           ));
            }
            else if (leftOperand.Type == PrimitiveType.Decimal)
            {
                // just go decimal
                return(new PrimitiveExpression(
                           index, length,
                           decimalOperation(leftOperand.DecimalValue, rightOperand.DecimalValue)
                           ));
            }
            else
            {
                Debug.Fail($"Unexpected primitive expression type '{leftOperand.Type}'.");
                return(new PrimitiveExpression(index, length, (long)0));
            }
        }
        public override PrimitiveExpression Simplified(Grimoire grimoire, CalcTimer timer)
        {
            timer.ThrowIfTimedOut();

            PrimitiveExpression primLeft = LeftSide.Simplified(grimoire, timer);

            timer.ThrowIfTimedOut();
            PrimitiveExpression primRight = RightSide.Simplified(grimoire, timer);

            timer.ThrowIfTimedOut();

            // type check
            switch (Operation)
            {
            case Operation.BinaryAnd:
            case Operation.BinaryOr:
            case Operation.BinaryXor:
                if (primLeft.Type == PrimitiveType.Decimal || primRight.Type == PrimitiveType.Decimal)
                {
                    throw new SimplificationException(
                              "Cannot perform a bitwise operation on a floating-point number.",
                              this
                              );
                }
                break;

            default:
                break;
            }

            try
            {
                // mixed types? coerce
                if (primLeft.Type == PrimitiveType.IntegerLong)
                {
                    if (primRight.Type == PrimitiveType.IntegerBig)
                    {
                        // IntegerLong < IntegerBig
                        primLeft = new PrimitiveExpression(primLeft.Index, primLeft.Length, new BigInteger(primLeft.LongValue));
                    }
                    else if (primRight.Type == PrimitiveType.Decimal)
                    {
                        // IntegerLong < Decimal
                        primLeft = new PrimitiveExpression(primLeft.Index, primLeft.Length, (decimal)primLeft.LongValue);
                    }
                }
                else if (primLeft.Type == PrimitiveType.IntegerBig)
                {
                    if (primRight.Type == PrimitiveType.IntegerLong)
                    {
                        // IntegerBig > IntegerLong
                        primRight = new PrimitiveExpression(primRight.Index, primRight.Length, new BigInteger(primRight.LongValue));
                    }
                    else if (primRight.Type == PrimitiveType.Decimal)
                    {
                        // IntegerBig < Decimal
                        primLeft = new PrimitiveExpression(primLeft.Index, primLeft.Length, (decimal)primLeft.BigIntegerValue);
                    }
                }
                else if (primLeft.Type == PrimitiveType.Decimal)
                {
                    if (primRight.Type == PrimitiveType.IntegerLong)
                    {
                        // Decimal > IntegerLong
                        primRight = new PrimitiveExpression(primRight.Index, primRight.Length, (decimal)primRight.LongValue);
                    }
                    else if (primRight.Type == PrimitiveType.IntegerBig)
                    {
                        // Decimal > IntegerBig
                        primRight = new PrimitiveExpression(primRight.Index, primRight.Length, (decimal)primRight.BigIntegerValue);
                    }
                }
            }
            catch (OverflowException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (DivideByZeroException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (FunctionDomainException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (TimeoutException ex)
            {
                throw new SimplificationException(this, ex);
            }

            timer.ThrowIfTimedOut();

            Debug.Assert(primLeft.Type == primRight.Type);

            int newIndex  = primLeft.Index;
            int newLength = primRight.Index + primRight.Length - primLeft.Index;

            try
            {
                switch (Operation)
                {
                case Operation.Add:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a + b),
                               (a, b) => checked (a + b),
                               (a, b) => checked (a + b)
                               ));

                case Operation.Divide:
                    return(new PrimitiveExpression(
                               newIndex, newLength,
                               checked (primLeft.ToDecimal() / primRight.ToDecimal())
                               ));

                case Operation.IntegralDivide:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a / b),
                               (a, b) => checked (a / b),
                               (a, b) => Math.Truncate(checked (a / b))
                               ));

                case Operation.Multiply:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a * b),
                               (a, b) => checked (a * b),
                               (a, b) => checked (a * b)
                               ));

                case Operation.Power:
                    if (primLeft.Type == PrimitiveType.IntegerLong)
                    {
                        return(Pow(newIndex, newLength, primLeft.LongValue, primRight.LongValue, timer));
                    }
                    else if (primLeft.Type == PrimitiveType.IntegerBig)
                    {
                        return(Pow(newIndex, newLength, primLeft.BigIntegerValue, primRight.BigIntegerValue, timer));
                    }
                    else if (primLeft.Type == PrimitiveType.Decimal)
                    {
                        return(Pow(newIndex, newLength, primLeft.DecimalValue, primRight.DecimalValue, timer));
                    }
                    break;

                case Operation.Remainder:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a % b),
                               (a, b) => checked (a % b),
                               (a, b) => checked (a % b)
                               ));

                case Operation.Subtract:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a - b),
                               (a, b) => checked (a - b),
                               (a, b) => checked (a - b)
                               ));

                case Operation.BinaryAnd:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a & b),
                               (a, b) => checked (a & b),
                               null
                               ));

                case Operation.BinaryOr:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a | b),
                               (a, b) => checked (a | b),
                               null
                               ));

                case Operation.BinaryXor:
                    return(BinaryOp(
                               newIndex, newLength, primLeft, primRight,
                               (a, b) => checked (a ^ b),
                               (a, b) => checked (a ^ b),
                               null
                               ));
                }
            }
            catch (OverflowException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (DivideByZeroException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (FunctionDomainException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (TimeoutException ex)
            {
                throw new SimplificationException(this, ex);
            }

            throw new SimplificationException($"Cannot handle binary operator {Operation}.", this);
        }
        public override PrimitiveExpression Simplified(Grimoire grimoire, CalcTimer timer)
        {
            timer.ThrowIfTimedOut();

            PrimitiveExpression primOperand = Operand.Simplified(grimoire, timer);

            try
            {
                switch (Operation)
                {
                case Operation.Negate:
                    if (primOperand.Type == PrimitiveType.IntegerLong)
                    {
                        return(new PrimitiveExpression(Index, Length, -primOperand.LongValue));
                    }
                    else if (primOperand.Type == PrimitiveType.IntegerBig)
                    {
                        return(new PrimitiveExpression(Index, Length, -primOperand.BigIntegerValue));
                    }
                    else if (primOperand.Type == PrimitiveType.Decimal)
                    {
                        return(new PrimitiveExpression(Index, Length, -primOperand.DecimalValue));
                    }
                    break;

                case Operation.Factorial:
                    if (primOperand.Type == PrimitiveType.IntegerLong)
                    {
                        return(new PrimitiveExpression(Index, Length, MathFuncs.Factorial(primOperand.LongValue, timer)));
                    }
                    else if (primOperand.Type == PrimitiveType.IntegerBig)
                    {
                        return(new PrimitiveExpression(Index, Length, MathFuncs.Factorial(primOperand.BigIntegerValue, timer)));
                    }
                    else if (primOperand.Type == PrimitiveType.Decimal)
                    {
                        throw new FunctionDomainException("Factorials are not defined on fractional numbers.");
                    }
                    break;

                default:
                    break;
                }
            }
            catch (OverflowException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (DivideByZeroException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (FunctionDomainException ex)
            {
                throw new SimplificationException(this, ex);
            }
            catch (TimeoutException ex)
            {
                throw new SimplificationException(this, ex);
            }

            throw new SimplificationException($"Cannot handle unary operator {Operation}.", this);
        }