1#----------------------------------------------------------------- 2# plyparser.py 3# 4# PLYParser class and other utilites for simplifying programming 5# parsers with PLY 6# 7# Eli Bendersky [https://eli.thegreenplace.net/] 8# License: BSD 9#----------------------------------------------------------------- 10 11import warnings 12 13class Coord(object): 14 """ Coordinates of a syntactic element. Consists of: 15 - File name 16 - Line number 17 - (optional) column number, for the Lexer 18 """ 19 __slots__ = ('file', 'line', 'column', '__weakref__') 20 def __init__(self, file, line, column=None): 21 self.file = file 22 self.line = line 23 self.column = column 24 25 def __str__(self): 26 str = "%s:%s" % (self.file, self.line) 27 if self.column: str += ":%s" % self.column 28 return str 29 30 31class ParseError(Exception): pass 32 33 34class PLYParser(object): 35 def _create_opt_rule(self, rulename): 36 """ Given a rule name, creates an optional ply.yacc rule 37 for it. The name of the optional rule is 38 <rulename>_opt 39 """ 40 optname = rulename + '_opt' 41 42 def optrule(self, p): 43 p[0] = p[1] 44 45 optrule.__doc__ = '%s : empty\n| %s' % (optname, rulename) 46 optrule.__name__ = 'p_%s' % optname 47 setattr(self.__class__, optrule.__name__, optrule) 48 49 def _coord(self, lineno, column=None): 50 return Coord( 51 file=self.clex.filename, 52 line=lineno, 53 column=column) 54 55 def _token_coord(self, p, token_idx): 56 """ Returns the coordinates for the YaccProduction objet 'p' indexed 57 with 'token_idx'. The coordinate includes the 'lineno' and 58 'column'. Both follow the lex semantic, starting from 1. 59 """ 60 last_cr = p.lexer.lexer.lexdata.rfind('\n', 0, p.lexpos(token_idx)) 61 if last_cr < 0: 62 last_cr = -1 63 column = (p.lexpos(token_idx) - (last_cr)) 64 return self._coord(p.lineno(token_idx), column) 65 66 def _parse_error(self, msg, coord): 67 raise ParseError("%s: %s" % (coord, msg)) 68 69 70def parameterized(*params): 71 """ Decorator to create parameterized rules. 72 73 Parameterized rule methods must be named starting with 'p_' and contain 74 'xxx', and their docstrings may contain 'xxx' and 'yyy'. These will be 75 replaced by the given parameter tuples. For example, ``p_xxx_rule()`` with 76 docstring 'xxx_rule : yyy' when decorated with 77 ``@parameterized(('id', 'ID'))`` produces ``p_id_rule()`` with the docstring 78 'id_rule : ID'. Using multiple tuples produces multiple rules. 79 """ 80 def decorate(rule_func): 81 rule_func._params = params 82 return rule_func 83 return decorate 84 85 86def template(cls): 87 """ Class decorator to generate rules from parameterized rule templates. 88 89 See `parameterized` for more information on parameterized rules. 90 """ 91 issued_nodoc_warning = False 92 for attr_name in dir(cls): 93 if attr_name.startswith('p_'): 94 method = getattr(cls, attr_name) 95 if hasattr(method, '_params'): 96 # Remove the template method 97 delattr(cls, attr_name) 98 # Create parameterized rules from this method; only run this if 99 # the method has a docstring. This is to address an issue when 100 # pycparser's users are installed in -OO mode which strips 101 # docstrings away. 102 # See: https://github.com/eliben/pycparser/pull/198/ and 103 # https://github.com/eliben/pycparser/issues/197 104 # for discussion. 105 if method.__doc__ is not None: 106 _create_param_rules(cls, method) 107 elif not issued_nodoc_warning: 108 warnings.warn( 109 'parsing methods must have __doc__ for pycparser to work properly', 110 RuntimeWarning, 111 stacklevel=2) 112 issued_nodoc_warning = True 113 return cls 114 115 116def _create_param_rules(cls, func): 117 """ Create ply.yacc rules based on a parameterized rule function 118 119 Generates new methods (one per each pair of parameters) based on the 120 template rule function `func`, and attaches them to `cls`. The rule 121 function's parameters must be accessible via its `_params` attribute. 122 """ 123 for xxx, yyy in func._params: 124 # Use the template method's body for each new method 125 def param_rule(self, p): 126 func(self, p) 127 128 # Substitute in the params for the grammar rule and function name 129 param_rule.__doc__ = func.__doc__.replace('xxx', xxx).replace('yyy', yyy) 130 param_rule.__name__ = func.__name__.replace('xxx', xxx) 131 132 # Attach the new method to the class 133 setattr(cls, param_rule.__name__, param_rule) 134