def compile_dsl_module(dsl_module, dsl_locals=None, dsl_globals=None): """ Returns something that can be evaluated. """ # It's a module compilation, so create a new namespace "context". if dsl_locals is None: dsl_locals = {} dsl_locals = DslNamespace(dsl_locals) if dsl_globals is None: dsl_globals = {} dsl_globals = DslNamespace(dsl_globals) # Can't do much with an empty module. if len(dsl_module.body) == 0: raise DslSyntaxError('empty module', node=dsl_module.node) function_defs, expressions = extract_defs_and_exprs( dsl_module, dsl_globals) # Handle different combinations of functions and module level expressions in different ways. # Todo: Simplify this, but support library files first? # Can't meaningfully evaluate more than one expression (since assignments are not supported). if len(expressions) > 1: raise DslSyntaxError('more than one expression in module', node=expressions[1].node) # Can't meaningfully evaluate more than one function def without a module level expression. elif len(expressions) == 0 and len(function_defs) > 1: second_def = function_defs[1] raise DslSyntaxError( 'more than one function def in module without an expression', '"def %s"' % second_def.name, node=function_defs[1].node) # If it's just a module with one function, then return the function def. elif len(expressions) == 0 and len(function_defs) == 1: return function_defs[0] # If there is one expression, reduce it with the function defs that it calls. else: assert len(expressions) == 1 dsl_expr = expressions[0] # assert isinstance(dsl_expr, DslExpression), dsl_expr # Compile the module for a single threaded recursive operation (faster but not distributed, # so also limited in space and perhaps time). For smaller computations only. dsl_obj = dsl_expr.substitute_names(dsl_globals.combine(dsl_locals)) return dsl_obj
def parse(self, dsl_source, filename='<unknown>'): """ Creates a DSL Module object from a DSL source text. """ if not isinstance(dsl_source, six.string_types): raise DslSyntaxError("Can't dsl_parse non-string object", dsl_source) # assert isinstance(dsl_source, six.string_types) try: # Parse as Python source code, into a Python abstract syntax tree. ast_module = ast.parse(dsl_source, filename=filename, mode='exec') except SyntaxError as e: raise DslSyntaxError("DSL source code is not valid Python code: {}".format(dsl_source), e) # Generate Quant DSL from Python AST. return self.visitAstNode(ast_module)
def visitCall(self, node): """ Visitor method for ast.Call. Returns a built-in DSL expression, or a DSL FunctionCall if the name refers to a user defined function. """ if node.keywords: raise DslSyntaxError( "Calling with keywords is not currently supported (positional args only)." ) if node.starargs: raise DslSyntaxError( "Calling with starargs is not currently supported (positional args only)." ) if node.kwargs: raise DslSyntaxError( "Calling with kwargs is not currently supported (positional args only)." ) # Collect the call arg expressions (whose values will be passed into the call when it is made). call_arg_exprs = [self.visitAstNode(arg) for arg in node.args] # Check the called node is an ast.Name. called_node = node.func # assert isinstance(called_node, ast.Name) called_node_name = called_node.id # Construct a DSL object for this call. try: # Resolve the name with a new instance of a DSL class. dsl_class = self.dsl_classes[called_node_name] except KeyError: # Resolve as a FunctionCall, and expect # to resolve the name to a function def later. dsl_name_class = self.dsl_classes['Name'] dsl_args = [ dsl_name_class(called_node_name, node=called_node), call_arg_exprs ] return self.dsl_classes['FunctionCall'](node=node, *dsl_args) else: dsl_object_class = self.dsl_classes['DslObject'] assert issubclass(dsl_class, dsl_object_class), dsl_class return dsl_class(node=node, *call_arg_exprs)
def visitUnaryOp(self, node): """ Visitor method for ast.UnaryOp. Returns a specific DSL UnaryOp object (e.g UnarySub), along with the operand. """ # assert isinstance(node, ast.UnaryOp) args = [self.visitAstNode(node.operand)] if isinstance(node.op, ast.USub): dsl_class = self.dsl_classes['UnarySub'] else: raise DslSyntaxError("Unsupported unary operator token: %s" % node.op) return dsl_class(node=node, *args)
def extract_defs_and_exprs(dsl_module, dsl_globals): # Pick out the expressions and function defs from the module body. function_defs = [] expressions = [] for dsl_obj in dsl_module.body: if isinstance(dsl_obj, FunctionDef): function_defs.append(dsl_obj) elif isinstance(dsl_obj, DslExpression): expressions.append(dsl_obj) else: raise DslSyntaxError("'%s' not allowed in module" % type(dsl_obj), dsl_obj, node=dsl_obj.node) return function_defs, expressions
def find_module_path(name): # Find path. if six.PY2: try: module = import_module(name) except SyntaxError as e: raise DslSyntaxError("Can't import {}: {}".format(name, e)) path = module.__file__.strip('c') elif six.PY3: if find_loader: loader = find_loader(name) path = loader.path else: spec = find_spec(name) path = spec.origin assert path.endswith('.py'), path return path
def visitBoolOp(self, node): """ Visitor method for ast.BoolOp. Returns a specific DSL BoolOp object (e.g And), along with the left and right operands. """ # assert isinstance(node, ast.BoolOp) type_map = { ast.And: self.dsl_classes['And'], ast.Or: self.dsl_classes['Or'], } try: dsl_class = type_map[type(node.op)] except KeyError: raise DslSyntaxError("Unsupported boolean operator token: %s" % node.op) else: values = [self.visitAstNode(v) for v in node.values] args = [values] return dsl_class(node=node, *args)
def visitAstNode(self, node): """ Identifies which "visit" method to call, according to type of node being visited. Returns the result of calling the identified "visit" method. """ # assert isinstance(node, ast.AST) # Construct the "visit" method name. dsl_element_name = node.__class__.__name__ method_name = 'visit' + dsl_element_name # Try to get_quantdsl_app the "visit" method object. try: method = getattr(self, method_name) except AttributeError: msg = "element '%s' is not supported (visit method '%s' not found on parser): %s" % ( dsl_element_name, method_name, node) raise DslSyntaxError(msg) # Call the "visit" method object, and return the result of visiting the node. return method(node=node)
def visitBinOp(self, node): """ Visitor method for ast.BinOp. Returns a specific DSL BinOp object (e.g Add), along with the left and right operands. """ # assert isinstance(node, ast.BinOp) type_map = { ast.Add: self.dsl_classes['Add'], ast.Sub: self.dsl_classes['Sub'], ast.Mult: self.dsl_classes['Mult'], ast.Div: self.dsl_classes['Div'], ast.Pow: self.dsl_classes['Pow'], ast.Mod: self.dsl_classes['Mod'], ast.FloorDiv: self.dsl_classes['FloorDiv'], } try: dsl_class = type_map[type(node.op)] except KeyError: raise DslSyntaxError("Unsupported binary operator token", node.op, node=node) args = [self.visitAstNode(node.left), self.visitAstNode(node.right)] return dsl_class(node=node, *args)