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 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 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 find_sequence(self, node): if not isinstance(node, ast.FunctionDef): NodeError.fatal_msg(node, 'not a function definition') return False self.qbits = find_all_channels(node) if len(self.qbits) == 0: NodeError.error_msg(node, 'no channels found') return False else: NodeError.diag_msg(node, "Found channels %s" % self.qbits) 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) assignment = self.is_qbit_create(stmnt) if assignment: self.qbit_creates.append(assignment) continue elif is_concur(stmnt): # print("Found concur at line %d: %s" % (lineNo+1,stmnt)) for s in stmnt.body: if is_seq(s): # print("Found with seq for qbits %s: %s" % (s.qgl_chan_list, ast2str(s))) #print("With seq next at line %d: %s" % (lineNo+1,s)) if str(s.qgl_chan_list) not in self.sequences: self.sequences[str(s.qgl_chan_list)] = list() thisSeq = self.sequences[str(s.qgl_chan_list)] # print("Append body %s" % s.body) # for s2 in s.body: # print(ast2str(s2)) thisSeq += s.body #print("lineNo now %d" % lineNo) else: NodeError.error_msg( s, "Not seq next at line %d: %s" % (lineNo + 1, s)) elif isinstance(stmnt, ast.Expr): if len(self.qbits) == 1: # print("Append expr %s to sequence for %s" % (ast2str(stmnt), self.qbits)) if len(self.sequences) == 0: self.sequences[list(self.qbits)[0]] = list() self.sequences[list(self.qbits)[0]].append(stmnt) else: NodeError.error_msg( stmnt, 'orphan statement %s' % ast.dump(stmnt)) else: NodeError.error_msg(stmnt, 'orphan statement %s' % ast.dump(stmnt)) # print("Seqs: %s" % self.sequences) return True
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 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 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 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 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 compile_function(filename, main_name=None, toplevel_bindings=None, saveOutput=False, intermediate_output=None): NodeError.reset() print('\n\nCOMPILING [%s] main %s' % (filename, main_name if main_name else '(default)')) # Use whether intermediate_output is None to decide # whether to call printout blocks at all # Old code set intermediate_output to /dev/null if intermediate_output: try: intermediate_fout = open(intermediate_output, 'w') except BaseException as exc: NodeError.fatal_msg(None, ('cannot save intermediate output in [%s]' % intermediate_output)) else: intermediate_fout = None # Process imports in the input file, and find the main. # If there's no main, then bail out right away. try: rel_path = os.path.relpath(filename) filename = rel_path except Exception as e: # If that wasn't a good path, give up immediately NodeError.error_msg( None, "Failed to make relpath from %s: %s" % (filename, e)) NodeError.halt_on_error() print('%s: CALLING IMPORTER' % datetime.now()) importer = NameSpaces(filename, main_name) if not importer.qglmain: NodeError.fatal_msg(None, 'no qglmain function found') NodeError.halt_on_error() ptree = importer.qglmain if intermediate_output: ast_text_orig = pyqgl2.ast_util.ast2str(ptree) print(('%s: ORIGINAL CODE:\n%s' % (datetime.now(), ast_text_orig)), file=intermediate_fout, flush=True) # When QGL2 flattens various kinds of control flow and runtime # computations it emits QGL1 instruction that the user may not # have imported. # # TODO: this is a hack, but the approach of adding these # blindly to the namespace is also a hack. This is a # placeholder until we figure out a cleaner approach. required_imports = [ 'Wait', 'Barrier', 'Goto', 'LoadCmp', 'CmpEq', 'CmpNeq', 'CmpGt', 'CmpLt', 'BlockLabel', 'Store' ] modname = ptree.qgl_fname for symbol in required_imports: if not add_import_from_as(importer, modname, 'qgl2.qgl1', symbol): NodeError.error_msg(ptree, 'Could not import %s' % symbol) NodeError.halt_on_error() ptree1 = ptree # We may need to iterate over the inlining processes a few times, # because inlining may expose new things to inline. # # TODO: as a stopgap, we're going to limit iterations to 20, which # is enough to handle fairly deeply-nested, complex non-recursive # programs. What we do is iterate until we converge (the outcome # stops changing) or we hit this limit. We should attempt at this # point to prove that the expansion is divergent, but we don't # do this, but instead assume the worst if the program is complex # enough to look like it's "probably" divergent. # print('%s: CALLING INLINER' % datetime.now()) MAX_ITERS = 20 for iteration in range(MAX_ITERS): print('%s: ITERATION %d' % (datetime.now(), iteration)) inliner = Inliner(importer) ptree1 = inliner.inline_function(ptree1) NodeError.halt_on_error() if intermediate_output: print(('INLINED CODE (iteration %d):\n%s' % (iteration, pyqgl2.ast_util.ast2str(ptree1))), file=intermediate_fout, flush=True) if inliner.change_cnt == 0: NodeError.diag_msg( None, ('expansion converged after iteration %d' % iteration)) break if iteration == (MAX_ITERS - 1): NodeError.error_msg( None, ('expansion did not converge after %d iterations' % MAX_ITERS)) # transform passed toplevel_bindings into a local_context dictionary # FIXME: If the qgl2main provides a default for an arg # that is 'missing', then don't count it as missing arg_names = [x.arg for x in ptree1.args.args] if isinstance(toplevel_bindings, tuple): if len(arg_names) != len(toplevel_bindings): NodeError.error_msg( None, 'Invalid number of arguments supplied to qgl2main (got %d, expected %d)' % (len(toplevel_bindings), len(arg_names))) local_context = { name: quickcopy(value) for name, value in zip(arg_names, toplevel_bindings) } elif isinstance(toplevel_bindings, dict): invalid_args = toplevel_bindings.keys() - arg_names if len(invalid_args) > 0: NodeError.error_msg( None, 'Invalid arguments supplied to qgl2main: {}'.format( invalid_args)) missing_args = arg_names - toplevel_bindings.keys() if len(missing_args) > 0: NodeError.error_msg( None, 'Missing arguments for qgl2main: {}'.format(missing_args)) local_context = quickcopy(toplevel_bindings) elif toplevel_bindings: NodeError.error_msg( None, 'Unrecognized type for toplevel_bindings: {}'.format( type(toplevel_bindings))) else: local_context = None NodeError.halt_on_error() evaluator = EvalTransformer(SimpleEvaluator(importer, local_context)) print('%s: CALLING EVALUATOR' % datetime.now()) ptree1 = evaluator.visit(ptree1) NodeError.halt_on_error() if DebugMsg.ACTIVE_LEVEL < 3: print('%s: EVALUATOR RESULT:\n%s' % (datetime.now(), pyqgl2.ast_util.ast2str(ptree1))) # It's very hard to read the intermediate form, before the # QBIT names are added, so we don't save this right now. # print(('EVALUATOR RESULT:\n%s' % pyqgl2.ast_util.ast2str(ptree1)), # file=intermediate_fout, flush=True) # Dump out all the variable bindings, for debugging purposes # # print('EV total state:') # evaluator.print_state() evaluator.replace_bindings(ptree1.body) if DebugMsg.ACTIVE_LEVEL < 3: print('%s: EVALUATOR REBINDINGS:\n%s' % (datetime.now(), pyqgl2.ast_util.ast2str(ptree1))) if intermediate_output: print( ('EVALUATOR + REBINDINGS:\n%s' % pyqgl2.ast_util.ast2str(ptree1)), file=intermediate_fout, flush=True) # base_namespace = importer.path2namespace[filename] # if intermediate_output: # text = base_namespace.pretty_print() # print(('EXPANDED NAMESPACE:\n%s' % text), # file=intermediate_fout, flush=True) new_ptree1 = ptree1 # Try to flatten out repeat, range, ifs flattener = Flattener() print('%s: CALLING FLATTENER' % datetime.now()) new_ptree2 = flattener.visit(new_ptree1) NodeError.halt_on_error() if intermediate_output: print(('%s: FLATTENED CODE:\n%s' % (datetime.now(), pyqgl2.ast_util.ast2str(new_ptree2))), file=intermediate_fout, flush=True) # TODO Is it ever necessary to replace bindings again at this point? # evaluator.replace_bindings(new_ptree2.body) # evaluator.get_state() if intermediate_output: print(('Final qglmain: %s\n' % new_ptree2.name), file=intermediate_fout, flush=True) new_ptree3 = new_ptree2 # Done. Time to generate the QGL1 # Try to guess the proper function name fname = main_name if not fname: if isinstance(ptree, ast.FunctionDef): fname = ptree.name else: fname = "qgl1Main" # Get the QGL1 function that produces the proper sequences print('%s: GENERATING QGL1 SEQUENCE FUNCTION' % datetime.now()) qgl1_main = get_sequence_function(new_ptree3, fname, importer, evaluator.allocated_qbits, intermediate_fout, saveOutput, filename, setup=evaluator.setup()) NodeError.halt_on_error() return qgl1_main