"""Transform mypy expression ASTs to mypyc IR (Intermediate Representation).

The top-level AST transformation logic is implemented in mypyc.irbuild.visitor
and mypyc.irbuild.builder.
"""

from __future__ import annotations

from typing import Callable, Sequence, cast

from mypy.nodes import (
    ARG_POS,
    LDEF,
    AssertTypeExpr,
    AssignmentExpr,
    BytesExpr,
    CallExpr,
    CastExpr,
    ComparisonExpr,
    ComplexExpr,
    ConditionalExpr,
    DictExpr,
    DictionaryComprehension,
    EllipsisExpr,
    Expression,
    FloatExpr,
    GeneratorExpr,
    IndexExpr,
    IntExpr,
    ListComprehension,
    ListExpr,
    MemberExpr,
    MypyFile,
    NameExpr,
    OpExpr,
    RefExpr,
    SetComprehension,
    SetExpr,
    SliceExpr,
    StarExpr,
    StrExpr,
    SuperExpr,
    TupleExpr,
    TypeApplication,
    TypeInfo,
    UnaryExpr,
    Var,
)
from mypy.types import Instance, ProperType, TupleType, TypeType, get_proper_type
from mypyc.common import MAX_SHORT_INT
from mypyc.ir.func_ir import FUNC_CLASSMETHOD, FUNC_STATICMETHOD
from mypyc.ir.ops import (
    Assign,
    BasicBlock,
    ComparisonOp,
    Integer,
    LoadAddress,
    LoadLiteral,
    RaiseStandardError,
    Register,
    TupleGet,
    TupleSet,
    Value,
)
from mypyc.ir.rtypes import (
    RTuple,
    bool_rprimitive,
    int_rprimitive,
    is_fixed_width_rtype,
    is_int_rprimitive,
    is_list_rprimitive,
    is_none_rprimitive,
    object_rprimitive,
    set_rprimitive,
)
from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional
from mypyc.irbuild.builder import IRBuilder, int_borrow_friendly_op
from mypyc.irbuild.constant_fold import constant_fold_expr
from mypyc.irbuild.for_helpers import (
    comprehension_helper,
    translate_list_comprehension,
    translate_set_comprehension,
)
from mypyc.irbuild.format_str_tokenizer import (
    convert_format_expr_to_bytes,
    convert_format_expr_to_str,
    join_formatted_bytes,
    join_formatted_strings,
    tokenizer_printf_style,
)
from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization
from mypyc.irbuild.util import bytes_from_str
from mypyc.primitives.bytes_ops import bytes_slice_op
from mypyc.primitives.dict_ops import dict_get_item_op, dict_new_op, dict_set_item_op
from mypyc.primitives.generic_ops import iter_op
from mypyc.primitives.int_ops import int_comparison_op_mapping
from mypyc.primitives.list_ops import list_append_op, list_extend_op, list_slice_op
from mypyc.primitives.misc_ops import ellipsis_op, get_module_dict_op, new_slice_op, type_op
from mypyc.primitives.registry import CFunctionDescription, builtin_names
from mypyc.primitives.set_ops import set_add_op, set_in_op, set_update_op
from mypyc.primitives.str_ops import str_slice_op
from mypyc.primitives.tuple_ops import list_tuple_op, tuple_slice_op

# Name and attribute references


def transform_name_expr(builder: IRBuilder, expr: NameExpr) -> Value:
    if expr.node is None:
        builder.add(
            RaiseStandardError(
                RaiseStandardError.RUNTIME_ERROR,
                "mypyc internal error: should be unreachable",
                expr.line,
            )
        )
        return builder.none()
    fullname = expr.node.fullname
    if fullname in builtin_names:
        typ, src = builtin_names[fullname]
        return builder.add(LoadAddress(typ, src, expr.line))
    # special cases
    if fullname == "builtins.None":
        return builder.none()
    if fullname == "builtins.True":
        return builder.true()
    if fullname == "builtins.False":
        return builder.false()

    if isinstance(expr.node, Var) and expr.node.is_final:
        value = builder.emit_load_final(
            expr.node,
            fullname,
            expr.name,
            builder.is_native_ref_expr(expr),
            builder.types[expr],
            expr.line,
        )
        if value is not None:
            return value

    if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
        return builder.load_module(expr.node.fullname)

    # If the expression is locally defined, then read the result from the corresponding
    # assignment target and return it. Otherwise if the expression is a global, load it from
    # the globals dictionary.
    # Except for imports, that currently always happens in the global namespace.
    if expr.kind == LDEF and not (isinstance(expr.node, Var) and expr.node.is_suppressed_import):
        # Try to detect and error when we hit the irritating mypy bug
        # where a local variable is cast to None. (#5423)
        if (
            isinstance(expr.node, Var)
            and is_none_rprimitive(builder.node_type(expr))
            and expr.node.is_inferred
        ):
            builder.error(
                'Local variable "{}" has inferred type None; add an annotation'.format(
                    expr.node.name
                ),
                expr.node.line,
            )

        # TODO: Behavior currently only defined for Var, FuncDef and MypyFile node types.
        if isinstance(expr.node, MypyFile):
            # Load reference to a module imported inside function from
            # the modules dictionary. It would be closer to Python
            # semantics to access modules imported inside functions
            # via local variables, but this is tricky since the mypy
            # AST doesn't include a Var node for the module. We
            # instead load the module separately on each access.
            mod_dict = builder.call_c(get_module_dict_op, [], expr.line)
            obj = builder.call_c(
                dict_get_item_op, [mod_dict, builder.load_str(expr.node.fullname)], expr.line
            )
            return obj
        else:
            return builder.read(builder.get_assignment_target(expr), expr.line)

    return builder.load_global(expr)


def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
    # First check if this is maybe a final attribute.
    final = builder.get_final_ref(expr)
    if final is not None:
        fullname, final_var, native = final
        value = builder.emit_load_final(
            final_var, fullname, final_var.name, native, builder.types[expr], expr.line
        )
        if value is not None:
            return value

    if isinstance(expr.node, MypyFile) and expr.node.fullname in builder.imports:
        return builder.load_module(expr.node.fullname)

    can_borrow = builder.is_native_attr_ref(expr)
    obj = builder.accept(expr.expr, can_borrow=can_borrow)
    rtype = builder.node_type(expr)

    # Special case: for named tuples transform attribute access to faster index access.
    typ = get_proper_type(builder.types.get(expr.expr))
    if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple:
        fields = typ.partial_fallback.type.metadata["namedtuple"]["fields"]
        if expr.name in fields:
            index = builder.builder.load_int(fields.index(expr.name))
            return builder.gen_method_call(obj, "__getitem__", [index], rtype, expr.line)

    check_instance_attribute_access_through_class(builder, expr, typ)

    borrow = can_borrow and builder.can_borrow
    return builder.builder.get_attr(obj, expr.name, rtype, expr.line, borrow=borrow)


def check_instance_attribute_access_through_class(
    builder: IRBuilder, expr: MemberExpr, typ: ProperType | None
) -> None:
    """Report error if accessing an instance attribute through class object."""
    if isinstance(expr.expr, RefExpr):
        node = expr.expr.node
        if isinstance(typ, TypeType) and isinstance(typ.item, Instance):
            # TODO: Handle other item types
            node = typ.item.type
        if isinstance(node, TypeInfo):
            class_ir = builder.mapper.type_to_ir.get(node)
            if class_ir is not None and class_ir.is_ext_class:
                sym = node.get(expr.name)
                if (
                    sym is not None
                    and isinstance(sym.node, Var)
                    and not sym.node.is_classvar
                    and not sym.node.is_final
                ):
                    builder.error(
                        'Cannot access instance attribute "{}" through class object'.format(
                            expr.name
                        ),
                        expr.line,
                    )
                    builder.note(
                        '(Hint: Use "x: Final = ..." or "x: ClassVar = ..." to define '
                        "a class attribute)",
                        expr.line,
                    )


def transform_super_expr(builder: IRBuilder, o: SuperExpr) -> Value:
    # warning(builder, 'can not optimize super() expression', o.line)
    sup_val = builder.load_module_attr_by_fullname("builtins.super", o.line)
    if o.call.args:
        args = [builder.accept(arg) for arg in o.call.args]
    else:
        assert o.info is not None
        typ = builder.load_native_type_object(o.info.fullname)
        ir = builder.mapper.type_to_ir[o.info]
        iter_env = iter(builder.builder.args)
        # Grab first argument
        vself: Value = next(iter_env)
        if builder.fn_info.is_generator:
            # grab sixth argument (see comment in translate_super_method_call)
            self_targ = list(builder.symtables[-1].values())[6]
            vself = builder.read(self_targ, builder.fn_info.fitem.line)
        elif not ir.is_ext_class:
            vself = next(iter_env)  # second argument is self if non_extension class
        args = [typ, vself]
    res = builder.py_call(sup_val, args, o.line)
    return builder.py_get_attr(res, o.name, o.line)


# Calls


def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value:
    callee = expr.callee
    if isinstance(expr.analyzed, CastExpr):
        return translate_cast_expr(builder, expr.analyzed)
    elif isinstance(expr.analyzed, AssertTypeExpr):
        # Compile to a no-op.
        return builder.accept(expr.analyzed.expr)
    elif (
        isinstance(callee, (NameExpr, MemberExpr))
        and isinstance(callee.node, TypeInfo)
        and callee.node.is_newtype
    ):
        # A call to a NewType type is a no-op at runtime.
        return builder.accept(expr.args[0])

    if isinstance(callee, IndexExpr) and isinstance(callee.analyzed, TypeApplication):
        callee = callee.analyzed.expr  # Unwrap type application

    if isinstance(callee, MemberExpr):
        return apply_method_specialization(builder, expr, callee) or translate_method_call(
            builder, expr, callee
        )
    elif isinstance(callee, SuperExpr):
        return translate_super_method_call(builder, expr, callee)
    else:
        return translate_call(builder, expr, callee)


def translate_call(builder: IRBuilder, expr: CallExpr, callee: Expression) -> Value:
    # The common case of calls is refexprs
    if isinstance(callee, RefExpr):
        return apply_function_specialization(builder, expr, callee) or translate_refexpr_call(
            builder, expr, callee
        )

    function = builder.accept(callee)
    args = [builder.accept(arg) for arg in expr.args]
    return builder.py_call(
        function, args, expr.line, arg_kinds=expr.arg_kinds, arg_names=expr.arg_names
    )


def translate_refexpr_call(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value:
    """Translate a non-method call."""
    # Gen the argument values
    arg_values = [builder.accept(arg) for arg in expr.args]

    return builder.call_refexpr_with_args(expr, callee, arg_values)


def translate_method_call(builder: IRBuilder, expr: CallExpr, callee: MemberExpr) -> Value:
    """Generate IR for an arbitrary call of form e.m(...).

    This can also deal with calls to module-level functions.
    """
    if builder.is_native_ref_expr(callee):
        # Call to module-level native function or such
        return translate_call(builder, expr, callee)
    elif (
        isinstance(callee.expr, RefExpr)
        and isinstance(callee.expr.node, TypeInfo)
        and callee.expr.node in builder.mapper.type_to_ir
        and builder.mapper.type_to_ir[callee.expr.node].has_method(callee.name)
    ):
        # Call a method via the *class*
        assert isinstance(callee.expr.node, TypeInfo)
        ir = builder.mapper.type_to_ir[callee.expr.node]
        decl = ir.method_decl(callee.name)
        args = []
        arg_kinds, arg_names = expr.arg_kinds[:], expr.arg_names[:]
        # Add the class argument for class methods in extension classes
        if decl.kind == FUNC_CLASSMETHOD and ir.is_ext_class:
            args.append(builder.load_native_type_object(callee.expr.node.fullname))
            arg_kinds.insert(0, ARG_POS)
            arg_names.insert(0, None)
        args += [builder.accept(arg) for arg in expr.args]

        if ir.is_ext_class:
            return builder.builder.call(decl, args, arg_kinds, arg_names, expr.line)
        else:
            obj = builder.accept(callee.expr)
            return builder.gen_method_call(
                obj,
                callee.name,
                args,
                builder.node_type(expr),
                expr.line,
                expr.arg_kinds,
                expr.arg_names,
            )

    elif builder.is_module_member_expr(callee):
        # Fall back to a PyCall for non-native module calls
        function = builder.accept(callee)
        args = [builder.accept(arg) for arg in expr.args]
        return builder.py_call(
            function, args, expr.line, arg_kinds=expr.arg_kinds, arg_names=expr.arg_names
        )
    else:
        receiver_typ = builder.node_type(callee.expr)

        # If there is a specializer for this method name/type, try calling it.
        # We would return the first successful one.
        val = apply_method_specialization(builder, expr, callee, receiver_typ)
        if val is not None:
            return val

        obj = builder.accept(callee.expr)
        args = [builder.accept(arg) for arg in expr.args]
        return builder.gen_method_call(
            obj,
            callee.name,
            args,
            builder.node_type(expr),
            expr.line,
            expr.arg_kinds,
            expr.arg_names,
        )


def translate_super_method_call(builder: IRBuilder, expr: CallExpr, callee: SuperExpr) -> Value:
    if callee.info is None or (len(callee.call.args) != 0 and len(callee.call.args) != 2):
        return translate_call(builder, expr, callee)

    # We support two-argument super but only when it is super(CurrentClass, self)
    # TODO: We could support it when it is a parent class in many cases?
    if len(callee.call.args) == 2:
        self_arg = callee.call.args[1]
        if (
            not isinstance(self_arg, NameExpr)
            or not isinstance(self_arg.node, Var)
            or not self_arg.node.is_self
        ):
            return translate_call(builder, expr, callee)

        typ_arg = callee.call.args[0]
        if (
            not isinstance(typ_arg, NameExpr)
            or not isinstance(typ_arg.node, TypeInfo)
            or callee.info is not typ_arg.node
        ):
            return translate_call(builder, expr, callee)

    ir = builder.mapper.type_to_ir[callee.info]
    # Search for the method in the mro, skipping ourselves. We
    # determine targets of super calls to native methods statically.
    for base in ir.mro[1:]:
        if callee.name in base.method_decls:
            break
    else:
        if (
            ir.is_ext_class
            and ir.builtin_base is None
            and not ir.inherits_python
            and callee.name == "__init__"
            and len(expr.args) == 0
        ):
            # Call translates to object.__init__(self), which is a
            # no-op, so omit the call.
            return builder.none()
        return translate_call(builder, expr, callee)

    decl = base.method_decl(callee.name)
    arg_values = [builder.accept(arg) for arg in expr.args]
    arg_kinds, arg_names = expr.arg_kinds[:], expr.arg_names[:]

    if decl.kind != FUNC_STATICMETHOD:
        # Grab first argument
        vself: Value = builder.self()
        if decl.kind == FUNC_CLASSMETHOD:
            vself = builder.call_c(type_op, [vself], expr.line)
        elif builder.fn_info.is_generator:
            # For generator classes, the self target is the 6th value
            # in the symbol table (which is an ordered dict). This is sort
            # of ugly, but we can't search by name since the 'self' parameter
            # could be named anything, and it doesn't get added to the
            # environment indexes.
            self_targ = list(builder.symtables[-1].values())[6]
            vself = builder.read(self_targ, builder.fn_info.fitem.line)
        arg_values.insert(0, vself)
        arg_kinds.insert(0, ARG_POS)
        arg_names.insert(0, None)

    return builder.builder.call(decl, arg_values, arg_kinds, arg_names, expr.line)


def translate_cast_expr(builder: IRBuilder, expr: CastExpr) -> Value:
    src = builder.accept(expr.expr)
    target_type = builder.type_to_rtype(expr.type)
    return builder.coerce(src, target_type, expr.line)


# Operators


def transform_unary_expr(builder: IRBuilder, expr: UnaryExpr) -> Value:
    folded = try_constant_fold(builder, expr)
    if folded:
        return folded

    return builder.unary_op(builder.accept(expr.expr), expr.op, expr.line)


def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
    if expr.op in ("and", "or"):
        return builder.shortcircuit_expr(expr)

    # Special case for string formatting
    if expr.op == "%" and (isinstance(expr.left, StrExpr) or isinstance(expr.left, BytesExpr)):
        ret = translate_printf_style_formatting(builder, expr.left, expr.right)
        if ret is not None:
            return ret

    folded = try_constant_fold(builder, expr)
    if folded:
        return folded

    borrow_left = False
    borrow_right = False

    ltype = builder.node_type(expr.left)
    rtype = builder.node_type(expr.right)

    # Special case some int ops to allow borrowing operands.
    if is_int_rprimitive(ltype) and is_int_rprimitive(rtype):
        if expr.op == "//":
            expr = try_optimize_int_floor_divide(expr)
        if expr.op in int_borrow_friendly_op:
            borrow_left = is_borrow_friendly_expr(builder, expr.right)
            borrow_right = True
    elif is_fixed_width_rtype(ltype) and is_fixed_width_rtype(rtype):
        borrow_left = is_borrow_friendly_expr(builder, expr.right)
        borrow_right = True

    left = builder.accept(expr.left, can_borrow=borrow_left)
    right = builder.accept(expr.right, can_borrow=borrow_right)
    return builder.binary_op(left, right, expr.op, expr.line)


def try_optimize_int_floor_divide(expr: OpExpr) -> OpExpr:
    """Replace // with a power of two with a right shift, if possible."""
    if not isinstance(expr.right, IntExpr):
        return expr
    divisor = expr.right.value
    shift = divisor.bit_length() - 1
    if 0 < shift < 28 and divisor == (1 << shift):
        return OpExpr(">>", expr.left, IntExpr(shift))
    return expr


def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
    index = expr.index
    base_type = builder.node_type(expr.base)
    is_list = is_list_rprimitive(base_type)
    can_borrow_base = is_list and is_borrow_friendly_expr(builder, index)

    base = builder.accept(expr.base, can_borrow=can_borrow_base)

    if isinstance(base.type, RTuple) and isinstance(index, IntExpr):
        return builder.add(TupleGet(base, index.value, expr.line))

    if isinstance(index, SliceExpr):
        value = try_gen_slice_op(builder, base, index)
        if value:
            return value

    index_reg = builder.accept(expr.index, can_borrow=is_list)
    return builder.gen_method_call(
        base, "__getitem__", [index_reg], builder.node_type(expr), expr.line
    )


def try_constant_fold(builder: IRBuilder, expr: Expression) -> Value | None:
    """Return the constant value of an expression if possible.

    Return None otherwise.
    """
    value = constant_fold_expr(builder, expr)
    if isinstance(value, int):
        return builder.load_int(value)
    elif isinstance(value, str):
        return builder.load_str(value)
    return None


def try_gen_slice_op(builder: IRBuilder, base: Value, index: SliceExpr) -> Value | None:
    """Generate specialized slice op for some index expressions.

    Return None if a specialized op isn't available.

    This supports obj[x:y], obj[:x], and obj[x:] for a few types.
    """
    if index.stride:
        # We can only handle the default stride of 1.
        return None

    if index.begin_index:
        begin_type = builder.node_type(index.begin_index)
    else:
        begin_type = int_rprimitive
    if index.end_index:
        end_type = builder.node_type(index.end_index)
    else:
        end_type = int_rprimitive

    # Both begin and end index must be int (or missing).
    if is_int_rprimitive(begin_type) and is_int_rprimitive(end_type):
        if index.begin_index:
            begin = builder.accept(index.begin_index)
        else:
            begin = builder.load_int(0)
        if index.end_index:
            end = builder.accept(index.end_index)
        else:
            # Replace missing end index with the largest short integer
            # (a sequence can't be longer).
            end = builder.load_int(MAX_SHORT_INT)
        candidates = [list_slice_op, tuple_slice_op, str_slice_op, bytes_slice_op]
        return builder.builder.matching_call_c(candidates, [base, begin, end], index.line)

    return None


def transform_conditional_expr(builder: IRBuilder, expr: ConditionalExpr) -> Value:
    if_body, else_body, next_block = BasicBlock(), BasicBlock(), BasicBlock()

    process_conditional(builder, expr.cond, if_body, else_body)
    expr_type = builder.node_type(expr)
    # Having actual Phi nodes would be really nice here!
    target = Register(expr_type)

    builder.activate_block(if_body)
    true_value = builder.accept(expr.if_expr)
    true_value = builder.coerce(true_value, expr_type, expr.line)
    builder.add(Assign(target, true_value))
    builder.goto(next_block)

    builder.activate_block(else_body)
    false_value = builder.accept(expr.else_expr)
    false_value = builder.coerce(false_value, expr_type, expr.line)
    builder.add(Assign(target, false_value))
    builder.goto(next_block)

    builder.activate_block(next_block)

    return target


def set_literal_values(builder: IRBuilder, items: Sequence[Expression]) -> list[object] | None:
    values: list[object] = []
    for item in items:
        const_value = constant_fold_expr(builder, item)
        if const_value is not None:
            values.append(const_value)
            continue

        if isinstance(item, RefExpr):
            if item.fullname == "builtins.None":
                values.append(None)
            elif item.fullname == "builtins.True":
                values.append(True)
            elif item.fullname == "builtins.False":
                values.append(False)
        elif isinstance(item, (BytesExpr, FloatExpr, ComplexExpr)):
            # constant_fold_expr() doesn't handle these (yet?)
            v = bytes_from_str(item.value) if isinstance(item, BytesExpr) else item.value
            values.append(v)
        elif isinstance(item, TupleExpr):
            tuple_values = set_literal_values(builder, item.items)
            if tuple_values is not None:
                values.append(tuple(tuple_values))

    if len(values) != len(items):
        # Bail if not all items can be converted into values.
        return None
    return values


def precompute_set_literal(builder: IRBuilder, s: SetExpr) -> Value | None:
    """Try to pre-compute a frozenset literal during module initialization.

    Return None if it's not possible.

    Supported items:
     - Anything supported by irbuild.constant_fold.constant_fold_expr()
     - None, True, and False
     - Float, byte, and complex literals
     - Tuple literals with only items listed above
    """
    values = set_literal_values(builder, s.items)
    if values is not None:
        return builder.add(LoadLiteral(frozenset(values), set_rprimitive))

    return None


def transform_comparison_expr(builder: IRBuilder, e: ComparisonExpr) -> Value:
    # x in (...)/[...]
    # x not in (...)/[...]
    first_op = e.operators[0]
    if (
        first_op in ["in", "not in"]
        and len(e.operators) == 1
        and isinstance(e.operands[1], (TupleExpr, ListExpr))
    ):
        items = e.operands[1].items
        n_items = len(items)
        # x in y -> x == y[0] or ... or x == y[n]
        # x not in y -> x != y[0] and ... and x != y[n]
        # 16 is arbitrarily chosen to limit code size
        if 1 < n_items < 16:
            if e.operators[0] == "in":
                bin_op = "or"
                cmp_op = "=="
            else:
                bin_op = "and"
                cmp_op = "!="
            lhs = e.operands[0]
            mypy_file = builder.graph["builtins"].tree
            assert mypy_file is not None
            bool_type = Instance(cast(TypeInfo, mypy_file.names["bool"].node), [])
            exprs = []
            for item in items:
                expr = ComparisonExpr([cmp_op], [lhs, item])
                builder.types[expr] = bool_type
                exprs.append(expr)

            or_expr: Expression = exprs.pop(0)
            for expr in exprs:
                or_expr = OpExpr(bin_op, or_expr, expr)
                builder.types[or_expr] = bool_type
            return builder.accept(or_expr)
        # x in [y]/(y) -> x == y
        # x not in [y]/(y) -> x != y
        elif n_items == 1:
            if e.operators[0] == "in":
                cmp_op = "=="
            else:
                cmp_op = "!="
            e.operators = [cmp_op]
            e.operands[1] = items[0]
        # x in []/() -> False
        # x not in []/() -> True
        elif n_items == 0:
            if e.operators[0] == "in":
                return builder.false()
            else:
                return builder.true()

    # x in {...}
    # x not in {...}
    if (
        first_op in ("in", "not in")
        and len(e.operators) == 1
        and isinstance(e.operands[1], SetExpr)
    ):
        set_literal = precompute_set_literal(builder, e.operands[1])
        if set_literal is not None:
            lhs = e.operands[0]
            result = builder.builder.call_c(
                set_in_op, [builder.accept(lhs), set_literal], e.line, bool_rprimitive
            )
            if first_op == "not in":
                return builder.unary_op(result, "not", e.line)
            return result

    if len(e.operators) == 1:
        # Special some common simple cases
        if first_op in ("is", "is not"):
            right_expr = e.operands[1]
            if isinstance(right_expr, NameExpr) and right_expr.fullname == "builtins.None":
                # Special case 'is None' / 'is not None'.
                return translate_is_none(builder, e.operands[0], negated=first_op != "is")
        left_expr = e.operands[0]
        if is_int_rprimitive(builder.node_type(left_expr)):
            right_expr = e.operands[1]
            if is_int_rprimitive(builder.node_type(right_expr)):
                if first_op in int_borrow_friendly_op:
                    borrow_left = is_borrow_friendly_expr(builder, right_expr)
                    left = builder.accept(left_expr, can_borrow=borrow_left)
                    right = builder.accept(right_expr, can_borrow=True)
                    return builder.compare_tagged(left, right, first_op, e.line)

    # TODO: Don't produce an expression when used in conditional context
    # All of the trickiness here is due to support for chained conditionals
    # (`e1 < e2 > e3`, etc). `e1 < e2 > e3` is approximately equivalent to
    # `e1 < e2 and e2 > e3` except that `e2` is only evaluated once.
    expr_type = builder.node_type(e)

    # go(i, prev) generates code for `ei opi e{i+1} op{i+1} ... en`,
    # assuming that prev contains the value of `ei`.
    def go(i: int, prev: Value) -> Value:
        if i == len(e.operators) - 1:
            return transform_basic_comparison(
                builder, e.operators[i], prev, builder.accept(e.operands[i + 1]), e.line
            )

        next = builder.accept(e.operands[i + 1])
        return builder.builder.shortcircuit_helper(
            "and",
            expr_type,
            lambda: transform_basic_comparison(builder, e.operators[i], prev, next, e.line),
            lambda: go(i + 1, next),
            e.line,
        )

    return go(0, builder.accept(e.operands[0]))


def translate_is_none(builder: IRBuilder, expr: Expression, negated: bool) -> Value:
    v = builder.accept(expr, can_borrow=True)
    return builder.binary_op(v, builder.none_object(), "is not" if negated else "is", expr.line)


def transform_basic_comparison(
    builder: IRBuilder, op: str, left: Value, right: Value, line: int
) -> Value:
    if (
        is_int_rprimitive(left.type)
        and is_int_rprimitive(right.type)
        and op in int_comparison_op_mapping.keys()
    ):
        return builder.compare_tagged(left, right, op, line)
    if is_fixed_width_rtype(left.type) and op in int_comparison_op_mapping.keys():
        if right.type == left.type:
            op_id = ComparisonOp.signed_ops[op]
            return builder.builder.comparison_op(left, right, op_id, line)
        elif isinstance(right, Integer):
            op_id = ComparisonOp.signed_ops[op]
            return builder.builder.comparison_op(
                left, Integer(right.value >> 1, left.type), op_id, line
            )
    elif (
        is_fixed_width_rtype(right.type)
        and op in int_comparison_op_mapping.keys()
        and isinstance(left, Integer)
    ):
        op_id = ComparisonOp.signed_ops[op]
        return builder.builder.comparison_op(
            Integer(left.value >> 1, right.type), right, op_id, line
        )

    negate = False
    if op == "is not":
        op, negate = "is", True
    elif op == "not in":
        op, negate = "in", True

    target = builder.binary_op(left, right, op, line)

    if negate:
        target = builder.unary_op(target, "not", line)
    return target


def translate_printf_style_formatting(
    builder: IRBuilder, format_expr: StrExpr | BytesExpr, rhs: Expression
) -> Value | None:
    tokens = tokenizer_printf_style(format_expr.value)
    if tokens is not None:
        literals, format_ops = tokens

        exprs = []
        if isinstance(rhs, TupleExpr):
            exprs = rhs.items
        elif isinstance(rhs, Expression):
            exprs.append(rhs)

        if isinstance(format_expr, BytesExpr):
            substitutions = convert_format_expr_to_bytes(
                builder, format_ops, exprs, format_expr.line
            )
            if substitutions is not None:
                return join_formatted_bytes(builder, literals, substitutions, format_expr.line)
        else:
            substitutions = convert_format_expr_to_str(
                builder, format_ops, exprs, format_expr.line
            )
            if substitutions is not None:
                return join_formatted_strings(builder, literals, substitutions, format_expr.line)

    return None


# Literals


def transform_int_expr(builder: IRBuilder, expr: IntExpr) -> Value:
    return builder.builder.load_int(expr.value)


def transform_float_expr(builder: IRBuilder, expr: FloatExpr) -> Value:
    return builder.builder.load_float(expr.value)


def transform_complex_expr(builder: IRBuilder, expr: ComplexExpr) -> Value:
    return builder.builder.load_complex(expr.value)


def transform_str_expr(builder: IRBuilder, expr: StrExpr) -> Value:
    return builder.load_str(expr.value)


def transform_bytes_expr(builder: IRBuilder, expr: BytesExpr) -> Value:
    return builder.load_bytes_from_str_literal(expr.value)


def transform_ellipsis(builder: IRBuilder, o: EllipsisExpr) -> Value:
    return builder.add(LoadAddress(ellipsis_op.type, ellipsis_op.src, o.line))


# Display expressions


def transform_list_expr(builder: IRBuilder, expr: ListExpr) -> Value:
    return _visit_list_display(builder, expr.items, expr.line)


def _visit_list_display(builder: IRBuilder, items: list[Expression], line: int) -> Value:
    return _visit_display(
        builder, items, builder.new_list_op, list_append_op, list_extend_op, line, True
    )


def transform_tuple_expr(builder: IRBuilder, expr: TupleExpr) -> Value:
    if any(isinstance(item, StarExpr) for item in expr.items):
        # create a tuple of unknown length
        return _visit_tuple_display(builder, expr)

    # create a tuple of fixed length (RTuple)
    tuple_type = builder.node_type(expr)
    # When handling NamedTuple et. al we might not have proper type info,
    # so make some up if we need it.
    types = (
        tuple_type.types
        if isinstance(tuple_type, RTuple)
        else [object_rprimitive] * len(expr.items)
    )

    items = []
    for item_expr, item_type in zip(expr.items, types):
        reg = builder.accept(item_expr)
        items.append(builder.coerce(reg, item_type, item_expr.line))
    return builder.add(TupleSet(items, expr.line))


def _visit_tuple_display(builder: IRBuilder, expr: TupleExpr) -> Value:
    """Create a list, then turn it into a tuple."""
    val_as_list = _visit_list_display(builder, expr.items, expr.line)
    return builder.call_c(list_tuple_op, [val_as_list], expr.line)


def transform_dict_expr(builder: IRBuilder, expr: DictExpr) -> Value:
    """First accepts all keys and values, then makes a dict out of them."""
    key_value_pairs = []
    for key_expr, value_expr in expr.items:
        key = builder.accept(key_expr) if key_expr is not None else None
        value = builder.accept(value_expr)
        key_value_pairs.append((key, value))

    return builder.builder.make_dict(key_value_pairs, expr.line)


def transform_set_expr(builder: IRBuilder, expr: SetExpr) -> Value:
    return _visit_display(
        builder, expr.items, builder.new_set_op, set_add_op, set_update_op, expr.line, False
    )


def _visit_display(
    builder: IRBuilder,
    items: list[Expression],
    constructor_op: Callable[[list[Value], int], Value],
    append_op: CFunctionDescription,
    extend_op: CFunctionDescription,
    line: int,
    is_list: bool,
) -> Value:
    accepted_items = []
    for item in items:
        if isinstance(item, StarExpr):
            accepted_items.append((True, builder.accept(item.expr)))
        else:
            accepted_items.append((False, builder.accept(item)))

    result: Value | None = None
    initial_items = []
    for starred, value in accepted_items:
        if result is None and not starred and is_list:
            initial_items.append(value)
            continue

        if result is None:
            result = constructor_op(initial_items, line)

        builder.call_c(extend_op if starred else append_op, [result, value], line)

    if result is None:
        result = constructor_op(initial_items, line)

    return result


# Comprehensions


def transform_list_comprehension(builder: IRBuilder, o: ListComprehension) -> Value:
    return translate_list_comprehension(builder, o.generator)


def transform_set_comprehension(builder: IRBuilder, o: SetComprehension) -> Value:
    return translate_set_comprehension(builder, o.generator)


def transform_dictionary_comprehension(builder: IRBuilder, o: DictionaryComprehension) -> Value:
    d = builder.maybe_spill(builder.call_c(dict_new_op, [], o.line))
    loop_params = list(zip(o.indices, o.sequences, o.condlists, o.is_async))

    def gen_inner_stmts() -> None:
        k = builder.accept(o.key)
        v = builder.accept(o.value)
        builder.call_c(dict_set_item_op, [builder.read(d), k, v], o.line)

    comprehension_helper(builder, loop_params, gen_inner_stmts, o.line)
    return builder.read(d)


# Misc


def transform_slice_expr(builder: IRBuilder, expr: SliceExpr) -> Value:
    def get_arg(arg: Expression | None) -> Value:
        if arg is None:
            return builder.none_object()
        else:
            return builder.accept(arg)

    args = [get_arg(expr.begin_index), get_arg(expr.end_index), get_arg(expr.stride)]
    return builder.call_c(new_slice_op, args, expr.line)


def transform_generator_expr(builder: IRBuilder, o: GeneratorExpr) -> Value:
    builder.warning("Treating generator comprehension as list", o.line)
    return builder.call_c(iter_op, [translate_list_comprehension(builder, o)], o.line)


def transform_assignment_expr(builder: IRBuilder, o: AssignmentExpr) -> Value:
    value = builder.accept(o.value)
    target = builder.get_assignment_target(o.target)
    builder.assign(target, value, o.line)
    return value
