"""
This module contains the implementation of a PLY-based parser utility
designed to aid in constructing and using parsers. It includes classes for
representing coordinates of syntax elements (`Coord`), custom exception handling
(`ParseError`), and a `PLYParser` class that provides helper methods for rule
creation, error handling, and token coordinate computation.
Additionally, the module defines utilities for dynamic rule generation, including
parameterized rule decorators (`parameterized`) and a class decorator (`template`)
to facilitate the creation of PLY rules from templates.
Based on the plyparser.py from the pycparser module by Eli Bendersky (https://eli.thegreenplace.net)
under the BSD license.
Key Components:
* `Coord`: Represents the file, line, and optionally column for syntax elements.
* `PLYParser`: Provides methods to handle optional rules, error reporting, and coordinate management for tokens during parsing.
* `parameterized`: Decorator for creating parameterized parsing rules.
* `template`: Class decorator for generating rules from parameterized templates.
This module can be used as part of a larger system to build a parser using
the PLY (Python Lex-Yacc) library.
"""
import warnings
[docs]
class Coord:
"""
Coordinates of a syntactic element. Consists of:
:ivar file: Name of the file.
:type file: str
:ivar line: The line number of the coordinate in the input string
:type line: int
:ivar column: The column number of the coordinate in the input string
:type column: int
"""
__slots__ = ("file", "line", "column", "__weakref__")
def __init__(self, file, line, column = None):
self.file = file
self.line = line
self.column = column
def __str__(self):
return f"{self.file}:{self.line}{f":{self.column}" if self.column else ""}"
[docs]
class ParseError(Exception):
"""
Thrown if yacc has an internal problem or by :class:`PLYParser` if a token cannot be parsed.
"""
[docs]
class PLYParser:
"""
Facilitates parsing tasks with helper methods and error handling specifically designed
to work with the PLY parsing library. This class aids in managing parsing rules,
tracking coordinates during parsing, and generating useful error messages for
syntax issues.
:ivar dlex: Lexer instance providing input and tokens for parsing.
:type dlex: Lexer
"""
__slots__ = ("dlex",)
def _create_opt_rule(self, rulename):
"""
Given a rule name, creates an optional ply.yacc rule
for it. The name of the optional rule is <rulename>_opt
"""
optname = f"{rulename}_opt"
# do NOT delete "self" attribute!
def optrule(self, p):
"""
Empty framework for a new rule to be added to yacc.
"""
p[0] = p[1]
optrule.__doc__ = f"{optname} : empty\n| {rulename}"
optrule.__name__ = f"p_{optname}"
setattr(self.__class__, optrule.__name__, optrule)
def _coord(self, lineno, column = None):
return Coord(
file = self.dlex.filename,
line = lineno,
column = column
)
def _token_coord(self, p, token_idx):
"""
Returns the coordinates for the YaccProduction object "p" indexed
with "token_idx". The coordinate includes the "lineno" and "column".
Both follow the lex semantic, starting from 1.
"""
last_cr = p.lexer.lexer.lexdata.rfind("\n", 0, p.lexpos(token_idx))
if last_cr < 0:
last_cr = -1
column = p.lexpos(token_idx) - last_cr
return self._coord(p.lineno(token_idx), column)
def _parse_error(self, msg, coord):
raise ParseError(f"Error: {coord}: {msg}.")
##
## Help methods
##
[docs]
def parameterized(*params):
"""
Decorator to create parameterized rules.
"""
# Parameterized rule methods must be named starting with "p_" and contain
# "xxx", and their docstrings may contain "xxx" and "yyy". These will be
# replaced by the given parameter tuples. For example, "p_xxx_rule()" with
# docstring "xxx_rule : yyy" when decorated with
# "@parameterized(("id", "ID"))" produces "p_id_rule()" with the docstring
# "id_rule : ID". Using multiple tuples produces multiple rules.
def decorate(rule_func):
"""
Decorates a rule with parameters.
"""
rule_func._params = params
return rule_func
return decorate
[docs]
def template(cls):
"""
Class decorator to generate rules from parameterized rule templates.
See `parameterized` for more information on parameterized rules.
"""
issued_nodoc_warning = False
for attr_name in dir(cls):
if attr_name.startswith("p_"):
method = getattr(cls, attr_name)
if hasattr(method, "_params"):
# Remove the template method
delattr(cls, attr_name)
# Create parameterized rules from this method; only run this if
# the method has a docstring.
if method.__doc__ is not None:
_create_param_rules(cls, method)
elif not issued_nodoc_warning:
warnings.warn(
"parsing methods must have __doc__ for pycparser to work properly",
RuntimeWarning,
stacklevel = 2)
issued_nodoc_warning = True
return cls
def _create_param_rules(cls, func):
"""
Create ply.yacc rules based on a parameterized rule function
Generates new methods (one per each pair of parameters) based on the
template rule function "func", and attaches them to "cls". The rule
function's parameters must be accessible via its "_params" attribute.
"""
for xxx, yyy in func._params:
# Use the template method's body for each new method
def param_rule(self, p):
"""
Empty framework for a new rule to be added to yacc.
"""
func(self, p)
# Substitute in the params for the grammar rule and function name
param_rule.__doc__ = func.__doc__.replace("xxx", xxx).replace("yyy", yyy)
param_rule.__name__ = func.__name__.replace("xxx", xxx)
# Attach the new method to the class
setattr(cls, param_rule.__name__, param_rule)