#     Copyright 2019, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#
""" Expression base classes.

These classes provide the generic base classes available for
expressions. They have a richer interface, mostly related to
abstract execution, and different from statements.

"""

from abc import abstractmethod

from nuitka.__past__ import long  # pylint: disable=I0021,redefined-builtin
from nuitka.Constants import isCompileTimeConstantValue
from nuitka.Options import isFullCompat
from nuitka.PythonVersions import python_version

from .NodeBases import ChildrenHavingMixin, NodeBase
from .NodeMakingHelpers import (
    getComputationResult,
    makeConstantReplacementNode,
    makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue,
    wrapExpressionWithNodeSideEffects,
    wrapExpressionWithSideEffects,
)
from .shapes.BuiltinTypeShapes import ShapeTypeDict, ShapeTypeStr, ShapeTypeUnicode
from .shapes.StandardShapes import ShapeUnknown


class ExpressionBase(NodeBase):
    # TODO: Maybe we can do this only for debug mode.
    __slots__ = ("code_generated",)

    def getTypeShape(self):
        # Virtual method, pylint: disable=no-self-use
        return ShapeUnknown

    def getValueShape(self):
        return self

    def isCompileTimeConstant(self):
        """ Has a value that we can use at compile time.

            Yes or no. If it has such a value, simulations can be applied at
            compile time and e.g. operations or conditions, or even calls may
            be executed against it.
        """
        # Virtual method, pylint: disable=no-self-use
        return False

    def getTruthValue(self):
        """ Return known truth value. The "None" value indicates unknown. """

        if self.isCompileTimeConstant():
            return bool(self.getCompileTimeConstant())
        else:
            return None

    def isKnownToBeIterable(self, count):
        """ Can be iterated at all (count is None) or exactly count times.

            Yes or no. If it can be iterated a known number of times, it may
            be asked to unpack itself.
        """

        # Virtual method, pylint: disable=no-self-use,unused-argument
        return False

    def isKnownToBeIterableAtMin(self, count):
        # Virtual method, pylint: disable=no-self-use,unused-argument
        return False

    def isKnownToBeIterableAtMax(self, count):
        # Virtual method, pylint: disable=no-self-use,unused-argument
        return False

    def getIterationLength(self):
        """ Value that "len" or "PyObject_Size" would give, if known.

            Otherwise it is "None" to indicate unknown.
        """

        # Virtual method, pylint: disable=no-self-use
        return None

    def getIterationMinLength(self):
        """ Value that "len" or "PyObject_Size" would give at minimum, if known.

            Otherwise it is "None" to indicate unknown.
        """

        return self.getIterationLength()

    def getIterationMaxLength(self):
        """ Value that "len" or "PyObject_Size" would give at maximum, if known.

            Otherwise it is "None" to indicate unknown.
        """

        return self.getIterationLength()

    def getStringValue(self):
        """ Node as string value, if possible."""
        # Virtual method, pylint: disable=no-self-use
        return None

    def getStrValue(self):
        """ Value that "str" or "PyObject_Str" would give, if known.

            Otherwise it is "None" to indicate unknown. Users must not
            forget to take side effects into account, when replacing a
            node with its string value.
        """
        string_value = self.getStringValue()

        if string_value is not None:
            return makeConstantReplacementNode(node=self, constant=string_value)

        return None

    def getTypeValue(self):
        """ Type of the node.

        """

        from .TypeNodes import ExpressionBuiltinType1

        return ExpressionBuiltinType1(
            value=self.makeClone(), source_ref=self.getSourceReference()
        )

    def isKnownToBeHashable(self):
        """ Is the value hashable, i.e. suitable for dictionary/set keying."""

        # Virtual method, pylint: disable=no-self-use
        # Unknown by default.
        return None

    def extractUnhashableNode(self):
        # Virtual method, pylint: disable=no-self-use
        # Not available by default.
        return None

    def onRelease(self, trace_collection):
        # print "onRelease", self
        pass

    def isKnownToHaveAttribute(self, attribute_name):
        # Virtual method, pylint: disable=no-self-use,unused-argument
        return None

    @abstractmethod
    def computeExpressionRaw(self, trace_collection):
        """ Replace this node with computation result. """

    def computeExpressionAttribute(self, lookup_node, attribute_name, trace_collection):
        # By default, an attribute lookup may change everything about the lookup
        # source.
        trace_collection.removeKnowledge(self)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        if not self.isKnownToHaveAttribute(attribute_name):
            trace_collection.onExceptionRaiseExit(BaseException)

        return lookup_node, None, None

    def computeExpressionAttributeSpecial(
        self, lookup_node, attribute_name, trace_collection
    ):
        # By default, an attribute lookup may change everything about the lookup
        # source. Virtual method, pylint: disable=unused-argument
        trace_collection.removeKnowledge(lookup_node)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        trace_collection.onExceptionRaiseExit(BaseException)

        return lookup_node, None, None

    def computeExpressionImportName(self, import_node, import_name, trace_collection):
        if self.mayRaiseExceptionImportName(BaseException, import_name):
            trace_collection.onExceptionRaiseExit(BaseException)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        return import_node, None, None

    def computeExpressionSetAttribute(
        self, set_node, attribute_name, value_node, trace_collection
    ):

        # By default, an attribute lookup may change everything about the lookup
        # source. Virtual method, pylint: disable=unused-argument
        trace_collection.removeKnowledge(self)
        trace_collection.removeKnowledge(value_node)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        trace_collection.onExceptionRaiseExit(BaseException)

        # Better mechanics?
        return set_node, None, None

    def computeExpressionDelAttribute(self, set_node, attribute_name, trace_collection):

        # By default, an attribute lookup may change everything about the lookup
        # source. Virtual method, pylint: disable=unused-argument
        trace_collection.removeKnowledge(self)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        trace_collection.onExceptionRaiseExit(BaseException)

        # Better mechanics?
        return set_node, None, None

    def computeExpressionSubscript(self, lookup_node, subscript, trace_collection):
        # By default, an subscript can execute any code and change all values
        # that escaped. This is a virtual method that may consider the subscript
        # but generally we don't know what to do. pylint: disable=unused-argument
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return lookup_node, None, None

    def computeExpressionSetSubscript(
        self, set_node, subscript, value_node, trace_collection
    ):
        # By default, an subscript can execute any code and change all values
        # that escaped. This is a virtual method that may consider the subscript
        # but generally we don't know what to do. pylint: disable=unused-argument
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return set_node, None, None

    def computeExpressionDelSubscript(self, del_node, subscript, trace_collection):
        # By default, an subscript can execute any code and change all values
        # that escaped. This is a virtual method that may consider the subscript
        # but generally we don't know what to do. pylint: disable=unused-argument
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return del_node, None, None

    def computeExpressionSlice(self, lookup_node, lower, upper, trace_collection):
        # By default, a slicing may change everything about the lookup source.
        trace_collection.removeKnowledge(self)
        trace_collection.removeKnowledge(lower)
        trace_collection.removeKnowledge(upper)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return lookup_node, None, None

    def computeExpressionSetSlice(
        self, set_node, lower, upper, value_node, trace_collection
    ):
        # By default, an subscript may change everything about the lookup
        # source.
        trace_collection.removeKnowledge(self)
        trace_collection.removeKnowledge(lower)
        trace_collection.removeKnowledge(upper)
        trace_collection.removeKnowledge(value_node)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return set_node, None, None

    def computeExpressionDelSlice(self, set_node, lower, upper, trace_collection):
        # By default, an subscript may change everything about the lookup
        # source.
        trace_collection.removeKnowledge(self)
        trace_collection.removeKnowledge(lower)
        trace_collection.removeKnowledge(upper)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return set_node, None, None

    def computeExpressionCall(self, call_node, call_args, call_kw, trace_collection):
        # The called and the arguments escape for good.
        self.onContentEscapes(trace_collection)
        if call_args is not None:
            call_args.onContentEscapes(trace_collection)
        if call_kw is not None:
            call_kw.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return call_node, None, None

    def computeExpressionLen(self, len_node, trace_collection):
        shape = self.getValueShape()

        has_len = shape.hasShapeSlotLen()

        if has_len is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                template="object of type '%s' has no len()",
                operation="len",
                original_node=len_node,
                value_node=self,
            )
        elif has_len is True:
            iter_length = self.getIterationLength()

            if iter_length is not None:
                from .ConstantRefNodes import makeConstantRefNode

                result = makeConstantRefNode(
                    constant=int(iter_length),  # make sure to downcast long
                    source_ref=len_node.getSourceReference(),
                )

                result = wrapExpressionWithNodeSideEffects(
                    new_node=result, old_node=self
                )

                return (
                    result,
                    "new_constant",
                    "Predicted 'len' result from value shape.",
                )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return len_node, None, None

    def computeExpressionInt(self, int_node, trace_collection):
        shape = self.getTypeShape()

        if shape.hasShapeSlotInt() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                template="int() argument must be a string or a number, not '%s'"
                if python_version < 300
                else "int() argument must be a string, a bytes-like object or a number, not '%s'",
                operation="int",
                original_node=int_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return int_node, None, None

    def computeExpressionLong(self, long_node, trace_collection):
        shape = self.getTypeShape()

        if shape.hasShapeSlotLong() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                template="long() argument must be a string or a number, not '%s'",
                operation="long",
                original_node=long_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return long_node, None, None

    def computeExpressionFloat(self, float_node, trace_collection):
        shape = self.getTypeShape()

        if shape.hasShapeSlotFloat() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                "float() argument must be a string or a number"
                if isFullCompat() and python_version < 300
                else "float() argument must be a string or a number, not '%s'",
                operation="long",
                original_node=float_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return float_node, None, None

    def computeExpressionBytes(self, bytes_node, trace_collection):
        shape = self.getTypeShape()

        if (
            shape.hasShapeSlotBytes() is False
            and shape.hasShapeSlotInt() is False
            and shape.hasShapeSlotIter() is False
        ):
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                "'%s' object is not iterable",
                operation="bytes",
                original_node=bytes_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return bytes_node, None, None

    def computeExpressionComplex(self, complex_node, trace_collection):
        shape = self.getTypeShape()

        if shape.hasShapeSlotComplex() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                "complex() argument must be a string or a number"
                if isFullCompat() and python_version < 300
                else "complex() argument must be a string or a number, not '%s'",
                operation="complex",
                original_node=complex_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return complex_node, None, None

    def computeExpressionIter1(self, iter_node, trace_collection):
        shape = self.getTypeShape()

        if shape.hasShapeSlotIter() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                template="'%s' object is not iterable",
                operation="iter",
                original_node=iter_node,
                value_node=self,
            )

        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return iter_node, None, None

    def computeExpressionNext1(self, next_node, trace_collection):
        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return next_node, None, None

    def computeExpressionAsyncIter(self, iter_node, trace_collection):
        self.onContentEscapes(trace_collection)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(self)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return iter_node, None, None

    def computeExpressionOperationNot(self, not_node, trace_collection):
        # Virtual method, pylint: disable=no-self-use

        # The value of that node escapes and could change its contents.
        trace_collection.removeKnowledge(not_node)

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(not_node)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return not_node, None, None

    def computeExpressionComparisonIn(self, in_node, value_node, trace_collection):
        # Virtual method, pylint: disable=unused-argument

        shape = self.getTypeShape()

        assert shape is not None, self

        if shape.hasShapeSlotContains() is False:
            return makeRaiseTypeErrorExceptionReplacementFromTemplateAndValue(
                template="argument of type '%s' object is not iterable",
                operation="in",
                original_node=in_node,
                value_node=self,
            )

        # Any code could be run, note that.
        trace_collection.onControlFlowEscape(in_node)

        # Any exception may be raised.
        trace_collection.onExceptionRaiseExit(BaseException)

        return in_node, None, None

    def computeExpressionDrop(self, statement, trace_collection):
        if not self.mayHaveSideEffects():
            return (
                None,
                "new_statements",
                lambda: "Removed %s without effect." % self.getDescription(),
            )

        return statement, None, None

    def computeExpressionBool(self, trace_collection):
        if not self.mayRaiseException(BaseException) and self.mayRaiseExceptionBool(
            BaseException
        ):
            trace_collection.onExceptionRaiseExit(BaseException)

    def onContentEscapes(self, trace_collection):
        pass

    def mayRaiseExceptionBool(self, exception_type):
        """ Unless we are told otherwise, everything may raise being checked. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionInt(self, exception_type):
        """ Unless we are told otherwise, everything may raise in __int__. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionLong(self, exception_type):
        """ Unless we are told otherwise, everything may raise in __long__. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionFloat(self, exception_type):
        """ Unless we are told otherwise, everything may raise in __float__. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionBytes(self, exception_type):
        """ Unless we are told otherwise, everything may raise in __bytes__. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionIn(self, exception_type, checked_value):
        """ Unless we are told otherwise, everything may raise being iterated. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionAttributeLookup(self, exception_type, attribute_name):
        """ Unless we are told otherwise, everything may raise for attribute access. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionAttributeLookupSpecial(self, exception_type, attribute_name):
        """ Unless we are told otherwise, everything may raise for attribute access. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionAttributeLookupObject(self, exception_type, attribute):
        """ Unless we are told otherwise, everything may raise for attribute access. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionAttributeCheck(self, exception_type, attribute_name):
        """ Unless we are told otherwise, everything may raise for attribute check. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionAttributeCheckObject(self, exception_type, attribute):
        """ Unless we are told otherwise, everything may raise for attribute check. """
        # Virtual method, pylint: disable=no-self-use,unused-argument

        return True

    def mayRaiseExceptionImportName(self, exception_type, import_name):
        """ Unless we are told otherwise, everything may raise for name import. """
        # Virtual method, pylint: disable=no-self-use,unused-argument
        return True

    def mayHaveSideEffectsBool(self):
        """ Unless we are told otherwise, everything may have a side effect for bool check. """
        # Virtual method, pylint: disable=no-self-use

        return True

    def hasShapeSlotLen(self):
        """ The type shape tells us, if "len" is available.

        """
        return self.getTypeShape().hasShapeSlotLen()

    def hasShapeSlotIter(self):
        """ The type shape tells us, if "iter" is available.

        """
        return self.getTypeShape().hasShapeSlotIter()

    def hasShapeSlotNext(self):
        """ The type shape tells us, if "next" is available.

        """
        return self.getTypeShape().hasShapeSlotNext()

    # TODO: Maybe this is a shape slot thing.
    def isIndexable(self):
        """ Unless we are told otherwise, it's not indexable. """
        # Virtual method, pylint: disable=no-self-use

        return False

    # TODO: The ought to be a type shape check for that too.
    def getIntegerValue(self):
        """ Node as integer value, if possible."""
        # Virtual method, pylint: disable=no-self-use
        return None

    def getIntValue(self):
        """ Value that "int" or "PyNumber_Int" (sp) would give, if known.

            Otherwise it is "None" to indicate unknown. Users must not
            forget to take side effects into account, when replacing a
            node with its string value.
        """
        # Virtual method, pylint: disable=no-self-use
        return None

    def hasShapeDictionaryExact(self):
        """ Does a node have exactly a dictionary shape.

        """

        return self.getTypeShape() is ShapeTypeDict

    def hasShapeStrExact(self):
        """ Does an expression have exactly a string shape.

        """
        return self.getTypeShape() is ShapeTypeStr

    def hasShapeUnicodeExact(self):
        """ Does an expression have exactly a unicode shape.

        """
        return self.getTypeShape() is ShapeTypeUnicode


class CompileTimeConstantExpressionBase(ExpressionBase):
    # Base classes can be abstract, pylint: disable=abstract-method

    # TODO: Do this for all computations, do this in the base class of all
    # nodes.
    __slots__ = ("computed_attribute",)

    def __init__(self, source_ref):
        ExpressionBase.__init__(self, source_ref=source_ref)

        self.computed_attribute = None

    def isCompileTimeConstant(self):
        """ Has a value that we can use at compile time.

            Yes or no. If it has such a value, simulations can be applied at
            compile time and e.g. operations or conditions, or even calls may
            be executed against it.
        """
        return True

    def isMutable(self):
        # Virtual method, pylint: disable=no-self-use
        return False

    def mayHaveSideEffects(self):
        # Virtual method overload
        return False

    def mayHaveSideEffectsBool(self):
        # Virtual method overload
        return False

    def mayRaiseException(self, exception_type):
        # Virtual method overload
        return False

    def mayRaiseExceptionBool(self, exception_type):
        # Virtual method overload
        return False

    def mayRaiseExceptionAttributeLookup(self, exception_type, attribute_name):
        # Virtual method overload

        # We remember it from our computation.
        return not self.computed_attribute

    def mayRaiseExceptionAttributeLookupSpecial(self, exception_type, attribute_name):
        # Virtual method overload

        # We remember it from our computation.
        return not self.computed_attribute

    def mayRaiseExceptionAttributeCheck(self, exception_type, attribute_name):
        # Virtual method overload

        # Checking attributes of compile time constants never raises.
        return False

    def computeExpressionOperationNot(self, not_node, trace_collection):
        return trace_collection.getCompileTimeComputationResult(
            node=not_node,
            computation=lambda: not self.getCompileTimeConstant(),
            description="""\
Compile time constant negation truth value pre-computed.""",
        )

    def computeExpressionLen(self, len_node, trace_collection):
        return trace_collection.getCompileTimeComputationResult(
            node=len_node,
            computation=lambda: len(self.getCompileTimeConstant()),
            description="""\
Compile time constant len value pre-computed.""",
        )

    def computeExpressionInt(self, int_node, trace_collection):
        return trace_collection.getCompileTimeComputationResult(
            node=int_node,
            computation=lambda: int(self.getCompileTimeConstant()),
            description="""\
Compile time constant int value pre-computed.""",
        )

    def computeExpressionLong(self, long_node, trace_collection):
        return trace_collection.getCompileTimeComputationResult(
            node=long_node,
            computation=lambda: long(self.getCompileTimeConstant()),
            description="""\
Compile time constant long value pre-computed.""",
        )

    def computeExpressionFloat(self, float_node, trace_collection):
        return trace_collection.getCompileTimeComputationResult(
            node=float_node,
            computation=lambda: float(self.getCompileTimeConstant()),
            description="""\
Compile time constant float value pre-computed.""",
        )

    def computeExpressionBytes(self, bytes_node, trace_collection):
        constant_value = self.getCompileTimeConstant()

        if type(constant_value) in (int, long):
            if constant_value > 1000:
                return bytes_node, None, None

        return trace_collection.getCompileTimeComputationResult(
            node=bytes_node,
            computation=lambda: bytes(constant_value),
            description="""\
Compile time constant bytes value pre-computed.""",
        )

    def isKnownToHaveAttribute(self, attribute_name):
        if self.computed_attribute is None:
            self.computed_attribute = hasattr(
                self.getCompileTimeConstant(), attribute_name
            )

        return self.computed_attribute

    def computeExpressionAttribute(self, lookup_node, attribute_name, trace_collection):
        value = self.getCompileTimeConstant()

        if self.computed_attribute is None:
            self.computed_attribute = hasattr(value, attribute_name)

        # If it raises, or the attribute itself is a compile time constant,
        # then do execute it.
        if not self.computed_attribute or isCompileTimeConstantValue(
            getattr(value, attribute_name)
        ):

            return trace_collection.getCompileTimeComputationResult(
                node=lookup_node,
                computation=lambda: getattr(value, attribute_name),
                description="Attribute '%s' pre-computed." % (attribute_name),
            )

        return lookup_node, None, None

    def computeExpressionSubscript(self, lookup_node, subscript, trace_collection):
        if subscript.isCompileTimeConstant():
            return trace_collection.getCompileTimeComputationResult(
                node=lookup_node,
                computation=lambda: self.getCompileTimeConstant()[
                    subscript.getCompileTimeConstant()
                ],
                description="Subscript of constant with constant value.",
            )

        # TODO: Look-up of subscript to index may happen.
        # Any code could be run due to that, note that.
        trace_collection.onControlFlowEscape(self)

        trace_collection.onExceptionRaiseExit(BaseException)

        return lookup_node, None, None

    def computeExpressionSlice(self, lookup_node, lower, upper, trace_collection):
        # TODO: Could be happy with predictable index values and not require
        # constants.
        if lower is not None:
            if upper is not None:
                if lower.isCompileTimeConstant() and upper.isCompileTimeConstant():

                    return getComputationResult(
                        node=lookup_node,
                        computation=lambda: self.getCompileTimeConstant()[
                            lower.getCompileTimeConstant() : upper.getCompileTimeConstant()
                        ],
                        description="""\
Slicing of constant with constant indexes.""",
                    )
            else:
                if lower.isCompileTimeConstant():
                    return getComputationResult(
                        node=lookup_node,
                        computation=lambda: self.getCompileTimeConstant()[
                            lower.getCompileTimeConstant() :
                        ],
                        description="""\
Slicing of constant with constant lower index only.""",
                    )
        else:
            if upper is not None:
                if upper.isCompileTimeConstant():
                    return getComputationResult(
                        node=lookup_node,
                        computation=lambda: self.getCompileTimeConstant()[
                            : upper.getCompileTimeConstant()
                        ],
                        description="""\
Slicing of constant with constant upper index only.""",
                    )
            else:
                return getComputationResult(
                    node=lookup_node,
                    computation=lambda: self.getCompileTimeConstant()[:],
                    description="Slicing of constant with no indexes.",
                )

        return lookup_node, None, None

    def computeExpressionComparisonIn(self, in_node, value_node, trace_collection):
        if value_node.isCompileTimeConstant():
            return getComputationResult(
                node=in_node,
                computation=lambda: in_node.getSimulator()(
                    value_node.getCompileTimeConstant(), self.getCompileTimeConstant()
                ),
                description="""\
Predicted '%s' on compiled time constant values."""
                % in_node.comparator,
            )

        # Look-up of __contains__ on compile time constants does mostly nothing.
        trace_collection.onExceptionRaiseExit(BaseException)

        return in_node, None, None

    def computeExpressionBool(self, trace_collection):
        constant = self.getCompileTimeConstant()

        if type(constant) is not bool:
            self.parent.replaceChild(
                self, makeConstantReplacementNode(bool(constant), self)
            )

            trace_collection.signalChange(
                tags="new_constant",
                source_ref=self.source_ref,
                message="Predicted compile time constant true value.",
            )


class ExpressionChildrenHavingBase(ChildrenHavingMixin, ExpressionBase):
    def __init__(self, values, source_ref):
        ExpressionBase.__init__(self, source_ref=source_ref)

        ChildrenHavingMixin.__init__(self, values=values)

    def computeExpressionRaw(self, trace_collection):
        """ Compute an expression.

            Default behavior is to just visit the child expressions first, and
            then the node "computeExpression". For a few cases this needs to
            be overloaded, e.g. conditional expressions.
        """
        # First apply the sub-expressions, as they are evaluated before
        # the actual operation.
        for count, sub_expression in enumerate(self.getVisitableNodes()):
            expression = trace_collection.onExpression(sub_expression)

            if expression.willRaiseException(BaseException):
                sub_expressions = self.getVisitableNodes()

                wrapped_expression = wrapExpressionWithSideEffects(
                    side_effects=sub_expressions[:count],
                    old_node=sub_expression,
                    new_node=expression,
                )

                return (
                    wrapped_expression,
                    "new_raise",
                    lambda: "For '%s' the child expression '%s' will raise."
                    % (self.getChildNameNice(), expression.getChildNameNice()),
                )

        # Then ask ourselves to work on it.
        return self.computeExpression(trace_collection=trace_collection)


class ExpressionChildHavingBase(ExpressionBase):
    checker = None

    def __init__(self, value, source_ref):
        ExpressionBase.__init__(self, source_ref=source_ref)

        assert type(self.named_child) is str and self.named_child

        if self.checker is not None:
            value = self.checker(value)  # False alarm, pylint: disable=not-callable

        assert type(value) is not list, self.named_child

        if type(value) is tuple:
            assert None not in value, self.named_child

            for val in value:
                val.parent = self
        elif value is not None:
            value.parent = self
        elif value is None:
            pass
        else:
            assert False, type(value)

        attr_name = "subnode_" + self.named_child
        setattr(self, attr_name, value)

    def finalize(self):
        del self.parent

        for c in self.getVisitableNodes():
            c.finalize()

    # TODO: De-duplicate this with multiple child variant.
    def computeExpressionRaw(self, trace_collection):
        """ Compute an expression.

            Default behavior is to just visit the child expressions first, and
            then the node "computeExpression". For a few cases this needs to
            be overloaded, e.g. conditional expressions.
        """
        # First apply the sub-expressions, as they are evaluated before.
        sub_expressions = self.getVisitableNodes()

        for count, sub_expression in enumerate(sub_expressions):
            assert sub_expression.isExpression(), (self, sub_expression)

            expression = trace_collection.onExpression(expression=sub_expression)

            if expression.willRaiseException(BaseException):
                wrapped_expression = wrapExpressionWithSideEffects(
                    side_effects=sub_expressions[:count],
                    old_node=sub_expression,
                    new_node=expression,
                )

                return (
                    wrapped_expression,
                    "new_raise",
                    lambda: "For '%s' the child expression '%s' will raise."
                    % (self.getChildNameNice(), expression.getChildNameNice()),
                )

        # Then ask ourselves to work on it.
        return self.computeExpression(trace_collection=trace_collection)

    def setChild(self, name, value):
        """ Set a child value.

            Do not overload, provider self.checkers instead.
        """
        # Only accept legal child names
        assert name == self.named_child, name

        # Lists as inputs are OK, but turn them into tuples.
        if type(value) is list:
            value = tuple(value)

        if self.checker is not None:
            value = self.checker(value)  # False alarm, pylint: disable=not-callable
        # Re-parent value to us.
        if type(value) is tuple:
            for val in value:
                val.parent = self
        elif value is not None:
            value.parent = self

        attr_name = "subnode_" + name

        # Determine old value, and inform it about losing its parent.
        old_value = getattr(self, attr_name)

        assert old_value is not value, value

        setattr(self, attr_name, value)

    def getChild(self, name):
        # Only accept legal child names
        attr_name = "subnode_" + name
        return getattr(self, attr_name)

    @staticmethod
    def childGetter(name):
        attr_name = "subnode_" + name

        def getter(self):
            return getattr(self, attr_name)

        return getter

    @staticmethod
    def childSetter(name):
        def setter(self, value):
            self.setChild(name, value)

        return setter

    def getVisitableNodes(self):
        # TODO: Consider if a generator would be faster.
        attr_name = "subnode_" + self.named_child
        value = getattr(self, attr_name)

        if value is None:
            return ()
        elif type(value) is tuple:
            return value
        elif isinstance(value, NodeBase):
            return (value,)
        else:
            raise AssertionError(self, "has illegal child", value, value.__class__)

    def getVisitableNodesNamed(self):
        """ Named children dictionary.

            For use in debugging and XML output.
        """
        attr_name = "subnode_" + self.named_child
        value = getattr(self, attr_name)

        yield self.named_child, value

    def replaceChild(self, old_node, new_node):
        if new_node is not None and not isinstance(new_node, NodeBase):
            raise AssertionError(
                "Cannot replace with", new_node, "old", old_node, "in", self
            )

        # Find the replaced node, as an added difficulty, what might be
        # happening, is that the old node is an element of a tuple, in which we
        # may also remove that element, by setting it to None.
        key = self.named_child
        value = self.getChild(key)

        if value is None:
            pass
        elif type(value) is tuple:
            if old_node in value:
                if new_node is not None:
                    self.setChild(
                        key,
                        tuple(
                            (val if val is not old_node else new_node) for val in value
                        ),
                    )
                else:
                    self.setChild(
                        key, tuple(val for val in value if val is not old_node)
                    )

                return key
        elif isinstance(value, NodeBase):
            if old_node is value:
                self.setChild(key, new_node)

                return key
        else:
            assert False, (key, value, value.__class__)

        raise AssertionError("Didn't find child", old_node, "in", self)

    def getCloneArgs(self):
        # Make clones of child nodes too.
        values = {}
        key = self.named_child

        value = self.getChild(key)

        assert type(value) is not list, key

        if value is None:
            values[key] = None
        elif type(value) is tuple:
            values[key] = tuple(v.makeClone() for v in value)
        else:
            values[key] = value.makeClone()

        values.update(self.getDetails())

        return values


class ExpressionSpecBasedComputationBase(ExpressionChildrenHavingBase):
    builtin_spec = None

    def computeBuiltinSpec(self, trace_collection, given_values):
        assert self.builtin_spec is not None, self

        for value in given_values:
            if value is not None and not value.isCompileTimeConstant():
                trace_collection.onExceptionRaiseExit(BaseException)

                return self, None, None

        if not self.builtin_spec.isCompileTimeComputable(given_values):
            trace_collection.onExceptionRaiseExit(BaseException)

            return self, None, None

        return trace_collection.getCompileTimeComputationResult(
            node=self,
            computation=lambda: self.builtin_spec.simulateCall(given_values),
            description="Built-in call to '%s' pre-computed."
            % (self.builtin_spec.getName()),
        )


class ExpressionBuiltinSingleArgBase(ExpressionSpecBasedComputationBase):
    named_children = ("value",)

    def __init__(self, value, source_ref):
        ExpressionSpecBasedComputationBase.__init__(
            self, values={"value": value}, source_ref=source_ref
        )

    getValue = ExpressionChildrenHavingBase.childGetter("value")

    def computeExpression(self, trace_collection):
        value = self.getValue()

        if value is None:
            return self.computeBuiltinSpec(
                trace_collection=trace_collection, given_values=()
            )
        else:
            return self.computeBuiltinSpec(
                trace_collection=trace_collection, given_values=(value,)
            )
