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 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 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 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_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 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 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 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 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 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_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 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)
def find_type_decl(self, node): """ Copied from check_qbit. Both need to be refactored. """ q_args = list() q_return = None if node is None: NodeError.warning_msg(node, 'unexpected None node') return None if not isinstance(node, ast.FunctionDef): NodeError.warning_msg( node, 'expected a FunctionDef, got [%s]' % ast.dump(node)) return None if node.returns: ret = node.returns if isinstance(ret, ast.Name): if ret.id == QGL2.QBIT: q_return = QGL2.QBIT elif ret.id == QGL2.CLASSICAL: q_return = QGL2.CLASSICAL # elif ret.id == QGL2.QBIT_LIST: # q_return = QGL2.QBIT_LIST elif ret.id == QGL2.PULSE: q_return = QGL2.PULSE elif ret.id == QGL2.CONTROL: q_return = QGL2.CONTROL elif ret.id == QGL2.SEQUENCE: q_return = QGL2.SEQUENCE else: NodeError.error_msg( node, 'unsupported return type [%s]' % ret.id) # FIXME: What about kwonlyargs? or storing the defaults? if node.args.args: for arg in node.args.args: # print('>> %s' % ast.dump(arg)) name = arg.arg annotation = arg.annotation if not annotation: q_args.append('%s:%s' % (name, QGL2.CLASSICAL)) elif isinstance(annotation, ast.Name): if annotation.id == QGL2.QBIT: q_args.append('%s:%s' % (name, QGL2.QBIT)) elif annotation.id == QGL2.CLASSICAL: q_args.append('%s:%s' % (name, QGL2.CLASSICAL)) # elif annotation.id == QGL2.QBIT_LIST: # q_args.append('%s:%s' % (name, QGL2.QBIT_LIST)) elif annotation.id == QGL2.PULSE: q_args.append('%s:%s' % (name, QGL2.PULSE)) elif annotation.id == QGL2.CONTROL: q_args.append('%s:%s' % (name, QGL2.CONTROL)) elif annotation.id == QGL2.SEQUENCE: q_args.append('%s:%s' % (name, QGL2.SEQUENCE)) else: NodeError.error_msg( node, ('unsupported parameter annotation [%s]' % annotation.id)) else: NodeError.error_msg( node, 'unsupported parameter annotation [%s]' % ast.dump(annotation)) # print('NN NAME %s (%s) -> %s' % # (node.name, str(q_args), str(q_return))) return (q_args, q_return)
def native_eval(self, expr, local_variables=None, mode='eval'): """ Evaluate the given expr, which may be an expression or a statement represented by an AST node or a text string. If mode is 'eval', then the expr must be an expression, but if it is 'exec' then it may be a statement. If local_variables is not None, it is assumed to reference a dictionary containing local bindings. It should NOT be a reference to the global bindings (either for this namespace, or any other global bindings). Returns (success, value), where success indicates whether the evaluation succeeded or failed, and value is the value of the expression. The process of evaluation the expression may also modify bindings in local_variables, Note that if the evaluation of the expr raises an exception, this exception will be caught and the result will be treated as failure (even if the intent of the expression was to raise an exception). QGL2 doesn't understand exceptions. NOTE: this evaluation is not safe, and may damage the environment of the caller. There is no safeguard against this right now. """ if (not isinstance(expr, str)) and (not isinstance(expr, ast.AST)): print('INVALID EXPR type %s' % str(type(expr))) return False, None # If we get AST, then there are many variations on what # we could get (it could look like an Expr, or an Expression, # or a Module, etc. By converting the AST to a text string, # this removes all of the ambiguity and lets us parse the # program again, in the local context. # # This is inefficient for the computer (to keep going back and # forth between text and parse trees) but efficient for the # implementer. # if isinstance(expr, ast.AST): expr_str = pyqgl2.ast_util.ast2str(expr) else: expr_str = expr try: final_expr = compile(expr_str, '<nofile>', mode=mode) except SyntaxError as exc: print('Syntax error in native_eval: %s' % str(exc)) return False, None except BaseException as exc: print('Error in native_eval: %s' % str(exc)) return False, None try: if local_variables is None: local_variables = dict() # global_variables = dict.copy(self.native_globals) # print('EXPR %s' % expr_str.strip()) val = eval(final_expr, self.native_globals, local_variables) return True, val except BaseException as exc: # If the expr was AST and came from the preprocessor, # try to format the error message accordingly # # Otherwise just attempt to print something meaningful # if isinstance(expr, ast.AST) and hasattr(expr, 'qgl_fname'): NodeError.error_msg( expr, ('ast eval failure [%s]: type %s %s' % (expr_str.strip(), str(type(exc)), str(exc)))) else: print('eval failure [%s]: %s' % (expr_str.strip(), str(exc))) return False, None
def __init__(self, path, qglmain_name=None, text=None): # map from path to AST # self.path2ast = dict() # map from path to NameSpace # self.path2namespace = dict() # The error/warning messages are clearer if we always # use the relpath # if not path or path == '<stdin>': self.base_fname = '<stdin>' else: self.base_fname = os.path.relpath(path) # Reference to the main function; initially None because # we haven't read it in yet # self.qglmain = None if text: self.read_import_str(text, self.base_fname) else: self.read_import(self.base_fname) # TODO: if the user asks for a specific main, then go # back and use it. Don't gripe if the user has already defined # one. Resolve the name with respect to the namespace # of base_fname if qglmain_name: qglmain_def = self.resolve_sym(self.base_fname, qglmain_name) if not qglmain_def: NodeError.error_msg( None, 'no definition for qglmain [%s]' % qglmain_name) elif not qglmain_def.qgl_func: NodeError.error_msg( None, 'qglmain [%s] not declared QGL' % qglmain_name) else: self.qglmain = qglmain_def qglmain_def.qgl_main = True if self.qglmain: NodeError.diag_msg(None, 'using [%s] as qglmain' % self.qglmain.name) else: NodeError.warning_msg(None, 'warning: no qglmain declared or chosen') # This is a hack to make sure that the base file # is read in as a "native import". Since there isn't # an explicit "import" of this file anywhere, we don't # have an AST node that contains the code for this import. # We can't use None, because this importer uses this as # a sentinel value, so we use self.qgl2main. This is # bogus -- we should make a fake node for this purpose # FIXME fin = open(self.base_fname, 'r') text = fin.read() fin.close() namespace = self.path2namespace[self.base_fname] namespace.native_import(text, self.qglmain)
def assign_simple(self, node): target = node.targets[0] value = node.value print('XX qbit_scope %s %s' % (str(self._qbit_scope()), ast.dump(node))) if not isinstance(target, ast.Name): return node if target.id in self._qbit_local(): msg = 'reassignment of qbit \'%s\' forbidden' % target.id self.error_msg(node, msg) return node if (target.id + ':qbit') in self._qbit_scope(): msg = 'reassignment of qbit parameter \'%s\' forbidden' % target.id self.error_msg(node, msg) return node print('XX qbit_scope %s %s' % (str(self._qbit_scope()), ast.dump(node))) if isinstance(value, ast.Name): # print('CHECKING %s' % str(self._qbit_scope())) if (value.id + ':qbit') in self._qbit_scope(): self.warning_msg( node, 'aliasing qbit parameter \'%s\' as \'%s\'' % (value.id, target.id)) self._extend_local(target.id) elif value.id in self._qbit_local(): self.warning_msg( node, 'aliasing local qbit \'%s\' as \'%s\'' % (value.id, target.id)) self._extend_local(target.id) elif isinstance(value, ast.Call): func_name = pyqgl2.importer.collapse_name(value.func) func_def = self.importer.resolve_sym(value.qgl_fname, func_name) # If we can't find the function definition, or it's not declared # to be QGL, then we can't handle it. Return immediately. # if not func_def: NodeError.error_msg(value, 'function [%s] not defined' % func_name) return node if func_def.returns: rtype = func_def.returns if (isinstance(rtype, ast.Name) and rtype.id == QGL2.QBIT): # Not sure what happens if we get here: we might # have a wandering variable that we know is a qbit, # but we never know which one. # print('XX EXTENDING LOCAL (%s)' % target.id) self._extend_local(target.id) target.qgl_is_qbit = True if not func_def.qgl_func: # TODO: this seems bogus. We should be able to call # out to non-QGL functions # NodeError.error_msg( value, 'function [%s] not declared to be QGL2' % func_name) return node print('NNN lookup [%s] got %s' % (func_name, str(func_def))) print('NNN FuncDef %s' % ast.dump(func_def)) print('NNN CALL [%s]' % func_name) # When we're figuring out whether something is a call to # the Qbit assignment function, we look at the name of the # function as it is defined (i.e, as func_def), not as it # is imported (i.e., as func_name). # # This makes the assumption that ANYTHING named 'Qubit' # is a Qbit assignment function, which is lame and should # be more carefully parameterized. Things to think about: # looking more deeply at its signature and making certain # that it looks like the 'right' function and not something # someone mistakenly named 'Qubit' in an unrelated context. # if isinstance(value, ast.Call) and (func_def.name == QGL2.QBIT_ALLOC): self._extend_local(target.id) print('XX EXTENDED to include %s %s' % (target.id, str(self._qbit_local()))) return node
def visit_If(self, node): """ flatten an "if" statement, returning a new list of expressions that represent the flattened sequence """ # make sure that the test involves runtime values. # This is the only kind of test that should survive # to this point; classical test would have already # been executed. # FIXME add this check # Also, if the test contains a call, we should # move the evaluation of that call to a expression before # the comparison if (isinstance(node.test, ast.Name) or isinstance(node.test, ast.Call)): mask = 0 cmp_addr = node.test.id cmp_operator = ast.NotEq() elif (isinstance(node.test, ast.UnaryOp) and isinstance(node.test.op, ast.Not)): mask = 0 cmp_addr = node.test.operand.id cmp_operator = ast.Eq() elif isinstance(node.test, ast.Compare): # FIXME the value can be on either side of the comparison # this assumes that it is on the right mask = node.test.comparators[0].n cmp_addr = node.test.left.id cmp_operator = node.test.ops[0] else: NodeError.error_msg( node.test, 'unhandled test expression [%s]' % ast2str(node.test)) return node if_label, end_label = LabelManager.allocate_labels('if', 'if_end') cond_ast = self.make_cgoto_call(if_label, node.test, cmp_operator, cmp_addr, mask) # cond_ast is actually a list of AST nodes new_body = cond_ast end_goto_ast = self.make_ugoto_call(end_label) if_ast = self.make_label_call(if_label) end_label_ast = self.make_label_call(end_label) pyqgl2.ast_util.copy_all_loc(end_goto_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(if_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(end_label_ast, node, recurse=True) if node.orelse: new_body += self.flatten_body(node.orelse) new_body.append(end_goto_ast) new_body.append(if_ast) new_body += self.flatten_body(node.body) new_body.append(end_label_ast) return new_body
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
def assign_simple(self, node): target = node.targets[0] value = node.value DebugMsg.log('XX qbit_scope %s %s' % (str(self._qbit_scope()), ast.dump(node))) if not isinstance(target, ast.Name): return node if target.id in self._qbit_local(): msg = 'reassignment of qbit \'%s\' forbidden' % target.id self.error_msg(node, msg) return node if (target.id + ':qbit') in self._qbit_scope(): msg = 'reassignment of qbit parameter \'%s\' forbidden' % target.id self.error_msg(node, msg) return node DebugMsg.log('XX qbit_scope %s %s' % (str(self._qbit_scope()), ast.dump(node))) if isinstance(value, ast.Name): # print('CHECKING %s' % str(self._qbit_scope())) if (value.id + ':qbit') in self._qbit_scope(): self.warning_msg( node, 'aliasing qbit parameter \'%s\' as \'%s\'' % (value.id, target.id)) self._extend_local(target.id) elif value.id in self._qbit_local(): self.warning_msg( node, 'aliasing local qbit \'%s\' as \'%s\'' % (value.id, target.id)) self._extend_local(target.id) elif isinstance(value, ast.Call): func_name = pyqgl2.importer.collapse_name(value.func) func_def = self.importer.resolve_sym(value.qgl_fname, func_name) # If we can't find the function definition, check to see # whether it's a builtin. If we can't find it, or it's # not declared to be QGL, then we can't check it. # Return immediately. # # TODO the way we check whether a function is a builtin # is a non-portable hack. # # The warning about the function not being defined "locally" # is annoying because it will occur for any function imported # from a module in the system libraries, because we don't # import these right now. This needs a better approach. # if not func_def: """ # This error is no longer valid; it's not an error # if it's not a builtin # if func_name not in __builtins__: NodeError.error_msg( value, 'function [%s] not defined' % func_name) """ return node if func_def.returns: rtype = func_def.returns if (isinstance(rtype, ast.Name) and rtype.id == QGL2.QBIT): # Not sure what happens if we get here: we might # have a wandering variable that we know is a qbit, # but we never know which one. # DebugMsg.log('XX EXTENDING LOCAL (%s)' % target.id) self._extend_local(target.id) target.qgl_is_qbit = True if not func_def.qgl_func: # TODO: this seems bogus. We should be able to call # out to non-QGL functions # NodeError.error_msg( value, 'function [%s] not declared to be QGL2' % func_name) return node DebugMsg.log('NNN lookup [%s] got %s' % (func_name, str(func_def))) DebugMsg.log('NNN FuncDef %s' % ast.dump(func_def)) DebugMsg.log('NNN CALL [%s]' % func_name) # When we're figuring out whether something is a call to # the Qbit assignment function, we look at the name of the # function as it is defined (i.e, as func_def), not as it # is imported (i.e., as func_name). # # This makes the assumption that ANYTHING named 'Qubit' # or 'QubitFactory' # is a Qbit assignment function, which is lame and should # be more carefully parameterized. Things to think about: # looking more deeply at its signature and making certain # that it looks like the 'right' function and not something # someone mistakenly named 'Qubit' in an unrelated context. # if (isinstance(value, ast.Call) and (func_def.name == QGL2.QBIT_ALLOC or func_def.name == QGL2.QBIT_ALLOC2)): self._extend_local(target.id) DebugMsg.log('XX EXTENDED to include %s %s' % (target.id, str(self._qbit_local()))) return node
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 node target = node.targets[0] print('VA target0: %s' % ast.dump(target)) value = node.value name = target.id if not isinstance(target, ast.Name): # should this be considered an error? # it's not an error in Python, but it's hard for us to handle. return node if self.is_qbit_parameter(name): msg = 'reassignment of qbit parameter \'%s\' forbidden' % name NodeError.error_msg(node, msg) return node if self.is_qbit_local(name): msg = 'reassignment of qbit \'%s\' forbidden' % name NodeError.error_msg(node, msg) return node if isinstance(value, ast.Name): if not self.name_is_in_lscope(value.id): NodeError.error_msg(node, 'unknown symbol \'%s\'' % value.id) return node if self.is_qbit_parameter(name): self.warning_msg( node, 'aliasing qbit parameter \'%s\' as \'%s\'' % (value.id, name)) self.add_type_binding(value, name, QGL2.QBIT) target.qgl_is_qbit = True elif self.is_qbit_local(name): self.warning_msg( node, 'aliasing local qbit \'%s\' as \'%s\'' % (value.id, name)) self.add_type_binding(value, name, QGL2.QBIT) target.qgl_is_qbit = True else: self.add_type_binding(value, name, QGL2.CLASSICAL) target.qgl_is_qbit = False elif isinstance(value, ast.Call): func_name = pyqgl2.importer.collapse_name(value.func) func_def = self.importer.resolve_sym(value.qgl_fname, func_name) # FIXME: for debugging only! new_scope = FindTypes.find_lscope(self.importer, func_def, value, self) # FIXME: end debugging # If we can't find the function definition, or it's not declared # to be QGL, then we can't handle it. Return immediately. # if not func_def: NodeError.warning_msg(value, 'function [%s] not found' % func_name) self.add_type_binding(value, name, 'unknown') return node if func_def.returns: rtype = func_def.returns if (isinstance(rtype, ast.Name) and rtype.id == QGL2.QBIT): # Not sure what happens if we get here: we might # have a wandering variable that we know is a qbit, # but we never know which one. # DebugMsg.log('extending local (%s)' % name) self.add_type_binding(value, name, QGL2.QBIT) target.qgl_is_qbit = True if not func_def.qgl_func: # TODO: this seems bogus. We should be able to call # out to non-QGL functions # NodeError.error_msg( value, 'function [%s] not declared to be QGL2' % func_name) return node # When we're figuring out whether something is a call to # the Qbit assignment function, we look at the name of the # function as it is defined (i.e, as func_def), not as it # is imported (i.e., as func_name). # # This makes the assumption that ANYTHING named 'Qubit' or 'QubitFactory' # is a Qbit assignment function, which is lame and should # be more carefully parameterized. Things to think about: # looking more deeply at its signature and making certain # that it looks like the 'right' function and not something # someone mistakenly named 'Qubit' in an unrelated context. # if isinstance(value, ast.Call) and (func_def.name == QGL2.QBIT_ALLOC or func_def.name == QGL2.QBIT_ALLOC2): self.add_type_binding(value, name, QGL2.QBIT) return node
def concur_wait(self, node): """ Synchronize the start of each seq block within a concur block, Add seq blocks for any "missing" channels so we can add a Barrier instruction for each of them as well """ global BARRIER_CTR # This method will be destructive, unless we make a new # copy of the AST tree first # node = deepcopy(node) seen_channels = set() # Channels in this with_concur concur_channels = find_all_channels(node) # For creating the Barriers, we want QGL1 scoped variables that will be real channel instances. # We basically have that already. real_chans = set() for chan in concur_channels: real_chans.add(chan) start_barrier = BARRIER_CTR end_barrier = start_barrier + 1 BARRIER_CTR += 2 for stmnt in node.body: if not is_seq(stmnt): NodeError.error_msg(stmnt, 'non-seq block inside concur block?') return node seq_channels = find_all_channels(stmnt) if seq_channels.intersection(seen_channels): NodeError.error_msg(stmnt, 'seq blocks have overlapping channels') return node seen_channels = seen_channels.union(seq_channels) chan_name = ','.join(seq_channels) # mark stmnt with chan_name or seq_channels in another way if hasattr(stmnt, 'qgl_chan_list'): oldChanSet = set(stmnt.qgl_chan_list) newChanSet = seq_channels oldMissing = newChanSet - oldChanSet oldExtra = oldChanSet - newChanSet if len(oldMissing) > 0: NodeError.diag_msg( stmnt, 'marked chan list %s was missing %s' % (str(oldChanSet), str(oldMissing))) if len(oldExtra) > 0: NodeError.diag_msg( stmnt, 'marked chan list %s had extra %s' % (str(oldChanSet), str(oldExtra))) NodeError.diag_msg(stmnt, 'Marking chan list %s' % (str(seq_channels))) stmnt.qgl_chan_list = list(seq_channels) new_seq_body = list() # Helper to ensure the string we feed to AST doesn't put quotes around # our Qubit variable names def appendChans(bString, chans): bString += '[' first = True for chan in chans: if first: bString += str(chan) first = False else: bString += "," + str(chan) bString += ']' return bString # Add global ctr, chanlist=concur_channels # FIXME: Hold concur_channels as a string? List? bstring = 'Barrier("%s", ' % str(start_barrier) bstring = appendChans(bstring, list(real_chans)) bstring += ')\n' barrier_ast = expr2ast(bstring) # barrier_ast = expr2ast('Barrier(%s, %s)\n' % (str(start_barrier), list(real_chans))) copy_all_loc(barrier_ast, node) barrier_ast.channels = concur_channels # print("*****Start barrier: %s" % pyqgl2.ast_util.ast2str(barrier_ast)) new_seq_body.append(barrier_ast) new_seq_body += stmnt.body bstring = 'Barrier("%s", ' % str(end_barrier) bstring = appendChans(bstring, list(real_chans)) bstring += ')\n' end_barrier_ast = expr2ast(bstring) #end_barrier_ast = expr2ast('Barrier(%s, %s)\n' % (str(end_barrier), list(real_chans))) copy_all_loc(end_barrier_ast, node) # Add global ctr, chanlist=concur_channels end_barrier_ast.channels = concur_channels # print('End AST: %s' % ast2str(end_barrier_ast)) new_seq_body.append(end_barrier_ast) stmnt.body = new_seq_body # FIXME: In new thinking, is the proper unseen set the global one, # Or only those local to this with concur. I think only local for unseen_chan in concur_channels - seen_channels: #print('DIAG %s' % ast2str(stmnt)) NodeError.diag_msg( stmnt, 'channels unreferenced in concur: %s' % str(unseen_chan)) bstring = 'with seq:\n Barrier("%s", ' % str(start_barrier) bstring = appendChans(bstring, list(real_chans)) bstring += ')\n Barrier("%s",' % str(end_barrier) bstring = appendChans(bstring, list(real_chans)) bstring += ')\n' empty_seq_ast = expr2ast(bstring) # print('Empty AST: %s' % ast2str(empty_seq_ast)) # empty_seq_ast = expr2ast( # 'with seq:\n Barrier(%s, %s)\n Barrier(%s, %s)' % (str(start_barrier), list(real_chans), str(end_barrier), list(real_chans))) # Mark empty_seq_ast with unseen_chan empty_seq_ast.qgl_chan_list = [unseen_chan] copy_all_loc(empty_seq_ast, node) node.body.append(empty_seq_ast) return node
def process_params(self, func_def, call=None, call_scope=None): # The formal parameters are an AST object. # The way they are represented is a little awkward; # all parameters (positional and keyword) are in a # positional list (because Python can handle keyword # parameters as positional parameters) and then the # keyword default values are in a separate positional # list.) type_bindings = dict() val_bindings = dict() all_arg_names = list() # First, pretend all the parameters are positional # for arg in func_def.args.args: arg_name = arg.arg arg_type = arg.annotation if arg_type and isinstance(arg_type, ast.Name): arg_type_name = arg_type.id else: arg_type_name = 'unknown' if arg_name in all_arg_names: NodeError.error_msg( arg, 'repeated parameter name \'%s\'' % arg_name) # if arg_type_name not in [QGL2.CLASSICAL, QGL2.QBIT, 'unknown', QGL2.CONTROL, QGL2.PULSE, QGL2.SEQUENCE, QGL2.QBIT_LIST]: if arg_type_name not in [QGL2.CLASSICAL, QGL2.QBIT, 'unknown']: NodeError.warning_msg( arg, ('parameter type \'%s\' is not supported' % arg_type_name)) all_arg_names.append(arg_name) type_bindings[arg_name] = arg_type_name val_bindings[arg_name] = None # Then process any defaults that were provided # default_vals = func_def.args.defaults if default_vals: default_names = all_arg_names[:-len(default_vals)] for ind in range(len(default_vals)): val_bindings[default_names[ind]] = default_vals[ind] # TODO: we need to make sure that the default # values actually match the declared type, if any # # NOTE that the default value is an AST, which could be # almost any expression. Many expressions are going to # be a headache for us, so maybe we should disallow # many of them. # Now replace the default values with whatever is in the # actuals, if any actuals are provided. if call: seen_args = set() print('CALL %s' % ast.dump(call)) if call.args: for ind in range(len(call.args)): seen_args.add(all_arg_names[ind]) val_bindings[all_arg_names[ind]] = call.args[ind] # TODO: If there were fewer args than required, then # gripe. TODO: if there were unexpected arguments, gripe for kwarg in call.keywords: name = kwarg.arg if name in seen_args: NodeError( call, 'more than one value for parameter \'%s\'' % name) seen_args.add(name) val_bindings[name] = kwarg.value print('CALL %s' % str(val_bindings)) # TODO: if provided a surrounding scope and a call, then try to # infer types from actual parameters. For example, if one of # the actual parameters is 'x', and we know the type of 'x', then # propogate it. # # Right now we don't try to statically determine values. # # # TODO: this is incomplete if call and call_scope: # Create a dictionary of known types from the given # call_scope. Note that we are only interested in # known types, so omit any "unknown" types # scope_types = dict() for name in call_scope.parameter_names: name_type = call_scope.parameter_names[name] if name_type != 'unknown': scope_types[name] = name_type for name in call_scope.local_names: name_type = call_scope.local_names[name] if name_type != 'unknown': scope_types[name] = name_type # Now look at each actual parameter, and try # to infer what type it has. If it's a number or # string, it's classical. If it's the value of # a variable, look in scope_types to see what we # know about that variable (if anything). If it's # a method call, look at the definition of the # method to see whether it has a declared type. # for name in type_bindings: actual_val = val_bindings[name] if isinstance(actual_val, ast.Num): type_bindings[name] = QGL2.CLASSICAL elif isinstance(actual_val, ast.Str): type_bindings[name] = QGL2.CLASSICAL elif isinstance(actual_val, ast.NameConstant): type_bindings[name] = QGL2.CLASSICAL elif isinstance(actual_val, ast.Name): if actual_val.id in scope_types: type_bindings[name] = scope_types[actual_val.id] elif isinstance(actual_val, ast.Call): called_func_name = pyqgl2.importer.collapse_name( actual_val.func) called_func = self.importer.resolve_sym( actual_val.qgl_fname, func_name) if not called_func: NodeError.warning_msg( value, 'function [%s] not found' % called_func_name) continue elif called_func.returns: rtype = called_func_def.returns if isinstance(rtype, ast.Name): rtype_name = rtype.id # if rtype_name not in [QGL2.CLASSICAL, QGL2.QBIT, 'unknown', QGL2.SEQUENCE, QGL2.PULSE, QGL2.CONTROL, QGL2.QBIT_LIST]: if rtype_name not in [ QGL2.CLASSICAL, QGL2.QBIT, 'unknown' ]: NodeError.warning_msg( arg, ('parameter type \'%s\' is not supported' % arg_type_name)) type_bindings[name] = rtype_name return val_bindings, type_bindings