1#-----------------------------------------------------------------
2# plyparser.py
3#
4# PLYParser class and other utilities 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 object '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)