# pylint: disable=too-many-lines
# pylint: disable=too-many-branches
# pylint: disable=too-many-return-statements
"""
The parser is the most important and alrgest piece of the Eikobot Compiler.
It takes tokens generated by the lexer and tries to make sense of them,
returning them as Abstract Syntax Tree Expressions.
These ExprASTs in turn can be compiled down to Eikobot data.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator, Optional, Tuple, Union

from .. import logger
from ..errors import (
    EikoCompilationError,
    EikoInternalError,
    EikoParserError,
    EikoSyntaxError,
)
from ._token import Token, TokenType, token_to_char
from .decorator import EikoDecorator
from .definitions._resource import (
    EikoPromiseDefinition,
    EikoResourceDefinition,
    ResourceProperty,
)
from .definitions.base_types import (
    EikoBaseType,
    EikoBool,
    EikoBoolType,
    EikoBuiltinFunction,
    EikoBuiltinTypes,
    EikoDict,
    EikoDictType,
    EikoEnumDefinition,
    EikoEnumValue,
    EikoFloat,
    EikoFloatType,
    EikoInt,
    EikoIntType,
    EikoList,
    EikoListType,
    EikoNoneType,
    EikoObjectType,
    EikoOptional,
    EikoPromise,
    EikoResource,
    EikoStr,
    EikoStrType,
    EikoType,
    EikoUnion,
    EikoUnset,
    eiko_none_object,
    type_list_to_type,
)
from .definitions.context import CompilerContext, LazyLoadModule, StorableTypes
from .definitions.function import (
    ConstructorArg,
    ConstructorDefinition,
    PassedArg,
    PluginDefinition,
)
from .definitions.typedef import EikoTypeDef
from .importlib import (
    Module,
    import_python_code,
    resolve_from_import,
    resolve_import,
)
from .lexer import Lexer
from .misc import Index
from .ops import BINOP_MATRIX, BinOP, ComparisonOP, compare


@dataclass
class ExprAST:
    """Base ExprAST. Purely Virtual."""

    token: Token

    def __post_init__(self) -> None:
        self.import_context: Optional[CompilerContext] = None

    def compile(self, context: CompilerContext) -> Optional[StorableTypes]:
        raise NotImplementedError(self.token)


@dataclass
class EOFExprAST(ExprAST):
    """This ExprAST marks end of parsing."""

    def compile(self, _: CompilerContext) -> None:
        raise NotImplementedError


@dataclass
class IntExprAST(ExprAST):
    """AST representing an integer in the source"""

    def __post_init__(self) -> None:
        self.value = int(self.token.content)

    def compile(self, _: CompilerContext) -> EikoInt:
        return EikoInt(self.value)


@dataclass
class FloatExprAST(ExprAST):
    """AST representing a float in the source."""

    def __post_init__(self) -> None:
        self.value = float(self.token.content)

    def compile(self, _: CompilerContext) -> EikoFloat:
        return EikoFloat(self.value)


@dataclass
class BoolExprAST(ExprAST):
    """AST representing a bool in the source."""

    def __post_init__(self) -> None:
        if self.token.type == TokenType.FALSE:
            self.value = False
        elif self.token.type == TokenType.TRUE:
            self.value = True
        else:
            raise EikoInternalError(
                "Error occured trying to compile BoolExprAST. "
                "This is deffinetly a bug, please report it on github."
            )

    def compile(self, _: CompilerContext) -> EikoBool:
        return EikoBool(self.value)


@dataclass
class StringExprAST(ExprAST):
    """AST representing a string in the source."""

    def __post_init__(self) -> None:
        self.value = self.token.content

    def compile(self, _: CompilerContext) -> EikoStr:
        return EikoStr(self.value)


class FStringLexer(Lexer):
    """
    The FStringLexer replaces the normal lexer,
    when lexing f-strings.
    """

    # pylint: disable=super-init-not-called
    def __init__(self, token: Token) -> None:
        self._str_token = token
        self._content = token.content
        self._index = token.index
        self.file_path = token.index.file

        self._lexing_f_tokens = False
        self.expressions: list[str] = []

        self._char_index = 0
        self._line = 0
        self._col = 0
        self._current_line = 0
        self._current = ""

    def _record_expression(self) -> None:
        expression = self._current
        char_index = self._char_index - 1
        while True:
            char_index -= 1
            prev_char = self._content[char_index]
            expression = self._content[char_index] + expression
            if prev_char == "{":
                break

        self.expressions.append(expression)

    def next_token(self) -> Token:
        if self._current == "\\" and not self._lexing_f_tokens:
            self._next()
            if self._current == "{":
                self._next()

        if self._current == "{":
            self._next()
            self._lexing_f_tokens = True
            return Token(TokenType.INDENT, "", self._current_index())

        if self._lexing_f_tokens:
            if self._current == "}":
                self._record_expression()
                self._next()
                self._lexing_f_tokens = False
            else:
                if self._current == "EOF":
                    raise EikoParserError(
                        "Reached end of f-string while parsing expression.",
                        token=self._str_token,
                    )
                return super().next_token()

        if not self._lexing_f_tokens:
            if self._current == "EOF":
                return super().next_token()

            self._next()
            return self.next_token()

        self._str_token.index = self._current_index()
        raise EikoParserError(
            "Failed to parse f-string.",
            token=self._str_token,
        )


@dataclass
class FStringExprAST(ExprAST):
    """
    An f-string expression is a string that contains sub-expressions.
    These expressions are seperated out at parse time and
    filled in at compile time.
    """

    def __post_init__(self) -> None:
        self.string = self.token.content
        self.expressions: dict[str, ExprAST] = {}

        # Find a way to not read the whole file again.
        parser = Parser(self.token.index.file)
        parser.lexer = FStringLexer(self.token)
        parser._current = parser.lexer.next_token()  # pylint: disable=protected-access
        parser._next = parser.lexer.next_token()  # pylint: disable=protected-access
        for index, expr in enumerate(parser.parse()):
            self.expressions[parser.lexer.expressions[index]] = expr

    def compile(self, context: CompilerContext) -> EikoStr:
        string = EikoStr(self.token.content)
        for raw_expr, expr in self.expressions.items():
            result = expr.compile(context)
            if result is None:
                raise EikoCompilationError(
                    "Tried to interpolate string, but expression did not result in a value",
                    token=self.token,
                )

            if not isinstance(result, (EikoBool, EikoFloat, EikoInt, EikoStr)):
                raise EikoCompilationError(
                    f"Unable to convert object of type {result.type} to a string "
                    "and interpolate it with an f-string. (Only bool, int, float and str are allowed.)",
                    token=self.token,
                )

            string.value = string.value.replace(raw_expr, str(result.value))

        return string


@dataclass
class UnaryNotExprAST(ExprAST):
    """AST representing a unary not operation."""

    rhs: ExprAST

    def compile(self, context: CompilerContext) -> EikoBaseType:
        rhs = self.rhs.compile(context)
        if isinstance(rhs, EikoBaseType):
            return EikoBool((not rhs.truthiness()))

        raise EikoCompilationError(
            "Bad value given to not operator.",
            token=self.rhs.token,
        )


@dataclass
class UnaryNegExprAST(ExprAST):
    """AST representing a unary negative operation."""

    rhs: ExprAST

    def compile(self, context: CompilerContext) -> Union[EikoInt, EikoFloat]:
        rhs = self.rhs.compile(context)
        if isinstance(rhs, EikoInt):
            return EikoInt(-rhs.value)

        if isinstance(rhs, EikoFloat):
            return EikoFloat(-rhs.value)

        raise EikoCompilationError(
            "Bad value given to unary negative exression.",
            token=self.rhs.token,
        )


@dataclass
class BinOpExprAST(ExprAST):
    """A binary operation taking a left and right hand side."""

    lhs: ExprAST
    rhs: ExprAST

    def __post_init__(self) -> None:
        self.bin_op = BinOP.from_str(self.token.content, self.token)

    def compile(self, context: CompilerContext) -> EikoBaseType:
        lhs = self.lhs.compile(context)
        rhs = self.rhs.compile(context)
        if lhs is None or rhs is None:
            raise EikoCompilationError(
                "Binary operation expected value but expression didn't return a value.",
                token=self.token,
            )

        if isinstance(lhs, EikoPromise):
            raise EikoCompilationError(
                "Binary operations cannot be used with promises.",
                token=self.lhs.token,
            )

        if isinstance(rhs, EikoPromise):
            raise EikoCompilationError(
                "Binary operations cannot be used with promises.",
                token=self.rhs.token,
            )

        arg_a_matrix = BINOP_MATRIX.get(lhs.type.get_top_level_type().name)
        if arg_a_matrix is None:
            raise EikoCompilationError(
                f"No overload of operation '{self.bin_op}' for arguments "
                f"of types {lhs.type} and {rhs.type} available.",
                token=self.token,
            )

        arg_b_matrix = arg_a_matrix.get(rhs.type.get_top_level_type().name)
        if arg_b_matrix is None:
            raise EikoCompilationError(
                f"No overload of operation {self.bin_op} for arguments"
                f"of types {lhs.type} and {rhs.type} available.",
                token=self.token,
            )

        op = arg_b_matrix.get(self.bin_op)
        if op is None:
            raise EikoCompilationError(
                f"No overload of operation {self.bin_op} for arguments"
                f"of types {lhs.type} and {rhs.type} available.",
                token=self.token,
            )

        # This needs to be cleaned up at some point, in some way,
        # but due to the way we retrieve 'op' from dictionaries,
        # mypy doesn't seem to know if this is valid.
        return op(lhs, rhs)  # type: ignore


@dataclass
class ComparisonExprAST(ExprAST):
    """An AST expressing a comparison."""

    lhs: ExprAST
    rhs: ExprAST

    def __post_init__(self) -> None:
        self.bin_op = ComparisonOP.from_str(self.token)

    def compile(self, context: CompilerContext) -> EikoBool:
        lhs = self.lhs.compile(context)
        rhs = self.rhs.compile(context)
        if lhs is None or rhs is None:
            raise EikoCompilationError(
                "Binary operation expected value but expression didn't return a value.",
                token=self.token,
            )

        if isinstance(lhs, (type, CompilerContext, EikoType)):
            raise EikoInternalError(
                "Something went horribly wrong, please submit a bug report.",
                token=self.lhs.token,
            )

        if isinstance(rhs, (type, CompilerContext, EikoType)):
            raise EikoInternalError(
                "Something went horribly wrong, please submit a bug report.",
                token=self.rhs.token,
            )

        return compare(lhs, rhs, self.bin_op, self.rhs.token)


@dataclass
class OrExprAST(ExprAST):
    """Expresses an OR operator."""

    lhs: ExprAST
    rhs: ExprAST

    def compile(self, context: CompilerContext) -> EikoBool:
        lhs = self.lhs.compile(context)

        if isinstance(lhs, EikoBaseType):
            rhs = self.rhs.compile(context)
            if isinstance(rhs, EikoBaseType):
                return EikoBool(lhs.truthiness() or rhs.truthiness())

            raise EikoCompilationError(
                "Expression did not result in a value usable by 'OR' operator",
                token=self.rhs.token,
            )

        raise EikoCompilationError(
            "Expression did not result in a value usable by 'OR' operator",
            token=self.lhs.token,
        )


@dataclass
class AndExprAST(ExprAST):
    """Expresses an AND operator."""

    lhs: ExprAST
    rhs: ExprAST

    def compile(self, context: CompilerContext) -> EikoBool:
        lhs = self.lhs.compile(context)

        if isinstance(lhs, EikoBaseType):
            rhs = self.rhs.compile(context)
            if isinstance(rhs, EikoBaseType):
                return EikoBool((lhs.truthiness() and rhs.truthiness()))

            raise EikoCompilationError(
                "Expression did not result in a value usable by 'AND' operator",
                token=self.rhs.token,
            )

        raise EikoCompilationError(
            "Expression did not result in a value usable by 'AND' operator",
            token=self.lhs.token,
        )


@dataclass
class VariableExprAST(ExprAST):
    """An AST expressing a variable of some kind."""

    type_expr: Optional["TypeExprAST"] = None

    def __post_init__(self) -> None:
        if self.token.type != TokenType.IDENTIFIER:
            raise EikoSyntaxError(
                "Expected an identifier.",
                index=self.token.index,
            )
        self.identifier = self.token.content

    def assign(
        self,
        value: StorableTypes,
        assign_context: CompilerContext | EikoResource,
        compile_context: CompilerContext,
        token: Token,
    ) -> StorableTypes:
        """Assigns the variable a value."""
        type_expr: EikoType | None = None
        prev_def: EikoBaseType | EikoUnset | None = None
        if isinstance(assign_context, EikoResource):
            prev_def = assign_context.properties.get(self.identifier)

        elif isinstance(assign_context, CompilerContext):
            _prev_def = assign_context.shallow_get(self.identifier)
            if isinstance(_prev_def, (EikoBaseType, EikoUnset)):
                prev_def = _prev_def

        if self.type_expr is not None:
            type_expr = self.type_expr.compile(compile_context)
            if type_expr.inverse_type_check(value.type):
                type_constr = compile_context.get(type_expr.name)
                if isinstance(type_constr, EikoTypeDef):
                    if isinstance(value, EikoBaseType):
                        value = type_constr.execute(value, token)
                    else:
                        raise EikoInternalError(
                            "Something went wrong trying to coerce a type to it's typedef.",
                            token=token,
                        )

            if prev_def is not None and not prev_def.type.type_check(type_expr):
                raise EikoCompilationError(
                    "Reassinging types is not allowed.",
                    token=self.token,
                )

        elif prev_def is not None:
            type_expr = prev_def.type

        if type_expr is not None:
            if (
                isinstance(value, EikoList)
                and value.type.element_type.name == ""
                and isinstance(type_expr, EikoListType)
            ):
                value.update_typing(type_expr)

            elif (
                isinstance(value, EikoDict)
                and value.type.key_type.name == ""
                and value.type.value_type.name == ""
                and isinstance(type_expr, EikoDictType)
            ):
                value.update_typing(type_expr)

            elif not value.type.type_check(type_expr):
                if value.type.inverse_type_check(type_expr):
                    if type_expr.typedef is None:
                        raise EikoCompilationError(
                            "A coercion failed. Type is not a TypeDef.",
                            token=token,
                        )
                    if isinstance(value, EikoBaseType):
                        value = type_expr.typedef.execute(value, token)
                    else:
                        raise EikoInternalError(
                            "A coercion failed due to a bug. Please report this on Github.",
                            token=token,
                        )
                else:
                    raise EikoCompilationError(
                        "Variable assigned incompatible type:"
                        f" given value of type '{value.type}' but expected type '{type_expr}'",
                        token=token,
                    )

            else:
                if isinstance(value, EikoList) and isinstance(type_expr, EikoListType):
                    value.update_typing(type_expr)

                if isinstance(value, EikoDict) and isinstance(type_expr, EikoDictType):
                    value.update_typing(type_expr)

        elif isinstance(value, EikoList) and len(value.elements) == 0:
            raise EikoCompilationError(
                "A list must be given a type expressing or initial values.",
                token=self.token,
            )

        elif isinstance(value, EikoDict) and len(value.elements) == 0:
            raise EikoCompilationError(
                "A dictionary must be given a type expressing or initial values.",
                token=self.token,
            )

        assign_context.set(self.identifier, value, self.token)
        return value

    def compile(
        self, context: Union[CompilerContext, EikoBaseType]
    ) -> Optional[StorableTypes]:
        value = context.get(self.identifier, self.token)
        if value is None:
            if self.type_expr is None:
                raise EikoCompilationError(
                    f"Unable to resolve identifier '{self.identifier}'.",
                    token=self.token,
                )

            if isinstance(context, EikoBaseType):
                raise EikoInternalError(
                    "Got in trouble trying to resolve an identifier. "
                    "Please report this error.",
                    token=self.token,
                )

            context.set(self.identifier, EikoUnset(self.type_expr.compile(context)))
            return None

        if self.type_expr is not None:
            raise EikoCompilationError(
                "Tried to reassign type of a variable that was already declared.",
                token=self.token,
            )

        return value

    def get_identifier_token(self) -> Token:
        return self.token


@dataclass
class AssignmentExprAST(ExprAST):
    """An AST expressing an assignment of some kind."""

    lhs: ExprAST
    rhs: ExprAST

    def compile(self, context: CompilerContext) -> StorableTypes:
        assignment_val = self.rhs.compile(context)
        if assignment_val is None:
            raise EikoCompilationError(
                "Assignment operation expected value on right hand side, "
                "but expression didn't return a usable value.",
                token=self.rhs.token,
            )

        if isinstance(self.lhs, (DotExprAST, VariableExprAST, IndexExprAst)):
            return self.lhs.assign(assignment_val, context, context, self.rhs.token)

        raise EikoCompilationError(
            "Assignment operation expected assignable variable on left hand side",
            token=self.lhs.token,
        )


@dataclass
class DotExprAST(ExprAST):
    """
    An AST expressing a dot expression. eg: 'thing.property'
    """

    lhs: Union["DotExprAST", VariableExprAST, "IndexExprAst", "CallExprAst"]
    rhs: VariableExprAST

    def __post_init__(self) -> None:
        self.identifier: str = f"{self.lhs.identifier}.{self.rhs.identifier}"

    def compile(
        self, context: Union[CompilerContext, EikoBaseType, EikoResourceDefinition]
    ) -> Optional[StorableTypes]:
        if isinstance(context, CompilerContext):
            lhs = self.lhs.compile(context)
        elif isinstance(self.lhs, IndexExprAst):
            raise EikoInternalError(
                "Something went wrong parsing a dot expression. Report this bug.",
                token=self.token,
            )

        if isinstance(lhs, (EikoBaseType, EikoResourceDefinition, CompilerContext)):
            return self.rhs.compile(lhs)

        raise EikoCompilationError(
            "Invalid dot expression.",
            token=self.lhs.token,
        )

    def assign(
        self,
        value: StorableTypes,
        context: CompilerContext,
        _: CompilerContext,
        value_token: Token,
    ) -> StorableTypes:
        """Assign this dot expression a value of some kind."""
        lhs = self.lhs.compile(context)
        if isinstance(lhs, (EikoResource, CompilerContext)):
            self.rhs.assign(value, lhs, context, value_token)
            return value

        raise EikoCompilationError(
            f"Unable to assign a value to {self.identifier}.",
            token=self.lhs.token,
        )

    def to_import_traversable_list(self, import_list: list[str]) -> None:
        """
        Returns this dot expression as a list that can be used by the import system.
        """
        if isinstance(self.lhs, VariableExprAST):
            import_list.append(self.lhs.identifier)
        else:
            raise EikoCompilationError(
                "Unexpected token in import statement.",
                token=self.lhs.token,
            )

        import_list.append(self.rhs.identifier)

    def get_identifier_token(self) -> Token:
        return self.rhs.get_identifier_token()


@dataclass
class ListExprAST(ExprAST):
    """A list of some type."""

    elements: list[ExprAST]

    def compile(self, context: CompilerContext) -> Optional[StorableTypes]:
        if len(self.elements) == 1 and self.token.type == TokenType.LEFT_PAREN:
            return self.elements[0].compile(context)

        if self.token.type == TokenType.LEFT_SQ_BRACKET:
            _elements: list[EikoBaseType] = []
            _types: list[EikoType] = []
            for expr in self.elements:
                element = expr.compile(context)
                if not isinstance(element, EikoBaseType):
                    raise EikoCompilationError(
                        "Invalid expression when trying to compose list.",
                        token=expr.token,
                    )
                _elements.append(element)
                if element.type not in _types:
                    _types.append(element.type)

            _type = type_list_to_type(_types)

            return EikoList(_type, _elements)

        raise NotImplementedError


@dataclass
class CallExprAst(ExprAST):
    """An AST expressing a resource-constructor or plugin."""

    identifier_expr: Union[DotExprAST, VariableExprAST]
    args: ListExprAST

    def __post_init__(self) -> None:
        self.identifier = self.identifier_expr.identifier

    # pylint: disable=too-many-statements
    def compile(self, context: CompilerContext) -> Optional[EikoBaseType]:
        eiko_callable = self.identifier_expr.compile(context)

        if isinstance(eiko_callable, EikoResourceDefinition):
            eiko_callable = eiko_callable.default_constructor

        args: list[PassedArg] = []
        keyword_args: dict[str, PassedArg] = {}
        if isinstance(eiko_callable, ConstructorDefinition):
            kw_args = False
            for arg_expr in self.args.elements:
                if isinstance(arg_expr, AssignmentExprAST):
                    kw_args = True
                    if not isinstance(arg_expr.lhs, VariableExprAST):
                        raise EikoCompilationError(
                            "Expected an identifier on the left hand side of keyword argument.",
                            token=arg_expr.lhs.token,
                        )
                    kw_arg_value = arg_expr.rhs.compile(context)
                    if not isinstance(kw_arg_value, EikoBaseType):
                        raise EikoCompilationError(
                            "The provided argument did not provide a valid value.",
                            token=arg_expr.rhs.token,
                        )
                    keyword_args[arg_expr.lhs.identifier] = PassedArg(
                        arg_expr.rhs.token, kw_arg_value
                    )
                else:
                    if kw_args:
                        raise EikoCompilationError(
                            "Positional arguments cannot come after keyword arguments.",
                            token=arg_expr.token,
                        )
                    arg_value = arg_expr.compile(context)
                    if not isinstance(arg_value, EikoBaseType):
                        raise EikoCompilationError(
                            "The provided argument did not provide a valid value.",
                            token=arg_expr.token,
                        )
                    args.append(PassedArg(arg_expr.token, arg_value))

            return eiko_callable.execute(self.token, args, keyword_args)

        if isinstance(eiko_callable, PluginDefinition):
            plugin_context = CompilerContext(
                f"{self.identifier}-plugin-call-context",
                context._context_cache,  # pylint: disable=protected-access
                context,
            )
            return eiko_callable.execute(
                self.args.elements, plugin_context, token=self.token
            )

        if isinstance(eiko_callable, EikoTypeDef):
            if len(self.args.elements) != 1:
                raise EikoCompilationError(
                    f"{eiko_callable.name} takes exactly 1 argument.",
                    token=self.token,
                )

            arg = self.args.elements[0]
            compiled_arg = arg.compile(context)
            if isinstance(compiled_arg, EikoBaseType):
                return eiko_callable.execute(compiled_arg, arg.token)

            raise EikoInternalError(
                "Not sure what happened here. This is probably a bug",
                token=self.token,
            )

        if isinstance(eiko_callable, EikoBuiltinFunction):
            parsing_kw = False
            for arg_expr in self.args.elements:
                if isinstance(arg_expr, AssignmentExprAST):
                    parsing_kw = True
                    arg_value = arg_expr.rhs.compile(context)
                    if not isinstance(arg_value, EikoBaseType):
                        raise EikoCompilationError(
                            "The provided argument is not a valid value.",
                            token=arg_expr.token,
                        )
                    kw_arg = PassedArg(arg_expr.lhs.token, arg_value)
                    keyword_args[kw_arg.token.content] = kw_arg
                else:
                    if parsing_kw:
                        raise EikoCompilationError(
                            "A positional argument cannot follow a keyword argument.",
                            token=arg_expr.token,
                        )
                    arg_value = arg_expr.compile(context)
                    if not isinstance(arg_value, EikoBaseType):
                        raise EikoCompilationError(
                            "The provided argument is not a valid value.",
                            token=arg_expr.token,
                        )
                    args.append(PassedArg(arg_expr.token, arg_value))

            return eiko_callable.execute(self.token, args, keyword_args)

        if isinstance(eiko_callable, EikoEnumDefinition):
            if len(self.args.elements) != 1:
                raise EikoCompilationError(
                    f"Enum '{eiko_callable.name}' takes exactly 1 argument.",
                    token=self.token,
                )

            compiled_arg = self.args.elements[0].compile(context)
            if compiled_arg is None:
                raise EikoCompilationError(
                    "Expected a value, but expression did not return one.",
                    token=self.args.elements[0].token,
                )

            if not isinstance(compiled_arg, EikoStr):
                raise EikoCompilationError(
                    "Expected a value of type 'str'.",
                    token=self.args.elements[0].token,
                )

            return eiko_callable.get(compiled_arg.value, self.args.elements[0].token)

        if eiko_callable in EikoBuiltinTypes:
            if len(self.args.elements) != 1:
                raise EikoCompilationError(
                    f"{eiko_callable.type.name} takes exactly 1 argument.",
                    token=self.token,
                )

            compiled_arg = self.args.elements[0].compile(context)
            if compiled_arg is None:
                raise EikoCompilationError(
                    "Expected a value, but expression did not return one.",
                    token=self.args.elements[0].token,
                )
            try:
                if not isinstance(compiled_arg, EikoBuiltinTypes):
                    raise ValueError
                return eiko_callable.convert(compiled_arg)
            except ValueError as e:
                raise EikoCompilationError(
                    f"Value of type '{compiled_arg.type.name}' "
                    f"can not be converted to a '{eiko_callable.type.name}'",
                    token=self.args.elements[0].token,
                ) from e

        raise EikoCompilationError(
            f"{self.identifier} is not callable.",
            token=self.token,
        )


@dataclass
class IndexExprAst(ExprAST):
    """An AST expressing an object being accessed by index."""

    identifier_expr: Union[DotExprAST, VariableExprAST]
    index_expr: ListExprAST

    def __post_init__(self) -> None:
        self.identifier = self.identifier_expr.identifier
        if len(self.index_expr.elements) != 1:
            raise EikoCompilationError(
                "Expected exactly 1 argument for index expression.",
                token=self.index_expr.token,
            )

    def compile(self, context: CompilerContext) -> Optional[EikoBaseType]:
        indexed_item = self.identifier_expr.compile(context)
        index = self.index_expr.elements[0].compile(context)

        if isinstance(indexed_item, EikoList):
            if not isinstance(index, EikoInt):
                raise EikoCompilationError(
                    "Index of a list can only be an integer.",
                    token=self.index_expr.token,
                )

            value = indexed_item.get_index(index.value)
            if value is None:
                raise EikoCompilationError(
                    "List index out of range.",
                    token=self.index_expr.token,
                )

            return value

        if isinstance(indexed_item, EikoDict):
            if not isinstance(index, EikoBaseType):
                raise EikoCompilationError(
                    "Illegal expression for dictionairy index.",
                    token=self.index_expr.token,
                )
            return indexed_item.get_index(index, self.index_expr.elements[0].token)

        raise EikoCompilationError(
            "Expression is not indexable.",
            token=self.token,
        )

    def assign(
        self,
        value: StorableTypes,
        context: CompilerContext,
        _: CompilerContext,
        value_token: Token,
    ) -> StorableTypes:
        """Assign this index expression a value of some kind."""
        indexed_item = self.identifier_expr.compile(context)
        index = self.index_expr.elements[0].compile(context)
        if not isinstance(indexed_item, EikoDict):
            raise EikoCompilationError(
                f"'{self.identifier}' is not an indexable item.",
                token=self.token,
            )

        if not isinstance(index, EikoBaseType):
            raise EikoCompilationError(
                "Illegal expression for dictionairy index.",
                token=self.index_expr.token,
            )

        if not isinstance(value, EikoBaseType):
            raise EikoCompilationError(
                "Illegal expression for dictionairy value.",
                token=value_token,
            )

        indexed_item.insert(index, value)

        return value


@dataclass
class ResourcePropertyAST:
    """An AST expressing a resource property."""

    expr: Union[VariableExprAST, AssignmentExprAST]

    def __post_init__(self) -> None:
        if isinstance(self.expr, VariableExprAST):
            self.token = self.expr.token
        else:
            self.token = self.expr.lhs.token

        self.name = self.token.content

    def compile_type(self, context: CompilerContext) -> EikoType:
        """
        Compiles only the type of the expression.
        """
        if isinstance(self.expr, VariableExprAST):
            if self.expr.type_expr is None:
                raise EikoInternalError(
                    "Unexpected behaviour in property typing.", token=self.token
                )
            return self.expr.type_expr.compile(context)

        if isinstance(self.expr, AssignmentExprAST) and isinstance(
            self.expr.lhs, VariableExprAST
        ):
            if self.expr.lhs.type_expr is not None:
                return self.expr.lhs.type_expr.compile(context)

            raise EikoCompilationError(
                "Property expected type expression.",
                token=self.token,
            )
        raise EikoInternalError(
            "Unexpectedly ran in to issues, please report this on github.",
            token=self.token,
        )

    def compile(self, context: CompilerContext, res_name: str) -> ResourceProperty:
        """Compile the ResourceProperty to something the ResourceDefinitionAST understands."""
        if isinstance(self.expr, VariableExprAST):
            if self.expr.type_expr is None:
                raise EikoCompilationError(
                    f"{res_name}.{self.name} requires a type definition.",
                    token=self.expr.token,
                )

            return ResourceProperty(self.name, self.expr.type_expr.compile(context))

        if not isinstance(self.expr.lhs, VariableExprAST):
            raise EikoCompilationError(
                "Unexpected expression. Resource property expected an identifier.",
                token=self.expr.lhs.token,
            )

        if self.expr.lhs.type_expr is None:
            raise EikoCompilationError(
                f"{res_name}.{self.name} reqquires a type definition.",
                token=self.expr.lhs.token,
            )

        _type = self.expr.lhs.type_expr.compile(context)
        default_value = self.expr.rhs.compile(context)
        if not isinstance(default_value, EikoBaseType):
            raise EikoCompilationError(
                f"{res_name}.{self.name} did not get a valid default value.",
                token=self.expr.rhs.token,
            )

        lhs_type_expr: TypeExprAST | None = None
        if isinstance(default_value, EikoList):
            if default_value.type.name == "list[]":
                if isinstance(_type, EikoListType):
                    lhs_type_expr = self.expr.lhs.type_expr
                    default_value.type = _type
                else:
                    raise EikoInternalError(
                        "Something went wrong initializing a list as default argument."
                    )

        elif isinstance(default_value, EikoDict):
            if default_value.type.name == "dict[]":
                if isinstance(_type, EikoDictType):
                    lhs_type_expr = self.expr.lhs.type_expr
                    default_value.type = _type
                else:
                    raise EikoInternalError(
                        "Something went wrong initializing a list as default argument."
                    )

        if not _type.type_check(default_value.type):
            if _type.inverse_type_check(default_value.type):
                if _type.typedef is None:
                    raise EikoInternalError(
                        "Something went wrong trying to coerce a type to it's typedef.",
                        token=self.expr.token,
                    )
                default_value = _type.typedef.execute(
                    default_value,
                    self.expr.rhs.token,
                )
            else:
                raise EikoCompilationError(
                    f"The default value of {res_name}.{self.name} does not fit type described in its type expression.",
                    token=self.expr.rhs.token,
                )

        return ResourceProperty(self.name, _type, default_value, lhs_type_expr)


@dataclass
class DecoratorExprAST(ExprAST):
    """A decorator, used to decorate resource definitions."""

    identifier: VariableExprAST
    args: list[ExprAST]

    def __post_init__(self) -> None:
        self.compiled_args: list[StorableTypes] = []

    def compile(self, context: CompilerContext) -> EikoDecorator:
        decorator = self.identifier.compile(context)
        if not isinstance(decorator, EikoDecorator):
            raise EikoCompilationError(
                "Decorator call applied to non decorator.", token=self.token
            )

        for arg in self.args:
            _arg = arg.compile(context)
            if _arg is None:
                raise EikoCompilationError(
                    "Expression did not return a value, but decorator expected a value.",
                    token=arg.token,
                )
            self.compiled_args.append(_arg)

        return decorator


@dataclass
class PromiseExprAST:
    """
    Construct represting a parsed EikoPromise.
    """

    token: Token
    var: VariableExprAST
    type_expr: "TypeExprAST"

    def compile(self, context: CompilerContext) -> EikoPromiseDefinition:
        return EikoPromiseDefinition(
            self.var.token.content,
            self.type_expr.compile(context),
        )


@dataclass
class ResourceDefinitionAST(ExprAST):
    """
    A resource definition represents the resources properties and constructors.
    """

    name: str
    decorators: list[DecoratorExprAST]
    super_expr: DotExprAST | VariableExprAST | None

    def __post_init__(self) -> None:
        self.constructor: Optional["ConstructorExprAST"] = None
        self.type = EikoType(self.name, EikoObjectType)
        self.properties: dict[str, ResourcePropertyAST] = {}
        self.promises: list[PromiseExprAST] = []

    def add_property(self, new_property: ResourcePropertyAST) -> None:
        self.properties[new_property.name] = new_property

    def add_promise(self, promise: PromiseExprAST) -> None:
        self.promises.append(promise)

    # pylint: disable = too-many-locals
    def compile(self, context: CompilerContext) -> EikoResourceDefinition:
        super_res: Optional[EikoResourceDefinition] = None
        if self.super_expr is not None:
            _super_res = self.super_expr.compile(context)
            if not isinstance(_super_res, EikoResourceDefinition):
                raise EikoCompilationError(
                    "Expected a resource definition to inherit from.",
                    token=self.super_expr.token,
                )

            super_res = _super_res
            self.type.super = super_res.instance_type
            new_prop_dict: dict[str, ResourcePropertyAST] = {}
            for super_property_ast in super_res.expr.properties.values():
                new_prop = self.properties.get(super_property_ast.name)
                if new_prop is not None:
                    new_type = new_prop.compile_type(context)
                    prev_type = super_property_ast.compile_type(context)
                    if not prev_type.type_check(new_type):
                        raise EikoCompilationError(
                            f"Property '{new_prop.name}' already defined "
                            f"in super type '{super_res.name}'.",
                            token=new_prop.token,
                        )
                new_prop_dict[super_property_ast.name] = super_property_ast
            new_prop_dict.update(self.properties)
            self.properties = new_prop_dict

            for super_promise in super_res.expr.promises:
                for promise in self.promises:
                    if super_promise.var.identifier == promise.var.identifier:
                        raise EikoCompilationError(
                            f"Promise '{super_promise.var.identifier}' already defined "
                            f"in super type '{super_res.name}'.",
                            token=promise.token,
                        )
                self.promises.append(super_promise)

        arg_properties: dict[str, ResourceProperty] = {}
        for property_ast in self.properties.values():
            prop = property_ast.compile(context, self.name)
            _prop = arg_properties.get(prop.name)
            if _prop is not None:
                raise EikoCompilationError(
                    f"Property '{prop.name}' was already defined in class '{self.name}'.",
                    token=property_ast.token,
                )
            arg_properties[prop.name] = prop

        if self.constructor is None:
            default_constructor = ConstructorDefinition("__init__", context)
            default_constructor.add_arg(ConstructorArg("self", self.type))
            for prop in arg_properties.values():
                default_constructor.add_arg(
                    ConstructorArg(prop.name, prop.type, prop.default_value)
                )
                token = self.properties[prop.name].token
                if token is None:
                    token = Token(TokenType.IDENTIFIER, prop.name, self.token.index)
                default_constructor.add_body_expr(
                    AssignmentExprAST(
                        self.token,
                        DotExprAST(
                            Token(TokenType.DOT, ".", token.index),
                            VariableExprAST(
                                Token(TokenType.IDENTIFIER, "self", token.index)
                            ),
                            VariableExprAST(token, prop.type_expr),
                        ),
                        VariableExprAST(token),
                    ),
                )
        else:
            default_constructor = self.constructor.compile(context)

        # pylint: disable=unnecessary-comprehension
        properties: dict[str, Union[EikoPromiseDefinition, ResourceProperty]] = {
            key: value for key, value in arg_properties.items()
        }
        promises: list[EikoPromiseDefinition] = []
        for promise_ast in self.promises:
            promise_def = promise_ast.compile(context)
            prev = properties.get(promise_def.name)
            if prev is not None:
                raise EikoCompilationError(
                    f"Property '{promise_def.name}' was already defined in class '{self.name}'.",
                    token=promise_ast.token,
                )

            promises.append(promise_def)
            properties[promise_def.name] = promise_def

        resource_def = EikoResourceDefinition(
            self, default_constructor, properties, promises
        )

        for decorator in self.decorators:
            decorator.compile(context).execute(
                resource_def, decorator.compiled_args, decorator.token
            )

        resource_def.default_constructor.index_def = resource_def.index_def
        context.set(self.name, resource_def, self.token)

        return resource_def


@dataclass
class ConstructorArgExprAST:
    """An AST expressing an arg for a Constructor."""

    expr: Union[VariableExprAST, AssignmentExprAST]

    def __post_init__(self) -> None:
        if isinstance(self.expr, VariableExprAST):
            self.token = self.expr.token
        else:
            self.token = self.expr.lhs.token

        self.name = self.token.content

    def compile(self, context: CompilerContext) -> ConstructorArg:
        """Compile the ResourceProperty to something the ResourceDefinitionAST understands."""
        if isinstance(self.expr, VariableExprAST):
            if self.expr.type_expr is None:
                raise EikoCompilationError(
                    f"Arg {self.name} requires a type definition.",
                    token=self.expr.token,
                )

            return ConstructorArg(self.name, self.expr.type_expr.compile(context))

        if not isinstance(self.expr.lhs, VariableExprAST):
            raise EikoCompilationError(
                "Unexpected expression. Resource property expected an identifier.",
                token=self.expr.lhs.token,
            )

        if self.expr.lhs.type_expr is None:
            raise EikoCompilationError(
                f"Arg {self.name} reqquires a type definition.",
                token=self.expr.lhs.token,
            )

        _type = self.expr.lhs.type_expr.compile(context)
        default_value = self.expr.rhs.compile(context)
        if not isinstance(default_value, EikoBaseType):
            raise EikoCompilationError(
                f"Arg {self.name} did not get a valid default value.",
                token=self.expr.rhs.token,
            )

        if not _type.type_check(default_value.type):
            raise EikoCompilationError(
                f"The default value of arg {self.name} does not fit type described in its type expression.",
            )

        return ConstructorArg(self.name, _type, default_value)


@dataclass
class ConstructorExprAST(ExprAST):
    """
    Represents a custom constructor.
    """

    identifier: VariableExprAST
    self_arg: VariableExprAST
    args: list[ConstructorArgExprAST]
    body: list[ExprAST]

    def __post_init__(self) -> None:
        self.return_type: EikoType

    def compile(self, context: CompilerContext) -> ConstructorDefinition:
        args: dict[str, ConstructorArg] = {}
        constructor = ConstructorDefinition(self.identifier.identifier, context)
        constructor.add_arg(ConstructorArg(self.self_arg.identifier, self.return_type))
        for arg_ast in self.args:
            arg = arg_ast.compile(context)
            _arg = args.get(arg.name)
            if _arg is not None:
                raise EikoCompilationError(
                    f"Property '{arg.name}' already defined.",
                    token=arg_ast.token,
                )
            constructor.add_arg(arg)

        constructor.body = self.body

        return constructor


@dataclass
class ImportExprAST(ExprAST):
    """
    Represents an import of eiko code.
    """

    rhs: Union[VariableExprAST, DotExprAST]

    def compile(self, context: CompilerContext) -> None:
        import_list: list[str] = []
        if isinstance(self.rhs, VariableExprAST):
            import_list.append(self.rhs.identifier)
        else:
            self.rhs.to_import_traversable_list(import_list)

        _context = context.get_cached_context(import_list)
        if _context is not None:
            tlc = _context.get_top_level_context()
            context.storage[tlc.name] = tlc
            return

        module = resolve_import(import_list, context)
        if module is None:
            raise EikoCompilationError(
                f"Failed to locate module {'.'.join(import_list)}.",
                token=self.token,
            )

        if not module.context.compiled:
            logger.debug(f"Importing module '{'.'.join(import_list)}'.")
            import_python_code(import_list, module.path, module.context)
            parser = Parser(module.path)
            for expr in parser.parse():
                expr.compile(module.context)
            module.context.flag_as_compiled()

        init_lazy_load_submodules(module, import_list)


@dataclass
class FromImportExprAST(ExprAST):
    """
    Represents a from ... import ... construct.
    """

    lhs: Union["DotExprAST", VariableExprAST, None]
    import_items: list[VariableExprAST]
    dots: list[Token]

    def compile(self, context: CompilerContext) -> None:
        import_module: list[str] = []
        from_import_list: list[str] = []
        if isinstance(self.lhs, VariableExprAST):
            import_module.append(self.lhs.identifier)
            from_import_list.append(self.lhs.identifier)
        elif isinstance(self.lhs, DotExprAST):
            self.lhs.to_import_traversable_list(import_module)
            self.lhs.to_import_traversable_list(from_import_list)

        # pylint: disable=unnecessary-comprehension
        _full_import_list = [x for x in from_import_list[::-1]]
        _parent: Optional[CompilerContext] = context
        for _ in self.dots:
            if _parent is not None:
                _parent = _parent.super_module
                if _parent is not None:
                    _full_import_list.append(_parent.name)

        _full_import_list.reverse()
        _context = context.get_cached_context(_full_import_list)
        if _context is not None and not _context.compiled:
            _context = None

        if _context is None:
            module = resolve_from_import(import_module, context, self.dots)
            if module is None:
                raise EikoCompilationError(
                    f"Module '{'.'.join(_full_import_list)}' not found.",
                    token=self.token,
                )

            if not module.context.compiled:
                import_python_code(import_module, module.path, module.context)
                parser = Parser(module.path)
                for expr in parser.parse():
                    expr.import_context = module.context
                    expr.compile(module.context)

                module.context.flag_as_compiled()

            init_lazy_load_submodules(module, import_module)
            _context = module.context

        for item in self.import_items:
            if item.identifier in import_module:
                context.set(item.identifier, _context, self.token)
            else:
                imported_item = _context.get(item.identifier)
                if isinstance(
                    imported_item,
                    (EikoBaseType, EikoResourceDefinition, CompilerContext),
                ):
                    context.set(item.identifier, imported_item)
                elif imported_item is None:
                    raise EikoCompilationError(
                        f"Failed to import '{item.identifier}' from '{'.' * len(self.dots)}"
                        f"{'.'.join(from_import_list)}'.",
                        token=item.token,
                    )
                else:
                    raise EikoInternalError(
                        "Something went horribly wrong during a from ... import. "
                        "Please submit a bug report on github."
                    )


def init_lazy_load_submodules(module: Module, import_list: list[str]) -> None:
    """Turns submodules in to LazyLoadModules."""

    for submodule in module.submodules:
        import_path = import_list.copy()
        import_path.append(submodule.name)
        module.context.storage[submodule.name] = LazyLoadModule(
            submodule.context, Parser(submodule.path), import_path
        )
        init_lazy_load_submodules(submodule, import_path)


@dataclass
class IfExprAST(ExprAST):
    """
    Represents an if ... else ... construct.
    """

    if_expr: ExprAST
    body: list[ExprAST]
    else_body: Optional[list[ExprAST]]

    def compile(self, context: CompilerContext) -> None:
        if_res = self.if_expr.compile(context)
        if isinstance(if_res, EikoBaseType):
            if if_res.truthiness():
                for expr in self.body:
                    expr.compile(context)

            else:
                if self.else_body is not None:
                    for expr in self.else_body:
                        expr.compile(context)

        else:
            raise EikoCompilationError(
                "Invalid expression for if statement.",
                token=self.if_expr.token,
            )


@dataclass
class TypedefExprAST(ExprAST):
    """A type definition used to alias or restrict types."""

    name: str
    super_type_expr: Union[VariableExprAST, DotExprAST]
    condition: Optional[ExprAST]

    def compile(self, context: CompilerContext) -> None:
        super_type = self.super_type_expr.compile(context)
        if isinstance(super_type, type) and issubclass(super_type, EikoBaseType):
            typedef = EikoTypeDef(self.name, super_type.type, self.condition, context)

        elif isinstance(super_type, EikoTypeDef):
            typedef = EikoTypeDef(self.name, super_type, self.condition, context)

        else:
            raise EikoCompilationError(
                f"Could not find type '{self.super_type_expr.identifier}'.",
                token=self.super_type_expr.get_identifier_token(),
            )

        context.set(self.name, typedef, self.token)


eiko_indexable_types = (
    EikoBoolType,
    EikoFloatType,
    EikoIntType,
    EikoNoneType,
    EikoStrType,
)


@dataclass
class TypeExprAST(ExprAST):
    """An ExprAST expressing a complex type."""

    primary_expr: Union[VariableExprAST, DotExprAST]
    sub_expressions: list["TypeExprAST"]

    def compile(self, context: CompilerContext) -> EikoType:
        primary_type = self.primary_expr.compile(context)

        if primary_type is eiko_none_object:
            return eiko_none_object.type

        if isinstance(primary_type, type) and issubclass(primary_type, EikoBaseType):
            return primary_type.type

        if isinstance(primary_type, EikoType):
            return primary_type

        if isinstance(primary_type, EikoResourceDefinition):
            return primary_type.instance_type

        if isinstance(primary_type, EikoTypeDef):
            return primary_type.type

        if primary_type is EikoUnion:
            if len(self.sub_expressions) < 2:
                raise EikoCompilationError(
                    "Union type expects at least 2 type arguments.", token=self.token
                )
            name = "Union["
            sub_expressions: list[EikoType] = []
            for expr in self.sub_expressions:
                compiled_expr = expr.compile(context)
                name += compiled_expr.name + ","
                sub_expressions.append(compiled_expr)

            name = name[:-1] + "]"

            return EikoUnion(name, sub_expressions)

        if primary_type is EikoOptional:
            if len(self.sub_expressions) != 1:
                raise EikoCompilationError(
                    "Optional type expects exactly 1 type argument.", token=self.token
                )

            compiled_expr = self.sub_expressions[0].compile(context)
            return EikoOptional(compiled_expr)

        if primary_type is EikoListType:
            if len(self.sub_expressions) != 1:
                raise EikoCompilationError(
                    "List type expects exactly 1 type argument.", token=self.token
                )

            compiled_expr = self.sub_expressions[0].compile(context)
            return EikoListType(compiled_expr)

        if primary_type is EikoDictType:
            if len(self.sub_expressions) != 2:
                raise EikoCompilationError(
                    "Dict type expects exactly 2 type arguments.", token=self.token
                )

            key_type = self.sub_expressions[0].compile(context)
            if isinstance(key_type, EikoUnion):
                for _sub_type in key_type.types:
                    if not self._is_valid_dict_key_type(_sub_type):
                        raise EikoCompilationError(
                            f"Type '{_sub_type.name}' can not be used for dictionary indexes.",
                            token=self.sub_expressions[0].token,
                        )
            elif not self._is_valid_dict_key_type(key_type):
                raise EikoCompilationError(
                    f"Type '{key_type.name}' can not be used for dictionary indexes.",
                    token=self.sub_expressions[0].token,
                )

            return EikoDictType(key_type, self.sub_expressions[1].compile(context))

        if isinstance(primary_type, EikoEnumDefinition):
            return primary_type.value_type

        raise EikoCompilationError(
            "Not a valid type expressions.",
            token=self.token,
        )

    @staticmethod
    def _is_valid_dict_key_type(_type: EikoType) -> bool:
        if _type in eiko_indexable_types:
            return True

        if _type.get_top_level_type() == EikoObjectType:
            return True

        return False


@dataclass
class DictExprAST(ExprAST):
    """A dict of some type."""

    kv_pairs: list[Tuple[ExprAST, ExprAST]]

    def compile(self, context: CompilerContext) -> Optional[StorableTypes]:
        key_types: list[EikoType] = []
        value_types: list[EikoType] = []
        elements: dict[Union[EikoBaseType, bool, float, int, str], EikoBaseType] = {}
        for key, value in self.kv_pairs:
            compiled_key = key.compile(context)
            if not isinstance(compiled_key, EikoBaseType):
                raise EikoCompilationError(
                    "Invalid dictionary key expression.",
                    token=key.token,
                )
            if compiled_key.type not in key_types:
                key_types.append(compiled_key.type)

            compiled_value = value.compile(context)
            if not isinstance(compiled_value, EikoBaseType):
                raise EikoCompilationError(
                    "Invalid dictionary value expression.",
                    token=key.token,
                )
            if compiled_value.type not in value_types:
                value_types.append(compiled_value.type)

            _key = EikoDict.convert_key(compiled_key, key.token)
            elements[_key] = compiled_value

        key_type = type_list_to_type(key_types)
        value_type = type_list_to_type(value_types)

        return EikoDict(key_type, value_type, elements)


@dataclass
class EnumValueExprAst(ExprAST):
    """Represents a value tied to an enum class."""

    def __post_init__(self) -> None:
        super().__post_init__()
        self.value = self.token.content

    def compile(self, context: CompilerContext) -> Optional[StorableTypes]:
        raise NotImplementedError(self.token)


@dataclass
class EnumExprAst(ExprAST):
    """Represents an enum definition."""

    values: list[EnumValueExprAst]

    def compile(self, context: CompilerContext) -> EikoEnumDefinition:
        value_type = EikoType(self.token.content)
        values: dict[str, EikoEnumValue] = {}
        for value in self.values:
            values[value.value] = EikoEnumValue(value_type, value.value)

        definition = EikoEnumDefinition(self.token.content, value_type, values)
        context.set(definition.name, definition)
        return definition


@dataclass
class ForExprAst(ExprAST):
    """Represents a for loop that loops over an iterable."""

    loop_var_expr: VariableExprAST
    iterable_expr: ExprAST
    body: list[ExprAST]

    def compile(self, context: CompilerContext) -> None:
        compiled_iterable = self.iterable_expr.compile(context)
        if not isinstance(compiled_iterable, EikoBaseType):
            raise EikoCompilationError(
                "Expression is not iterable, but FOR requires an iterable.",
                token=self.iterable_expr.token,
            )

        for obj in compiled_iterable.iterate(self.iterable_expr.token):
            sub_context = context.get_subcontext(f"{context.name}-for")
            self.loop_var_expr.assign(
                obj, sub_context, context, self.loop_var_expr.token
            )
            for line in self.body:
                line.compile(sub_context)


@dataclass
class InExprAst(ExprAST):
    """Represents a for loop that loops over an iterable."""

    lhs: ExprAST
    rhs: ExprAST

    def compile(self, context: CompilerContext) -> EikoBool:
        lhs = self.lhs.compile(context)
        rhs = self.rhs.compile(context)
        if not isinstance(rhs, (EikoStr, EikoList, EikoDict)):
            raise EikoCompilationError(
                "Membership test expects a value of of type 'str', "
                f"'dict', or 'list', not '{self.rhs.token}'"
            )

        if lhs is None:
            raise EikoCompilationError(
                "Membership test left hand side expression did not return a value.",
                token=self.lhs.token,
            )

        if not isinstance(lhs, EikoBaseType):
            raise EikoCompilationError(
                f"Membership test left hand side value not useable. ('{lhs.type}').",
                token=self.lhs.token,
            )

        return rhs.test_membership(lhs, self.lhs.token)


class Parser:
    """
    Parses tokens 1 by 1, and turns them in to Expressions.
    """

    def __init__(self, file: Path) -> None:
        self.lexer = Lexer(file)
        self._current = self.lexer.next_token()
        self._next = self.lexer.next_token()
        self._previous = self._current
        self._current_indent = ""
        self._bin_op_precedence = {
            "=": 10,
            "or": 20,
            "and": 30,
            "unot": 40,
            "in": 50,
            "==": 50,
            "!=": 50,
            "<": 50,
            ">": 50,
            "<=": 50,
            ">=": 50,
            "+": 60,
            "-": 60,
            "*": 70,
            "/": 70,
            "//": 70,
            "%": 70,
            "u-": 80,
            "**": 90,
            ".": 100,
            "(": 100,
            "[": 100,
            ":": 100,
        }

    def parse(self) -> Iterator[ExprAST]:
        """Parses tokens and constructs the next set of ASTs."""
        expr = self._parse_top_level()
        while not isinstance(expr, EOFExprAST):
            yield expr
            expr = self._parse_top_level()

    def print_op_precedence(self) -> None:
        """outputs every level and ops associated with said level."""
        op_precedents: dict[int, list[str]] = {}
        for key, value in self._bin_op_precedence.items():
            _list = op_precedents.get(value)
            if _list is None:
                _list = []
                op_precedents[value] = _list
            _list.append(key)

        for level, _list in sorted(op_precedents.items()):
            print(f"{level}: [", ", ".join(_list), "]")

    def _advance(self, skip_indentation: bool = False) -> None:
        if self._current.type != TokenType.INDENT:
            self._previous = self._current
        self._current = self._next
        self._next = self.lexer.next_token()

        if skip_indentation and self._current.type == TokenType.INDENT:
            self._advance(skip_indentation)

        if (
            self._current.type == TokenType.STRING
            and self._next.type == TokenType.STRING
        ):
            self._next = Token(
                TokenType.STRING,
                self._current.content + self._next.content,
                self._current.index,
            )
            self._advance(skip_indentation)

        if (
            self._current.type == TokenType.INDENT
            and self._current.content == ""
            and self._next.type == TokenType.INDENT
        ):
            self._advance()

    def _parse_top_level(self) -> ExprAST:
        if not (
            (self._current.type == TokenType.INDENT and self._current.content == "")
            or (self._current.type == TokenType.EOF and self._current.content == "EOF")
        ):
            raise EikoParserError(
                f"Unexpected token: '{self._current.content}'.", token=self._current
            )

        self._advance(skip_indentation=True)
        if self._current.type == TokenType.EOF:
            return EOFExprAST(self._current)

        if self._current.type == TokenType.RESOURCE:
            return self._parse_resource_definition([])

        if self._current.type == TokenType.IMPORT:
            return self._parse_import()

        if self._current.type == TokenType.FROM:
            return self._parse_from_import()

        expr = self._parse_expression()
        if isinstance(expr, VariableExprAST) and self._current.type == TokenType.COLON:
            self._advance()
            expr.type_expr = self._parse_type()
            return self._parse_bin_op_rhs(0, expr)

        return expr

    def _parse_expression(self, precedence: int = 0) -> ExprAST:
        lhs = self._parse_primary()
        return self._parse_bin_op_rhs(precedence, lhs)

    def _parse_primary(self) -> ExprAST:
        if self._current.type == TokenType.INDENT:
            if self._current.content == "":
                self._advance()
                return self._parse_primary()

        if self._current.content in ["-", "not"]:
            return self._parse_unary_op()

        if self._current.type in [TokenType.TRUE, TokenType.FALSE]:
            token = self._current
            self._advance()
            return BoolExprAST(token)

        if self._current.type == TokenType.INTEGER:
            token = self._current
            self._advance()
            return IntExprAST(token)

        if self._current.type == TokenType.FLOAT:
            token = self._current
            self._advance()
            return FloatExprAST(token)

        if self._current.type == TokenType.STRING:
            token = self._current
            self._advance()
            return StringExprAST(token)

        if self._current.type == TokenType.F_STRING:
            token = self._current
            self._advance()
            return FStringExprAST(token)

        if self._current.type == TokenType.LEFT_PAREN:
            return ListExprAST(self._current, self._parse_list(TokenType.RIGHT_PAREN))

        if self._current.type == TokenType.IDENTIFIER:
            return self._parse_identifier()

        if self._current.type == TokenType.IF:
            return self._parse_if()

        if self._current.type == TokenType.TYPEDEF:
            return self._parse_typedef()

        if self._current.type == TokenType.LEFT_SQ_BRACKET:
            return ListExprAST(
                self._current, self._parse_list(TokenType.RIGHT_SQ_BRACKET)
            )

        if self._current.type == TokenType.LEFT_BRACE:
            return self._parse_dict()

        if self._current.type == TokenType.AT_SIGN:
            return self._parse_decorator([])

        if self._current.type == TokenType.ENUM:
            return self._parse_enum()

        if self._current.type == TokenType.FOR:
            return self._parse_for()

        raise EikoSyntaxError(
            f"Unexpected token {self._current.type.name}.", index=self._current.index
        )

    def _parse_unary_op(self) -> Union[UnaryNegExprAST, UnaryNotExprAST]:
        token = self._current
        self._advance()
        if token.content == "-":
            rhs = self._parse_expression(self._bin_op_precedence["u-"])
            return UnaryNegExprAST(token, rhs)

        rhs = self._parse_expression(self._bin_op_precedence["unot"])
        return UnaryNotExprAST(token, rhs)

    def _parse_parens(self) -> ExprAST:
        self._advance(skip_indentation=True)
        next_expr = self._parse_expression()
        if self._current.type == TokenType.INDENT:
            self._advance(skip_indentation=True)
        if self._current.type != TokenType.RIGHT_PAREN:
            raise EikoParserError("Unexpected token.", token=self._current)

        self._advance()
        return next_expr

    def _parse_bin_op_rhs(
        self, expr_precedence: Union[float, int], lhs: ExprAST
    ) -> ExprAST:
        while True:
            current_predecedence = self._bin_op_precedence.get(self._current.content, 0)
            if current_predecedence < expr_precedence:
                return lhs
            if self._current.type in [
                TokenType.INDENT,
                TokenType.RIGHT_PAREN,
                TokenType.RIGHT_SQ_BRACKET,
                TokenType.RIGHT_BRACE,
                TokenType.COMMA,
                TokenType.IMPORT,
                TokenType.COLON,
                TokenType.IF,
                TokenType.EOF,
            ]:
                return lhs

            bin_op_token = self._current
            if self._current.type not in [
                TokenType.LEFT_PAREN,
                TokenType.LEFT_SQ_BRACKET,
            ]:
                self._advance()
            rhs = self._parse_primary()
            if (
                isinstance(rhs, VariableExprAST)
                and self._current.type == TokenType.COLON
                and self._next.type == TokenType.IDENTIFIER
            ):
                self._advance()
                rhs = VariableExprAST(rhs.token, self._parse_type())

            # if current op binds less tightly with rhs than the operator after rhs,
            # let the pending operator take rhs as it's lhs
            next_op_precedence = self._bin_op_precedence.get(self._current.content, 0)
            if expr_precedence < next_op_precedence:
                rhs = self._parse_bin_op_rhs(current_predecedence + 0.0001, rhs)

            if bin_op_token.type == TokenType.ASSIGNMENT_OP:
                lhs = AssignmentExprAST(bin_op_token, lhs, rhs)
            elif bin_op_token.type == TokenType.DOT:
                if isinstance(
                    lhs, (VariableExprAST, DotExprAST, IndexExprAst, CallExprAst)
                ):
                    if not isinstance(rhs, VariableExprAST):
                        raise EikoParserError(
                            "Unexpected token. "
                            "Expected an identifier on the right side of dot expression.",
                            token=rhs.token,
                        )
                    lhs = DotExprAST(bin_op_token, lhs, rhs)
                else:
                    raise EikoParserError(
                        "Unexpected token. "
                        "Expected an identifier on the left side of dot expression.",
                        token=rhs.token,
                    )
            elif bin_op_token.type == TokenType.COMPARISON_OP:
                lhs = ComparisonExprAST(bin_op_token, lhs, rhs)
            elif bin_op_token.type == TokenType.OR:
                lhs = OrExprAST(bin_op_token, lhs, rhs)
            elif bin_op_token.type == TokenType.AND:
                lhs = AndExprAST(bin_op_token, lhs, rhs)
            elif bin_op_token.type == TokenType.LEFT_PAREN:
                if isinstance(lhs, (DotExprAST, VariableExprAST)):
                    if isinstance(rhs, ListExprAST):
                        lhs = CallExprAst(lhs.get_identifier_token(), lhs, rhs)
                    else:
                        raise EikoParserError(
                            "Unexpected expression. "
                            "Expected a list of expressions as arguments for callable.",
                            token=rhs.token,
                        )
                else:
                    raise EikoParserError(
                        "Unexpected expression. "
                        "Expected an identifier for call expression.",
                        token=lhs.token,
                    )
            elif bin_op_token.type == TokenType.LEFT_SQ_BRACKET:
                if isinstance(lhs, (DotExprAST, VariableExprAST)):
                    if isinstance(rhs, ListExprAST):
                        lhs = IndexExprAst(lhs.token, lhs, rhs)
                    else:
                        raise EikoParserError(
                            "Unexpected expression. "
                            "Expected an expressions as arguments for index call.",
                            token=rhs.token,
                        )
                else:
                    raise EikoParserError(
                        "Unexpected expression. "
                        "Expected an identifier for index expression.",
                        token=lhs.token,
                    )
            elif bin_op_token.type == TokenType.IN:
                lhs = InExprAst(bin_op_token, lhs, rhs)
            else:
                lhs = BinOpExprAST(bin_op_token, lhs, rhs)

    def _parse_identifier(self) -> VariableExprAST:
        token = self._current
        self._advance()

        return VariableExprAST(token)

    def _parse_list(self, closer: TokenType) -> list[ExprAST]:
        expressions: list[ExprAST] = []
        self._advance(skip_indentation=True)
        while True:
            if self._current.type == closer:
                self._advance()
                break

            expr = self._parse_expression()
            expressions.append(expr)
            if self._current.type == closer:
                self._advance()
                break

            if self._current.type == TokenType.INDENT:
                self._advance(skip_indentation=True)

            if self._current.type == closer:
                self._advance()
                break

            if self._current.type != TokenType.COMMA:
                closer_char = token_to_char.get(closer, "??")
                raise EikoParserError(
                    f"Unexpected token. Expected a ',' or a '{closer_char}'.",
                    token=self._current,
                )

            self._advance(skip_indentation=True)
            if self._current.type == closer:
                self._advance()
                break

        return expressions

    def _parse_decorator(
        self, decorators: list[DecoratorExprAST]
    ) -> ResourceDefinitionAST:
        deco_token = self._current
        self._advance()
        if self._current.type != TokenType.IDENTIFIER:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, "
                "expected resource identifier.",
                token=self._next,
            )

        identifier = VariableExprAST(self._current)
        self._advance()
        if self._current.type == TokenType.LEFT_PAREN:
            list_expr = self._parse_list(TokenType.RIGHT_PAREN)
        else:
            list_expr = []
        deco_expr = DecoratorExprAST(deco_token, identifier, list_expr)
        decorators.append(deco_expr)
        if self._current.type != TokenType.INDENT and self._current.content != "":
            raise EikoParserError(
                "Unexpected token.",
                token=self._current,
            )
        self._advance(skip_indentation=True)

        if self._current.type == TokenType.AT_SIGN:
            return self._parse_decorator(decorators)

        if self._current.type == TokenType.RESOURCE:
            return self._parse_resource_definition(decorators)

        raise EikoCompilationError(
            "Expected a resource definition after a decorator.",
            token=self._current,
        )

    def _parse_resource_definition(
        self, decorators: list[DecoratorExprAST]
    ) -> ResourceDefinitionAST:
        if self._next.type != TokenType.IDENTIFIER:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, "
                "expected resource identifier.",
                token=self._next,
            )

        identifier_token = self._next
        self._advance()
        self._advance()

        # Inheritance
        super_expr: VariableExprAST | DotExprAST | None = None
        if self._current.type == TokenType.LEFT_PAREN:
            self._advance()
            super_expr = self._parse_identifier()
            while True:
                if self._current.type != TokenType.DOT:
                    break

                self._advance()
                super_expr = DotExprAST(
                    self._previous, super_expr, self._parse_identifier()
                )

            if self._current.type != TokenType.RIGHT_PAREN:
                raise EikoParserError(
                    f"Unexpected token {self._current.content}. Expected ')'.",
                    token=self._current,
                )
            self._advance()

        rd_ast = ResourceDefinitionAST(
            identifier_token, identifier_token.content, decorators, super_expr
        )

        if self._current.type != TokenType.COLON:
            raise EikoParserError(
                f"Unexpected token {self._current.content}. Expected a ':'.",
                token=self._current,
            )
        self._advance()

        while self._next.type == TokenType.INDENT:
            self._advance()

        if self._current.content == "":
            if self._next.type == TokenType.EOF:
                raise EikoParserError(
                    "Unexpectedly reached end of file, expected indented code block.",
                    token=self._previous,
                )
            raise EikoParserError(
                f"Unexpected token {self._current.content}, "
                "expected indented code block.",
                token=self._current,
            )

        if self._next.type == TokenType.TRIPLE_DOT:
            self._advance()
            self._advance()
            return rd_ast

        indent = self._current.content
        constructor: Optional[ConstructorExprAST] = None
        while True:
            while self._next.type == TokenType.INDENT:
                self._advance()
            if self._current.content != indent:
                break
            self._advance()

            if self._current.type == TokenType.DEF:
                constructor = self._parse_constructor()
                constructor.return_type = rd_ast.type
            elif self._current.type == TokenType.IDENTIFIER:
                rd_ast.add_property(self._parse_resource_property())
            elif self._current.type == TokenType.PROMISE:
                rd_ast.add_promise(self._parse_promise())
            elif self._current.type == TokenType.INDENT:
                pass
            else:
                raise EikoSyntaxError(
                    f"Unexpected token '{self._current.content}' in resource definition '{rd_ast.name}'",
                    index=self._current.index,
                )

        if not rd_ast.properties and rd_ast.super_expr is None:
            raise EikoSyntaxError(
                "A resource must have atleast 1 property. (Besides promises.)",
                index=rd_ast.token.index,
            )
        rd_ast.constructor = constructor

        return rd_ast

    def _parse_resource_property(self) -> ResourcePropertyAST:
        identifier = self._parse_identifier()
        if not self._current.type == TokenType.COLON:
            raise EikoSyntaxError(
                "Expected type identifier for resource property.",
                index=self._current.index,
            )

        self._advance()
        identifier.type_expr = self._parse_type()

        expr = self._parse_bin_op_rhs(0, identifier)

        if not isinstance(expr, (VariableExprAST, AssignmentExprAST)):
            raise EikoParserError(
                "Unexpected expression in resource definition.",
                token=expr.token,
            )

        return ResourcePropertyAST(expr)

    def _parse_constructor(self) -> ConstructorExprAST:
        def_token = self._current
        self._advance()
        identifier = self._parse_identifier()

        if self._current.type != TokenType.LEFT_PAREN:
            raise EikoSyntaxError("Expected a '('.", index=self._current.index)
        self._advance(skip_indentation=True)

        self_arg = VariableExprAST(self._current)
        self._advance()

        args: list[ConstructorArgExprAST] = []
        while True:
            if self._current.type == TokenType.RIGHT_PAREN:
                self._advance()
                break
            if self._current.type != TokenType.COMMA:
                raise EikoSyntaxError(
                    "Expected a ',' or a ')'.", index=self._current.index
                )
            self._advance(skip_indentation=True)
            if (
                self._current.type == TokenType.RIGHT_PAREN
                and self._next.type == TokenType.COLON
            ):
                self._advance()
                break
            args.append(self._parse_consructor_arg())
            if self._current.type == TokenType.INDENT:
                self._advance(skip_indentation=True)

        if self._current.type != TokenType.COLON:
            raise EikoSyntaxError("Expected a ':'.", index=self._current.index)
        self._advance()

        body = self._parse_body()

        return ConstructorExprAST(def_token, identifier, self_arg, args, body)

    def _parse_consructor_arg(self) -> ConstructorArgExprAST:
        identifier = self._parse_identifier()
        if not self._current.type == TokenType.COLON:
            raise EikoSyntaxError(
                "Expected type identifier for arg.",
                index=self._current.index,
            )

        self._advance()
        identifier.type_expr = self._parse_type()

        expr = self._parse_bin_op_rhs(0, identifier)

        if not isinstance(expr, (VariableExprAST, AssignmentExprAST)):
            raise EikoParserError(
                "Unexpected expression.",
                token=expr.token,
            )

        return ConstructorArgExprAST(expr)

    def _parse_promise(self) -> PromiseExprAST:
        promise_token = self._current
        self._advance()
        if not self._current.type == TokenType.IDENTIFIER:
            raise EikoParserError(
                "Expected an identifier for promise expression.",
                token=self._current,
            )

        var_expr = self._parse_identifier()
        if self._current.type != TokenType.COLON:
            raise EikoParserError(
                "Expected a ':'.",
                token=self._current,
            )

        self._advance()
        type_expr = self._parse_type()

        return PromiseExprAST(promise_token, var_expr, type_expr)

    def _parse_import(self) -> ImportExprAST:
        token = self._current
        self._advance()
        rhs = self._parse_expression()

        if isinstance(rhs, (VariableExprAST, DotExprAST)):
            return ImportExprAST(token, rhs)

        raise EikoParserError(
            "Unable to import given expression.",
            token=token,
        )

    def _parse_from_import(self) -> FromImportExprAST:
        import_token = self._current
        self._advance()

        dots: list[Token] = []
        while self._current.type in (
            TokenType.DOT,
            TokenType.DOUBLE_DOT,
            TokenType.TRIPLE_DOT,
        ):
            if self._current.type == TokenType.DOT:
                dots.append(self._current)
            elif self._current.type == TokenType.DOUBLE_DOT:
                dots.append(self._current)
                dots.append(self._current)
            else:
                dots.append(self._current)
                dots.append(self._current)
                dots.append(self._current)
            self._advance()

        lhs: Union[DotExprAST, VariableExprAST, None] = None
        if self._current.type == TokenType.IMPORT:
            if not dots:
                raise EikoParserError(
                    "Expected an expression between 'from' and 'import'.",
                    token=import_token,
                )
            self._advance()
        else:
            _lhs = self._parse_expression()
            if not isinstance(_lhs, (DotExprAST, VariableExprAST)):
                raise EikoParserError(
                    "Invalid expression in import statement.",
                    token=_lhs.token,
                )
            lhs = _lhs
            self._advance()

        import_items: list[VariableExprAST] = []
        while True:
            rhs = self._parse_expression()
            if not isinstance(rhs, VariableExprAST):
                raise EikoParserError(
                    "Invalid expression in import statement. "
                    "Only identifiers are llowed after 'from'.",
                    token=rhs.token,
                )
            import_items.append(rhs)
            if self._current.type == TokenType.COMMA:
                self._advance()
            else:
                break

        return FromImportExprAST(import_token, lhs, import_items, dots)

    def _parse_if(self) -> IfExprAST:
        if_token = self._current
        self._advance()

        if_expr = self._parse_expression()
        if not self._current.type == TokenType.COLON:
            raise EikoParserError(
                "Expected a ':' token to close IF expression.",
                token=Token(
                    TokenType.UNKNOWN,
                    "",
                    Index(
                        self._previous.index.line,
                        self._previous.index.col + len(self._previous.content),
                        self._previous.index.file,
                    ),
                ),
            )
        self._advance()

        if self._current.type == TokenType.INDENT:
            if_body = self._parse_body()
        else:
            if_body = [self._parse_expression()]
        else_body: Optional[list[ExprAST]] = None

        if (
            self._current.type == TokenType.INDENT
            and self._current.content == self._current_indent
        ):
            if self._next.type == TokenType.ELIF:
                self._advance()
                else_body = [self._parse_if()]

            elif self._next.type == TokenType.ELSE:
                self._advance()
                self._advance()
                if not self._current.type == TokenType.COLON:
                    raise EikoParserError(
                        "Expected a ':' after ELSE expression.",
                        token=Token(
                            TokenType.UNKNOWN,
                            "",
                            Index(
                                self._previous.index.line,
                                self._previous.index.col + len(self._previous.content),
                                self._previous.index.file,
                            ),
                        ),
                    )
                self._advance()
                if self._current.type == TokenType.INDENT:
                    else_body = self._parse_body()
                else:
                    else_body = [self._parse_expression()]

        return IfExprAST(if_token, if_expr, if_body, else_body)

    def _parse_body(self) -> list[ExprAST]:
        if (
            not self._current.type == TokenType.INDENT
            or self._current_indent >= self._current.content
        ):
            raise EikoParserError("Expected indented code block.", token=self._current)

        prev_indent = self._current_indent
        self._current_indent = self._current.content
        self._advance()

        body: list[ExprAST] = []
        while True:
            body.append(self._parse_expression())
            while self._next.type == TokenType.INDENT:
                self._advance()
            if self._current.type != TokenType.INDENT:
                raise EikoInternalError(
                    "Unexpected issue, please report this on github.",
                    token=self._current,
                )
            if self._current.content != self._current_indent:
                break
            self._advance()

        self._current_indent = prev_indent
        return body

    def _parse_typedef(self) -> TypedefExprAST:
        typedef_token = self._current
        self._advance()

        if not self._current.type == TokenType.IDENTIFIER:
            raise EikoParserError(
                "Expected identifier after typedef.", token=self._current
            )

        name = self._current.content
        self._advance()

        base_type_expr = self._parse_expression()
        if not isinstance(base_type_expr, (VariableExprAST, DotExprAST)):
            raise EikoParserError(
                "Expected a type identifier.", token=base_type_expr.token
            )

        if_expr = None
        if self._current.type == TokenType.IF:
            self._advance()
            if_expr = self._parse_expression()

        return TypedefExprAST(typedef_token, name, base_type_expr, if_expr)

    def _parse_type(self) -> TypeExprAST:
        primary_expr: Union[VariableExprAST, DotExprAST] = self._parse_identifier()
        while True:
            if self._current.type != TokenType.DOT:
                break

            self._advance()
            primary_expr = DotExprAST(
                primary_expr.token, primary_expr, self._parse_identifier()
            )

        sub_expressions: list[TypeExprAST] = []
        if self._current.type == TokenType.LEFT_SQ_BRACKET:
            while True:
                self._advance()
                sub_expressions.append(self._parse_type())
                if self._current.type == TokenType.RIGHT_SQ_BRACKET:
                    self._advance()
                    break

                if self._current.type != TokenType.COMMA:
                    raise EikoSyntaxError(
                        "Unexpected token",
                        index=self._current.index,
                    )

        return TypeExprAST(primary_expr.token, primary_expr, sub_expressions)

    def _parse_dict(self) -> DictExprAST:
        token = self._current
        kv_pairs: list[Tuple[ExprAST, ExprAST]] = []
        self._advance(skip_indentation=True)

        while True:
            if self._current.type == TokenType.RIGHT_BRACE:
                self._advance()
                break

            key_expr = self._parse_expression()
            if self._current.type != TokenType.COLON:
                raise EikoParserError(
                    "Expected a ':' after a key expression.",
                    token=self._current,
                )

            self._advance(skip_indentation=True)
            value_expr = self._parse_expression()
            kv_pairs.append((key_expr, value_expr))

            if self._current.type == TokenType.RIGHT_BRACE:
                self._advance()
                break

            if self._current.type != TokenType.COMMA:
                raise EikoParserError(
                    "Expected a ',' or a '}'",
                    token=self._current,
                )
            self._advance(skip_indentation=True)

            if self._current.type == TokenType.RIGHT_BRACE:
                self._advance()
                break

        return DictExprAST(token, kv_pairs)

    def _parse_enum(self) -> EnumExprAst:
        if self._next.type != TokenType.IDENTIFIER:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, "
                "expected resource identifier.",
                token=self._next,
            )

        identifier_token = self._next
        self._advance()
        self._advance()

        if self._current.type != TokenType.COLON:
            raise EikoParserError(
                f"Unexpected token '{self._current.content}'.",
                token=self._current,
            )
        self._advance()

        if self._current.type != TokenType.INDENT:
            raise EikoParserError(
                f"Unexpected token '{self._current.content}'.",
                token=self._previous,
            )

        if self._current.content == "":
            raise EikoParserError("Expected indentation.", token=self._previous)

        values: list[EnumValueExprAst] = []
        indent = self._current.content
        self._advance()
        while True:
            if self._current.type != TokenType.IDENTIFIER:
                raise EikoParserError(
                    f"Unexpected token '{self._current.content}', expected an enum value identifier.",
                    token=self._current,
                )

            values.append(EnumValueExprAst(self._current))
            self._advance()

            while self._next.type == TokenType.INDENT:
                self._advance()
            if self._current.type != TokenType.INDENT:
                raise EikoParserError(
                    f"Unexpected token '{self._current.content}'. Expected an indent while parsing enum.",
                    token=self._current,
                )
            if self._current.content != indent:
                break
            self._advance()

        return EnumExprAst(identifier_token, values)

    def _parse_for(self) -> ForExprAst:
        token = self._current
        self._advance()

        if self._current.type != TokenType.IDENTIFIER:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, " "expected an identifier.",
                token=self._next,
            )

        loop_var_expr = self._parse_identifier()
        if self._current.type != TokenType.IN:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, " "expected an 'in' token.",
                token=self._next,
            )
        self._advance()
        iterable_expr = self._parse_expression()

        if self._current.type != TokenType.COLON:
            raise EikoParserError(
                f"Unexpected token {self._next.content}, " "expected a ':'.",
                token=self._next,
            )
        self._advance()

        body = self._parse_body()
        return ForExprAst(token, loop_var_expr, iterable_expr, body)
