"""The `parser` file mainly contains the `walker` which is used to
traversal the lark tree generated by lark-parser and generate the kcl
ast.
The KCL parser is mainly based on `ast.TreeWalker` to implement
traversal of all lark tree nodes, generate corresponding ast nodes.

The `walk` method will automatically call the walk_{node name} method
corresponding lark tree nodes.

The work flow of walk_{node name} method looks like:

`lark tree node -> walk -> walk_{node name} -> ast tree node`

The `generic_walk` method is the default implementation of `walk` method
If one lark tree node does not need the walk_{node name} method.

The work flow of `generic_walk` method looks like:

`lark tree node -> walk -> # no walk_{node name} # -> generic_walk -> ast tree node`

There are also some util methods to simplify each walk_{node name}
method in class `ASTBuilderWalker`.

:note: Some util methods are for special scenarios. When using these methods,
please carefully read the comments of the method
:copyright: Copyright 2020 The KCL Authors. All rights reserved.
"""
import typing
import os
import re
import glob

from ast import literal_eval
from pathlib import Path
from enum import IntEnum

import lark

import kclvm.kcl.error as kcl_error
import kclvm.config
import kclvm.api.object.internal as internal
import kclvm.compiler.vfs as vfs
import kclvm.compiler.parser.lark_pb2 as lark_pb
import kclvm.compiler.parser.lark_parser as lark_parser
import kclvm.compiler.parser.lark_tree as lark_tree
import kclvm.compiler.extension.builtin.builtin as builtin
import kclvm.compiler.astutil.fix as fix
import kclvm.compiler.build.preprocess as preprocess
import kclvm.kcl.ast as ast
import kclvm.internal.util.check_utils as check_utils

from kclvm.internal.util.check_utils import PreCheck, PostCheck, CheckRules
from kclvm.kcl.ast.precedence import OpPrecedence, OP_PREC_MAP

_INVALID_NEWLINE_STRING_MSG = "invalid NEWLINE token string value {}"

ENDLINE_TOKEN = "\n"
COMMENT_START_TOKEN = "#"
STRING_INTERPOLATION_REGEX = re.compile(r"\$\{(.*?(:.*?)?)\}")
INLINE_COMMENT_WITH_MULTILINE_REGEX = re.compile("#[^\n]*[\n\t ]*")
KCL_MOD_PATH_ENV = "${KCL_MOD}"
DEFAULT_CACHE_OPTION = vfs.CacheOption()
AST_WITH_PARENT_CACHE_OPTION = vfs.CacheOption(cache_dir=vfs.FST_CACHE_DIR)


class ParseMode(IntEnum):
    Null = 0
    ParseComments = 1


class StrWithLocation(str):
    def __init__(self) -> None:
        super().__init__()
        self.line: int = 0
        self.column: int = 0
        self.end_line: int = 0
        self.end_column: int = 0

    def copy_line_column_from(self, node):
        self.line = node.line
        self.column = node.column
        self.end_line = node.end_line
        self.end_column = node.end_column

        return self


class Token(StrWithLocation):
    def __init__(self, value: typing.Optional[str] = None) -> None:
        super().__init__()
        self.value = value


class Mixins(ast.AST):
    def __init__(self) -> None:
        super().__init__()
        self.values: typing.List[ast.Identifier] = []

    @PreCheck((lambda v: CheckRules.check_type_not_none(v, ast.Identifier)), "value")
    def append(self, value: ast.Identifier):
        self.values.append(value)


def report_complier_err(
    arg=None,
    err_type=kcl_error.ErrType.CompileError_TYPE,
    filename=None,
    line_no=None,
    col_no=None,
    end_col_no=None,
):
    kcl_error.report_exception(
        err_type=err_type,
        file_msgs=[
            kcl_error.ErrFileMsg(
                filename=filename, line_no=line_no, col_no=col_no, end_col_no=end_col_no
            )
        ],
        arg_msg=arg,
    )


def report_syntax_err(
    arg=None,
    err_type=kcl_error.ErrType.IllegalArgumentError_Syntax_TYPE,
    filename=None,
    line_no=None,
    col_no=None,
    end_col_no=None,
    source=None,
):
    kcl_error.report_exception(
        err_type=err_type,
        file_msgs=[
            kcl_error.ErrFileMsg(
                filename=filename,
                line_no=line_no,
                col_no=col_no,
                end_col_no=end_col_no,
                src_code=source,
            )
        ],
        arg_msg=arg,
    )


def get_ast_cache_option(set_ast_parent: bool = False) -> vfs.CacheOption:
    """Get AST cache option with the `set_ast_parent` parameter"""
    return AST_WITH_PARENT_CACHE_OPTION if set_ast_parent else DEFAULT_CACHE_OPTION


class ASTBuilderWalker(ast.TreeWalker):
    def __init__(self):
        self.tokens_with_pos = [
            ast.LarkToken.L_COLON,
            ast.LarkToken.L_RIGHT_BRACKETS,
            ast.LarkToken.L_LEFT_BRACKETS,
        ]

    def walk_non_token_children(
        self, node: lark_pb.Tree
    ) -> typing.List[typing.Union[None, ast.AST, list, str, tuple]]:
        """
        Traverse all non-terminal child nodes of the current lark tree node,
        and call the corresponding walk method.
        Return the list of the walk method returns.

        Args:
            node: The lark tree node

        Returns: the list of AST node generated from child nodes of the current lark tree node

        """
        return [
            self.walk(n) for n in node.children if not lark_parser.IsTokenType(n.type)
        ]

    def walk_filtered_elements_ordered_by_types(
        self,
        node: lark_pb.Tree,
        filter_types: typing.List[str],
        with_token: bool = False,
    ) -> typing.List[typing.Union[None, ast.AST, list, str, tuple]]:
        """
        Traverse child nodes, not be selected by node types, of the current lark tree node,
        and call the corresponding walk method.
        Return the list of the walk method returns.

        Args:
            node: The lark tree node
            filter_types: The list of nodes that do not need to be traversed
            with_token: A bool for traverses the terminal node or not

        Returns: the list of AST node generated from child nodes selected of the current lark tree node

        """
        if with_token:
            return [self.walk(n) for n in node.children if n.type not in filter_types]
        else:
            return [
                self.walk(n)
                for n in node.children
                if n.type not in filter_types and not lark_parser.IsTokenType(n.type)
            ]

    def walk_elements_ordered_by_types(
        self, node: lark_pb.Tree, node_types: typing.List[str]
    ) -> typing.Tuple:
        """
        Traverse child nodes, selected by node types, of the current lark tree node,
        and call the corresponding walk method.
        Return the tuple of the walk method returns.

        Args:
            node: The lark tree node
            node_types: The list of nodes that need to be traversed

        Returns: the tuple of the walk method returns

        Notes: The order of the elements in node_types must be the same as the order of the lark tree node children.
        The order of the elements in return tuple will be the same as the order of the node_types.

        """
        p: typing.List[typing.Union[None, ast.AST, list, str, tuple]] = []
        for n in node.children:
            for i, nt in enumerate(node_types):
                if n.type == nt:
                    if i < len(p):
                        continue
                    p.extend([None] * (i - len(p)))
                    p.append(self.walk(n))
                    break

        p.extend([None] * (len(node_types) - len(p)))

        assert len(p) == len(node_types)
        return tuple(p)

    def walk_all_by_type(
        self, node: lark_pb.Tree, node_type: str
    ) -> typing.List[typing.Union[None, ast.AST, list, str, tuple]]:
        """
        Find all children nodes, selected by node type, of the current lark tree node,
        and call the corresponding walk methods.
        Return the list of the walk method returns.

        Args:
            node: the lark tree node
            node_type: type of selected node

        Returns: the list of the walk method returns.

        """
        return [self.walk(n) for n in node.children if n.type == node_type]

    def walk_one_by_type(
        self, node: lark_pb.Tree, node_type: str
    ) -> typing.Union[None, ast.AST, list, str, tuple]:
        """
        Find the first child node, selected by node type, of the current lark tree node,
        and call the corresponding walk method.
        Return the walk method return.

        Args:
            node: the lark tree node
            node_type: type of selected node

        Returns: the walk method return.

        """
        for n in node.children:
            if n.type == node_type:
                return self.walk(n)
        return None

    @PreCheck((lambda v: CheckRules.check_type_not_none(v, lark_pb.Tree)), "node")
    @PostCheck(lambda v: CheckRules.check_type_allow_none(v, Token))
    def get_token_with_pos(self, node: lark_pb.Tree) -> typing.Optional[Token]:
        return (
            Token(node.token_value).copy_line_column_from(node)
            if node.type in self.tokens_with_pos
            else None
        )

    def walk(self, node: lark_pb.Tree) -> typing.Union[None, ast.AST, list, str, tuple]:
        """
        The walk method will automatically call the walk_{node name} method
        corresponding lark tree nodes.
        Return the ast node, the list/tuple of ast nodes and the str of token value generated by lark tree nodes

        Args:
            node: the lark tree node

        Returns: the ast node, the list/tuple of ast nodes and the str of token value generated by lark tree nodes

        """
        assert node
        method = self._WALK_FUNCTION_PREFIX + self.get_node_name(node)
        walker = getattr(self, method, self.generic_walk)
        return walker(node)

    def generic_walk(
        self, node: lark_pb.Tree
    ) -> typing.Union[None, ast.AST, list, str, tuple]:
        """
        The generic_walk method is the default implementation of `walk` method
        If one lark tree node does not need the walk_{node name} method.
        Up to now, only the node with one child, the node with one non-terminal
        child and one "NEWLINE" child, the node with one terminal node is allowed

        Args:
            node: the lark tree node in Allowed Cases, Allowed Cases : A -> B, A->B NEWLINE, A -> TOKEN

        Returns: For each case in allowed cases, return walk(B), walk(B), TOKEN.token_value

        """
        assert node
        assert len(node.children) <= 2
        if len(node.children) == 1:
            return self.walk(node.children[0])
        elif (
            len(node.children) == 2 and node.children[1].type == ast.LarkToken.L_NEWLINE
        ):
            return self.walk(node.children[0])
        elif len(node.children) == 0 and lark_parser.IsTokenType(node.type):
            return (
                node.token_value
                if node.type not in self.tokens_with_pos
                else self.get_token_with_pos(node)
            )

        if not node.type:
            return None

        raise AttributeError(f"Some 'walk_{node.type}()' methods are missing")

    def get_node_name(self, node: lark_pb.Tree) -> str:
        return node.type


class ASTBuilder(ASTBuilderWalker):
    def __init__(
        self,
        root: lark_pb.Tree,
        *,
        pkg: str = "",
        name: str = "",
        filename: str = "",
        mode: ParseMode = ParseMode.Null,
    ):
        super().__init__()
        self.root: lark_pb.Tree = root
        self.pkg: str = pkg
        self.filename: str = filename
        self.name = name
        self.mode = mode
        self.ast_prece_table: dict = {}

    def BuildAST(self) -> ast.Module:
        return check_utils.check_allow_none(self.walk(self.root), ast.Module)

    def register_precedence(
        self,
        op: typing.Union[ast.BinOp, ast.AugOp, ast.UnaryOp, ast.CmpOp],
        node: typing.Union[ast.BinOp, ast.AugOp, ast.UnaryOp, ast.CmpOp, ast.AST],
    ):
        if not op or not node:
            raise ValueError("A operation symbol is needed for precedence registration")
        if not isinstance(op, (ast.BinOp, ast.AugOp, ast.UnaryOp, ast.CmpOp)):
            raise ValueError(
                "{} can not be used to register precedence".format(type(op).__name__)
            )
        if not isinstance(
            node, (ast.BinOp, ast.AugOp, ast.UnaryOp, ast.CmpOp, ast.AST)
        ):
            raise ValueError("{} do not need precedence".format(type(node).__name__))

        prece = OP_PREC_MAP[op]
        self.ast_prece_table[hash(node)] = prece

    def get_precedence(
        self, node: typing.Union[ast.BinOp, ast.AugOp, ast.UnaryOp, ast.CmpOp, ast.AST]
    ) -> int:
        return self.ast_prece_table.get(hash(node), OpPrecedence.HIGHEST)

    @property
    def is_parse_comments(self) -> bool:
        return bool(self.mode & ParseMode.ParseComments)

    def split_newline_value(self, value: str) -> typing.List[str]:
        """Split a NEWLINE token value into newline parts and inline comment parts
        Input: "\n \n # comment \n # comment \n\n # comment \n"
        Output: ["\n \n ", "# comment ", "\n ", "# comment ", "\n\n ", "# comment ", "\n"]
        """
        if not value:
            return []
        value = value.replace("\r", ENDLINE_TOKEN).replace("\t", "    ")
        parts: typing.List[str] = []
        # Mark containing COMMENT token
        index = value.find(COMMENT_START_TOKEN)
        if index == -1:
            return [value]  # Single NEWLINE without COMMENT
        elif index > 0:
            parts.append(value[:index])  # Add first NEWLINE token
        inline_comments = INLINE_COMMENT_WITH_MULTILINE_REGEX.findall(value)
        for comment in inline_comments:
            index = comment.find(ENDLINE_TOKEN)
            if index == -1:
                report_complier_err(
                    _INVALID_NEWLINE_STRING_MSG.format(comment), filename=self.filename
                )
            parts.append(comment[:index])  # Add COMMENT token
            parts.append(comment[index:])  # Add NEWLINE token
        if len(parts) > 1 and ENDLINE_TOKEN not in parts[-1]:
            # Last part is not NEWLINE, raise an error
            report_complier_err(
                _INVALID_NEWLINE_STRING_MSG.format(value), filename=self.filename
            )
        return parts

    def lark_name_to_ast_name(self, lark_name: lark_pb.Tree) -> ast.Name:
        assert lark_name.type == lark_tree.Token.NAME
        return ast.Name(value=lark_name.token_value).set_ast_position(
            lark_name, filename=self.filename
        )

    def walk_NEWLINE(self, node: lark_pb.Tree) -> typing.List[ast.Comment]:
        """Syntax
        COMMENT: /#[^\n]*/
        NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+
        """
        if not node or node.type != ast.LarkToken.L_NEWLINE:
            return []
        if not self.is_parse_comments:
            return []
        parts = self.split_newline_value(node.token_value)
        newline_count = 0
        blank_count = 0
        comments: typing.List[ast.Comment] = []
        col_no = node.column
        for part in parts:
            if part and part.startswith(COMMENT_START_TOKEN):
                comments.append(
                    ast.Comment(
                        filename=self.filename,
                        line=node.line + newline_count,
                        end_line=node.line + newline_count,
                        column=col_no + blank_count,
                        end_column=col_no + blank_count + len(part),
                        text=part,
                    )
                )
            else:
                newline_count += part.count("\n")
                blank_count += part[part.rfind("\n") :].count(" ")
                col_no = 1 if newline_count > 0 else node.column
        return comments

    def walk_start(self, node: lark_pb.Tree) -> ast.Module:
        """Syntax
        start: (NEWLINE | statement)*
        statement: simple_stmt | compound_stmt
        simple_stmt: (assign_stmt | expr_stmt | assert_stmt | import_stmt | type_alias_stmt) NEWLINE
        compound_stmt: if_stmt | schema_stmt
        """

        def __build_doc() -> str:
            if not p.doc and len(p.body) >= 1:
                for i, stmt in enumerate(p.body):
                    p.body[i].filename = p.filename
                    if isinstance(stmt, ast.ExprStmt):
                        expr_stmt = typing.cast(ast.ExprStmt, stmt)
                        if len(expr_stmt.exprs) == 1 and isinstance(
                            expr_stmt.exprs[0], ast.StringLit
                        ):
                            str_lit = check_utils.check_not_none(
                                expr_stmt.exprs[0], ast.StringLit
                            )
                            if str_lit.is_long_string:
                                return self.simplify_StringLitValue(str_lit)
                    break
            return ""

        def __build_comments():
            if not self.is_parse_comments:
                return []
            comments: typing.List[ast.Comment] = []
            newline_token_list = lark_parser.GetNodeList(node, ast.LarkToken.L_NEWLINE)
            for newline in newline_token_list:
                comments.extend(self.walk(newline))
            return comments

        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast_module(node, self.pkg, self.filename, self.name),
            ast.Module,
        )

        p.body = check_utils.check_all_allow_none(
            list, self.walk_non_token_children(node), ast.Stmt
        )
        p.doc = check_utils.check_allow_none(__build_doc(), str)
        p.comments = check_utils.check_all_allow_none(
            list, __build_comments(), ast.Comment
        )

        if not self.is_parse_comments:
            if p.body:
                p.set_end_line_column(p.body[-1])
            else:
                p.offset_end_line(-1)
        else:
            if p.body and p.comments:
                if p.body[-1].end_line > p.comments[-1].end_line:
                    p.set_end_line_column(p.body[-1])
                else:
                    p.set_end_line_column(p.comments[-1])
            elif p.comments:
                p.set_end_line_column(p.comments[-1])
            elif p.body:
                p.set_end_line_column(p.body[-1])
            else:
                p.offset_end_line(-1)

        return p

    def walk_unification_stmt(self, node: lark_pb.Tree) -> ast.UnificationStmt:
        """Syntax
        unification_stmt: NAME COLON schema_expr
        """
        stmt = ast.ASTFactory.get_ast(ast.UnificationStmt, node, self.filename)
        stmt.target, stmt.value = self.walk(node.children[0]), self.walk(
            node.children[2]
        )
        stmt.target.ctx = ast.ExprContext.STORE
        return stmt

    # -----------------------------------------------------
    # type_alias_stmt
    # -----------------------------------------------------

    def walk_type_alias_stmt(self, node: lark_pb.Tree) -> ast.TypeAliasStmt:
        """Syntax
        type_alias_stmt: "type" NAME ASSIGN type
        """
        stmt = ast.ASTFactory.get_ast(ast.TypeAliasStmt, node, self.filename)
        types = [ast.LarkToken.L_NAME, ast.LarkToken.L_type]
        name, type_node = self.walk_elements_ordered_by_types(node, types)
        stmt.type_value = check_utils.check_allow_none(type_node, ast.Type)
        stmt.type_name = check_utils.check_not_none(name, ast.Identifier)
        return stmt

    # -----------------------------------------------------
    # assign_stmt
    # -----------------------------------------------------

    def walk_assign_stmt(
        self, node: lark_pb.Tree
    ) -> typing.Union[ast.AssignStmt, ast.AugAssignStmt]:
        """Syntax
        assign_stmt: identifier [COLON type] (ASSIGN identifier)* ASSIGN test
           | identifier (COMP_PLUS | COMP_MINUS | COMP_MULTIPLY | COMP_DOUBLE_STAR | COMP_DIVIDE
                       | COMP_DOUBLE_DIVIDE | COMP_MOD | COMP_AND | COMP_OR | COMP_XOR | COMP_SHIFT_LEFT
                       | COMP_SHIFT_RIGHT | L_OR | L_AND)
                       test
        """

        def __build_assign_stmt() -> ast.AssignStmt:
            assign_stmt: ast.AssignStmt = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.AssignStmt, node, self.filename),
                ast.AssignStmt,
            )

            assign_stmt.targets = targets
            assign_stmt.type_annotation = (
                type_annotation_node.plain_type_str if type_annotation_node else None
            )
            assign_stmt.type_annotation_node = type_annotation_node
            for target in assign_stmt.targets:
                target.ctx = ast.ExprContext.STORE
            assign_stmt.value = test
            return assign_stmt

        def __build_aug_assign_stmt() -> ast.AugAssignStmt:
            aug_assign_stmt = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.AugAssignStmt, node, self.filename),
                ast.AugAssignStmt,
            )

            op_node = node.children[1] if len(node.children) == 3 else None
            aug_assign_stmt.op = ast.AugOp(op_node.type)
            aug_assign_stmt.target = targets[0]
            aug_assign_stmt.target.ctx = ast.ExprContext.AUGSTORE
            aug_assign_stmt.value = test
            return aug_assign_stmt

        targets = check_utils.check_all_not_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_identifier),
            ast.Identifier,
        )

        test = (
            check_utils.check_not_none(self.walk(node.children[-1]), ast.Expr)
            if len(node.children) > 0
            else None
        )

        types = [ast.LarkToken.L_type, ast.LarkToken.L_ASSIGN]
        type_annotation_node, has_assign = self.walk_elements_ordered_by_types(
            node, types
        )
        type_annotation_node: ast.Type = check_utils.check_allow_none(
            type_annotation_node, ast.Type
        )
        has_assign = check_utils.check_allow_none(has_assign, str)

        if has_assign:
            return __build_assign_stmt()
        elif len(targets) == 1:
            return __build_aug_assign_stmt()

    def walk_simple_assign_stmt(
        self, node: lark_pb.Tree
    ) -> typing.Union[ast.AssignStmt, ast.AugAssignStmt]:
        return self.walk_assign_stmt(node)

    # -----------------------------------------------------
    # assert_stmt
    # -----------------------------------------------------

    def walk_assert_stmt(self, node: lark_pb.Tree) -> ast.AssertStmt:
        """Syntax
        assert_stmt: ASSERT simple_expr (IF simple_expr)? (COMMA test)?
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.AssertStmt, node, self.filename), ast.AssertStmt
        )

        p.test, p.if_cond, p.msg = self.walk_elements_ordered_by_types(
            node,
            [
                ast.LarkToken.L_simple_expr,
                ast.LarkToken.L_simple_expr,
                ast.LarkToken.L_test,
            ],
        )
        p.test = check_utils.check_not_none(p.test, ast.Expr)
        p.if_cond = check_utils.check_allow_none(p.if_cond, ast.Expr)
        p.msg = check_utils.check_allow_none(p.msg, ast.Expr)
        return p

    # -----------------------------------------------------
    # import_stmt
    # -----------------------------------------------------

    def walk_import_stmt(self, node: lark_pb.Tree) -> ast.ImportStmt:
        """Syntax
        import_stmt: IMPORT dot_name (AS NAME)?
        """
        p: ast.ImportStmt = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ImportStmt, node, self.filename), ast.ImportStmt
        )

        path, ident = self.walk_elements_ordered_by_types(
            node, [ast.LarkToken.L_dot_name, ast.LarkToken.L_NAME]
        )
        p.path = path[0]
        p.path = check_utils.check_not_none(p.path, str)
        ident = check_utils.check_allow_none(ident, ast.Identifier)

        p.name = p.path[p.path.rfind(".") + 1 :]
        p.asname = "".join(ident.names) if ident else None

        p.path_nodes = path[1]
        if ident:
            assert (
                len(ident.name_nodes) == 1
            ), "as name in the import stmt must be a single NAME"
            p.as_name_node = ident.name_nodes[0]

        return p

    def walk_dot_name(self, node: lark_pb.Tree) -> (str, typing.List[ast.Name]):
        """Syntax
        dot_name: (leading_dots identifier) | identifier
        """
        types = [ast.LarkToken.L_leading_dots, ast.LarkToken.L_identifier]
        dots, ident = self.walk_elements_ordered_by_types(node, types)

        dots = check_utils.check_allow_none(dots, str)
        ident: ast.Identifier = check_utils.check_not_none(ident, ast.Identifier)

        dot_name = dots or ""
        dot_name += ".".join([name for name in ident.names])

        return dot_name, ident.name_nodes

    def walk_leading_dots(self, node: lark_pb.Tree) -> str:
        """Syntax
        leading_dots: DOT+
        """
        return "".join([child.token_value for child in node.children])

    # -----------------------------------------------------
    # if_stmt
    # -----------------------------------------------------

    def walk_if_stmt(self, node: lark_pb.Tree) -> ast.IfStmt:
        """Syntax
        if_stmt: IF test COLON execution_block (ELIF test COLON execution_block)* (ELSE COLON execution_block)?
        execution_block: if_simple_stmt | NEWLINE _INDENT schema_init_stmt+ _DEDENT
        schema_init_stmt: if_simple_stmt | if_stmt
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.IfStmt, node, self.filename), ast.IfStmt
        )
        test_list = check_utils.check_all_not_none(
            list, self.walk_all_by_type(node, ast.LarkToken.L_test), ast.Expr
        )
        execution_block_list = check_utils.check_all_not_none(
            list, self.walk_all_by_type(node, ast.LarkToken.L_execution_block), list
        )

        for execution_block in execution_block_list:
            check_utils.check_all_not_none(list, execution_block, ast.Stmt)

        has_else = check_utils.check_allow_none(
            self.walk_one_by_type(node, ast.LarkToken.L_ELSE), str
        )

        p.cond = test_list.pop(0)
        if not p.body:
            p.body = execution_block_list.pop(0)
            p.set_end_line_column(p.body[-1])
        if has_else and p.body:
            p.else_body = execution_block_list.pop(-1)
            p.set_end_line_column(p.else_body[-1])
        if p.body and len(execution_block_list) > 0:
            p.elif_body = execution_block_list
            assert len(p.elif_body[-1]) > 0
            p.set_end_line_column(p.elif_body[-1][-1])
        if len(test_list) > 0:
            p.elif_cond = test_list

        if p.else_body:
            p.set_end_line_column(p.else_body[-1])

        return p

    def walk_execution_block(self, node: lark_pb.Tree) -> typing.List[ast.Stmt]:
        """Syntax
        execution_block: if_simple_stmt | NEWLINE _INDENT schema_init_stmt+ _DEDENT
        schema_init_stmt: if_simple_stmt | if_stmt
        """
        if len(node.children) == 1:
            return [check_utils.check_allow_none(self.walk(node.children[0]), ast.Stmt)]
        else:
            stmt_list = self.walk_all_by_type(node, ast.LarkToken.L_schema_init_stmt)
            return check_utils.check_all_not_none(
                list,
                stmt_list,
                ast.IfStmt,
                ast.ExprStmt,
                ast.AssertStmt,
                ast.AssignStmt,
                ast.AugAssignStmt,
            )

    # -----------------------------------------------------
    # schema_stmt
    # -----------------------------------------------------

    def walk_schema_stmt(self, node: lark_pb.Tree) -> ast.SchemaStmt:
        """Syntax
        schema_stmt: [decorators] (SCHEMA|MIXIN|PROTOCOL) [RELAXED] NAME
                    [LEFT_BRACKETS [schema_arguments] RIGHT_BRACKETS]
                    [LEFT_PARENTHESES identifier (COMMA identifier)* RIGHT_PARENTHESES]
                    [for_host] COLON NEWLINE [schema_body]
        schema_arguments: schema_argument (COMMA schema_argument)*
        schema_body: _INDENT (string NEWLINE)* [mixin_stmt]
                    (schema_attribute_stmt|schema_init_stmt|schema_index_signature)*
                    [check_block] _DEDENT
        schema_attribute_stmt: attribute_stmt NEWLINE
        attribute_stmt: [decorators] (FINAL)? NAME COLON type [(ASSIGN|COMP_OR) test]
        schema_init_stmt: if_simple_stmt | if_stmt
        """

        def __build_schema_stmt_parent_name() -> typing.Optional[ast.Identifier]:
            """Syntax
            schema_stmt:[LEFT_PARENTHESES identifier (COMMA identifier)* RIGHT_PARENTHESES]
            """
            idents = self.walk_all_by_type(node, ast.LarkToken.L_identifier)
            if len(idents) > 1:
                report_complier_err(
                    arg=kcl_error.MULTI_INHERIT_MSG.format(f"{p.name}"),
                    err_type=kcl_error.ErrType.MultiInheritError_TYPE,
                    filename=self.filename,
                    line_no=idents[0].line,
                    col_no=idents[0].column,
                    end_col_no=idents[-1].end_column,
                )
            return (
                idents[0] if idents and isinstance(idents[0], ast.Identifier) else None
            )

        def __build_schema_stmt_doc(sch_body: lark_pb.Tree) -> str:
            """Syntax
            schema_body: (string NEWLINE)*
            ?string: STRING | LONG_STRING
            """
            for child in sch_body.children:
                if child.type == ast.LarkToken.L_string:
                    if (
                        len(child.children) > 0
                        and child.children[0].type == ast.LarkToken.L_LONG_STRING
                    ):
                        return literal_eval(child.children[0].token_value)
                    return ""

            return ""

        def __build_schema_stmt_index_signature(
            sch_body: lark_pb.Tree,
        ) -> typing.Optional[ast.SchemaIndexSignature]:
            """Syntax
            schema_body: (schema_index_signature)*
            schema_index_signature: LEFT_BRACKETS [NAME COLON] [ELLIPSIS] basic_type RIGHT_BRACKETS
                                    COLON type [ASSIGN test] NEWLINE
            """
            index_sig = check_utils.check_all_allow_none(
                list,
                self.walk_all_by_type(sch_body, ast.LarkToken.L_schema_index_signature),
                ast.SchemaIndexSignature,
            )
            if len(index_sig) > 1:
                report_complier_err(
                    err_type=kcl_error.ErrType.IndexSignatureError_TYPE,
                    filename=self.filename,
                    line_no=index_sig[1].line,
                    col_no=index_sig[1].column,
                    end_col_no=index_sig[1].end_column,
                    arg="only one index signature is allowed in the schema",
                )
            return index_sig[0] if index_sig else None

        p: ast.SchemaStmt = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.SchemaStmt, node, self.filename), ast.SchemaStmt
        )

        types = [
            ast.LarkToken.L_decorators,
            ast.LarkToken.L_MIXIN,
            ast.LarkToken.L_PROTOCOL,
            ast.LarkToken.L_NAME,
            ast.LarkToken.L_COLON,
            ast.LarkToken.L_schema_body,
        ]
        (
            decorators,
            is_mixin,
            is_protocol,
            ident,
            colon,
            body,
        ) = self.walk_elements_ordered_by_types(node, types)
        colon = check_utils.check_not_none(colon, Token)
        ident = check_utils.check_not_none(ident, ast.Identifier)
        p.decorators = (
            check_utils.check_all_allow_none(list, decorators, ast.Decorator) or []
        )
        p.body = check_utils.check_all_allow_none(list, body or [], ast.Stmt)
        p.name = "".join(ident.names)
        p.is_mixin = bool(is_mixin) or p.name.endswith("Mixin")
        p.is_protocol = bool(is_protocol) or p.name.endswith("Protocol")
        assert len(ident.name_nodes) == 1, "schema name must be a single NAME"
        p.name_node = ident.name_nodes[0]

        mixins = None
        for n in node.children:
            if n.type == ast.LarkToken.L_schema_body:
                p.doc = check_utils.check_allow_none(__build_schema_stmt_doc(n), str)
                p.index_signature = check_utils.check_allow_none(
                    __build_schema_stmt_index_signature(n), ast.SchemaIndexSignature
                )
                types = [ast.LarkToken.L_mixin_stmt, ast.LarkToken.L_check_block]
                mixins, checks = self.walk_elements_ordered_by_types(n, types)
                p.mixins = (
                    check_utils.check_all_allow_none(
                        list, mixins.values, ast.Identifier
                    )
                    if mixins
                    else []
                )
                p.checks = (
                    check_utils.check_all_allow_none(list, checks, ast.CheckExpr) or []
                )
            elif n.type == ast.LarkToken.L_schema_arguments:
                p.args = check_utils.check_allow_none(self.walk(n), ast.Arguments)
            elif n.type == ast.LarkToken.L_for_host:
                p.for_host_name = self.walk(n)

        p.parent_name = check_utils.check_allow_none(
            __build_schema_stmt_parent_name(), ast.Identifier
        )

        if p.checks:
            p.set_end_line_column(p.checks[-1])
        elif p.body:
            p.set_end_line_column(p.body[-1])
            if (
                p.index_signature
                and p.index_signature.get_end_line() > p.body[-1].get_end_line()
            ):
                p.set_end_line_column(p.index_signature)
        elif p.index_signature:
            p.set_end_line_column(p.index_signature)
        elif p.mixins and mixins and isinstance(mixins, Mixins):
            p.set_end_line_column(mixins)
        else:
            p.set_end_line(colon.end_line)
            p.set_end_column(colon.end_column)

        return p

    def walk_for_host(self, node: lark_pb.Tree) -> typing.Optional[ast.Identifier]:
        """Syntax
        for_host: FOR identifier
        """

        for n in node.children or []:
            if n.type == ast.LarkToken.L_identifier:
                return typing.cast(ast.Identifier, self.walk(n))
        return None

    def walk_schema_arguments(
        self, node: lark_pb.Tree
    ) -> typing.Optional[ast.Arguments]:
        """Syntax
        schema_stmt: [decorators] SCHEMA [RELAXED] NAME [LEFT_BRACKETS [schema_arguments] RIGHT_BRACKETS]
                    [LEFT_PARENTHESES identifier (COMMA identifier)* RIGHT_PARENTHESES] COLON NEWLINE [schema_body]
        schema_arguments: schema_argument (COMMA schema_argument)*
        schema_argument: NAME [COLON type] [ASSIGN test]
        """

        p: ast.Arguments = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.Arguments, node, self.filename), ast.Arguments
        )

        arguments: typing.List[
            (ast.Identifier, typing.Optional[ast.Type], typing.Optional[ast.Expr])
        ] = self.walk_non_token_children(node)
        assert all(len(argument) == 3 for argument in arguments)

        p.args = [argument[0] for argument in arguments]
        for arg in p.args:
            arg.ctx = ast.ExprContext.STORE
        p.type_annotation_node_list = [argument[1] for argument in arguments]
        p.type_annotation_list = [
            t.plain_type_str if t else None for t in p.type_annotation_node_list
        ]
        p.defaults = [argument[2] for argument in arguments]

        return p

    def walk_schema_argument(
        self, node: lark_pb.Tree
    ) -> (ast.Identifier, typing.Optional[ast.Type], typing.Optional[ast.Expr]):
        """Syntax
        schema_argument: NAME [COLON type] [ASSIGN test]
        """
        types = [ast.LarkToken.L_NAME, ast.LarkToken.L_type, ast.LarkToken.L_test]
        arg_name, arg_type_node, arg_value = self.walk_elements_ordered_by_types(
            node, types
        )
        arg_type_node: ast.Type = check_utils.check_allow_none(arg_type_node, ast.Type)
        arg_name = check_utils.check_not_none(arg_name, ast.Identifier)
        return arg_name, arg_type_node, arg_value

    def walk_schema_body(
        self, node: lark_pb.Tree
    ) -> typing.List[typing.Union[ast.SchemaAttr, ast.Stmt]]:
        """
        schema_body: _INDENT (string NEWLINE)* [mixin_stmt]
                    (schema_attribute_stmt|schema_init_stmt|schema_index_signature)*
                    [check_block] _DEDENT
        schema_init_stmt: if_simple_stmt | if_stmt
        """
        filters = [
            ast.LarkToken.L_string,
            ast.LarkToken.L_mixin_stmt,
            ast.LarkToken.L_schema_index_signature,
            ast.LarkToken.L_check_block,
        ]
        return check_utils.check_all_allow_none(
            list,
            self.walk_filtered_elements_ordered_by_types(node, filters),
            ast.SchemaAttr,
            ast.Stmt,
        )

    def walk_attribute_stmt(self, node: lark_pb.Tree) -> ast.SchemaAttr():
        """Syntax
        schema_attribute_stmt: attribute_stmt NEWLINE
        attribute_stmt: [decorators] (FINAL)? NAME [QUESTION] COLON type [(ASSIGN|COMP_OR) test]
        """
        p: ast.SchemaAttr = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.SchemaAttr, node, self.filename), ast.SchemaAttr
        )

        types = [
            ast.LarkToken.L_decorators,
            ast.LarkToken.L_identifier,
            ast.LarkToken.L_QUESTION,
            ast.LarkToken.L_type,
            ast.LarkToken.L_test,
        ]
        (
            decorators,
            name,
            is_optional,
            type_annotation,
            p.value,
        ) = self.walk_elements_ordered_by_types(node, types)
        name: ast.Identifier = check_utils.check_not_none(name, ast.Identifier)
        type_annotation = check_utils.check_not_none(type_annotation, ast.Type)

        p.name = ".".join(name.names)
        p.type_str = type_annotation.plain_type_str
        p.decorators = decorators or []
        p.is_optional = bool(is_optional)
        p.name_node = name.name_nodes[0]
        p.type_node = type_annotation

        for n in node.children:
            if n.type in [ast.LarkToken.L_ASSIGN, ast.LarkToken.L_COMP_OR]:
                p.op = ast.AugOp(n.type)
                continue

        return p

    def walk_schema_index_signature(
        self, node: lark_pb.Tree
    ) -> ast.SchemaIndexSignature:
        """Syntax
        schema_index_signature: LEFT_BRACKETS [NAME COLON] [ELLIPSIS] basic_type RIGHT_BRACKETS
                                COLON type [ASSIGN test] NEWLINE
        """
        p: ast.SchemaIndexSignature = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.SchemaIndexSignature, node, self.filename),
            ast.SchemaIndexSignature,
        )

        types = [
            ast.LarkToken.L_NAME,
            ast.LarkToken.L_ELLIPSIS,
            ast.LarkToken.L_basic_type,
            ast.LarkToken.L_type,
            ast.LarkToken.L_test,
        ]
        (
            ident,
            any_other,
            key_type_node,
            p.value_type_node,
            p.value,
        ) = self.walk_elements_ordered_by_types(node, types)

        p.key_type = check_utils.check_not_none(key_type_node.type_name, str)
        p.value_type = check_utils.check_not_none(p.value_type_node.plain_type_str, str)
        p.value_type_node = check_utils.check_not_none(p.value_type_node, ast.Type)
        ident = check_utils.check_allow_none(ident, ast.Identifier)

        p.any_other = bool(any_other)
        p.key_name = "".join(ident.names) if ident else None
        if ident:
            assert (
                len(ident.name_nodes) == 1
            ), "name of the schema index signature must be a single NAME"
            p.name_node = ident.name_nodes[0]

        p.set_end_line(p.value_type_node.end_line)
        p.set_end_column(p.value_type_node.end_column)

        if p.value:
            p.set_end_line_column(p.value)

        return p

    # -----------------------------------------------------
    # decorators
    # -----------------------------------------------------

    def walk_decorators(self, node: lark_pb.Tree) -> typing.List[ast.Decorator]:
        """Syntax
        decorators: (AT decorator_expr NEWLINE)+
        decorator_expr: identifier [call_suffix]
        """
        return check_utils.check_all_not_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_decorator_expr),
            ast.Decorator,
        )

    def walk_decorator_expr(self, node: lark_pb.Tree) -> ast.Decorator:
        """Syntax
        decorator_expr: identifier [call_suffix]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.Decorator, node, self.filename), ast.Decorator
        )

        types = [ast.LarkToken.L_identifier, ast.LarkToken.L_call_suffix]
        p.name, p.args = self.walk_elements_ordered_by_types(node, types)
        p.name = check_utils.check_not_none(p.name, ast.Identifier)
        p.args = check_utils.check_allow_none(p.args, ast.CallExpr)
        return p

    # -----------------------------------------------------
    # rule_stmt
    # -----------------------------------------------------

    def walk_rule_stmt(self, node: lark_pb.Tree) -> ast.RuleStmt:
        """Syntax
        rule_stmt: [decorators] RULE NAME [LEFT_BRACKETS [schema_arguments] RIGHT_BRACKETS] [LEFT_PARENTHESES identifier (COMMA identifier)* RIGHT_PARENTHESES] [for_host] COLON NEWLINE [rule_body]
        rule_body: _INDENT (string NEWLINE)* check_expr+ _DEDENT
        """

        def __build_rule_stmt_parent_names() -> typing.List[ast.Identifier]:
            """Syntax
            rule_stmt: [LEFT_PARENTHESES identifier (COMMA identifier)* RIGHT_PARENTHESES]
            """
            idents = self.walk_all_by_type(node, ast.LarkToken.L_identifier)
            return idents or []

        def __build_rule_stmt_doc(rule_body: lark_pb.Tree) -> str:
            """Syntax
            rule_body: (string NEWLINE)*
            ?string: STRING | LONG_STRING
            """
            for child in rule_body.children:
                if child.type == ast.LarkToken.L_string:
                    if (
                        len(child.children) > 0
                        and child.children[0].type == ast.LarkToken.L_LONG_STRING
                    ):
                        return literal_eval(child.children[0].token_value)
                    return ""

            return ""

        p: ast.RuleStmt = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.RuleStmt, node, self.filename), ast.RuleStmt
        )

        types = [
            ast.LarkToken.L_decorators,
            ast.LarkToken.L_NAME,
            ast.LarkToken.L_COLON,
        ]
        (
            decorators,
            ident,
            colon,
        ) = self.walk_elements_ordered_by_types(node, types)
        colon = check_utils.check_not_none(colon, Token)
        ident = check_utils.check_not_none(ident, ast.Identifier)
        p.decorators = (
            check_utils.check_all_allow_none(list, decorators, ast.Decorator) or []
        )
        p.name = "".join(ident.names)
        assert len(ident.name_nodes) == 1, "rule name must be a single NAME"
        p.name_node = ident.name_nodes[0]

        checks = None
        for n in node.children:
            if n.type == ast.LarkToken.L_rule_body:
                p.doc = check_utils.check_allow_none(__build_rule_stmt_doc(n), str)
                checks = self.walk_all_by_type(n, ast.LarkToken.L_check_expr)
                p.checks = (
                    check_utils.check_all_allow_none(list, checks, ast.CheckExpr) or []
                )
            elif n.type == ast.LarkToken.L_schema_arguments:
                p.args = check_utils.check_allow_none(self.walk(n), ast.Arguments)
            elif n.type == ast.LarkToken.L_for_host:
                p.for_host_name = self.walk(n)

        p.parent_rules = check_utils.check_allow_none(
            __build_rule_stmt_parent_names(), list
        )

        if p.checks:
            p.set_end_line_column(p.checks[-1])
        else:
            p.set_end_line(colon.end_line)
            p.set_end_column(colon.end_column)

        return p

    # -----------------------------------------------------
    # type
    # -----------------------------------------------------

    def walk_type(self, node: lark_pb.Tree) -> ast.Type:
        """Syntax
        type: type_element (OR type_element)*
        type_element: schema_type | basic_type | compound_type | literal_type
        schema_type: identifier
        basic_type: basic_type: STRING_TYPE | INT_TYPE | FLOAT_TYPE | BOOL_TYPE | ANY_TYPE
        compound_type: list_type | dict_type
        literal_type: string | number | TRUE | FALSE | NONE
        list_type: LEFT_BRACKETS (type)? RIGHT_BRACKETS
        dict_type: LEFT_BRACE (type)? COLON (type)? RIGHT_BRACE
        """
        p: ast.Type = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.Type, node, filename=self.filename), ast.Type
        )
        type_elements = check_utils.check_all_not_none(
            list,
            self.walk_non_token_children(node),
            ast.Identifier,
            ast.BasicType,
            ast.ListType,
            ast.DictType,
            ast.LiteralType,
        )
        p.type_elements = type_elements

        plain_types: typing.List[str] = []
        for type_element in type_elements:
            if isinstance(type_element, ast.Identifier):
                plain_types.append(".".join(type_element.names))
                continue
            if isinstance(type_element, ast.BasicType):
                plain_types.append(type_element.type_name)
                continue
            if isinstance(type_element, (ast.ListType, ast.DictType)):
                plain_types.append(type_element.plain_type_str)
                continue
            if isinstance(type_element, ast.LiteralType):
                plain_types.append(type_element.plain_value)
                continue

            report_complier_err(
                "Invalid type annotation. Valid type annotations: SchemaType, BasicType, ListType, LiteralType",
                filename=self.filename,
                line_no=type_element.line,
                col_no=type_element.column,
                end_col_no=type_element.end_column,
            )
        p.plain_type_str = "|".join(plain_types)
        return p

    def walk_basic_type(self, node: lark_pb.Tree) -> ast.BasicType:
        assert len(node.children) == 1
        p: ast.BasicType = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.BasicType, node, self.filename), ast.BasicType
        )
        p.type_name = node.children[0].token_value
        return p

    def walk_literal_type(self, node: lark_pb.Tree) -> ast.LiteralType:
        assert len(node.children) == 1
        p: ast.LiteralType = ast.LiteralType().set_ast_position(
            node, filename=self.filename
        )
        value_type = node.children[0].type
        if value_type in ["string", "number"]:
            lit = self.walk(node.children[0])
            if isinstance(lit, ast.StringLit):
                p.value_type = lit.type
                p.string_value = lit
                p.plain_value = lit.raw_value
                return p
            if isinstance(lit, ast.NumberLit):
                p.value_type = lit.type
                p.number_value = lit
                p.plain_value = (
                    f"{str(lit.value)}{lit.binary_suffix}"
                    if lit.binary_suffix
                    else str(lit.value)
                )
                return p
            report_complier_err(
                f"Invalid Literal Type {lit}. Valid Literal Types: StringLiteral, NumberLiteral",
                filename=self.filename,
                line_no=node.children[0].line,
                col_no=node.children[0].column,
                end_col_no=node.children[0].end_column,
            )
        else:
            assert lark_parser.IsTokenType(value_type)
            value = node.children[0].token_value
            assert value in ["True", "False", "None"]
            p.plain_value = value
            p.value_type = "bool" if value in ["True", "False"] else "None"
            return p

    def walk_list_type(self, node: lark_pb.Tree) -> ast.ListType:
        """
        list_type: LEFT_BRACKETS (type)? RIGHT_BRACKETS
        """
        p: ast.ListType = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ListType, node, filename=self.filename),
            ast.ListType,
        )
        inner_type: ast.Type = check_utils.check_allow_none(
            self.walk_one_by_type(node, ast.LarkToken.L_type), ast.Type
        )
        p.inner_type = inner_type
        p.plain_type_str = f"[{inner_type.plain_type_str}]" if inner_type else "[]"
        return p

    def walk_dict_type(self, node: lark_pb.Tree) -> ast.DictType:
        """
        dict_type: LEFT_BRACE (type)? COLON (type)? RIGHT_BRACE
        """
        p: ast.DictType = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.DictType, node, filename=self.filename),
            ast.DictType,
        )
        types = [ast.LarkToken.L_type, ast.LarkToken.L_COLON, ast.LarkToken.L_type]
        key, colon, val = self.walk_elements_ordered_by_types(node, types)

        key: ast.Type = check_utils.check_allow_none(key, ast.Type)
        val: ast.Type = check_utils.check_allow_none(val, ast.Type)
        assert colon and colon == ":"

        p.key_type = key
        p.value_type = val

        plain_key = key.plain_type_str if key else ""
        plain_value = val.plain_type_str if val else ""
        p.plain_type_str = "{" + f"{plain_key}:{plain_value}" + "}"
        return p

    # -----------------------------------------------------
    # check_stmt
    # -----------------------------------------------------

    def walk_check_block(self, node: lark_pb.Tree) -> typing.List[ast.CheckExpr]:
        """Syntax
        check_block: CHECK COLON NEWLINE _INDENT check_expr+ _DEDENT
        check_expr: simple_expr [IF simple_expr] [COMMA primary_expr] NEWLINE
        """
        return check_utils.check_all_allow_none(
            list, self.walk_non_token_children(node), ast.CheckExpr
        )

    def walk_check_expr(self, node: lark_pb.Tree) -> ast.CheckExpr:
        """Syntax
        check_expr: simple_expr [IF simple_expr] [COMMA primary_expr] NEWLINE
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.CheckExpr, node, self.filename), ast.CheckExpr
        )

        types = [
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_primary_expr,
        ]
        p.test, p.if_cond, p.msg = self.walk_elements_ordered_by_types(node, types)

        p.msg = check_utils.check_allow_none(p.msg, ast.Expr)
        p.test = check_utils.check_not_none(p.test, ast.Expr)

        if p.msg:
            p.set_end_line_column(p.msg)
        elif p.if_cond:
            p.set_end_line_column(p.if_cond)
        else:
            p.set_end_line_column(p.test)

        return p

    # -----------------------------------------------------
    # mixin_stmt
    # -----------------------------------------------------

    def walk_mixin_stmt(self, node: lark_pb.Tree) -> Mixins:
        """Syntax
        mixin_stmt: MIXIN LEFT_BRACKETS [mixins | multiline_mixins] RIGHT_BRACKETS NEWLINE
        multiline_mixins: NEWLINE _INDENT mixins NEWLINE _DEDENT
        mixins: identifier (COMMA (NEWLINE mixins | identifier))*
        """
        mixins = Mixins()
        left_bracket = check_utils.check_not_none(
            self.walk_one_by_type(node, ast.LarkToken.L_LEFT_BRACKETS), Token
        )
        right_bracket = check_utils.check_not_none(
            self.walk_one_by_type(node, ast.LarkToken.L_RIGHT_BRACKETS), Token
        )

        mixins.set_line(left_bracket.line)
        mixins.set_column(left_bracket.column)
        mixins.set_end_line(right_bracket.end_line)
        mixins.set_end_column(right_bracket.end_column)

        def walk_fn(n: lark_pb.Tree):
            if n.type == ast.LarkToken.L_identifier:
                mixins.append(
                    check_utils.check_allow_none(self.walk(n), ast.Identifier)
                )

        lark_parser.WalkTree(node, walk_fn)
        return mixins

    # -----------------------------------------------------
    # expression_stmt
    # -----------------------------------------------------

    def walk_testlist_expr(self, node: lark_pb.Tree) -> ast.ExprStmt:
        """
        expr_stmt: testlist_expr
        testlist_expr: test (COMMA test)*
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ExprStmt, node, self.filename), ast.ExprStmt
        )
        p.exprs = check_utils.check_all_not_none(
            list, self.walk_non_token_children(node), ast.Expr
        )
        return p

    def walk_if_expr(self, node: lark_pb.Tree) -> ast.IfExpr:
        """
        if_expr: simple_expr IF simple_expr ELSE test
        """
        assert len(node.children) == 5
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.IfExpr, node, self.filename), ast.IfExpr
        )

        types = [
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_test,
        ]
        p.body, p.cond, p.orelse = self.walk_elements_ordered_by_types(node, types)

        p.body = check_utils.check_not_none(p.body, ast.Expr)
        p.cond = check_utils.check_not_none(p.cond, ast.Expr)
        p.orelse = check_utils.check_not_none(p.orelse, ast.Expr)
        return p

    def walk_simple_expr(self, node: lark_pb.Tree) -> ast.Expr:
        """This function is equivalent to a filter, only the child nodes of the
        current node need to adjust the priority, and all expressions passing
        through this node will adjust the priority.
        """
        assert len(node.children) == 1
        simple_expr = check_utils.check_not_none(self.walk(node.children[0]), ast.Expr)
        return self.add_precedence(simple_expr)

    def add_precedence(
        self, expr: ast.Expr
    ) -> typing.Union[ast.UnaryExpr, ast.BinaryExpr, ast.Compare, ast.Expr]:
        if not isinstance(expr, (ast.BinaryExpr, ast.UnaryExpr, ast.Compare)):
            return expr
        p = (
            self.add_compare_precedence(expr)
            if isinstance(expr, ast.Compare)
            else self.add_cal_expr_precedence(expr)
        )
        p = check_utils.check_not_none(p, ast.UnaryExpr, ast.BinaryExpr, ast.Compare)
        return p

    def add_cal_expr_precedence(
        self, expr: typing.Union[ast.BinaryExpr, ast.UnaryExpr]
    ) -> typing.Union[ast.UnaryExpr, ast.BinaryExpr, ast.Compare, ast.Expr]:
        if isinstance(expr, ast.UnaryExpr):
            right_expr = expr.operand
        elif isinstance(expr, ast.BinaryExpr):
            right_expr = expr.right
        else:
            return expr

        start_local = right_expr
        prece_op = self.get_precedence(expr.op)
        prece_operand = self.get_precedence(right_expr)

        col_no_stack = (
            []
        )  # The stack is used to pass up the line and column number changes
        if prece_op >= prece_operand and isinstance(
            right_expr, (ast.BinaryExpr, ast.Compare)
        ):
            tmp = right_expr
            while prece_op >= self.get_precedence(tmp.left) and isinstance(
                tmp.left, (ast.BinaryExpr, ast.Compare)
            ):
                col_no_stack.append(tmp)
                tmp = tmp.left
            if isinstance(expr, ast.UnaryExpr):
                expr.operand = tmp.left
            elif isinstance(expr, ast.BinaryExpr):
                expr.right = tmp.left
            expr.set_end_column(tmp.left.get_end_column())
            tmp.left = expr
            tmp.set_column(expr.get_column())
            # Calculate the change of the line and column number and pass it up
            while col_no_stack:
                tmp_col = col_no_stack.pop()
                tmp_col.set_column(expr.get_column())
            expr = start_local

        expr = check_utils.check_not_none(
            expr, ast.UnaryExpr, ast.BinaryExpr, ast.Compare
        )
        return expr

    def add_compare_precedence(
        self, expr: ast.Compare
    ) -> typing.Union[ast.BinaryExpr, ast.Compare, ast.Expr]:
        prece_op = self.get_precedence(expr)
        comparators = expr.comparators
        for i in range(len(comparators)):
            prece_child = self.get_precedence(comparators[i])
            col_no_stack = []
            if isinstance(comparators[i], ast.BinaryExpr) and prece_op > prece_child:
                tmp = comparators.pop(i)
                start_local = tmp
                while prece_op >= self.get_precedence(tmp.left) and isinstance(
                    tmp.left, ast.BinaryExpr
                ):
                    col_no_stack.append(tmp)
                    tmp = tmp.left
                comparators.insert(i, tmp.left)
                expr.set_end_column(comparators[-1].get_end_column())
                tmp.left = expr
                tmp.set_column(expr.get_column())
                # Calculate the change of the line and column number and pass it up
                while col_no_stack:
                    tmp_col = col_no_stack.pop()
                    tmp_col.set_column(expr.get_column())
                expr = start_local

        expr = check_utils.check_not_none(expr, ast.BinaryExpr, ast.Compare)
        return expr

    def walk_unary_expr(self, node: lark_pb.Tree) -> ast.UnaryExpr:
        """Syntax
        simple_expr : primary_expr | unary_expr | binary_expr
        unary_expr: un_op simple_expr
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.UnaryExpr, node, self.filename), ast.UnaryExpr
        )

        types = [ast.LarkToken.L_un_op, ast.LarkToken.L_simple_expr]
        p.op, p.operand = self.walk_elements_ordered_by_types(node, types)

        p.op = check_utils.check_not_none(p.op, ast.UnaryOp)
        p.operand = check_utils.check_not_none(p.operand, ast.Expr)

        self.register_precedence(p.op, p)
        return p

    def walk_binary_expr(
        self, node: lark_pb.Tree
    ) -> typing.Union[ast.BinaryExpr, ast.Compare]:
        """Syntax
        binary_expr: simple_expr bin_op simple_expr
        simple_expr : primary_expr | unary_expr | binary_expr
        """

        def __build_compare() -> typing.Union[ast.BinaryExpr, ast.Compare]:
            assert len(node.children) == 3
            cmp_expr = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.Compare, node, self.filename), ast.Compare
            )
            cmp_expr.left = check_utils.check_not_none(
                self.walk(node.children[0]), ast.Expr
            )

            for i in range(1, len(node.children)):
                n = node.children[i]
                if (
                    n.type == ast.LarkToken.L_bin_op
                    and len(n.children) > 0
                    and ast.judge_compare_op(n.children[0].type)
                ):
                    cmp_op = check_utils.check_not_none(self.walk(n), ast.CmpOp)
                    cmp_expr.ops.append(cmp_op)
                    continue
                elif n.type == ast.LarkToken.L_simple_expr:
                    comparator = check_utils.check_not_none(self.walk(n), ast.Expr)
                    if isinstance(comparator, ast.Compare):
                        cmp_expr.ops.extend(comparator.ops)
                        cmp_expr.comparators.append(comparator.left)
                        cmp_expr.comparators.extend(comparator.comparators)
                    elif isinstance(comparator, ast.Expr):
                        cmp_expr.comparators.append(comparator)
                    else:
                        report_complier_err(
                            f"Unsupported comparator type: {type(comparator)}",
                            filename=self.filename,
                            line_no=node.line,
                            col_no=node.column,
                            end_col_no=node.end_column,
                        )
                    continue
                else:
                    report_complier_err(
                        f"Unsupported compare operation type: {n.type}",
                        filename=self.filename,
                        line_no=node.line,
                        col_no=node.column,
                        end_col_no=node.end_column,
                    )

            if len(cmp_expr.ops) > 0:
                self.register_precedence(cmp_expr.ops[0], cmp_expr)
            return cmp_expr

        assert len(node.children) > 2
        assert len(node.children[1].children) > 0
        if ast.judge_compare_op(node.children[1].children[0].type):
            return __build_compare()

        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.BinaryExpr, node, self.filename), ast.BinaryExpr
        )

        types = [
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_bin_op,
            ast.LarkToken.L_simple_expr,
        ]
        p.left, p.op, p.right = self.walk_elements_ordered_by_types(node, types)

        p.left = check_utils.check_not_none(p.left, ast.Expr)
        p.op = check_utils.check_not_none(p.op, ast.BinOp)
        p.right = check_utils.check_not_none(p.right, ast.Expr)

        self.register_precedence(p.op, p)
        return p

    def walk_un_op(self, node: lark_pb.Tree) -> ast.UnaryOp:
        """Syntax
        un_op: L_NOT | PLUS | MINUS | NOT
        """
        assert len(node.children) > 0
        p = check_utils.check_not_none(
            ast.ASTFactory.get_op(ast.UnaryOp, node.children[0].type), ast.UnaryOp
        )
        self.register_precedence(p, p)
        return p

    def walk_bin_op(self, node: lark_pb.Tree) -> typing.Union[ast.BinOp, ast.CmpOp]:
        """Syntax
        bin_op: L_OR | L_AND
            | OR | XOR | AND
            | SHIFT_LEFT | SHIFT_RIGHT
            | PLUS | MINUS | MULTIPLY | DIVIDE | MOD | DOUBLE_DIVIDE
            | DOUBLE_STAR | EQUAL_TO | NOT_EQUAL_TO
            | LESS_THAN | GREATER_THAN | LESS_THAN_OR_EQUAL_TO | GREATER_THAN_OR_EQUAL_TO
            | IN | L_NOT IN | IS | IS L_NOT | L_NOT | AS
        """

        def __build_cmp_op() -> ast.CmpOp:
            real_type: str = ""
            for index in range(len(node.children)):
                if index != len(node.children) - 1:
                    real_type += node.children[index].type + " "
                else:
                    real_type += node.children[index].type

            cmp_op = check_utils.check_not_none(
                ast.ASTFactory.get_op(ast.CmpOp, real_type), ast.CmpOp
            )
            return cmp_op

        assert len(node.children) > 0
        if ast.judge_compare_op(node.children[0].type):
            p = __build_cmp_op()
        else:
            p = check_utils.check_not_none(
                ast.ASTFactory.get_op(ast.BinOp, node.children[0].type), ast.BinOp
            )
        self.register_precedence(p, p)
        return p

    def walk_primary_expr(
        self, node: lark_pb.Tree
    ) -> typing.Union[ast.Literal, ast.Expr]:
        """Syntax
        primary_expr: identifier call_suffix | operand | primary_expr select_suffix | primary_expr call_suffix
                    | primary_expr slice_suffix
        select_suffix:[question] DOT NAME
        call_suffix: LEFT_PARENTHESES [arguments [COMMA]] RIGHT_PARENTHESES
        slice_suffix: LEFT_BRACKETS (test | [test] COLON [test] [COLON [test]]) RIGHT_BRACKETS
        arguments: argument (COMMA argument)*
        argument: test | NAME ASSIGN test | MULTIPLY test | DOUBLE_STAR test
        number: DEC_NUMBER | HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | FLOAT_NUMBER | IMAG_NUMBER
        string: STRING | LONG_STRING
        """
        assert len(node.children) > 0
        if len(node.children) == 1:
            expr = check_utils.check_not_none(
                self.walk(node.children[0]), ast.Literal, ast.Expr
            )
            return expr
        elif len(node.children) == 2:
            n0 = node.children[0]
            if n0.type == ast.LarkToken.L_identifier:
                func_expr: ast.Identifier = check_utils.check_not_none(
                    self.walk(n0), ast.Identifier
                )
                call_suffix: ast.CallExpr = check_utils.check_not_none(
                    self.walk(node.children[1]), ast.CallExpr
                )
                call_suffix.func = func_expr
                call_suffix.set_ast_position(node, self.filename)
                return call_suffix
            elif n0.type == ast.LarkToken.L_primary_expr:
                func_expr = check_utils.check_not_none(
                    self.walk(n0), ast.Literal, ast.Expr
                )
                p = check_utils.check_not_none(
                    self.walk(node.children[1]),
                    ast.SelectorExpr,
                    ast.Subscript,
                    ast.CallExpr,
                )

                if node.children[1].type in [
                    ast.LarkToken.L_select_suffix,
                    ast.LarkToken.L_slice_suffix,
                ]:
                    p.value = func_expr
                elif node.children[1].type == ast.LarkToken.L_call_suffix:
                    p.func = func_expr
                p.set_ast_position(node, self.filename)
                return p

        report_complier_err(
            "Unsupported expression",
            filename=self.filename,
            line_no=node.line,
            col_no=node.column,
            end_col_no=node.end_column,
        )

    def walk_operand(
        self, node: lark_pb.Tree
    ) -> typing.Union[str, ast.Expr, ast.ParenExpr, ast.Identifier]:
        """Syntax
        operand: identifier | number | string | TRUE | FALSE | NONE | list_expr | list_comp | dict_expr | dict_comp
                            | schema_expr | LEFT_PARENTHESES expression RIGHT_PARENTHESES
        """
        if len(node.children) == 1:
            return self.walk(node.children[0])
        elif len(node.children) == 3:
            p = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.ParenExpr, node, self.filename),
                ast.ParenExpr,
            )
            p.expr = self.walk(node.children[1])
            return p
        else:
            report_complier_err(
                "Unsupported expression",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )

    def walk_call_suffix(self, node: lark_pb.Tree) -> ast.CallExpr:
        """Syntax
        call_suffix: LEFT_PARENTHESES [arguments [COMMA]] RIGHT_PARENTHESES
        arguments: argument (COMMA argument)*
        argument: test | NAME ASSIGN test | MULTIPLY test | DOUBLE_STAR test
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.CallExpr, node, self.filename), ast.CallExpr
        )

        args = self.walk_one_by_type(node, ast.LarkToken.L_arguments)
        if args and isinstance(args, ast.CallExpr):
            args.set_end_line_column(p)
            args.set_ast_position(node, self.filename)
            return args
        return p

    def walk_select_suffix(self, node: lark_pb.Tree) -> ast.SelectorExpr:
        """Syntax
        select_suffix: [QUESTION] DOT NAME
        """
        p: ast.SelectorExpr = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.SelectorExpr, node, self.filename),
            ast.SelectorExpr,
        )
        p.attr = check_utils.check_not_none(
            self.walk_one_by_type(node, ast.LarkToken.L_NAME), ast.Identifier
        )
        p.has_question = len(node.children) == 3
        return p

    def walk_slice_suffix(self, node: lark_pb.Tree) -> ast.Subscript:
        """Syntax
        slice_suffix: [QUESTION] LEFT_BRACKETS (test | [test] COLON [test] [COLON [test]]) RIGHT_BRACKETS
        test: if_expr | primary_expr | unary_expr | binary_expr
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.Subscript, node, self.filename), ast.Subscript
        )

        types = [
            ast.LarkToken.L_QUESTION,
            ast.LarkToken.L_test,
            ast.LarkToken.L_COLON,
            ast.LarkToken.L_test,
            ast.LarkToken.L_COLON,
            ast.LarkToken.L_test,
        ]
        (
            has_question,
            p.lower,
            colon1,
            p.upper,
            colon2,
            p.step,
        ) = self.walk_elements_ordered_by_types(node, types)

        has_question = check_utils.check_allow_none(has_question, str)
        p.lower = check_utils.check_allow_none(p.lower, ast.Expr)
        colon1 = check_utils.check_allow_none(colon1, str)
        p.upper = check_utils.check_allow_none(p.upper, ast.Expr)
        colon2 = check_utils.check_allow_none(colon2, str)
        p.step = check_utils.check_allow_none(p.step, ast.Expr)

        p.has_question = bool(has_question)
        if not colon1 and not colon2:
            p.index = p.lower
            p.lower = None

        return p

    def walk_arguments(self, node: lark_pb.Tree) -> ast.CallExpr:
        """Syntax
        arguments: argument (COMMA argument)*
        argument: test | NAME ASSIGN test | MULTIPLY test | DOUBLE_STAR test
        """
        p: ast.CallExpr = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.CallExpr, node, self.filename), ast.CallExpr
        )
        has_keyword = False

        for arg in node.children:
            if arg.type == ast.LarkToken.L_argument:
                assert len(arg.children) > 0
                n0 = arg.children[0]
                if n0.type == ast.LarkToken.L_test:
                    if has_keyword:
                        report_syntax_err(
                            arg="positional argument follows keyword argument",
                            filename=self.filename,
                            line_no=n0.line,
                            col_no=n0.column,
                            end_col_no=n0.end_column,
                        )
                    p.args.append(
                        check_utils.check_allow_none(self.walk(arg), ast.Expr)
                    )
                    continue
                elif n0.type == ast.LarkToken.L_NAME:
                    p.keywords.append(
                        check_utils.check_allow_none(self.walk(arg), ast.Keyword)
                    )
                    has_keyword = True
                    continue
                else:
                    report_complier_err(
                        "Unsupported argument",
                        filename=self.filename,
                        line_no=node.line,
                        col_no=node.column,
                        end_col_no=node.end_column,
                    )
        return p

    def walk_argument(self, node: lark_pb.Tree) -> ast.Expr:
        """Syntax
        argument: test | NAME ASSIGN test | MULTIPLY test | DOUBLE_STAR test
        """
        assert len(node.children) > 0
        name, test = self.walk_elements_ordered_by_types(
            node, [ast.LarkToken.L_NAME, ast.LarkToken.L_test]
        )
        name = check_utils.check_allow_none(name, ast.Identifier)
        test = check_utils.check_not_none(test, ast.Expr)
        if name:
            keyword: ast.Keyword = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.Keyword, node.children[0], self.filename),
                ast.Keyword,
            )
            keyword.arg = name
            keyword.value = test
            assert (
                len(name.name_nodes) == 1
            ), "name of the keyword argument must be a single NAME"
            return keyword
        else:
            return test

    # -----------------------------------------------------
    # operand
    # -----------------------------------------------------

    def walk_identifier(
        self, node: lark_pb.Tree, *, _ctx: ast.ExprContext = ast.ExprContext.LOAD
    ) -> ast.Identifier:
        """Syntax
        identifier: NAME (DOT NAME)*
        """
        p: ast.Identifier = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.Identifier, node, self.filename), ast.Identifier
        )
        p.names = [
            n.token_value for n in node.children if n.type == ast.LarkToken.L_NAME
        ]
        p.name_nodes = [
            self.lark_name_to_ast_name(n)
            for n in node.children
            if n.type == ast.LarkToken.L_NAME
        ]
        return p

    def walk_quant_expr(self, node: lark_pb.Tree) -> ast.QuantExpr:
        """Syntax
        quant_expr: quant_op [ identifier COMMA ] identifier IN quant_target LEFT_BRACE (simple_expr [IF simple_expr]
                    | NEWLINE _INDENT simple_expr [IF simple_expr] NEWLINE _DEDENT)? RIGHT_BRACE
        quant_target: string | identifier | list_expr | list_comp | dict_expr | dict_comp
        quant_op: ALL | ANY | FILTER | MAP
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.QuantExpr, node, self.filename), ast.QuantExpr
        )
        types = [
            ast.LarkToken.L_quant_op,
            ast.LarkToken.L_quant_target,
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_simple_expr,
        ]
        op, p.target, p.test, p.if_cond = self.walk_elements_ordered_by_types(
            node, types
        )
        op = check_utils.check_not_none(op, str)
        p.target = check_utils.check_not_none(p.target, ast.Expr)
        p.test = check_utils.check_not_none(p.test, ast.Expr)
        p.if_cond = check_utils.check_allow_none(p.if_cond, ast.Expr)
        p.variables = check_utils.check_all_not_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_identifier),
            ast.Identifier,
        )
        p.op = ast.QuantOperation.MAPPING[op]

        return p

    def walk_list_expr(self, node: lark_pb.Tree) -> ast.ListExpr:
        """Syntax
        list_expr: LEFT_BRACKETS [list_items | NEWLINE _INDENT list_items _DEDENT] RIGHT_BRACKETS
        list_items: list_item ((COMMA [NEWLINE] | NEWLINE) list_item)* [COMMA] [NEWLINE]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ListExpr, node, self.filename), ast.ListExpr
        )
        p.elts = (
            check_utils.check_all_allow_none(
                list, self.walk_one_by_type(node, ast.LarkToken.L_list_items), ast.Expr
            )
            or []
        )

        return p

    def walk_list_items(self, node: lark_pb.Tree) -> typing.List[ast.Expr]:
        """Syntax
        list_items: list_item ((COMMA [NEWLINE] | NEWLINE) list_item)* [COMMA] [NEWLINE]
        list_item: test | star_expr | if_item
        """
        return check_utils.check_all_allow_none(
            list, self.walk_all_by_type(node, ast.LarkToken.L_list_item), ast.Expr
        )

    def walk_list_comp(self, node: lark_pb.Tree) -> ast.ListComp:
        """Syntax
        list_comp: LEFT_BRACKETS (list_item comp_clause+
                    | NEWLINE _INDENT list_item comp_clause+ _DEDENT) RIGHT_BRACKETS
        list_item: test | star_expr
        comp_clause: FOR loop_variables [COMMA] IN test [NEWLINE] [IF test [NEWLINE]]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ListComp, node, self.filename), ast.ListComp
        )
        p.elt = check_utils.check_not_none(
            self.walk_one_by_type(node, ast.LarkToken.L_list_item), ast.Expr
        )
        p.generators = check_utils.check_all_not_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_comp_clause),
            ast.CompClause,
        )
        return p

    def walk_dict_comp(self, node: lark_pb.Tree) -> ast.DictComp:
        """Syntax
        dict_comp: LEFT_BRACE (entry comp_clause+ | NEWLINE _INDENT entry comp_clause+ _DEDENT) RIGHT_BRACE
        entry: test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr
        comp_clause: FOR loop_variables [COMMA] IN test [NEWLINE] [IF test [NEWLINE]]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.DictComp, node, self.filename), ast.DictComp
        )

        key_value = self.walk_one_by_type(node, ast.LarkToken.L_entry)
        assert len(key_value) == 3
        assert key_value[0] is None or isinstance(key_value[0], ast.Expr)
        p.key = key_value[0]
        p.operation = key_value[1]
        p.value = check_utils.check_not_none(key_value[2], ast.Expr)
        p.generators = check_utils.check_all_not_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_comp_clause),
            ast.CompClause,
        )
        return p

    def walk_entries(
        self, node: lark_pb.Tree
    ) -> typing.List[typing.Tuple[typing.Optional[ast.Expr], ast.Expr]]:
        """Syntax
        entries: entry ((COMMA [NEWLINE] | NEWLINE) entry)* [COMMA] [NEWLINE]
        entry: test COLON test | double_star_expr
        """
        return self.walk_non_token_children(node)

    def walk_entry(
        self, node: lark_pb.Tree
    ) -> typing.Tuple[typing.Optional[ast.Expr], ast.Expr]:
        """Syntax
        entry: test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr | if_entry
        double_star_expr: DOUBLE_STAR primary_expr
        """
        if len(node.children) == 3:
            key = check_utils.check_not_none(self.walk(node.children[0]), ast.Expr)
            val = check_utils.check_not_none(self.walk(node.children[2]), ast.Expr)
            op = ast.ConfigEntryOperation.MAPPING[node.children[1].type]
            return key, op, val
        elif len(node.children) == 1:
            return self.walk(node.children[0])
        else:
            report_complier_err(
                "Unsupported entry",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )

    def walk_comp_clause(self, node: lark_pb.Tree) -> ast.CompClause:
        """Syntax
        comp_clause: FOR loop_variables [COMMA] IN simple_expr [NEWLINE] [IF test [NEWLINE]]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.CompClause, node, self.filename), ast.CompClause
        )

        types = [
            ast.LarkToken.L_loop_variables,
            ast.LarkToken.L_simple_expr,
            ast.LarkToken.L_test,
        ]
        p.targets, p.iter, test = self.walk_elements_ordered_by_types(node, types)
        p.targets = check_utils.check_all_not_none(list, p.targets, ast.Identifier)
        for i, target in enumerate(p.targets):
            p.targets[i].ctx = ast.ExprContext.STORE
        p.iter = check_utils.check_not_none(p.iter, ast.Expr)
        test = check_utils.check_allow_none(test, ast.Expr)

        p.ifs = [test] if test else []
        return p

    def walk_if_entry(
        self, node: lark_pb.Tree
    ) -> typing.Tuple[None, ast.ConfigIfEntryExpr]:
        """Syntax
        if_entry: IF test COLON if_entry_exec_block (ELIF test COLON if_entry_exec_block)* (ELSE COLON if_entry_exec_block)?
        if_entry_exec_block: (test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr | if_entry) [NEWLINE] | NEWLINE _INDENT (test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr | if_entry) ((COMMA [NEWLINE] | [NEWLINE]) (test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr | if_entry))* [COMMA] [NEWLINE] _DEDENT
        """
        if_entry_node = node
        tests = lark_parser.GetNodeList(
            if_entry_node, ast.LarkToken.L_test, recursively=False
        )
        exec_blocks = lark_parser.GetNodeList(
            if_entry_node, ast.LarkToken.L_if_entry_exec_block, recursively=False
        )
        assert tests and exec_blocks
        if_exec_blocks = exec_blocks[0]
        key_value_tests = lark_parser.GetNodeList(
            if_exec_blocks,
            ast.LarkToken.L_test,
            ast.LarkToken.L_double_star_expr,
            ast.LarkToken.L_if_entry,
            recursively=False,
        )
        operation_nodes = lark_parser.GetNodeList(
            if_exec_blocks,
            ast.LarkToken.L_COLON,
            ast.LarkToken.L_ASSIGN,
            ast.LarkToken.L_COMP_PLUS,
            recursively=False,
        )
        val = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ConfigIfEntryExpr, node, self.filename),
            ast.ConfigIfEntryExpr,
        )
        val.if_cond = self.walk(tests[0])
        i = 0
        j = 0
        while i < len(key_value_tests):
            if (
                key_value_tests[i].type == ast.LarkToken.L_double_star_expr
                or key_value_tests[i].type == ast.LarkToken.L_if_entry
            ):
                key, value = self.walk(key_value_tests[i])
                val.keys.append(key)
                val.values.append(value)
                val.operations.append(ast.ConfigEntryOperation.OVERRIDE)
                i += 1
            else:
                val.keys.append(self.walk(key_value_tests[i]))
                val.values.append(self.walk(key_value_tests[i + 1]))
                val.operations.append(
                    ast.ConfigEntryOperation.MAPPING.get(operation_nodes[j].type)
                )
                j += 1
                i += 2
        if len(exec_blocks) > 1:
            if len(tests) >= 1:
                has_else = len(exec_blocks) > len(tests)
                next_node = None
                if has_else:
                    block = exec_blocks[-1]
                    config_expr = ast.ConfigExpr()
                    config_expr.set_ast_position(block, self.filename)
                    key_value_tests = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_test,
                        ast.LarkToken.L_double_star_expr,
                        ast.LarkToken.L_if_entry,
                        recursively=False,
                    )
                    operation_nodes = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_COLON,
                        ast.LarkToken.L_ASSIGN,
                        ast.LarkToken.L_COMP_PLUS,
                        recursively=False,
                    )
                    i = 0
                    j = 0
                    while i < len(key_value_tests):
                        if (
                            key_value_tests[i].type == ast.LarkToken.L_double_star_expr
                            or key_value_tests[i].type == ast.LarkToken.L_if_entry
                        ):
                            key, value = self.walk(key_value_tests[i])
                            operation = ast.ConfigEntryOperation.OVERRIDE
                            i += 1
                        else:
                            key = self.walk(key_value_tests[i])
                            value = self.walk(key_value_tests[i + 1])
                            operation = ast.ConfigEntryOperation.MAPPING.get(
                                operation_nodes[j].type
                            )
                            j += 1
                            i += 2
                        config_expr.items.append(
                            check_utils.check_not_none(
                                ast.ASTFactory.get_ast_configentry(
                                    key, value, operation, self.filename
                                ),
                                ast.ConfigEntry,
                            )
                        )
                    next_node = config_expr
                    exec_blocks = exec_blocks[:-1]
                for test, block in zip(tests[1:][::-1], exec_blocks[1:][::-1]):
                    config_if_expr = ast.ConfigIfEntryExpr()
                    config_if_expr.set_ast_position(test, self.filename)
                    key_value_tests = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_test,
                        ast.LarkToken.L_double_star_expr,
                        ast.LarkToken.L_if_entry,
                        recursively=False,
                    )
                    operation_nodes = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_COLON,
                        ast.LarkToken.L_ASSIGN,
                        ast.LarkToken.L_COMP_PLUS,
                        recursively=False,
                    )
                    config_if_expr.if_cond = self.walk(test)
                    i = 0
                    j = 0
                    while i < len(key_value_tests):
                        if (
                            key_value_tests[i].type == ast.LarkToken.L_double_star_expr
                            or key_value_tests[i].type == ast.LarkToken.L_if_entry
                        ):
                            key, value = self.walk(key_value_tests[i])
                            config_if_expr.keys.append(key)
                            config_if_expr.values.append(value)
                            config_if_expr.operations.append(
                                ast.ConfigEntryOperation.OVERRIDE
                            )
                            i += 1
                        else:
                            config_if_expr.keys.append(self.walk(key_value_tests[i]))
                            config_if_expr.values.append(
                                self.walk(key_value_tests[i + 1])
                            )
                            config_if_expr.operations.append(
                                ast.ConfigEntryOperation.MAPPING.get(
                                    operation_nodes[j].type
                                )
                            )
                            j += 1
                            i += 2
                    config_if_expr.orelse = next_node
                    next_node = config_if_expr
            val.orelse = next_node
        return None, val

    def walk_if_item(self, node: lark_pb.Tree) -> ast.ListIfItemExpr:
        """Syntax
        if_item: IF test COLON if_item_exec_block (ELIF test COLON if_item_exec_block)* (ELSE COLON if_item_exec_block)?
        if_item_exec_block: list_item [NEWLINE] | NEWLINE _INDENT list_item ((COMMA [NEWLINE] | NEWLINE) list_item)* [COMMA] [NEWLINE] _DEDENT
        """
        x = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ListIfItemExpr, node, self.filename),
            ast.ListIfItemExpr,
        )
        if_item_node = node
        tests = lark_parser.GetNodeList(
            if_item_node, ast.LarkToken.L_test, recursively=False
        )
        exec_blocks = lark_parser.GetNodeList(
            if_item_node, ast.LarkToken.L_if_item_exec_block, recursively=False
        )
        x.if_cond = self.walk(tests[0])
        item_tests = lark_parser.GetNodeList(
            exec_blocks[0],
            ast.LarkToken.L_list_item,
            recursively=False,
        )
        x.exprs = [self.walk(test) for test in item_tests]
        next_node = None
        if len(exec_blocks) > 1:
            if len(tests) >= 1:
                has_else = len(exec_blocks) > len(tests)
                if has_else:
                    block = exec_blocks[-1]
                    item_tests = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_list_item,
                        recursively=False,
                    )
                    list_expr = ast.ListExpr()
                    list_expr.set_ast_position(block, self.filename)
                    list_expr.elts = [self.walk(test) for test in item_tests]
                    next_node = list_expr
                    exec_blocks = exec_blocks[:-1]
                for test, block in zip(tests[1:][::-1], exec_blocks[1:][::-1]):
                    list_if_expr = ast.ListIfItemExpr()
                    list_if_expr.set_ast_position(test, self.filename)
                    item_tests = lark_parser.GetNodeList(
                        block,
                        ast.LarkToken.L_list_item,
                        recursively=False,
                    )
                    list_if_expr.if_cond = self.walk(test)
                    list_if_expr.exprs = [self.walk(test) for test in item_tests]
                    list_if_expr.orelse = next_node
                    next_node = list_if_expr
            x.orelse = next_node
        return x

    def walk_star_expr(self, node: lark_pb.Tree) -> ast.StarredExpr:
        """Syntax
        star_expr: MULTIPLY test
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.StarredExpr, node, self.filename),
            ast.StarredExpr,
        )
        assert len(node.children) == 2
        p.value = check_utils.check_not_none(self.walk(node.children[1]), ast.Expr)
        return p

    def walk_double_star_expr(self, node: lark_pb.Tree) -> typing.Tuple:
        """Syntax
        double_star_expr: DOUBLE_STAR test
        """
        val = check_utils.check_not_none(
            self.walk_one_by_type(node, ast.LarkToken.L_test),
            ast.Literal,
            ast.Expr,
        )
        return None, val

    def walk_loop_variables(self, node: lark_pb.Tree) -> typing.List:
        """Syntax
        loop_variables: primary_expr (COMMA primary_expr)*
        """
        loop_variables = self.walk_all_by_type(node, ast.LarkToken.L_primary_expr)
        if len(loop_variables) == 0 or len(loop_variables) > 2:
            report_complier_err(
                arg=f"the number of loop variables is {len(loop_variables)}, which can only be 1 or 2",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )

        for n in loop_variables:
            is_identifier = isinstance(n, ast.Identifier)
            identifier = typing.cast(ast.Identifier, n)
            if not is_identifier or (is_identifier and len(identifier.names) != 1):
                report_complier_err(
                    "loop variables can only be ordinary identifiers",
                    filename=self.filename,
                    line_no=node.line,
                    col_no=node.column,
                    end_col_no=node.end_column,
                )
        return loop_variables

    def walk_schema_expr(self, node: lark_pb.Tree) -> ast.SchemaExpr:
        """Syntax
        schema_expr: identifier (LEFT_PARENTHESES [arguments] RIGHT_PARENTHESES)? config_expr
        arguments: argument (COMMA argument)*
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.SchemaExpr, node, self.filename), ast.SchemaExpr
        )

        types = [
            ast.LarkToken.L_identifier,
            ast.LarkToken.L_arguments,
            ast.LarkToken.L_config_expr,
        ]
        p.name, call_expr, p.config = self.walk_elements_ordered_by_types(node, types)

        p.name = check_utils.check_not_none(p.name, ast.Identifier)
        p.config = check_utils.check_not_none(p.config, ast.ConfigExpr)
        call_expr = check_utils.check_allow_none(call_expr, ast.CallExpr)

        p.args = call_expr.args if call_expr else []
        p.kwargs = call_expr.keywords if call_expr else []
        return p

    def walk_config_expr(self, node: lark_pb.Tree) -> ast.ConfigExpr:
        """Syntax
        config_expr: LEFT_BRACE [config_entries | NEWLINE _INDENT config_entries _DEDENT] RIGHT_BRACE
        config_entries: config_entry ((COMMA [NEWLINE] | NEWLINE) config_entry)* [COMMA] [NEWLINE]
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ConfigExpr, node, self.filename), ast.ConfigExpr
        )
        p.items = (
            check_utils.check_all_allow_none(
                list,
                self.walk_one_by_type(node, ast.LarkToken.L_config_entries),
                ast.ConfigEntry,
            )
            or []
        )
        return p

    def walk_config_entries(self, node: lark_pb.Tree) -> typing.List[ast.ConfigEntry]:
        """Syntax
        config_entries: config_entry ((COMMA [NEWLINE] | NEWLINE) config_entry)* [COMMA] [NEWLINE]
        config_entry: (test config_op test) | DOUBLE_STAR primary_expr
        config_op: COLON | ASSIGN | COMP_PLUS
        """
        return check_utils.check_all_allow_none(
            list,
            self.walk_all_by_type(node, ast.LarkToken.L_config_entry),
            ast.ConfigEntry,
        )

    def walk_config_entry(self, node: lark_pb.Tree) -> ast.ConfigEntry:
        """Syntax
        config_entry: test (COLON | ASSIGN | COMP_PLUS) test | double_star_expr
        double_star_expr: DOUBLE_STAR primary_expr
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.ConfigEntry, node, self.filename),
            ast.ConfigEntry,
        )

        if len(node.children) == 3:
            config_op = node.children[1]
            p.key = check_utils.check_not_none(self.walk(node.children[0]), ast.Expr)
            p.value = check_utils.check_not_none(self.walk(node.children[2]), ast.Expr)
            p.operation = ast.ConfigEntryOperation.MAPPING.get(config_op.type)
            assert p.operation in range(
                ast.ConfigEntryOperation.get_min(),
                ast.ConfigEntryOperation.get_max() + 1,
            )
        elif len(node.children) == 1:
            key_value = self.walk(node.children[0])
            assert len(key_value) == 2
            assert key_value[0] is None
            p.value = check_utils.check_not_none(key_value[1], ast.Literal, ast.Expr)
            p.key = key_value[0]
        else:
            report_complier_err(
                "Unsupported schema config entry",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )
        return p

    def walk_lambda_expr(self, node: lark_pb.Tree) -> ast.LambdaExpr:
        """Syntax
        lambda_expr: LAMBDA [schema_arguments] [RIGHT_ARROW type] LEFT_BRACE [expr_stmt | NEWLINE _INDENT schema_init_stmt+] _DEDENT RIGHT_BRACE
        """
        p = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.LambdaExpr, node, self.filename), ast.LambdaExpr
        )
        types = [
            ast.LarkToken.L_schema_arguments,
            ast.LarkToken.L_type,
        ]
        p.args, p.return_type_node = self.walk_elements_ordered_by_types(node, types)
        p.return_type_str = (
            p.return_type_node.plain_type_str if p.return_type_node else ""
        )
        expr_stmt_list = lark_parser.GetNodeList(
            node, ast.LarkToken.L_expr_stmt, recursively=False
        )
        p.body = (
            [self.walk(expr_stmt_list[0])]
            if expr_stmt_list
            else self.walk_all_by_type(node, ast.LarkToken.L_schema_init_stmt)
        )
        return p

    def walk_NAME(self, node: lark_pb) -> ast.Identifier:
        def __build_identifier_by_name() -> ast.Identifier:
            p: ast.Identifier = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.Identifier, node, self.filename),
                ast.Identifier,
            )
            p.names.append(node.token_value)
            p.name_nodes.append(self.lark_name_to_ast_name(node))
            return p

        assert node
        return __build_identifier_by_name()

    def walk_constant(self, node: lark_pb.Tree) -> ast.NameConstantLit:
        """
        Grammar:
        NONE | TRUE | FALSE

        Input:
        constant node token.

        Output:
        constant ast.
        """

        if len(node.children) == 1 and ast.LarkToken.is_name_constant(
            node.children[0].type
        ):
            token_type = node.children[0].type
            p = check_utils.check_not_none(
                ast.ASTFactory.get_ast(
                    ast.NameConstantLit, node.children[0], self.filename
                ),
                ast.NameConstantLit,
            )
            p.value = (
                literal_eval(node.children[0].token_value)
                if token_type != ast.LarkToken.L_UNDEFINED
                else internal.Undefined
            )
            return p
        else:
            report_complier_err(
                "Unsupported Constant",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )

    def walk_number(self, node: lark_pb.Tree) -> ast.NumberLit:
        """
        Grammar:
        number: DEC_NUMBER [multiplier] | HEX_NUMBER | BIN_NUMBER | OCT_NUMBER | FLOAT_NUMBER
        Input:
        number lit tree node.
        Output:
        number lit ast.
        """
        p: ast.NumberLit = check_utils.check_not_none(
            ast.ASTFactory.get_ast(ast.NumberLit, node, self.filename), ast.NumberLit
        )

        if len(node.children) >= 1 and ast.LarkToken.is_int_number(
            node.children[0].type
        ):
            p.value = (
                literal_eval(node.children[0].token_value)
                if node.children[0].type != ast.LarkToken.L_OCT_NUMBER
                else int(node.children[0].token_value, base=8)
            )
            if len(node.children) == 2:
                p.binary_suffix = self.walk(node.children[1])
            return p
        elif len(node.children) >= 1 and ast.LarkToken.is_float_number(
            node.children[0].type
        ):
            p.value = literal_eval(node.children[0].token_value)
            return p
        else:
            report_complier_err(
                "Unsupported Number",
                filename=self.filename,
                line_no=node.line,
                col_no=node.column,
                end_col_no=node.end_column,
            )

    def walk_multiplier(self, node: lark_pb.Tree) -> str:
        """
        Grammar:
        multiplier: ( SI_N_L | SI_U_L | SI_M_L | SI_K_L | SI_K | SI_M | SI_G | SI_T | SI_P ) [ IEC ]
        Input:
        multiplier tree node.
        Output:
        multiplier ast.
        """
        assert len(node.children) >= 1
        bp = node.children[0].token_value
        if len(node.children) == 2:
            bp += "i"

        if not hasattr(ast.NumberBinarySuffix, bp):
            report_complier_err(
                f"unsupported quantity type: {bp}",
                filename=self.filename,
                line_no=node.children[0].line,
                col_no=node.children[0].column,
                end_col_no=node.children[0].end_column,
            )

        return getattr(ast.NumberBinarySuffix, bp)

    def walk_string(
        self, node: lark_pb.Tree
    ) -> typing.Union[ast.StringLit, ast.JoinedString]:
        """
        According to pypy JoinedString
        """
        assert len(node.children) >= 1
        node = node.children[0]
        token_value = literal_eval(node.token_value)
        string_value = (
            bytes.decode(token_value)
            if isinstance(token_value, bytes)
            else str(token_value)
        )
        quotation = '"' if node.token_value.endswith('"') else "'"
        quotation_index = node.token_value.find(quotation)
        has_raw_prefix = (
            "r" in node.token_value[:quotation_index]
            or "R" in node.token_value[:quotation_index]
        )
        has_interpolation = bool(STRING_INTERPOLATION_REGEX.search(string_value))
        if has_raw_prefix or not has_interpolation:
            p = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.StringLit, node, self.filename),
                ast.StringLit,
            )
            p.value = string_value
        else:
            p = check_utils.check_not_none(
                ast.ASTFactory.get_ast(ast.JoinedString, node, self.filename),
                ast.JoinedString,
            )
            end = 0
            str_len = len(string_value)
            if "$" in string_value.replace("${", "").replace("$$", ""):
                # Check invalid single `$`
                report_complier_err(
                    arg="invalid single '$', expecting '$' or '{'",
                    err_type=kcl_error.ErrType.InvalidFormatSpec_TYPE,
                    filename=self.filename,
                    line_no=node.line,
                    col_no=node.column,
                    end_col_no=node.end_column,
                )
            while string_value and STRING_INTERPOLATION_REGEX.search(string_value):
                match = STRING_INTERPOLATION_REGEX.search(string_value)
                expr_string = match.group(1)
                format_spec = None
                if ":" in expr_string:
                    parts = expr_string.split(":")
                    expr_string, format_spec = parts[0], parts[1].strip()
                start, end = match.span()
                string_lit = string_value[:start]
                if string_lit is not None:
                    p.values.append(
                        ast.ASTFactory.get_ast_literal(
                            ast.StringLit,
                            line=node.line,
                            column=node.column + start,
                            value=string_lit.replace("$$", "$"),
                            filename=self.filename,
                        )
                    )
                string_value = string_value[end:]
                expr = ParseExpr(
                    expr_string,
                    filename=self.filename,
                    line_offset=node.line - 1,
                    column_offset=node.column + 2 + start,
                )
                if expr:
                    expr.filename = self.filename
                if not expr:
                    report_complier_err(
                        arg=f"invalid string interpolation expression '{expr_string}'",
                        err_type=kcl_error.ErrType.InvalidFormatSpec_TYPE,
                        filename=self.filename,
                        line_no=node.line,
                        col_no=node.column,
                        end_col_no=node.end_column,
                    )
                formatted_value = check_utils.check_not_none(
                    ast.ASTFactory.get_ast_formatted_value(
                        expr, format_spec, self.filename
                    ),
                    ast.FormattedValue,
                )
                p.values.append(formatted_value)
            if end < str_len and string_value is not None:
                p.values.append(
                    check_utils.check_not_none(
                        ast.ASTFactory.get_ast_literal(
                            ast.StringLit,
                            line=node.line,
                            column=node.column + end,
                            value=string_value.replace("$$", "$"),
                            filename=self.filename,
                        ),
                        ast.StringLit,
                    )
                )
        p.raw_value = node.token_value
        p.is_long_string = node.type == ast.LarkToken.L_LONG_STRING
        return p

    # -------------------------------------------------------------------------
    # simplify_StringLitValue
    # -------------------------------------------------------------------------

    def simplify_StringLitValue(self, str_lit: ast.StringLit) -> str:
        if not str_lit or not str_lit.value:
            return ""

        s: str = str_lit.value
        # """str_value""" -> str_value
        if s.startswith('"""'):
            return s[3:-3]

        # '''str_value''' -> str_value
        if s.startswith("'''"):
            return s[3:-3]

        # "str_value" -> str_value
        if s.startswith('"'):
            return s[1:-1]

        # 'str_value' -> str_value
        if s.startswith("'"):
            return s[1:-1]

        # str_value -> str_value
        return s


# -----------------------------------------------------------------------------
# ParseFile
# -----------------------------------------------------------------------------


def ParseFile(
    filename: typing.Optional[str],
    code: typing.Optional[str] = None,
    *,
    pkg: str = "",
    mode: ParseMode = ParseMode.Null,
) -> ast.Module:
    try:
        if code is None:
            code = Path(filename).read_text(encoding="utf-8")
        # Store the KCL error file cache map
        kcl_error.FileCache.put(filename, code)
        lark_parser.filename = filename
        larktree = lark_parser.ParseCode(code)
        return ASTBuilder(larktree, pkg=pkg, filename=filename, mode=mode).BuildAST()
    except (
        lark.exceptions.UnexpectedCharacters,
        lark.exceptions.UnexpectedToken,
    ) as err:
        if kclvm.config.debug > 0:
            raise
        token_msgs = None
        allowed_tokens = (
            list(err.accepts)
            if isinstance(err, lark.exceptions.UnexpectedToken)
            else list(err.allowed)
        )
        if allowed_tokens:
            allowed_tokens.sort()
            token_msgs = [
                ParseAcceptToken(allowed_token) for allowed_token in allowed_tokens
            ]
        arg_msg = f"Expected one of {token_msgs}" if token_msgs else None
        syntax_err = kcl_error.get_exception(
            err_type=kcl_error.ErrType.InvalidSyntax_TYPE,
            file_msgs=[
                kcl_error.ErrFileMsg(
                    filename=filename,
                    line_no=err.line,
                    col_no=err.column,
                    src_code=code,
                    arg_msg=arg_msg,
                )
            ],
        )
        syntax_err.accepts_lark = allowed_tokens
        syntax_err.accepts_msg = token_msgs
        raise syntax_err


@PreCheck((lambda v: CheckRules.check_type_not_none(v, str)), "key")
@PostCheck(lambda v: CheckRules.check_type_not_none(v, str))
def ParseAcceptToken(key: str) -> str:
    if key in ast.lark_token.LarkToken.LL_token_str_value_map.keys():
        token_value = ast.lark_token.LarkToken.LL_token_str_value_map[key]
    elif key in ast.lark_token.LarkToken.LL_token_list:
        token_value = key.lower()
    else:
        token_value = key
    return token_value


# -----------------------------------------------------------------------------
# ParseExpr
# -----------------------------------------------------------------------------


def ParseExpr(
    code: str,
    filename: str = None,
    line_offset: int = 0,
    column_offset: int = 0,
    *,
    pkg: str = "",
    mode: ParseMode = ParseMode.Null,
) -> ast.Expr:
    ast.AST.set_offset(line_offset, column_offset)
    module = ParseFile(filename, code, pkg=pkg, mode=mode)
    ast.AST.reset_offset()
    return module.GetFirstExprInExprStmt()


# -----------------------------------------------------------------------------
# LoadProgram
# -----------------------------------------------------------------------------


def _loadPackages(
    modfile: kclvm.config.KclModFile,
    import_spec: ast.ImportStmt,
    pkgs: typing.Dict[str, typing.List[ast.Module]],
    missing_pkgs: typing.List[str],
    main_files: typing.List[str],
    mode: ParseMode = ParseMode.Null,
    set_ast_parent: bool = False,
):
    root: str = modfile.root
    pkgpath: str = import_spec.path

    assert root, f"root={root}"
    assert pkgs

    if not pkgpath:
        return

    # plugin pkgs
    if pkgpath.startswith("kcl_plugin."):
        return  # ignore import error

    # builtin pkgs
    if pkgpath in builtin.STANDARD_SYSTEM_MODULES:
        return

    if pkgpath in pkgs:
        return
    if pkgpath in missing_pkgs:
        return

    # try cache
    if modfile.build.enable_pkg_cache:
        if modfile.build.cached_pkg_prefix and pkgpath.startswith(
            modfile.build.cached_pkg_prefix
        ):
            m_list = vfs.LoadPkgCache(
                root, pkgpath, option=get_ast_cache_option(set_ast_parent)
            )
            if m_list and isinstance(m_list, list):
                pkgs[pkgpath] = m_list
                for m in pkgs[pkgpath]:
                    import_spec_list = fix.fix_and_get_module_import_list(
                        root, m, False
                    )
                    for import_spec in import_spec_list:
                        _loadPackages(
                            modfile,
                            import_spec,
                            pkgs,
                            missing_pkgs,
                            main_files,
                            mode=mode,
                            set_ast_parent=set_ast_parent,
                        )
                return

    # scan all kcl files
    all_k_files = glob.glob(
        f"{root}{os.sep}{pkgpath.replace('.', os.sep)}{os.sep}*.k", recursive=False
    )

    # skip `_*.k` and `*_test.k` kcl files
    k_files: typing.List[str] = []
    for s in all_k_files:
        if os.path.basename(s).startswith("_"):
            continue
        if s.endswith("_test.k"):
            continue

        k_files.append(s)

    if len(k_files) == 0 and os.path.isfile(
        f"{root}{os.sep}{pkgpath.replace('.', os.sep)}.k"
    ):
        file = f"{root}{os.sep}{pkgpath.replace('.', os.sep)}.k"
        k_files.append(file)

    if len(k_files) == 0:
        missing_pkgs.append(pkgpath)
        return

    pkgs[pkgpath]: typing.List[ast.Module] = []

    k_files.sort()
    for filename in k_files:
        with open(filename, "r", encoding="utf-8") as f:
            code = str(f.read())

        m = ParseFile(filename, code, pkg=pkgpath, mode=mode)
        m.relative_filename = m.filename.replace(root, ".", 1)
        preprocess.fix_identifier_prefix(m)
        fix.fix_and_get_module_import_list(root, m)
        fix.fix_qualified_identifier(m)
        if set_ast_parent:
            fix.fix_set_parent_info(m)
        pkgs[pkgpath].append(m)

    # save cache
    if modfile.build.enable_pkg_cache:
        if modfile.build.cached_pkg_prefix and pkgpath.startswith(
            modfile.build.cached_pkg_prefix
        ):
            vfs.SavePkgCache(
                root,
                pkgpath,
                pkgs[pkgpath],
                option=get_ast_cache_option(set_ast_parent),
            )

    for m in pkgs[pkgpath]:
        import_spec_list = fix.fix_and_get_module_import_list(root, m, False)
        for import_spec in import_spec_list:
            _loadPackages(
                modfile,
                import_spec,
                pkgs,
                missing_pkgs,
                main_files,
                mode=mode,
                set_ast_parent=set_ast_parent,
            )

    return


def LoadProgram(
    *path: str,
    work_dir: str = "",
    k_code_list: typing.List[str] = None,
    mode: ParseMode = ParseMode.Null,
    set_ast_parent: bool = False,
    load_packages: bool = True,
) -> ast.Program:
    assert len(path) > 0
    # Clear the KCL error file cache map
    kcl_error.FileCache.clear()
    k_code_list = k_code_list or []
    if work_dir or kclvm.config.current_path:
        path_list = [
            str(x).replace(
                KCL_MOD_PATH_ENV,
                vfs.GetPkgRoot(work_dir or kclvm.config.current_path) or "",
            )
            for x in path
        ]
    else:
        path_list = [str(x) for x in path]
    k_files: typing.List[str] = []
    kclvm.config.input_file = path_list
    for i, s in enumerate(path_list):
        s = os.path.abspath(s)
        if os.path.isdir(s):
            for filename in glob.glob(f"{s}{os.sep}*.k", recursive=False):
                if os.path.basename(filename).startswith("_"):
                    continue
                k_files.append(os.path.abspath(filename))
        elif i < len(k_code_list) or os.path.isfile(s):
            k_files.append(os.path.abspath(s))
        else:
            arg_msg = (
                f"Cannot find the kcl file, please check whether the file path {s}"
                if s
                else f"The file path {s} is None"
            )
            kcl_error.report_exception(
                err_type=kcl_error.ErrType.CannotFindModule_TYPE, arg_msg=arg_msg
            )

    if not k_files:
        report_complier_err("No input KCL files")

    if not work_dir:
        work_dir = os.path.dirname(k_files[0])

    root: str = vfs.MustGetPkgRoot(k_files)
    if not root:
        root = work_dir

    modfile: kclvm.config.KclModFile = vfs.LoadModFile(root)

    pkgs: typing.Dict[str, typing.List[ast.Module]] = {}
    missing_pkgs: typing.List[str] = []

    main_pkg_modules: typing.List[ast.Module] = []
    __kcl_main__: str = "__main__"
    import_names = {}

    for i, filename in enumerate(k_files):
        code: typing.Optional[str] = None
        if i < len(k_code_list):
            code = k_code_list[i]
        # Save main package cache
        m = None
        if modfile.build.enable_pkg_cache:
            m = vfs.LoadMainPkgCache(
                root, filename, option=get_ast_cache_option(set_ast_parent)
            )
        if not m:
            m = ParseFile(
                filename, pkg=__kcl_main__, code=code, mode=ParseMode.ParseComments
            )
            m.relative_filename = m.filename.replace(root, ".", 1)
            preprocess.fix_identifier_prefix(m)
            fix.fix_test_schema_auto_relaxed(m)
            import_list = fix.fix_and_get_module_import_list(
                root, m, is_fix=load_packages
            )
            for import_stmt in import_list:
                import_names[import_stmt.name] = import_stmt.path
            fix.fix_qualified_identifier(m, import_names=import_names)
            if set_ast_parent:
                fix.fix_set_parent_info(m)
        else:
            import_list = m.GetImportList()
            for import_stmt in import_list:
                import_names[import_stmt.name] = import_stmt.path
        # Save main package cache
        if modfile.build.enable_pkg_cache:
            vfs.SaveMainPkgCache(
                root, filename, m, option=get_ast_cache_option(set_ast_parent)
            )
        m.name = __kcl_main__
        main_pkg_modules.append(m)

    pkgs[__kcl_main__] = main_pkg_modules

    for m in main_pkg_modules:
        import_spec_list = fix.fix_and_get_module_import_list(root, m, False)
        if load_packages:
            for import_spec in import_spec_list:
                _loadPackages(
                    modfile,
                    import_spec,
                    pkgs,
                    missing_pkgs,
                    k_files,
                    mode=mode,
                    set_ast_parent=set_ast_parent,
                )

    return ast.Program(root=root, main=__kcl_main__, pkgs=pkgs)


# -----------------------------------------------------------------------------
# END
# -----------------------------------------------------------------------------
