Commit c38397ff authored by Igor Dejanovic's avatar Igor Dejanovic

fix #15 Node visitor implementation.

Implemented node visitor pattern.
Idea suggested by Yash in issue #15.
calc_peg_visitor.py example shows basic usage.
parent 1977f6e3
...@@ -746,6 +746,41 @@ class ParseTreeNode(object): ...@@ -746,6 +746,41 @@ class ParseTreeNode(object):
def name(self): def name(self):
return "%s [%s]" % (self.rule_name, self.position) return "%s [%s]" % (self.rule_name, self.position)
def visit(self, visitor):
"""
Visitor pattern implementation.
Args:
visitor(PTNodeVisitor): The visitor object.
"""
if visitor.debug:
print("Visiting ", self.name, " type:",
type(self).__name__, "str:", text(self))
children = SemanticActionResults()
if isinstance(self, NonTerminal):
for node in self:
child = node.visit(visitor)
# If visit returns None suppress that child node
if child is not None:
children.append(child)
visit_name = "visit_%s" % self.rule_name
if hasattr(visitor, visit_name):
# Call visit method.
result = getattr(visitor, visit_name)(self, children)
# If there is a method with 'second' prefix save
# the result of visit for post-processing
if hasattr(visitor, "second_%s" % self.rule_name):
visitor.for_second_pass.append((self.rule_name, result))
return result
elif visitor.defaults:
# If default actions are enabled
return visitor.visit__default__(self, children)
class Terminal(ParseTreeNode): class Terminal(ParseTreeNode):
""" """
...@@ -869,6 +904,87 @@ class NonTerminal(ParseTreeNode, list): ...@@ -869,6 +904,87 @@ class NonTerminal(ParseTreeNode, list):
# ---------------------------------------------------- # ----------------------------------------------------
# Semantic Actions # Semantic Actions
# #
class PTNodeVisitor(object):
"""
Base class for all parse tree visitors.
"""
def __init__(self, defaults=True, debug=False):
"""
Args:
defaults(bool): If the default visit method should be applied in
case no method is defined.
debug(bool): Print debug messages?
"""
self.for_second_pass = []
self.debug = debug
self.defaults = defaults
def visit__default__(self, node, children):
"""
Called if no visit method is defined for the node.
Args:
node(ParseTreeNode):
children(processed children ParseTreeNode-s):
"""
if isinstance(node, Terminal):
# Default for Terminal is to convert to string unless suppress flag
# is set in which case it is suppressed by setting to None.
retval = text(node) if not node.suppress else None
else:
retval = node
# Special case. If only one child exist return it.
if len(children) == 1:
retval = children[0]
else:
# If there is only one non-string child return
# that by default. This will support e.g. bracket
# removals.
last_non_str = None
for c in children:
if not isstr(c):
if last_non_str is None:
last_non_str = c
else:
# If there is multiple non-string objects
# by default convert non-terminal to string
if self.debug:
print("*** Warning: Multiple non-string objects found in default visit. Converting non-terminal to a string.")
retval = text(node)
break
else:
# Return the only non-string child
retval = last_non_str
return retval
def visit_parse_tree(parse_tree, visitor):
"""
Applies visitor to parse_tree and runs the second pass
afterwards.
Args:
parse_tree(ParseTreeNode):
visitor(PTNodeVisitor):
"""
if not parse_tree:
raise Exception(
"Parse tree is empty. You did call parse(), didn't you?")
if visitor.debug:
print("ASG: First pass")
# Visit tree.
result = parse_tree.visit(visitor)
# Second pass
if visitor.debug:
print("ASG: Second pass")
for sa_name, asg_node in visitor.for_second_pass:
getattr(visitor, "second_%s" % sa_name)(asg_node)
return result
class SemanticAction(object): class SemanticAction(object):
""" """
......
#######################################################################
# Name: calc_peg.py
# Purpose: Simple expression evaluator example using PEG language and
# visitor pattern for semantic analysis.
# Author: Igor R. Dejanovic <igor DOT dejanovic AT gmail DOT com>
# Copyright: (c) 2009-2014 Igor R. Dejanovic <igor DOT dejanovic AT gmail DOT com>
# License: MIT License
#
# This example is functionally equivalent to calc_peg.py.
# It is a demonstration of visitor pattern approach for semantic analysis.
# Parser model as well as parse tree exported to dot files should be
# the same as parser model and parse tree generated in calc.py example.
#######################################################################
from __future__ import absolute_import, unicode_literals, print_function
try:
text = unicode
except:
text = str
from arpeggio.cleanpeg import ParserPEG
from arpeggio import PTNodeVisitor, visit_parse_tree
# Grammar is defined using textual specification based on PEG language.
calc_grammar = """
number = r'\d*\.\d*|\d+'
factor = ("+" / "-")?
(number / "(" expression ")")
term = factor (( "*" / "/") factor)*
expression = term (("+" / "-") term)*
calc = expression+ EOF
"""
class CalcVisitor(PTNodeVisitor):
def visit_number(self, node, children):
"""
Converts node value to float.
"""
if self.debug:
print("Converting {}.".format(node.value))
return float(node.value)
def visit_factor(self, node, children):
"""
Removes parenthesis if exists and returns what was contained inside.
"""
if self.debug:
print("Factor {}".format(children))
if len(children) == 1:
return children[0]
sign = -1 if children[0] == '-' else 1
return sign * children[-1]
def visit_term(self, node, children):
"""
Divides or multiplies factors.
Factor nodes will be already evaluated.
"""
if self.debug:
print("Term {}".format(children))
term = children[0]
for i in range(2, len(children), 2):
if children[i-1] == "*":
term *= children[i]
else:
term /= children[i]
if self.debug:
print("Term = {}".format(term))
return term
def visit_expression(self, node, children):
"""
Adds or substracts terms.
Term nodes will be already evaluated.
"""
if self.debug:
print("Expression {}".format(children))
expr = 0
start = 0
# Check for unary + or - operator
if text(children[0]) in "+-":
start = 1
for i in range(start, len(children), 2):
if i and children[i - 1] == "-":
expr -= children[i]
else:
expr += children[i]
if self.debug:
print("Expression = {}".format(expr))
return expr
def main(debug=False):
# First we will make a parser - an instance of the calc parser model.
# Parser model is given in the form of PEG notation therefore we
# are using ParserPEG class. Root rule name (parsing expression) is "calc".
parser = ParserPEG(calc_grammar, "calc", debug=debug)
# An expression we want to evaluate
input_expr = "-(4-1)*5+(2+4.67)+5.89/(.2+7)"
# Then parse tree is created out of the input_expr expression.
parse_tree = parser.parse(input_expr)
result = visit_parse_tree(parse_tree, CalcVisitor(debug=debug))
# visit_parse_tree will start semantic analysis.
# In this case semantic analysis will evaluate expression and
# returned value will be evaluated result of the input_expr expression.
print("{} = {}".format(input_expr, result))
if __name__ == "__main__":
# In debug mode dot (graphviz) files for parser model
# and parse tree will be created for visualization.
# Checkout current folder for .dot files.
main(debug=False)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment