at master 174 lines 5.2 kB view raw
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 )