Просмотр исходного кода

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.
Taddeus Kroes 14 лет назад
Родитель
Сommit
d86dda9273
5 измененных файлов с 215 добавлено и 151 удалено
  1. 43 23
      src/node.py
  2. 30 87
      src/parser.py
  3. 5 27
      src/possibilities.py
  4. 42 0
      src/rules/precedences.py
  5. 95 14
      src/strategy.py

+ 43 - 23
src/node.py

@@ -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

+ 30 - 87
src/parser.py

@@ -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):

+ 5 - 27
src/possibilities.py

@@ -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.

+ 42 - 0
src/rules/precedences.py

@@ -0,0 +1,42 @@
+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)])

+ 95 - 14
src/strategy.py

@@ -1,19 +1,100 @@
-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)