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