#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Beartype :pep:`695`-compliant **abstract syntax tree (AST) transformers** (i.e.,
low-level classes instrumenting :pep:`695`-compliant ``type`` alias statements
in well-typed third-party modules with runtime type-checking dynamically
generated by the :func:`beartype.beartype` decorator).

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ TODO                               }....................
#FIXME: CPython's current implementation of PEP 695 type aliases is
#fundamentally broken with respect to unquoted relative forward references.
#Please submit an upstream issue describing this patent failure. On doing so,
#please also publicly declare that PEP 695 appears to have been poorly tested.
#As evidence, note that PEP 695 itself advises use of the following idiom:
#    # A type alias that includes a forward reference
#    type AnimalOrVegetable = Animal | "Vegetable"
#
#*THAT DOES NOT ACTUALLY WORK AT RUNTIME.* Nobody tested that. This is why I
#facepalm. Notably, PEP 604-compliant new-style unions prohibit strings. They
#probably shouldn't, but they've *ALWAYS* behaved that way, and nobody's updated
#them to behave more intelligently -- probably because doing so would require
#updating the isinstance() builtin (which also accepts PEP 604-compliant
#new-style unions) to behave more intelligently and ain't nobody goin' there:
#    $ python3.12
#    >>> type AnimalOrVegetable = "Animal" | "Vegetable"
#    >>> AnimalOrVegetable.__value__
#    Traceback (most recent call last):
#      Cell In[3], line 1
#        AnimalOrVegetable.__value__
#      Cell In[2], line 1 in AnimalOrVegetable
#        type AnimalOrVegetable = "Animal" | "Vegetable"
#    TypeError: unsupported operand type(s) for |: 'str' and 'str'
#
#However, even ignoring that obvious syntactic issue, PEP 695 still fails to
#actually support forward references -- because exceptions are *NOT* forward
#references. Forward references are proxy objects that refer to other objects
#that have yet to be defined at runtime. Notably:
#    $ python3.12
#    # This is a forward reference.
#    >>> type VegetableRef = 'Vegetable'
#    >>> VegetableRef.__value__
#    'Vegetable'
#
#    # So is this.
#    >>> from typing import ForwardRef
#    >>> type FruityRef = ForwardRef('Fruit')
#    >>> FruityRef.__value__
#    ForwardRef('Fruit')
#
#    # This is *NOT* a forward reference.
#    >>> type AnimalOrAnimals = Animal
#    >>> AnimalOrAnimals.__value__
#    Traceback (most recent call last):
#      Cell In[2], line 1
#        AnimalRef.__value__
#      Cell In[1], line 1 in AnimalRef
#        type AnimalRef = Animal
#    NameError: name 'Animal' is not defined
#
#*FACEPALM*
#FIXME: *BIG YIKES.* CPython's low-level C-based implementation of PEP
#695-compliant type aliases currently fails to properly resolve unquoted
#relative forward references defined in a local rather than global scope. I
#tried literally everything to get this to work via AST transformations -- but
#whatever arcane type alias machinery it is that they've implemented simply does
#*NOT* behave as expected at local scope. That said, we've verified this
#*SHOULD* work via this simple snippet:
#    def foo():
#        type bar = wut
#        globals()['wut'] = str
#        print(bar.__value__)
#    foo()
#
#That behaves as expected -- until you actually then define the expected class
#at local scope:
#    def foo():
#        type bar = wut
#        globals()['wut'] = str
#        print(bar.__value__)
#        class wut(object): pass  # <-- this causes madness; WTF!?!?!?
#
#The above print() statement now raises non-human readable exceptions
#resembling:
#    NameError: cannot access free variable 'wut' where it is not associated
#    with a value in enclosing scope
#
#Clearly, this is madness. At the point at which the print() statement is run,
#the "wut" class has yet to be redefined as a class. This constitutes a profound
#CPython bug. Please submit us up the F-F-F-bomb.

# ....................{ IMPORTS                            }....................
from ast import (
    AST,
    Assign,
    # Constant,
    For,
    # JoinedStr,
    Subscript,
)
from beartype.claw._clawmagic import (
    BEARTYPE_HINT_PEP695_FORWARDREF_ITER_FUNC_NAME)
from beartype.claw._clawtyping import NodeVisitResult
from beartype._data.ast.dataast import NODE_CONTEXT_STORE
from beartype._util.ast.utilastmunge import copy_node_metadata
from beartype._util.ast.utilastmake import (
    make_node_attribute_load,
    make_node_call,
    # make_node_call_expr,
    # make_node_fstr_field,
    make_node_name_load,
    make_node_name_store,
)

# ....................{ SUBCLASSES                         }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: To improve forward compatibility with the superclass API over which
# we have *NO* control, avoid accidental conflicts by suffixing *ALL* private
# and public attributes of this subclass by "_beartype".
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

class BeartypeNodeTransformerPep695Mixin(object):
    '''
    Beartype :pep:`695`-compliant **abstract syntax tree (AST) node
    transformer** (i.e., visitor pattern recursively transforming *all*
    :pep:`695`-compliant ``type`` alias statements in the AST tree passed to the
    :meth:`visit` method of the
    :class:`beartype.claw._ast.clawastmain.BeartypeNodeTransformer` subclass
    also subclassing this mixin).
    '''

    # ..................{ VISITORS ~ pep : 695               }..................
    def visit_TypeAlias(self, node: 'ast.TypeAlias') -> NodeVisitResult:  # type: ignore[name-defined]
        '''
        Add new sibling nodes following the passed **type alias statement**
        (i.e., node signifying the definition of a :pep:`695`-compliant ``type``
        alias) iteratively defining one **forward reference proxy** (i.e.,
        :class:`beartype._check.forward.reference.fwdrefabc.BeartypeForwardRefABC`
        subclass) for each unquoted relative forward reference in this
        statement.

        Doing so is required, as :pep:`695` fails to actually support unquoted
        relative forward references despite publicly claiming to do so. Notably,
        :pep:`695`-compliant type aliases raise non-human-readable
        :exc:`NameError` and :exc:`UnboundLocalError` exceptions when attempting
        to resolve type aliases containing one or more unquoted relative forward
        references. Clearly, exceptions are *not* valid forward references.
        Forward references are proxy objects that refer to other objects that
        have yet to be defined at runtime. Notably, this is very awful:

        .. code-block:: pycon

        >>> type AnimalOrAnimals = Animal | list[Animal]
        >>> AnimalOrAnimals.__value__
        Traceback (most recent call last):
          Cell In[2], line 1
            AnimalOrAnimals.__value__
          Cell In[1], line 1 in AnimalOrAnimals
            type AnimalOrAnimals = Animal | list[Animal]
        NameError: name 'Animal' is not defined

        Circumventing this patent oversight on the part of both Guido and PEP
        695 authors requires transforming this AST to inject new sibling nodes
        encapsulating each unquoted relative forward reference in this alias
        with a new forward reference proxy and then redefining this alias to
        forcefully uncache this alias.

        Parameters
        ----------
        node : TypeAlias
            Type alias to be transformed.

        Returns
        -------
        NodeVisitResult
            A list comprising (in order):

            #. This type alias node as is.
            #. New sibling nodes encapsulating each unquoted relative forward
               reference in this alias with a new forward reference proxy.
            #. This type alias node recapitulated to undo any prior caching of
               this type alias.
        '''

        # Recursively transform *ALL* child nodes of this type alias node.
        self.generic_visit(node)  # type: ignore[attr-defined]

        # If this type alias is declared at module scope, generate efficient
        # code permissible *ONLY* at module scope for optimally iteratively
        # defining one forward reference proxy for each unquoted relative
        # forward reference in this type alias. Notably, generate this:
        #     for _ in __iter_hint_pep695_forwardref_beartype__({alias_name}):
        #         globals()[_.__name_beartype__] = _
        #
        # Else, this type alias is *NOT* declared at module scope and is thus
        # declared at a lower scope (e.g., class, callable). In this case,
        # fallback to generating inefficient code globally permissible at all
        # possible scopes. Notably, generate this:
        #     for _ in __iter_hint_pep695_forwardref_beartype__({alias_name}):
        #         exec(f'{_.__name_beartype__} = _')

        # Child nodes both accessing and assigning this type alias as a global
        # or local variable.
        node_alias_var_name_load = make_node_name_load(
            name=node.name.id, node_sibling=node)
        # node_alias_var_name_store = make_node_name_store(
        #     name=node.name.id, node_sibling=node)

        # Child nodes both accessing and assigning the standard "_" scratch
        # (i.e., placeholder) local variable.
        node_scratch_var_name_load = make_node_name_load(
            name='_', node_sibling=node)
        node_scratch_var_name_store = make_node_name_store(
            name='_', node_sibling=node)

        # Child node accessing the unqualified basename of the current forward
        # reference proxy to be defined in the current lexical scope via the
        # "BeartypeForwardRefABC.__name_beartype__" class variable of this
        # proxy, which is currently stored in the scratch variable. Notably,
        # "_" is a subclass of the "BeartypeForwardRefABC" superclass.
        node_forwardref_name_load = make_node_attribute_load(
            node_name_load=node_scratch_var_name_load,
            attr_name='__name_beartype__',
            node_sibling=node,
        )

        # Child node passing this iterator this type alias, which then returns a
        # C-based generator object via the standard Python idiom for
        # "yield"-specific generators.
        node_forwardref_iter_call = make_node_call(
            func_name=BEARTYPE_HINT_PEP695_FORWARDREF_ITER_FUNC_NAME,
            nodes_args=[node_alias_var_name_load],
            node_sibling=node,
        )

        # Child node defining a new global or local variable whose:
        # * Name is the unqualified basename of the undefined attribute referred
        #   to by the currently iterated forward reference proxy.
        # * Value is that proxy.
        node_forwardref_define: AST = None  # type: ignore[assignment]

        # Child node subscripting the...
        node_forwardref_global_store = Subscript(
            # Dictionary of all currently defined global variables returned by a
            # call to the builtin globals() function. Thankfully, this
            # dictionary is efficiently modifiable and behaves in the typical
            # way when directly modified.
            #
            # Note that the same *CANNOT* be said for the builtin locals()
            # function, whose behaviour is effectively non-deterministic. Ergo,
            # the inefficient fallback approach adopted below.
            value=make_node_call(func_name='globals', node_sibling=node),
            # Assign the key of the returned dictionary whose name is given by
            # the "BeartypeForwardRefABC.__name_beartype__" class variable of
            # this proxy, stored in the scratch variable.
            slice=node_forwardref_name_load,
            ctx=NODE_CONTEXT_STORE,
        )

        # Child node efficiently defining this proxy as a new global,
        # implemented as an assignment to...
        node_forwardref_define = Assign(
            # The global variable whose name is the unqualified basename of the
            # undefined attribute referred to by the currently iterated forward
            # reference proxy.
            targets=[node_forwardref_global_store],
            # Assigned the value of the scratch variable, which is a subclass of
            # the "BeartypeForwardRefABC" superclass.
            value=node_scratch_var_name_load,
        )

        # Child node iterating over all forward reference proxies generated by
        # this iterator and, for each such proxy:
        # * Locally assigning that proxy to the standard "_" scratch (i.e.,
        #   placeholder) local variable.
        # * Defining a new global or local variable whose:
        #   * Name is the unqualified basename of the undefined attribute
        #     referred to by that proxy.
        #   * Value is that proxy.
        node_forwardrefs_define = For(
            target=node_scratch_var_name_store,
            iter=node_forwardref_iter_call,
            body=[node_forwardref_define],
        )

        # Copy all source code metadata from this type alias node onto *ALL*
        # sibling nodes created above.
        copy_node_metadata(node_src=node, node_trg=(
            node_forwardref_global_store,
            node_forwardref_define,
            node_forwardrefs_define,
        ))

        # Return a list comprising these adjacent nodes.
        #
        # Note that order is *EXTREMELY* significant.
        return [
            # Initial definition of this type alias preserved as is.
            node,
            # For loop iteratively defining one forward reference proxy global
            # or local variable for each undefined attribute in this type alias.
            node_forwardrefs_define,
            # Intentionally redefine this alias. Although this appears to be an
            # inefficient noop, this is in fact an essential operation. Why?
            # Because the prior successful access of the "__value__" dunder
            # variable of this type alias in the iter_hint_pep695_forwardrefs()
            # iterator called above silently cached and thus froze the value of
            # this alias. However, alias values are *NOT* necessarily safely
            # freezable at alias definition time. A canonical example of alias
            # values that are *NOT* safely freezable at alias definition time
            # is mutually recursive aliases (i.e., aliases whose values
            # circularly refer to one another): e.g.,
            #     type a = b
            #     type b = a
            #
            # PEP 695 provides no explicit means of uncaching alias values. Our
            # only recourse is to repetitiously redefine this alias. It sucks.
            node,
        ]
