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