xref: /linux/tools/net/sunrpc/xdrgen/xdr_parse.py (revision 4c53b89032f14577e94d747a3ca0aee63f18d856)
1#!/usr/bin/env python3
2# ex: set filetype=python:
3
4"""Common parsing code for xdrgen"""
5
6import sys
7from typing import Callable
8
9from lark import Lark
10from lark.exceptions import UnexpectedInput, UnexpectedToken, VisitError
11
12
13# Set to True to emit annotation comments in generated source
14annotate = False
15
16# Map internal Lark token names to human-readable names
17TOKEN_NAMES = {
18    "__ANON_0": "identifier",
19    "__ANON_1": "number",
20    "SEMICOLON": "';'",
21    "LBRACE": "'{'",
22    "RBRACE": "'}'",
23    "LPAR": "'('",
24    "RPAR": "')'",
25    "LSQB": "'['",
26    "RSQB": "']'",
27    "LESSTHAN": "'<'",
28    "MORETHAN": "'>'",
29    "EQUAL": "'='",
30    "COLON": "':'",
31    "COMMA": "','",
32    "STAR": "'*'",
33    "$END": "end of file",
34}
35
36
37class XdrParseError(Exception):
38    """Raised when XDR parsing fails"""
39
40
41def set_xdr_annotate(set_it: bool) -> None:
42    """Set 'annotate' if --annotate was specified on the command line"""
43    global annotate
44    annotate = set_it
45
46
47def get_xdr_annotate() -> bool:
48    """Return True if --annotate was specified on the command line"""
49    return annotate
50
51
52def make_error_handler(source: str, filename: str) -> Callable[[UnexpectedInput], bool]:
53    """Create an error handler that reports the first parse error and aborts.
54
55    Args:
56        source: The XDR source text being parsed
57        filename: The name of the file being parsed
58
59    Returns:
60        An error handler function for use with Lark's on_error parameter
61    """
62    lines = source.splitlines()
63
64    def handle_parse_error(e: UnexpectedInput) -> bool:
65        """Report a parse error with context and abort parsing"""
66        line_num = e.line
67        column = e.column
68        line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
69
70        # Build the error message
71        msg_parts = [f"{filename}:{line_num}:{column}: parse error"]
72
73        # Show what was found vs what was expected
74        if isinstance(e, UnexpectedToken):
75            token = e.token
76            if token.type == "__ANON_0":
77                found = f"identifier '{token.value}'"
78            elif token.type == "__ANON_1":
79                found = f"number '{token.value}'"
80            else:
81                found = f"'{token.value}'"
82            msg_parts.append(f"Unexpected {found}")
83
84            # Provide helpful expected tokens list
85            expected = e.expected
86            if expected:
87                readable = [
88                    TOKEN_NAMES.get(exp, exp.lower().replace("_", " "))
89                    for exp in sorted(expected)
90                ]
91                if len(readable) == 1:
92                    msg_parts.append(f"Expected {readable[0]}")
93                elif len(readable) <= 4:
94                    msg_parts.append(f"Expected one of: {', '.join(readable)}")
95        else:
96            msg_parts.append(str(e).split("\n")[0])
97
98        # Show the offending line with a caret pointing to the error
99        msg_parts.append("")
100        msg_parts.append(f"    {line_text}")
101        prefix = line_text[: column - 1].expandtabs()
102        msg_parts.append(f"    {' ' * len(prefix)}^")
103
104        sys.stderr.write("\n".join(msg_parts) + "\n")
105        raise XdrParseError()
106
107    return handle_parse_error
108
109
110def handle_transform_error(e: VisitError, source: str, filename: str) -> None:
111    """Report a transform error with context.
112
113    Args:
114        e: The VisitError from Lark's transformer
115        source: The XDR source text being parsed
116        filename: The name of the file being parsed
117    """
118    lines = source.splitlines()
119
120    # Extract position from the tree node if available
121    line_num = 0
122    column = 0
123    if hasattr(e.obj, "meta") and e.obj.meta:
124        line_num = e.obj.meta.line
125        column = e.obj.meta.column
126
127    line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
128
129    # Build the error message
130    msg_parts = [f"{filename}:{line_num}:{column}: semantic error"]
131
132    # The original exception is typically a KeyError for undefined types
133    if isinstance(e.orig_exc, KeyError):
134        msg_parts.append(f"Undefined type '{e.orig_exc.args[0]}'")
135    else:
136        msg_parts.append(str(e.orig_exc))
137
138    # Show the offending line with a caret pointing to the error
139    if line_text:
140        msg_parts.append("")
141        msg_parts.append(f"    {line_text}")
142        prefix = line_text[: column - 1].expandtabs()
143        msg_parts.append(f"    {' ' * len(prefix)}^")
144
145    sys.stderr.write("\n".join(msg_parts) + "\n")
146
147
148def xdr_parser() -> Lark:
149    """Return a Lark parser instance configured with the XDR language grammar"""
150
151    return Lark.open(
152        "grammars/xdr.lark",
153        rel_to=__file__,
154        start="specification",
155        debug=True,
156        strict=True,
157        propagate_positions=True,
158        parser="lalr",
159        lexer="contextual",
160    )
161