"""
Module to gather the logic to lower Lkt syntax trees to Langkit internal data
structures.
"""

from __future__ import annotations

from collections import OrderedDict
from dataclasses import dataclass
import itertools
import json
import os.path
from typing import (
    Any, Callable, ClassVar, Dict, List, Optional, Set, Tuple, Type, TypeVar,
    Union, cast
)

import liblktlang as L

from langkit.compile_context import CompileCtx
from langkit.compiled_types import (
    ASTNodeType, AbstractNodeData, Argument, CompiledType, CompiledTypeOrDefer,
    CompiledTypeRepo, EnumNodeAlternative, EnumType, Field, StructType, T,
    TypeRepo, UserField
)
from langkit.diagnostics import (
    DiagnosticError, Location, check_source_language, diagnostic_context, error
)
import langkit.expressions as E
from langkit.expressions import (
    AbstractExpression, AbstractKind, AbstractProperty, AbstractVariable, Cast,
    Let, LocalVars, Property, PropertyDef, PropertyError, create_lazy_field
)
from langkit.lexer import (
    Action, Alt, Case, Ignore, Lexer, LexerToken, Literal, Matcher, NoCaseLit,
    Pattern, RuleAssoc, TokenAction, TokenFamily, WithSymbol, WithText,
    WithTrivia
)
import langkit.names as names
from langkit.parsers import (
    Cut, Defer, Discard, DontSkip, Grammar, List as PList, Null, Opt,
    Or, Parser, Pick, Predicate, Skip, StopCut, _Row, _Token, _Transform
)


# List of annotations that we don't compute here but that we can safely ignore
ANNOTATIONS_WHITELIST = ['builtin']


def check_referenced_decl(expr: L.Expr) -> L.Decl:
    """
    Wrapper around ``Expr.p_check_referenced_decl``.

    Since we are supposed to lower Lkt code only when it has no semantic error,
    this property should never fail. If it does, there is a bug somewhere in
    Langkit: raise an assertion error that points to the relevant Lkt node.
    """
    try:
        return expr.p_check_referenced_decl
    except L.PropertyError as exc:
        assert False, f"Cannot get referenced decl for {expr}: {exc}"


def same_node(left: L.LktNode, right: L.LktNode) -> bool:
    """
    Return whether ``left`` and ``right`` designate the same node, regardless
    of generic instantiation information.

    .. todo:: This should probably belong to Liblktlang, in one form or
       another. See use cases.
    """
    return left.unit == right.unit and left.sloc_range == right.sloc_range


def pattern_as_str(str_lit: Union[L.StringLit, L.TokenPatternLit]) -> str:
    """
    Return the regexp string associated to this string literal node.
    """
    return json.loads(str_lit.text[1:])


def parse_static_bool(ctx: CompileCtx, expr: L.Expr) -> bool:
    """
    Return the bool value that this expression denotes.
    """
    with ctx.lkt_context(expr):
        check_source_language(isinstance(expr, L.RefId)
                              and expr.text in ('false', 'true'),
                              'Boolean literal expected')

    return expr.text == 'true'


def denoted_char_lit(char_lit: L.CharLit) -> str:
    """
    Return the character that ``char_lit`` denotes.
    """
    text = char_lit.text
    assert text[0] == "'" and text[-1] == "'"
    result = json.loads('"' + text[1:-1] + '"')
    assert len(result) == 1
    return result


def denoted_string_lit(string_lit: Union[L.StringLit, L.TokenLit]) -> str:
    """
    Return the string that ``string_lit`` denotes.
    """
    result = json.loads(string_lit.text)
    assert isinstance(result, str)
    return result


def ada_id_for(n: str) -> names.Name:
    """
    Turn the lower cased name ``n`` into a valid Ada identifier (for code
    generation).
    """
    return names.Name.check_from_lower("ignored" if n == "_" else n)


def load_lkt(lkt_file: str) -> List[L.AnalysisUnit]:
    """
    Load a Lktlang source file and return the closure of Lkt units referenced.
    Raise a DiagnosticError if there are parsing errors.

    :param lkt_file: Name of the file to parse.
    """
    units_map = OrderedDict()
    diagnostics = []

    def process_unit(unit: L.AnalysisUnit) -> None:
        if unit.filename in units_map:
            return

        # Register this unit and its diagnostics
        units_map[unit.filename] = unit
        for d in unit.diagnostics:
            diagnostics.append((unit, d))

        # Recursively process the units it imports. In case of parsing error,
        # just stop the recursion: the collection of diagnostics is enough.
        if not unit.diagnostics:
            assert isinstance(unit.root, L.LangkitRoot)
            import_stmts = list(unit.root.f_imports)
            for imp in import_stmts:
                process_unit(imp.p_referenced_unit)

    # Load ``lkt_file`` and all the units it references, transitively
    process_unit(L.AnalysisContext().get_from_file(lkt_file))

    # If there are diagnostics, forward them to the user. TODO: hand them to
    # langkit.diagnostic.
    if diagnostics:
        for u, d in diagnostics:
            print('{}:{}'.format(os.path.basename(u.filename), d))
        raise DiagnosticError()
    return list(units_map.values())


def find_toplevel_decl(ctx: CompileCtx,
                       lkt_units: List[L.AnalysisUnit],
                       node_type: type,
                       label: str) -> L.FullDecl:
    """
    Look for a top-level declaration of type ``node_type`` in the given units.

    If none or several are found, emit error diagnostics. Return the associated
    full declaration.

    :param lkt_units: List of units where to look.
    :param node_type: Node type to look for.
    :param label: Human readable string for what to look for. Used to create
        diagnostic mesages.
    """
    result = None
    for unit in lkt_units:
        assert isinstance(unit.root, L.LangkitRoot)
        for decl in unit.root.f_decls:
            if not isinstance(decl.f_decl, node_type):
                continue

            with ctx.lkt_context(decl):
                if result is not None:
                    check_source_language(
                        False,
                        'only one {} allowed (previous found at {}:{})'.format(
                            label,
                            os.path.basename(result.unit.filename),
                            result.sloc_range.start
                        )
                    )
                result = decl

    # Report errors on the entry point Lkt source file (no particular line)
    with diagnostic_context(Location(file=lkt_units[0].filename)):
        if result is None:
            error('missing {}'.format(label))

    return result


class AnnotationSpec:
    """
    Synthetic description of how a declaration annotation works.
    """

    def __init__(self, name: str, unique: bool, require_args: bool,
                 default_value: Any = None):
        """
        :param name: Name of the annotation (``foo`` for the ``@foo``
            annotation).
        :param unique: Whether this annotation can appear at most once for a
            given declaration.
        :param require_args: Whether this annotation requires arguments.
        :param default_value: For unique annotations, value to use in case the
            annotation is absent.
        """
        self.name = name
        self.unique = unique
        self.require_args = require_args
        self.default_value = default_value if unique else []

    def interpret(self, ctx: CompileCtx,
                  args: List[L.Expr],
                  kwargs: Dict[str, L.Expr]) -> Any:
        """
        Subclasses must override this in order to interpret an annotation.

        This method must validate and interpret ``args`` and ``kwargs``, and
        return a value suitable for annotations processing.

        :param args: Positional arguments for the annotation.
        :param kwargs: Keyword arguments for the annotation.
        """
        raise NotImplementedError

    def parse_single_annotation(self,
                                ctx: CompileCtx,
                                result: Dict[str, Any],
                                annotation: L.DeclAnnotation) -> None:
        """
        Parse an annotation node according to this spec. Add the result to
        ``result``.
        """
        check_source_language(
            self.name not in result or not self.unique,
            'This annotation cannot appear multiple times'
        )

        # Check that parameters presence comply to the spec
        if not annotation.f_params:
            check_source_language(not self.require_args,
                                  'Arguments required for this annotation')
            value = self.interpret(ctx, [], {})
        else:
            check_source_language(self.require_args,
                                  'This annotation accepts no argument')

            # Collect positional and named arguments
            args = []
            kwargs = {}
            for param in annotation.f_params.f_params:
                with ctx.lkt_context(param):
                    if param.f_name:
                        name = param.f_name.text
                        check_source_language(name not in kwargs,
                                              'Named argument repeated')
                        kwargs[name] = param.f_value

                    else:
                        check_source_language(not kwargs,
                                              'Positional arguments must'
                                              ' appear before named ones')
                        args.append(param.f_value)

            # Evaluate this annotation
            value = self.interpret(ctx, args, kwargs)

        # Store annotation evaluation into the result
        if self.unique:
            result[self.name] = value
        else:
            result.setdefault(self.name, [])
            result[self.name].append(value)


class FlagAnnotationSpec(AnnotationSpec):
    """
    Convenience subclass for flags.
    """
    def __init__(self, name: str):
        super().__init__(name, unique=True, require_args=False,
                         default_value=False)

    def interpret(self,
                  ctx: CompileCtx,
                  args: List[L.Expr],
                  kwargs: Dict[str, L.Expr]) -> Any:
        return True


class SpacingAnnotationSpec(AnnotationSpec):
    """
    Interpreter for @spacing annotations for token families.
    """
    def __init__(self) -> None:
        super().__init__('unparse_spacing', unique=False, require_args=True)

    def interpret(self,
                  ctx: CompileCtx,
                  args: List[L.Expr],
                  kwargs: Dict[str, L.Expr]) -> L.RefId:
        check_source_language(not args, 'No positional argument allowed')

        try:
            expr = kwargs.pop('with')
        except KeyError:
            error('Missing "with" argument')
        else:
            check_source_language(
                not kwargs,
                'Invalid arguments: {}'.format(', '.join(sorted(kwargs)))
            )
            if not isinstance(expr, L.RefId):
                error('Token family name expected')
            return expr


class TokenAnnotationSpec(AnnotationSpec):
    """
    Interpreter for @text/symbol/trivia annotations for tokens.
    """
    def __init__(self, name: str):
        super().__init__(name, unique=True, require_args=True)

    def interpret(self,
                  ctx: CompileCtx,
                  args: List[L.Expr],
                  kwargs: Dict[str, L.Expr]) -> Tuple[bool, bool]:
        check_source_language(not args, 'No positional argument allowed')

        try:
            expr = kwargs.pop('start_ignore_layout')
        except KeyError:
            start_ignore_layout = False
        else:
            start_ignore_layout = parse_static_bool(ctx, expr)

        try:
            expr = kwargs.pop('end_ignore_layout')
        except KeyError:
            end_ignore_layout = False
        else:
            end_ignore_layout = parse_static_bool(ctx, expr)

        check_source_language(
            not kwargs,
            'Invalid arguments: {}'.format(', '.join(sorted(kwargs)))
        )

        return (start_ignore_layout, end_ignore_layout)


class WithLexerAnnotationSpec(AnnotationSpec):
    """
    Interpreter for @with_lexer annotations for grammar declarations.
    """
    def __init__(self) -> None:
        super().__init__('with_lexer', unique=True, require_args=True)

    def interpret(self,
                  ctx: CompileCtx,
                  args: List[L.Expr],
                  kwargs: Dict[str, L.Expr]) -> L.LexerDecl:
        assert not kwargs
        assert len(args) == 1

        lexer_decl = check_referenced_decl(args[0])
        with ctx.lkt_context(args[0]):
            if not isinstance(lexer_decl, L.LexerDecl):
                error(
                    f"lexer expected, got {lexer_decl.p_decl_type_name}"
                )
            return lexer_decl


token_cls_map = {'text': WithText,
                 'trivia': WithTrivia,
                 'symbol': WithSymbol}


@dataclass
class ParsedAnnotations:
    """
    Namespace object to hold annotation parsed values.
    """

    annotations: ClassVar[List[AnnotationSpec]]


@dataclass
class GrammarAnnotations(ParsedAnnotations):
    with_lexer: L.LexerDecl
    annotations = [WithLexerAnnotationSpec()]


@dataclass
class GrammarRuleAnnotations(ParsedAnnotations):
    main_rule: bool
    entry_point: bool
    annotations = [FlagAnnotationSpec('main_rule'),
                   FlagAnnotationSpec('entry_point')]


@dataclass
class TokenAnnotations(ParsedAnnotations):
    text: Tuple[bool, bool]
    trivia: Tuple[bool, bool]
    symbol: Tuple[bool, bool]
    unparse_newline_after: bool
    pre_rule: bool
    ignore: bool
    annotations = [TokenAnnotationSpec('text'),
                   TokenAnnotationSpec('trivia'),
                   TokenAnnotationSpec('symbol'),
                   FlagAnnotationSpec('unparse_newline_after'),
                   FlagAnnotationSpec('pre_rule'),
                   FlagAnnotationSpec('ignore')]


@dataclass
class LexerAnnotations(ParsedAnnotations):
    track_indent: bool
    annotations = [FlagAnnotationSpec('track_indent')]


@dataclass
class TokenFamilyAnnotations(ParsedAnnotations):
    unparse_spacing: List[L.RefId]
    annotations = [SpacingAnnotationSpec()]


@dataclass
class BaseNodeAnnotations(ParsedAnnotations):
    has_abstract_list: bool
    synthetic: bool
    annotations = [
        FlagAnnotationSpec('has_abstract_list'),
        FlagAnnotationSpec('synthetic'),
    ]


@dataclass
class NodeAnnotations(BaseNodeAnnotations):
    abstract: bool
    annotations = BaseNodeAnnotations.annotations + [
        FlagAnnotationSpec('abstract')
    ]


@dataclass
class EnumNodeAnnotations(BaseNodeAnnotations):
    qualifier: bool
    annotations = BaseNodeAnnotations.annotations + [
        FlagAnnotationSpec('qualifier')
    ]


@dataclass
class FieldAnnotations(ParsedAnnotations):
    abstract: bool
    export: bool
    final: bool
    lazy: bool
    null_field: bool
    nullable: bool
    parse_field: bool
    trace: bool
    annotations = [FlagAnnotationSpec('abstract'),
                   FlagAnnotationSpec('export'),
                   FlagAnnotationSpec('final'),
                   FlagAnnotationSpec('lazy'),
                   FlagAnnotationSpec('null_field'),
                   FlagAnnotationSpec('nullable'),
                   FlagAnnotationSpec('parse_field'),
                   FlagAnnotationSpec('trace')]


@dataclass
class EnumAnnotations(ParsedAnnotations):
    annotations: ClassVar[List[AnnotationSpec]] = []


@dataclass
class StructAnnotations(ParsedAnnotations):
    annotations: ClassVar[List[AnnotationSpec]] = []


@dataclass
class FunAnnotations(ParsedAnnotations):
    abstract: bool
    export: bool
    external: bool
    final: bool
    memoized: bool
    trace: bool
    uses_entity_info: bool
    uses_envs: bool
    annotations = [
        FlagAnnotationSpec('abstract'),
        FlagAnnotationSpec('export'),
        FlagAnnotationSpec('external'),
        FlagAnnotationSpec('final'),
        FlagAnnotationSpec('memoized'),
        FlagAnnotationSpec('trace'),
        FlagAnnotationSpec('uses_entity_info'),
        FlagAnnotationSpec('uses_envs'),
    ]


def check_no_annotations(full_decl: L.FullDecl) -> None:
    """
    Check that the declaration has no annotation.
    """
    check_source_language(
        len(full_decl.f_decl_annotations) == 0, 'No annotation allowed'
    )


AnyPA = TypeVar('AnyPA', bound=ParsedAnnotations)


def parse_annotations(ctx: CompileCtx,
                      annotation_class: Type[AnyPA],
                      full_decl: L.FullDecl) -> AnyPA:
    """
    Parse annotations according to the specs in
    ``annotation_class.annotations``. Return a ParsedAnnotations that contains
    the interpreted annotation values for each present annotation.

    :param annotation_class: ParsedAnnotations subclass for the result, holding
        the annotation specs to guide parsing.
    :param full_decl: Declaration whose annotations are to be parsed.
    """
    # Build a mapping for all specs
    specs_map: Dict[str, AnnotationSpec] = {}
    for s in annotation_class.annotations:
        assert s.name not in specs_map
        specs_map[s.name] = s

    # Process annotations
    values: Dict[str, Any] = {}
    for a in full_decl.f_decl_annotations:
        name = a.f_name.text
        spec = specs_map.get(name)
        with ctx.lkt_context(a):
            if spec is None:
                if name not in ANNOTATIONS_WHITELIST:
                    check_source_language(
                        False, 'Invalid annotation: {}'.format(name)
                    )
            else:
                spec.parse_single_annotation(ctx, values, a)

    # Use the default value for absent annotations
    for s in annotation_class.annotations:
        values.setdefault(s.name, s.default_value)

    # Create the namespace object to hold results
    return annotation_class(**values)  # type: ignore


def create_lexer(ctx: CompileCtx, lkt_units: List[L.AnalysisUnit]) -> Lexer:
    """
    Create and populate a lexer from a Lktlang unit.

    :param lkt_units: Non-empty list of analysis units where to look for the
        grammar.
    """
    # Look for the LexerDecl node in top-level lists
    full_lexer = find_toplevel_decl(ctx, lkt_units, L.LexerDecl, 'lexer')
    assert isinstance(full_lexer.f_decl, L.LexerDecl)

    with ctx.lkt_context(full_lexer):
        lexer_annot = parse_annotations(ctx, LexerAnnotations, full_lexer)

    patterns: Dict[names.Name, Tuple[str, Location]] = {}
    """
    Mapping from pattern names to the corresponding regular expression.
    """

    token_family_sets: Dict[names.Name, Tuple[Set[TokenAction], Location]] = {}
    """
    Mapping from token family names to the corresponding sets of tokens that
    belong to this family, and the location for the token family declaration.
    """

    token_families: Dict[names.Name, TokenFamily] = {}
    """
    Mapping from token family names to the corresponding token families.  We
    build this late, once we know all tokens and all families.
    """

    spacings: List[Tuple[names.Name, L.RefId]] = []
    """
    Couple of names for token family between which unparsing must insert
    spaces. The first name is known to be valid, but not the second one, so we
    keep it as a node to create a diagnostic context.
    """

    tokens: Dict[names.Name, Action] = {}
    """
    Mapping from token names to the corresponding tokens.
    """

    rules: List[Union[RuleAssoc, Tuple[Matcher, Action]]] = []
    pre_rules: List[Tuple[Matcher, Action]] = []
    """
    Lists of regular and pre lexing rules for this lexer.
    """

    newline_after: List[TokenAction] = []
    """
    List of tokens after which we must introduce a newline during unparsing.
    """

    def ignore_constructor(start_ignore_layout: bool,
                           end_ignore_layout: bool) -> Action:
        """
        Adapter to build a Ignore instance with the same API as WithText
        constructors.
        """
        del start_ignore_layout, end_ignore_layout
        return Ignore()

    def process_family(f: L.LexerFamilyDecl) -> None:
        """
        Process a LexerFamilyDecl node. Register the token family and process
        the rules it contains.
        """
        with ctx.lkt_context(f):
            # Create the token family, if needed
            name = names.Name.check_from_lower(f.f_syn_name.text)
            token_set, _ = token_family_sets.setdefault(
                name,
                (set(), Location.from_lkt_node(f)),
            )

            for r in f.f_rules:
                if not isinstance(r.f_decl, L.GrammarRuleDecl):
                    error('Only lexer rules allowed in family blocks')
                process_token_rule(r, token_set)

            family_annotations = parse_annotations(ctx, TokenFamilyAnnotations,
                                                   cast(L.FullDecl, f.parent))

            for spacing in family_annotations.unparse_spacing:
                spacings.append((name, spacing))

    def process_token_rule(
        r: L.FullDecl,
        token_set: Optional[Set[TokenAction]] = None
    ) -> None:
        """
        Process the full declaration of a GrammarRuleDecl node: create the
        token it declares and lower the optional associated lexing rule.

        :param r: Full declaration for the GrammarRuleDecl to process.
        :param token_set: If this declaration appears in the context of a token
            family, this adds the new token to this set.  Must be left to None
            otherwise.
        """
        with ctx.lkt_context(r):
            rule_annot: TokenAnnotations = parse_annotations(
                ctx, TokenAnnotations, r
            )

            # Gather token action info from the annotations. If absent,
            # fallback to WithText.
            token_cons = None
            start_ignore_layout = False
            end_ignore_layout = False
            if rule_annot.ignore:
                token_cons = ignore_constructor
            for name in ('text', 'trivia', 'symbol'):
                annot = getattr(rule_annot, name)
                if not annot:
                    continue
                start_ignore_layout, end_ignore_layout = annot

                check_source_language(token_cons is None,
                                      'At most one token action allowed')
                token_cons = token_cls_map[name]
            is_pre = rule_annot.pre_rule
            if token_cons is None:
                token_cons = WithText

            # Create the token and register it where needed: the global token
            # mapping, its token family (if any) and the "newline_after" group
            # if the corresponding annotation is present.
            token_lower_name = r.f_decl.f_syn_name.text
            token_name = (
                None
                if token_lower_name == "_"
                else names.Name.check_from_lower(token_lower_name)
            )

            check_source_language(
                token_lower_name not in ('termination', 'lexing_failure'),
                '{} is a reserved token name'.format(token_lower_name)
            )
            check_source_language(token_name not in tokens,
                                  'Duplicate token name')

            token = token_cons(start_ignore_layout, end_ignore_layout)
            if token_name is not None:
                tokens[token_name] = token
            if isinstance(token, TokenAction):
                if token_set is not None:
                    token_set.add(token)
                if rule_annot.unparse_newline_after:
                    newline_after.append(token)

            # Lower the lexing rule, if present
            assert isinstance(r.f_decl, L.GrammarRuleDecl)
            matcher_expr = r.f_decl.f_expr
            if matcher_expr is not None:
                rule = (lower_matcher(matcher_expr), token)
                if is_pre:
                    pre_rules.append(rule)
                else:
                    rules.append(rule)

    def process_pattern(full_decl: L.FullDecl) -> None:
        """
        Process a pattern declaration.

        :param full_decl: Full declaration for the ValDecl to process.
        """
        check_no_annotations(full_decl)
        decl = full_decl.f_decl
        assert isinstance(decl, L.ValDecl)
        lower_name = decl.f_syn_name.text
        name = names.Name.check_from_lower(lower_name)

        with ctx.lkt_context(decl):
            check_source_language(name not in patterns,
                                  'Duplicate pattern name')
            with ctx.lkt_context(decl.f_decl_type):
                check_source_language(
                    decl.f_decl_type is None,
                    "Types are not allowed in lexer declarations"
                )
            if (
                not isinstance(decl.f_val, L.StringLit)
                or not decl.f_val.p_is_regexp_literal
            ):
                error('Pattern string literal expected')
            # TODO: use StringLit.p_denoted_value when properly implemented
            patterns[name] = (
                pattern_as_str(decl.f_val), Location.from_lkt_node(decl)
            )

    def lower_matcher(expr: L.GrammarExpr) -> Matcher:
        """
        Lower a token matcher to our internals.
        """
        loc = Location.from_lkt_node(expr)
        with ctx.lkt_context(expr):
            if isinstance(expr, L.TokenLit):
                return Literal(json.loads(expr.text), location=loc)
            elif isinstance(expr, L.TokenNoCaseLit):
                return NoCaseLit(json.loads(expr.text), location=loc)
            elif isinstance(expr, L.TokenPatternLit):
                return Pattern(pattern_as_str(expr), location=loc)
            else:
                error('Invalid lexing expression')

    def lower_token_ref(ref: L.RefId) -> Action:
        """
        Return the Token that `ref` refers to.
        """
        with ctx.lkt_context(ref):
            token_name = names.Name.check_from_lower(ref.text)
            check_source_language(token_name in tokens,
                                  'Unknown token: {}'.format(token_name.lower))
            return tokens[token_name]

    def lower_case_alt(alt: L.BaseLexerCaseRuleAlt) -> Alt:
        """
        Lower the alternative of a case lexing rule.
        """
        prev_token_cond = None
        if isinstance(alt, L.LexerCaseRuleCondAlt):
            prev_token_cond = [lower_token_ref(ref)
                               for ref in alt.f_cond_exprs]
        return Alt(prev_token_cond=prev_token_cond,
                   send=lower_token_ref(alt.f_send.f_sent),
                   match_size=int(alt.f_send.f_match_size.text))

    # Go through all rules to register tokens, their token families and lexing
    # rules.
    for full_decl in full_lexer.f_decl.f_rules:
        with ctx.lkt_context(full_decl):
            if isinstance(full_decl, L.FullDecl):
                # There can be various types of declarations in lexers...
                decl = full_decl.f_decl

                if isinstance(decl, L.GrammarRuleDecl):
                    # Here, we have a token declaration, potentially associated
                    # with a lexing rule.
                    process_token_rule(full_decl)

                elif isinstance(decl, L.ValDecl):
                    # This is the declaration of a pattern
                    process_pattern(full_decl)

                elif isinstance(decl, L.LexerFamilyDecl):
                    # This is a family block: go through all declarations
                    # inside it.
                    process_family(decl)

                else:
                    check_source_language(False,
                                          'Unexpected declaration in lexer')

            elif isinstance(full_decl, L.LexerCaseRule):
                syn_alts = list(full_decl.f_alts)

                # This is a rule for conditional lexing: lower its matcher and
                # its alternative rules.
                matcher = lower_matcher(full_decl.f_expr)
                check_source_language(
                    len(syn_alts) == 2 and
                    isinstance(syn_alts[0], L.LexerCaseRuleCondAlt) and
                    isinstance(syn_alts[1], L.LexerCaseRuleDefaultAlt),
                    'Invalid case rule topology'
                )
                rules.append(Case(matcher,
                                  lower_case_alt(syn_alts[0]),
                                  lower_case_alt(syn_alts[1])))

            else:
                # The grammar should make the following dead code
                assert False, 'Invalid lexer rule: {}'.format(full_decl)

    # Create the LexerToken subclass to define all tokens and token families
    items: Dict[str, Union[Action, TokenFamily]] = {}
    for name, token in tokens.items():
        items[name.camel] = token
    for name, (token_set, loc) in token_family_sets.items():
        tf = TokenFamily(*list(token_set), location=loc)
        token_families[name] = tf
        items[name.camel] = tf
    token_class = type('Token', (LexerToken, ), items)

    # Create the Lexer instance and register all patterns and lexing rules
    result = Lexer(token_class,
                   lexer_annot.track_indent,
                   pre_rules)
    for name, (regexp, loc) in patterns.items():
        result._add_pattern(name.lower, regexp, location=loc)
    result.add_rules(*rules)

    # Register spacing/newline rules
    for f1_name, f2_ref in spacings:
        f2_name = names.Name.check_from_lower(f2_ref.text)
        with ctx.lkt_context(f2_ref):
            check_source_language(
                f2_name in token_families,
                'Unknown token family: {}'.format(f2_name.lower)
            )
        result.add_spacing((token_families[f1_name],
                            token_families[f2_name]))
    result.add_newline_after(*newline_after)

    return result


def extract_doc(doc: L.Doc) -> str:
    """
    Turn a Doc node into the corresponding doc string.
    """
    # Extract the text from all doc lines
    lines: List[str] = []
    for d_line in doc.f_lines:
        line = d_line.text
        assert line.startswith("##")
        lines.append(line[2:])

    # Remove the biggest common indentation
    if lines:
        common_indent = min(
            len(line) - len(line.lstrip(" "))
            for line in lines
        )
        lines = [line[common_indent:] for line in lines]

    return "\n".join(lines)


def create_grammar(ctx: CompileCtx,
                   lkt_units: List[L.AnalysisUnit]) -> Grammar:
    """
    Create a grammar from a set of Lktlang units.

    Note that this only initializes a grammar and fetches relevant declarations
    in the Lktlang unit. The actual lowering on grammar rules happens in a
    separate pass: see lower_all_lkt_rules.

    :param lkt_units: Non-empty list of analysis units where to look for the
        grammar.
    """
    # Look for the GrammarDecl node in top-level lists
    full_grammar = find_toplevel_decl(ctx, lkt_units, L.GrammarDecl, 'grammar')
    assert isinstance(full_grammar.f_decl, L.GrammarDecl)

    with ctx.lkt_context(full_grammar):
        parse_annotations(ctx, GrammarAnnotations, full_grammar)

    # Collect the list of grammar rules. This is where we check that we only
    # have grammar rules, that their names are unique, and that they have valid
    # annotations.
    all_rules = OrderedDict()
    main_rule_name = None
    entry_points: Set[str] = set()
    for full_rule in full_grammar.f_decl.f_rules:
        with ctx.lkt_context(full_rule):
            r = full_rule.f_decl

            if not isinstance(r, L.GrammarRuleDecl):
                error(f"grammar rule expected, got {r.p_decl_type_name}")
            rule_name = r.f_syn_name.text

            # Register this rule as a main rule or an entry point if the
            # corresponding annotations are present.
            anns = parse_annotations(ctx, GrammarRuleAnnotations, full_rule)
            if anns.main_rule:
                check_source_language(
                    main_rule_name is None,
                    "only one main rule allowed",
                )
                main_rule_name = rule_name
            if anns.main_rule or anns.entry_point:
                entry_points.add(rule_name)

            all_rules[rule_name] = (full_rule.f_doc, r.f_expr)

    # Now create the result grammar
    if main_rule_name is None:
        with ctx.lkt_context(full_grammar.f_decl):
            error("main rule missing (@main_rule annotation)")
    result = Grammar(
        main_rule_name, entry_points, Location.from_lkt_node(full_grammar)
    )

    # Translate rules (all_rules) later, as node types are not available yet
    result._all_lkt_rules.update(all_rules)
    return result


def lower_grammar_rules(ctx: CompileCtx) -> None:
    """
    Translate syntactic L rules to Parser objects.
    """
    lexer = ctx.lexer
    assert lexer is not None

    lexer_tokens = lexer.tokens
    assert lexer_tokens is not None

    grammar = ctx.grammar
    assert grammar is not None

    # Build a mapping for all tokens registered in the lexer. Use lower case
    # names, as this is what the concrete syntax is supposed to use.
    tokens = {cast(names.Name, token.name).lower: token
              for token in lexer_tokens.tokens}

    # Build a mapping for all nodes created in the DSL. We cannot use T (the
    # TypeRepo instance) as types are not processed yet.
    nodes = {n.raw_name.camel: n
             for n in CompiledTypeRepo.astnode_types}

    # For every non-qualifier enum node, build a mapping from value names
    # (camel cased) to the corresponding enum node subclass.
    enum_nodes = {
        node: node._alternatives_map
        for node in nodes.values()
        if node.is_enum_node and not node.is_bool_node
    }

    NodeRefTypes = Union[L.DotExpr, L.TypeRef, L.RefId]

    def resolve_node_ref_or_none(
        node_ref: Optional[NodeRefTypes]
    ) -> Optional[ASTNodeType]:
        """
        Convenience wrapper around resolve_node_ref to handle None values.
        """
        if node_ref is None:
            return None
        return resolve_node_ref(node_ref)

    def resolve_node_ref(node_ref: NodeRefTypes) -> ASTNodeType:
        """
        Helper to resolve a node name to the actual AST node.

        :param node_ref: Node that is the reference to the AST node.
        """
        if isinstance(node_ref, L.DotExpr):
            # Get the altenatives mapping for the prefix_node enum node
            prefix_node = resolve_node_ref(cast(NodeRefTypes,
                                                node_ref.f_prefix))
            with ctx.lkt_context(node_ref.f_prefix):
                try:
                    alt_map = enum_nodes[prefix_node]
                except KeyError:
                    error('Non-qualifier enum node expected (got {})'
                          .format(prefix_node.dsl_name))

            # Then resolve the alternative
            suffix = node_ref.f_suffix.text
            with ctx.lkt_context(node_ref.f_suffix):
                try:
                    return alt_map[suffix]
                except KeyError:
                    error('Unknown enum node alternative: {}'.format(suffix))

        elif isinstance(node_ref, L.GenericTypeRef):
            with ctx.lkt_context(node_ref.f_type_name):
                check_source_language(
                    node_ref.f_type_name.text == u'ASTList',
                    'Bad generic type name: only ASTList is valid in this'
                    ' context'
                )

            params = node_ref.f_params
            with ctx.lkt_context(node_ref):
                check_source_language(
                    len(params) == 2,
                    '2 type arguments expected, got {}'.format(len(params))
                )
            node_params = [resolve_node_ref(cast(NodeRefTypes, p))
                           for p in params]
            assert node_params[0] == T.root_node
            return node_params[1].list

        elif isinstance(node_ref, L.SimpleTypeRef):
            return resolve_node_ref(cast(NodeRefTypes, node_ref.f_type_name))

        else:
            assert isinstance(node_ref, L.RefId)
            with ctx.lkt_context(node_ref):
                node_name = node_ref.text
                try:
                    return nodes[node_name]
                except KeyError:
                    error('Unknown node: {}'.format(node_name))

        raise RuntimeError("unreachable code")

    def lower_or_none(
        rule: Union[None, L.GrammarExpr, L.GrammarExprList]
    ) -> Optional[Parser]:
        """
        Like ``lower``, but also accept null grammar expressions.
        """
        return None if rule is None else lower(rule)

    def lower(
        rule: Union[L.GrammarExpr, L.GrammarExprList]
    ) -> Parser:
        """
        Helper to lower one parser.

        :param rule: Grammar rule to lower.
        """
        loc = Location.from_lkt_node(rule)
        with ctx.lkt_context(rule):
            if isinstance(rule, L.ParseNodeExpr):
                node = resolve_node_ref(cast(NodeRefTypes, rule.f_node_name))

                # Lower the subparsers
                subparsers = [lower(subparser)
                              for subparser in rule.f_sub_exprs]

                # Qualifier nodes are a special case: we produce one subclass
                # or the other depending on whether the subparsers accept the
                # input.
                if node.is_bool_node:
                    return Opt(*subparsers, location=loc).as_bool(node)

                # Likewise for enum nodes
                elif node.base and node.base.is_enum_node:
                    return _Transform(_Row(*subparsers, location=loc),
                                      node,
                                      location=loc)

                # For other nodes, always create the node when the subparsers
                # accept the input.
                else:
                    return _Transform(parser=_Row(*subparsers), typ=node,
                                      location=loc)

            elif isinstance(rule, L.TokenRef):
                token_name = rule.f_token_name.text
                try:
                    val = tokens[token_name]
                except KeyError:
                    with ctx.lkt_context(rule.f_token_name):
                        error(f"Unknown token: {token_name}")

                match_text = ''
                if rule.f_expr:
                    # The grammar is supposed to mainain this invariant
                    assert isinstance(rule.f_expr, L.TokenLit)
                    match_text = denoted_string_lit(rule.f_expr)

                return _Token(val=val, match_text=match_text, location=loc)

            elif isinstance(rule, L.TokenLit):
                return _Token(denoted_string_lit(rule), location=loc)

            elif isinstance(rule, L.GrammarList):
                return PList(
                    lower(rule.f_expr),
                    empty_valid=rule.f_kind.text == '*',
                    list_cls=resolve_node_ref_or_none(rule.f_list_type),
                    sep=lower_or_none(rule.f_sep),
                    location=loc
                )

            elif isinstance(rule, (L.GrammarImplicitPick,
                                   L.GrammarPick)):
                return Pick(*[lower(subparser) for subparser in rule.f_exprs],
                            location=loc)

            elif isinstance(rule, L.GrammarRuleRef):
                assert grammar is not None
                rule_name = rule.f_node_name.text
                return Defer(rule_name,
                             grammar.rule_resolver(rule_name),
                             location=loc)

            elif isinstance(rule, L.GrammarOrExpr):
                return Or(*[lower(subparser)
                            for subparser in rule.f_sub_exprs],
                          location=loc)

            elif isinstance(rule, L.GrammarOpt):
                return Opt(lower(rule.f_expr), location=loc)

            elif isinstance(rule, L.GrammarOptGroup):
                return Opt(*[lower(subparser) for subparser in rule.f_expr],
                           location=loc)

            elif isinstance(rule, L.GrammarExprList):
                return Pick(*[lower(subparser) for subparser in rule],
                            location=loc)

            elif isinstance(rule, L.GrammarDiscard):
                return Discard(lower(rule.f_expr), location=loc)

            elif isinstance(rule, L.GrammarNull):
                return Null(resolve_node_ref(rule.f_name), location=loc)

            elif isinstance(rule, L.GrammarSkip):
                return Skip(resolve_node_ref(rule.f_name), location=loc)

            elif isinstance(rule, L.GrammarDontSkip):
                return DontSkip(lower(rule.f_expr),
                                lower(rule.f_dont_skip),
                                location=loc)

            elif isinstance(rule, L.GrammarCut):
                return Cut()

            elif isinstance(rule, L.GrammarStopCut):
                return StopCut(lower(rule.f_expr))

            elif isinstance(rule, L.GrammarPredicate):
                if not isinstance(rule.f_prop_ref, L.DotExpr):
                    error('Invalid property reference')
                node = resolve_node_ref(cast(NodeRefTypes,
                                             rule.f_prop_ref.f_prefix))
                prop_name = rule.f_prop_ref.f_suffix.text
                try:
                    prop = node.get_abstract_node_data_dict()[prop_name]
                except KeyError:
                    check_source_language(
                        False,
                        '{} has no {} property'
                        .format(node.dsl_name, prop_name)
                    )
                return Predicate(lower(rule.f_expr), prop, location=loc)

            else:
                raise NotImplementedError('unhandled parser: {}'.format(rule))

    for name, (rule_doc, rule_expr) in grammar._all_lkt_rules.items():
        grammar._add_rule(name, lower(rule_expr), extract_doc(rule_doc))


# Mapping to associate declarations to the corresponding AbstractVariable
# instances. This is useful when lowering expressions.
LocalsEnv = Dict[L.BaseValDecl, AbstractVariable]


class LktTypesLoader:
    """
    Helper class to instantiate ``CompiledType`` for all types described in
    Lkt.
    """

    # Map Lkt type declarations to the corresponding CompiledType instances, or
    # to None when the type declaration is currently being lowered. Keeping a
    # None entry in this case helps detecting illegal circular type
    # dependencies.
    compiled_types: Dict[L.TypeDecl, Optional[CompiledType]]

    # Map Lkt type declarations to TypeRepo.Defer instances that resolve to the
    # corresponding CompiledType instances.
    type_refs: Dict[L.TypeDecl, TypeRepo.Defer]

    def __init__(self, ctx: CompileCtx, lkt_units: List[L.AnalysisUnit]):
        """
        :param ctx: Context in which to create these types.
        :param lkt_units: Non-empty list of analysis units where to look for
            type declarations.
        """
        self.ctx = ctx
        self.type_refs = {}

        root = lkt_units[0].root

        def get_field(decl: L.NamedTypeDecl, name: str) -> L.Decl:
            """
            Return the (assumed existing and unique) declaration called
            ``name`` nested in ``decl``.
            """
            decls = [fd.f_decl
                     for fd in decl.f_decls
                     if fd.f_decl.p_name == name]
            assert len(decls) == 1, str(decls)
            return decls[0]

        # Pre-fetch the declaration of generic types so that resolve_type_decl
        # has an efficient access to them.
        self.analysis_unit_trait = root.p_analysis_unit_trait
        self.array_type = root.p_array_type
        self.astlist_type = root.p_astlist_type
        self.error_node_trait = root.p_error_node_trait
        self.iterator_trait = root.p_iterator_trait
        self.node_trait = root.p_node_trait
        self.property_error_type = root.p_property_error_type
        self.string_type = root.p_string_type
        self.token_node_trait = root.p_token_node_trait

        self.find_method = get_field(self.iterator_trait, 'find')
        self.map_method = get_field(self.iterator_trait, 'map')
        self.to_symbol_method = get_field(self.string_type, 'to_symbol')
        self.unique_method = get_field(self.array_type, 'unique')

        # Map Lkt nodes for the declarations of builtin types to the
        # corresponding CompiledType instances.
        self.compiled_types = {
            root.p_char_type: T.Character,
            root.p_int_type: T.Int,
            root.p_bool_type: T.Bool,
            root.p_bigint_type: T.BigInt,
            root.p_string_type: T.String,
            root.p_symbol_type: T.Symbol,
        }

        # Go through all units, build a map for all type definitions, indexed
        # by name. This first pass allows to check for type name unicity.
        named_type_decls: Dict[str, L.FullDecl] = {}
        for unit in lkt_units:
            assert isinstance(unit.root, L.LangkitRoot)
            for full_decl in unit.root.f_decls:
                if not isinstance(full_decl.f_decl, L.TypeDecl):
                    continue
                name = full_decl.f_decl.f_syn_name.text
                check_source_language(
                    name not in named_type_decls,
                    'Duplicate type name: {}'.format(name)
                )
                named_type_decls[name] = full_decl

        # Now create CompiledType instances for each user type. To properly
        # handle node derivation, this recurses on bases first and reject
        # inheritance loops.
        for _, decl in sorted(named_type_decls.items()):
            assert isinstance(decl.f_decl, L.TypeDecl)
            self.lower_type_decl(decl.f_decl)

    def resolve_type_decl(self,
                          decl: L.TypeDecl,
                          force_lowering: bool = False) -> CompiledTypeOrDefer:
        """
        Fetch the CompiledType instance corresponding to the given type
        declaration.

        When ``force_lowering`` is ``False``, if ``decl`` is not lowered yet,
        return an appropriate TypeRepo.Defer instance instead.

        :param decl: Lkt type declaration to resolve.
        """
        result: Optional[CompiledTypeOrDefer]

        # First, look for an actual CompiledType instance
        result = self.compiled_types.get(decl)
        if result is not None:
            return result

        # Not found: unless lowering is forced, look now for an existing
        # TypeRepo.Defer instance.
        if not force_lowering:
            result = self.type_refs.get(decl)
            if result is not None:
                return result

        # If this is an instantiated generic type, try to build the
        # corresponding CompiledType from the type actuals.
        if isinstance(decl, L.InstantiatedGenericType):
            inner_type = decl.p_get_inner_type
            actuals = decl.p_get_actuals

            if inner_type == self.array_type:
                assert len(actuals) == 1
                result = self.resolve_type_decl(
                    actuals[0], force_lowering
                ).array

            elif inner_type == self.iterator_trait:
                assert len(actuals) == 1
                result = self.resolve_type_decl(
                    actuals[0], force_lowering
                ).iterator

            elif inner_type == self.astlist_type:
                assert len(actuals) == 2
                root_node = actuals[0]
                node = self.resolve_type_decl(
                    actuals[1], force_lowering=force_lowering
                )

                # Make sure that "root_node" is indeed a root node (a class
                # with no base type). Lkt type checking as already supposed to
                # make sure that "node" is a class (i.e. a node), and lowering
                # already checks that there is exactly one node types
                # hierarchy.
                check_source_language(
                    isinstance(root_node, L.ClassDecl)
                    and root_node.f_syn_base_type is None,
                    "In ASTList[N1, N2], N1 must be the root node"
                )

                assert isinstance(node, (ASTNodeType, TypeRepo.Defer))
                result = node.list

            elif inner_type == self.analysis_unit_trait:
                result = T.AnalysisUnit

            else:
                assert False, (
                    'Unknown generic type: {} (from {})'
                    .format(inner_type, decl)
                )

        # Otherwise, "decl" is not lowered yet: create a Defer object or lower
        # it depending on "force_lowering".
        else:
            assert isinstance(decl, L.NamedTypeDecl)
            result = (
                self.lower_type_decl(decl)
                if force_lowering else
                getattr(T, decl.f_syn_name.text)
            )

        if isinstance(result, TypeRepo.Defer):
            assert not force_lowering
            self.type_refs[decl] = result
        else:
            assert isinstance(result, CompiledType)
            self.compiled_types[decl] = result
        return result

    def lower_type_decl(self, decl: L.TypeDecl) -> CompiledType:
        """
        Create the CompiledType instance corresponding to the given Lkt type
        declaration. Do nothing if it has been already lowered, and stop with
        an error if the lowering for this type is already running (case of
        invalid circular type dependency).
        """
        with self.ctx.lkt_context(decl):
            # Sentinel for the dict lookup below, as compiled_type can contain
            # None entries.
            try:
                t = self.compiled_types[decl]
            except KeyError:
                # The type is not lowered yet: let's do it. Add the sentinel to
                # reject type inheritance loop during recursion.
                self.compiled_types[decl] = None
            else:
                if t is None:
                    error('Type inheritance loop detected')
                else:
                    # The type is already lowered: there is nothing to do
                    return t

            check_source_language(
                isinstance(decl, L.BasicClassDecl)
                or decl.f_traits is None
                or len(decl.f_traits) == 0,
                'No traits allowed except on nodes'
            )

            # Dispatch now to the appropriate lowering helper
            result: CompiledType
            if isinstance(decl, L.InstantiatedGenericType):
                # At this stage, the only generic types should come from the
                # prelude (Array, ASTList), so there is no need to do anything
                # special for them. However the type actuals must be lowered so
                # that we can return a compiled type, and not a Defer object.
                resolved = self.resolve_type_decl(decl, force_lowering=True)
                assert isinstance(resolved, CompiledType)
                result = resolved
            else:
                full_decl = decl.parent
                assert isinstance(full_decl, L.FullDecl)
                if isinstance(decl, L.BasicClassDecl):

                    specs = (EnumNodeAnnotations
                             if isinstance(decl, L.EnumClassDecl)
                             else NodeAnnotations)
                    result = self.create_node(
                        decl, parse_annotations(self.ctx, specs, full_decl)
                    )

                elif isinstance(decl, L.EnumTypeDecl):
                    result = self.create_enum(
                        decl,
                        parse_annotations(self.ctx, EnumAnnotations, full_decl)
                    )

                elif isinstance(decl, L.StructDecl):
                    result = self.create_struct(
                        decl,
                        parse_annotations(
                            self.ctx, StructAnnotations, full_decl
                        )
                    )

                else:
                    raise NotImplementedError(
                        'Unhandled type declaration: {}'.format(decl)
                    )

            self.compiled_types[decl] = result
            return result

    def lower_base_field(
        self,
        full_decl: L.FullDecl,
        allowed_field_types: Tuple[Type[AbstractNodeData], ...]
    ) -> AbstractNodeData:
        """
        Lower the field described in ``decl``.

        :param allowed_field_types: Set of types allowed for the fields to
            load.
        """
        decl = full_decl.f_decl
        assert isinstance(decl, L.FieldDecl)
        annotations = parse_annotations(self.ctx, FieldAnnotations, full_decl)
        field_type = self.resolve_type_decl(decl.f_decl_type.p_designated_type)
        doc = self.ctx.lkt_doc(full_decl)

        cls: Type[AbstractNodeData]
        constructor: Callable[..., AbstractNodeData]
        kwargs: Dict[str, Any] = {'type': field_type, 'doc': doc}

        check_source_language(
            annotations.parse_field or not annotations.null_field,
            '@nullable is valid only for parse fields'
        )

        if annotations.lazy:
            check_source_language(
                not annotations.null_field,
                'Lazy fields cannot be null'
            )
            check_source_language(
                not annotations.final,
                'Lazy fields are implicitly final'
            )
            cls = PropertyDef
            constructor = create_lazy_field

            _, expr, local_vars = self.lower_property_expr(
                abstract=annotations.abstract,
                external=False,
                arg_decl_list=None,
                body=decl.f_default_val,
            )

            kwargs = {
                'expr': expr,
                'doc': doc,
                'public': annotations.export,
                'return_type': field_type,
                'kind': (AbstractKind.abstract
                         if annotations.abstract
                         else AbstractKind.concrete),
                'activate_tracing': annotations.trace,
                'local_vars': local_vars,
            }

        elif annotations.parse_field:
            assert decl.f_default_val is None
            check_source_language(
                not annotations.export,
                'Parse fields are implicitly exported'
            )
            check_source_language(
                not annotations.final,
                'Concrete parse fields are implicitly final'
            )
            check_source_language(
                not annotations.lazy,
                'Parse fields cannot be lazy'
            )
            check_source_language(
                not annotations.trace,
                'Parse fields cannot be traced'
            )
            cls = constructor = Field
            kwargs['abstract'] = annotations.abstract
            kwargs['null'] = annotations.null_field
            kwargs['nullable'] = annotations.nullable

        else:
            check_source_language(
                not annotations.abstract,
                'Regular fields cannot be abstract'
            )
            check_source_language(
                not annotations.export,
                'Regular fields are implicitly exported'
            )
            check_source_language(
                not annotations.final,
                'Regular fields are implicitly final'
            )
            check_source_language(
                not annotations.lazy,
                'Regular fields cannot be lazy'
            )
            check_source_language(
                not annotations.null_field,
                'Regular fields cannot be null'
            )
            check_source_language(
                not annotations.trace,
                'Regular fields cannot be traced'
            )
            cls = constructor = UserField
            kwargs['default_value'] = (
                self.lower_expr(decl.f_default_val, {}, None)
                if decl.f_default_val
                else None
            )

        check_source_language(
            issubclass(cls, allowed_field_types),
            'Invalid field type in this context'
        )

        return constructor(**kwargs)

    def lower_expr(self,
                   expr: L.Expr,
                   env: LocalsEnv,
                   local_vars: Optional[LocalVars]) -> AbstractExpression:
        """
        Lower the given expression.

        :param expr: Expression to lower.
        :param env: Variable to use when resolving references.
        :param local_vars: If lowering a property expression, set of local
            variables for this property.
        """
        # Counter to generate unique names
        counter = itertools.count(0)

        def var_for_lambda_arg(
            arg: L.LambdaArgDecl,
            prefix: str,
            type: Optional[CompiledType] = None
        ) -> AbstractVariable:
            """
            Create an AbstractVariable to translate a lambda argument.

            This also registers this decl/variable association in ``env``.

            :param prefix: Lower-case prefix for the name of the variable in
                the generated code.
            """
            assert arg not in env
            source_name = arg.f_syn_name.text
            result = AbstractVariable(
                names.Name.check_from_lower(f"{prefix}_{next(counter)}"),
                source_name=source_name,
                type=type,
            )
            env[arg] = result
            return result

        def extract_call_args(expr: L.CallExpr) -> Tuple[List[L.Expr],
                                                         Dict[str, L.Expr]]:
            """
            Extract positional and keyword arguments from a call expression.
            """
            args = []
            kwargs = {}
            for arg in expr.f_args:
                value = arg.f_value
                if arg.f_name:
                    kwargs[arg.f_name.text] = value
                else:
                    args.append(value)
            return args, kwargs

        def is_array_expr(expr: L.Expr) -> bool:
            """
            Return whether ``expr`` computes an array.
            """
            expr_type = self.resolve_type_decl(
                expr.p_check_expr_type,
                force_lowering=True,
            )
            assert isinstance(expr_type, CompiledType)
            return expr_type.is_array_type

        def lower(expr: L.Expr) -> AbstractExpression:
            """
            Wrapper around "_lower" to set the expression location.

            Calling this function instead of ``_lower`` below to lower
            individual expression nodes is what correctly assigns the Lkt
            location to each instantiated ``AbstractExpression``.
            """
            with AbstractExpression.with_location(
                Location.from_lkt_node(expr)
            ):
                return _lower(expr)

        def _lower(expr: L.Expr) -> AbstractExpression:
            """
            Do the actual expression lowering. Since all recursive calls use
            the same environment, this helper allows to skip passing it.
            """
            result: AbstractExpression

            if isinstance(expr, L.ArrayLiteral):
                elts = [lower(e) for e in expr.f_exprs]
                array_type = self.resolve_type_decl(expr.p_check_expr_type)
                return E.ArrayLiteral(elts,
                                      element_type=array_type.element_type)

            elif isinstance(expr, L.BinOp):
                # Lower both operands
                left = lower(expr.f_left)
                right = lower(expr.f_right)

                # Dispatch to the appropriate abstract expression constructor
                if isinstance(expr.f_op, L.OpEq):
                    return E.Eq(left, right)

                elif isinstance(expr.f_op, (L.OpLt, L.OpGt, L.OpLte, L.OpGte)):
                    operator = {
                        L.OpLt: E.OrderingTest.LT,
                        L.OpLte: E.OrderingTest.LE,
                        L.OpGt: E.OrderingTest.GT,
                        L.OpGte: E.OrderingTest.GE,
                    }[type(expr.f_op)]
                    return E.OrderingTest(operator, left, right)

                elif (
                    isinstance(expr.f_op, L.OpAmp)
                    and is_array_expr(expr.f_left)
                ):
                    assert is_array_expr(expr.f_right)
                    return left.concat(right)  # type: ignore

                else:
                    operator = {
                        L.OpAmp: '&',
                        L.OpAnd: '&',
                        L.OpOr: '|',
                        L.OpPlus: '+',
                        L.OpMinus: '-',
                        L.OpMult: '*',
                        L.OpDiv: '/',
                    }[type(expr.f_op)]
                    return E.Arithmetic(left, right, operator)

            elif isinstance(expr, L.BlockExpr):
                assert local_vars is not None

                # Lower declarations for this block
                vars = []
                var_exprs = []

                for v in expr.f_val_defs:
                    if isinstance(v, L.ValDecl):
                        source_name = v.f_syn_name.text
                        v_name = ada_id_for(source_name)
                        v_type = (
                            self.resolve_type_decl(
                                v.f_decl_type.p_designated_type
                            )
                            if v.f_decl_type
                            else None
                        )
                        var = AbstractVariable(
                            v_name, v_type, source_name=source_name
                        )
                        vars.append(var)
                        var_exprs.append(lower(v.f_val))

                        # Make this variable available to the inner expression
                        # lowering, and register it as a local variable in the
                        # generated code.
                        env[v] = var
                        var.local_var = local_vars.create_scopeless(
                            v_name, v_type
                        )

                    else:
                        assert False, f'Unhandled def in BlockExpr: {v}'

                # Then lower the block main expression
                inner_expr = lower(expr.f_expr)

                return Let((vars, var_exprs, inner_expr))

            elif isinstance(expr, L.CallExpr):
                # Depending on its name, a call can have different meanings
                name_decl = check_referenced_decl(expr.f_name)
                call_expr = expr

                def lower_args() -> Tuple[List[AbstractExpression],
                                          Dict[str, AbstractExpression]]:
                    """
                    Collect call positional and keyword arguments.
                    """
                    arg_nodes, kwarg_nodes = extract_call_args(call_expr)
                    args = [lower(v) for v in arg_nodes]
                    kwargs = {k: lower(v) for k, v in kwarg_nodes.items()}
                    return args, kwargs

                if isinstance(name_decl, L.StructDecl):
                    # If the name refers to a struct type, this expression
                    # create a struct value.
                    struct_type = self.resolve_type_decl(name_decl)
                    args, kwargs = lower_args()
                    assert not args
                    return E.New(struct_type, **kwargs)

                elif same_node(name_decl, self.find_method):
                    # Build variable for the iteration variable from the lambda
                    # expression arguments.
                    assert len(call_expr.f_args) == 1
                    lambda_expr = call_expr.f_args[0].f_value
                    assert isinstance(lambda_expr, L.LambdaExpr)
                    lambda_args = lambda_expr.f_params

                    # We expect excatly one argument: the collection element
                    elt_arg, = lambda_args
                    elt_var = var_for_lambda_arg(elt_arg, 'item')

                    # Lower the collection expression and the predicate
                    # expression.
                    assert isinstance(call_expr.f_name, L.DotExpr)
                    coll_expr = lower(call_expr.f_name.f_prefix)
                    inner_expr = lower(lambda_expr.f_body)

                    return E.Find.create_expanded(
                        coll_expr, inner_expr, elt_var, index_var=None,
                    )

                elif same_node(name_decl, self.map_method):
                    # Build variable for iteration variables from the lambda
                    # expression arguments.
                    assert len(call_expr.f_args) == 1
                    lambda_expr = call_expr.f_args[0].f_value
                    assert isinstance(lambda_expr, L.LambdaExpr)
                    lambda_args = lambda_expr.f_params

                    # We expect either one argument (for the collection
                    # element) or two arguments (the collection element and the
                    # iteration index).
                    assert len(lambda_args) in (1, 2)
                    element_var = var_for_lambda_arg(lambda_args[0], 'item')
                    index_var = (
                        var_for_lambda_arg(lambda_args[1], 'index', T.Int)
                        if len(lambda_args) == 2
                        else None
                    )

                    # Finally lower the expressions
                    assert isinstance(call_expr.f_name, L.DotExpr)
                    coll_expr = lower(call_expr.f_name.f_prefix)
                    inner_expr = lower(lambda_expr.f_body)
                    result = E.Map.create_expanded(coll_expr, inner_expr,
                                                   element_var, index_var)
                    return result

                elif same_node(name_decl, self.to_symbol_method):
                    # Lower the prefix (the string to convert)
                    prefix = lower(expr.f_name)

                    # Defensive programming: make sure we have no argument to
                    # lower.
                    args, kwargs = lower_args()
                    assert not args and not kwargs

                    return prefix.to_string  # type: ignore

                elif same_node(name_decl, self.unique_method):
                    # Lower the prefix (the array to copy)
                    prefix = lower(expr.f_name)

                    # Defensive programming: make sure we have no argument to
                    # lower.
                    args, kwargs = lower_args()
                    assert not args and not kwargs

                    # ".unique" works with our auto_attr magic: not worth type
                    # checking until we get rid of the syntax magic.
                    return prefix.unique  # type: ignore

                else:
                    # Otherwise, this call must be a method invocation. Note
                    # that not all methods map to actual field access in the
                    # generated code. For instance, calls to the String.join
                    # built-in method are turned into Join instances, so the
                    # "callee" variable below is not necessarily a FieldAccess
                    # instance.
                    callee = lower(expr.f_name)
                    args, kwargs = lower_args()
                    return callee(*args, **kwargs)

            elif isinstance(expr, L.CastExpr):
                subexpr = lower(expr.f_expr)
                excludes_null = expr.f_excludes_null.p_as_bool
                dest_type = self.resolve_type_decl(
                    expr.f_dest_type.p_designated_type
                )
                return Cast(subexpr, dest_type, do_raise=excludes_null)

            elif isinstance(expr, L.CharLit):
                return E.CharacterLiteral(denoted_char_lit(expr))

            elif isinstance(expr, L.DotExpr):
                # Dotted expressions can designate an enum value or a member
                # access. Resolving the suffix determines how to process this.
                suffix_decl = check_referenced_decl(expr.f_suffix)

                if isinstance(suffix_decl, L.EnumLitDecl):
                    # The suffix refers to the declaration of en enum
                    # value: the prefix must designate the corresponding enum
                    # type.
                    enum_type_node = check_referenced_decl(expr.f_prefix)
                    assert isinstance(enum_type_node, L.EnumTypeDecl)
                    enum_type = self.lower_type_decl(enum_type_node)
                    assert isinstance(enum_type, EnumType)

                    name = names.Name.check_from_lower(expr.f_suffix.text)
                    return enum_type.values_dict[name].to_abstract_expr

                else:
                    # Otherwise, the prefix is a regular expression, so this
                    # dotted expression is an access to a member.
                    prefix = lower(expr.f_prefix)
                    assert isinstance(prefix, E.AbstractExpression)
                    return getattr(prefix, expr.f_suffix.text)

            elif isinstance(expr, L.IfExpr):
                # We want to turn the following pattern::
                #
                #   IfExpr(C1, E1, [(C2, E2), (C3, E3), ...], E_last)
                #
                # into the following expression tree::
                #
                #   If(C1, E1,
                #      If(C2, E2,
                #         If(C3, E3,
                #            ... E_Last)))
                #
                # so first translate the "else" expression (E_last), then
                # reverse iterate on the alternatives to wrap this expression
                # with the conditional checks.
                result = lower(expr.f_else_expr)
                conditions = [(alt.f_cond_expr, alt.f_then_expr)
                              for alt in expr.f_alternatives]
                conditions.append((expr.f_cond_expr, expr.f_then_expr))
                for c, e in reversed(conditions):
                    result = E.If(lower(c), lower(e), result)
                return result

            elif isinstance(expr, L.Isa):
                subexpr = lower(expr.f_expr)
                nodes = [
                    self.resolve_type_decl(type_ref.p_designated_type)
                    for type_ref in expr.f_dest_type
                ]
                return E.IsA(subexpr, *nodes)

            elif isinstance(expr, L.CastExpr):
                subexpr = lower(expr.f_expr)
                excludes_null = expr.f_excludes_null.p_as_bool
                dest_type = self.resolve_type_decl(
                    expr.f_dest_type.p_designated_type
                )
                return Cast(subexpr, dest_type, do_raise=excludes_null)

            elif isinstance(expr, L.NotExpr):
                return E.Not(lower(expr.f_expr))

            elif isinstance(expr, L.NullLit):
                result_type = self.resolve_type_decl(expr.p_check_expr_type)
                return E.No(result_type)

            elif isinstance(expr, L.NumLit):
                return E.Literal(int(expr.text))

            elif isinstance(expr, L.ParenExpr):
                return lower(expr.f_expr)

            elif isinstance(expr, L.RaiseExpr):
                # A raise expression can only contain a PropertyError struct
                # constructor.
                cons_expr = expr.f_except_expr
                assert isinstance(cons_expr, L.CallExpr)
                assert (check_referenced_decl(cons_expr.f_name)
                        == self.property_error_type)

                # Get the exception message argument
                args_nodes, kwargs_nodes = extract_call_args(cons_expr)
                msg_expr: Optional[L.Expr] = None
                if args_nodes:
                    msg_expr = args_nodes.pop()
                elif kwargs_nodes:
                    msg_expr = kwargs_nodes.pop("exception_message")
                assert not args_nodes
                assert not kwargs_nodes

                if msg_expr is None:
                    msg = "PropertyError exception"
                else:
                    # TODO (S321-013): handle dynamic error message
                    assert isinstance(msg_expr, L.StringLit)
                    msg = msg_expr.p_denoted_value

                expr_type = self.resolve_type_decl(expr.p_check_expr_type)
                return PropertyError(expr_type, msg)

            elif isinstance(expr, L.RefId):
                decl = check_referenced_decl(expr)
                if isinstance(decl, L.NodeDecl):
                    return E.Self
                elif isinstance(decl, L.SelfDecl):
                    return E.Entity
                elif isinstance(decl, L.EnumLitDecl):
                    # TODO: handle all enum types
                    enum_type_decl = decl.p_get_type()
                    assert self.compiled_types.get(enum_type_decl) == T.Bool
                    assert decl.text in ('true', 'false')
                    return E.Literal(decl.text == 'true')
                else:
                    assert isinstance(decl, L.BaseValDecl), str(decl)
                    return env[decl]

            elif isinstance(expr, L.StringLit):
                return E.SymbolLiteral(denoted_string_lit(expr))

            elif isinstance(expr, L.TryExpr):
                return E.Try(
                    try_expr=lower(expr.f_try_expr),
                    else_expr=(
                        None
                        if expr.f_or_expr is None
                        else lower(expr.f_or_expr)
                    ),
                )

            else:
                assert False, 'Unhandled expression: {}'.format(expr)

        return lower(expr)

    def lower_property_expr(
        self,
        abstract: bool,
        external: bool,
        arg_decl_list: Optional[L.FunArgDeclList],
        body: Optional[L.Expr]
    ) -> Tuple[List[Argument], Optional[AbstractExpression], LocalVars]:
        """
        Common code to lower properties and lazy fields.
        """
        env: LocalsEnv = {}
        local_vars = LocalVars()

        # Lower arguments and register them in the environment
        args: List[Argument] = []
        for a in arg_decl_list or []:
            if a.f_default_val is None:
                default_value = None
            else:
                default_value = self.lower_expr(a.f_default_val, env, None)
                default_value.prepare()

            arg = Argument(
                name=ada_id_for(a.f_syn_name.text),
                type=self.resolve_type_decl(a.f_decl_type.p_designated_type),
                default_value=default_value
            )
            args.append(arg)
            env[a] = arg.var

        # Lower the expression itself
        if abstract or external:
            assert body is None
            expr = None
        else:
            assert body is not None
            expr = self.lower_expr(body, env, local_vars)

        return args, expr, local_vars

    def lower_property(self, full_decl: L.FullDecl) -> PropertyDef:
        """
        Lower the property described in ``decl``.
        """
        decl = full_decl.f_decl
        assert isinstance(decl, L.FunDecl)
        annotations = parse_annotations(self.ctx, FunAnnotations, full_decl)
        return_type = self.resolve_type_decl(
            decl.f_return_type.p_designated_type
        )

        args, expr, local_vars = self.lower_property_expr(
            annotations.abstract,
            annotations.external,
            decl.f_args,
            decl.f_body,
        )

        # If @uses_entity_info and @uses_envs are not present for non-external
        # properties, use None instead of False, for the validation machinery
        # in PropertyDef to work properly (we expect False/true for external
        # properties, and None for non-external ones).
        uses_entity_info: Optional[bool]
        uses_envs: Optional[bool]
        if annotations.external:
            uses_entity_info = annotations.uses_entity_info
            uses_envs = annotations.uses_envs
        else:
            uses_entity_info = annotations.uses_entity_info or None
            uses_envs = annotations.uses_envs or None

        result = PropertyDef(
            expr=expr,
            prefix=AbstractNodeData.PREFIX_PROPERTY,
            doc=self.ctx.lkt_doc(full_decl),

            # When the @export annotation is missing, use "None" to mean
            # "public status unspecified", as the property can still be public
            # thanks to inheritance.
            public=annotations.export or None,

            abstract=annotations.abstract,
            type=return_type,
            abstract_runtime_check=False,
            dynamic_vars=None,
            memoized=annotations.memoized,
            call_memoizable=False,
            memoize_in_populate=False,
            external=annotations.external,
            uses_entity_info=uses_entity_info,
            uses_envs=uses_envs,
            optional_entity_info=False,
            warn_on_unused=True,
            ignore_warn_on_node=None,
            call_non_memoizable_because=None,
            activate_tracing=annotations.trace,
            dump_ir=False,
            local_vars=local_vars,
            final=annotations.final,
        )
        result.arguments.extend(args)

        return result

    def lower_fields(self,
                     decls: L.DeclBlock,
                     allowed_field_types: Tuple[Type[AbstractNodeData], ...]) \
            -> List[Tuple[names.Name, AbstractNodeData]]:
        """
        Lower the fields described in the given DeclBlock node.

        :param decls: Declarations to process.
        :param allowed_field_types: Set of types allowed for the fields to
        load.
        """
        result = []
        for full_decl in decls:
            with self.ctx.lkt_context(full_decl):
                decl = full_decl.f_decl

                # Check field name conformity
                name_text = decl.f_syn_name.text
                check_source_language(
                    not name_text.startswith('_'),
                    'Underscore-prefixed field names are not allowed'
                )
                check_source_language(
                    name_text.lower() == name_text,
                    'Field names must be lower-case'
                )
                name = names.Name.check_from_lower(name_text)

                field: AbstractNodeData

                if isinstance(decl, L.FunDecl):
                    check_source_language(
                        any(issubclass(PropertyDef, cls)
                            for cls in allowed_field_types),
                        'Properties not allowed in this context'
                    )
                    field = self.lower_property(full_decl)
                else:
                    field = self.lower_base_field(
                        full_decl, allowed_field_types
                    )

                field.location = Location.from_lkt_node(decl)
                result.append((name, cast(AbstractNodeData, field)))

        return result

    def create_node(self,
                    decl: L.BasicClassDecl,
                    annotations: BaseNodeAnnotations) -> ASTNodeType:
        """
        Create an ASTNodeType instance.

        :param decl: Corresponding declaration node.
        :param annotations: Annotations for this declaration.
        """
        is_enum_node = isinstance(annotations, EnumNodeAnnotations)
        loc = Location.from_lkt_node(decl)

        # Resolve the base node (if any)
        base_type: Optional[ASTNodeType]

        # Check the set of traits that this node implements
        node_trait_ref: Optional[L.LktNode] = None
        token_node_trait_ref: Optional[L.LktNode] = None
        error_node_trait_ref: Optional[L.LktNode] = None
        for trait_ref in decl.f_traits:
            trait_decl: L.TypeDecl = trait_ref.p_designated_type
            if (
                isinstance(trait_decl, L.InstantiatedGenericType)
                and trait_decl.p_get_inner_type == self.node_trait
            ):
                # If this trait is an instantiation of the Node trait, make
                # sure it is instantiated on the root node itself (i.e.
                # "decl").
                actuals = trait_decl.p_get_actuals
                assert len(actuals) == 1
                with self.ctx.lkt_context(trait_ref):
                    check_source_language(
                        actuals[0] == decl,
                        "The Node generic trait must be instantiated with the"
                        f" root node ({decl.f_syn_name.text})"
                    )
                node_trait_ref = trait_ref

            elif trait_decl == self.token_node_trait:
                token_node_trait_ref = trait_ref

            elif trait_decl == self.error_node_trait:
                error_node_trait_ref = trait_ref

            else:
                with self.ctx.lkt_context(trait_ref):
                    error("Nodes cannot implement this trait")

        def check_trait(trait_ref: Optional[L.LktNode],
                        expected: bool,
                        message: str) -> None:
            """
            If ``expected`` is ``True``, emit an error if ``trait_ref`` is
            ``None``. If ``expected`` is ``False``, emit an error if
            ``trait_ref`` is not ``None``. In both cases, use ``message`` as
            the error message.
            """
            if expected:
                check_source_language(trait_ref is not None, message)
            elif trait_ref is not None:
                with self.ctx.lkt_context(trait_ref):
                    error(message)

        # Root node case
        if decl.p_base_type is None:
            check_trait(
                node_trait_ref,
                True,
                "The root node must implement the Node trait"
            )
            check_trait(
                token_node_trait_ref,
                False,
                "The root node cannot be a token node"
            )
            check_trait(
                error_node_trait_ref,
                False,
                "The root node cannot be an error node"
            )

            if CompiledTypeRepo.root_grammar_class is not None:
                check_source_language(
                    False,
                    'There can be only one root node ({})'.format(
                        CompiledTypeRepo.root_grammar_class.dsl_name
                    )
                )

            base_type = None
            is_token_node = is_error_node = False
        else:
            base_type_decl = decl.p_base_type.p_designated_type
            base_type = cast(ASTNodeType,
                             self.lower_type_decl(base_type_decl))

            check_trait(
                node_trait_ref,
                False,
                "Only the root node can implement the Node trait"
            )

            # This is a token node if either the TokenNode trait is implemented
            # or if the base node is a token node itself. Likewise for
            # ErrorNode.
            is_token_node = token_node_trait_ref is not None
            is_error_node = error_node_trait_ref is not None

            check_source_language(
                base_type is not base_type.is_enum_node,
                'Inheritting from an enum node is forbidden'
            )

        with self.ctx.lkt_context(error_node_trait_ref):
            # Determine whether this node is abstract. Remember that base enum
            # node types are abstract (it is their derivations that are
            # concrete).
            is_abstract = (
                not isinstance(annotations, NodeAnnotations)
                or annotations.abstract
            )
            if is_abstract and is_error_node:
                error("Error nodes cannot be abstract")

            # Determine whether this node is synthetic
            is_synthetic = annotations.synthetic
            if is_synthetic and is_error_node:
                error("Error nodes cannot be synthetic")

            if base_type and base_type.is_list and is_error_node:
                error("Error nodes cannot be lists")

            if is_token_node and is_error_node:
                error("Error nodes cannot be token nodes")

        # Lower fields. Regular nodes can hold all types of fields, but token
        # nodes and enum nodes can hold only user field and properties.
        allowed_field_types = (
            (UserField, PropertyDef)
            if is_token_node or is_enum_node
            else (AbstractNodeData, )
        )
        fields = self.lower_fields(decl.f_decls, allowed_field_types)

        # For qualifier enum nodes, add the synthetic "as_bool" abstract
        # property that each alternative will override.
        is_bool_node = False
        if (
            isinstance(annotations, EnumNodeAnnotations)
            and annotations.qualifier
        ):
            prop = AbstractProperty(
                type=T.Bool, public=True,
                doc='Return whether this node is present'
            )
            prop.location = loc
            fields.append((names.Name('As_Bool'), prop))
            is_bool_node = True

        result = ASTNodeType(
            names.Name.check_from_camel(decl.f_syn_name.text),
            location=loc,
            doc=self.ctx.lkt_doc(decl.parent),
            base=base_type,
            fields=fields,
            is_abstract=is_abstract,
            is_token_node=is_token_node,
            is_error_node=is_error_node,
            is_synthetic=is_synthetic,
            has_abstract_list=annotations.has_abstract_list,
            is_enum_node=is_enum_node,
            is_bool_node=is_bool_node,
        )

        # Create alternatives for enum nodes
        if isinstance(annotations, EnumNodeAnnotations):
            assert isinstance(decl, L.EnumClassDecl)
            self.create_enum_node_alternatives(
                alternatives=sum(
                    (list(b.f_decls) for b in decl.f_branches), []
                ),
                enum_node=result,
                qualifier=annotations.qualifier
            )

        # Reject non-null fields for error nodes. Non-null fields can come from
        # this node's own declaration, or they can come from inheritance.
        if is_error_node:
            error_msg = "Error nodes can only have null fields"
            for f in result.get_parse_fields(include_inherited=True):
                if not (f.null or f.abstract):
                    if f.struct != result:
                        error(f"{error_msg}: {f.qualname} is not null")
                    else:
                        with f.diagnostic_context:
                            error(error_msg)

        return result

    def create_enum_node_alternatives(
        self,
        alternatives: List[L.EnumClassAltDecl],
        enum_node: ASTNodeType,
        qualifier: bool
    ) -> None:
        """
        Create ASTNodeType instances for the alternatives of an enum node.

        :param alternatives: Declarations for the alternatives to lower.
        :param enum_node: Enum node that owns these alternatives.
        :param qualifier: Whether this enum node has the "@qualifier"
            annotation.
        """
        # RA22-015: initialize this to True for enum nodes directly in
        # ASTNodeType's constructor.
        enum_node.is_type_resolved = True

        enum_node._alternatives = []
        enum_node._alternatives_map = {}

        # All enum classes must have at least one alternative, except those
        # with the "@qualifier" annotation, which implies automatic
        # alternatives.
        if qualifier:
            check_source_language(
                not len(alternatives),
                'Enum nodes with @qualifier cannot have explicit alternatives'
            )
            alt_descriptions = [
                EnumNodeAlternative(names.Name(alt_name),
                                    enum_node,
                                    None,
                                    enum_node.location)
                for alt_name in ('Present', 'Absent')
            ]
        else:
            check_source_language(
                len(alternatives) > 0,
                'Missing alternatives for this enum node'
            )
            alt_descriptions = [
                EnumNodeAlternative(
                    names.Name.check_from_camel(alt.f_syn_name.text),
                    enum_node,
                    None,
                    Location.from_lkt_node(alt)
                )
                for alt in alternatives
            ]

        # Now create the ASTNodeType instances themselves
        alt_nodes: List[ASTNodeType] = []
        for i, alt in enumerate(alt_descriptions):
            # Override the abstract "as_bool" property that all qualifier enum
            # nodes define.
            fields: List[Tuple[str, AbstractNodeData]] = []
            if qualifier:
                is_present = i == 0
                prop = Property(is_present)
                prop.location = enum_node.location
                fields.append(('as_bool', prop))

            alt.alt_node = ASTNodeType(
                name=alt.full_name, location=enum_node.location, doc='',
                base=enum_node,
                fields=fields,
                dsl_name='{}.{}'.format(enum_node.dsl_name,
                                        alt.base_name.camel)
            )
            alt_nodes.append(alt.alt_node)

        # Finally create enum node-local indexes to easily fetch the
        # ASTNodeType instances later on.
        enum_node._alternatives = alt_nodes
        enum_node._alternatives_map = {
            alt.base_name.camel: alt_node
            for alt, alt_node in zip(alt_descriptions, alt_nodes)
        }

    def create_enum(self,
                    decl: L.EnumTypeDecl,
                    annotations: EnumAnnotations) -> EnumType:
        """
        Create an EnumType instance.

        :param decl: Corresponding declaration node.
        :param annotations: Annotations for this declaration.
        """
        # Decode the list of enum literals and validate them
        value_names = []
        for lit in decl.f_literals:
            name = lit.f_syn_name.text
            check_source_language(
                name not in value_names,
                'The "{}" literal is present twice'
            )
            value_names.append(name)

        return EnumType(
            name=names.Name.check_from_camel(decl.f_syn_name.text),
            location=Location.from_lkt_node(decl),
            doc=self.ctx.lkt_doc(decl.parent),
            value_names=[names.Name.check_from_lower(n) for n in value_names],
        )

    def create_struct(self,
                      decl: L.StructDecl,
                      annotations: StructAnnotations) -> StructType:
        """
        Create a StructType instance.

        :param decl: Corresponding declaration node.
        :param annotations: Annotations for this declaration.
        """
        return StructType(
            name=names.Name.check_from_camel(decl.f_syn_name.text),
            location=Location.from_lkt_node(decl),
            doc=self.ctx.lkt_doc(decl.parent),
            fields=self.lower_fields(decl.f_decls, (UserField, )),
        )


def create_types(ctx: CompileCtx, lkt_units: List[L.AnalysisUnit]) -> None:
    """
    Create types from Lktlang units.

    :param ctx: Context in which to create these types.
    :param lkt_units: Non-empty list of analysis units where to look for type
        declarations.
    """
    LktTypesLoader(ctx, lkt_units)
