Commit d86dda92 authored by Taddeus Kroes's avatar Taddeus Kroes

Finished separating parser state and interactive state, and added first version of strategy.

- Strategy is now implemented using HIGH, LOW and RELATIVE lists.
- node 'clone' function has been modified to fit needs of suggestion
  application.
parent fc747674
......@@ -126,16 +126,24 @@ TOKEN_MAP = {
def to_expression(obj):
return obj if isinstance(obj, ExpressionBase) else ExpressionLeaf(obj)
return obj.clone() if isinstance(obj, ExpressionBase) else ExpressionLeaf(obj)
class ExpressionBase(object):
hash_counter = 1
def __init__(self, *args, **kwargs):
self.negated = 0
def clone(self):
return copy.deepcopy(self)
# Create a unique hash value
self.hash_value = self.__class__.hash_counter
self.__class__.hash_counter += 1
def __hash__(self):
return self.hash_value
def __cmp__(self):
return hash(other) - hash(self)
def __lt__(self, other):
"""
......@@ -355,6 +363,18 @@ class ExpressionNode(Node, ExpressionBase):
return isinstance(other, ExpressionNode) and self.op == other.op \
and self.negated == other.negated and self.nodes == other.nodes
def clone(self):
"""
Create a clone of the current node. Copy the hash value for comparison
in Scope operations.
"""
children = [child.clone() for child in self]
clone = ExpressionNode(self.op, *children)
clone.negated = self.negated
#clone.hash_value = self.hash_value
return clone
def substitute(self, old_child, new_child):
self.nodes[self.nodes.index(old_child)] = new_child
......@@ -494,6 +514,17 @@ class ExpressionLeaf(Leaf, ExpressionBase):
def __repr__(self):
return str(self)
def clone(self):
"""
Create a clone of the current leaf. Copy the hash value for comparison
in Scope operations.
"""
clone = ExpressionLeaf(self.value)
clone.negated = self.negated
#clone.hash_value = self.hash_value
return clone
def equals(self, other, ignore_negation=False):
"""
Check non-strict equivalence.
......@@ -555,27 +586,16 @@ class Scope(object):
return '<Scope of "%s">' % repr(self.node)
def remove(self, node, **kwargs):
if node.is_leaf:
node_cmp = hash(node)
else:
node_cmp = node
try:
i = self.nodes.index(node)
for i, n in enumerate(self.nodes):
if n.is_leaf:
n_cmp = hash(n)
if 'replacement' in kwargs:
self[i] = kwargs['replacement']
else:
n_cmp = n
if n_cmp == node_cmp:
if 'replacement' in kwargs:
self[i] = kwargs['replacement']
else:
del self.nodes[i]
return
raise ValueError('Node "%s" is not in the scope of "%s".'
% (node, self.node))
del self.nodes[i]
except ValueError:
raise ValueError('Node "%s" is not in the scope of "%s".'
% (node, self.node))
def replace(self, node, replacement):
self.remove(node, replacement=replacement)
......@@ -614,7 +634,7 @@ def negate(node, n=1):
"""Negate the given node n times."""
assert n >= 0
new_node = node.clone()
new_node = copy.deepcopy(node)
new_node.negated = n
return new_node
......
......@@ -14,15 +14,14 @@ sys.path.insert(1, EXTERNAL_MODS)
from pybison import BisonParser, BisonSyntaxError
from graph_drawing.graph import generate_graph
from node import ExpressionBase, ExpressionNode as Node, \
from node import ExpressionNode as Node, \
ExpressionLeaf as Leaf, OP_MAP, OP_DER, TOKEN_MAP, TYPE_OPERATOR, \
OP_COMMA, OP_NEG, OP_MUL, OP_DIV, OP_POW, OP_LOG, OP_ADD, Scope, E, \
OP_COMMA, OP_MUL, OP_POW, OP_LOG, OP_ADD, Scope, E, OP_ABS, \
DEFAULT_LOGARITHM_BASE, OP_VALUE_MAP, SPECIAL_TOKENS, OP_INT, \
OP_INT_INDEF, OP_ABS, OP_NEG, negation_to_node
from rules import RULES
OP_INT_INDEF, negation_to_node
from rules.utils import find_variable
#from strategy import sort_possiblities
from possibilities import filter_duplicates, apply_suggestion
from strategy import find_possibilities
from possibilities import apply_suggestion
import Queue
import re
......@@ -58,34 +57,6 @@ def find_integration_variable(exp):
return exp, find_variable(exp)
def find_possibilities(node, depth=0):
"""
Find all possibilities inside a node and return them in a list.
"""
p = []
handlers = []
# Add negation handlers
if node.negated:
handlers.extend(RULES[OP_NEG])
if not node.is_leaf:
# Traverse through child nodes first using postorder traversal
for child in node:
p.extend(find_possibilities(child, depth + 1))
# Add operator-specific handlers
if node.op in RULES:
handlers.extend(RULES[node.op])
# Run handlers
for handler in handlers:
possibilities = [(pos, depth) for pos in handler(node)]
p.extend(possibilities)
return p
class Parser(BisonParser):
"""
Implements the calculator parser. Grammar rules are defined in the method
......@@ -133,7 +104,7 @@ class Parser(BisonParser):
self.interactive = kwargs.get('interactive', 0)
self.timeout = kwargs.get('timeout', 0)
self.root_node = None
self.root_node_changed = True
self.possibilities = None
self.reset()
......@@ -143,7 +114,7 @@ class Parser(BisonParser):
#self.subtree_map = {}
self.set_root_node(None)
self.possibilities = self.last_possibilities = []
self.possibilities = None
def run(self, *args, **kwargs):
self.reset()
......@@ -172,15 +143,7 @@ class Parser(BisonParser):
return read_buffer[:nbytes]
def hook_read_before(self):
if self.possibilities:
if self.verbose: # pragma: nocover
print 'possibilities:'
items = filter_duplicates(self.possibilities)
self.last_possibilities = self.possibilities
if self.verbose: # pragma: nocover
print ' ' + '\n '.join(map(str, items))
pass
def hook_read_after(self, data):
"""
......@@ -191,8 +154,6 @@ class Parser(BisonParser):
if not data.strip():
return data
self.possibilities = []
# Replace known keywords with escape sequences.
words = list(self.__class__.words)
words.insert(10, '\n')
......@@ -270,66 +231,43 @@ class Parser(BisonParser):
def hook_handler(self, target, option, names, values, retval):
return retval
#if target in ['exp', 'line', 'input'] \
# or not isinstance(retval, ExpressionBase):
# return retval
#if not retval.negated and retval.type != TYPE_OPERATOR:
# return retval
#if retval.negated:
# handlers = RULES[OP_NEG]
#elif retval.type == TYPE_OPERATOR and retval.op in RULES:
# handlers = RULES[retval.op]
#else:
# return retval
#for handler in handlers:
# possibilities = handler(retval)
# self.possibilities.extend(possibilities)
#return retval
def set_root_node(self, node):
self.root_node = node
self.root_node_changed = True
self.possibilities = None
def find_possibilities(self):
if not self.root_node:
raise RuntimeError('No expression')
if not self.root_node_changed:
if self.possibilities != None:
if self.verbose:
print 'Expression has not changed, do not update possibilities'
print 'Expression has not changed, not updating possibilities'
return
p = find_possibilities(self.root_node)
#sort_possiblities(p)
self.root_possibilities = [pos for pos, depth in p]
self.root_node_changed = False
self.possibilities = find_possibilities(self.root_node)
def display_hint(self):
self.find_possibilities()
if self.root_possibilities:
print self.root_possibilities[0]
else:
print 'No further reduction is possible.'
if self.interactive:
if self.possibilities:
print self.possibilities[0]
else:
print 'No further reduction is possible.'
def display_possibilities(self):
self.find_possibilities()
if self.root_possibilities:
print '\n'.join(map(str, self.root_possibilities))
for i, p in enumerate(self.possibilities):
print '%d %s' % (i, p)
def rewrite(self):
def rewrite(self, index=0):
self.find_possibilities()
if not self.root_possibilities:
if not self.possibilities:
return False
suggestion = self.root_possibilities[0]
suggestion = self.possibilities[index]
if self.verbose:
print 'Applying suggestion:', suggestion
......@@ -337,7 +275,7 @@ class Parser(BisonParser):
expression = apply_suggestion(self.root_node, suggestion)
if self.verbose:
print 'After application:', expression
print 'After application: ', expression
self.set_root_node(expression)
......@@ -391,6 +329,7 @@ class Parser(BisonParser):
| HINT NEWLINE
| POSSIBILITIES NEWLINE
| REWRITE NEWLINE
| REWRITE NUMBER NEWLINE
| REWRITE_ALL NEWLINE
| RAISE NEWLINE
"""
......@@ -410,11 +349,15 @@ class Parser(BisonParser):
self.rewrite()
return self.root_node
if option == 6: # rule: REWRITE_ALL NEWLINE
if option == 6: # rule: REWRITE NUMBER NEWLINE
self.rewrite(int(values[1]))
return self.root_node
if option == 7: # rule: REWRITE_ALL NEWLINE
self.rewrite_all()
return self.root_node
if option == 7:
if option == 8:
raise RuntimeError('on_line: exception raised')
def on_debug(self, target, option, names, values):
......
......@@ -25,35 +25,15 @@ class Possibility(object):
% (self.root, self.handler.func_name, self.args)
def __eq__(self, other):
"""
Use node hash comparison when comparing to other Possibility to assert
that its is the same object as in this one.
"""
return self.handler == other.handler \
and hash(self.root) == hash(other.root) \
and self.args == other.args
def filter_duplicates(possibilities):
"""
Filter duplicated possibilities. Duplicated possibilities occur in n-ary
nodes, the root-level node and a lower-level node will both recognize a
rewrite possibility within their scope, whereas only the root-level one
matters.
Example: 1 + 2 + 3
The addition of 1 and 2 is recognized by n-ary additions "1 + 2" and
"1 + 2 + 3". The "1 + 2" addition should be removed by this function.
"""
features = []
unique = []
for p in reversed(possibilities):
feature = (p.handler, p.args)
if feature not in features:
features.append(feature)
unique.insert(0, p)
return unique
def find_parent_node(root, child):
nodes = [root]
......@@ -61,7 +41,6 @@ def find_parent_node(root, child):
node = nodes.pop()
while node:
if node.type != TYPE_OPERATOR:
break
......@@ -78,10 +57,9 @@ def apply_suggestion(root, suggestion):
# TODO: clone the root node before modifying. After deep copying the root
# node, the subtree_map cannot be used since the hash() of each node in the
# deep copied root node has changed.
#root_clone = root.clone()
#root = root.clone()
subtree = suggestion.handler(suggestion.root, suggestion.args)
parent_node = find_parent_node(root, suggestion.root)
# There is either a parent node or the subtree is the root node.
......
from .factors import expand_double, expand_single
from .sort import move_constant
from .numerics import reduce_fraction_constants
from .logarithmic import factor_in_exponent_multiplicant, \
factor_out_exponent, raised_base
from .derivatives import chain_rule
# Functions to move to the beginning of the possibilities list. Pairs of within
# the list itself are compared by their position in the list: lower in the list
# means lower priority
HIGH = [
raised_base,
]
# Functions to move to the end of the possibilities list. Pairs of within the
# list itself are compared by their position in the list: lower in the list
# means lower priority
LOW = [
move_constant,
reduce_fraction_constants,
factor_in_exponent_multiplicant,
]
# Fucntion precedences relative to eachother. Tuple (A, B) means that A has a
# higer priority than B. This list ignores occurences in the HIGH or LOW lists
# above
RELATIVE = [
# Precedences needed for 'power rule'
(chain_rule, raised_base),
(raised_base, factor_out_exponent),
# Expand 'single' before 'double' to avoid unnessecary complexity
(expand_single, expand_double),
]
# Convert to dictionaries for efficient lookup
HIGH = dict([(h, i) for i, h in enumerate(HIGH)])
LOW = dict([(h, i) for i, h in enumerate(LOW)])
from rules.sort import move_constant
from rules.numerics import reduce_fraction_constants
from rules.logarithmic import factor_in_exponent_multiplicant
from node import OP_NEG
from rules import RULES
from rules.precedences import HIGH, LOW, RELATIVE
def pick_suggestion(possibilities):
if not possibilities:
return
def compare_possibilities(a, b):
"""
Comparable function for (possibility, depth) pairs.
Returns a positive number if A has a lower priority than B, a negative
number for the reverse case, and 0 if the possibilities have equal
priorities.
"""
(pa, da), (pb, db) = a, b
ha, hb = pa.handler, pb.handler
# TODO: pick the best suggestion.
for suggestion, p in enumerate(possibilities + [None]):
if p and p.handler not in [move_constant, reduce_fraction_constants,
factor_in_exponent_multiplicant]:
break
# Check if A and B have a precedence relative to eachother
if (ha, hb) in RELATIVE:
return -1
if not p:
return possibilities[0]
if (hb, ha) in RELATIVE:
return 1
return possibilities[suggestion]
# If A has a high priority, it might be moved to the start of the list
if ha in HIGH:
# Id B has a high priority too, compare the positions in the list
if hb in HIGH:
return HIGH[ha] - HIGH[hb]
# Move A towards the beginning of the list
return -1
# If only B has a high priority, move it up with respect to A
if hb in HIGH:
return 1
# If A has a low priority, it might be moved to the end of the list
if ha in LOW:
# Id B has a low priority too, compare the positions in the list
if hb in LOW:
return LOW[ha] - LOW[hb]
# Move A towards the end of the list
return 1
# If only B has a high priority, move it down with respect to A
if hb in LOW:
return -1
# default: use order that was generated implicitely by leftmost-innermost
# expression traversal
return 0
def depth_possibilities(node, depth=0, parent_op=None):
p = []
handlers = []
# Add operator-specific handlers
if not node.is_leaf:
# Traverse through child nodes first using postorder traversal
for child in node:
# FIXME: "depth + 1" is disabled for the purpose of
# leftmost-innermost traversal
p += depth_possibilities(child, depth, node.op)
# Add operator-specific handlers. Prevent duplicate possibilities in
# n-ary nodes by only executing the handlers on the outermost node of
# related nodes with the same operator
if node.op != parent_op and node.op in RULES:
handlers += RULES[node.op]
# Add negation handlers after operator-specific handlers to obtain an
# outermost effect for negations
if node.negated:
handlers += RULES[OP_NEG]
# Run handlers
for handler in handlers:
p += [(pos, depth) for pos in handler(node)]
#print node, p
return p
def find_possibilities(node):
"""
Find all possibilities inside a node and return them in a list.
"""
possibilities = depth_possibilities(node)
#import copy
#old_possibilities = copy.deepcopy(possibilities)
possibilities.sort(compare_possibilities)
#get_handler = lambda (p, d): str(p.handler)
#if old_possibilities != possibilities:
# print 'before:', '\n '.join(map(get_handler, old_possibilities))
# print 'after:', '\n '.join(map(get_handler, possibilities))
return [p for p, depth in possibilities]
# 2x ^ 2 = 3x ^ (1 + 1)
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