def visit_Assign(self, node): """ Flatten an assignment. The only assignments we should see are Qubits, measurements, and runtime computations. If we see a measurement, we need to schedule the pulse (the RHS). A runtime computation is passed through as an opaque STORE command. """ if not isinstance(node.value, ast.Call): NodeError.error_msg(node, "Unexpected assignment [%s]" % ast2str(node)) if hasattr(node, 'qgl2_type'): if node.qgl2_type == 'qbit': return node elif node.qgl2_type == 'measurement': new_node = ast.Expr(value=node.value) new_node.qgl2_type = 'measurement' pyqgl2.ast_util.copy_all_loc(new_node, node) return new_node elif node.qgl2_type == 'runtime_call': # put the runtime_call in a STORE command new_node = expr2ast('Store()') # first argument is the STORE destination # TODO we want to re-write the target as an address target_str = ast2str(node.targets[0]).strip() new_node.value.args.append(ast.Str(s=target_str)) # second argument is the str() representation # of the runtime call call_str = ast2str(node.value).strip() new_node.value.args.append(ast.Str(s=call_str)) new_node.qgl2_type = 'runtime_call' pyqgl2.ast_util.copy_all_loc(new_node, node, recurse=True) return new_node return node
def read_import(self, path): """ Recursively read the imports from the module at the given path """ # TODO: error/warning/diagnostics if path in self.path2ast: return self.path2ast[path] # TODO: this doesn't do anything graceful if the file # can't be opened, or doesn't exist, or anything else goes # wrong. We just assume that Python will raise an exception # that includes a useful error message. FIXME we should # be more proactive about making sure that the user # gets the info necessary to diagnose the problem. # try: fin = open(path, 'r') text = fin.read() fin.close() except BaseException as exc: NodeError.fatal_msg(None, 'cannot open [%s]: %s' % (path, str(exc))) return None try: return self.read_import_str(text, path) except BaseException as exc: NodeError.fatal_msg( None, 'failed to import [%s]: %s %s' % (path, type(exc), exc)) return None
def visit_For(self, node): """ Discover loop variables. TODO: this is incomplete; we just assume that loop variables are all classical. We don't attempt to infer anything about the iterator. """ for subnode in ast.walk(node.target): if isinstance(subnode, ast.Attribute): # This is a fatal error and we don't want to confuse # ourselves by trying to process the ast.Name # nodes beneath # name_text = pyqgl2.importer.collapse_name(subnode) NodeError.fatal_msg(subnode, ('loop var [%s] is not local' % name_text)) elif isinstance(subnode, ast.Name): name = subnode.id # Warn the user if they're doing something that's # likely to provoke an error # if self.name_is_in_lscope(name): NodeError.warning_msg( subnode, ('loop var [%s] hides sym in outer scope' % name)) DebugMsg.log('FOR (%s)' % name) self.add_type_binding(subnode, name, QGL2.CLASSICAL) self.visit_body(node.body) self.visit_body(node.orelse)
def read_import_str(self, text, path='<stdin>', module_name='__main__'): ptree = ast.parse(text, mode='exec') self.path2ast[path] = ptree # label each node with the name of the input file; # this will make error messages that reference these # notes much more readable # for node in ast.walk(ptree): node.qgl_fname = path node.qgl_modname = module_name # The preprocessor will ignore any imports that are not # at the "top level" (imports that happen conditionally, # or when a function is executed for the first time, etc) # because it can't figure out if/when these imports would # occur, and it only understands imports that occur before # the execution of any other statements of the program. # # Therefore warn the programmer that any such detected # imports will be ignored. # # TODO: we don't make any attempt to find calls to # __import__() or importlib.import_module(). The # preprocessor always ignores these, without warning. # for node in ast.walk(ptree): if ((isinstance(node, ast.Import) or isinstance(node, ast.ImportFrom)) and (node.col_offset != 0)): NodeError.warning_msg( node, ('conditional/runtime import [%s] ignored by pyqgl2' % pyqgl2.ast_util.ast2str(node).strip())) # Populate the namespace # namespace = NameSpace(path, ptree=ptree) self.path2namespace[path] = namespace for stmnt in ptree.body: if isinstance(stmnt, ast.FunctionDef): self.add_function(namespace, stmnt.name, stmnt) elif isinstance(stmnt, ast.Import): # print('NN ADDING import %s' % ast.dump(stmnt)) self.add_import_as(namespace, stmnt) elif isinstance(stmnt, ast.ImportFrom): # print('NN ADDING import-from %s' % ast.dump(stmnt)) self.add_from_as(namespace, stmnt.module, stmnt) # We're not doing module-level variables right now; no globals """ elif isinstance(stmnt, ast.Assign): print('NN ASSIGN %s' % ast.dump(stmnt)) """ # print('NN NAMESPACE %s' % str(self.path2namespace)) return self.path2ast[path]
def visit_Assign(self, node): # FIXME: can't handle nested tuples properly # For now we're not even going to try. if not isinstance(node.targets[0], ast.Name): NodeError.warning_msg(node, 'tuple returns not supported yet') self.generic_visit(node) return target = node.targets[0] print('CP target0: %s' % ast.dump(target)) value = node.value name = target.id # TODO: what if the lval is an array or dict expression? # Need to sort out what's referenced by the lval. if isinstance(value, ast.Str): print('CP looks like a str assignment %s' % ast.dump(node)) elif isinstance(value, ast.Num): print('CP looks like a num assignment %s' % ast.dump(node)) elif isinstance(value, ast.List): print('CP looks like a list assignment %s' % ast.dump(node)) elif isinstance(value, ast.Call): print('CP looks like a call assignment %s' % ast.dump(node)) self.generic_visit(node)
def single_sequence(node, func_name, importer, setup=None): """ Create a function that encapsulates the QGL code (for a single sequence) from the given AST node, which is presumed to already be fully pre-processed. TODO: we don't test that the node is fully pre-processed. TODO: each step of the preprocessor should mark the nodes so that we know whether or not they've been processed. """ builder = SingleSequence(importer) if builder.find_sequence(node) and builder.find_imports(node): code = builder.emit_function(func_name, setup=setup) NodeError.diag_msg(node, 'generated code:\n#start\n%s\n#end code' % code) # TODO: we might want to pass in elements of the local scope scratch_scope = dict() eval(compile(code, '<none>', mode='exec'), globals(), scratch_scope) return scratch_scope[func_name] else: NodeError.fatal_msg(node, 'find_sequence failed: not a single sequence') return None
def add_local_var(self, name, ptree): if not self.check_dups(name, 'local-variable'): NodeError.warning_msg(ptree, 'redefinition of variable [%s]' % name) else: self.order_added.append(('V', ptree)) self.local_vars[name] = ptree
def add_local_func(self, name, ptree): if not self.check_dups(name, 'local-function'): NodeError.warning_msg(ptree, 'redefinition of function [%s]' % name) else: self.order_added.append(('D', ptree)) self.local_defs[name] = ptree
def visit_ExceptHandler(self, node): name = node.name if self.name_is_in_lscope(name): NodeError.warn_msg( node, ('exception var [%s] hides sym in outer scope' % name)) # assume all exceptions are classical self.add_type_binding(subnode, subnode.id, QGL2.CLASSICAL) pass
def find_imports(self, node): '''Fill in self.stub_imports with all the per module imports needed''' default_namespace = node.qgl_fname for subnode in ast.walk(node): if (isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Name)): funcname = subnode.func.id # QRegister calls will be stripped by find_sequences, so skip if funcname == 'QRegister': continue # If we created a node without an qgl_fname, # then use the default namespace instead. # FIXME: This is a hack, but it will work for now. # if not hasattr(subnode, 'qgl_fname'): namespace = default_namespace else: namespace = subnode.qgl_fname if hasattr(subnode, 'qgl_implicit_import'): (sym_name, module_name, orig_name) = \ subnode.qgl_implicit_import else: fdef = self.importer.resolve_sym(namespace, funcname) if not fdef: NodeError.error_msg( subnode, 'cannot find import info for [%s]' % funcname) return False elif not fdef.qgl_stub_import: NodeError.error_msg(subnode, 'not a stub: [%s]' % funcname) return False (sym_name, module_name, orig_name) = fdef.qgl_stub_import if orig_name: import_str = '%s as %s' % (orig_name, sym_name) else: import_str = sym_name if module_name not in self.stub_imports: self.stub_imports[module_name] = set() self.stub_imports[module_name].add(import_str) return True
def check_conflicts(self, node): all_seen = set() for refs in self.qbit_sets.values(): if not refs.isdisjoint(all_seen): conflict = refs.intersection(all_seen) NodeError.error_msg( node, '%s appear in multiple concurrent statements' % str(', '.join(list(conflict)))) all_seen.update(refs) return all_seen
def do_concur(self, body): for stmnt in body: if not is_seq(stmnt): # TODO: this error message is not helpful NodeError.fatal_msg(stmnt, 'expected a "with seq" block') return else: # TODO: The grouper should annotate the seq statement # so we don't have to find the qbits again. # qbits = find_all_channels(stmnt) if not qbits: print('XXN body\n%s' % ast2str(stmnt)) self.do_seq(qbits, stmnt.body)
def add_import_as(self, namespace, stmnt): namespace.native_import(pyqgl2.ast_util.ast2str(stmnt), stmnt) namespace.add_import_as_stmnt(stmnt) for imp in stmnt.names: subpath = resolve_path(imp.name) if not subpath: NodeError.warning_msg(stmnt, 'path to [%s] not found' % imp.name) elif is_system_file(subpath): continue else: namespace.add_import_as(imp.name, imp.asname) self.read_import(subpath)
def do_lval(self, lval): assigned_names, _dotted, _arrays = self.name_finder.find_names(lval) for name in assigned_names: if name not in self.local_names: self.local_names[name] = self.nesting_depth elif self.local_names[name] > self.nesting_depth: # we permit the nest depth to decrease, but # not increase # self.local_names[name] = self.nesting_depth elif self.local_names[name] == CheckScoping.BUILTIN_SCOPE: # If the symbol is a builtin, then warn that # it's being reassigned # NodeError.warning_msg( lval, 'reassignment of a builtin symbol [%s]' % name)
def collapse_name(node): """ Given the AST for a symbol reference, collapse it back into the original reference string Example, instead of the AST Attribute(Name(id='x'), addr='y') return 'x.y' """ if isinstance(node, ast.Name): return node.id elif isinstance(node, ast.Attribute): return collapse_name(node.value) + '.' + node.attr else: # TODO: handle this more gracefully NodeError.warning_msg( node, 'unexpected failure to resolve [%s]' % ast.dump(node)) return None
def visit_With(self, node): """ If the node is a "with concur", then add a sequence for each "with seq" block in its body, with a WAIT preamble and SYNC(?) postamble. All other kinds of "with" blocks cause an error. """ if is_concur(node): self.do_concur(node.body) # TODO: if there's an orelse, or anything like # that, then gripe here. We can't handle that yet. else: # TODO: this error message is not helpful NodeError.fatal_msg(node, 'Unexpected with block')
def visit_FunctionDef(self, node): """ The usual entry point: insert the names used by the formal parameters, and then process the body """ for arg in node.args.args: name = arg.arg if name not in self.local_names: self.local_names[name] = CheckScoping.PARAM_SCOPE elif self.local_names[name] == CheckScoping.MODULE_SCOPE: NodeError.warning_msg( node, 'formal parameter masks a module symbol [%s]' % name) else: NodeError.warning_msg( node, 'formal parameter masks a builtin symbol [%s]' % name) self.do_body(node.body)
def get_sequence_function(node, func_name, importer, allocated_qregs, intermediate_fout=None, saveOutput=False, filename=None, setup=None): """ Create a function that encapsulates the QGL code from the given AST node, which is presumed to already be fully pre-processed. TODO: we don't test that the node is fully pre-processed. TODO: each step of the preprocessor should mark the nodes so that we know whether or not they've been processed. """ builder = SequenceExtractor(importer, allocated_qregs) builder.find_sequences(node) builder.find_imports(node) code = builder.emit_function(func_name, setup) if intermediate_fout: print(('#start function\n%s\n#end function' % code), file=intermediate_fout, flush=True) if saveOutput and filename: newf = os.path.abspath(filename[:-3] + "qgl1.py") with open(newf, 'w') as compiledFile: compiledFile.write(code) print("Saved compiled code to %s" % newf) NodeError.diag_msg(node, 'generated code:\n#start\n%s\n#end code' % code) # TODO: we might want to pass in elements of the local scope scratch_scope = dict() eval(compile(code, '<none>', mode='exec'), globals(), scratch_scope) NodeError.halt_on_error() return scratch_scope[func_name]
def find_imports(self, node): default_namespace = node.qgl_fname for subnode in ast.walk(node): if (isinstance(subnode, ast.Call) and isinstance(subnode.func, ast.Name)): funcname = subnode.func.id # If we created a node without an qgl_fname, # then use the default namespace instead. # FIXME: This is a hack, but it will work for now. # if not hasattr(subnode, 'qgl_fname'): namespace = default_namespace else: namespace = subnode.qgl_fname fdef = self.importer.resolve_sym(namespace, funcname) if not fdef: print('ERROR %s funcname' % funcname) NodeError.error_msg( subnode, 'cannot find import info for [%s]' % funcname) elif not fdef.qgl_stub_import: NodeError.error_msg(subnode, 'not a stub: [%s]' % funcname) else: # print('FI AST %s' % ast.dump(fdef)) (sym_name, module_name, orig_name) = fdef.qgl_stub_import if orig_name: import_str = '%s as %s' % (orig_name, sym_name) else: import_str = sym_name if module_name not in self.stub_imports: self.stub_imports[module_name] = set() self.stub_imports[module_name].add(import_str) return True
def expand_arg(self, arg): ''' Expands a single argument to a stub call. QRegisters are expanded to a list of constituent Qubits. QRegister subscripts are similar, except that the slice selects which Qubits to return. So, given a = QRegister(2) b = QRegister(1) we do (in AST shorthand): expand_arg("a") -> ["QBIT_1", "QBIT_2"] expand_arg("a[1]") -> ["QBIT_2"] ''' expanded_args = [] if isinstance(arg, ast.Name) and arg.id in self.allocated_qregs: qreg = self.allocated_qregs[arg.id] # add an argument for each constituent qubit in the QRegister for n in range(len(qreg)): new_arg = ast.Name(id=qreg.use_name(n), ctx=ast.Load()) expanded_args.append(new_arg) elif (isinstance(arg, ast.Subscript) and arg.value.id in self.allocated_qregs): # add an argument for the subset of qubits referred to # by the QReference # eval the subscript to extract the slice qreg = self.allocated_qregs[arg.value.id] try: qref = eval(ast2str(arg), None, self.allocated_qregs) except: NodeError.error_msg( arg, "Error evaluating QReference [%s]" % ast2str(arg)) # convert the slice into a list of indices idx = range(len(qreg))[qref.idx] if not hasattr(idx, '__iter__'): idx = (idx, ) for n in idx: new_arg = ast.Name(id=qreg.use_name(n), ctx=ast.Load()) expanded_args.append(new_arg) else: # don't expand it expanded_args.append(arg) return expanded_args
def add_type_binding(self, node, name, name_type): """ Add a binding between a name and a type, in the local context. Gripe and do nothing if there is already a binding for this name in either the parameter or local scope, and it disagrees with the requested binding. The node parameter is used only to generate error messages that can be traced back to the original code, since the node contains the file and line number of the code prior to any transformation """ if name in self.parameter_names: old_type = self.parameter_names[name] if old_type != name_type: NodeError.error_msg(node, ('parameter type changed %s -> %s' % (old_type, name_type))) elif name in self.local_names: old_type = self.local_names[name] if old_type != name_type: NodeError.error_msg( node, 'type changed %s -> %s' % (old_type, name_type)) else: NodeError.diag_msg(node, 'add type %s -> %s' % (name, name_type)) self.local_names[name] = name_type
def visit_Name(self, node): """ Process an ast.Name node for a symbol reference (not a symbol assignment, which should be done in do_lval). If we find a name that doesn't have a binding in the current scope, or that was defined at a higher nesting level than we're currently in, then warn the user that this might be an error. (these aren't always errors, but they're strange enough that they're worth calling out) """ name = node.id if name not in self.local_names: NodeError.warning_msg(node, 'potentially undefined symbol [%s]' % name) elif self.local_names[name] > self.nesting_depth: NodeError.warning_msg( node, 'symbol [%s] referenced outside defining block' % name)
def make_cgoto_call(self, label, node, cmp_operator, cmp_addr, value): """ Create a conditional goto call """ if isinstance(cmp_operator, ast.Eq): cmp_ast = expr2ast('CmpEq("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.NotEq): cmp_ast = expr2ast('CmpNeq("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.Gt): cmp_ast = expr2ast('CmpGt("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.Lt): cmp_ast = expr2ast('CmpLt("%s", %s)' % (cmp_addr, str(value))) else: NodeError.error_msg( node, 'Unallowed comparison operator [%s]' % ast2str(node)) return None label_ast = expr2ast('Goto(BlockLabel(\'%s\'))' % label) pyqgl2.ast_util.copy_all_loc(cmp_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(label_ast, node, recurse=True) return list([cmp_ast, label_ast])
def native_load(self, node=None): """ Exec the entire text of the file, so that the native_globals will be properly initialized """ try: fin = open(self.path, 'r') text = fin.read() fin.close() except BaseException as exc: NodeError.error_msg( None, 'read of [%s] failed: %s' % (self.path, str(exc))) return False try: exec(text, self.native_globals) return True except BaseException as exc: NodeError.error_msg( None, 'import of [%s] failed: %s' % (self.path, str(exc))) return False return True
def native_import(self, text, node): """ Do a "native import", updating self.native_globals with the results. This can be ugly if the import executes arbitrary code (i.e. prints things on the screen, or futzes with something else). The text must be an import statement, or sequence of import statements (ast2str turns a single statement with a list of symbol clauses into a list of statements) i.e. "from foo import bar as baz" or "from whatever import *" or "import something" The node is used to create meaningful diagnostic or error messages, and must be provided. Returns True if successful, False otherwise. """ # A hack to avoid doing imports on "synthesized" imports # that don't have line numbers in the original source code # if (not node) or (not hasattr(node, 'lineno')): return try: exec(text, self.native_globals) return True except BaseException as exc: if node: caller_fname = node.qgl_fname else: caller_fname = '<unknown>' NodeError.error_msg( node, 'in %s [%s] failed: %s' % (caller_fname, text, str(exc))) return False
def visit_With(self, node): """ TODO: this is incomplete; we just assume that with-as variables are all classical. We don't attempt to infer anything about their type. (This is likely to be true in most cases, however) """ for item in node.items: if not item.optional_vars: continue for subnode in ast.walk(item.optional_vars): if isinstance(subnode, ast.Attribute): # This is a fatal error and we don't want to confuse # ourselves by trying to process the ast.Name # nodes beneath # name_text = pyqgl2.importer.collapse_name(subnode) NodeError.fatal_msg( subnode, ('with-as var [%s] is not local' % name_text)) elif isinstance(subnode, ast.Name): name = subnode.id DebugMsg.log('GOT WITH (%s)' % name) # Warn the user if they're doing something that's # likely to provoke an error # if self.name_is_in_lscope(name): NodeError.warn_msg( subnode, ('with-as var [%s] hides sym in outer scope' % name)) self.add_type_binding(subnode, subnode.id, QGL2.CLASSICAL) self.visit_body(node.body)
def find_sys_path_prefix(): """ Find the prefix of the path to the "system" libraries, which we want to exclude from importing and searching for QGL stuff. Where these are located depends on where Python was installed on the local system (and which version of Python, etc). The heuristic we use is to search through the include path for 'ast' and assume that the path we has the prefix we want to omit. This is a hack. In Python3, modules can be loaded directly out of zip files, in which case they don't have a "file". We use 'ast' because it typically does, but there's no guarantee that this will work in all cases. """ global SYS_PATH_PREFIX if SYS_PATH_PREFIX: return SYS_PATH_PREFIX try: path = inspect.getfile(ast) except TypeError as exc: NodeError.fatal_msg(None, 'cannot find path to system modules') relpath = os.path.relpath(path) path_prefix = relpath.rpartition(os.sep)[0] SYS_PATH_PREFIX = path_prefix return path_prefix
def find_sequences(self, node): ''' Input AST node is the main function definition. Strips out QRegister creation statements and builds a list of corresponding Qubit creation statements. Converts calls on QRegisters into calls on Qubits.''' if not isinstance(node, ast.FunctionDef): NodeError.fatal_msg(node, 'not a function definition') return False self.qbits = self.qbits_from_qregs(self.allocated_qregs) for q in self.qbits: # Using QubitFactory here means the qubit must already exist # If did Channels.Qubit, we'd be creating it new; but it wouldn't be in the ChannelLibrary stmnt = ast.parse("QBIT_{0} = QubitFactory('q{0}')".format(q)) self.qbit_creates.append(stmnt) lineNo = -1 while lineNo + 1 < len(node.body): lineNo += 1 # print("Line %d of %d" % (lineNo+1, len(node.body))) stmnt = node.body[lineNo] # print("Looking at stmnt %s" % stmnt) if is_qbit_create(stmnt): # drop it continue elif isinstance(stmnt, ast.Expr): # expand calls on QRegisters into calls on Qubits if (hasattr(stmnt, 'qgl2_type') and (stmnt.qgl2_type == 'stub' or stmnt.qgl2_type == 'measurement')): new_stmnts = self.expand_qreg_call(stmnt) self.sequence.extend(new_stmnts) else: self.sequence.append(stmnt) else: NodeError.error_msg(stmnt, 'orphan statement %s' % ast.dump(stmnt)) # print("Seqs: %s" % self.sequences) if not self.sequence: NodeError.warning_msg(node, "No qubit operations discovered") return False return True
def find_stub_import(self, decnode, funcname): """ Find the import info encoded in a stub declaration TODO: doesn't do anything useful with errors/bad input """ if not isinstance(decnode, ast.Call): NodeError.fatal_msg( decnode, 'bad use of find_stub_import [%s]' % ast.dump(decnode)) args = decnode.args n_args = len(args) from_name = None orig_name = None if n_args == 0: # TODO: should complain pass if n_args > 0: if not isinstance(args[0], ast.Str): NodeError.error_msg( decnode, 'qgl2stub arg[0] must be str [%s]' % ast.dump(args[0])) else: from_name = args[0].s if n_args > 1: if not isinstance(args[1], ast.Str): NodeError.error_msg( decnode, 'qgl2stub arg[1] must be str [%s]' % ast.dump(args[1])) else: orig_name = args[1].s if n_args > 2: # TODO: should complain pass return (funcname, from_name, orig_name)
def factory(node, local_vars): ''' Evaluates a ast.Call node of a QRegister and returns its value. local_vars is a dictionary of symbol -> value bindings ''' if not is_qbit_create(node): NodeError.error_msg( node, "Attempted to create a QRegister from an invalid AST node [%s]." % ast2str(node)) # convert args into values arg_values = [] for arg in node.value.args: if isinstance(arg, ast.Num): arg_values.append(arg.n) elif isinstance(arg, ast.Str): arg_values.append(arg.s) elif isinstance(arg, ast.Name) and arg.id in local_vars: arg_values.append(local_vars[arg.id]) elif is_qbit_subscript(arg, local_vars): # evaluate the subscript to extract the referenced qubits parent_qreg = local_vars[arg.value.id] try: arg_val = eval(ast2str(arg), None, local_vars) except: NodeError.error_msg( node, "Unhandled qreg subscript [%s]" % ast2str(arg)) sub_qubits = parent_qreg.qubits[arg_val.idx] if hasattr(sub_qubits, '__iter__'): arg_values.extend("q" + str(n) for n in sub_qubits) else: arg_values.append("q" + str(sub_qubits)) else: NodeError.error_msg( node, "Unhandled argument to QRegister [%s]" % ast2str(arg)) return QRegister(*arg_values)