"""pyDatalogCopyright (C) 2012 Pierre CarbonnelleCopyright (C) 2004 Shai BergerThis library is free software; you can redistribute it and/or modifyit under the terms of the GNU Lesser General Public License aspublished by the Free Software Foundation; either version 2 of theLicense, or (at your option) any later version.This library is distributed in the hope that it will be useful, butWITHOUT ANY WARRANTY; without even the implied warranty ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNULesser General Public License for more details.You should have received a copy of the GNU Lesser General PublicLicense along with this library; if not, write to the Free SoftwareFoundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301USAThis work is derived from Pythologic, (C) 2004 Shai Berger, in accordance with the Python Software Foundation licence.(See http://code.activestate.com/recipes/303057/ andhttp://www.python.org/download/releases/2.0.1/license/ )""""""Design principle:Instead of writing our own parser, we use python's parser. The datalog code is first compiled in python byte code, then "undefined" variables are initialized as instance of Symbol, then the code is finally executed to load the clauses.This is done in the load() and add_program() method of Parser class.Methods exposed by this file:* load(code)* add_program(func)* ask(code)Classes hierarchy contained in this file: see class diagram on http://bit.ly/YRnMPH* ProgramContext : class to safely differentiate between In-line queries and pyDatalog program / ask(), using ProgramMode global variable* _transform_ast : performs some modifications of the abstract syntax tree of the datalog program* LazyList : a subclassable list that is populated when it is accessed. Mixin for pyDatalog.Variable. * LazyListOfList : Mixin for Literal and Body * Literal : made of a predicate and a list of arguments. Instantiated when a symbol is called while executing the datalog program * Body : a list of literals to be used in a clause. Instantiated when & is executed in the datalog program* Expression : base class for objects that can be part of an inequality or operation * VarSymbol : represents the symbol of a variable. Mixin for pyDatalog.Variable * Symbol : contains a constant, a variable or a predicate. Instantiated before executing the datalog program * Function : represents f[X] * Operation : made of an operator and 2 operands. Instantiated when an operator is applied to a symbol while executing the datalog program * Lambda : represents a lambda function, used in expressions* Aggregate : represents calls to aggregation method, e.g. min(X)"""importastfromcollectionsimportdefaultdict,OrderedDictimportinspectimportosimportreimportstringimportsixsix.add_move(six.MovedModule('UserList','UserList','collections'))fromsix.movesimportbuiltins,xrange,UserListimportsysimportweakrefPY3=sys.version_info[0]==3func_code='__code__'ifPY3else'func_code'try:from.importpyEngineexceptValueError:importpyEnginepyDatalog=None#circ: later set by pyDatalog to avoid circular import""" global variable to differentiate between in-line queries and pyDatalog program / ask"""ProgramMode=FalseclassProgramContext(object):"""class to safely use ProgramMode within the "with" statement"""def__enter__(self):globalProgramModeProgramMode=Truedef__exit__(self,exc_type,exc_value,traceback):globalProgramModeProgramMode=False""" Parser methods """defadd_symbols(names,variables):fornameinnames:variables[name]=Symbol(name)class_transform_ast(ast.NodeTransformer):""" does some transformation of the Abstract Syntax Tree of the datalog program """defvisit_Call(self,node):"""rename builtins to allow customization"""self.generic_visit(node)ifhasattr(node.func,'id'):node.func.id='__sum__'ifnode.func.id=='sum'elsenode.func.idnode.func.id='__len__'ifnode.func.id=='len'elsenode.func.idnode.func.id='__min__'ifnode.func.id=='min'elsenode.func.idnode.func.id='__max__'ifnode.func.id=='max'elsenode.func.idreturnnodedefvisit_Compare(self,node):""" rename 'in' to allow customization of (X in (1,2))"""self.generic_visit(node)if1<len(node.comparators):raisepyDatalog.DatalogError("Syntax error: please verify parenthesis around (in)equalities",node.lineno,None)ifnotisinstance(node.ops[0],(ast.In,ast.NotIn)):returnnodevar=node.left# X, an _ast.Name objectcomparators=node.comparators[0]# (1,2), an _ast.Tuple objectnewNode=ast.Call(ast.Attribute(var,'_in'ifisinstance(node.ops[0],ast.In)else'_not_in',var.ctx),# func[comparators],# args[],# keywordsNone,# starargsNone# kwargs)returnast.fix_missing_locations(newNode)defload(code,newglobals={},defined=set([]),function='load'):""" code : a string or list of string newglobals : global variables for executing the code defined : reserved symbols """# remove indentation based on first non-blank linelines=code.splitlines()ifisinstance(code,six.string_types)elsecoder=re.compile('^\s*')forlineinlines:spaces=r.match(line).group()ifspacesandline!=spaces:breakcode='\n'.join([line.replace(spaces,'')forlineinlines])tree=ast.parse(code,function,'exec')try:tree=_transform_ast().visit(tree)exceptpyDatalog.DatalogErrorase:e.function=functione.message=e.valuee.value="%s\n%s"%(e.value,lines[e.lineno-1])six.reraise(*sys.exc_info())code=compile(tree,function,'exec')defined=defined.union(dir(builtins))defined.add('None')fornameinset(code.co_names).difference(defined):# for names that are not definedadd_symbols((name,),newglobals)try:withProgramContext():six.exec_(code,newglobals)exceptpyDatalog.DatalogErrorase:e.function=functiontraceback=sys.exc_info()[2]e.lineno=1whileTrue:iftraceback.tb_frame.f_code.co_name=='<module>':e.lineno=traceback.tb_linenobreakeliftraceback.tb_next:traceback=traceback.tb_nexte.message=e.valuee.value="%s\n%s"%(e.value,lines[e.lineno-1])six.reraise(*sys.exc_info())class_NoCallFunction(object):""" This class prevents a call to a datalog program created using the 'program' decorator """def__call__(self):raiseTypeError("Datalog programs are not callable")defadd_program(func):""" A helper for decorator implementation """source_code=inspect.getsource(func)lines=source_code.splitlines()# drop the first 2 lines (@pydatalog and def _() )if'@'inlines[0]:dellines[0]if'def'inlines[0]:dellines[0]source_code=linestry:code=func.__code__except:raiseTypeError("function or method argument expected")newglobals=func.__globals__.copy()ifPY3elsefunc.func_globals.copy()func_name=func.__name__ifPY3elsefunc.func_namedefined=set(code.co_varnames).union(set(newglobals.keys()))# local variables and global variablesload(source_code,newglobals,defined,function=func_name)return_NoCallFunction()defask(code,_fast=None):withProgramContext():tree=ast.parse(code,'ask','eval')tree=_transform_ast().visit(tree)code=compile(tree,'ask','eval')newglobals={}add_symbols(code.co_names,newglobals)parsed_code=eval(code,newglobals)parsed_code=parsed_code.literal()ifisinstance(parsed_code,Body)elseparsed_codereturnpyEngine.toAnswer(parsed_code.lua,parsed_code.lua.ask(_fast))""" Parser classes """classLazyList(UserList.UserList):"""a subclassable list that is populated when it is accessed """"""used by Literal, Body, pyDatalog.Variable to delay evaluation of datalog queries written in python """""" during debugging, beware that viewing a Lazylist will force its udpate"""def__init__(self):self.todo=None# self.todo.ask() calculates self.dataself._data=[]@propertydefdata(self):# returns the list, after recalculation if neededifself.todoisnotNone:self.todo.ask()returnself._datadef_value(self):returnself.datadefv(self):returnself.data[0]ifself.dataelseNoneclassLazyListOfList(LazyList):""" represents the result of an inline query (a Literal or Body)"""def__eq__(self,other):returnset(self.data)==set(other)def__ge__(self,other):# returns the first occurrence of 'other' variable in the result of a functionifself.data:assertisinstance(other,pyDatalog.Variable)fortinself.literal().args:ifid(t)==id(other):returnt.data[0]classExpression(object):def_precalculation(self):returnBody()# by default, there is no precalculation needed to evaluate an expressiondef__eq__(self,other):ifself._pyD_type=='variable'andnotisinstance(other,Symbol):returnself._make_expression_literal('==',other)else:returnLiteral.make("=",(self,other))def__ne__(self,other):returnself._make_expression_literal('!=',other)def__le__(self,other):returnself._make_expression_literal('<=',other)def__lt__(self,other):returnself._make_expression_literal('<',other)def__ge__(self,other):returnself._make_expression_literal('>=',other)def__gt__(self,other):returnself._make_expression_literal('>',other)def_in(self,values):""" called when compiling (X in (1,2)) """returnself._make_expression_literal('in',values)def_not_in(self,values):""" called when compiling (X not in (1,2)) """returnself._make_expression_literal('not in',values)def__add__(self,other):returnOperation(self,'+',other)def__sub__(self,other):returnOperation(self,'-',other)def__mul__(self,other):returnOperation(self,'*',other)def__div__(self,other):returnOperation(self,'/',other)def__truediv__(self,other):returnOperation(self,'/',other)def__floordiv__(self,other):returnOperation(self,'//',other)def__radd__(self,other):returnOperation(other,'+',self)def__rsub__(self,other):returnOperation(other,'-',self)def__rmul__(self,other):returnOperation(other,'*',self)def__rdiv__(self,other):returnOperation(other,'/',self)def__rtruediv__(self,other):returnOperation(self,'/',other)def__rfloordiv__(self,other):returnOperation(other,'//',self)classVarSymbol(Expression):""" represents the symbol of a variable, both inline and in pyDatalog program """def__init__(self,name):self._pyD_name=nameself._pyD_negated=False# for aggregate with sort in descending orderifisinstance(name,(int,list,tuple,xrange))ornotnameorname[0]notinstring.ascii_uppercase+'_':self._pyD_type='constant'self._pyD_lua=pyEngine.Const(name)else:self._pyD_type='variable'self._pyD_lua=pyEngine.Var(name)def_make_expression_literal(self,operator,other):"""private function to create a literal for comparisons"""ifisinstance(other,type(lambda:None)):other=Lambda(other)name='='+str(self)+operator+str(other)ifotherisNoneorisinstance(other,(int,six.string_types,list,tuple,xrange)):literal=Literal.make(name,[self])expr=pyEngine.make_operand('constant',other)else:ifnotisinstance(other,(Symbol,Expression)):raisepyDatalog.DatalogError("Syntax error: Symbol or Expression expected",None,None)literal=Literal.make(name,[self]+list(other._variables().values()))expr=other.lua_expr(list(self._variables().keys())+list(other._variables().keys()))literal.pre_calculations=other._precalculation()pyEngine.add_expr_to_predicate(literal.lua.pred,operator,expr)returnliteraldef__neg__(self):""" called when compiling -X """neg=Symbol(self._pyD_name)neg._pyD_negated=Truereturnnegdeflua_expr(self,variables):ifself._pyD_type=='variable':returnpyEngine.make_operand('variable',variables.index(self._pyD_name))else:returnpyEngine.make_operand('constant',self._pyD_name)def_variables(self):ifself._pyD_type=='variable':returnOrderedDict({self._pyD_name:self})else:returnOrderedDict()classSymbol(VarSymbol):""" can be constant, variable or predicate name ask() creates a query created when analysing the datalog program """def__call__(self,*args,**kwargs):""" called when compiling p(args) """"time to create a literal !"ifself._pyD_name=='ask':if1<len(args):raiseRuntimeError('Too many arguments for ask !')fast=kwargs['_fast']if'_fast'inlist(kwargs.keys())elseFalseliteral=args[0]ifnotisinstance(args[0],Body)elseargs[0].literal()returnpyEngine.toAnswer(literal.lua,literal.lua.ask(fast))elifself._pyD_name=='__sum__':ifisinstance(args[0],(Symbol,pyDatalog.Variable)):returnSum_aggregate(args[0],for_each=kwargs.get('for_each',kwargs.get('key',[])))else:returnsum(args)elifself._pyD_name=='concat':returnConcat_aggregate(args[0],order_by=kwargs.get('order_by',kwargs.get('key',[])),sep=kwargs['sep'])elifself._pyD_name=='__min__':ifisinstance(args[0],Symbol):returnMin_aggregate(args[0],order_by=kwargs.get('order_by',kwargs.get('key',[])),)else:returnmin(args)elifself._pyD_name=='__max__':ifisinstance(args[0],Symbol):returnMax_aggregate(args[0],order_by=kwargs.get('order_by',kwargs.get('key',[])),)else:returnmax(args)elifself._pyD_name=='rank':returnRank_aggregate(None,for_each=kwargs.get('for_each',[]),order_by=kwargs.get('order_by',[]))elifself._pyD_name=='running_sum':returnRunning_sum(args[0],for_each=kwargs.get('for_each',[]),order_by=kwargs.get('order_by',[]))elifself._pyD_name=='__len__':ifisinstance(args[0],Symbol):returnLen_aggregate(args[0])else:returnlen(args[0])else:new_args,pre_calculations=[],Body()forarginargs:ifisinstance(arg,(Operation,Function,Lambda)):Y=Function.newSymbol()new_args.append(Y)pre_calculations=pre_calculations&(Y==arg)else:new_args.append(arg)literal=Literal.make(self._pyD_name,tuple(new_args))literal.pre_calculations=pre_calculationsreturnliteraldef__getattr__(self,name):""" called when compiling class.attribute """returnSymbol(self._pyD_name+'.'+name)def__getitem__(self,keys):""" called when compiling name[keys] """returnFunction(self._pyD_name,keys)def__str__(self):returnstr(self._pyD_name)def__setitem__(self,keys,value):""" called when compiling f[X] = expression """function=Function(self._pyD_name,keys)# following statement translates it into (f[X]==V) <= (V==expression)(function==function.symbol)<=(function.symbol==value)classFunction(Expression):""" represents predicate[a, b]"""Counter=0@classmethoddefnewSymbol(cls):Function.Counter+=1returnSymbol('_pyD_X%i'%Function.Counter)def__init__(self,name,keys):ifnotisinstance(keys,tuple):keys=(keys,)self.name="%s[%i]"%(name,len(keys))self.keys,self.pre_calculations=[],Body()forkeyinkeys:ifisinstance(key,(Operation,Function,Lambda)):Y=Function.newSymbol()self.keys.append(Y)self.pre_calculations=self.pre_calculations&(Y==key)else:self.keys.append(key)self.symbol=Function.newSymbol()self.dummy_variable_name='_pyD_X%i'%Function.Counterdef_make_expression_literal(self,operator,other):ifisinstance(other,type(lambda:None)):other=Lambda(other)assertoperator=="=="ornotisinstance(other,Aggregate),"Aggregate operators can only be used with =="ifoperator=='=='andnotisinstance(other,(Operation,Function,Lambda)):# p[X]==Y # TODO use positive list of classreturnLiteral.make(self.name+'==',list(self.keys)+[other],prearity=len(self.keys))literal=Literal.make(self.name+'==',list(self.keys)+[self.symbol],prearity=len(self.keys))if'.'notinself.name:# p[X]<Y+Z transformed into (p[X]=Y1) & (Y1<Y+Z)returnliteral&pyEngine.compare2(self.symbol,operator,other)elifisinstance(other,(Operation,Function,Lambda)):# a.p[X]<Y+Z transformed into (Y2==Y+Z) & (a.p[X]<Y2)Y2=Function.newSymbol()return(Y2==other)&Literal.make(self.name+operator,list(self.keys)+[Y2],prearity=len(self.keys))else:returnLiteral.make(self.name+operator,list(self.keys)+[other],prearity=len(self.keys))def__eq__(self,other):returnself._make_expression_literal('==',other)def__pos__(self):raisepyDatalog.DatalogError("bad operand type for unary +: 'Function'. Please consider adding parenthesis",None,None)def__neg__(self):raisepyDatalog.DatalogError("bad operand type for unary -: 'Function'. Please consider adding parenthesis",None,None)# following methods are used when the function is used in an expressiondef_variables(self):return{self.dummy_variable_name:self.symbol}deflua_expr(self,variables):returnpyEngine.make_operand('variable',variables.index(self.dummy_variable_name))def_precalculation(self):returnself.pre_calculations&(self==self.symbol)classOperation(Expression):"""made of an operator and 2 operands. Instantiated when an operator is applied to a symbol while executing the datalog program"""def__init__(self,lhs,operator,rhs):self.operator=operatordef_convert(operand):ifoperandisNoneorisinstance(operand,(six.string_types,int,list,tuple,xrange)):returnSymbol(operand)elifisinstance(operand,type(lambda:None)):returnLambda(operand)returnoperandself.lhs=_convert(lhs)self.rhs=_convert(rhs)def_variables(self):temp=self.lhs._variables()temp.update(self.rhs._variables())returntempdef_precalculation(self):returnself.lhs._precalculation()&self.rhs._precalculation()deflua_expr(self,variables):returnpyEngine.make_expression(self.operator,self.lhs.lua_expr(variables),self.rhs.lua_expr(variables))def__str__(self):return'('+str(self.lhs)+self.operator+str(self.rhs)+')'classLambda(Expression):"""represents a lambda function, used in expressions"""def__init__(self,other):self.operator='<lambda>'self.lambda_object=otherdef_variables(self):returndict([[var,Symbol(var)]forvaringetattr(self.lambda_object,func_code).co_varnames])deflua_expr(self,variables):operands=[pyEngine.make_operand('variable',variables.index(varname))forvarnameingetattr(self.lambda_object,func_code).co_varnames]returnpyEngine.make_lambda(self.lambda_object,operands)def__str__(self):return'lambda%i(%s)'%(id(self.lambda_object),','.join(getattr(self.lambda_object,func_code).co_varnames))classLiteral(LazyListOfList):""" created by source code like 'p(a, b)' unary operator '+' means insert it as fact binary operator '&' means 'and', and returns a Body operator '<=' means 'is true if', and creates a Clause """def__init__(self,predicate_name,terms,prearity=None,aggregate=None):LazyListOfList.__init__(self)self.predicate_name=predicate_nameself.prearity=prearityorlen(terms)self.pre_calculations=Body()self.has_variables,self.has_symbols,self.is_fact=(False,False,True)fortinterms:self.has_variables=self.has_variablesorisinstance(t,pyDatalog.Variable)self.has_symbols=self.has_symbolsorisinstance(t,Symbol)self.is_fact=self.is_factandnot(isinstance(t,pyDatalog.Variable)andnot(isinstance(t,Symbol)andt._pyD_type=='variable'))# TODO cleanup by redefining self.args, .HasSymbols, .has_variables, .prefixifnotProgramMode:# in-line, thus. --> replace variables in terms by Symbolsself.todo=selfself.args=termscls_name=predicate_name.split('.')[0].replace('~','')if1<len(predicate_name.split('.'))else''terms,env=[],{}fori,arginenumerate(self.args):ifisinstance(arg,pyDatalog.Variable):arg.todo=selfdelarg._data[:]# reset variables# deal with (X,X)variable=env.get(id(arg),Symbol('X%i'%id(arg)))env[id(arg)]=variableterms.append(variable)elifisinstance(arg,Symbol):terms.append(arg)elifi==0andcls_nameandarg.__class__.__name__!=cls_name:# TODO use __mro__ !raiseTypeError("Object is incompatible with the class that is queried.")else:terms.append(arg)else:self.args=[]self.terms=terms# adjust head literal for aggregateh_terms=list(terms)iftermsandself.prearity!=len(terms)andisinstance(terms[-1],Aggregate):# example : a[X] = sum(Y, key = Z)self.aggregate=terms[-1]# sum(Y, key=Z)h_predicate_name=predicate_name+'!'# compute list of termsdelh_terms[-1]# --> (X,)base_terms=list(h_terms)# creates a copy# OK to use any variable to represent the aggregate valuebase_terms.append(Symbol('X'))# (X, X)h_terms.extend(self.aggregate.args)h_prearity=len(h_terms)# (X,Y,Z)# create the second predicate # TODO use Pred() insteadl=Literal.make(predicate_name,base_terms,prearity,self.aggregate)pyDatalog.add_clause(l,l)# body will be ignored, but is needed to make the clause safe# TODO check that l.pred.aggregate_method is correctelse:self.aggregate=Noneh_predicate_name=predicate_nameh_prearity=prearitytbl=[]forainh_terms:ifisinstance(a,Symbol):tbl.append(a._pyD_lua)elifisinstance(a,Literal):raisepyDatalog.DatalogError("Syntax error: Literals cannot have a literal as argument : %s%s"%(predicate_name,terms),None,None)elifisinstance(a,Aggregate):raisepyDatalog.DatalogError("Syntax error: Incorrect use of aggregation.",None,None)else:tbl.append(pyEngine.Const(a))# now create the literal for the head of a clauseself.lua=pyEngine.Literal(h_predicate_name,tbl,h_prearity,aggregate)# TODO check that l.pred.aggregate is empty@classmethoddefmake(self,predicate_name,terms,prearity=None,aggregate=None):returnQuery(predicate_name,terms,prearity,aggregate)def__le__(self,body):" head <= body"globalProgramMode#TODO assert ProgramMode # '<=' cannot be used with literal containing pyDatalog.Variable instancesifisinstance(body,Literal):newBody=body.pre_calculations&bodyelse:ifnotisinstance(body,Body):raisepyDatalog.DatalogError("Invalid body for clause",None,None)newBody=Body()forliteralinbody.literals:newBody=newBody&literal.pre_calculations&literalresult=pyDatalog.add_clause(self,newBody)ifnotresult:raisepyDatalog.DatalogError("Can't create clause",None,None)returnresultclassQuery(Literal,LazyListOfList):defask(self):self._data=self.lua.ask(False)self.todo=NoneifnotProgramModeandself.data:transposed=list(zip(*(self.data)))# transpose resultresult=[]fori,arginenumerate(self.args):ifisinstance(arg,pyDatalog.Variable)andlen(arg._data)==0:arg._data.extend(transposed[i])arg.todo=Noneresult.append(transposed[i])self._data=list(zip(*result))ifresultelse[()]def__pos__(self):" unary + means insert into database as fact "assertself.is_fact,"Cannot assert a fact containing Variables"pyDatalog._assert_fact(self)def__neg__(self):" unary - means retract fact from database "assertself.is_fact,"Cannot assert a fact containing Variables"pyDatalog._retract_fact(self)def__invert__(self):"""unary ~ means negation """# TODO test with python queriesreturnLiteral.make('~'+self.predicate_name,self.terms)def__and__(self,other):" literal & literal"returnBody(self,other)def__str__(self):ifProgramMode:terms=list(map(str,self.terms))returnstr(self.predicate_name)+"("+','.join(terms)+")"else:returnLazyListOfList.__str__(self)def__eq__(self,other):ifProgramMode:raisepyDatalog.DatalogError("Syntax error near equality: consider using brackets. %s"%str(self),None,None)else:returnsuper(Literal,self).__eq__(other)defliteral(self):returnselfclassBody(LazyListOfList):""" created by p(a,b) & q(c,d) operator '&' means 'and', and returns a Body """Counter=0def__init__(self,*args):LazyListOfList.__init__(self)self.literals=[]forarginargs:self.literals+=[arg]ifisinstance(arg,Literal)elsearg.literalsself.has_variables=Falseforliteralinself.literals:ifhasattr(literal,'args'):self.has_variables=Trueself.todo=selfforarginliteral.args:ifisinstance(arg,pyDatalog.Variable):arg.todo=selfdef__and__(self,body2):ifnot(isinstance(body2,Body)ornotbody2.aggregate):raisepyDatalog.DatalogError("Aggregation cannot appear in the body of a clause",None,None)returnBody(self,body2)def__str__(self):ifself.has_variables:returnLazyListOfList.__str__(self)return' & '.join(list(map(str,self.literals)))defliteral(self,permanent=False):# return a literal that can be queried to resolve the bodyenv,args=OrderedDict(),OrderedDict()forliteralinself.literals:forterm,arginzip(literal.terms,literal.argsorliteral.terms):ifisinstance(term,Symbol)andterm._pyD_type=='variable'and(isinstance(arg,pyDatalog.Variable)orProgramMode):env[term._pyD_name]=termargs[id(arg)]=argargs=args.values()ifnotProgramModeelse[]# TODO cleanup : use args instead of env.values() ?ifpermanent:literal=Literal.make('_pyD_query'+str(Body.Counter),list(env.values()))Body.Counter=Body.Counter+1else:literal=Literal.make('_pyD_query',list(env.values()))literal.lua.pred.reset_clauses()literal<=selfliteral.args=argsreturnliteraldef__invert__(self):"""unary ~ means negation """return~(self.literal(permanent=True))defask(self):literal=self.literal()literal.ask()self._data=literal.data##################################### Aggregation #####################################classAggregate(object):""" represents a generic aggregation_method(X, for_each=Y, order_by=Z, sep=sep) e.g. 'sum(Y,key=Z)' in '(a[X]==sum(Y,key=Z))' pyEngine calls sort_result(), key(), reset(), add() and fact() to compute the aggregate """def__init__(self,Y=None,for_each=tuple(),order_by=tuple(),sep=None):# convert for_each=Z to for_each=(Z,)self.Y=Yself.for_each=(for_each,)ifisinstance(for_each,Symbol)elsetuple(for_each)self.order_by=(order_by,)ifisinstance(order_by,Symbol)elsetuple(order_by)ifsepandnotisinstance(sep,six.string_types):raisepyDatalog.DatalogError("Separator in aggregation must be a string",None,None)self.sep=sep# verify presence of keyword argumentsforkwinself.required_kw:arg=getattr(self,kw)ifargisNoneor(isinstance(arg,tuple)andarg==tuple()):raisepyDatalog.DatalogError("Error: argument missing in aggregate",None,None)# used to create literal. TODO : filter on symbolsself.args=((Y,)ifYisnotNoneelsetuple())+self.for_each+self.order_by+((sep,)ifsepisnotNoneelsetuple())self.Y_arity=1ifYisnotNoneelse0self.sep_arity=1ifsepisnotNoneelse0@propertydefarity(self):# of the aggregate function, not of the full predicate returnlen(self.args)defsort_result(self,result):# significant indexes in the result rowsorder_by_start=len(result[0])-len(self.order_by)-self.sep_arityfor_each_start=order_by_start-len(self.for_each)self.to_add=for_each_start-1self.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)# first sort per order_by, allowing for _pyD_negatedforiinself.reversed_order_by:result.sort(key=lambdaliteral,i=i:literal[i].id,reverse=self.order_by[i-order_by_start]._pyD_negated)# then sort per group_byresult.sort(key=lambdaliteral,self=self:[id(term)forterminliteral[self.slice_group_by]])passdefkey(self,result):# return the grouping key of a resultreturnlist(result[:len(result)-self.arity])defreset(self):self._value=0@propertydefvalue(self):returnself._valuedeffact(self,k):returnk+[pyEngine.Const(self.value)]classSum_aggregate(Aggregate):""" represents sum(Y, for_each=(Z,T))"""required_kw=('Y','for_each')defadd(self,row):self._value+=row[-self.arity].idclassLen_aggregate(Aggregate):""" represents len(X)"""required_kw=('Y')defadd(self,row):self._value+=1classConcat_aggregate(Aggregate):""" represents concat(Y, order_by=(Z1,Z2), sep=sep)"""required_kw=('Y','order_by','sep')defreset(self):self._value=[]defadd(self,row):self._value.append(row[-self.arity].id)@propertydefvalue(self):returnself.sep.join(self._value)classMin_aggregate(Aggregate):""" represents min(Y, order_by=(Z,T))"""required_kw=('Y','order_by')defreset(self):self._value=Nonedefadd(self,row):self._value=row[-self.arity].idifself._valueisNoneelseself._valueclassMax_aggregate(Min_aggregate):""" represents max(Y, order_by=(Z,T))"""def__init__(self,*args,**kwargs):Min_aggregate.__init__(self,*args,**kwargs)forainself.order_by:a._pyD_negated=not(a._pyD_negated)classRank_aggregate(Aggregate):""" represents rank(for_each=(Z), order_by(T))"""required_kw=('for_each','order_by')defreset(self):self.count=0self._value=Nonedefadd(self,row):# retain the value if (X,) == (Z,)ifrow[self.slice_group_by]==row[self.slice_for_each]:self._value=list(row[self.slice_group_by])+[pyEngine.Const(self.count),]returnself._valueself.count+=1deffact(self,k):returnself._valueclassRunning_sum(Rank_aggregate):""" represents running_sum(Y, for_each=(Z), order_by(T)"""required_kw=('Y','for_each','order_by')defadd(self,row):self.count+=row[self.to_add].id# TODOifrow[:self.to_add]==row[self.slice_for_each]:self._value=list(row[:self.to_add])+[pyEngine.Const(self.count),]returnself._value