diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 1eed72347da6..4eb50d7e5014 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -359,6 +359,16 @@ impl StandardGate { pub fn get_name(&self) -> &str { self.name() } + + pub fn __eq__(&self, other: &Bound) -> Py { + let py = other.py(); + let Ok(other) = other.extract::() else { return py.NotImplemented() }; + (*self == other).into_py(py) + } + + pub fn __hash__(&self) -> isize { + *self as isize + } } // This must be kept up-to-date with `StandardGate` when adding or removing diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 0bae60144afc..eaac830edf65 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -456,39 +456,23 @@ class SubroutineBlock(ProgramBlock): pass -class QuantumArgument(QuantumDeclaration): - """ - quantumArgument - : 'qreg' Identifier designator? | 'qubit' designator? Identifier - """ - - -class QuantumGateSignature(ASTNode): +class QuantumGateDefinition(Statement): """ - quantumGateSignature - : quantumGateName ( LPAREN identifierList? RPAREN )? identifierList + quantumGateDefinition + : 'gate' quantumGateSignature quantumBlock """ def __init__( self, name: Identifier, - qargList: List[Identifier], - params: Optional[List[Expression]] = None, + params: Tuple[Identifier, ...], + qubits: Tuple[Identifier, ...], + body: QuantumBlock, ): self.name = name - self.qargList = qargList self.params = params - - -class QuantumGateDefinition(Statement): - """ - quantumGateDefinition - : 'gate' quantumGateSignature quantumBlock - """ - - def __init__(self, quantumGateSignature: QuantumGateSignature, quantumBlock: QuantumBlock): - self.quantumGateSignature = quantumGateSignature - self.quantumBlock = quantumBlock + self.qubits = qubits + self.body = body class SubroutineDefinition(Statement): diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 6d5344bcc255..6cae1c54b58e 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -12,20 +12,26 @@ """QASM3 Exporter""" +from __future__ import annotations + import collections -import re +import contextlib +import dataclasses import io import itertools +import math import numbers -from os.path import dirname, join, abspath +import re from typing import Iterable, List, Sequence, Union +from qiskit._accelerate.circuit import StandardGate from qiskit.circuit import ( + library, Barrier, CircuitInstruction, Clbit, + ControlledGate, Gate, - Instruction, Measure, Parameter, ParameterExpression, @@ -47,7 +53,6 @@ ContinueLoopOp, CASE_DEFAULT, ) -from qiskit.circuit.library import standard_gates from qiskit.circuit.register import Register from qiskit.circuit.tools import pi_check @@ -115,13 +120,8 @@ # This probably isn't precisely the same as the OQ3 spec, but we'd need an extra dependency to fully # handle all Unicode character classes, and this should be close enough for users who aren't # actively _trying_ to break us (fingers crossed). -_VALID_IDENTIFIER = re.compile(r"[\w][\w\d]*", flags=re.U) - - -def _escape_invalid_identifier(name: str) -> str: - if name in _RESERVED_KEYWORDS or not _VALID_IDENTIFIER.fullmatch(name): - name = "_" + re.sub(r"[^\w\d]", "_", name) - return name +_VALID_IDENTIFIER = re.compile(r"(\$[\d]+|[\w][\w\d]*)", flags=re.U) +_BAD_IDENTIFIER_CHARACTERS = re.compile(r"[^\w\d]", flags=re.U) class Exporter: @@ -196,139 +196,308 @@ def dump(self, circuit, stream): ) -class GlobalNamespace: - """Global namespace dict-like.""" - - BASIS_GATE = object() - - qiskit_gates = { - "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"))] +# Just needs to have enough parameters to support the largest standard (non-controlled) gate in our +# standard library. We have to use the same `Parameter` instances each time so the equality +# comparisons will work. +_FIXED_PARAMETERS = (Parameter("p0"), Parameter("p1"), Parameter("p2"), Parameter("p3")) + +# Mapping of symbols defined by `stdgates.inc` to their gate definition source. +_KNOWN_INCLUDES = { + "stdgates.inc": { + "p": library.PhaseGate(*_FIXED_PARAMETERS[:1]), + "x": library.XGate(), + "y": library.YGate(), + "z": library.ZGate(), + "h": library.HGate(), + "s": library.SGate(), + "sdg": library.SdgGate(), + "t": library.TGate(), + "tdg": library.TdgGate(), + "sx": library.SXGate(), + "rx": library.RXGate(*_FIXED_PARAMETERS[:1]), + "ry": library.RYGate(*_FIXED_PARAMETERS[:1]), + "rz": library.RZGate(*_FIXED_PARAMETERS[:1]), + "cx": library.CXGate(), + "cy": library.CYGate(), + "cz": library.CZGate(), + "cp": library.CPhaseGate(*_FIXED_PARAMETERS[:1]), + "crx": library.CRXGate(*_FIXED_PARAMETERS[:1]), + "cry": library.CRYGate(*_FIXED_PARAMETERS[:1]), + "crz": library.CRZGate(*_FIXED_PARAMETERS[:1]), + "ch": library.CHGate(), + "swap": library.SwapGate(), + "ccx": library.CCXGate(), + "cswap": library.CSwapGate(), + "cu": library.CUGate(*_FIXED_PARAMETERS[:4]), + "CX": library.CXGate(), + "phase": library.PhaseGate(*_FIXED_PARAMETERS[:1]), + "cphase": library.CPhaseGate(*_FIXED_PARAMETERS[:1]), + "id": library.IGate(), + "u1": library.U1Gate(*_FIXED_PARAMETERS[:1]), + "u2": library.U2Gate(*_FIXED_PARAMETERS[:2]), + "u3": library.U3Gate(*_FIXED_PARAMETERS[:3]), + }, +} + +_BUILTIN_GATES = { + "U": library.UGate(*_FIXED_PARAMETERS[:3]), +} + + +@dataclasses.dataclass +class GateDefinition: + """Symbol-table information on a gate.""" + + source: Gate | None + """The definition source of the gate. This can be ``None`` if the gate was an overridden "basis + gate" for this export.""" + node: ast.QuantumGateDefinition | None + """An AST node containing the gate definition. This can be ``None`` if the gate came from an + included file, or is an overridden "basis gate" of the export.""" + + +class SymbolTable: + """Track Qiskit objects and the OQ3 identifiers used to refer to them.""" + + def __init__(self): + self.gates: collections.OrderedDict[str, GateDefinition | None] = {} + """Mapping of the symbol name to the "definition source" of the gate, which provides its + signature and decomposition. The definition source can be `None` if the user set the gate + as a custom "basis gate". + + Gates can only be declared in the global scope, so there is just a single look-up for this. + + This is insertion ordered, and that can be relied on for iteration later.""" + self.standard_gate_idents: dict[StandardGate, ast.Identifier] = {} + """Mapping of standard gate enumeration values to the identifier we represent that as.""" + self.user_gate_idents: dict[int, ast.Identifier] = {} + """Mapping of `id`s of user gates to the identifier we use for it.""" + + self.variables: list[dict[str, object]] = [{}] + """Stack of mappings of variable names to the Qiskit object that represents them. + + The zeroth index corresponds to the global scope, the highest index to the current scope.""" + self.objects: list[dict[object, ast.Identifier]] = [{}] + """Stack of mappings of Qiskit objects to the identifier (or subscripted identifier) that + refers to them. This is similar to the inverse mapping of ``variables``. + + The zeroth index corresponds to the global scope, the highest index to the current scope.""" + + # Quick-and-dirty method of getting unique salts for names. + self._counter = itertools.count() - def __init__(self, includelist, basis_gates=()): - self._data = {gate: self.BASIS_GATE for gate in basis_gates} - self._data["U"] = self.BASIS_GATE + def push_scope(self): + """Enter a new variable scope.""" + self.variables.append({}) + self.objects.append({}) + + def pop_scope(self): + """Exit the current scope, returning to a previous scope.""" + self.objects.pop() + self.variables.pop() + + def new_context(self) -> SymbolTable: + """Create a new context, such as for a gate definition. + + Contexts share the same set of globally defined gates, but have no access to other variables + defined in any scope.""" + out = SymbolTable() + out.gates = self.gates + out.standard_gate_idents = self.standard_gate_idents + out.user_gate_idents = self.user_gate_idents + return out - for includefile in includelist: - if includefile == "stdgates.inc": - self._data.update(self.qiskit_gates) - else: - # TODO What do if an inc file is not standard? - # Should it be parsed? - pass - - def __setitem__(self, name_str, instruction): - self._data[name_str] = instruction.base_class - self._data[id(instruction)] = name_str - ctrl_state = str(getattr(instruction, "ctrl_state", "")) - - self._data[f"{instruction.name}_{ctrl_state}_{instruction.params}"] = name_str - - def __getitem__(self, key): - if isinstance(key, Instruction): - try: - # Registered gates. - return self._data[id(key)] - except KeyError: - pass - # Built-in gates. - if key.name not in self._data: - # Registerd qiskit standard gate without stgates.inc - ctrl_state = str(getattr(key, "ctrl_state", "")) - return self._data[f"{key.name}_{ctrl_state}_{key.params}"] - return key.name - return self._data[key] - - def __iter__(self): - return iter(self._data) - - def __contains__(self, instruction): - if isinstance(instruction, standard_gates.UGate): - return True - if id(instruction) in self._data: - return True - if self._data.get(instruction.name) is self.BASIS_GATE: - return True - if type(instruction) in [Gate, Instruction]: # user-defined instructions/gate - return self._data.get(instruction.name, None) == instruction - type_ = self._data.get(instruction.name) - if isinstance(type_, type) and isinstance(instruction, type_): - return True - return False + def symbol_defined(self, name: str) -> bool: + """Whether this identifier has a defined meaning already.""" + return ( + name in _RESERVED_KEYWORDS + or name in self.gates + or name in itertools.chain.from_iterable(reversed(self.variables)) + ) - def has_symbol(self, name: str) -> bool: - """Whether a symbol's name is present in the table.""" - return name in self._data - - def register(self, instruction): - """Register an instruction in the namespace""" - # The second part of the condition is a nasty hack to ensure that gates that come with at - # least one parameter always have their id in the name. This is a workaround a bug, where - # gates with parameters do not contain the information required to build the gate definition - # in symbolic form (unless the parameters are all symbolic). The exporter currently - # (2021-12-01) builds gate declarations with parameters in the signature, but then ignores - # those parameters during the body, and just uses the concrete values from the first - # instance of the gate it sees, such as: - # gate rzx(_gate_p_0) _gate_q_0, _gate_q_1 { - # h _gate_q_1; - # cx _gate_q_0, _gate_q_1; - # rz(0.2) _gate_q_1; // <- note the concrete value. - # cx _gate_q_0, _gate_q_1; - # h _gate_q_1; - # } - # This then means that multiple calls to the same gate with different parameters will be - # incorrect. By forcing all gates to be defined including their id, we generate a QASM3 - # program that does what was intended, even though the output QASM3 is silly. See gh-7335. - if instruction.name in self._data or ( - isinstance(instruction, Gate) - and not all(isinstance(param, Parameter) for param in instruction.params) - ): - key = f"{instruction.name}_{id(instruction)}" - else: - key = instruction.name - self[key] = instruction + def can_shadow_symbol(self, name: str) -> bool: + """Whether a new definition of this symbol can be made within the OpenQASM 3 shadowing + rules.""" + return ( + name not in self.variables[-1] + and name not in self.gates + and name not in _RESERVED_KEYWORDS + ) + + def escaped_declarable_name(self, name: str, *, allow_rename: bool, unique: bool = False): + """Get an identifier based on ``name`` that can be safely shadowed within this scope. + + If ``unique`` is ``True``, then the name is required to be unique across all live scopes, + not just able to be redefined.""" + name_allowed = ( + (lambda name: not self.symbol_defined(name)) if unique else self.can_shadow_symbol + ) + if allow_rename: + if not _VALID_IDENTIFIER.fullmatch(name): + name = "_" + _BAD_IDENTIFIER_CHARACTERS.sub("_", name) + base = name + while not name_allowed(name): + name = f"{base}_{next(self._counter)}" + return name + if not _VALID_IDENTIFIER.fullmatch(name): + raise QASM3ExporterError(f"cannot use '{name}' as a name; it is not a valid identifier") + if name in _RESERVED_KEYWORDS: + raise QASM3ExporterError(f"cannot use the keyword '{name}' as a variable name") + if not name_allowed(name): + if self.gates.get(name) is not None: + raise QASM3ExporterError( + f"cannot shadow variable '{name}', as it is already defined as a gate" + ) + for scope in reversed(self.variables): + if (other := scope.get(name)) is not None: + break + else: # pragma: no cover + raise RuntimeError(f"internal error: could not locate unshadowable '{name}'") + raise QASM3ExporterError( + f"cannot shadow variable '{name}', as it is already defined as '{other}'" + ) + return name + + def register_variable( + self, + name: str, + variable: object, + *, + allow_rename: bool, + global_: bool = False, + ) -> ast.Identifier: + """Register a variable in the symbol table for the given scope, returning the name that + should be used to refer to the variable. The same name will be returned by subsequent calls + to :meth:`get_variable` within the same scope. + Args: + name: the name to base the identifier on. + variable: the Qiskit object this refers to. This can be ``None`` in the case of + reserving a dummy variable name that does not actually have a Qiskit object backing + it. + allow_rename: whether to allow the name to be mutated to escape it and/or make it safe + to define (avoiding keywords, subject to shadowing rules, etc). + global_: force this declaration to be in the global scope. + """ + scope_index = 0 if global_ else -1 + # We still need to do this escaping and shadow checking if `global_`, because we don't want + # a previous variable declared in the currently active scope to shadow the global. This + # kind of logic would be cleaner if we made the naming choices later, after AST generation + # (e.g. by using only indices as the identifiers until we're ready to output the program). + name = self.escaped_declarable_name(name, allow_rename=allow_rename, unique=global_) + identifier = ast.Identifier(name) + self.variables[scope_index][name] = variable + if variable is not None: + self.objects[scope_index][variable] = identifier + return identifier + + def set_object_ident(self, ident: ast.Identifier, variable: object): + """Set the identifier used to refer to a given object for this scope. + + This overwrites any previously set identifier, such as during the original registration. + + This is generally only useful for tracking "sub" objects, like bits out of a register, which + will have an `SubscriptedIdentifier` as their identifier.""" + self.objects[-1][variable] = ident + + def get_variable(self, variable: object) -> ast.Identifier: + """Lookup a non-gate variable in the symbol table.""" + for scope in reversed(self.objects): + if (out := scope.get(variable)) is not None: + return out + raise KeyError(f"'{variable}' is not defined in the current context") + + def register_gate_without_definition(self, name: str, gate: Gate | None) -> ast.Identifier: + """Register a gate that does not require an OQ3 definition. + + If the ``gate`` is given, it will be used to validate that a call to it is compatible (such + as a known gate from an included file). If it is not given, it is treated as a user-defined + "basis gate" that assumes that all calling signatures are valid and that all gates of this + name are exactly compatible, which is somewhat dangerous.""" + # Validate the name is usable. + name = self.escaped_declarable_name(name, allow_rename=False) + ident = ast.Identifier(name) + if gate is None: + self.gates[name] = GateDefinition(None, None) + else: + source = _gate_definition_source(gate) + self.gates[name] = GateDefinition(source, None) + if (standard_gate := getattr(source, "_standard_gate")) is not None: + self.standard_gate_idents[standard_gate] = ident + else: + self.user_gate_idents[id(source)] = ident + return ident -# 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. 'symbol_map' is a bidirectional mapping of -# ': Identifier' and 'str: ', where the string in the second map is the -# name of the identifier. This is a cheap hack around actually implementing a proper symbol table. -_Scope = collections.namedtuple("_Scope", ("circuit", "bit_map", "symbol_map")) + def register_gate( + self, + name: str, + source: Gate, + params: Iterable[ast.Identifier], + qubits: Iterable[ast.Identifier], + body: ast.QuantumBlock, + ) -> ast.Identifier: + """Register the given gate in the symbol table, using the given components to build up the + full AST definition.""" + name = self.escaped_declarable_name(name, allow_rename=True) + ident = ast.Identifier(name) + self.gates[name] = GateDefinition( + source, ast.QuantumGateDefinition(ident, tuple(params), tuple(qubits), body) + ) + # Add the gate object with a magic lookup keep to the objects dictionary so we can retrieve + # it later. Standard gates are not guaranteed to have stable IDs (they're preferentially + # not even created in Python space), but user gates are. + if (standard_gate := getattr(source, "_standard_gate")) is not None: + self.standard_gate_idents[standard_gate] = ident + else: + self.user_gate_idents[id(source)] = ident + return ident + + def get_gate(self, gate: Gate) -> ast.Identifier | None: + """Lookup the identifier for a given `Gate`, if it exists.""" + source = _gate_definition_source(gate) + # `our_defn.source` means a basis gate that we should assume is always valid. + if (our_defn := self.gates.get(gate.name)) is not None and ( + our_defn.source is None or our_defn.source == source + ): + return ast.Identifier(gate.name) + if (standard_gate := getattr(source, "_standard_gate")) is not None: + if (our_ident := self.standard_gate_idents.get(standard_gate)) is None: + return None + return our_ident if self.gates[our_ident.string].source == source else None + # No need to check equality if we're looking up by `id`; we must have the same object. + return self.user_gate_idents.get(id(source)) + + +def _gate_definition_source(gate: Gate) -> Gate: + """Get the "definition source" of a gate. + + This is the gate object that should be used to provide the OpenQASM 3 definition of a gate (but + not the call site; that's the input object). This lets us return a re-parametrised gate in + terms of general parameters, in cases where we can be sure that that is valid. This is + currently only Qiskit standard gates. + + The definition source provides the number of qubits, the parameter signature and the body of the + `gate` statement. It does not provide the name of the symbol being defined.""" + # If a gate is part of the Qiskit standard-library gates, we know we can safely produce a + # reparameterised gate by passing the parameters positionally to the standard-gate constructor + # (and control state, if appropriate). + if gate._standard_gate and not isinstance(gate, ControlledGate): + return gate.base_class(*_FIXED_PARAMETERS[: len(gate.params)]) + elif gate._standard_gate: + return gate.base_class(*_FIXED_PARAMETERS[: len(gate.params)], ctrl_state=gate.ctrl_state) + return gate + + +@dataclasses.dataclass +class BuildScope: + """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.""" + + circuit: QuantumCircuit + """The circuit block that we're currently working on exporting.""" + bit_map: dict[Bit, Bit] + """Mapping of bit objects in ``circuit`` to the bit objects in the global-scope program + :class:`.QuantumCircuit` that they are bound to.""" class QASM3Builder: @@ -349,13 +518,11 @@ def __init__( allow_aliasing, experimental=ExperimentalFeatures(0), ): - # 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.scope = BuildScope( + quantumcircuit, + {x: x for x in itertools.chain(quantumcircuit.qubits, quantumcircuit.clbits)}, + ) + self.symbols = SymbolTable() # `_global_io_declarations` and `_global_classical_declarations` are stateful, and any # operation that needs a parameter can append to them during the build. We make all # classical declarations global because the IBM qe-compiler stack (our initial consumer of @@ -364,108 +531,86 @@ def __init__( # in the near term. self._global_io_declarations = [] self._global_classical_forward_declarations = [] - # An arbitrary counter to help with generation of unique ids for symbol names when there are - # clashes (though we generally prefer to keep user names if possible). - self._counter = itertools.count() self.disable_constants = disable_constants self.allow_aliasing = allow_aliasing - self.global_namespace = GlobalNamespace(includeslist, basis_gates) + self.includes = includeslist + self.basis_gates = basis_gates self.experimental = experimental - def _unique_name(self, prefix: str, scope: _Scope) -> str: - table = scope.symbol_map - name = basename = _escape_invalid_identifier(prefix) - while name in table or name in _RESERVED_KEYWORDS or self.global_namespace.has_symbol(name): - name = f"{basename}__generated{next(self._counter)}" - return name - - def _register_gate(self, gate): - self.global_namespace.register(gate) - - def _register_opaque(self, instruction): - self.global_namespace.register(instruction) - - def _register_variable(self, variable, scope: _Scope, name=None) -> ast.Identifier: - """Register a variable in the symbol table for the given scope, returning the name that - should be used to refer to the variable. The same name will be returned by subsequent calls - to :meth:`_lookup_variable` within the same scope. - - If ``name`` is given explicitly, it must not already be defined in the scope. - """ - # Note that the registration only checks for the existence of a variable that was declared - # in the current scope, not just one that's available. This is a rough implementation of - # the shadowing proposal currently being drafted for OpenQASM 3, though we expect it to be - # expanded and modified in the future (2022-03-07). - table = scope.symbol_map - if name is not None: - if name in _RESERVED_KEYWORDS: - raise QASM3ExporterError(f"cannot reserve the keyword '{name}' as a variable name") - if name in table: - raise QASM3ExporterError( - f"tried to reserve '{name}', but it is already used by '{table[name]}'" - ) - if self.global_namespace.has_symbol(name): - raise QASM3ExporterError( - f"tried to reserve '{name}', but it is already used by a gate" - ) - else: - name = self._unique_name(variable.name, scope) - identifier = ast.Identifier(name) - table[identifier.string] = variable - table[variable] = identifier - return identifier - - def _reserve_variable_name(self, name: ast.Identifier, scope: _Scope) -> ast.Identifier: - """Reserve a variable name in the given scope, raising a :class:`.QASM3ExporterError` if - the name is already in use. - - This is useful for autogenerated names that the exporter itself reserves when dealing with - objects that have no standard Terra object backing them. - - Returns the same identifier, for convenience in chaining.""" - table = scope.symbol_map - if name.string in table: - variable = table[name.string] - raise QASM3ExporterError( - f"tried to reserve '{name.string}', but it is already used by '{variable}'" + @contextlib.contextmanager + def new_scope(self, circuit: QuantumCircuit, qubits: Iterable[Qubit], clbits: Iterable[Clbit]): + """Context manager that pushes a new scope (like a ``for`` or ``while`` loop body) onto the + current context stack.""" + current_map = self.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." ) - table[name.string] = "" - return name + 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.symbols.push_scope() + old_scope, self.scope = self.scope, BuildScope(circuit, mapping) + yield self.scope + self.scope = old_scope + self.symbols.pop_scope() + + @contextlib.contextmanager + def new_context(self, body: QuantumCircuit): + """Push a new context (like for a ``gate`` or ``def`` body) onto the stack.""" + mapping = {bit: bit for bit in itertools.chain(body.qubits, body.clbits)} + + old_symbols, self.symbols = self.symbols, self.symbols.new_context() + old_scope, self.scope = self.scope, BuildScope(body, mapping) + yield self.scope + self.scope = old_scope + self.symbols = old_symbols def _lookup_variable(self, variable) -> ast.Identifier: - """Lookup a Terra object within the current context, and return the name that should be used - to represent it in OpenQASM 3 programmes.""" + """Lookup a Qiskit object within the current context, and return the name that should be + used to represent it in OpenQASM 3 programmes.""" if isinstance(variable, Bit): - variable = self.current_scope().bit_map[variable] - for scope in reversed(self.current_context()): - if variable in scope.symbol_map: - return scope.symbol_map[variable] - raise KeyError(f"'{variable}' is not defined in the current context") - - def build_header(self): - """Builds a Header""" - version = ast.Version("3.0") - includes = self.build_includes() - return ast.Header(version, includes) + variable = self.scope.bit_map[variable] + return self.symbols.get_variable(variable) def build_program(self): """Builds a Program""" - circuit = self.global_scope(assert_=True).circuit + circuit = self.scope.circuit if circuit.num_captured_vars: raise QASM3ExporterError( "cannot export an inner scope with captured variables as a top-level program" ) - header = self.build_header() - opaques_to_declare, gates_to_declare = self.hoist_declarations( - circuit.data, opaques=[], gates=[] - ) - opaque_definitions = [ - self.build_opaque_definition(instruction) for instruction in opaques_to_declare - ] - gate_definitions = [ - self.build_gate_definition(instruction) for instruction in gates_to_declare - ] + # The order we build parts of the AST has an effect on which names will get escaped to avoid + # collisions. The current ideas are: + # + # * standard-library include files _must_ define symbols of the correct name. + # * classical registers, IO variables and `Var` nodes are likely to be referred to by name + # by a user, so they get very high priority - we search for them before doing anything. + # * qubit registers are not typically referred to by name by users, so they get a lower + # priority than the classical variables. + # * we often have to escape user-defined gate names anyway because of our dodgy parameter + # handling, so they get the lowest priority; they get defined as they are encountered. + # + # An alternative approach would be to defer naming decisions until we are outputting the + # AST, and using some UUID for each symbol we're going to define in the interrim. This + # would require relatively large changes to the symbol-table and AST handling, however. + + for builtin, gate in _BUILTIN_GATES.items(): + self.symbols.register_gate_without_definition(builtin, gate) + for builtin in self.basis_gates: + if builtin in _BUILTIN_GATES: + # It's built into the langauge; we don't need to re-add it. + continue + self.symbols.register_gate_without_definition(builtin, None) + + header = ast.Header(ast.Version("3.0"), list(self.build_includes())) # Early IBM runtime parametrization uses unbound `Parameter` instances as `input` variables, # not the explicit realtime `Var` variables, so we need this explicit scan. @@ -481,9 +626,11 @@ def build_program(self): # Similarly, QuantumCircuit qubits/registers are only new variables in the global scope. quantum_declarations = self.build_quantum_declarations() + # This call has side-effects - it can populate `self._global_io_declarations` and # `self._global_classical_declarations` as a courtesy to the qe-compiler that prefers our - # hacky temporary `switch` target variables to be globally defined. + # hacky temporary `switch` target variables to be globally defined. It also populates the + # symbol table with encountered gates that weren't previously defined. main_statements = self.build_current_scope() statements = [ @@ -492,8 +639,7 @@ def build_program(self): # In older versions of the reference OQ3 grammar, IO declarations had to come before # anything else, so we keep doing that as a courtesy. self._global_io_declarations, - opaque_definitions, - gate_definitions, + (gate.node for gate in self.symbols.gates.values() if gate.node is not None), self._global_classical_forward_declarations, quantum_declarations, main_statements, @@ -502,172 +648,98 @@ def build_program(self): ] return ast.Program(header, statements) - def hoist_declarations(self, instructions, *, opaques, gates): - """Walks the definitions in gates/instructions to make a list of gates to declare. - - Mutates ``opaques`` and ``gates`` in-place if given, and returns them.""" - for instruction in instructions: - if isinstance(instruction.operation, ControlFlowOp): - for block in instruction.operation.blocks: - self.hoist_declarations(block.data, opaques=opaques, gates=gates) - continue - if instruction.operation in self.global_namespace or isinstance( - instruction.operation, self.builtins - ): - continue - - if isinstance(instruction.operation, standard_gates.CXGate): - # CX gets super duper special treatment because it's the base of Terra's definition - # tree, but isn't an OQ3 built-in. We use `isinstance` because we haven't fully - # fixed what the name/class distinction is (there's a test from the original OQ3 - # exporter that tries a naming collision with 'cx'). - self._register_gate(instruction.operation) - gates.append(instruction.operation) - elif instruction.operation.definition is None: - self._register_opaque(instruction.operation) - opaques.append(instruction.operation) - elif not isinstance(instruction.operation, Gate): - raise QASM3ExporterError("Exporting non-unitary instructions is not yet supported.") - else: - self.hoist_declarations( - instruction.operation.definition.data, opaques=opaques, gates=gates - ) - self._register_gate(instruction.operation) - gates.append(instruction.operation) - return opaques, gates - - 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_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.current_context().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 [ast.Include(filename) for filename in self.includeslist] - - def build_opaque_definition(self, instruction): - """Builds an Opaque gate definition as a CalibrationDefinition""" - # We can't do anything sensible with this yet, so it's better to loudly say that. - raise QASM3ExporterError( - "Exporting opaque instructions with pulse-level calibrations is not yet supported by" - " the OpenQASM 3 exporter. Received this instruction, which appears opaque:" - f"\n{instruction}" - ) - - def build_gate_definition(self, gate): - """Builds a QuantumGateDefinition""" - if isinstance(gate, standard_gates.CXGate): - # CX gets super duper special treatment because it's the base of Terra's definition - # tree, but isn't an OQ3 built-in. We use `isinstance` because we haven't fully - # fixed what the name/class distinction is (there's a test from the original OQ3 - # exporter that tries a naming collision with 'cx'). + for filename in self.includes: + if (definitions := _KNOWN_INCLUDES.get(filename)) is None: + raise QASM3ExporterError(f"Unknown OpenQASM 3 include file: '{filename}'") + for name, gate in definitions.items(): + self.symbols.register_gate_without_definition(name, gate) + yield ast.Include(filename) + + def define_gate(self, gate: Gate) -> ast.Identifier: + """Define a gate in the symbol table, including building the gate-definition statement for + it. + + This recurses through gate-definition statements.""" + if issubclass(gate.base_class, library.CXGate) and gate.ctrl_state == 1: + # CX gets super duper special treatment because it's the base of Qiskit's definition + # tree, but isn't an OQ3 built-in (it was in OQ2). We use `isinstance` because we + # haven't fully fixed what the name/class distinction is (there's a test from the + # original OQ3 exporter that tries a naming collision with 'cx'). control, target = ast.Identifier("c"), ast.Identifier("t") - call = ast.QuantumGateCall( - ast.Identifier("U"), - [control, target], - parameters=[ast.Constant.PI, ast.IntegerLiteral(0), ast.Constant.PI], - modifiers=[ast.QuantumGateModifier(ast.QuantumGateModifierName.CTRL)], - ) - return ast.QuantumGateDefinition( - ast.QuantumGateSignature(ast.Identifier("cx"), [control, target]), - ast.QuantumBlock([call]), + body = ast.QuantumBlock( + [ + ast.QuantumGateCall( + self.symbols.get_gate(library.UGate(math.pi, 0, math.pi)), + [control, target], + parameters=[ast.Constant.PI, ast.IntegerLiteral(0), ast.Constant.PI], + modifiers=[ast.QuantumGateModifier(ast.QuantumGateModifierName.CTRL)], + ) + ] ) + return self.symbols.register_gate(gate.name, gate, (), (control, target), body) + if gate.definition is None: + raise QASM3ExporterError(f"failed to export gate '{gate.name}' that has no definition") + source = _gate_definition_source(gate) + with self.new_context(source.definition): + defn = self.scope.circuit + # If `defn.num_parameters == 0` but `gate.params` is non-empty, we are likely in the + # case where the gate's circuit definition is fully bound (so we can't detect its inputs + # anymore). This is a problem in our data model - for arbitrary user gates, there's no + # way we can reliably get a parametric version of the gate through our interfaces. In + # this case, we output a gate that has dummy parameters, and rely on it being a + # different `id` each time to avoid duplication. We assume that the parametrisation + # order matches (which is a _big_ assumption). + # + # If `defn.num_parameters > 0`, we enforce that it must match how it's called. + if defn.num_parameters > 0: + if defn.num_parameters != len(gate.params): + raise QASM3ExporterError( + "parameter mismatch in definition of '{gate}':" + f" call has {len(gate.params)}, definition has {defn.num_parameters}" + ) + params = [ + self.symbols.register_variable(param.name, param, allow_rename=True) + for param in defn.parameters + ] + else: + # Fill with dummy parameters. The name is unimportant, because they're not actually + # used in the definition. + params = [ + self.symbols.register_variable( + f"{self.gate_parameter_prefix}_{i}", None, allow_rename=True + ) + for i in range(len(gate.params)) + ] + qubits = [ + self.symbols.register_variable( + f"{self.gate_qubit_prefix}_{i}", qubit, allow_rename=True + ) + for i, qubit in enumerate(defn.qubits) + ] + body = ast.QuantumBlock(self.build_current_scope()) + # We register the gate only after building its body so that any gates we needed for that in + # turn are registered in the correct order. Gates can't be recursive in OQ3, so there's no + # problem with delaying this. + return self.symbols.register_gate(source.name, source, params, qubits, body) - self.push_context(gate.definition) - signature = self.build_gate_signature(gate) - body = ast.QuantumBlock(self.build_current_scope()) - self.pop_context() - return ast.QuantumGateDefinition(signature, body) - - def build_gate_signature(self, gate): - """Builds a QuantumGateSignature""" - name = self.global_namespace[gate] - params = [] - definition = gate.definition - # Dummy parameters - scope = self.current_scope() - for num in range(len(gate.params) - len(definition.parameters)): - param_name = f"{self.gate_parameter_prefix}_{num}" - params.append(self._reserve_variable_name(ast.Identifier(param_name), scope)) - params += [self._register_variable(param, scope) for param in definition.parameters] - quantum_arguments = [ - self._register_variable( - qubit, scope, self._unique_name(f"{self.gate_qubit_prefix}_{i}", scope) - ) - for i, qubit in enumerate(definition.qubits) - ] - return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None) + def assert_global_scope(self): + """Raise an error if we are not in the global scope, as a defensive measure.""" + if len(self.symbols.variables) > 1: # pragma: no cover + raise RuntimeError("not currently in the global scope") def hoist_global_parameter_declarations(self): """Extend ``self._global_io_declarations`` and ``self._global_classical_declarations`` with any implicit declarations used to support the early IBM efforts to use :class:`.Parameter` as an input variable.""" - global_scope = self.global_scope(assert_=True) - for parameter in global_scope.circuit.parameters: - parameter_name = self._register_variable(parameter, global_scope) - declaration = _infer_variable_declaration( - global_scope.circuit, parameter, parameter_name + self.assert_global_scope() + circuit = self.scope.circuit + for parameter in circuit.parameters: + parameter_name = self.symbols.register_variable( + parameter.name, parameter, allow_rename=True ) + declaration = _infer_variable_declaration(circuit, parameter, parameter_name) if declaration is None: continue if isinstance(declaration, ast.IODeclaration): @@ -688,8 +760,9 @@ def hoist_classical_register_declarations(self): for the loose :obj:`.Clbit` instances, and will raise :obj:`QASM3ExporterError` if any registers overlap. """ - scope = self.global_scope(assert_=True) - if any(len(scope.circuit.find_bit(q).registers) > 1 for q in scope.circuit.clbits): + self.assert_global_scope() + circuit = self.scope.circuit + if any(len(circuit.find_bit(q).registers) > 1 for q in circuit.clbits): # There are overlapping registers, so we need to use aliases to emit the structure. if not self.allow_aliasing: raise QASM3ExporterError( @@ -699,34 +772,32 @@ def hoist_classical_register_declarations(self): clbits = ( ast.ClassicalDeclaration( ast.BitType(), - self._register_variable( - clbit, scope, self._unique_name(f"{self.loose_bit_prefix}{i}", scope) + self.symbols.register_variable( + f"{self.loose_bit_prefix}{i}", clbit, allow_rename=True ), ) - for i, clbit in enumerate(scope.circuit.clbits) + for i, clbit in enumerate(circuit.clbits) ) self._global_classical_forward_declarations.extend(clbits) - self._global_classical_forward_declarations.extend( - self.build_aliases(scope.circuit.cregs) - ) + self._global_classical_forward_declarations.extend(self.build_aliases(circuit.cregs)) return # If we're here, we're in the clbit happy path where there are no clbits that are in more # than one register. We can output things very naturally. self._global_classical_forward_declarations.extend( ast.ClassicalDeclaration( ast.BitType(), - self._register_variable( - clbit, scope, self._unique_name(f"{self.loose_bit_prefix}{i}", scope) + self.symbols.register_variable( + f"{self.loose_bit_prefix}{i}", clbit, allow_rename=True ), ) - for i, clbit in enumerate(scope.circuit.clbits) - if not scope.circuit.find_bit(clbit).registers + for i, clbit in enumerate(circuit.clbits) + if not circuit.find_bit(clbit).registers ) - for register in scope.circuit.cregs: - name = self._register_variable(register, scope) + for register in circuit.cregs: + name = self.symbols.register_variable(register.name, register, allow_rename=True) for i, bit in enumerate(register): - scope.symbol_map[bit] = ast.SubscriptedIdentifier( - name.string, ast.IntegerLiteral(i) + self.symbols.set_object_ident( + ast.SubscriptedIdentifier(name.string, ast.IntegerLiteral(i)), bit ) self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.BitArrayType(len(register)), name) @@ -738,27 +809,29 @@ def hoist_classical_io_var_declarations(self): Local :class:`.expr.Var` declarations are handled by the regular local-block scope builder, and the :class:`.QuantumCircuit` data model ensures that the only time an IO variable can occur is in an outermost block.""" - scope = self.global_scope(assert_=True) - for var in scope.circuit.iter_input_vars(): + self.assert_global_scope() + circuit = self.scope.circuit + for var in circuit.iter_input_vars(): self._global_io_declarations.append( ast.IODeclaration( ast.IOModifier.INPUT, _build_ast_type(var.type), - self._register_variable(var, scope), + self.symbols.register_variable(var.name, var, allow_rename=True), ) ) 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.""" - scope = self.global_scope(assert_=True) - if scope.circuit.layout is not None: + self.assert_global_scope() + circuit = self.scope.circuit + if circuit.layout is not None: # We're referring to physical qubits. These can't be declared in OQ3, but we need to # track the bit -> expression mapping in our symbol table. - for i, bit in enumerate(scope.circuit.qubits): - scope.symbol_map[bit] = ast.Identifier(f"${i}") + for i, bit in enumerate(circuit.qubits): + self.symbols.register_variable(f"${i}", bit, allow_rename=False) return [] - if any(len(scope.circuit.find_bit(q).registers) > 1 for q in scope.circuit.qubits): + if any(len(circuit.find_bit(q).registers) > 1 for q in circuit.qubits): # There are overlapping registers, so we need to use aliases to emit the structure. if not self.allow_aliasing: raise QASM3ExporterError( @@ -767,30 +840,30 @@ def build_quantum_declarations(self): ) qubits = [ ast.QuantumDeclaration( - self._register_variable( - qubit, scope, self._unique_name(f"{self.loose_qubit_prefix}{i}", scope) + self.symbols.register_variable( + f"{self.loose_qubit_prefix}{i}", qubit, allow_rename=True ) ) - for i, qubit in enumerate(scope.circuit.qubits) + for i, qubit in enumerate(circuit.qubits) ] - return qubits + self.build_aliases(scope.circuit.qregs) + return qubits + self.build_aliases(circuit.qregs) # If we're here, we're in the virtual-qubit happy path where there are no qubits that are in # more than one register. We can output things very naturally. loose_qubits = [ ast.QuantumDeclaration( - self._register_variable( - qubit, scope, self._unique_name(f"{self.loose_qubit_prefix}{i}", scope) + self.symbols.register_variable( + f"{self.loose_qubit_prefix}{i}", qubit, allow_rename=True ) ) - for i, qubit in enumerate(scope.circuit.qubits) - if not scope.circuit.find_bit(qubit).registers + for i, qubit in enumerate(circuit.qubits) + if not circuit.find_bit(qubit).registers ] registers = [] - for register in scope.circuit.qregs: - name = self._register_variable(register, scope) + for register in circuit.qregs: + name = self.symbols.register_variable(register.name, register, allow_rename=True) for i, bit in enumerate(register): - scope.symbol_map[bit] = ast.SubscriptedIdentifier( - name.string, ast.IntegerLiteral(i) + self.symbols.set_object_ident( + ast.SubscriptedIdentifier(name.string, ast.IntegerLiteral(i)), bit ) registers.append( ast.QuantumDeclaration(name, ast.Designator(ast.IntegerLiteral(len(register)))) @@ -800,15 +873,14 @@ def build_quantum_declarations(self): 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.""" - scope = self.current_scope() out = [] for register in registers: - name = self._register_variable(register, scope) + name = self.symbols.register_variable(register.name, register, allow_rename=True) elements = [self._lookup_variable(bit) for bit in register] for i, bit in enumerate(register): # This might shadow previous definitions, but that's not a problem. - scope.symbol_map[bit] = ast.SubscriptedIdentifier( - name.string, ast.IntegerLiteral(i) + self.symbols.set_object_ident( + ast.SubscriptedIdentifier(name.string, ast.IntegerLiteral(i)), bit ) out.append(ast.AliasStatement(name, ast.IndexSet(elements))) return out @@ -819,7 +891,6 @@ def build_current_scope(self) -> List[ast.Statement]: In addition to everything literally in the circuit's ``data`` field, this also includes declarations for any local :class:`.expr.Var` nodes. """ - scope = self.current_scope() # We forward-declare all local variables uninitialised at the top of their scope. It would # be nice to declare the variable at the point of first store (so we can write things like @@ -829,10 +900,13 @@ def build_current_scope(self) -> List[ast.Statement]: # variable, or the initial write to a variable is within a control-flow scope. (It would be # easier to see the def/use chain needed to do this cleanly if we were using `DAGCircuit`.) statements = [ - ast.ClassicalDeclaration(_build_ast_type(var.type), self._register_variable(var, scope)) - for var in scope.circuit.iter_declared_vars() + ast.ClassicalDeclaration( + _build_ast_type(var.type), + self.symbols.register_variable(var.name, var, allow_rename=True), + ) + for var in self.scope.circuit.iter_declared_vars() ] - for instruction in scope.circuit.data: + for instruction in self.scope.circuit.data: if isinstance(instruction.operation, ControlFlowOp): if isinstance(instruction.operation, ForLoopOp): statements.append(self.build_for_loop(instruction)) @@ -876,7 +950,10 @@ def build_current_scope(self) -> List[ast.Statement]: elif isinstance(instruction.operation, ContinueLoopOp): nodes = [ast.ContinueStatement()] else: - nodes = [self.build_subroutine_call(instruction)] + raise QASM3ExporterError( + "non-unitary subroutine calls are not yet supported," + f" but received '{instruction.operation}'" + ) if instruction.operation.condition is None: statements.extend(nodes) @@ -895,24 +972,21 @@ def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingSt condition = self.build_expression(_lift_condition(instruction.operation.condition)) true_circuit = instruction.operation.blocks[0] - self.push_scope(true_circuit, instruction.qubits, instruction.clbits) - true_body = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + with self.new_scope(true_circuit, instruction.qubits, instruction.clbits): + true_body = ast.ProgramBlock(self.build_current_scope()) if len(instruction.operation.blocks) == 1: return ast.BranchingStatement(condition, true_body, None) false_circuit = instruction.operation.blocks[1] - self.push_scope(false_circuit, instruction.qubits, instruction.clbits) - false_body = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + with self.new_scope(false_circuit, instruction.qubits, instruction.clbits): + false_body = ast.ProgramBlock(self.build_current_scope()) return ast.BranchingStatement(condition, true_body, false_body) def build_switch_statement(self, instruction: CircuitInstruction) -> Iterable[ast.Statement]: """Build a :obj:`.SwitchCaseOp` into a :class:`.ast.SwitchStatement`.""" real_target = self.build_expression(expr.lift(instruction.operation.target)) - global_scope = self.global_scope() - target = self._reserve_variable_name( - ast.Identifier(self._unique_name("switch_dummy", global_scope)), global_scope + target = self.symbols.register_variable( + "switch_dummy", None, allow_rename=True, global_=True ) self._global_classical_forward_declarations.append( ast.ClassicalDeclaration(ast.IntType(), target, None) @@ -926,9 +1000,8 @@ def case(values, case_block): ast.DefaultCase() if v is CASE_DEFAULT else self.build_integer(v) for v in values ] - self.push_scope(case_block, instruction.qubits, instruction.clbits) - case_body = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + with self.new_scope(case_block, instruction.qubits, instruction.clbits): + case_body = ast.ProgramBlock(self.build_current_scope()) return values, case_body return [ @@ -946,9 +1019,8 @@ def case(values, case_block): cases = [] default = None for values, block in instruction.operation.cases_specifier(): - self.push_scope(block, instruction.qubits, instruction.clbits) - case_body = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + with self.new_scope(block, instruction.qubits, instruction.clbits): + case_body = ast.ProgramBlock(self.build_current_scope()) if CASE_DEFAULT in values: # Even if it's mixed in with other cases, we can skip them and only output the # `default` since that's valid and execution will be the same; the evaluation of @@ -966,39 +1038,34 @@ def build_while_loop(self, instruction: CircuitInstruction) -> ast.WhileLoopStat """Build a :obj:`.WhileLoopOp` into a :obj:`.ast.WhileLoopStatement`.""" condition = self.build_expression(_lift_condition(instruction.operation.condition)) loop_circuit = instruction.operation.blocks[0] - self.push_scope(loop_circuit, instruction.qubits, instruction.clbits) - loop_body = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + with self.new_scope(loop_circuit, instruction.qubits, instruction.clbits): + loop_body = ast.ProgramBlock(self.build_current_scope()) return ast.WhileLoopStatement(condition, loop_body) def build_for_loop(self, instruction: CircuitInstruction) -> ast.ForLoopStatement: """Build a :obj:`.ForLoopOp` into a :obj:`.ast.ForLoopStatement`.""" indexset, loop_parameter, loop_circuit = instruction.operation.params - self.push_scope(loop_circuit, instruction.qubits, instruction.clbits) - scope = self.current_scope() - 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. - loop_parameter_ast = self._reserve_variable_name(ast.Identifier("_"), scope) - else: - loop_parameter_ast = self._register_variable(loop_parameter, scope) - if isinstance(indexset, range): - # OpenQASM 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, + with self.new_scope(loop_circuit, instruction.qubits, instruction.clbits): + name = "_" if loop_parameter is None else loop_parameter.name + loop_parameter_ast = self.symbols.register_variable( + name, loop_parameter, allow_rename=True ) - else: - try: - indexset_ast = ast.IndexSet([self.build_integer(value) for value in indexset]) - except QASM3ExporterError: - raise QASM3ExporterError( - "The values in OpenQASM 3 'for' loops must all be integers, but received" - f" '{indexset}'." - ) from None - body_ast = ast.ProgramBlock(self.build_current_scope()) - self.pop_scope() + if isinstance(indexset, range): + # OpenQASM 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 OpenQASM 3 'for' loops must all be integers, but received" + f" '{indexset}'." + ) from None + body_ast = ast.ProgramBlock(self.build_current_scope()) return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast) def build_expression(self, node: expr.Expr) -> ast.Expression: @@ -1053,11 +1120,13 @@ def _rebind_scoped_parameters(self, expression): ) def build_gate_call(self, instruction: CircuitInstruction): - """Builds a QuantumGateCall""" - if isinstance(instruction.operation, standard_gates.UGate): - gate_name = ast.Identifier("U") - else: - gate_name = ast.Identifier(self.global_namespace[instruction.operation]) + """Builds a gate-call AST node. + + This will also push the gate into the symbol table (if required), including recursively + defining the gate blocks.""" + ident = self.symbols.get_gate(instruction.operation) + if ident is None: + ident = self.define_gate(instruction.operation) qubits = [self._lookup_variable(qubit) for qubit in instruction.qubits] if self.disable_constants: parameters = [ @@ -1070,7 +1139,7 @@ def build_gate_call(self, instruction: CircuitInstruction): for param in instruction.operation.params ] - return ast.QuantumGateCall(gate_name, qubits, parameters=parameters) + return ast.QuantumGateCall(ident, qubits, parameters=parameters) def _infer_variable_declaration( diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 58f689c2c2e7..221c99bc4f90 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -431,26 +431,17 @@ def _visit_ReturnStatement(self, node: ast.ReturnStatement) -> None: self.stream.write("return") self._end_statement() - 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: 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: ast.QuantumGateDefinition) -> None: self._start_line() self.stream.write("gate ") - self.visit(node.quantumGateSignature) + self.visit(node.name) + if node.params: + self._visit_sequence(node.params, start="(", end=")", separator=", ") self.stream.write(" ") - self.visit(node.quantumBlock) + if node.qubits: + self._visit_sequence(node.qubits, separator=", ") + self.stream.write(" ") + self.visit(node.body) self._end_line() def _visit_CalibrationDefinition(self, node: ast.CalibrationDefinition) -> None: diff --git a/releasenotes/notes/qasm3-symbol-table-efad35629639c77d.yaml b/releasenotes/notes/qasm3-symbol-table-efad35629639c77d.yaml new file mode 100644 index 000000000000..144e0dee279c --- /dev/null +++ b/releasenotes/notes/qasm3-symbol-table-efad35629639c77d.yaml @@ -0,0 +1,18 @@ +--- +features_qasm: + - | + The internal symbol table of the OpenQASM 3 exporter (:mod:`qiskit.qasm3`) has been rewritten, + which should result in cleaner outputs when using Qiskit standard-library gates that are not in + the OpenQASM 3 standard-library headers, and more deterministic outputs. For example, using + several :class:`.RZXGate`\ s will now result in only a single parametric definition, and when + naming collisions occur, the symbol table will assign a deterministic counter to make names + unique, rather than a non-deterministic integer (previously, the object identity was used). +fixes: + - | + The OpenQASM 3 exporter (:mod:`qiskit.qasm3`) will now correctly export multiple instances of + :class:`.PauliEvolutionGate` from a circuit. Previously, only a single instance would be exported, + and all other instances would silently use the same (incorrect) version. + - | + The OpenQASM 3 exporter (:mod:`qiskit.qasm3`) will now correctly escape gate names. Previously, + a gate whose name was an invalid OpenQASM 3 identifier would cause invalid OpenQASM 3 to be + generated. diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 6df041420882..edb3c9162bf7 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -18,25 +18,21 @@ from io import StringIO from math import pi import re -import unittest from ddt import ddt, data from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile -from qiskit.circuit import Parameter, Qubit, Clbit, Instruction, Gate, Delay, Barrier +from qiskit.circuit import Parameter, Qubit, Clbit, Gate, Delay, Barrier, ParameterVector from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import CASE_DEFAULT +from qiskit.circuit.library import PauliEvolutionGate from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError, ExperimentalFeatures from qiskit.qasm3.exporter import QASM3Builder from qiskit.qasm3.printer import BasicPrinter +from qiskit.quantum_info import Pauli from test import QiskitTestCase # pylint: disable=wrong-import-order -# Tests marked with this decorator should be restored after gate definition with parameters is fixed -# properly, and the dummy tests after them should be deleted. See gh-7335. -requires_fixed_parameterisation = unittest.expectedFailure - - class TestQASM3Functions(QiskitTestCase): """QASM3 module - high level functions""" @@ -307,9 +303,7 @@ def test_composite_circuits_with_same_name(self): circuit = QuantumCircuit(qr, name="circuit") circuit.append(my_gate_inst1, [qr[0]]) circuit.append(my_gate_inst2, [qr[0]]) - my_gate_inst2_id = id(circuit.data[-1].operation) circuit.append(my_gate_inst3, [qr[0]]) - my_gate_inst3_id = id(circuit.data[-1].operation) expected_qasm = "\n".join( [ "OPENQASM 3.0;", @@ -317,16 +311,16 @@ def test_composite_circuits_with_same_name(self): "gate my_gate _gate_q_0 {", " h _gate_q_0;", "}", - f"gate my_gate_{my_gate_inst2_id} _gate_q_0 {{", + "gate my_gate_0 _gate_q_0 {", " x _gate_q_0;", "}", - f"gate my_gate_{my_gate_inst3_id} _gate_q_0 {{", + "gate my_gate_1 _gate_q_0 {", " x _gate_q_0;", "}", "qubit[1] qr;", "my_gate qr[0];", - f"my_gate_{my_gate_inst2_id} qr[0];", - f"my_gate_{my_gate_inst3_id} qr[0];", + "my_gate_0 qr[0];", + "my_gate_1 qr[0];", "", ] ) @@ -412,7 +406,6 @@ def test_custom_gate_with_bound_parameter(self): ) self.assertEqual(Exporter().dumps(circuit), expected_qasm) - @requires_fixed_parameterisation def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" parameter0 = Parameter("p0") @@ -429,11 +422,14 @@ def test_custom_gate_with_params_bound_main_call(self): circuit.assign_parameters({parameter0: pi, parameter1: pi / 2}, inplace=True) + # NOTE: this isn't exactly what we want; note that the parameters in the signature are not + # actually used. It would be fine to change the output of the exporter to make `custom` non + # parametric in this case. expected_qasm = "\n".join( [ "OPENQASM 3.0;", 'include "stdgates.inc";', - "gate custom(_gate_p_0, _gate_p_0) _gate_q_0, _gate_q_1 {", + "gate custom(_gate_p_0, _gate_p_1) _gate_q_0, _gate_q_1 {", " rz(pi) _gate_q_0;", " rz(pi/4) _gate_q_1;", "}", @@ -445,6 +441,58 @@ def test_custom_gate_with_params_bound_main_call(self): ) self.assertEqual(Exporter().dumps(circuit), expected_qasm) + def test_multiple_pauli_evolution_gates(self): + """Pauli evolution gates should be detected as distinct.""" + vec = ParameterVector("t", 3) + qc = QuantumCircuit(2) + qc.append(PauliEvolutionGate(Pauli("XX"), vec[0]), [0, 1]) + qc.append(PauliEvolutionGate(Pauli("YY"), vec[1]), [0, 1]) + qc.append(PauliEvolutionGate(Pauli("ZZ"), vec[2]), [0, 1]) + expected = """\ +OPENQASM 3.0; +include "stdgates.inc"; +input float[64] _t_0_; +input float[64] _t_1_; +input float[64] _t_2_; +gate rxx(p0) _gate_q_0, _gate_q_1 { + h _gate_q_0; + h _gate_q_1; + cx _gate_q_0, _gate_q_1; + rz(p0) _gate_q_1; + cx _gate_q_0, _gate_q_1; + h _gate_q_1; + h _gate_q_0; +} +gate PauliEvolution(_t_0_) _gate_q_0, _gate_q_1 { + rxx(2.0*_t_0_) _gate_q_0, _gate_q_1; +} +gate ryy(p0) _gate_q_0, _gate_q_1 { + rx(pi/2) _gate_q_0; + rx(pi/2) _gate_q_1; + cx _gate_q_0, _gate_q_1; + rz(p0) _gate_q_1; + cx _gate_q_0, _gate_q_1; + rx(-pi/2) _gate_q_0; + rx(-pi/2) _gate_q_1; +} +gate PauliEvolution_0(_t_1_) _gate_q_0, _gate_q_1 { + ryy(2.0*_t_1_) _gate_q_0, _gate_q_1; +} +gate rzz(p0) _gate_q_0, _gate_q_1 { + cx _gate_q_0, _gate_q_1; + rz(p0) _gate_q_1; + cx _gate_q_0, _gate_q_1; +} +gate PauliEvolution_1(_t_2_) _gate_q_0, _gate_q_1 { + rzz(2.0*_t_2_) _gate_q_0, _gate_q_1; +} +qubit[2] q; +PauliEvolution(_t_0_) q[0], q[1]; +PauliEvolution_0(_t_1_) q[0], q[1]; +PauliEvolution_1(_t_2_) q[0], q[1]; +""" + self.assertEqual(dumps(qc), expected) + def test_reused_custom_parameter(self): """Test reused custom gate with parameter.""" parameter_a = Parameter("a") @@ -456,8 +504,8 @@ def test_reused_custom_parameter(self): circuit.append(custom.assign_parameters({parameter_a: 0.5}).to_gate(), [0]) circuit.append(custom.assign_parameters({parameter_a: 1}).to_gate(), [0]) - circuit_name_0 = circuit.data[0].operation.definition.name - circuit_name_1 = circuit.data[1].operation.definition.name + circuit_name_0 = "_" + circuit.data[0].operation.definition.name.replace("-", "_") + circuit_name_1 = "_" + circuit.data[1].operation.definition.name.replace("-", "_") expected_qasm = "\n".join( [ @@ -494,7 +542,7 @@ def test_unbound_circuit(self): ) self.assertEqual(Exporter().dumps(qc), expected_qasm) - def test_unknown_parameterized_gate_called_multiple_times(self): + def test_standard_parameterized_gate_called_multiple_times(self): """Test that a parameterized gate is called correctly if the first instance of it is generic.""" x, y = Parameter("x"), Parameter("y") @@ -508,10 +556,10 @@ def test_unknown_parameterized_gate_called_multiple_times(self): "OPENQASM 3.0;", "input float[64] x;", "input float[64] y;", - "gate rzx(x) _gate_q_0, _gate_q_1 {", + "gate rzx(p0) _gate_q_0, _gate_q_1 {", " h _gate_q_1;", " cx _gate_q_0, _gate_q_1;", - " rz(x) _gate_q_1;", + " rz(p0) _gate_q_1;", " cx _gate_q_0, _gate_q_1;", " h _gate_q_1;", "}", @@ -556,22 +604,20 @@ def test_custom_gate_collision_with_stdlib(self): qc = QuantumCircuit(2) qc.append(custom_gate, [0, 1]) - custom_gate_id = id(qc.data[-1].operation) expected_qasm = "\n".join( [ "OPENQASM 3.0;", 'include "stdgates.inc";', - f"gate cx_{custom_gate_id} _gate_q_0, _gate_q_1 {{", + "gate cx_0 _gate_q_0, _gate_q_1 {", " cx _gate_q_0, _gate_q_1;", "}", "qubit[2] q;", - f"cx_{custom_gate_id} q[0], q[1];", + "cx_0 q[0], q[1];", "", ] ) self.assertEqual(Exporter().dumps(qc), expected_qasm) - @requires_fixed_parameterisation def test_no_include(self): """Test explicit gate declaration (no include)""" q = QuantumRegister(2, "q") @@ -579,45 +625,41 @@ def test_no_include(self): circuit.rz(pi / 2, 0) circuit.sx(0) circuit.cx(0, 1) - expected_qasm = "\n".join( - [ - "OPENQASM 3.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(_gate_p_0) _gate_q_0 {", - " u3(0, 0, pi/2) _gate_q_0;", - "}", - "gate rz(_gate_p_0) _gate_q_0 {", - " u1(pi/2) _gate_q_0;", - "}", - "gate sdg _gate_q_0 {", - " u1(-pi/2) _gate_q_0;", - "}", - "gate u2(_gate_p_0, _gate_p_1) _gate_q_0 {", - " u3(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - " u2(0, pi) _gate_q_0;", - "}", - "gate sx _gate_q_0 {", - " sdg _gate_q_0;", - " h _gate_q_0;", - " sdg _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "qubit[2] q;", - "rz(pi/2) q[0];", - "sx q[0];", - "cx q[0], q[1];", - "", - ] - ) + expected_qasm = """\ +OPENQASM 3.0; +gate u3(p0, p1, p2) _gate_q_0 { + U(p0, p1, p2) _gate_q_0; +} +gate u1(p0) _gate_q_0 { + u3(0, 0, p0) _gate_q_0; +} +gate rz(p0) _gate_q_0 { + u1(p0) _gate_q_0; +} +gate sdg _gate_q_0 { + u1(-pi/2) _gate_q_0; +} +gate u2(p0, p1) _gate_q_0 { + u3(pi/2, p0, p1) _gate_q_0; +} +gate h _gate_q_0 { + u2(0, pi) _gate_q_0; +} +gate sx _gate_q_0 { + sdg _gate_q_0; + h _gate_q_0; + sdg _gate_q_0; +} +gate cx c, t { + ctrl @ U(pi, 0, pi) c, t; +} +qubit[2] q; +rz(pi/2) q[0]; +sx q[0]; +cx q[0], q[1]; +""" self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) - @requires_fixed_parameterisation def test_teleportation(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) @@ -633,52 +675,48 @@ def test_teleportation(self): qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) - expected_qasm = "\n".join( - [ - "OPENQASM 3.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(_gate_p_0, _gate_p_1) _gate_q_0 {", - " u3(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - " u2(0, pi) _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "gate x _gate_q_0 {", - " u3(pi, 0, pi) _gate_q_0;", - "}", - "gate u1(_gate_p_0) _gate_q_0 {", - " u3(0, 0, pi) _gate_q_0;", - "}", - "gate z _gate_q_0 {", - " u1(pi) _gate_q_0;", - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - ) + expected_qasm = """\ +OPENQASM 3.0; +gate u3(p0, p1, p2) _gate_q_0 { + U(p0, p1, p2) _gate_q_0; +} +gate u2(p0, p1) _gate_q_0 { + u3(pi/2, p0, p1) _gate_q_0; +} +gate h _gate_q_0 { + u2(0, pi) _gate_q_0; +} +gate cx c, t { + ctrl @ U(pi, 0, pi) c, t; +} +gate x _gate_q_0 { + u3(pi, 0, pi) _gate_q_0; +} +gate u1(p0) _gate_q_0 { + u3(0, 0, p0) _gate_q_0; +} +gate z _gate_q_0 { + u1(pi) _gate_q_0; +} +bit[2] c; +h $1; +cx $1, $2; +barrier $0, $1, $2; +cx $0, $1; +h $0; +barrier $0, $1, $2; +c[0] = measure $0; +c[1] = measure $1; +barrier $0, $1, $2; +if (c[1]) { + x $2; +} +if (c[0]) { + z $2; +} +""" self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) - @requires_fixed_parameterisation def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) @@ -694,40 +732,37 @@ def test_basis_gates(self): qc.z(2).c_if(qc.clbits[0], 1) transpiled = transpile(qc, initial_layout=[0, 1, 2]) - expected_qasm = "\n".join( - [ - "OPENQASM 3.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(_gate_p_0, _gate_p_1) _gate_q_0 {", - " u3(pi/2, 0, pi) _gate_q_0;", - "}", - "gate h _gate_q_0 {", - " u2(0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - " u3(pi, 0, pi) _gate_q_0;", - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - ) + expected_qasm = """\ +OPENQASM 3.0; +gate u3(p0, p1, p2) _gate_q_0 { + U(p0, p1, p2) _gate_q_0; +} +gate u2(p0, p1) _gate_q_0 { + u3(pi/2, p0, p1) _gate_q_0; +} +gate h _gate_q_0 { + u2(0, pi) _gate_q_0; +} +gate x _gate_q_0 { + u3(pi, 0, pi) _gate_q_0; +} +bit[2] c; +h $1; +cx $1, $2; +barrier $0, $1, $2; +cx $0, $1; +h $0; +barrier $0, $1, $2; +c[0] = measure $0; +c[1] = measure $1; +barrier $0, $1, $2; +if (c[1]) { + x $2; +} +if (c[0]) { + z $2; +} +""" self.assertEqual( Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(transpiled), expected_qasm, @@ -1398,7 +1433,6 @@ def test_custom_gate_used_in_loop_scope(self): qc = QuantumCircuit(1) qc.for_loop(range(2), parameter_b, loop_body, [0], []) - expected_qasm = "\n".join( [ "OPENQASM 3.0;", @@ -1446,9 +1480,9 @@ def test_parameter_expression_after_naming_escape(self): [ "OPENQASM 3.0;", 'include "stdgates.inc";', - "input float[64] _measure;", + "input float[64] measure_0;", "qubit[1] q;", - "U(2*_measure, 0, 0) q[0];", + "U(2*measure_0, 0, 0) q[0];", "", ] ) @@ -1885,8 +1919,8 @@ def test_var_naming_clash_parameter(self): include "stdgates.inc"; input float[64] a; qubit[1] q; -bool a__generated0; -a__generated0 = false; +bool a_0; +a_0 = false; rx(a) q[0]; """ self.assertEqual(dumps(qc), expected) @@ -1900,11 +1934,11 @@ def test_var_naming_clash_register(self): expected = """\ OPENQASM 3.0; include "stdgates.inc"; -input bool c__generated0; +input bool c_0; bit[2] c; qubit[2] q; -bool q__generated1; -q__generated1 = false; +bool q_1; +q_1 = false; """ self.assertEqual(dumps(qc), expected) @@ -1922,254 +1956,16 @@ def test_var_naming_clash_gate(self): expected = """\ OPENQASM 3.0; include "stdgates.inc"; -input bool cx__generated0; -input bool U__generated1; +input bool cx_0; +input bool U_1; qubit[2] q; -uint[8] rx__generated2; -rx__generated2 = 5; +uint[8] rx_2; +rx_2 = 5; cx q[0], q[1]; U(0.5, 0.125, 0.25) q[0]; """ self.assertEqual(dumps(qc), expected) - -class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCase): - """Test functionality that is not what we _want_, but is what we need to do while the definition - of custom gates with parameterization does not work correctly. - - These tests are modified versions of those marked with the `requires_fixed_parameterisation` - decorator, and this whole class can be deleted once those are fixed. See gh-7335. - """ - - maxDiff = 1_000_000 - - def test_basis_gates(self): - """Teleportation with physical qubits""" - qc = QuantumCircuit(3, 2) - qc.h(1) - qc.cx(1, 2) - qc.barrier() - qc.cx(0, 1) - qc.h(0) - qc.barrier() - qc.measure([0, 1], [0, 1]) - qc.barrier() - first_x = qc.x(2).c_if(qc.clbits[1], 1)[0].operation - qc.z(2).c_if(qc.clbits[0], 1) - - id_len = len(str(id(first_x))) - expected_qasm = [ - "OPENQASM 3.0;", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(pi/2, 0, pi) _gate_q_0;", - "}", - re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), - "}", - "gate h _gate_q_0 {", - re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), - "}", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), - "}", - "bit[2] c;", - "qubit[3] q;", - "h q[1];", - "cx q[1], q[2];", - "barrier q[0], q[1], q[2];", - "cx q[0], q[1];", - "h q[0];", - "barrier q[0], q[1], q[2];", - "c[0] = measure q[0];", - "c[1] = measure q[1];", - "barrier q[0], q[1], q[2];", - "if (c[1]) {", - " x q[2];", - "}", - "if (c[0]) {", - " z q[2];", - "}", - "", - ] - res = Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc).splitlines() - for result, expected in zip(res, expected_qasm): - if isinstance(expected, str): - self.assertEqual(result, expected) - else: - self.assertTrue( - expected.search(result), f"Line {result} doesn't match regex: {expected}" - ) - - def test_teleportation(self): - """Teleportation with physical qubits""" - qc = QuantumCircuit(3, 2) - qc.h(1) - qc.cx(1, 2) - qc.barrier() - qc.cx(0, 1) - qc.h(0) - qc.barrier() - qc.measure([0, 1], [0, 1]) - qc.barrier() - qc.x(2).c_if(qc.clbits[1], 1) - qc.z(2).c_if(qc.clbits[0], 1) - - transpiled = transpile(qc, initial_layout=[0, 1, 2]) - id_len = len(str(id(transpiled.data[0].operation))) - - expected_qasm = [ - "OPENQASM 3.0;", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(pi/2, 0, pi) _gate_q_0;", - "}", - re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), - "}", - "gate h _gate_q_0 {", - re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(pi, 0, pi) _gate_q_0;", - "}", - "gate x _gate_q_0 {", - re.compile(r" u3_\d{%s}\(pi, 0, pi\) _gate_q_0;" % id_len), - "}", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(0, 0, pi) _gate_q_0;", - "}", - re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(0, 0, pi\) _gate_q_0;" % id_len), - "}", - "gate z _gate_q_0 {", - re.compile(r" u1_\d{%s}\(pi\) _gate_q_0;" % id_len), - "}", - "bit[2] c;", - "h $1;", - "cx $1, $2;", - "barrier $0, $1, $2;", - "cx $0, $1;", - "h $0;", - "barrier $0, $1, $2;", - "c[0] = measure $0;", - "c[1] = measure $1;", - "barrier $0, $1, $2;", - "if (c[1]) {", - " x $2;", - "}", - "if (c[0]) {", - " z $2;", - "}", - "", - ] - res = Exporter(includes=[]).dumps(transpiled).splitlines() - for result, expected in zip(res, expected_qasm): - if isinstance(expected, str): - self.assertEqual(result, expected) - else: - self.assertTrue( - expected.search(result), f"Line {result} doesn't match regex: {expected}" - ) - - def test_custom_gate_with_params_bound_main_call(self): - """Custom gate with unbound parameters that are bound in the main circuit""" - parameter0 = Parameter("p0") - parameter1 = Parameter("p1") - - custom = QuantumCircuit(2, name="custom") - custom.rz(parameter0, 0) - custom.rz(parameter1 / 2, 1) - - qr_all_qubits = QuantumRegister(3, "q") - qr_r = QuantumRegister(3, "r") - 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) - custom_id = id(circuit.data[0].operation) - - expected_qasm = "\n".join( - [ - "OPENQASM 3.0;", - 'include "stdgates.inc";', - f"gate custom_{custom_id}(_gate_p_0, _gate_p_1) _gate_q_0, _gate_q_1 {{", - " rz(pi) _gate_q_0;", - " rz(pi/4) _gate_q_1;", - "}", - "qubit[3] q;", - "qubit[3] r;", - f"custom_{custom_id}(pi, pi/2) q[0], r[0];", - "", - ] - ) - self.assertEqual(Exporter().dumps(circuit), expected_qasm) - - def test_no_include(self): - """Test explicit gate declaration (no include)""" - q = QuantumRegister(2, "q") - circuit = QuantumCircuit(q) - circuit.rz(pi / 2, 0) - circuit.sx(0) - circuit.cx(0, 1) - - id_len = len(str(id(circuit.data[0].operation))) - expected_qasm = [ - "OPENQASM 3.0;", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(0, 0, pi/2) _gate_q_0;", - "}", - re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(0, 0, pi/2\) _gate_q_0;" % id_len), - "}", - re.compile(r"gate rz_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), - re.compile(r" u1_\d{%s}\(pi/2\) _gate_q_0;" % id_len), - "}", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(0, 0, -pi/2) _gate_q_0;", - "}", - re.compile(r"gate u1_\d{%s}\(_gate_p_0\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(0, 0, -pi/2\) _gate_q_0;" % id_len), - "}", - "gate sdg _gate_q_0 {", - re.compile(r" u1_\d{%s}\(-pi/2\) _gate_q_0;" % id_len), - "}", - re.compile(r"gate u3_\d{%s}\(_gate_p_0, _gate_p_1, _gate_p_2\) _gate_q_0 \{" % id_len), - " U(pi/2, 0, pi) _gate_q_0;", - "}", - re.compile(r"gate u2_\d{%s}\(_gate_p_0, _gate_p_1\) _gate_q_0 \{" % id_len), - re.compile(r" u3_\d{%s}\(pi/2, 0, pi\) _gate_q_0;" % id_len), - "}", - "gate h _gate_q_0 {", - re.compile(r" u2_\d{%s}\(0, pi\) _gate_q_0;" % id_len), - "}", - "gate sx _gate_q_0 {", - " sdg _gate_q_0;", - " h _gate_q_0;", - " sdg _gate_q_0;", - "}", - "gate cx c, t {", - " ctrl @ U(pi, 0, pi) c, t;", - "}", - "qubit[2] q;", - re.compile(r"rz_\d{%s}\(pi/2\) q\[0\];" % id_len), - "sx q[0];", - "cx q[0], q[1];", - "", - ] - res = Exporter(includes=[]).dumps(circuit).splitlines() - for result, expected in zip(res, expected_qasm): - if isinstance(expected, str): - self.assertEqual(result, expected) - else: - self.assertTrue( - expected.search(result), f"Line {result} doesn't match regex: {expected}" - ) - def test_unusual_conditions(self): """Test that special QASM constructs such as ``measure`` are correctly handled when the Terra instructions have old-style conditions.""" @@ -2378,11 +2174,11 @@ def test_multiple_switches_dont_clash_on_dummy(self): OPENQASM 3.0; include "stdgates.inc"; bit[2] switch_dummy; -int switch_dummy__generated0; -int switch_dummy__generated1; +int switch_dummy_0; +int switch_dummy_1; qubit _qubit0; -switch_dummy__generated0 = switch_dummy; -switch (switch_dummy__generated0) { +switch_dummy_0 = switch_dummy; +switch (switch_dummy_0) { case 0 { x _qubit0; } @@ -2390,8 +2186,8 @@ def test_multiple_switches_dont_clash_on_dummy(self): y _qubit0; } } -switch_dummy__generated1 = switch_dummy; -switch (switch_dummy__generated1) { +switch_dummy_1 = switch_dummy; +switch (switch_dummy_1) { case 0 { x _qubit0; } @@ -2426,7 +2222,7 @@ def test_switch_nested_in_if(self): include "stdgates.inc"; bit[2] c; int switch_dummy; -int switch_dummy__generated0; +int switch_dummy_0; qubit _qubit0; if (c == 1) { switch_dummy = c; @@ -2439,8 +2235,8 @@ def test_switch_nested_in_if(self): } } } else { - switch_dummy__generated0 = c; - switch (switch_dummy__generated0) { + switch_dummy_0 = c; + switch (switch_dummy_0) { case 0 { x _qubit0; } @@ -2470,7 +2266,7 @@ def test_switch_expr_target(self): bit _bit0; bit[2] cr; int switch_dummy; -int switch_dummy__generated0; +int switch_dummy_0; qubit _qubit0; switch_dummy = !_bit0; switch (switch_dummy) { @@ -2478,8 +2274,8 @@ def test_switch_expr_target(self): x _qubit0; } } -switch_dummy__generated0 = cr & 3; -switch (switch_dummy__generated0) { +switch_dummy_0 = cr & 3; +switch (switch_dummy_0) { case 3 { x _qubit0; } @@ -2660,11 +2456,11 @@ def test_multiple_switches_dont_clash_on_dummy(self): OPENQASM 3.0; include "stdgates.inc"; bit[2] switch_dummy; -int switch_dummy__generated0; -int switch_dummy__generated1; +int switch_dummy_0; +int switch_dummy_1; qubit _qubit0; -switch_dummy__generated0 = switch_dummy; -switch (switch_dummy__generated0) { +switch_dummy_0 = switch_dummy; +switch (switch_dummy_0) { case 0: { x _qubit0; } @@ -2675,8 +2471,8 @@ def test_multiple_switches_dont_clash_on_dummy(self): } break; } -switch_dummy__generated1 = switch_dummy; -switch (switch_dummy__generated1) { +switch_dummy_1 = switch_dummy; +switch (switch_dummy_1) { case 0: { x _qubit0; } @@ -2714,7 +2510,7 @@ def test_switch_v1_nested_in_if(self): include "stdgates.inc"; bit[2] c; int switch_dummy; -int switch_dummy__generated0; +int switch_dummy_0; qubit _qubit0; if (c == 1) { switch_dummy = c; @@ -2730,8 +2526,8 @@ def test_switch_v1_nested_in_if(self): break; } } else { - switch_dummy__generated0 = c; - switch (switch_dummy__generated0) { + switch_dummy_0 = c; + switch (switch_dummy_0) { case 0: { x _qubit0; } @@ -2764,7 +2560,7 @@ def test_switch_v1_expr_target(self): bit _bit0; bit[2] cr; int switch_dummy; -int switch_dummy__generated0; +int switch_dummy_0; qubit _qubit0; switch_dummy = !_bit0; switch (switch_dummy) { @@ -2773,8 +2569,8 @@ def test_switch_v1_expr_target(self): } break; } -switch_dummy__generated0 = cr & 3; -switch (switch_dummy__generated0) { +switch_dummy_0 = cr & 3; +switch (switch_dummy_0) { case 3: { x _qubit0; } @@ -2824,7 +2620,7 @@ def test_disallow_custom_subroutine_with_parameters(self): exporter = Exporter() with self.assertRaisesRegex( - QASM3ExporterError, "Exporting non-unitary instructions is not yet supported" + QASM3ExporterError, "non-unitary subroutine calls are not yet supported" ): exporter.dumps(qc) @@ -2833,11 +2629,11 @@ def test_disallow_opaque_instruction(self): ``defcal`` block, while this is not supported.""" qc = QuantumCircuit(1) - qc.append(Instruction("opaque", 1, 0, []), [0], []) + qc.append(Gate("opaque", 1, []), [0], []) exporter = Exporter() with self.assertRaisesRegex( - QASM3ExporterError, "Exporting opaque instructions .* is not yet supported" + QASM3ExporterError, "failed to export .* that has no definition" ): exporter.dumps(qc)