diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 503f6220d050..ba265e7ba525 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -206,8 +206,8 @@ def _define_qasm3(self): call = QuantumGateCall( Identifier("U"), [control, target], - parameters=[Constant.pi, Integer(0), Constant.pi], - modifiers=[QuantumGateModifier(QuantumGateModifierName.ctrl)], + parameters=[Constant.PI, Integer(0), Constant.PI], + modifiers=[QuantumGateModifier(QuantumGateModifierName.CTRL)], ) return QuantumGateDefinition( QuantumGateSignature(Identifier("cx"), [control, target]), diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index f95fbf88a7b2..8218141c1d22 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -26,6 +26,7 @@ """ from .exporter import Exporter +from .exceptions import QASM3Error, QASM3ExporterError def dumps(circuit, **kwargs) -> str: diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 17d8783d3f83..63e35281f2ca 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -114,6 +114,27 @@ def __init__(self): pass +class ClassicalType(ASTNode): + """Information about a classical type. This is just an abstract base for inheritance tests.""" + + +class FloatType(ClassicalType, enum.Enum): + """Allowed values for the width of floating-point types.""" + + HALF = 16 + SINGLE = 32 + DOUBLE = 64 + QUAD = 128 + OCT = 256 + + +class BitArrayType(ClassicalType): + """Type information for a sized number of classical bits.""" + + def __init__(self, size: int): + self.size = size + + class Identifier(ASTNode): """ Identifier : FirstIdCharacter GeneralIdCharacter* ; @@ -175,12 +196,23 @@ def __init__(self, identifier: Identifier, subscript: Union[Range, Expression]): self.subscript = subscript +class IndexSet(ASTNode): + """ + A literal index set of values:: + + { Expression (, Expression)* } + """ + + def __init__(self, values: List[Expression]): + self.values = values + + class Constant(Expression, enum.Enum): """A constant value defined by the QASM 3 spec.""" - pi = enum.auto() - euler = enum.auto() - tau = enum.auto() + PI = enum.auto() + EULER = enum.auto() + TAU = enum.auto() class QuantumMeasurement(ASTNode): @@ -240,17 +272,13 @@ def __init__(self, expression: Expression): self.expression = expression -class BitDeclaration(ASTNode): - """ - bitDeclaration - : ( 'creg' Identifier designator? | # NOT SUPPORTED - 'bit' designator? Identifier ) equalsExpression? - """ +class ClassicalDeclaration(Statement): + """Declaration of a classical type, optionally initialising it to a value.""" - def __init__(self, identifier: Identifier, designator=None, equalsExpression=None): + def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=None): + self.type = type_ self.identifier = identifier - self.designator = designator - self.equalsExpression = equalsExpression + self.initializer = initializer class QuantumDeclaration(ASTNode): @@ -279,10 +307,10 @@ def __init__(self, identifier: Identifier, concatenation: List[Identifier]): class QuantumGateModifierName(enum.Enum): """The names of the allowed modifiers of quantum gates.""" - ctrl = enum.auto() - negctrl = enum.auto() - inv = enum.auto() - pow = enum.auto() + CTRL = enum.auto() + NEGCTRL = enum.auto() + INV = enum.auto() + POW = enum.auto() class QuantumGateModifier(ASTNode): @@ -510,25 +538,68 @@ class BranchingStatement(Statement): : 'if' LPAREN booleanExpression RPAREN programBlock ( 'else' programBlock )? """ + def __init__(self, condition: BooleanExpression, true_body: ProgramBlock, false_body=None): + self.condition = condition + self.true_body = true_body + self.false_body = false_body + + +class ForLoopStatement(Statement): + """ + AST node for ``for`` loops. + + :: + + ForLoop: "for" Identifier "in" SetDeclaration ProgramBlock + SetDeclaration: + | Identifier + | "{" Expression ("," Expression)* "}" + | "[" Range "]" + """ + def __init__( - self, booleanExpression: BooleanExpression, programTrue: ProgramBlock, programFalse=None + self, + parameter: Identifier, + indexset: Union[Identifier, IndexSet, Range], + body: ProgramBlock, ): - self.booleanExpression = booleanExpression - self.programTrue = programTrue - self.programFalse = programFalse + self.parameter = parameter + self.indexset = indexset + self.body = body + + +class WhileLoopStatement(Statement): + """ + AST node for ``while`` loops. + + :: + + WhileLoop: "while" "(" Expression ")" ProgramBlock + """ + + def __init__(self, condition: BooleanExpression, body: ProgramBlock): + self.condition = condition + self.body = body + + +class BreakStatement(Statement): + """AST node for ``break`` statements. Has no associated information.""" + + +class ContinueStatement(Statement): + """AST node for ``continue`` statements. Has no associated information.""" class IOModifier(enum.Enum): """IO Modifier object""" - input = enum.auto() - output = enum.auto() + INPUT = enum.auto() + OUTPUT = enum.auto() -class IO(ASTNode): - """UNDEFINED in the grammar yet""" +class IODeclaration(ClassicalDeclaration): + """A declaration of an IO variable.""" - def __init__(self, modifier: IOModifier, input_type, input_variable): + def __init__(self, modifier: IOModifier, type_: ClassicalType, identifier: Identifier): + super().__init__(type_, identifier) self.modifier = modifier - self.type = input_type - self.variable = input_variable diff --git a/qiskit/qasm3/exceptions.py b/qiskit/qasm3/exceptions.py new file mode 100644 index 000000000000..b853b6e74345 --- /dev/null +++ b/qiskit/qasm3/exceptions.py @@ -0,0 +1,23 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Exceptions that may be raised during processing OpenQASM 3.""" + +from qiskit.exceptions import QiskitError + + +class QASM3Error(QiskitError): + """An error raised while working with OpenQASM 3 representations of circuits.""" + + +class QASM3ExporterError(QASM3Error): + """An error raised during running the OpenQASM 3 exporter.""" diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 8425182b0c79..e92ace52a850 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -10,89 +10,42 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=invalid-name - """QASM3 Exporter""" +import collections import io -from os.path import dirname, join, abspath, exists -from typing import Sequence - -from qiskit.circuit.tools import pi_check -from qiskit.circuit import Gate, Barrier, Measure, QuantumRegister, Instruction, Reset -from qiskit.circuit.library.standard_gates import ( - UGate, - PhaseGate, - XGate, - YGate, - ZGate, - HGate, - SGate, - SdgGate, - TGate, - TdgGate, - SXGate, - RXGate, - RYGate, - RZGate, - CXGate, - CYGate, - CZGate, - CPhaseGate, - CRXGate, - CRYGate, - CRZGate, - CHGate, - SwapGate, - CCXGate, - CSwapGate, - CUGate, - # CXGate Again - # PhaseGate Again, - # CPhaseGate Again, - IGate, # Is this id? - U1Gate, - U2Gate, - U3Gate, +import itertools +import numbers +from os.path import dirname, join, abspath +from typing import Iterable, List, Sequence, Union + +from qiskit.circuit import ( + Barrier, + Clbit, + Gate, + Instruction, + Measure, + Parameter, + QuantumCircuit, + QuantumRegister, + Qubit, + Reset, ) from qiskit.circuit.bit import Bit -from qiskit.circuit import Qubit, Clbit -from .ast import ( - Program, - Version, - Include, - Header, - Identifier, - SubscriptedIdentifier, - Range, - QuantumBlock, - QuantumBarrier, - Designator, - Statement, - SubroutineCall, - SubroutineDefinition, - SubroutineBlock, - BranchingStatement, - QuantumGateCall, - QuantumDeclaration, - QuantumGateSignature, - QuantumGateDefinition, - QuantumMeasurement, - QuantumMeasurementAssignment, - Integer, - ProgramBlock, - ComparisonExpression, - BitDeclaration, - EqualsOperator, - QuantumArgument, - Expression, - CalibrationDefinition, - IOModifier, - IO, - PhysicalQubitIdentifier, - AliasStatement, - QuantumReset, +from qiskit.circuit.controlflow import ( + IfElseOp, + ForLoopOp, + WhileLoopOp, + ControlFlowOp, + BreakLoopOp, + ContinueLoopOp, ) +from qiskit.circuit.library import standard_gates +from qiskit.circuit.register import Register +from qiskit.circuit.tools import pi_check + +from . import ast +from .exceptions import QASM3ExporterError from .printer import BasicPrinter @@ -104,6 +57,7 @@ def __init__( includes: Sequence[str] = ("stdgates.inc",), basis_gates: Sequence[str] = ("U",), disable_constants: bool = False, + alias_classical_registers: bool = False, indent: str = " ", ): """ @@ -116,11 +70,21 @@ def __init__( parameter values. If ``False`` (the default), then values close to multiples of QASM 3 constants (``pi``, ``euler``, and ``tau``) will be emitted in terms of those constants instead, potentially improving accuracy in the output. + alias_classical_registers: If ``True``, then classical bit and classical register + declarations will look similar to quantum declarations, where the whole set of bits + will be declared in a flat array, and the registers will just be aliases to + collections of these bits. This is inefficient for running OpenQASM 3 programs, + however, and may not be well supported on backends. Instead, the default behaviour + of ``False`` means that individual classical registers will gain their own + ``bit[size] register;`` declarations, and loose :obj:`.Clbit`\\ s will go onto their + own declaration. In this form, each :obj:`.Clbit` must be in either zero or one + :obj:`.ClassicalRegister`\\ s. indent: the indentation string to use for each level within an indented block. Can be set to the empty string to disable indentation. """ self.basis_gates = basis_gates self.disable_constants = disable_constants + self.alias_classical_registers = alias_classical_registers self.includes = list(includes) self.indent = indent @@ -132,7 +96,13 @@ def dumps(self, circuit): def dump(self, circuit, stream): """Convert the circuit to QASM 3, dumping the result to a file or text stream.""" - builder = Qasm3Builder(circuit, self.includes, self.basis_gates, self.disable_constants) + builder = QASM3Builder( + circuit, + includeslist=self.includes, + basis_gates=self.basis_gates, + disable_constants=self.disable_constants, + alias_classical_registers=self.alias_classical_registers, + ) BasicPrinter(stream, indent=self.indent).visit(builder.build_program()) @@ -140,38 +110,38 @@ class GlobalNamespace: """Global namespace dict-like.""" qiskit_gates = { - "p": PhaseGate, - "x": XGate, - "y": YGate, - "z": ZGate, - "h": HGate, - "s": SGate, - "sdg": SdgGate, - "t": TGate, - "tdg": TdgGate, - "sx": SXGate, - "rx": RXGate, - "ry": RYGate, - "rz": RZGate, - "cx": CXGate, - "cy": CYGate, - "cz": CZGate, - "cp": CPhaseGate, - "crx": CRXGate, - "cry": CRYGate, - "crz": CRZGate, - "ch": CHGate, - "swap": SwapGate, - "ccx": CCXGate, - "cswap": CSwapGate, - "cu": CUGate, - "CX": CXGate, - "phase": PhaseGate, - "cphase": CPhaseGate, - "id": IGate, - "u1": U1Gate, - "u2": U2Gate, - "u3": U3Gate, + "p": standard_gates.PhaseGate, + "x": standard_gates.XGate, + "y": standard_gates.YGate, + "z": standard_gates.ZGate, + "h": standard_gates.HGate, + "s": standard_gates.SGate, + "sdg": standard_gates.SdgGate, + "t": standard_gates.TGate, + "tdg": standard_gates.TdgGate, + "sx": standard_gates.SXGate, + "rx": standard_gates.RXGate, + "ry": standard_gates.RYGate, + "rz": standard_gates.RZGate, + "cx": standard_gates.CXGate, + "cy": standard_gates.CYGate, + "cz": standard_gates.CZGate, + "cp": standard_gates.CPhaseGate, + "crx": standard_gates.CRXGate, + "cry": standard_gates.CRYGate, + "crz": standard_gates.CRZGate, + "ch": standard_gates.CHGate, + "swap": standard_gates.SwapGate, + "ccx": standard_gates.CCXGate, + "cswap": standard_gates.CSwapGate, + "cu": standard_gates.CUGate, + "CX": standard_gates.CXGate, + "phase": standard_gates.PhaseGate, + "cphase": standard_gates.CPhaseGate, + "id": standard_gates.IGate, + "u1": standard_gates.U1Gate, + "u2": standard_gates.U2Gate, + "u3": standard_gates.U3Gate, } include_paths = [abspath(join(dirname(__file__), "..", "qasm", "libs"))] @@ -186,21 +156,6 @@ def __init__(self, includelist, basis_gates=()): # Should it be parsed? pass - def _extract_gates_from_file(self, filename): - gates = set() - for filepath in self._find_lib(filename): - with open(filepath) as fp: - for line in fp.readlines(): - if line.startswith("gate "): - gates.add(line[5:].split("(")[0].split()[0]) - return gates - - def _find_lib(self, filename): - for include_path in self.include_paths: - full_path = join(include_path, filename) - if exists(full_path): - yield full_path - def __setitem__(self, name_str, instruction): self._data[name_str] = type(instruction) self._data[id(instruction)] = name_str @@ -214,7 +169,7 @@ def __iter__(self): return iter(self._data) def __contains__(self, instruction): - if isinstance(instruction, UGate): + if isinstance(instruction, standard_gates.UGate): return True if id(instruction) in self._data: return True @@ -235,20 +190,45 @@ def register(self, instruction): self[instruction.name] = instruction -class Qasm3Builder: +# A _Scope is the structure used in the builder to store the contexts and re-mappings of bits from +# the top-level scope where the bits were actually defined. In the class, 'circuit' is an instance +# of QuantumCircuit that defines this level, and 'bit_map' is a mapping of 'Bit: Bit', where the +# keys are bits in the circuit in this scope, and the values are the Bit in the top-level scope in +# this context that this bit actually represents. This is a cheap hack around actually implementing +# a proper symbol table. +_Scope = collections.namedtuple("_Scope", ("circuit", "bit_map")) + + +class QASM3Builder: """QASM3 builder constructs an AST from a QuantumCircuit.""" - builtins = (Barrier, Measure, Reset) + builtins = (Barrier, Measure, Reset, BreakLoopOp, ContinueLoopOp) + gate_parameter_prefix = "_gate_p" + gate_qubit_prefix = "_gate_q" - def __init__(self, quantumcircuit, includeslist, basis_gates, disable_constants): - self.circuit_ctx = [quantumcircuit] + def __init__( + self, + quantumcircuit, + includeslist, + basis_gates, + disable_constants, + alias_classical_registers, + ): + # This is a stack of stacks; the outer stack is a list of "outer" look-up contexts, and the + # inner stack is for scopes within these. A "outer" look-up context in this sense means + # the main program body or a gate/subroutine definition, whereas the scopes are for things + # like the body of a ``for`` loop construct. + self._circuit_ctx = [] + self.push_context(quantumcircuit) self.includeslist = includeslist self._gate_to_declare = {} self._subroutine_to_declare = {} self._opaque_to_declare = {} self._flat_reg = False self._physical_qubit = False + self._loose_clbit_index_lookup = {} self.disable_constants = disable_constants + self.alias_classical_registers = alias_classical_registers self.global_namespace = GlobalNamespace(includeslist, basis_gates) def _register_gate(self, gate): @@ -266,20 +246,25 @@ def _register_opaque(self, instruction): def build_header(self): """Builds a Header""" - version = Version("3") + version = ast.Version("3") includes = self.build_includes() - return Header(version, includes) + return ast.Header(version, includes) def build_program(self): """Builds a Program""" - self.hoist_declarations(self.circuit_ctx[-1].data) - return Program(self.build_header(), self.build_globalstatements()) + self.hoist_declarations(self.global_scope(assert_=True).circuit.data) + return ast.Program(self.build_header(), self.build_global_statements()) def hoist_declarations(self, instructions): """Walks the definitions in gates/instructions to make a list of gates to declare.""" for instruction in instructions: + if isinstance(instruction[0], ControlFlowOp): + for block in instruction[0].blocks: + self.hoist_declarations(block.data) + continue if instruction[0] in self.global_namespace or isinstance(instruction[0], self.builtins): continue + if instruction[0].definition is None: self._register_opaque(instruction[0]) else: @@ -289,11 +274,82 @@ def hoist_declarations(self, instructions): else: self._register_subroutine(instruction[0]) + def global_scope(self, assert_=False): + """Return the global circuit scope that is used as the basis of the full program. If + ``assert_=True``, then this raises :obj:`.QASM3ExporterError` if the current context is not + the global one.""" + if assert_ and len(self._circuit_ctx) != 1 and len(self._circuit_ctx[0]) != 1: + # Defensive code to help catch logic errors. + raise QASM3ExporterError( # pragma: no cover + f"Not currently in the global context. Current contexts are: {self._circuit_ctx}" + ) + return self._circuit_ctx[0][0] + + def current_outermost_scope(self): + """Return the outermost scope for this context. If building the main program, then this is + the :obj:`.QuantumCircuit` instance that the full program is being built from. If building + a gate or subroutine definition, this is the body that defines the gate or subroutine.""" + return self._circuit_ctx[-1][0] + + def current_scope(self): + """Return the current circuit scope.""" + return self._circuit_ctx[-1][-1] + + def current_context(self): + """Return the current context (list of scopes).""" + return self._circuit_ctx[-1] + + def push_scope(self, circuit: QuantumCircuit, qubits: Iterable[Qubit], clbits: Iterable[Clbit]): + """Push a new scope (like a ``for`` or ``while`` loop body) onto the current context + stack.""" + current_map = self.current_scope().bit_map + qubits = tuple(current_map[qubit] for qubit in qubits) + clbits = tuple(current_map[clbit] for clbit in clbits) + if circuit.num_qubits != len(qubits): + raise QASM3ExporterError( # pragma: no cover + f"Tried to push a scope whose circuit needs {circuit.num_qubits} qubits, but only" + f" provided {len(qubits)} qubits to create the mapping." + ) + if circuit.num_clbits != len(clbits): + raise QASM3ExporterError( # pragma: no cover + f"Tried to push a scope whose circuit needs {circuit.num_clbits} clbits, but only" + f" provided {len(clbits)} clbits to create the mapping." + ) + mapping = dict(itertools.chain(zip(circuit.qubits, qubits), zip(circuit.clbits, clbits))) + self._circuit_ctx[-1].append(_Scope(circuit, mapping)) + + def pop_scope(self) -> _Scope: + """Pop the current scope (like a ``for`` or ``while`` loop body) off the current context + stack.""" + if len(self._circuit_ctx[-1]) <= 1: + raise QASM3ExporterError( # pragma: no cover + "Tried to pop a scope from the current context, but there are no current scopes." + ) + return self._circuit_ctx[-1].pop() + + def push_context(self, outer_context: QuantumCircuit): + """Push a new context (like for a ``gate`` or ``def`` body) onto the stack.""" + mapping = {bit: bit for bit in itertools.chain(outer_context.qubits, outer_context.clbits)} + self._circuit_ctx.append([_Scope(outer_context, mapping)]) + + def pop_context(self): + """Pop the current context (like for a ``gate`` or ``def`` body) onto the stack.""" + if len(self._circuit_ctx) == 1: + raise QASM3ExporterError( # pragma: no cover + "Tried to pop the current context, but that is the global context." + ) + if len(self._circuit_ctx[-1]) != 1: + raise QASM3ExporterError( # pragma: no cover + "Tried to pop the current context while there are still" + f" {len(self._circuit_ctx[-1]) - 1} unclosed scopes." + ) + self._circuit_ctx.pop() + def build_includes(self): """Builds a list of included files.""" - return [Include(filename) for filename in self.includeslist] + return [ast.Include(filename) for filename in self.includeslist] - def build_globalstatements(self) -> [Statement]: + def build_global_statements(self) -> List[ast.Statement]: """ globalStatement : subroutineDefinition @@ -316,38 +372,40 @@ def build_globalstatements(self) -> [Statement]: ; """ definitions = self.build_definitions() - inputs = self.build_inputs() - bitdeclarations = self.build_bitdeclarations() - quantumdeclarations = None - if hasattr(self.circuit_ctx[-1], "_layout") and self.circuit_ctx[-1]._layout is not None: + inputs, outputs, variables = self.build_variable_declarations() + bit_declarations = self.build_classical_declarations() + context = self.global_scope(assert_=True).circuit + if getattr(context, "_layout", None) is not None: self._physical_qubit = True + quantum_declarations = [] else: - quantumdeclarations = self.build_quantumdeclarations() - quantuminstructions = self.build_quantuminstructions(self.circuit_ctx[-1].data) + quantum_declarations = self.build_quantum_declarations() + quantum_instructions = self.build_quantum_instructions(context.data) self._physical_qubit = False - ret = [] - if definitions: - ret += definitions - if inputs: - ret += inputs - if bitdeclarations: - ret += bitdeclarations - if quantumdeclarations: - ret += quantumdeclarations - if quantuminstructions: - ret += quantuminstructions - return ret + return [ + statement + for source in ( + definitions, + inputs, + outputs, + variables, + bit_declarations, + quantum_declarations, + quantum_instructions, + ) + for statement in source + ] def build_definitions(self): """Builds all the definition.""" ret = [] for instruction in self._opaque_to_declare.values(): - ret.append(self.build_definition(instruction, self.build_opaquedefinition)) + ret.append(self.build_definition(instruction, self.build_opaque_definition)) for instruction in self._subroutine_to_declare.values(): - ret.append(self.build_definition(instruction, self.build_subroutinedefinition)) + ret.append(self.build_definition(instruction, self.build_subroutine_definition)) for instruction in self._gate_to_declare.values(): - ret.append(self.build_definition(instruction, self.build_quantumgatedefinition)) + ret.append(self.build_definition(instruction, self.build_gate_definition)) return ret def build_definition(self, instruction, builder): @@ -361,124 +419,192 @@ def build_definition(self, instruction, builder): self._flat_reg = False return definition - def build_opaquedefinition(self, instruction): + def build_opaque_definition(self, instruction): """Builds an Opaque gate definition as a CalibrationDefinition""" name = self.global_namespace[instruction] - quantumArgumentList = [Identifier(f"q_{n}") for n in range(instruction.num_qubits)] - return CalibrationDefinition(Identifier(name), quantumArgumentList) + quantum_arguments = [ + ast.Identifier(f"{self.gate_qubit_prefix}_{n}") for n in range(instruction.num_qubits) + ] + return ast.CalibrationDefinition(ast.Identifier(name), quantum_arguments) - def build_subroutinedefinition(self, instruction): + def build_subroutine_definition(self, instruction): """Builds a SubroutineDefinition""" name = self.global_namespace[instruction] - self.circuit_ctx.append(instruction.definition) - quantumArgumentList = self.build_quantumArgumentList( - instruction.definition.qregs, instruction.definition + self.push_context(instruction.definition) + quantum_arguments = [ + ast.QuantumArgument(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}")) + for n_qubit in range(len(instruction.definition.qubits)) + ] + subroutine_body = ast.SubroutineBlock( + self.build_quantum_instructions(instruction.definition.data), ) - subroutineBlock = SubroutineBlock( - self.build_quantuminstructions(instruction.definition.data), - ) - self.circuit_ctx.pop() - return SubroutineDefinition(Identifier(name), subroutineBlock, quantumArgumentList) + self.pop_context() + return ast.SubroutineDefinition(ast.Identifier(name), subroutine_body, quantum_arguments) - def build_quantumgatedefinition(self, gate): + def build_gate_definition(self, gate): """Builds a QuantumGateDefinition""" - quantumGateSignature = self.build_quantumGateSignature(gate) - - self.circuit_ctx.append(gate.definition) - quantumBlock = QuantumBlock(self.build_quantuminstructions(gate.definition.data)) - self.circuit_ctx.pop() - return QuantumGateDefinition(quantumGateSignature, quantumBlock) + signature = self.build_gate_signature(gate) + self.push_context(gate.definition) + body = ast.QuantumBlock(self.build_quantum_instructions(gate.definition.data)) + self.pop_context() + return ast.QuantumGateDefinition(signature, body) - def build_quantumGateSignature(self, gate): + def build_gate_signature(self, gate): """Builds a QuantumGateSignature""" name = self.global_namespace[gate] params = [] + definition = gate.definition # Dummy parameters - for num in range(len(gate.params) - len(gate.definition.parameters)): - param_name = f"param_{num}" - params.append(Identifier(param_name)) - params += [Identifier(param.name) for param in gate.definition.parameters] - - self.circuit_ctx.append(gate.definition) - qargList = [] - for qreg in gate.definition.qregs: - for qubit in qreg: - qreg, idx = self.find_bit(qubit) - qubit_name = f"{qreg.name}_{idx}" - qargList.append(Identifier(qubit_name)) - self.circuit_ctx.pop() - - return QuantumGateSignature(Identifier(name), qargList, params or None) - - def build_inputs(self): - """Builds a list of Inputs""" - ret = [] - for param in self.circuit_ctx[-1].parameters: - ret.append(IO(IOModifier.input, Identifier("float[32]"), Identifier(param.name))) - return ret + for num in range(len(gate.params) - len(definition.parameters)): + param_name = f"{self.gate_parameter_prefix}_{num}" + params.append(ast.Identifier(param_name)) + params += [ast.Identifier(param.name) for param in definition.parameters] + quantum_arguments = [ + ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}") + for n_qubit in range(len(definition.qubits)) + ] + return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None) + + def build_variable_declarations(self): + """Builds lists of the input, output and standard variables used in this program.""" + inputs, outputs, variables = [], [], [] + global_scope = self.global_scope(assert_=True).circuit + for parameter in global_scope.parameters: + declaration = _infer_variable_declaration(global_scope, parameter) + if declaration is None: + continue + if isinstance(declaration, ast.IODeclaration): + if declaration.modifier is ast.IOModifier.INPUT: + inputs.append(declaration) + else: + outputs.append(declaration) + else: + variables.append(declaration) + return inputs, outputs, variables - def build_bitdeclarations(self): - """Builds a list of BitDeclarations""" - ret = [] - for creg in self.circuit_ctx[-1].cregs: - ret.append(BitDeclaration(Identifier(creg.name), Designator(Integer(creg.size)))) - return ret + @property + def base_classical_register_name(self): + """The base register name""" + name = "_all_clbits" if self.alias_classical_registers else "_loose_clbits" + if name in self.global_namespace._data: + raise NotImplementedError # TODO choose a different name if there is a name collision + return name @property - def base_register_name(self): + def base_quantum_register_name(self): """The base register name""" - name = "_q" + name = "_all_qubits" if name in self.global_namespace._data: raise NotImplementedError # TODO choose a different name if there is a name collision return name - def build_quantumdeclarations(self): - """Builds a single QuantumDeclaration for the base register and a list of aliases + def build_classical_declarations(self): + """Return a list of AST nodes declaring all the classical bits and registers. + + The behaviour of this function depends on the setting ``alias_classical_registers``. If this + is ``True``, then the output will be in the same form as the output of + :meth:`.build_classical_declarations`, with the registers being aliases. If ``False``, it + will instead return a :obj:`.ast.ClassicalDeclaration` for each classical register, and one + for the loose :obj:`.Clbit` instances, and will raise :obj:`QASM3ExporterError` if any + registers overlap. - The Base register name is the way the exporter handle registers. - All the qubits are part of a long flat register and the QuantumRegisters are aliases + This function populates the lookup table ``self._loose_clbit_index_lookup``. """ - ret = [] + circuit = self.current_scope().circuit + if self.alias_classical_registers: + self._loose_clbit_index_lookup = { + bit: index for index, bit in enumerate(circuit.clbits) + } + flat_declaration = self.build_clbit_declaration( + len(circuit.clbits), self.base_classical_register_name + ) + return [flat_declaration] + self.build_aliases(circuit.cregs) + loose_register_size = 0 + for index, bit in enumerate(circuit.clbits): + found_bit = circuit.find_bit(bit) + if len(found_bit.registers) > 1: + raise QASM3ExporterError( + f"Clbit {index} is in multiple registers, but 'alias_classical_registers' is" + f" False. Registers and indices: {found_bit.registers}." + ) + if not found_bit.registers: + self._loose_clbit_index_lookup[bit] = loose_register_size + loose_register_size += 1 + if loose_register_size > 0: + loose = [ + self.build_clbit_declaration(loose_register_size, self.base_classical_register_name) + ] + else: + loose = [] + return loose + [ + self.build_clbit_declaration(len(register), register.name) for register in circuit.cregs + ] + + def build_clbit_declaration(self, n_clbits: int, name: str) -> ast.ClassicalDeclaration: + """Return a declaration of the :obj:`.Clbit`\\ s as a ``bit[n]``.""" + return ast.ClassicalDeclaration(ast.BitArrayType(n_clbits), ast.Identifier(name)) + + def build_quantum_declarations(self): + """Return a list of AST nodes declaring all the qubits in the current scope, and all the + alias declarations for these qubits.""" + return [self.build_qubit_declarations()] + self.build_aliases( + self.current_scope().circuit.qregs + ) + def build_qubit_declarations(self): + """Return a declaration of all the :obj:`.Qubit`\\ s in the current scope.""" # Base register - ret.append( - QuantumDeclaration( - Identifier(self.base_register_name), - Designator(Integer(self.circuit_ctx[-1].num_qubits)), - ) + return ast.QuantumDeclaration( + ast.Identifier(self.base_quantum_register_name), + ast.Designator(self.build_integer(self.current_scope().circuit.num_qubits)), ) - # aliases - for qreg in self.circuit_ctx[-1].qregs: + + def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatement]: + """Return a list of alias declarations for the given registers. The registers can be either + classical or quantum.""" + out = [] + for register in registers: elements = [] - # Greedily consolidate runs of qubits into ranges. We don't bother trying to handle - # steps; there's no need in generated code. Even single qubits are referenced as ranges + # Greedily consolidate runs of bits into ranges. We don't bother trying to handle + # steps; there's no need in generated code. Even single bits are referenced as ranges # because the concatenation in an alias statement can only concatenate arraylike values. start_index, prev_index = None, None - for qubit in qreg: - cur_index = self.circuit_ctx[-1].find_bit(qubit).index + register_identifier = ( + ast.Identifier(self.base_quantum_register_name) + if isinstance(register, QuantumRegister) + else ast.Identifier(self.base_classical_register_name) + ) + for bit in register: + cur_index = self.find_bit(bit).index if start_index is None: start_index = cur_index elif cur_index != prev_index + 1: elements.append( - SubscriptedIdentifier( - Identifier(self.base_register_name), - Range(start=Integer(start_index), end=Integer(prev_index)), + ast.SubscriptedIdentifier( + register_identifier, + ast.Range( + start=self.build_integer(start_index), + end=self.build_integer(prev_index), + ), ) ) start_index = prev_index = cur_index prev_index = cur_index # After the loop, if there were any bits at all, there's always one unemitted range. - if len(qreg) != 0: + if len(register) != 0: elements.append( - SubscriptedIdentifier( - Identifier(self.base_register_name), - Range(start=Integer(start_index), end=Integer(prev_index)), + ast.SubscriptedIdentifier( + register_identifier, + ast.Range( + start=self.build_integer(start_index), + end=self.build_integer(prev_index), + ), ) ) - ret.append(AliasStatement(Identifier(qreg.name), elements)) - return ret + out.append(ast.AliasStatement(ast.Identifier(register.name), elements)) + return out - def build_quantuminstructions(self, instructions): + def build_quantum_instructions(self, instructions): """Builds a list of call statements""" ret = [] for instruction in instructions: @@ -487,95 +613,232 @@ def build_quantuminstructions(self, instructions): eqcondition = self.build_eqcondition(instruction[0].condition) instruction_without_condition = instruction[0].copy() instruction_without_condition.condition = None - programTrue = self.build_programblock( + true_body = self.build_program_block( [(instruction_without_condition, instruction[1], instruction[2])] ) - ret.append(BranchingStatement(eqcondition, programTrue)) + ret.append(ast.BranchingStatement(eqcondition, true_body)) else: - ret.append(self.build_quantumgatecall(instruction)) + ret.append(self.build_gate_call(instruction)) elif isinstance(instruction[0], Barrier): operands = [self.build_single_bit_reference(operand) for operand in instruction[1]] - ret.append(QuantumBarrier(operands)) + ret.append(ast.QuantumBarrier(operands)) elif isinstance(instruction[0], Measure): - quantumMeasurement = QuantumMeasurement( + measurement = ast.QuantumMeasurement( [self.build_single_bit_reference(operand) for operand in instruction[1]] ) qubit = self.build_single_bit_reference(instruction[2][0]) - ret.append(QuantumMeasurementAssignment(qubit, quantumMeasurement)) + ret.append(ast.QuantumMeasurementAssignment(qubit, measurement)) elif isinstance(instruction[0], Reset): for operand in instruction[1]: - ret.append(QuantumReset(self.build_single_bit_reference(operand))) + ret.append(ast.QuantumReset(self.build_single_bit_reference(operand))) + elif isinstance(instruction[0], ForLoopOp): + ret.append(self.build_for_loop(*instruction)) + elif isinstance(instruction[0], WhileLoopOp): + ret.append(self.build_while_loop(*instruction)) + elif isinstance(instruction[0], IfElseOp): + ret.append(self.build_if_statement(*instruction)) + elif isinstance(instruction[0], BreakLoopOp): + ret.append(ast.BreakStatement()) + elif isinstance(instruction[0], ContinueLoopOp): + ret.append(ast.ContinueStatement()) else: - ret.append(self.build_subroutinecall(instruction)) + ret.append(self.build_subroutine_call(instruction)) return ret - def build_programblock(self, instructions): + def build_if_statement( + self, instruction: IfElseOp, qubits: Iterable[Qubit], clbits: Iterable[Clbit] + ) -> ast.BranchingStatement: + """Build an :obj:`.IfElseOp` into a :obj:`.ast.BranchingStatement`.""" + condition = self.build_eqcondition(instruction.condition) + + true_circuit = instruction.blocks[0] + self.push_scope(true_circuit, qubits, clbits) + true_body = self.build_program_block(true_circuit.data) + self.pop_scope() + if len(instruction.blocks) == 1: + return ast.BranchingStatement(condition, true_body, None) + + false_circuit = instruction.blocks[1] + self.push_scope(false_circuit, qubits, clbits) + false_body = self.build_program_block(false_circuit.data) + self.pop_scope() + return ast.BranchingStatement(condition, true_body, false_body) + + def build_while_loop( + self, instruction: WhileLoopOp, qubits: Iterable[Qubit], clbits: Iterable[Clbit] + ) -> ast.WhileLoopStatement: + """Build a :obj:`.WhileLoopOp` into a :obj:`.ast.WhileLoopStatement`.""" + condition = self.build_eqcondition(instruction.condition) + loop_circuit = instruction.blocks[0] + self.push_scope(loop_circuit, qubits, clbits) + loop_body = self.build_program_block(loop_circuit.data) + self.pop_scope() + return ast.WhileLoopStatement(condition, loop_body) + + def build_for_loop( + self, instruction: ForLoopOp, qubits: Iterable[Qubit], clbits: Iterable[Clbit] + ) -> ast.ForLoopStatement: + """Build a :obj:`.ForLoopOp` into a :obj:`.ast.ForLoopStatement`.""" + loop_parameter, indexset, loop_circuit = instruction.params + if loop_parameter is None: + # The loop parameter is implicitly declared by the ``for`` loop (see also + # _infer_parameter_declaration), so it doesn't matter that we haven't declared this, + # except for the concerns about symbol collision which can only be alleviated by + # introducing a proper symbol table to this exporter. + loop_parameter_ast = ast.Identifier("_") + else: + loop_parameter_ast = ast.Identifier(loop_parameter.name) + if isinstance(indexset, range): + # QASM 3 uses inclusive ranges on both ends, unlike Python. + indexset_ast = ast.Range( + start=self.build_integer(indexset.start), + end=self.build_integer(indexset.stop - 1), + step=self.build_integer(indexset.step) if indexset.step != 1 else None, + ) + else: + try: + indexset_ast = ast.IndexSet([self.build_integer(value) for value in indexset]) + except QASM3ExporterError: + raise QASM3ExporterError( + "The values in QASM 3 'for' loops must all be integers, but received" + f" '{indexset}'." + ) from None + self.push_scope(loop_circuit, qubits, clbits) + body_ast = self.build_program_block(loop_circuit) + self.pop_scope() + return ast.ForLoopStatement(loop_parameter_ast, indexset_ast, body_ast) + + def build_integer(self, value) -> ast.Integer: + """Build an integer literal, raising a :obj:`.QASM3ExporterError` if the input is not + actually an + integer.""" + if not isinstance(value, numbers.Integral): + # This is meant to be purely defensive, in case a non-integer slips into the logic + # somewhere, but no valid Terra object should trigger this. + raise QASM3ExporterError(f"'{value}' is not an integer") # pragma: no cover + return ast.Integer(int(value)) + + def build_program_block(self, instructions): """Builds a ProgramBlock""" - return ProgramBlock(self.build_quantuminstructions(instructions)) + return ast.ProgramBlock(self.build_quantum_instructions(instructions)) def build_eqcondition(self, condition): """Classical Conditional condition from a instruction.condition""" if isinstance(condition[0], Clbit): condition_on = self.build_single_bit_reference(condition[0]) else: - condition_on = Identifier(condition[0].name) - return ComparisonExpression(condition_on, EqualsOperator(), Integer(int(condition[1]))) - - def build_quantumArgumentList(self, qregs: [QuantumRegister], circuit=None): - """Builds a quantumArgumentList""" - quantumArgumentList = [] - for qreg in qregs: - if self._flat_reg: - for qubit in qreg: - if circuit is None: - raise Exception - reg, idx = self.find_bit(qubit) - qubit_name = f"{reg.name}_{idx}" - quantumArgumentList.append(QuantumArgument(Identifier(qubit_name))) - else: - quantumArgumentList.append( - QuantumArgument(Identifier(qreg.name), Designator(Integer(qreg.size))) - ) - return quantumArgumentList + condition_on = ast.Identifier(condition[0].name) + return ast.ComparisonExpression( + condition_on, ast.EqualsOperator(), self.build_integer(condition[1]) + ) - def build_quantumgatecall(self, instruction): + def build_gate_call(self, instruction): """Builds a QuantumGateCall""" - if isinstance(instruction[0], UGate): - quantumGateName = Identifier("U") + if isinstance(instruction[0], standard_gates.UGate): + gate_name = ast.Identifier("U") else: - quantumGateName = Identifier(self.global_namespace[instruction[0]]) + gate_name = ast.Identifier(self.global_namespace[instruction[0]]) qubits = [self.build_single_bit_reference(qubit) for qubit in instruction[1]] if self.disable_constants: - parameters = [Expression(param) for param in instruction[0].params] + parameters = [ast.Expression(param) for param in instruction[0].params] else: parameters = [ - Expression(pi_check(param, output="qasm")) for param in instruction[0].params + ast.Expression(pi_check(param, output="qasm")) for param in instruction[0].params ] - return QuantumGateCall(quantumGateName, qubits, parameters=parameters) + return ast.QuantumGateCall(gate_name, qubits, parameters=parameters) - def build_subroutinecall(self, instruction): + def build_subroutine_call(self, instruction): """Builds a SubroutineCall""" - identifier = Identifier(self.global_namespace[instruction[0]]) - expressionList = [Expression(param) for param in instruction[0].params] + identifier = ast.Identifier(self.global_namespace[instruction[0]]) + expressions = [ast.Expression(param) for param in instruction[0].params] # TODO: qubits should go inside the brackets of subroutine calls, but neither Terra nor the # AST here really support the calls, so there's no sensible way of writing it yet. - indexIdentifierList = [self.build_single_bit_reference(bit) for bit in instruction[1]] - return SubroutineCall(identifier, indexIdentifierList, expressionList) + bits = [self.build_single_bit_reference(bit) for bit in instruction[1]] + return ast.SubroutineCall(identifier, bits, expressions) - def build_single_bit_reference(self, bit: Bit) -> Identifier: + def build_single_bit_reference(self, bit: Bit) -> ast.Identifier: """Get an identifier node that refers to one particular bit.""" + found_bit = self.find_bit(bit) if self._physical_qubit and isinstance(bit, Qubit): - return PhysicalQubitIdentifier( - Identifier(str(self.circuit_ctx[-1].find_bit(bit).index)) - ) - reg, idx = self.find_bit(bit) + return ast.PhysicalQubitIdentifier(ast.Identifier(str(found_bit.index))) if self._flat_reg: - bit_name = f"{reg.name}_{idx}" - return Identifier(bit_name) - return SubscriptedIdentifier(Identifier(reg.name), Integer(idx)) - - def find_bit(self, bit): - """Returns the register and the index in that register for a particular bit""" - bit_location = self.circuit_ctx[-1].find_bit(bit) - return bit_location.registers[0] + return ast.Identifier(f"{self.gate_qubit_prefix}_{found_bit.index}") + if found_bit.registers: + # We preferentially return a reference via a register in the hope that this is what the + # user is used to seeing as well. + register, index = found_bit.registers[0] + return ast.SubscriptedIdentifier( + ast.Identifier(register.name), self.build_integer(index) + ) + # Otherwise reference via the list of all qubits, or the list of loose clbits. + if isinstance(bit, Qubit): + return ast.SubscriptedIdentifier( + ast.Identifier(self.base_quantum_register_name), self.build_integer(found_bit.index) + ) + return ast.SubscriptedIdentifier( + ast.Identifier(self.base_classical_register_name), + self.build_integer(self._loose_clbit_index_lookup[bit]), + ) + + def find_bit(self, bit: Bit): + """Look up the bit using :meth:`.QuantumCircuit.find_bit` in the current outermost scope.""" + # This is a hacky work-around for now. Really this should be a proper symbol-table lookup, + # but with us expecting to put in a whole new AST for Terra 0.20, this should be sufficient + # for the use-cases we support. (Jake, 2021-11-22.) + if len(self.current_context()) > 1: + ancestor_bit = self.current_scope().bit_map[bit] + return self.current_outermost_scope().circuit.find_bit(ancestor_bit) + return self.current_scope().circuit.find_bit(bit) + + +def _infer_variable_declaration( + circuit: QuantumCircuit, parameter: Parameter +) -> Union[ast.ClassicalDeclaration, None]: + """Attempt to infer what type a parameter should be declared as to work with a circuit. + + This is very simplistic; it assumes all parameters are real numbers that need to be input to the + program, unless one is used as a loop variable, in which case it shouldn't be declared at all, + because the ``for`` loop declares it implicitly (per the Qiskit/QSS reading of the OpenQASM + spec at Qiskit/openqasm@8ee55ec). + + .. note:: + + This is a hack around not having a proper type system implemented in Terra, and really this + whole function should be removed in favour of proper symbol-table building and lookups. + This function is purely to try and hack the parameters for ``for`` loops into the exporter + for now. + + Args: + circuit: The global-scope circuit, which is the base of the exported program. + parameter: The parameter to infer the type of. + + Returns: + A suitable :obj:`.ast.ClassicalDeclaration` node, or, if the parameter should *not* be + declared, then ``None``. + """ + + def is_loop_variable(circuit, parameter): + """Recurse into the instructions a parameter is used in, checking at every level if it is + used as the loop variable of a ``for`` loop.""" + # This private access is hacky, and shouldn't need to happen; the type of a parameter + # _should_ be an intrinsic part of the parameter, or somewhere publicly accessible, but + # Terra doesn't have those concepts yet. We can only try and guess at the type by looking + # at all the places it's used in the circuit. + for instruction, index in circuit._parameter_table[parameter]: + if isinstance(instruction, ForLoopOp): + # The parameters of ForLoopOp are (loop_parameter, indexset, body). + if index == 0: + return True + if isinstance(instruction, ControlFlowOp): + if is_loop_variable(instruction.params[index], parameter): + return True + return False + + if is_loop_variable(circuit, parameter): + return None + # Arbitrary choice of double-precision float for all other parameters, but it's what we actually + # expect people to be binding to their Parameters right now. + return ast.IODeclaration( + ast.IOModifier.INPUT, ast.FloatType.DOUBLE, ast.Identifier(parameter.name) + ) diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 2c59f103c3d2..9d07af8b9052 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -15,48 +15,7 @@ import io from typing import Sequence -from .ast import ( - ASTNode, - AliasStatement, - BitDeclaration, - BranchingStatement, - CalibrationDefinition, - CalibrationGrammarDeclaration, - ComparisonExpression, - Constant, - Designator, - EqualsOperator, - Expression, - GtOperator, - Header, - IO, - IOModifier, - Identifier, - Include, - Integer, - LtOperator, - PhysicalQubitIdentifier, - Pragma, - Program, - ProgramBlock, - QuantumArgument, - QuantumBarrier, - QuantumDeclaration, - QuantumGateCall, - QuantumGateDefinition, - QuantumGateModifier, - QuantumGateModifierName, - QuantumGateSignature, - QuantumMeasurement, - QuantumMeasurementAssignment, - QuantumReset, - Range, - ReturnStatement, - SubroutineCall, - SubroutineDefinition, - SubscriptedIdentifier, - Version, -) +from . import ast class BasicPrinter: @@ -64,32 +23,61 @@ class BasicPrinter: formatting is simple block indentation.""" _CONSTANT_LOOKUP = { - Constant.pi: "pi", - Constant.euler: "euler", - Constant.tau: "tau", + ast.Constant.PI: "pi", + ast.Constant.EULER: "euler", + ast.Constant.TAU: "tau", } _MODIFIER_LOOKUP = { - QuantumGateModifierName.ctrl: "ctrl", - QuantumGateModifierName.negctrl: "negctrl", - QuantumGateModifierName.inv: "inv", - QuantumGateModifierName.pow: "pow", + ast.QuantumGateModifierName.CTRL: "ctrl", + ast.QuantumGateModifierName.NEGCTRL: "negctrl", + ast.QuantumGateModifierName.INV: "inv", + ast.QuantumGateModifierName.POW: "pow", } + _FLOAT_WIDTH_LOOKUP = {type: str(type.value) for type in ast.FloatType} + # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name - def __init__(self, stream: io.TextIOBase, *, indent: str): + def __init__(self, stream: io.TextIOBase, *, indent: str, chain_else_if: bool = False): """ Args: stream (io.TextIOBase): the stream that the output will be written to. indent (str): the string to use as a single indentation level. + chain_else_if (bool): If ``True``, then constructs of the form:: + + if (x == 0) { + // ... + } else { + if (x == 1) { + // ... + } else { + // ... + } + } + + will be collapsed into the equivalent but flatter:: + + if (x == 0) { + // ... + } else if (x == 1) { + // ... + } else { + // ... + } + + This collapsed form may have less support on backends, so it is turned off by + default. While the output of this printer is always unambiguous, using ``else`` + without immediately opening an explicit scope with ``{ }`` in nested contexts can + cause issues, in the general case, which is why it is sometimes less supported. """ self.stream = stream self.indent = indent self._current_indent = 0 + self._chain_else_if = chain_else_if - def visit(self, node: ASTNode) -> None: + def visit(self, node: ast.ASTNode) -> None: """Visit this node of the AST, printing it out to the stream in this class instance. Normally, you will want to call this function on a complete :obj:`~qiskit.qasm3.ast.Program` @@ -131,7 +119,7 @@ def _write_statement(self, line: str) -> None: self._end_statement() def _visit_sequence( - self, nodes: Sequence[ASTNode], *, start: str = "", end: str = "", separator: str + self, nodes: Sequence[ast.ASTNode], *, start: str = "", end: str = "", separator: str ) -> None: if start: self.stream.write(start) @@ -143,48 +131,54 @@ def _visit_sequence( if end: self.stream.write(end) - def _visit_Program(self, node: Program) -> None: + def _visit_Program(self, node: ast.Program) -> None: self.visit(node.header) for statement in node.statements: self.visit(statement) - def _visit_Header(self, node: Header) -> None: + def _visit_Header(self, node: ast.Header) -> None: self.visit(node.version) for include in node.includes: self.visit(include) - def _visit_Version(self, node: Version) -> None: + def _visit_Version(self, node: ast.Version) -> None: self._write_statement(f"OPENQASM {node.version_number}") - def _visit_Include(self, node: Include) -> None: + def _visit_Include(self, node: ast.Include) -> None: self._write_statement(f'include "{node.filename}"') - def _visit_Pragma(self, node: Pragma) -> None: + def _visit_Pragma(self, node: ast.Pragma) -> None: self._write_statement(f"#pragma {node.content}") - def _visit_CalibrationGrammarDeclaration(self, node: CalibrationGrammarDeclaration) -> None: + def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDeclaration) -> None: self._write_statement(f'defcalgrammar "{node.name}"') - def _visit_Identifier(self, node: Identifier) -> None: + def _visit_FloatType(self, node: ast.FloatType) -> None: + self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]") + + def _visit_BitArrayType(self, node: ast.BitArrayType) -> None: + self.stream.write(f"bit[{node.size}]") + + def _visit_Identifier(self, node: ast.Identifier) -> None: self.stream.write(node.string) - def _visit_PhysicalQubitIdentifier(self, node: PhysicalQubitIdentifier) -> None: + def _visit_PhysicalQubitIdentifier(self, node: ast.PhysicalQubitIdentifier) -> None: self.stream.write("$") self.visit(node.identifier) - def _visit_Expression(self, node: Expression) -> None: + def _visit_Expression(self, node: ast.Expression) -> None: self.stream.write(str(node.something)) - def _visit_Constant(self, node: Constant) -> None: + def _visit_Constant(self, node: ast.Constant) -> None: self.stream.write(self._CONSTANT_LOOKUP[node]) - def _visit_SubscriptedIdentifier(self, node: SubscriptedIdentifier) -> None: + def _visit_SubscriptedIdentifier(self, node: ast.SubscriptedIdentifier) -> None: self.visit(node.identifier) self.stream.write("[") self.visit(node.subscript) self.stream.write("]") - def _visit_Range(self, node: Range) -> None: + def _visit_Range(self, node: ast.Range) -> None: if node.start is not None: self.visit(node.start) self.stream.write(":") @@ -194,43 +188,54 @@ def _visit_Range(self, node: Range) -> None: if node.end is not None: self.visit(node.end) - def _visit_QuantumMeasurement(self, node: QuantumMeasurement) -> None: + def _visit_IndexSet(self, node: ast.IndexSet) -> None: + self._visit_sequence(node.values, start="{", separator=", ", end="}") + + def _visit_QuantumMeasurement(self, node: ast.QuantumMeasurement) -> None: self.stream.write("measure ") self._visit_sequence(node.identifierList, separator=", ") - def _visit_QuantumMeasurementAssignment(self, node: QuantumMeasurementAssignment) -> None: + def _visit_QuantumMeasurementAssignment(self, node: ast.QuantumMeasurementAssignment) -> None: self._start_line() self.visit(node.identifier) self.stream.write(" = ") self.visit(node.quantumMeasurement) self._end_statement() - def _visit_QuantumReset(self, node: QuantumReset) -> None: + def _visit_QuantumReset(self, node: ast.QuantumReset) -> None: self._start_line() self.stream.write("reset ") self.visit(node.identifier) self._end_statement() - def _visit_Integer(self, node: Integer) -> None: + def _visit_Integer(self, node: ast.Integer) -> None: self.stream.write(str(node.something)) - def _visit_Designator(self, node: Designator) -> None: + def _visit_Designator(self, node: ast.Designator) -> None: self.stream.write("[") self.visit(node.expression) self.stream.write("]") - def _visit_BitDeclaration(self, node: BitDeclaration) -> None: + def _visit_ClassicalDeclaration(self, node: ast.ClassicalDeclaration) -> None: self._start_line() - self.stream.write("bit") - self.visit(node.designator) + self.visit(node.type) + self.stream.write(" ") + self.visit(node.identifier) + if node.initializer is not None: + self.stream.write(" = ") + self.visit(node.initializer) + self._end_statement() + + def _visit_IODeclaration(self, node: ast.IODeclaration) -> None: + self._start_line() + modifier = "input" if node.modifier is ast.IOModifier.INPUT else "output" + self.stream.write(modifier + " ") + self.visit(node.type) self.stream.write(" ") self.visit(node.identifier) - if node.equalsExpression: - self.stream.write(" ") - self.visit(node.equalsExpression) self._end_statement() - def _visit_QuantumDeclaration(self, node: QuantumDeclaration) -> None: + def _visit_QuantumDeclaration(self, node: ast.QuantumDeclaration) -> None: self._start_line() self.stream.write("qubit") self.visit(node.designator) @@ -238,7 +243,7 @@ def _visit_QuantumDeclaration(self, node: QuantumDeclaration) -> None: self.visit(node.identifier) self._end_statement() - def _visit_AliasStatement(self, node: AliasStatement) -> None: + def _visit_AliasStatement(self, node: ast.AliasStatement) -> None: self._start_line() self.stream.write("let ") self.visit(node.identifier) @@ -246,14 +251,14 @@ def _visit_AliasStatement(self, node: AliasStatement) -> None: self._visit_sequence(node.concatenation, separator=" ++ ") self._end_statement() - def _visit_QuantumGateModifier(self, node: QuantumGateModifier) -> None: + def _visit_QuantumGateModifier(self, node: ast.QuantumGateModifier) -> None: self.stream.write(self._MODIFIER_LOOKUP[node.modifier]) if node.argument: self.stream.write("(") self.visit(node.argument) self.stream.write(")") - def _visit_QuantumGateCall(self, node: QuantumGateCall) -> None: + def _visit_QuantumGateCall(self, node: ast.QuantumGateCall) -> None: self._start_line() if node.modifiers: self._visit_sequence(node.modifiers, end=" @ ", separator=" @ ") @@ -264,7 +269,7 @@ def _visit_QuantumGateCall(self, node: QuantumGateCall) -> None: self._visit_sequence(node.indexIdentifierList, separator=", ") self._end_statement() - def _visit_SubroutineCall(self, node: SubroutineCall) -> None: + def _visit_SubroutineCall(self, node: ast.SubroutineCall) -> None: self._start_line() self.visit(node.identifier) if node.expressionList: @@ -273,13 +278,13 @@ def _visit_SubroutineCall(self, node: SubroutineCall) -> None: self._visit_sequence(node.indexIdentifierList, separator=", ") self._end_statement() - def _visit_QuantumBarrier(self, node: QuantumBarrier) -> None: + def _visit_QuantumBarrier(self, node: ast.QuantumBarrier) -> None: self._start_line() self.stream.write("barrier ") self._visit_sequence(node.indexIdentifierList, separator=", ") self._end_statement() - def _visit_ProgramBlock(self, node: ProgramBlock) -> None: + def _visit_ProgramBlock(self, node: ast.ProgramBlock) -> None: self.stream.write("{\n") self._current_indent += 1 for statement in node.statements: @@ -288,7 +293,7 @@ def _visit_ProgramBlock(self, node: ProgramBlock) -> None: self._start_line() self.stream.write("}") - def _visit_ReturnStatement(self, node: ReturnStatement) -> None: + def _visit_ReturnStatement(self, node: ast.ReturnStatement) -> None: self._start_line() if node.expression: self.stream.write("return ") @@ -297,21 +302,21 @@ def _visit_ReturnStatement(self, node: ReturnStatement) -> None: self.stream.write("return") self._end_statement() - def _visit_QuantumArgument(self, node: QuantumArgument) -> None: + def _visit_QuantumArgument(self, node: ast.QuantumArgument) -> None: self.stream.write("qubit") if node.designator: self.visit(node.designator) self.stream.write(" ") self.visit(node.identifier) - def _visit_QuantumGateSignature(self, node: QuantumGateSignature) -> None: + def _visit_QuantumGateSignature(self, node: ast.QuantumGateSignature) -> None: self.visit(node.name) if node.params: self._visit_sequence(node.params, start="(", end=")", separator=", ") self.stream.write(" ") self._visit_sequence(node.qargList, separator=", ") - def _visit_QuantumGateDefinition(self, node: QuantumGateDefinition) -> None: + def _visit_QuantumGateDefinition(self, node: ast.QuantumGateDefinition) -> None: self._start_line() self.stream.write("gate ") self.visit(node.quantumGateSignature) @@ -319,7 +324,7 @@ def _visit_QuantumGateDefinition(self, node: QuantumGateDefinition) -> None: self.visit(node.quantumBlock) self._end_line() - def _visit_SubroutineDefinition(self, node: SubroutineDefinition) -> None: + def _visit_SubroutineDefinition(self, node: ast.SubroutineDefinition) -> None: self._start_line() self.stream.write("def ") self.visit(node.identifier) @@ -328,7 +333,7 @@ def _visit_SubroutineDefinition(self, node: SubroutineDefinition) -> None: self.visit(node.subroutineBlock) self._end_line() - def _visit_CalibrationDefinition(self, node: CalibrationDefinition) -> None: + def _visit_CalibrationDefinition(self, node: ast.CalibrationDefinition) -> None: self._start_line() self.stream.write("defcal ") self.visit(node.name) @@ -342,38 +347,75 @@ def _visit_CalibrationDefinition(self, node: CalibrationDefinition) -> None: self.stream.write(" {}") self._end_line() - def _visit_LtOperator(self, _node: LtOperator) -> None: + def _visit_LtOperator(self, _node: ast.LtOperator) -> None: self.stream.write(">") - def _visit_GtOperator(self, _node: GtOperator) -> None: + def _visit_GtOperator(self, _node: ast.GtOperator) -> None: self.stream.write("<") - def _visit_EqualsOperator(self, _node: EqualsOperator) -> None: + def _visit_EqualsOperator(self, _node: ast.EqualsOperator) -> None: self.stream.write("==") - def _visit_ComparisonExpression(self, node: ComparisonExpression) -> None: + def _visit_ComparisonExpression(self, node: ast.ComparisonExpression) -> None: self.visit(node.left) self.stream.write(" ") self.visit(node.relation) self.stream.write(" ") self.visit(node.right) - def _visit_BranchingStatement(self, node: BranchingStatement) -> None: - self._start_line() + def _visit_BreakStatement(self, _node: ast.BreakStatement) -> None: + self._write_statement("break") + + def _visit_ContinueStatement(self, _node: ast.ContinueStatement) -> None: + self._write_statement("continue") + + def _visit_BranchingStatement( + self, node: ast.BranchingStatement, chained: bool = False + ) -> None: + if not chained: + self._start_line() self.stream.write("if (") - self.visit(node.booleanExpression) + self.visit(node.condition) self.stream.write(") ") - self.visit(node.programTrue) - if node.programFalse: + self.visit(node.true_body) + if node.false_body is not None: self.stream.write(" else ") - self.visit(node.programFalse) + # Special handling to flatten a perfectly nested structure of + # if {...} else { if {...} else {...} } + # into the simpler + # if {...} else if {...} else {...} + # but only if we're allowed to by our options. + if ( + self._chain_else_if + and len(node.false_body.statements) == 1 + and isinstance(node.false_body.statements[0], ast.BranchingStatement) + ): + self._visit_BranchingStatement(node.false_body.statements[0], chained=True) + else: + self.visit(node.false_body) + if not chained: + # The visitor to the first ``if`` will end the line. + self._end_line() + + def _visit_ForLoopStatement(self, node: ast.ForLoopStatement) -> None: + self._start_line() + self.stream.write("for ") + self.visit(node.parameter) + self.stream.write(" in ") + if isinstance(node.indexset, ast.Range): + self.stream.write("[") + self.visit(node.indexset) + self.stream.write("]") + else: + self.visit(node.indexset) + self.stream.write(" ") + self.visit(node.body) self._end_line() - def _visit_IO(self, node: IO) -> None: + def _visit_WhileLoopStatement(self, node: ast.WhileLoopStatement) -> None: self._start_line() - modifier = "input" if node.modifier == IOModifier.input else "output" - self.stream.write(modifier + " ") - self.visit(node.type) - self.stream.write(" ") - self.visit(node.variable) - self._end_statement() + self.stream.write("while (") + self.visit(node.condition) + self.stream.write(") ") + self.visit(node.body) + self._end_line() diff --git a/test/python/circuit/test_circuit_qasm3.py b/test/python/circuit/test_circuit_qasm3.py index 7df4e5882271..e3a975a3ec6c 100644 --- a/test/python/circuit/test_circuit_qasm3.py +++ b/test/python/circuit/test_circuit_qasm3.py @@ -12,16 +12,24 @@ """Test QASM3 exporter.""" +# We can't really help how long the lines output by the exporter are in some cases. +# pylint: disable=line-too-long + +import unittest from io import StringIO +import ddt + from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile -from qiskit.circuit import Parameter, Qubit +from qiskit.circuit import Parameter, Qubit, Clbit from qiskit.test import QiskitTestCase -from qiskit.qasm3 import Exporter, dumps, dump +from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError +from qiskit.qasm3.exporter import QASM3Builder +from qiskit.qasm3.printer import BasicPrinter from qiskit.qasm import pi -class TestQasm3Functions(QiskitTestCase): +class TestQASM3Functions(QiskitTestCase): """QASM3 module - high level functions""" def setUp(self): @@ -31,8 +39,8 @@ def setUp(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", "U(2*pi, 3*pi, -5*pi) q[0];", "", ] @@ -52,7 +60,7 @@ def test_dump(self): self.assertEqual(result, self.expected_qasm) -class TestCircuitQasm3(QiskitTestCase): +class TestCircuitQASM3(QiskitTestCase): """QASM3 exporter.""" def test_regs_conds_qasm(self): @@ -72,9 +80,9 @@ def test_regs_conds_qasm(self): "OPENQASM 3;", 'include "stdgates.inc";', "bit[3] cr;", - "qubit[3] _q;", - "let qr1 = _q[0:0];", - "let qr2 = _q[1:2];", + "qubit[3] _all_qubits;", + "let qr1 = _all_qubits[0:0];", + "let qr2 = _all_qubits[1:2];", "cr[0] = measure qr1[0];", "cr[1] = measure qr2[0];", "cr[2] = measure qr2[1];", @@ -104,12 +112,12 @@ def test_registers_as_aliases(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "qubit[10] _q;", - "let first_four = _q[0:3];", - "let last_five = _q[5:9];", + "qubit[10] _all_qubits;", + "let first_four = _all_qubits[0:3];", + "let last_five = _all_qubits[5:9];", # The exporter does not attempt to output steps. - "let alternate = _q[0:0] ++ _q[2:2] ++ _q[4:4] ++ _q[6:6] ++ _q[8:8];", - "let sporadic = _q[4:4] ++ _q[2:2] ++ _q[9:9];", + "let alternate = _all_qubits[0:0] ++ _all_qubits[2:2] ++ _all_qubits[4:4] ++ _all_qubits[6:6] ++ _all_qubits[8:8];", + "let sporadic = _all_qubits[4:4] ++ _all_qubits[2:2] ++ _all_qubits[9:9];", "", ] ) @@ -137,14 +145,14 @@ def test_composite_circuit(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "def composite_circ(qubit q_0, qubit q_1) {", - " h q_0;", - " x q_1;", - " cx q_0, q_1;", + "def composite_circ(qubit _gate_q_0, qubit _gate_q_1) {", + " h _gate_q_0;", + " x _gate_q_1;", + " cx _gate_q_0, _gate_q_1;", "}", "bit[2] cr;", - "qubit[2] _q;", - "let qr = _q[0:1];", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", "h qr[0];", "cx qr[0], qr[1];", "barrier qr[0], qr[1];", @@ -178,14 +186,14 @@ def test_custom_gate(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "gate composite_circ q_0, q_1 {", - " h q_0;", - " x q_1;", - " cx q_0, q_1;", + "gate composite_circ _gate_q_0, _gate_q_1 {", + " h _gate_q_0;", + " x _gate_q_1;", + " cx _gate_q_0, _gate_q_1;", "}", "bit[2] cr;", - "qubit[2] _q;", - "let qr = _q[0:1];", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", "h qr[0];", "cx qr[0], qr[1];", "barrier qr[0], qr[1];", @@ -220,14 +228,14 @@ def test_same_composite_circuits(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "def composite_circ(qubit q_0, qubit q_1) {", - " h q_0;", - " x q_1;", - " cx q_0, q_1;", + "def composite_circ(qubit _gate_q_0, qubit _gate_q_1) {", + " h _gate_q_0;", + " x _gate_q_1;", + " cx _gate_q_0, _gate_q_1;", "}", "bit[2] cr;", - "qubit[2] _q;", - "let qr = _q[0:1];", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", "h qr[0];", "cx qr[0], qr[1];", "barrier qr[0], qr[1];", @@ -241,7 +249,8 @@ def test_same_composite_circuits(self): self.assertEqual(Exporter().dumps(qc), expected_qasm) def test_composite_circuits_with_same_name(self): - """Test when multiple composite circuit instructions same name and different implementation""" + """Test when multiple composite circuit instructions same name and different + implementation.""" my_gate = QuantumCircuit(1, name="my_gate") my_gate.h(0) my_gate_inst1 = my_gate.to_instruction() @@ -265,17 +274,17 @@ def test_composite_circuits_with_same_name(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "def my_gate(qubit q_0) {", - " h q_0;", + "def my_gate(qubit _gate_q_0) {", + " h _gate_q_0;", "}", - f"def my_gate_{my_gate_inst2_id}(qubit q_0) {{", - " x q_0;", + f"def my_gate_{my_gate_inst2_id}(qubit _gate_q_0) {{", + " x _gate_q_0;", "}", - f"def my_gate_{my_gate_inst3_id}(qubit q_0) {{", - " x q_0;", + f"def my_gate_{my_gate_inst3_id}(qubit _gate_q_0) {{", + " x _gate_q_0;", "}", - "qubit[1] _q;", - "let qr = _q[0:0];", + "qubit[1] _all_qubits;", + "let qr = _all_qubits[0:0];", "my_gate qr[0];", f"my_gate_{my_gate_inst2_id} qr[0];", f"my_gate_{my_gate_inst3_id} qr[0];", @@ -292,8 +301,8 @@ def test_pi_disable_constants_false(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", "U(2*pi, 3*pi, -5*pi) q[0];", "", ] @@ -308,8 +317,8 @@ def test_pi_disable_constants_true(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", "U(6.283185307179586, 9.42477796076938, -15.707963267948966) q[0];", "", ] @@ -329,12 +338,12 @@ def test_custom_gate_with_unbound_parameter(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "gate custom(a) q_0 {", - " rx(a) q_0;", + "gate custom(a) _gate_q_0 {", + " rx(a) _gate_q_0;", "}", - "input float[32] a;", - "qubit[1] _q;", - "let q = _q[0:0];", + "input float[64] a;", + "qubit[1] _all_qubits;", + "let q = _all_qubits[0:0];", "custom(a) q[0];", "", ] @@ -357,11 +366,11 @@ def test_custom_gate_with_bound_parameter(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "gate custom q_0 {", - " rx(0.5) q_0;", + "gate custom _gate_q_0 {", + " rx(0.5) _gate_q_0;", "}", - "qubit[1] _q;", - "let q = _q[0:0];", + "qubit[1] _all_qubits;", + "let q = _all_qubits[0:0];", "custom q[0];", "", ] @@ -370,39 +379,38 @@ def test_custom_gate_with_bound_parameter(self): def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" - parameter0 = Parameter("param_0") - parameter1 = Parameter("param_1") + parameter0 = Parameter("p0") + parameter1 = Parameter("p1") custom = QuantumCircuit(2, name="custom") custom.rz(parameter0, 0) custom.rz(parameter1 / 2, 1) - qr_q = QuantumRegister(3, "q") + qr_all_qubits = QuantumRegister(3, "q") qr_r = QuantumRegister(3, "r") - circuit = QuantumCircuit(qr_q, qr_r) - circuit.append(custom.to_gate(), [qr_q[0], qr_r[0]]) + circuit = QuantumCircuit(qr_all_qubits, qr_r) + circuit.append(custom.to_gate(), [qr_all_qubits[0], qr_r[0]]) circuit.assign_parameters({parameter0: pi, parameter1: pi / 2}, inplace=True) - qubit_name = circuit.data[0][0].definition.qregs[0].name expected_qasm = "\n".join( [ "OPENQASM 3;", 'include "stdgates.inc";', - f"gate custom(param_0, param_1) {qubit_name}_0, {qubit_name}_1 {{", - f" rz(pi) {qubit_name}_0;", - f" rz(pi/4) {qubit_name}_1;", + "gate custom(p0, p1) _gate_q_0, _gate_q_1 {", + " rz(pi) _gate_q_0;", + " rz(pi/4) _gate_q_1;", "}", - "qubit[6] _q;", - "let q = _q[0:2];", - "let r = _q[3:5];", + "qubit[6] _all_qubits;", + "let q = _all_qubits[0:2];", + "let r = _all_qubits[3:5];", "custom(pi, pi/2) q[0], r[0];", "", ] ) self.assertEqual(Exporter().dumps(circuit), expected_qasm) - def test_reused_custom_gate_parameter(self): + def test_reused_custom_parameter(self): """Test reused custom gate with parameter.""" parameter_a = Parameter("a") @@ -420,14 +428,14 @@ def test_reused_custom_gate_parameter(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - f"gate {circuit_name_0} q_0 {{", - " rx(0.5) q_0;", + f"gate {circuit_name_0} _gate_q_0 {{", + " rx(0.5) _gate_q_0;", "}", - f"gate {circuit_name_1} q_0 {{", - " rx(1) q_0;", + f"gate {circuit_name_1} _gate_q_0 {{", + " rx(1) _gate_q_0;", "}", - "qubit[1] _q;", - "let q = _q[0:0];", + "qubit[1] _all_qubits;", + "let q = _all_qubits[0:0];", f"{circuit_name_0} q[0];", f"{circuit_name_1} q[0];", "", @@ -444,9 +452,9 @@ def test_unbound_circuit(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "input float[32] θ;", - "qubit[1] _q;", - "let q = _q[0:0];", + "input float[64] θ;", + "qubit[1] _all_qubits;", + "let q = _all_qubits[0:0];", "rz(θ) q[0];", "", ] @@ -462,13 +470,13 @@ def test_gate_qasm_with_ctrl_state(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - "gate ch_o0 q_0, q_1 {", - " x q_0;", - " ch q_0, q_1;", - " x q_0;", + "gate ch_o0 _gate_q_0, _gate_q_1 {", + " x _gate_q_0;", + " ch _gate_q_0, _gate_q_1;", + " x _gate_q_0;", "}", - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", "ch_o0 q[0], q[1];", "", ] @@ -488,11 +496,11 @@ def test_custom_gate_collision_with_stdlib(self): [ "OPENQASM 3;", 'include "stdgates.inc";', - f"gate cx_{custom_gate_id} q_0, q_1 {{", - " cx q_0, q_1;", + f"gate cx_{custom_gate_id} _gate_q_0, _gate_q_1 {{", + " cx _gate_q_0, _gate_q_1;", "}", - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", f"cx_{custom_gate_id} q[0], q[1];", "", ] @@ -512,31 +520,31 @@ def test_no_include(self): "gate cx c, t {", " ctrl @ U(pi, 0, pi) c, t;", "}", - "gate u3(param_0, param_1, param_2) q_0 {", - " U(0, 0, pi/2) q_0;", + "gate u3(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {", + " U(0, 0, pi/2) _gate_q_0;", "}", - "gate u1(param_0) q_0 {", - " u3(0, 0, pi/2) q_0;", + "gate u1(_gate_p_0) _gate_q_0 {", + " u3(0, 0, pi/2) _gate_q_0;", "}", - "gate rz(param_0) q_0 {", - " u1(pi/2) q_0;", + "gate rz(_gate_p_0) _gate_q_0 {", + " u1(pi/2) _gate_q_0;", "}", - "gate sdg q_0 {", - " u1(-pi/2) q_0;", + "gate sdg _gate_q_0 {", + " u1(-pi/2) _gate_q_0;", "}", - "gate u2(param_0, param_1) q_0 {", - " u3(pi/2, 0, pi) q_0;", + "gate u2(_gate_p_0, _gate_p_1) _gate_q_0 {", + " u3(pi/2, 0, pi) _gate_q_0;", "}", - "gate h q_0 {", - " u2(0, pi) q_0;", + "gate h _gate_q_0 {", + " u2(0, pi) _gate_q_0;", "}", - "gate sx q_0 {", - " sdg q_0;", - " h q_0;", - " sdg q_0;", + "gate sx _gate_q_0 {", + " sdg _gate_q_0;", + " h _gate_q_0;", + " sdg _gate_q_0;", "}", - "qubit[2] _q;", - "let q = _q[0:1];", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", "rz(pi/2) q[0];", "sx q[0];", "cx q[0], q[1];", @@ -566,23 +574,23 @@ def test_teleportation(self): "gate cx c, t {", " ctrl @ U(pi, 0, pi) c, t;", "}", - "gate u3(param_0, param_1, param_2) q_0 {", - " U(pi/2, 0, pi) q_0;", + "gate u3(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {", + " U(pi/2, 0, pi) _gate_q_0;", "}", - "gate u2(param_0, param_1) q_0 {", - " u3(pi/2, 0, pi) q_0;", + "gate u2(_gate_p_0, _gate_p_1) _gate_q_0 {", + " u3(pi/2, 0, pi) _gate_q_0;", "}", - "gate h q_0 {", - " u2(0, pi) q_0;", + "gate h _gate_q_0 {", + " u2(0, pi) _gate_q_0;", "}", - "gate x q_0 {", - " u3(pi, 0, pi) q_0;", + "gate x _gate_q_0 {", + " u3(pi, 0, pi) _gate_q_0;", "}", - "gate u1(param_0) q_0 {", - " u3(0, 0, pi) q_0;", + "gate u1(_gate_p_0) _gate_q_0 {", + " u3(0, 0, pi) _gate_q_0;", "}", - "gate z q_0 {", - " u1(pi) q_0;", + "gate z _gate_q_0 {", + " u1(pi) _gate_q_0;", "}", "bit[2] c;", "h $1;", @@ -623,17 +631,17 @@ def test_basis_gates(self): expected_qasm = "\n".join( [ "OPENQASM 3;", - "gate u3(param_0, param_1, param_2) q_0 {", - " U(pi/2, 0, pi) q_0;", + "gate u3(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {", + " U(pi/2, 0, pi) _gate_q_0;", "}", - "gate u2(param_0, param_1) q_0 {", - " u3(pi/2, 0, pi) q_0;", + "gate u2(_gate_p_0, _gate_p_1) _gate_q_0 {", + " u3(pi/2, 0, pi) _gate_q_0;", "}", - "gate h q_0 {", - " u2(0, pi) q_0;", + "gate h _gate_q_0 {", + " u2(0, pi) _gate_q_0;", "}", - "gate x q_0 {", - " u3(pi, 0, pi) q_0;", + "gate x _gate_q_0 {", + " u3(pi, 0, pi) _gate_q_0;", "}", "bit[2] c;", "h $1;", @@ -672,14 +680,629 @@ def test_reset_statement(self): expected_qasm = "\n".join( [ "OPENQASM 3;", - "def inner_gate(qubit q_0) {", - " reset q_0;", + "def inner_gate(qubit _gate_q_0) {", + " reset _gate_q_0;", "}", - "qubit[2] _q;", - "let qr = _q[0:1];", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", "reset qr[0];", "inner_gate qr[1];", "", ] ) self.assertEqual(Exporter(includes=[]).dumps(qc), expected_qasm) + + def test_loose_qubits(self): + """Test that qubits that are not in any register can be used without issue.""" + bits = [Qubit(), Qubit()] + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(bits, qr, cr) + qc.h(0) + qc.h(1) + qc.h(2) + qc.h(3) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[4] _all_qubits;", + "let qr = _all_qubits[2:3];", + "h _all_qubits[0];", + "h _all_qubits[1];", + "h qr[0];", + "h qr[1];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_loose_clbits(self): + """Test that clbits that are not in any register can be used without issue.""" + qreg = QuantumRegister(1, name="qr") + bits = [Clbit() for _ in [None] * 7] + cr1 = ClassicalRegister(name="cr1", bits=bits[1:3]) + cr2 = ClassicalRegister(name="cr2", bits=bits[4:6]) + qc = QuantumCircuit(bits, qreg, cr1, cr2) + qc.measure(0, 0) + qc.measure(0, 1) + qc.measure(0, 2) + qc.measure(0, 3) + qc.measure(0, 4) + qc.measure(0, 5) + qc.measure(0, 6) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[3] _loose_clbits;", + "bit[2] cr1;", + "bit[2] cr2;", + "qubit[1] _all_qubits;", + "let qr = _all_qubits[0:0];", + "_loose_clbits[0] = measure qr[0];", + "cr1[0] = measure qr[0];", + "cr1[1] = measure qr[0];", + "_loose_clbits[1] = measure qr[0];", + "cr2[0] = measure qr[0];", + "cr2[1] = measure qr[0];", + "_loose_clbits[2] = measure qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_alias_classical_registers(self): + """Test that clbits that are not in any register can be used without issue.""" + qreg = QuantumRegister(1, name="qr") + bits = [Clbit() for _ in [None] * 7] + cr1 = ClassicalRegister(name="cr1", bits=bits[1:3]) + cr2 = ClassicalRegister(name="cr2", bits=bits[4:6]) + # cr3 overlaps cr2, but this should be allowed in this alias form. + cr3 = ClassicalRegister(name="cr3", bits=bits[5:]) + qc = QuantumCircuit(bits, qreg, cr1, cr2, cr3) + qc.measure(0, 0) + qc.measure(0, 1) + qc.measure(0, 2) + qc.measure(0, 3) + qc.measure(0, 4) + qc.measure(0, 5) + qc.measure(0, 6) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[7] _all_clbits;", + "let cr1 = _all_clbits[1:2];", + "let cr2 = _all_clbits[4:5];", + "let cr3 = _all_clbits[5:6];", + "qubit[1] _all_qubits;", + "let qr = _all_qubits[0:0];", + "_all_clbits[0] = measure qr[0];", + "cr1[0] = measure qr[0];", + "cr1[1] = measure qr[0];", + "_all_clbits[3] = measure qr[0];", + "cr2[0] = measure qr[0];", + "cr2[1] = measure qr[0];", + "cr3[1] = measure qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc, alias_classical_registers=True), expected_qasm) + + def test_simple_for_loop(self): + """Test that a simple for loop outputs the expected result.""" + parameter = Parameter("x") + loop_body = QuantumCircuit(1) + loop_body.rx(parameter, 0) + loop_body.break_loop() + loop_body.continue_loop() + + qc = QuantumCircuit(2) + qc.for_loop(parameter, [0, 3, 4], loop_body, [1], []) + qc.x(0) + + qr_name = qc.qregs[0].name + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "qubit[2] _all_qubits;", + f"let {qr_name} = _all_qubits[0:1];", + f"for {parameter.name} in {{0, 3, 4}} {{", + f" rx({parameter.name}) {qr_name}[1];", + " break;", + " continue;", + "}", + f"x {qr_name}[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_nested_for_loop(self): + """Test that a for loop nested inside another outputs the expected result.""" + inner_parameter = Parameter("x") + outer_parameter = Parameter("y") + + inner_body = QuantumCircuit(2) + inner_body.rz(inner_parameter, 0) + inner_body.rz(outer_parameter, 1) + inner_body.break_loop() + + outer_body = QuantumCircuit(2) + outer_body.h(0) + outer_body.rz(outer_parameter, 1) + # Note we reverse the order of the bits here to test that this is traced. + outer_body.for_loop(inner_parameter, range(1, 5, 2), inner_body, [1, 0], []) + outer_body.continue_loop() + + qc = QuantumCircuit(2) + qc.for_loop(outer_parameter, range(4), outer_body, [0, 1], []) + qc.x(0) + + qr_name = qc.qregs[0].name + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "qubit[2] _all_qubits;", + f"let {qr_name} = _all_qubits[0:1];", + f"for {outer_parameter.name} in [0:3] {{", + f" h {qr_name}[0];", + f" rz({outer_parameter.name}) {qr_name}[1];", + f" for {inner_parameter.name} in [1:2:4] {{", + # Note the reversed bit order. + f" rz({inner_parameter.name}) {qr_name}[1];", + f" rz({outer_parameter.name}) {qr_name}[0];", + " break;", + " }", + " continue;", + "}", + f"x {qr_name}[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + # This test _should_ pass, but the inner "regular" parameter won't get declared in the global + # scope until gh-7280 is closed. "expectedFailure" seems to be ignored by stestr. + @unittest.expectedFailure + def test_regular_parameter_in_nested_for_loop(self): + """Test that a for loop nested inside another outputs the expected result, including + defining parameters that are used in nested loop scopes.""" + inner_parameter = Parameter("x") + outer_parameter = Parameter("y") + regular_parameter = Parameter("t") + + inner_body = QuantumCircuit(2) + inner_body.h(0) + inner_body.rx(regular_parameter, 1) + inner_body.break_loop() + + outer_body = QuantumCircuit(2) + outer_body.h(0) + outer_body.h(1) + # Note we reverse the order of the bits here to test that this is traced. + outer_body.for_loop(inner_parameter, range(1, 5, 2), inner_body, [1, 0], []) + outer_body.continue_loop() + + qc = QuantumCircuit(2) + qc.for_loop(outer_parameter, range(4), outer_body, [0, 1], []) + qc.x(0) + + qr_name = qc.qregs[0].name + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + # This next line will be missing until gh-7280 is fixed. + f"input float[64] {regular_parameter.name};", + "qubit[2] _all_qubits;", + f"let {qr_name} = _all_qubits[0:1];", + f"for {outer_parameter.name} in [0:3] {{", + f" h {qr_name}[0];", + f" h {qr_name}[1];", + f" for {inner_parameter.name} in [1:2:4] {{", + # Note the reversed bit order. + f" h {qr_name}[1];", + f" rx({regular_parameter.name}) {qr_name}[0];", + " break;", + " }", + " continue;", + "}", + f"x {qr_name}[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_for_loop_with_no_parameter(self): + """Test that a for loop with the parameter set to ``None`` outputs the expected result.""" + loop_body = QuantumCircuit(1) + loop_body.h(0) + + qc = QuantumCircuit(2) + qc.for_loop(None, [0, 3, 4], loop_body, [1], []) + qr_name = qc.qregs[0].name + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "qubit[2] _all_qubits;", + f"let {qr_name} = _all_qubits[0:1];", + "for _ in {0, 3, 4} {", + f" h {qr_name}[1];", + "}", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_simple_while_loop(self): + """Test that a simple while loop works correctly.""" + loop_body = QuantumCircuit(1) + loop_body.h(0) + loop_body.break_loop() + loop_body.continue_loop() + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.while_loop((cr, 0), loop_body, [1], []) + qc.x(0) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "while (cr == 0) {", + " h qr[1];", + " break;", + " continue;", + "}", + "x qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_nested_while_loop(self): + """Test that a while loop nested inside another outputs the expected result.""" + inner_body = QuantumCircuit(2, 2) + inner_body.measure(0, 0) + inner_body.measure(1, 1) + inner_body.break_loop() + + outer_body = QuantumCircuit(2, 2) + outer_body.measure(0, 0) + outer_body.measure(1, 1) + # We reverse the order of the bits here to test this works, and test a single-bit condition. + outer_body.while_loop((outer_body.clbits[0], 0), inner_body, [1, 0], [1, 0]) + outer_body.continue_loop() + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.while_loop((cr, 0), outer_body, [0, 1], [0, 1]) + qc.x(0) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "while (cr == 0) {", + " cr[0] = measure qr[0];", + " cr[1] = measure qr[1];", + # Note the reversed bits in the body. + " while (cr[0] == 0) {", + " cr[1] = measure qr[1];", + " cr[0] = measure qr[0];", + " break;", + " }", + " continue;", + "}", + "x qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_simple_if_statement(self): + """Test that a simple if statement with no else works correctly.""" + true_body = QuantumCircuit(1) + true_body.h(0) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.if_test((cr, 0), true_body, [1], []) + qc.x(0) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "if (cr == 0) {", + " h qr[1];", + "}", + "x qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_simple_if_else_statement(self): + """Test that a simple if statement with an else branch works correctly.""" + true_body = QuantumCircuit(1) + true_body.h(0) + false_body = QuantumCircuit(1) + false_body.z(0) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.if_else((cr, 0), true_body, false_body, [1], []) + qc.x(0) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "if (cr == 0) {", + " h qr[1];", + "} else {", + " z qr[1];", + "}", + "x qr[0];", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_nested_if_else_statement(self): + """Test that a nested if/else statement works correctly.""" + inner_true_body = QuantumCircuit(2, 2) + inner_true_body.measure(0, 0) + inner_false_body = QuantumCircuit(2, 2) + inner_false_body.measure(1, 1) + + outer_true_body = QuantumCircuit(2, 2) + outer_true_body.if_else( + (outer_true_body.clbits[0], 0), inner_true_body, inner_false_body, [0, 1], [0, 1] + ) + outer_false_body = QuantumCircuit(2, 2) + # Note the flipped bits here. + outer_false_body.if_else( + (outer_false_body.clbits[0], 1), inner_true_body, inner_false_body, [1, 0], [1, 0] + ) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.if_else((cr, 0), outer_true_body, outer_false_body, [0, 1], [0, 1]) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "if (cr == 0) {", + " if (cr[0] == 0) {", + " cr[0] = measure qr[0];", + " } else {", + " cr[1] = measure qr[1];", + " }", + "} else {", + " if (cr[0] == 1) {", + " cr[1] = measure qr[1];", + " } else {", + " cr[0] = measure qr[0];", + " }", + "}", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + def test_chain_else_if(self): + """Test the basic 'else/if' chaining logic for flattening the else scope if its content is a + single if/else statement.""" + inner_true_body = QuantumCircuit(2, 2) + inner_true_body.measure(0, 0) + inner_false_body = QuantumCircuit(2, 2) + inner_false_body.measure(1, 1) + + outer_true_body = QuantumCircuit(2, 2) + outer_true_body.if_else( + (outer_true_body.clbits[0], 0), inner_true_body, inner_false_body, [0, 1], [0, 1] + ) + outer_false_body = QuantumCircuit(2, 2) + # Note the flipped bits here. + outer_false_body.if_else( + (outer_false_body.clbits[0], 1), inner_true_body, inner_false_body, [1, 0], [1, 0] + ) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.if_else((cr, 0), outer_true_body, outer_false_body, [0, 1], [0, 1]) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "if (cr == 0) {", + " if (cr[0] == 0) {", + " cr[0] = measure qr[0];", + " } else {", + " cr[1] = measure qr[1];", + " }", + "} else if (cr[0] == 1) {", + " cr[1] = measure qr[1];", + "} else {", + " cr[0] = measure qr[0];", + "}", + "", + ] + ) + # This is not the default behaviour, and it's pretty buried how you'd access it. + builder = QASM3Builder( + qc, + includeslist=("stdgates.inc",), + basis_gates=("U",), + disable_constants=False, + alias_classical_registers=False, + ) + stream = StringIO() + BasicPrinter(stream, indent=" ", chain_else_if=True).visit(builder.build_program()) + self.assertEqual(stream.getvalue(), expected_qasm) + + def test_chain_else_if_does_not_chain_if_extra_instructions(self): + """Test the basic 'else/if' chaining logic for flattening the else scope if its content is a + single if/else statement does not cause a flattening if the 'else' block is not a single + if/else.""" + inner_true_body = QuantumCircuit(2, 2) + inner_true_body.measure(0, 0) + inner_false_body = QuantumCircuit(2, 2) + inner_false_body.measure(1, 1) + + outer_true_body = QuantumCircuit(2, 2) + outer_true_body.if_else( + (outer_true_body.clbits[0], 0), inner_true_body, inner_false_body, [0, 1], [0, 1] + ) + outer_false_body = QuantumCircuit(2, 2) + # Note the flipped bits here. + outer_false_body.if_else( + (outer_false_body.clbits[0], 1), inner_true_body, inner_false_body, [1, 0], [1, 0] + ) + outer_false_body.h(0) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + qc = QuantumCircuit(qr, cr) + qc.if_else((cr, 0), outer_true_body, outer_false_body, [0, 1], [0, 1]) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "bit[2] cr;", + "qubit[2] _all_qubits;", + "let qr = _all_qubits[0:1];", + "if (cr == 0) {", + " if (cr[0] == 0) {", + " cr[0] = measure qr[0];", + " } else {", + " cr[1] = measure qr[1];", + " }", + "} else {", + " if (cr[0] == 1) {", + " cr[1] = measure qr[1];", + " } else {", + " cr[0] = measure qr[0];", + " }", + " h qr[0];", + "}", + "", + ] + ) + # This is not the default behaviour, and it's pretty buried how you'd access it. + builder = QASM3Builder( + qc, + includeslist=("stdgates.inc",), + basis_gates=("U",), + disable_constants=False, + alias_classical_registers=False, + ) + stream = StringIO() + BasicPrinter(stream, indent=" ", chain_else_if=True).visit(builder.build_program()) + self.assertEqual(stream.getvalue(), expected_qasm) + + def test_custom_gate_used_in_loop_scope(self): + """Test that a custom gate only used within a loop scope still gets a definition at the top + level.""" + parameter_a = Parameter("a") + parameter_b = Parameter("b") + + custom = QuantumCircuit(1) + custom.rx(parameter_a, 0) + custom_gate = custom.bind_parameters({parameter_a: 0.5}).to_gate() + custom_gate.name = "custom" + + loop_body = QuantumCircuit(1) + loop_body.append(custom_gate, [0]) + + qc = QuantumCircuit(1) + qc.for_loop(parameter_b, range(2), loop_body, [0], []) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + "gate custom _gate_q_0 {", + " rx(0.5) _gate_q_0;", + "}", + "qubit[1] _all_qubits;", + "let q = _all_qubits[0:0];", + "for b in [0:1] {", + " custom q[0];", + "}", + "", + ] + ) + self.assertEqual(dumps(qc), expected_qasm) + + +@ddt.ddt +class TestQASM3ExporterFailurePaths(QiskitTestCase): + """Tests of the failure paths for the exporter.""" + + def test_disallow_overlapping_classical_registers_if_no_aliasing(self): + """Test that the exporter rejects circuits with a classical bit in more than one register if + the ``alias_classical_registers`` option is set false.""" + qubits = [Qubit() for _ in [None] * 3] + clbits = [Clbit() for _ in [None] * 5] + registers = [ClassicalRegister(bits=clbits[:4]), ClassicalRegister(bits=clbits[1:])] + qc = QuantumCircuit(qubits, *registers) + exporter = Exporter(alias_classical_registers=False) + with self.assertRaisesRegex(QASM3ExporterError, r"Clbit .* is in multiple registers.*"): + exporter.dumps(qc) + + @ddt.data([1, 2, 1.1], [1j, 2]) + def test_disallow_for_loops_with_non_integers(self, indices): + """Test that the exporter rejects ``for`` loops that include non-integer values in their + index sets.""" + loop_body = QuantumCircuit() + qc = QuantumCircuit(2, 2) + qc.for_loop(None, indices, loop_body, [], []) + exporter = Exporter() + with self.assertRaisesRegex( + QASM3ExporterError, r"The values in QASM 3 'for' loops must all be integers.*" + ): + exporter.dumps(qc)