xref: /linux/tools/net/sunrpc/xdrgen/xdr_parse.py (revision d141ec2825b4d3ec52f27c43bdd864090159273a)
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# Set to True to emit enum value validation in decoders
17enum_validation = True
18
19# Map internal Lark token names to human-readable names
20TOKEN_NAMES = {
21    "__ANON_0": "identifier",
22    "__ANON_1": "number",
23    "SEMICOLON": "';'",
24    "LBRACE": "'{'",
25    "RBRACE": "'}'",
26    "LPAR": "'('",
27    "RPAR": "')'",
28    "LSQB": "'['",
29    "RSQB": "']'",
30    "LESSTHAN": "'<'",
31    "MORETHAN": "'>'",
32    "EQUAL": "'='",
33    "COLON": "':'",
34    "COMMA": "','",
35    "STAR": "'*'",
36    "$END": "end of file",
37}
38
39
40class XdrParseError(Exception):
41    """Raised when XDR parsing fails"""
42
43
44def set_xdr_annotate(set_it: bool) -> None:
45    """Set 'annotate' if --annotate was specified on the command line"""
46    global annotate
47    annotate = set_it
48
49
50def get_xdr_annotate() -> bool:
51    """Return True if --annotate was specified on the command line"""
52    return annotate
53
54
55def set_xdr_enum_validation(set_it: bool) -> None:
56    """Set 'enum_validation' based on command line options"""
57    global enum_validation
58    enum_validation = set_it
59
60
61def get_xdr_enum_validation() -> bool:
62    """Return True when enum validation is enabled for decoder generation"""
63    return enum_validation
64
65
66def format_source_caret(line_text: str, column: int) -> list[str]:
67    """Render an offending source line with a caret beneath a column.
68
69    Args:
70        line_text: The raw source line containing the error
71        column: 1-based column of the offending token within line_text
72
73    Returns:
74        Output lines for the diagnostic: a blank separator, the source
75        line with tabs expanded, and a caret aligned under the column.
76    """
77    expanded = line_text.expandtabs()
78    caret = len(line_text[: column - 1].expandtabs())
79    return ["", f"    {expanded}", f"    {' ' * caret}^"]
80
81
82def make_error_handler(source: str, filename: str) -> Callable[[UnexpectedInput], bool]:
83    """Create an error handler that reports the first parse error and aborts.
84
85    Args:
86        source: The XDR source text being parsed
87        filename: The name of the file being parsed
88
89    Returns:
90        An error handler function for use with Lark's on_error parameter
91    """
92    lines = source.splitlines()
93
94    def handle_parse_error(e: UnexpectedInput) -> bool:
95        """Report a parse error with context and abort parsing"""
96        line_num = e.line
97        column = e.column
98        line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
99
100        # Build the error message
101        msg_parts = [f"{filename}:{line_num}:{column}: parse error"]
102
103        # Show what was found vs what was expected
104        if isinstance(e, UnexpectedToken):
105            token = e.token
106            if token.type == "__ANON_0":
107                found = f"identifier '{token.value}'"
108            elif token.type == "__ANON_1":
109                found = f"number '{token.value}'"
110            else:
111                found = f"'{token.value}'"
112            msg_parts.append(f"Unexpected {found}")
113
114            # Provide helpful expected tokens list
115            expected = e.expected
116            if expected:
117                readable = [
118                    TOKEN_NAMES.get(exp, exp.lower().replace("_", " "))
119                    for exp in sorted(expected)
120                ]
121                if len(readable) == 1:
122                    msg_parts.append(f"Expected {readable[0]}")
123                elif len(readable) <= 4:
124                    msg_parts.append(f"Expected one of: {', '.join(readable)}")
125        else:
126            msg_parts.append(str(e).split("\n")[0])
127
128        # Show the offending line with a caret pointing to the error
129        msg_parts.extend(format_source_caret(line_text, column))
130
131        sys.stderr.write("\n".join(msg_parts) + "\n")
132        raise XdrParseError()
133
134    return handle_parse_error
135
136
137def handle_transform_error(e: VisitError, source: str, filename: str) -> None:
138    """Report a transform error with context.
139
140    Args:
141        e: The VisitError from Lark's transformer
142        source: The XDR source text being parsed
143        filename: The name of the file being parsed
144    """
145    lines = source.splitlines()
146
147    # Extract position from the tree node if available
148    line_num = 0
149    column = 0
150    if hasattr(e.obj, "meta") and e.obj.meta:
151        line_num = e.obj.meta.line
152        column = e.obj.meta.column
153
154    line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
155
156    # Build the error message
157    msg_parts = [f"{filename}:{line_num}:{column}: semantic error"]
158
159    # The original exception is typically a KeyError for undefined types
160    if isinstance(e.orig_exc, KeyError):
161        msg_parts.append(f"Undefined type '{e.orig_exc.args[0]}'")
162    else:
163        msg_parts.append(str(e.orig_exc))
164
165    # Show the offending line with a caret pointing to the error
166    if line_text:
167        msg_parts.extend(format_source_caret(line_text, column))
168
169    sys.stderr.write("\n".join(msg_parts) + "\n")
170
171
172def handle_semantic_error(e, source: str, filename: str) -> None:
173    """Report a semantic error (e.g., a duplicate name) with context.
174
175    Args:
176        e: The XdrSemanticError carrying message and source position
177        source: The XDR source text being parsed
178        filename: The name of the file being parsed
179    """
180    lines = source.splitlines()
181    line_num = getattr(e, "line", 0)
182    column = getattr(e, "column", 0)
183    line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
184
185    msg_parts = [f"{filename}:{line_num}:{column}: semantic error", e.message]
186    if line_text:
187        msg_parts.extend(format_source_caret(line_text, column))
188
189    sys.stderr.write("\n".join(msg_parts) + "\n")
190
191
192def xdr_parser() -> Lark:
193    """Return a Lark parser instance configured with the XDR language grammar"""
194
195    return Lark.open(
196        "grammars/xdr.lark",
197        rel_to=__file__,
198        start="specification",
199        debug=True,
200        strict=True,
201        propagate_positions=True,
202        parser="lalr",
203        lexer="contextual",
204    )
205