| #!/usr/bin/env python3 |
| # ex: set filetype=python: |
| |
| """Common parsing code for xdrgen""" |
| |
| import sys |
| from typing import Callable |
| |
| from lark import Lark |
| from lark.exceptions import UnexpectedInput, UnexpectedToken, VisitError |
| |
| |
| # Set to True to emit annotation comments in generated source |
| annotate = False |
| |
| # Set to True to emit enum value validation in decoders |
| enum_validation = True |
| |
| # Map internal Lark token names to human-readable names |
| TOKEN_NAMES = { |
| "__ANON_0": "identifier", |
| "__ANON_1": "number", |
| "SEMICOLON": "';'", |
| "LBRACE": "'{'", |
| "RBRACE": "'}'", |
| "LPAR": "'('", |
| "RPAR": "')'", |
| "LSQB": "'['", |
| "RSQB": "']'", |
| "LESSTHAN": "'<'", |
| "MORETHAN": "'>'", |
| "EQUAL": "'='", |
| "COLON": "':'", |
| "COMMA": "','", |
| "STAR": "'*'", |
| "$END": "end of file", |
| } |
| |
| |
| class XdrParseError(Exception): |
| """Raised when XDR parsing fails""" |
| |
| |
| def set_xdr_annotate(set_it: bool) -> None: |
| """Set 'annotate' if --annotate was specified on the command line""" |
| global annotate |
| annotate = set_it |
| |
| |
| def get_xdr_annotate() -> bool: |
| """Return True if --annotate was specified on the command line""" |
| return annotate |
| |
| |
| def set_xdr_enum_validation(set_it: bool) -> None: |
| """Set 'enum_validation' based on command line options""" |
| global enum_validation |
| enum_validation = set_it |
| |
| |
| def get_xdr_enum_validation() -> bool: |
| """Return True when enum validation is enabled for decoder generation""" |
| return enum_validation |
| |
| |
| def make_error_handler(source: str, filename: str) -> Callable[[UnexpectedInput], bool]: |
| """Create an error handler that reports the first parse error and aborts. |
| |
| Args: |
| source: The XDR source text being parsed |
| filename: The name of the file being parsed |
| |
| Returns: |
| An error handler function for use with Lark's on_error parameter |
| """ |
| lines = source.splitlines() |
| |
| def handle_parse_error(e: UnexpectedInput) -> bool: |
| """Report a parse error with context and abort parsing""" |
| line_num = e.line |
| column = e.column |
| line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else "" |
| |
| # Build the error message |
| msg_parts = [f"{filename}:{line_num}:{column}: parse error"] |
| |
| # Show what was found vs what was expected |
| if isinstance(e, UnexpectedToken): |
| token = e.token |
| if token.type == "__ANON_0": |
| found = f"identifier '{token.value}'" |
| elif token.type == "__ANON_1": |
| found = f"number '{token.value}'" |
| else: |
| found = f"'{token.value}'" |
| msg_parts.append(f"Unexpected {found}") |
| |
| # Provide helpful expected tokens list |
| expected = e.expected |
| if expected: |
| readable = [ |
| TOKEN_NAMES.get(exp, exp.lower().replace("_", " ")) |
| for exp in sorted(expected) |
| ] |
| if len(readable) == 1: |
| msg_parts.append(f"Expected {readable[0]}") |
| elif len(readable) <= 4: |
| msg_parts.append(f"Expected one of: {', '.join(readable)}") |
| else: |
| msg_parts.append(str(e).split("\n")[0]) |
| |
| # Show the offending line with a caret pointing to the error |
| msg_parts.append("") |
| msg_parts.append(f" {line_text}") |
| prefix = line_text[: column - 1].expandtabs() |
| msg_parts.append(f" {' ' * len(prefix)}^") |
| |
| sys.stderr.write("\n".join(msg_parts) + "\n") |
| raise XdrParseError() |
| |
| return handle_parse_error |
| |
| |
| def handle_transform_error(e: VisitError, source: str, filename: str) -> None: |
| """Report a transform error with context. |
| |
| Args: |
| e: The VisitError from Lark's transformer |
| source: The XDR source text being parsed |
| filename: The name of the file being parsed |
| """ |
| lines = source.splitlines() |
| |
| # Extract position from the tree node if available |
| line_num = 0 |
| column = 0 |
| if hasattr(e.obj, "meta") and e.obj.meta: |
| line_num = e.obj.meta.line |
| column = e.obj.meta.column |
| |
| line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else "" |
| |
| # Build the error message |
| msg_parts = [f"{filename}:{line_num}:{column}: semantic error"] |
| |
| # The original exception is typically a KeyError for undefined types |
| if isinstance(e.orig_exc, KeyError): |
| msg_parts.append(f"Undefined type '{e.orig_exc.args[0]}'") |
| else: |
| msg_parts.append(str(e.orig_exc)) |
| |
| # Show the offending line with a caret pointing to the error |
| if line_text: |
| msg_parts.append("") |
| msg_parts.append(f" {line_text}") |
| prefix = line_text[: column - 1].expandtabs() |
| msg_parts.append(f" {' ' * len(prefix)}^") |
| |
| sys.stderr.write("\n".join(msg_parts) + "\n") |
| |
| |
| def xdr_parser() -> Lark: |
| """Return a Lark parser instance configured with the XDR language grammar""" |
| |
| return Lark.open( |
| "grammars/xdr.lark", |
| rel_to=__file__, |
| start="specification", |
| debug=True, |
| strict=True, |
| propagate_positions=True, |
| parser="lalr", |
| lexer="contextual", |
| ) |