xref: /linux/tools/net/sunrpc/xdrgen/xdr_parse.py (revision ca220141fa8ebae09765a242076b2b77338106b0)
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 make_error_handler(source: str, filename: str) -> Callable[[UnexpectedInput], bool]:
67    """Create an error handler that reports the first parse error and aborts.
68
69    Args:
70        source: The XDR source text being parsed
71        filename: The name of the file being parsed
72
73    Returns:
74        An error handler function for use with Lark's on_error parameter
75    """
76    lines = source.splitlines()
77
78    def handle_parse_error(e: UnexpectedInput) -> bool:
79        """Report a parse error with context and abort parsing"""
80        line_num = e.line
81        column = e.column
82        line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
83
84        # Build the error message
85        msg_parts = [f"{filename}:{line_num}:{column}: parse error"]
86
87        # Show what was found vs what was expected
88        if isinstance(e, UnexpectedToken):
89            token = e.token
90            if token.type == "__ANON_0":
91                found = f"identifier '{token.value}'"
92            elif token.type == "__ANON_1":
93                found = f"number '{token.value}'"
94            else:
95                found = f"'{token.value}'"
96            msg_parts.append(f"Unexpected {found}")
97
98            # Provide helpful expected tokens list
99            expected = e.expected
100            if expected:
101                readable = [
102                    TOKEN_NAMES.get(exp, exp.lower().replace("_", " "))
103                    for exp in sorted(expected)
104                ]
105                if len(readable) == 1:
106                    msg_parts.append(f"Expected {readable[0]}")
107                elif len(readable) <= 4:
108                    msg_parts.append(f"Expected one of: {', '.join(readable)}")
109        else:
110            msg_parts.append(str(e).split("\n")[0])
111
112        # Show the offending line with a caret pointing to the error
113        msg_parts.append("")
114        msg_parts.append(f"    {line_text}")
115        prefix = line_text[: column - 1].expandtabs()
116        msg_parts.append(f"    {' ' * len(prefix)}^")
117
118        sys.stderr.write("\n".join(msg_parts) + "\n")
119        raise XdrParseError()
120
121    return handle_parse_error
122
123
124def handle_transform_error(e: VisitError, source: str, filename: str) -> None:
125    """Report a transform error with context.
126
127    Args:
128        e: The VisitError from Lark's transformer
129        source: The XDR source text being parsed
130        filename: The name of the file being parsed
131    """
132    lines = source.splitlines()
133
134    # Extract position from the tree node if available
135    line_num = 0
136    column = 0
137    if hasattr(e.obj, "meta") and e.obj.meta:
138        line_num = e.obj.meta.line
139        column = e.obj.meta.column
140
141    line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else ""
142
143    # Build the error message
144    msg_parts = [f"{filename}:{line_num}:{column}: semantic error"]
145
146    # The original exception is typically a KeyError for undefined types
147    if isinstance(e.orig_exc, KeyError):
148        msg_parts.append(f"Undefined type '{e.orig_exc.args[0]}'")
149    else:
150        msg_parts.append(str(e.orig_exc))
151
152    # Show the offending line with a caret pointing to the error
153    if line_text:
154        msg_parts.append("")
155        msg_parts.append(f"    {line_text}")
156        prefix = line_text[: column - 1].expandtabs()
157        msg_parts.append(f"    {' ' * len(prefix)}^")
158
159    sys.stderr.write("\n".join(msg_parts) + "\n")
160
161
162def xdr_parser() -> Lark:
163    """Return a Lark parser instance configured with the XDR language grammar"""
164
165    return Lark.open(
166        "grammars/xdr.lark",
167        rel_to=__file__,
168        start="specification",
169        debug=True,
170        strict=True,
171        propagate_positions=True,
172        parser="lalr",
173        lexer="contextual",
174    )
175