| #!/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 format_source_caret(line_text: str, column: int) -> list[str]: | |
| """Render an offending source line with a caret beneath a column. | |
| Args: | |
| line_text: The raw source line containing the error | |
| column: 1-based column of the offending token within line_text | |
| Returns: | |
| Output lines for the diagnostic: a blank separator, the source | |
| line with tabs expanded, and a caret aligned under the column. | |
| """ | |
| expanded = line_text.expandtabs() | |
| caret = len(line_text[: column - 1].expandtabs()) | |
| return ["", f" {expanded}", f" {' ' * caret}^"] | |
| 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.extend(format_source_caret(line_text, column)) | |
| 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.extend(format_source_caret(line_text, column)) | |
| sys.stderr.write("\n".join(msg_parts) + "\n") | |
| def handle_semantic_error(e, source: str, filename: str) -> None: | |
| """Report a semantic error (e.g., a duplicate name) with context. | |
| Args: | |
| e: The XdrSemanticError carrying message and source position | |
| source: The XDR source text being parsed | |
| filename: The name of the file being parsed | |
| """ | |
| lines = source.splitlines() | |
| line_num = getattr(e, "line", 0) | |
| column = getattr(e, "column", 0) | |
| line_text = lines[line_num - 1] if 0 < line_num <= len(lines) else "" | |
| msg_parts = [f"{filename}:{line_num}:{column}: semantic error", e.message] | |
| if line_text: | |
| msg_parts.extend(format_source_caret(line_text, column)) | |
| 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", | |
| ) |