Commits

Pierre Carbonnelle committed 16bed7b

Add pre-term to prefixed literal, in preparation for X.a[1]==Y, where X is a variable

  • Participants
  • Parent commits a8a9199

Comments (0)

Files changed (5)

File pyDatalog/doc.py

 len_(X)        (c1#v2)       (c1#v2)        (c1#v1)
 p              p/2
 p(X,1)         p/2v2c3       p/2v2c3        p/2v1c3
-p[X]==1        p/2v2c3       p/2v2c3        p/2v1
+p[X]==1        p/2v2c3       p/2v2c3        p/2v1 
+A.p[X]==1     see #comparison
 
 
 
+#comparison prefixed literal #######################
+
+A term is pre-appended to expression of the form P[X]==Y, P.s[X]==Y, and P.s(X).
+
+The added term represents P.  
+When P is a class, the extra term is a constant string '_pyD_class'
+When P is a variable, the extra term is subject to substitution 
+in the search algorithm, so that its bound value can be used 
+to evaluate the expression.
+
+When calling python resolvers, this term is removed, 
+and added back to the result received.
+
+In aggregate predicate, this term also needs special care.
+
+TODO : do no add this term for P[X]==Y (beware of aggregates, though)
+
+
 #unify unify and substitution ###################################
 
 An environment is a map from variables to terms.

File pyDatalog/examples/test.py

     assert (A.len[X]==Y) == [(b, 1), (a, 1)]
     assert (A.len[a]==Y) == [(1,)]
 
+    assert(A.x(X)) == []
+
     """ subclass                                              """
 
     class Z(A):
         a.z == 'z'
     except Exception as e:
         e_message = e.message if hasattr(e, 'message') else e.args[0]
-        if e_message != "Predicate without definition (or error in resolver): A.z[1]==/2":
+        if e_message != "Predicate without definition (or error in resolver): A.z[1]==/3":
             print(e_message)
     else:
         assert False
     assert (Z.len[X]==Y) == [(w, 1), (z, 1)]
     assert (Z.len[z]==Y) == [(1,)]
     
+    assert (A.c[X]==Y) & (Z.c[X]==Y) == [(w, 'wa'), (z, 'za')]
     # TODO print (A.b[w]==Y)
             
     """ python resolvers                                              """
     assert_error("ask(X<1)", 'Error: left hand side of comparison must be bound: ')
     assert_error("ask(X<Y)", 'Error: left hand side of comparison must be bound: ')
     assert_error("ask(1<Y)", 'Error: left hand side of comparison must be bound: ')
-    assert_error("ask( (A.c[X]==Y) & (Z.c[X]==Y))", "TypeError: First argument of Z.c\[1\]==\('.','.'\) must be a Z, not a A ")
-    assert_ask("A.u[X]==Y", "Predicate without definition \(or error in resolver\): A.u\[1\]==/2")
-    assert_ask("A.u[X,Y]==Z", "Predicate without definition \(or error in resolver\): A.u\[2\]==/3")
+    assert_ask("A.u[X]==Y", "Predicate without definition \(or error in resolver\): A.u\[1\]==/3")
+    assert_ask("A.u[X,Y]==Z", "Predicate without definition \(or error in resolver\): A.u\[2\]==/4")
     assert_error('(a_sum[X] == sum(Y, key=Y)) <= p(X, Z, Y)', "Error: Duplicate definition of aggregate function.")
     assert_error('(two(X)==Z) <= (Z==X+(lambda X: X))', 'Syntax error near equality: consider using brackets. two\(X\)')
     assert_error('p(X) <= sum(X, key=X)', 'Invalid body for clause')

File pyDatalog/pyDatalog.py

 
 """ ****************** python Mixin ***************** """
 
-#Keep a dictionary of classes with datalog capabilities.  
-#This list is used by pyEngine to resolve prefixed literals.
-Class_dict = {}
-pyEngine.Class_dict = Class_dict # TODO better way to share it with pyEngine.py ?
-
 class metaMixin(type):
     """Metaclass used to define the behavior of a subclass of Mixin"""
     __refs__ = defaultdict(weakref.WeakSet)
     def __init__(cls, name, bases, dct):
         """when creating a subclass of Mixin, save the subclass in Class_dict. """
         super(metaMixin, cls).__init__(name, bases, dct)
-        Class_dict[name]=cls
+        pyEngine.add_class(cls, name)
         cls.has_SQLAlchemy = any(base.__module__ in ('sqlalchemy.ext.declarative', 
                             'sqlalchemy.ext.declarative.api') for base in bases)
         
             """ responds to instance.method by asking datalog engine """
             if not attribute == '__iter__' and not attribute.startswith('_sa_'):
                 predicate_name = "%s.%s[1]==" % (self.__class__.__name__, attribute)
-                literal = pyParser.Literal.make(predicate_name, (self, pyParser.Symbol("X")))
+                terms = (pyParser.VarSymbol('_pyD_class', forced_type='constant'), self, pyParser.Symbol("X")) #prefixed
+                literal = pyParser.Literal.make(predicate_name, terms) #TODO predicate_name[:-2]
                 result = literal.lua.ask()
                 return result[0][-1] if result else None                    
             raise AttributeError
         """Called by pyEngine to resolve a prefixed literal for a subclass of Mixin."""
         terms = literal.terms
         attr_name = literal.pred.suffix
-        operator = (literal.pred.name.split(']')[1:2]+[None])[0] # what's after ']' or None
+        operator = literal.pred.name.split(']')[1] # what's after ']' or None
         
         # TODO check prearity
         def check_attribute(X):
             if attr_name not in X.__dict__ and attr_name not in cls.__dict__:
                 raise AttributeError("%s does not have %s attribute" % (cls.__name__, attr_name))
 
-        if len(terms)==2:
-            X, Y = terms[0], terms[1]
+        if len(terms)==3: #prefixed
+            X, Y = terms[1], terms[2]
             if X.is_constant:
                 # try accessing the attribute of the first term in literal
                 check_attribute(X.id)
                 Y1 = getattr(X.id, attr_name)
                 if not Y.is_constant or not operator or pyEngine.compare(Y1,operator,Y.id):
-                    yield (X.id, Y.id if Y.is_constant else Y1 if operator=='==' else None)
+                    yield (terms[0], X.id, Y.id if Y.is_constant else Y1 if operator=='==' else None)
             elif cls.has_SQLAlchemy:
                 if cls.session:
                     q = cls.session.query(cls)
                     for r in q:
                         Y1 = getattr(r, attr_name)
                         if not Y.is_constant or not operator or pyEngine.compare(Y1,operator,Y.id):
-                                yield (r, Y.id if Y.is_constant else Y1 if operator=='==' else None)
+                                yield (terms[0], r, Y.id if Y.is_constant else Y1 if operator=='==' else None)
             else:
                 # python object with Mixin
                 for X in metaMixin.__refs__[cls]:
                     check_attribute(X)
                     Y1 = getattr(X, attr_name)
                     if not Y.is_constant or not operator or pyEngine.compare(Y1,operator,Y.id):
-                        yield (X, Y.id if Y.is_constant else Y1 if operator=='==' else None)
+                        yield (terms[0], X, Y.id if Y.is_constant else Y1 if operator=='==' else None)
             return
         else:
             raise AttributeError ("%s could not be resolved" % literal.pred.name)
 def __init__(self):
     if not self.__class__.has_SQLAlchemy:
         for cls in self.__class__.__mro__:
-            if cls.__name__ in Class_dict and cls not in (Mixin, object):
+            if cls.__name__ in pyEngine.Class_dict and cls not in (Mixin, object):
                 metaMixin.__refs__[cls].add(self)
 Mixin.__init__ = __init__
 

File pyDatalog/pyEngine.py

 Python_resolvers = {} # dictionary  of python functions that can resolve a predicate
 Logic = None # place holder for Logic class from Logic module
 
+# Keep a dictionary of classes with datalog capabilities.  
+Class_dict = {}
+
+
 #       DATA TYPES          #####################################
 
 # Internalize objects based on an identifier.
         """ returns a new literal with '==' instead of comparison """
         return self._renamed(self.pred.name.replace(self.pred.comparison, '=='))
     
+    def subst_first(self, env): #prefixed
+        """ substitute the first term of prefixed literal, using env """
+        if self.pred.prefix:
+            self.terms[0] = self.terms[0].subst(env)
+                
     def __str__(self): 
         return "%s(%s)" % (self.pred.name, ','.join([str(term) for term in self.terms])) 
 
     if result is True:
         return fact(subgoal, True)
     result = [Interned.of(r) for r in result]
-    if class0 and result[0].id and not isinstance(result[0].id, class0): 
+    if class0 and result[1].id and not isinstance(result[1].id, class0): #prefixed
         return
     result = Literal(subgoal.literal.pred.name, result)
     env = unify(subgoal.literal, result)
 
 ###############     SEARCH     ##################################
 
+def add_class(cls, name):
+    """ Update the list of pyDatalog-capable classes"""
+    Class_dict[name] = cls
+    #prefixed replace the first term of each functional comparison literal for that class..
+    env = {Var(name): Const('_pyD_class')}
+    for pred in Logic.tl.logic.Db.values():
+        for clause in pred.db.values():
+            clause.head.subst_first(env)
+            for literal in clause.body:
+                literal.subst_first(env)
+
 def _(self):
     " iterator of the parent classes that have pyDatalog capabilities"
     if self._class():
                 lambda base_subgoal=base_subgoal, subgoal=subgoal:
                     fact(subgoal, True) if not base_subgoal.facts else None)
         return
-    if class0 and terms[0].is_constant and not isinstance(terms[0].id, class0):
-        raise util.DatalogError("TypeError: First argument of %s must be a %s, not a %s " 
-                    % (str(literal0), class0.__name__, type(terms[0].id).__name__), None, None)
+    
     for _class in literal0.pred.parent_classes():
         literal = literal0.rebased(_class)
         
                     fact_candidate(subgoal, class0, result)
                 return
         if _class: 
-            method_name = '_pyD_%s%i'% (literal.pred.suffix, int(literal.pred.arity)) # TODO special method for comparison
+             # TODO add special method for custom comparison
+            method_name = '_pyD_%s%i' % (literal.pred.suffix, int(literal.pred.arity - 1)) #prefixed
             if literal.pred.suffix and method_name in _class.__dict__:
                 if Logging : logging.debug("pyDatalog uses class resolvers for %s" % literal)
-                for result in getattr(_class, method_name)(*terms):
-                    fact_candidate(subgoal, class0, result)
+                for result in getattr(_class, method_name)(*(terms[1:])): 
+                    fact_candidate(subgoal, class0, (terms[0],) + result)
                 return        
             try: # call class._pyD_query
                 resolver = _class._pyD_query
                 if not _class.has_SQLAlchemy : gc.collect() # to make sure pyDatalog.metaMixin.__refs__[cls] is correct
-                for result in resolver(literal.pred.name, terms):
-                    fact_candidate(subgoal, class0, result)
+                for result in resolver(literal.pred.name, terms[1:]):
+                    fact_candidate(subgoal, class0, (terms[0],) + result)
                 if Logging : logging.debug("pyDatalog has used _pyD_query resolvers for %s" % literal)
                 return
             except:

File pyDatalog/pyParser.py

             self._pyD_type = 'variable'
             self._pyD_lua = pyEngine.Var(name)
 
+    @classmethod
+    def make_for_prefix(cls, name): #prefixed
+        prefix = name.split('.')[0]
+        return VarSymbol('_pyD_class', forced_type='constant') if prefix in pyEngine.Class_dict else VarSymbol(prefix)
+
     def __neg__(self):
         """ called when evaluating -X. Used in aggregate arguments """
         neg = Symbol(self._pyD_value)
         elif self._pyD_name == 'format_':
             return Operation(args[0], '%', args[1:])
         else: # create a literal
-            literal = Literal.make(self._pyD_name, tuple(args))
+            pre_term = (VarSymbol.make_for_prefix(self._pyD_name),) if '.' in self._pyD_name else ()
+            literal = Literal.make(self._pyD_name, pre_term + tuple(args)) #prefixed
             return literal
 
     def __getattr__(self, name):
         self.args = args
         self.todo = self
         cls_name = predicate_name.split('.')[0].replace('~','') if 1< len(predicate_name.split('.')) else ''
+        if cls_name and 2<=len(args) and not isinstance(args[1], VarSymbol) and cls_name not in [c.__name__ for c in args[1].__class__.__mro__]:
+            raise TypeError("Object is incompatible with the class that is queried.") #prefixed
+
         self.terms = [] # the list of args converted to Expression
         for i, arg in enumerate(args):
             if isinstance(arg, Literal):
                 raise util.DatalogError("Syntax error: Literals cannot have a literal as argument : %s%s" % (predicate_name, self.terms), None, None)
-            elif not isinstance(arg, VarSymbol) and i==0 and cls_name and cls_name not in [c.__name__ for c in arg.__class__.__mro__]:
-                raise TypeError("Object is incompatible with the class that is queried.")
             elif isinstance(arg, Aggregate):
                 raise util.DatalogError("Syntax error: Incorrect use of aggregation.", None, None)
             if isinstance(arg, Variable):
         """ factory of Literal (or Body) for a comparison. """
         other = Expression._for(other)
         if isinstance(self, Function):
+            #TODO perf : do not add pre-term for non prefixed #prefixed
+            name, prearity = self.name + operator, 1+len(self.keys)
+            terms = [VarSymbol.make_for_prefix(self.name)] + list(self.keys) + [other]  #prefixed
             if isinstance(other, Aggregate): # p[X]==aggregate() # TODO create 2 literals here
                 if operator != '==':
                     raise util.DatalogError("Aggregate operator can only be used with equality.", None, None)
-                name, terms, prearity = (self.name + '==', list(self.keys) + [other], len(self.keys))
                 
                 # 1 create literal for queries
                 terms[-1] = (Symbol('X')) # (X, X)
                 add_clause(l, l) # body will be ignored, but is needed to make the clause safe
 
                 # 2 prepare literal for the calculation. It can be used in the head only
+                #TODO for speed use terms[1:], prearity-1
                 del terms[-1] # --> (X,)
                 terms.extend(other.args)
                 prearity = len(terms) # (X,Y,Z)
                 return Literal.make(name + '!', terms, prearity=prearity) #pred
-            literal = Query(self.name + operator, list(self.keys) + [other], prearity=len(self.keys))
+            literal = Query(name, terms, prearity)
             return self._argument_precalculations & other._pyD_precalculations & literal
         else:
             if not isinstance(other, Expression):
         
         self.slice_for_each = slice(for_each_start, order_by_start)
         self.reversed_order_by = range(len(result[0])-1-self.sep_arity, order_by_start-1,  -1)
-        self.slice_group_by = slice(0, for_each_start-self.Y_arity)
+        self.slice_group_by = slice(1, for_each_start-self.Y_arity)  #prefixed
         # first sort per order_by, allowing for _pyD_negated
         for i in self.reversed_order_by:
             result.sort(key=lambda literal, i=i: literal[i].id,
     def add(self, row):
         # retain the value if (X,) == (Z,)
         if row[self.slice_group_by] == row[self.slice_for_each]:
-            self._value = list(row[self.slice_group_by]) + [pyEngine.Const(self.count),]
+            self._value = [row[0],] + list(row[self.slice_group_by]) + [pyEngine.Const(self.count),] #prefixed
             return self._value
         self.count += 1
         
     
     def add(self,row):
         self.count += row[self.to_add].id # TODO
-        if row[:self.to_add] == row[self.slice_for_each]:
+        if row[1:self.to_add] == row[self.slice_for_each]: #prefixed
             self._value = list(row[:self.to_add]) + [pyEngine.Const(self.count),]
             return self._value