# Part of the Tapl Language project, under the Apache License v2.0 with LLVM
# Exceptions. See /LICENSE for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import dataclasses
import enum
from collections.abc import Callable, Generator
from typing import Any, cast

from tapl_lang.core import syntax, tapl_error

################################################################################
# Python AST Terms
#
# These terms closely mirror the classes in the `ast` module.
# Keep them sorted as in https://docs.python.org/3/library/ast.html
################################################################################

Identifier = str | Callable[[syntax.BackendSetting], str]


@dataclasses.dataclass
class Module(syntax.Term):
    body: list[syntax.Term]

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.body

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: Module(body=[layer(b) for b in self.body]))


################################################################################
# STATEMENTS


@dataclasses.dataclass
class FunctionDef(syntax.Term):
    name: Identifier
    posonlyargs: list[str]
    args: list[str]
    vararg: str | None
    kwonlyargs: list[str]
    kw_defaults: list[syntax.Term]
    kwarg: str | None
    defaults: list[syntax.Term]
    body: syntax.Term
    decorator_list: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.kw_defaults
        yield from self.defaults
        yield self.body
        yield from self.decorator_list

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: FunctionDef(
                name=self.name,
                posonlyargs=self.posonlyargs,
                args=self.args,
                vararg=self.vararg,
                kwonlyargs=self.kwonlyargs,
                kw_defaults=[layer(k) for k in self.kw_defaults],
                kwarg=self.kwarg,
                defaults=[layer(d) for d in self.defaults],
                body=layer(self.body),
                decorator_list=[layer(d) for d in self.decorator_list],
                location=self.location,
            )
        )


@dataclasses.dataclass
class ClassDef(syntax.Term):
    name: Identifier
    bases: list[syntax.Term]
    keywords: list[tuple[str, syntax.Term]]
    body: syntax.Term
    decorator_list: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.bases
        yield from (t for _, t in self.keywords)
        yield self.body
        yield from self.decorator_list

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: ClassDef(
                name=self.name,
                bases=[layer(b) for b in self.bases],
                keywords=[(k, layer(v)) for k, v in self.keywords],
                body=layer(self.body),
                decorator_list=[layer(d) for d in self.decorator_list],
                location=self.location,
            )
        )


@dataclasses.dataclass
class Return(syntax.Term):
    value: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        if self.value is None:
            raise ValueError('Return statement must have a value')
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: Return(value=layer(self.value), location=self.location))


@dataclasses.dataclass
class Delete(syntax.Term):
    targets: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.targets

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: Delete(targets=[layer(t) for t in self.targets], location=self.location))


@dataclasses.dataclass
class Assign(syntax.Term):
    targets: list[syntax.Term]
    value: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.targets
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Assign(
                targets=[layer(t) for t in self.targets], value=layer(self.value), location=self.location
            )
        )


@dataclasses.dataclass
class For(syntax.Term):
    target: syntax.Term
    iter: syntax.Term
    body: syntax.Term
    orelse: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.target
        yield self.iter
        yield self.body
        yield self.orelse

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: For(
                target=layer(self.target),
                iter=layer(self.iter),
                body=layer(self.body),
                orelse=layer(self.orelse),
                location=self.location,
            )
        )


@dataclasses.dataclass
class While(syntax.Term):
    test: syntax.Term
    body: syntax.Term
    orelse: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.test
        yield self.body
        yield self.orelse

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: While(
                test=layer(self.test),
                body=layer(self.body),
                orelse=layer(self.orelse),
                location=self.location,
            )
        )


@dataclasses.dataclass
class If(syntax.Term):
    test: syntax.Term
    body: syntax.Term
    orelse: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.test
        yield self.body
        yield self.orelse

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: If(
                test=layer(self.test),
                body=layer(self.body),
                orelse=layer(self.orelse),
                location=self.location,
            )
        )


@dataclasses.dataclass
class WithItem(syntax.Term):
    context_expr: syntax.Term
    optional_vars: syntax.Term

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.context_expr
        yield self.optional_vars

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: WithItem(
                context_expr=layer(self.context_expr),
                optional_vars=layer(self.optional_vars),
            )
        )


@dataclasses.dataclass
class With(syntax.Term):
    items: list[syntax.Term]
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.items
        yield self.body

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: With(
                items=[layer(i) for i in self.items],
                body=layer(self.body),
                location=self.location,
            )
        )


@dataclasses.dataclass
class Raise(syntax.Term):
    exception: syntax.Term
    cause: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.exception
        yield self.cause

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Raise(
                exception=layer(self.exception),
                cause=layer(self.cause),
                location=self.location,
            )
        )


@dataclasses.dataclass
class Try(syntax.Term):
    body: syntax.Term
    handlers: list[syntax.Term]
    orelse: syntax.Term
    finalbody: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.body
        yield from self.handlers
        yield self.orelse
        yield self.finalbody

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Try(
                body=layer(self.body),
                handlers=[layer(h) for h in self.handlers],
                orelse=layer(self.orelse),
                finalbody=layer(self.finalbody),
                location=self.location,
            )
        )


@dataclasses.dataclass
class ExceptHandler(syntax.Term):
    exception_type: syntax.Term
    name: Identifier | None
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.exception_type
        yield self.body

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: ExceptHandler(
                exception_type=layer(self.exception_type),
                name=self.name,
                body=layer(self.body),
                location=self.location,
            )
        )


@dataclasses.dataclass
class Alias:
    name: str
    asname: str | None = None


@dataclasses.dataclass
class Import(syntax.Term):
    names: list[Alias]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda _: Import(names=[Alias(name=n.name, asname=n.asname) for n in self.names], location=self.location)
        )


@dataclasses.dataclass
class ImportFrom(syntax.Term):
    module: str | None
    names: list[Alias]
    level: int
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda _: ImportFrom(
                module=self.module,
                names=[Alias(name=n.name, asname=n.asname) for n in self.names],
                level=self.level,
                location=self.location,
            )
        )


@dataclasses.dataclass
class Expr(syntax.Term):
    value: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: Expr(value=layer(self.value), location=self.location))


@dataclasses.dataclass
class Pass(syntax.Term):
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda _: Pass(location=self.location))


################################################################################
# EXPRESSIONS


@dataclasses.dataclass
class BoolOp(syntax.Term):
    operator: str
    values: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.values

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: BoolOp(operator=self.operator, values=[layer(v) for v in self.values], location=self.location)
        )


# FIXME: target of ast.NamedExpr accepts only ast.Name. This prevents us to assign attributes like s0.name := s0.Int. Figure out how to support that.
@dataclasses.dataclass
class NamedExpr(syntax.Term):
    target: syntax.Term
    value: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.target
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: NamedExpr(
                target=layer(self.target),
                value=layer(self.value),
                location=self.location,
            )
        )


@dataclasses.dataclass
class BinOp(syntax.Term):
    left: syntax.Term
    op: str
    right: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.left
        yield self.right

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: BinOp(left=layer(self.left), op=self.op, right=layer(self.right), location=self.location)
        )


@dataclasses.dataclass
class UnaryOp(syntax.Term):
    op: str
    operand: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.operand

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: UnaryOp(op=self.op, operand=layer(self.operand), location=self.location))


@dataclasses.dataclass
class Set(syntax.Term):
    elements: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.elements

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: Set(elements=[layer(v) for v in self.elements], location=self.location))


@dataclasses.dataclass
class Dict(syntax.Term):
    keys: list[syntax.Term]
    values: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.keys
        yield from self.values

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Dict(
                keys=[layer(k) for k in self.keys],
                values=[layer(v) for v in self.values],
                location=self.location,
            )
        )


@dataclasses.dataclass
class Compare(syntax.Term):
    left: syntax.Term
    operators: list[str]
    comparators: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.left
        yield from self.comparators

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Compare(
                left=layer(self.left),
                operators=self.operators,
                comparators=[layer(v) for v in self.comparators],
                location=self.location,
            )
        )


@dataclasses.dataclass
class Call(syntax.Term):
    func: syntax.Term
    args: list[syntax.Term]
    keywords: list[tuple[str, syntax.Term]]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.func
        yield from self.args
        yield from (v for _, v in self.keywords)

    def separate(self, ls) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Call(
                func=layer(self.func),
                args=[layer(v) for v in self.args],
                keywords=[(k, layer(v)) for k, v in self.keywords],
                location=self.location,
            )
        )


@dataclasses.dataclass
class Constant(syntax.Term):
    value: Any
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda _: Constant(value=self.value, location=self.location))


@dataclasses.dataclass
class Attribute(syntax.Term):
    value: syntax.Term
    attr: Identifier
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Attribute(value=layer(self.value), attr=self.attr, ctx=self.ctx, location=self.location)
        )


@dataclasses.dataclass
class Subscript(syntax.Term):
    value: syntax.Term
    slice: syntax.Term
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.value
        yield self.slice

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Subscript(
                value=layer(self.value),
                slice=layer(self.slice),
                ctx=self.ctx,
                location=self.location,
            )
        )


@dataclasses.dataclass
class Name(syntax.Term):
    id: Identifier
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda _: Name(id=self.id, ctx=self.ctx, location=self.location))


@dataclasses.dataclass
class List(syntax.Term):
    elements: list[syntax.Term]
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.elements

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: List(elements=[layer(v) for v in self.elements], ctx=self.ctx, location=self.location)
        )


@dataclasses.dataclass
class Tuple(syntax.Term):
    elements: list[syntax.Term]
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.elements

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Tuple(elements=[layer(v) for v in self.elements], ctx=self.ctx, location=self.location)
        )


@dataclasses.dataclass
class Slice(syntax.Term):
    lower: syntax.Term
    upper: syntax.Term
    step: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.lower
        yield self.upper
        yield self.step

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Slice(
                lower=layer(self.lower),
                upper=layer(self.upper),
                step=layer(self.step),
                location=self.location,
            )
        )


################################################################################
# Untyped Terms
#
# These terms extend the Python AST terms defined above.
################################################################################


@dataclasses.dataclass
class Select(syntax.Term):
    value: syntax.Term
    names: list[str]
    ctx: str
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.value

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Select(
                value=layer(self.value),
                names=self.names,
                ctx=self.ctx,
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if not self.names:
            return syntax.ErrorTerm(
                message='At least one name is required to select a path.',
                location=self.location,
            )
        value = self.value
        for i in range(len(self.names) - 1):
            value = Attribute(
                value=value,
                attr=self.names[i],
                ctx='load',
                location=self.location,
            )
        return Attribute(
            value=value,
            attr=self.names[-1],
            ctx=self.ctx,
            location=self.location,
        )


@dataclasses.dataclass
class Path(syntax.Term):
    names: list[str]
    # FIXME: Find a better name for the ctx field. options: context, reference_mode. Or keep it as it is since it's already used in the Python AST and has a clear meaning.
    ctx: str
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Path(names=self.names, ctx=self.ctx, mode=layer(self.mode), location=self.location)
        )

    def unfold(self) -> syntax.Term:
        if len(self.names) <= 1:
            return syntax.ErrorTerm(message='At least two names are required to create a path.', location=self.location)
        value: syntax.Term = TypedName(location=self.location, id=self.names[0], ctx='load', mode=self.mode)
        for i in range(1, len(self.names) - 1):
            value = Attribute(value=value, attr=self.names[i], ctx='load', location=self.location)
        return Attribute(value=value, attr=self.names[-1], ctx=self.ctx, location=self.location)


@dataclasses.dataclass
class BranchTyping(syntax.Term):
    branches: list[syntax.Term]
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.branches

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: BranchTyping(branches=[layer(b) for b in self.branches], location=self.location))

    def unfold(self) -> syntax.Term:
        def nested_scope(inner_term: syntax.Term) -> syntax.Term:
            return syntax.BackendSettingTerm(
                backend_setting_changer=syntax.BackendSettingChanger(
                    lambda setting: setting.clone(scope_level=setting.scope_level + 1)
                ),
                term=inner_term,
            )

        def new_scope() -> syntax.Term:
            return Assign(
                targets=[
                    nested_scope(
                        Name(
                            id=lambda setting: setting.scope_name,
                            ctx='store',
                            location=self.location,
                        )
                    )
                ],
                value=Call(
                    func=Path(
                        names=['tapl_typing', 'fork_scope'],
                        ctx='load',
                        mode=MODE_TYPECHECK_NO_SCOPE,
                        location=self.location,
                    ),
                    args=[
                        Name(
                            id=lambda setting: setting.forker_name,
                            ctx='load',
                            location=self.location,
                        )
                    ],
                    keywords=[],
                    location=self.location,
                ),
                location=self.location,
            )

        body: list[syntax.Term] = []
        for b in self.branches:
            body.append(new_scope())
            body.append(nested_scope(b))

        return With(
            location=self.location,
            items=[
                WithItem(
                    context_expr=Call(
                        func=Path(
                            names=['tapl_typing', 'scope_forker'],
                            ctx='load',
                            mode=MODE_TYPECHECK_NO_SCOPE,
                            location=self.location,
                        ),
                        args=[
                            Name(
                                id=lambda setting: setting.scope_name,
                                ctx='load',
                                location=self.location,
                            )
                        ],
                        keywords=[],
                        location=self.location,
                    ),
                    optional_vars=Name(
                        id=lambda setting: setting.forker_name,
                        ctx='store',
                        location=self.location,
                    ),
                )
            ],
            body=syntax.TermList(terms=body),
        )


################################################################################
# Typed Terms
#
# These terms add a type layer to enable type checking.
################################################################################


@dataclasses.dataclass
class ModeTerm(syntax.Term):
    typecheck: bool = False
    use_scope: bool = False

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from ()

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda _: self)


MODE_EVALUATE = ModeTerm(typecheck=False, use_scope=False)
MODE_EVALUATE_WITH_SCOPE = ModeTerm(typecheck=False, use_scope=True)
MODE_TYPECHECK = ModeTerm(typecheck=True, use_scope=True)
MODE_TYPECHECK_NO_SCOPE = ModeTerm(typecheck=True, use_scope=False)
MODE_SAFE = syntax.Layers(layers=[MODE_EVALUATE, MODE_TYPECHECK])
MODE_LIFT = syntax.Layers(layers=[MODE_EVALUATE, MODE_EVALUATE_WITH_SCOPE])
SAFE_LAYER_COUNT = len(MODE_SAFE.layers)


@dataclasses.dataclass
class LayerOnly(syntax.Term):
    layer_index: int
    term: syntax.Term

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.term

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        separated = ls.build(lambda layer: layer(self.term))
        return [separated[i] if i == self.layer_index else syntax.Empty for i in range(ls.layer_count)]

    def unfold(self) -> syntax.Term:
        return self.term


def get_mode(*, typecheck: bool, use_scope: bool) -> ModeTerm:
    if typecheck:
        return MODE_TYPECHECK if use_scope else MODE_TYPECHECK_NO_SCOPE
    return MODE_EVALUATE_WITH_SCOPE if use_scope else MODE_EVALUATE


@dataclasses.dataclass
class TypedName(syntax.Term):
    id: Identifier
    ctx: str
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedName(id=self.id, ctx=self.ctx, mode=layer(self.mode), location=self.location)
        )

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.use_scope:
                return Attribute(
                    value=Name(id=lambda setting: setting.scope_name, ctx='load', location=self.location),
                    attr=self.id,
                    ctx=self.ctx,
                    location=self.location,
                )
            return Name(id=self.id, ctx=self.ctx, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedAssign(syntax.Term):
    target_name: syntax.Term
    target_type: syntax.Term
    value: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.target_name
        yield self.target_type
        yield self.value
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedAssign(
                target_name=layer(self.target_name),
                target_type=layer(self.target_type),
                value=layer(self.value),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Assign(
                targets=[self.target_name],
                value=self.value,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            expected = Assign(
                targets=[self.target_name],
                value=self.target_type,
                location=self.location,
            )
            assigned = Assign(targets=[self.target_name], value=self.value, location=self.location)
            return syntax.TermList(terms=[expected, assigned])
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class NoneLiteral(syntax.Term):
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(lambda layer: NoneLiteral(mode=layer(self.mode), location=self.location))

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.typecheck:
                return TypedName(id='NoneType', ctx='load', mode=self.mode, location=self.location)
            return Constant(value=None, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class BooleanLiteral(syntax.Term):
    value: bool
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: BooleanLiteral(
                value=self.value,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.typecheck:
                return TypedName(id='Bool', ctx='load', mode=self.mode, location=self.location)
            return Constant(value=self.value, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class IntegerLiteral(syntax.Term):
    value: int
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: IntegerLiteral(
                value=self.value,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.typecheck:
                return TypedName(id='Int', ctx='load', mode=self.mode, location=self.location)
            return Constant(value=self.value, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class FloatLiteral(syntax.Term):
    value: float
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: FloatLiteral(
                value=self.value,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.typecheck:
                return TypedName(id='Float', ctx='load', mode=self.mode, location=self.location)
            return Constant(value=self.value, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class StringLiteral(syntax.Term):
    value: str
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: StringLiteral(
                value=self.value,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if isinstance(self.mode, ModeTerm):
            if self.mode.typecheck:
                return TypedName(id='Str', ctx='load', mode=self.mode, location=self.location)
            return Constant(value=self.value, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedList(syntax.Term):
    elements: list[syntax.Term]
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.elements
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedList(
                elements=[layer(e) for e in self.elements],
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return List(elements=self.elements, ctx='load', location=self.location)
        if self.mode is MODE_TYPECHECK:
            return Call(
                func=Path(
                    names=['tapl_typing', 'create_typed_list'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=self.elements,
                keywords=[],
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedSet(syntax.Term):
    elements: list[syntax.Term]
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.elements
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedSet(
                elements=[layer(e) for e in self.elements],
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Set(
                elements=self.elements,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            return Call(
                func=Path(
                    names=['tapl_typing', 'create_typed_set'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=self.elements,
                keywords=[],
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedDict(syntax.Term):
    keys: list[syntax.Term]
    values: list[syntax.Term]
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.keys
        yield from self.values
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedDict(
                keys=[layer(k) for k in self.keys],
                values=[layer(v) for v in self.values],
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Dict(
                keys=self.keys,
                values=self.values,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            return Call(
                func=Path(
                    names=['tapl_typing', 'create_typed_dict'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[
                    List(
                        elements=self.keys,
                        ctx='load',
                        location=self.location,
                    ),
                    List(
                        elements=self.values,
                        ctx='load',
                        location=self.location,
                    ),
                ],
                keywords=[],
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class BoolNot(syntax.Term):
    operand: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.operand
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: BoolNot(operand=layer(self.operand), mode=layer(self.mode), location=self.location)
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return UnaryOp(
                op='not',
                operand=self.operand,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            # unary not operator always returns Bool type
            return TypedName(
                id='Bool',
                ctx='load',
                mode=MODE_TYPECHECK,
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedBoolOp(syntax.Term):
    operator: str
    values: list[syntax.Term]
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.values
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedBoolOp(
                operator=self.operator,
                values=[layer(v) for v in self.values],
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return BoolOp(operator=self.operator, values=self.values, location=self.location)
        if self.mode is MODE_TYPECHECK:
            return Call(
                func=Path(
                    names=['tapl_typing', 'create_union'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=self.values,
                keywords=[],
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedReturn(syntax.Term):
    value: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.value
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedReturn(value=layer(self.value), mode=layer(self.mode), location=self.location)
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Return(
                value=self.value,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            call = Call(
                func=Path(
                    names=['tapl_typing', 'add_return_type'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[
                    Name(
                        id=lambda setting: setting.scope_name,
                        ctx='load',
                        location=self.location,
                    ),
                    self.value,
                ],
                keywords=[],
                location=self.location,
            )
            return Expr(
                value=call,
                location=self.location,
            )
        raise tapl_error.UnhandledError


class ParamCategory(enum.Enum):
    POSITIONAL_ONLY = 'positional_only'
    REGULAR = 'regular'
    VAR_POSITIONAL = 'var_positional'
    KEYWORD_ONLY = 'keyword_only'
    VAR_KEYWORD = 'var_keyword'


@dataclasses.dataclass
class Parameter(syntax.Term):
    name: str
    type_: syntax.Term
    default: syntax.Term
    mode: syntax.Term
    category: ParamCategory
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.type_
        yield self.default
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: Parameter(
                name=self.name,
                type_=layer(self.type_),
                default=layer(self.default),
                mode=layer(self.mode),
                category=self.category,
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_TYPECHECK:
            return self.type_
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedFunctionDef(syntax.Term):
    name: str
    parameters: list[syntax.Term]
    return_type: syntax.Term
    body: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.parameters
        yield self.return_type
        yield self.body
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedFunctionDef(
                name=self.name,
                return_type=layer(self.return_type),
                parameters=[layer(p) for p in self.parameters],
                body=layer(self.body),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold_evaluate(self) -> syntax.Term:
        if not all(cast('Parameter', p).type_ is syntax.Empty for p in self.parameters):
            raise tapl_error.TaplError(
                'All parameter types must be a syntax.Empty when generating a function in evaluate mode.'
            )

        return FunctionDef(
            name=self.name,
            posonlyargs=[],
            args=[cast('Parameter', p).name for p in self.parameters],
            vararg=None,
            kwonlyargs=[],
            kw_defaults=[],
            kwarg=None,
            defaults=[],
            body=self.body,
            decorator_list=[],
            location=self.location,
        )

    def unfold_typecheck_main(self, *, is_method: bool = False) -> syntax.Term:
        def nested_scope(nested_term: syntax.Term) -> syntax.Term:
            return syntax.BackendSettingTerm(
                backend_setting_changer=syntax.BackendSettingChanger(
                    lambda setting: setting.clone(scope_level=setting.scope_level + 1)
                ),
                term=nested_term,
            )

        param_names = [cast('Parameter', p).name for p in self.parameters]

        keywords: list[tuple[str, syntax.Term]] = []
        keywords.append(
            (
                'parent__sa',
                Name(
                    id=lambda setting: setting.scope_name,
                    ctx='load',
                    location=self.location,
                ),
            )
        )
        keywords.extend(
            (
                name,
                Name(
                    id=name,
                    ctx='load',
                    location=self.location,
                ),
            )
            for name in param_names
        )
        new_scope = Assign(
            targets=[
                nested_scope(
                    Name(location=self.location, id=lambda setting: setting.scope_name, ctx='load'),
                )
            ],
            value=Call(
                func=Path(
                    names=['tapl_typing', 'create_scope'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[],
                keywords=keywords,
                location=self.location,
            ),
            location=self.location,
        )
        tmp_function: syntax.Term = syntax.Empty
        set_return_type: syntax.Term = syntax.Empty
        if self.return_type is not syntax.Empty:
            params = [cast('Parameter', p).type_ for p in self.parameters]
            if is_method:
                params = params[1:]  # skip 'self' parameter type
            tmp_function = Assign(
                location=self.location,
                targets=[
                    TypedName(
                        id=self.name,
                        ctx='store',
                        mode=self.mode,
                        location=self.location,
                    )
                ],
                value=Call(
                    func=Path(
                        names=['tapl_typing', 'create_function'],
                        ctx='load',
                        mode=MODE_TYPECHECK_NO_SCOPE,
                        location=self.location,
                    ),
                    args=[
                        List(
                            elements=params,
                            ctx='load',
                            location=self.location,
                        ),
                        self.return_type,
                    ],
                    keywords=[],
                    location=self.location,
                ),
            )
            set_return_type = Expr(
                value=Call(
                    func=Path(
                        names=['tapl_typing', 'set_return_type'],
                        ctx='load',
                        mode=MODE_TYPECHECK_NO_SCOPE,
                        location=self.location,
                    ),
                    args=[
                        Name(
                            id=lambda setting: setting.scope_name,
                            ctx='load',
                            location=self.location,
                        ),
                        self.return_type,
                    ],
                    keywords=[],
                    location=self.location,
                ),
                location=self.location,
            )

        get_return_type = Return(
            value=Call(
                func=Path(
                    names=['tapl_typing', 'get_return_type'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[
                    Name(
                        id=lambda setting: setting.scope_name,
                        ctx='load',
                        location=self.location,
                    )
                ],
                keywords=[],
                location=self.location,
            ),
            location=self.location,
        )

        return FunctionDef(
            name=self.name,
            posonlyargs=[],
            args=param_names,
            vararg=None,
            kwonlyargs=[],
            kw_defaults=[],
            kwarg=None,
            defaults=[],
            body=syntax.TermList(
                terms=[
                    new_scope,
                    nested_scope(syntax.TermList(terms=[tmp_function, set_return_type, self.body, get_return_type])),
                ]
            ),
            decorator_list=[],
            location=self.location,
        )

    def unfold_typecheck_type(self) -> syntax.Term:
        if self.parameters and all(cast('Parameter', p).type_ is syntax.Empty for p in self.parameters):
            return Assign(
                targets=[
                    TypedName(
                        id=self.name,
                        ctx='store',
                        mode=self.mode,
                        location=self.location,
                    )
                ],
                value=Name(
                    id=self.name,
                    ctx='load',
                    location=self.location,
                ),
                location=self.location,
            )
        if any(cast('Parameter', p).type_ is syntax.Empty for p in self.parameters):
            raise tapl_error.TaplError(
                'Parameter types must be either all specified or all omitted when generating a function type in typecheck mode.'
            )
        return Assign(
            targets=[
                TypedName(
                    id=self.name,
                    ctx='store',
                    mode=self.mode,
                    location=self.location,
                )
            ],
            value=Call(
                func=Path(
                    names=['tapl_typing', 'create_function'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[
                    List(
                        elements=[cast('Parameter', p).type_ for p in self.parameters],
                        ctx='load',
                        location=self.location,
                    ),
                    Call(
                        func=Name(
                            id=self.name,
                            ctx='load',
                            location=self.location,
                        ),
                        args=[cast('Parameter', p).type_ for p in self.parameters],
                        keywords=[],
                        location=self.location,
                    ),
                ],
                keywords=[],
                location=self.location,
            ),
            location=self.location,
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return self.unfold_evaluate()
        if self.mode is MODE_TYPECHECK:
            return syntax.TermList(terms=[self.unfold_typecheck_main(is_method=False), self.unfold_typecheck_type()])
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedIf(syntax.Term):
    test: syntax.Term
    body: syntax.Term
    elifs: list[tuple[syntax.Term, syntax.Term]]  # (test, body)
    orelse: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.test
        yield self.body
        for test, body in self.elifs:
            yield test
            yield body
        yield self.orelse
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedIf(
                test=layer(self.test),
                body=layer(self.body),
                elifs=[(layer(test), layer(body)) for test, body in self.elifs],
                orelse=layer(self.orelse),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def codegen_evaluate(self) -> syntax.Term:
        return If(
            test=self.test,
            body=self.body,
            orelse=self.orelse,
            location=self.location,
        )

    def codegen_typecheck(self) -> syntax.Term:
        true_side = syntax.TermList(
            terms=[
                Expr(
                    value=self.test,
                    location=self.location,
                ),
                self.body,
            ]
        )
        return BranchTyping(branches=[true_side, self.orelse], location=self.location)

    def unfold(self) -> syntax.Term:
        # FIXME: handle elifs
        if self.elifs:
            raise tapl_error.UnhandledError('Elif clauses are not yet supported in TypedIf unfold.')
        if self.mode is MODE_EVALUATE:
            return self.codegen_evaluate()
        if self.mode is MODE_TYPECHECK:
            return self.codegen_typecheck()
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class ElifSibling(syntax.SiblingTerm):
    test: syntax.Term
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.test
        yield self.body

    def integrate_into(self, previous_siblings: list[syntax.Term]) -> None:
        term = previous_siblings[-1]
        if isinstance(term, syntax.ErrorTerm):
            return
        if not isinstance(term, TypedIf):
            error = syntax.ErrorTerm('Elif can only be integrated into If.' + repr(term), location=self.location)
            previous_siblings.append(error)
        else:
            term.elifs.append((self.test, self.body))


@dataclasses.dataclass
class ElseSibling(syntax.SiblingTerm):
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.body

    def integrate_into(self, previous_siblings: list[syntax.Term]) -> None:
        term = previous_siblings[-1]
        if isinstance(term, syntax.ErrorTerm):
            return
        if not isinstance(term, TypedIf):
            error = syntax.ErrorTerm('Else can only be integrated into If.' + repr(term), location=self.location)
            previous_siblings.append(error)
        elif term.orelse is not syntax.Empty:
            error = syntax.ErrorTerm('An If statement can only have one Else clause.', location=self.location)
            previous_siblings.append(error)
        else:
            term.orelse = self.body


@dataclasses.dataclass
class TypedWhile(syntax.Term):
    test: syntax.Term
    body: syntax.Term
    orelse: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.test
        yield self.body
        yield self.orelse
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedWhile(
                test=layer(self.test),
                body=layer(self.body),
                orelse=layer(self.orelse),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def codegen_evaluate(self) -> syntax.Term:
        return While(
            test=self.test,
            body=self.body,
            orelse=self.orelse,
            location=self.location,
        )

    def codegen_typecheck(self) -> syntax.Term:
        return TypedIf(
            test=self.test,
            body=self.body,
            elifs=[],
            orelse=self.orelse,
            mode=self.mode,
            location=self.location,
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return self.codegen_evaluate()
        if self.mode is MODE_TYPECHECK:
            return self.codegen_typecheck()
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedFor(syntax.Term):
    target: syntax.Term
    iter: syntax.Term
    body: syntax.Term
    orelse: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.target
        yield self.iter
        yield self.body
        yield self.orelse
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedFor(
                target=layer(self.target),
                iter=layer(self.iter),
                body=layer(self.body),
                orelse=layer(self.orelse),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def codegen_evaluate(self) -> syntax.Term:
        return For(
            target=self.target,
            iter=self.iter,
            body=self.body,
            orelse=self.orelse,
            location=self.location,
        )

    def codegen_typecheck(self) -> syntax.Term:
        iterator_type = Call(
            func=Attribute(
                value=self.iter,
                attr='__iter__',
                ctx='load',
                location=self.location,
            ),
            args=[],
            keywords=[],
            location=self.location,
        )
        item_type = Call(
            location=self.location,
            func=Attribute(
                value=iterator_type,
                attr='__next__',
                ctx='load',
                location=self.location,
            ),
            args=[],
            keywords=[],
        )
        assign_target = Assign(
            targets=[self.target],
            value=item_type,
            location=self.location,
        )
        for_branch = syntax.TermList(terms=[assign_target, self.body])
        return BranchTyping(
            branches=[for_branch, self.orelse],
            location=self.location,
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return self.codegen_evaluate()
        if self.mode is MODE_TYPECHECK:
            return self.codegen_typecheck()
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedTry(syntax.Term):
    body: syntax.Term
    handlers: list[syntax.Term]
    finalbody: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.body
        yield from self.handlers
        yield self.finalbody
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedTry(
                body=layer(self.body),
                handlers=[layer(h) for h in self.handlers],
                finalbody=layer(self.finalbody),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Try(
                body=self.body,
                handlers=self.handlers,
                orelse=syntax.Empty,
                finalbody=self.finalbody,
                location=self.location,
            )
        if self.mode is MODE_TYPECHECK:
            # FIXME: Implement scope forker to preserve parent scope in try, except, and finally blocks except return type
            handlers: list[syntax.Term] = [self.body]
            for handler in self.handlers:
                if not isinstance(handler, ExceptHandler):
                    raise tapl_error.TaplError('TypedTry handlers must be ExceptHandler in type-check mode.')
                exception_name: syntax.Term = syntax.Empty
                if handler.name is not None:
                    exception_name = Assign(
                        targets=[Name(id=handler.name, ctx='store', location=self.location)],
                        value=handler.exception_type,
                        location=self.location,
                    )
                handlers.append(syntax.TermList(terms=[exception_name, handler.body]))
            handlers.append(self.finalbody)
            return BranchTyping(branches=handlers, location=self.location)
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class ExceptSibling(syntax.SiblingTerm):
    exception_type: syntax.Term
    name: str | None
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.exception_type
        yield self.body

    def integrate_into(self, previous_siblings: list[syntax.Term]) -> None:
        term = previous_siblings[-1]
        if isinstance(term, syntax.ErrorTerm):
            return
        if not isinstance(term, TypedTry):
            error = syntax.ErrorTerm(
                'Except can only be integrated into typed Try.' + repr(term), location=self.location
            )
            previous_siblings.append(error)
        else:
            handler = ExceptHandler(
                exception_type=self.exception_type,
                name=self.name,
                body=self.body,
                location=self.location,
            )
            term.handlers.append(handler)


@dataclasses.dataclass
class FinallySibling(syntax.SiblingTerm):
    body: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.body

    def integrate_into(self, previous_siblings: list[syntax.Term]) -> None:
        term = previous_siblings[-1]
        if isinstance(term, syntax.ErrorTerm):
            return
        if not isinstance(term, TypedTry):
            error = syntax.ErrorTerm(
                'Finally can only be integrated into typed Try.' + repr(term), location=self.location
            )
            previous_siblings.append(error)
        elif term.finalbody is not syntax.Empty:
            error = syntax.ErrorTerm('A Try statement can only have one Finally clause.', location=self.location)
            previous_siblings.append(error)
        else:
            term.finalbody = self.body


@dataclasses.dataclass
class TypedImport(syntax.Term):
    names: list[Alias]
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedImport(
                names=self.names,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return Import(location=self.location, names=self.names)
        if self.mode is MODE_TYPECHECK:
            if len(self.names) > 1:
                raise tapl_error.TaplError(
                    'Import does not support multiple names yet.'
                )  # FIXME: Support multiple import names
            return Expr(
                value=Call(
                    location=self.location,
                    func=Path(
                        names=['tapl_typing', 'import_module'],
                        ctx='load',
                        mode=MODE_TYPECHECK_NO_SCOPE,
                        location=self.location,
                    ),
                    args=[
                        Name(
                            id=lambda setting: setting.scope_name,
                            ctx='load',
                            location=self.location,
                        ),
                        List(
                            elements=[Constant(location=self.location, value=self.names[0].name)],
                            ctx='load',
                            location=self.location,
                        ),
                    ],
                    keywords=[],
                ),
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedImportFrom(syntax.Term):
    module: str | None
    names: list[Alias]
    level: int
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield self.mode

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedImportFrom(
                module=self.module,
                names=self.names,
                level=self.level,
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return ImportFrom(module=self.module, names=self.names, level=self.level, location=self.location)
        if self.mode is MODE_TYPECHECK:
            return Expr(
                value=Call(
                    location=self.location,
                    func=Path(
                        names=['tapl_typing', 'import_module'],
                        ctx='load',
                        mode=MODE_TYPECHECK_NO_SCOPE,
                        location=self.location,
                    ),
                    args=[
                        Name(
                            id=lambda setting: setting.scope_name,
                            ctx='load',
                            location=self.location,
                        ),
                        List(
                            elements=[Constant(location=self.location, value=name.name) for name in self.names],
                            ctx='load',
                            location=self.location,
                        ),
                        Constant(location=self.location, value=self.module),
                        Constant(location=self.location, value=self.level),
                    ],
                    keywords=[],
                ),
                location=self.location,
            )
        raise tapl_error.UnhandledError


@dataclasses.dataclass
class TypedClassDef(syntax.Term):
    name: str
    bases: list[syntax.Term]
    body: syntax.Term
    mode: syntax.Term
    location: syntax.Location

    def children(self) -> Generator[syntax.Term, None, None]:
        yield from self.bases
        yield self.body

    def separate(self, ls: syntax.LayerSeparator) -> list[syntax.Term]:
        return ls.build(
            lambda layer: TypedClassDef(
                name=self.name,
                bases=[layer(b) for b in self.bases],
                body=layer(self.body),
                mode=layer(self.mode),
                location=self.location,
            )
        )

    def codegen_evaluate(self) -> syntax.Term:
        return ClassDef(
            name=self.name,
            bases=self.bases,
            keywords=[],
            body=self.body,
            decorator_list=[],
            location=self.location,
        )

    def codegen_typecheck(self) -> syntax.Term:
        body = []
        methods: list[TypedFunctionDef] = []
        constructor_args = []
        for item in self.body.children():
            if isinstance(item, TypedFunctionDef):
                if item.name == '__init__':
                    constructor_args = item.parameters[1:]
                else:
                    methods.append(item)
                body.append(item.unfold_typecheck_main(is_method=True))
            elif isinstance(item, Assign):
                targets: list[syntax.Term] = []
                for target in item.targets:
                    if not isinstance(target, TypedName):
                        raise tapl_error.TaplError(
                            'Class variable assignments must use TypedName as target in type-check mode.'
                        )
                    targets.append(
                        TypedName(
                            id=target.id,
                            ctx='store',
                            mode=MODE_TYPECHECK_NO_SCOPE,
                            location=target.location,
                        )
                    )
                body.append(Assign(targets=targets, value=item.value, location=item.location))
            else:
                body.append(item)
        class_stmt = ClassDef(
            name=self.name,
            bases=self.bases,
            keywords=[],
            body=syntax.TermList(terms=body),
            decorator_list=[],
            location=self.location,
        )

        method_types: list[syntax.Term] = []
        for method in methods:
            # The first parameter is the instance itself, so we can set it to the instance type.
            if not (
                len(method.parameters) >= 1
                and isinstance(method.parameters[0], Parameter)
                and method.parameters[0].type_ is syntax.Empty
            ):
                raise tapl_error.TaplError(
                    f'First parameter of method {method.name} in class {self.name} must be self with no type annotation.'
                )
            tail_args = method.parameters[1:]
            method_types.append(
                Tuple(
                    elements=[
                        Constant(
                            value=method.name,
                            location=self.location,
                        ),
                        List(
                            elements=tail_args,
                            ctx='load',
                            location=self.location,
                        ),
                    ],
                    ctx='load',
                    location=self.location,
                )
            )

        create_class = Assign(
            targets=[
                TypedName(
                    id=self.name,
                    ctx='store',
                    mode=self.mode,
                    location=self.location,
                ),
            ],
            value=Call(
                func=Path(
                    names=['tapl_typing', 'create_class'],
                    ctx='load',
                    mode=MODE_TYPECHECK_NO_SCOPE,
                    location=self.location,
                ),
                args=[],
                keywords=[
                    (
                        'cls',
                        Name(
                            id=self.name,
                            ctx='load',
                            location=self.location,
                        ),
                    ),
                    (
                        'init_args',
                        List(
                            elements=constructor_args,
                            ctx='load',
                            location=self.location,
                        ),
                    ),
                    (
                        'methods',
                        List(
                            elements=method_types,
                            ctx='load',
                            location=self.location,
                        ),
                    ),
                ],
                location=self.location,
            ),
            location=self.location,
        )

        return syntax.TermList(terms=[class_stmt, create_class])

    def unfold(self) -> syntax.Term:
        if self.mode is MODE_EVALUATE:
            return self.codegen_evaluate()
        if self.mode is MODE_TYPECHECK:
            return self.codegen_typecheck()
        raise tapl_error.UnhandledError
