OpenQASM input and output¶
MQT Core accepts OpenQASM as a compiler input and can export structured programs from the QC dialect.
The OpenQASM specification defines the language. This page describes the subset supported by MQT Core.
Import OpenQASM¶
The frontend parses and validates the source before translating it directly to QC. The C++ compiler API accepts strings and files:
auto fromString = mlir::QCProgram::fromQASMString(source);
auto fromFile = mlir::QCProgram::fromQASMFile("program.qasm");
The lower-level mlir::qc::translateQASM3ToQC importer accepts
QASM3ImportOptions. Its frontend field selects the gate policy, and
maxOperations limits the number of inserted QC operations (10,000,000 by
default). Exceeding the limit emits a diagnostic and returns no program.
Python provides the corresponding constructors:
from mqt.core.mlir import QCProgram
from_string = QCProgram.from_qasm_str(source)
from_file = QCProgram.from_qasm_file("program.qasm")
qiskit_circuit = QCProgram.from_qasm_str(source).to_qiskit()
mqt-cc recognizes .qasm files automatically. Use --input-format=qasm when
the filename does not identify the format:
mqt-cc program.qasm
mqt-cc --input-format=qasm program.txt
Input support¶
OpenQASM concept |
Support and restrictions |
|---|---|
Versions and includes |
Versionless input and versions 3.0 and 3.1 use the maintained OpenQASM profile. |
Classical types |
|
Outputs |
Explicit |
Gates |
Language gates, the standard libraries, custom gates, broadcasting, and |
Quantum statements |
Measurement, reset, barrier, logical qubits, and physical qubits are supported. The QC target rejects programs that mix logical allocation with physical qubits. |
Expressions |
Scalar arithmetic, comparisons, Boolean expressions, and the supported math functions are type checked before translation. Initialized bit registers support |
Structured control |
|
Dynamic indexing |
Classical bit indices can be dynamic and receive runtime bounds checks. A nonconstant qubit index must be a proven affine expression as described below. |
Unsupported language areas |
Subroutines, |
Sized uint[N](bits) and int[N](bits) casts accept an initialized bit[N]
register when the constant width is 1 through 64. Bit zero is the least
significant bit. Signed casts use two’s-complement representation, with bit
N - 1 as the sign bit.
float(value) and float[64](value) accept numeric and Boolean values.
Bit-string literals contain binary digits, optionally separated by underscores,
and must match the destination register width.
Textual includes use LLVM SourceMgr lookup: paths are tried relative to the process working directory, then in the include directories supplied to SourceMgr. They are not resolved relative to the including file. The built-in standard libraries do not require files on disk.
Syntax and semantic diagnostics retain source locations and include stacks. Classical-index bounds and integer-power preconditions are represented explicitly in QC. Runtime integer arithmetic uses machine-width promotion and wraps modulo that width; explicit integer casts truncate or extend to their declared width. Compile-time invalid arithmetic is diagnosed. Runtime division by zero remains undefined. Dynamic-index checks are supported by compiler/QIR paths but remain outside the source-export subset.
OpenQASM 3 supports all six comparisons between fixed-width bit-register
expressions. Direct register comparisons use unsigned meaning. An exact-width
int[N] cast selects signed two’s-complement interpretation before the
language’s usual integer promotion. The frontend also accepts these conditions
in OpenQASM 2 as a compatibility extension; version-specific initialization and
gates are unchanged.
Runtime shift distances have unsigned interpretation. Overshifts produce zero. The frontend checks the original distance before narrowing it and uses a safe count even in the unselected shift. Constant distances fold without guards. The same helper is used by Qiskit import.
For Qiskit-generated source, nonnegative constant operands of typed bitwise expressions are accepted when they fit the unsigned operand width. Standalone unsized constant bitwise expressions use the 64-bit machine width. This does not give runtime signed integers an implicit unsigned interpretation.
For the same compatibility reason, a whole-register assignment accepts a nonnegative integer constant that fits the register width. Use an exact-width bit-string literal for strict OpenQASM source.
Fixed-width angles are a compile-time input feature. An omitted angle width
resolves to 52 bits. Both const angle[N] and initialized angle[N]
declarations are accepted as write-once values. Initializers and angle casts
must be compile-time expressions. MQT Core supports float-to-angle conversion,
angle resizing, unary negation, addition and subtraction, multiplication and
division by nonnegative integer literals that fit the angle width, comparisons,
and sin, cos, and tan. Mixed-width angle operands promote to the wider
width. It uses round-to-nearest, ties-to-even for float conversion and
narrowing. Runtime angle state, reassignment, bit-level angle operations, and
angle inputs or outputs are not supported.
The frontend accepts a nonconstant qubit index only when it proves that every
value is in the register and that operands of one gate or explicit barrier are
distinct. Proven expressions can contain constants, positive constant-step for
induction variables, known scalar values, negation, addition, subtraction,
multiplication by an integer constant, and value-preserving int/uint casts.
Assignments and control-flow joins preserve a scalar value only while its affine
form remains known. A nested loop bound can use proven induction variables from
enclosing loops. The proof treats an inclusive range as its full interval and
does not use the step’s congruence.
The frontend normalizes constant negative indices relative to the register width. It rejects measurement-derived values, nonconstant negative indices, nonlinear expressions, unsupported integer operators, and ranges whose step is not known to be positive when their induction variable reaches a qubit index. Mutations in repeating loops and unequal branch values invalidate scalar facts. Branch conditions do not add proof facts. Classical bit indexing and loops that do not index qubits keep their runtime behavior.
Bit registers use !cbit.reg<N> in QC. OpenQASM 2 initializes each register to
zero. OpenQASM 3 leaves each register undefined until a statement writes it. A
static read requires its bit to be initialized. A dynamic read requires the
whole register to be initialized; writing one dynamic index does not prove that
a later dynamic read accesses an initialized bit. Whole-register reads and
writes lower to cbit.read and cbit.write. Standard integer operations
represent computation, including all comparisons: cbit.read produces the
snapshot, arith.constant the comparison constant, and arith.cmpi determines
signedness. CBit operations carry storage memory effects. jeff legalization
preserves native widths and promotes other widths up to 64 to 8, 16, 32, or 64
bits, masking results to retain exact-width semantics. Wider
register-versus-constant comparisons remain supported; wider general integer
expressions are rejected. Integer-to-floating-point casts (for example, using a
runtime population count as a rotation angle) remain outside the jeff subset.
Explicit outputs and implicit global outputs are returned by the entry function;
internal CBit allocations are not outputs. Other scalar outputs use builtin MLIR
scalar types. Programs without classical outputs have a void entry function; a
returned integer zero is ordinary output data. A scalar qubit lowers to
qc.alloc, while qubit[1] remains a one-element qubit register.
Export OpenQASM¶
The exporter prints validated QC and SCF operations. The translation is failure-atomic: it prepares the complete source before writing to the requested stream.
Use the translation API for a ModuleOp:
#include "mlir/Dialect/QC/Translation/TranslateQCToOpenQASM3.h"
auto source = mlir::qc::translateQCToOpenQASM3(moduleOp);
if (mlir::failed(source)) {
// An MLIR diagnostic describes the unsupported operation.
}
The compiler API returns an owned textual program:
auto qc = mlir::QCProgram::fromQASMFile("input.qasm");
auto direct = qc->toOpenQASM3(); // Export without QCO optimization.
direct->write("direct.qasm");
auto reimported = mlir::runDefaultPipeline(
mlir::CompilerInput{*direct}, mlir::ProgramFormat::QCImport);
auto optimized = mlir::runDefaultPipeline(
mlir::CompilerInput{std::move(*qc)}, mlir::ProgramFormat::OpenQASM3);
Python exposes both forms:
from mqt.core.mlir import OutputFormat, QCProgram, compile_program
qc = QCProgram.from_qasm_file("input.qasm")
direct = qc.to_openqasm3()
print(direct.source)
direct.write("direct.qasm")
optimized = compile_program("input.qasm", output=OutputFormat.OPENQASM3)
optimized.write("optimized.qasm")
The command-line driver writes to standard output unless -o is given:
mqt-cc input.qasm --emit=openqasm3
mqt-cc input.qasm --emit=openqasm3 -o optimized.qasm
The compiler-pipeline path performs target compilation when requested, runs the
QCO optimization pipeline, converts back to QC, and then exports. Calling
to_openqasm3() or
mlir::QCProgram::toOpenQASM3 applies the QC cleanup pipeline but
bypasses that QCO optimization round trip.
Export and round-trip support¶
QC or MLIR concept |
Export support |
|---|---|
Qubits and classical bits |
Logical and physical qubits, scalar qubit allocations, static rank-one qubit memrefs, and CBit registers. Qubit memory indices must resolve statically. CBit indices can be dynamic. |
Quantum operations |
Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, |
Reusable gates |
Private functions with leading |
Gate modifiers |
Nested |
Scalar values |
Integers of widths 1–64, |
Structured control |
|
Results |
Multiple scalar and bit-register outputs using the canonical type and naming rules below. |
For loops preserve their exclusive upper bound when exported to an inclusive OpenQASM range. Dynamic bounds are supported in the entry function and are evaluated before the loop. An empty-range guard prevents underflow when converting the upper bound. Loop-carried values retain their initial values when the range is empty.
On import, dynamic ranges with signed bounds and a positive signed constant step
use 64-bit arithmetic when their bodies contain no break or continue. The
loop tests the unsigned distance to the inclusive endpoint before continuing, so
a final increment that wraps cannot cause another iteration. This supports round
trips without restricting the signed range of the endpoints. Other range forms
can still require wider arithmetic or runtime checks that the exporter rejects.
Entry-function while loops use while (true) and a conditional break, so
condition-region expressions are evaluated once per iteration. Gate functions
retain direct while (condition) syntax because they cannot declare local
state. The before and after regions may have different argument counts and
types. Local variables preserve scalar state, exit values, and simultaneous
updates such as swaps. The condition and its forwarded values are evaluated
before any continuation updates.
For example, this terminating do-while form executes its body three times:
OPENQASM 3.1;
include "stdgates.inc";
qubit q;
int count = 0;
while (true) {
x q;
count += 1;
if (!(count < 3)) { break; }
}
Import recognizes this form structurally and produces one scf.while with a
forwarding after region. No additional conditional is needed solely to exit.
Initialization facts include every reachable break: a value assigned before all
exits of this do-while form is initialized afterwards. An ordinary while loop
may execute zero times. Constant-true loops remain valid; translation does not
prove termination.
Resources must be allocated outside loops and conditionals. Unsupported scalar types, operations, captures, or runtime gate parameters produce a target-specific diagnostic. Export preserves the source module and buffers output until it succeeds.
The exporter writes an OpenQASM 3.1 version declaration and includes
stdgates.inc. Gates in MQT Core’s compatibility catalog, such as r, rzz,
and ecr, receive definitions under their catalog names. Strict consumers use
those definitions. MQT Core’s default compatibility mode recognizes a definition
with the catalog name and signature and imports calls directly as the
corresponding native QC operation; the definition body is deliberately ignored.
A same-name definition with a mismatched signature is rejected. Strict mode
always analyzes the custom definition normally.
The _mqt_ prefix is reserved for generated composite-modifier gates,
temporaries, and collision-safe identifiers. Existing classical-register
allocation names are reused when valid and distinct from catalog gates; scalar
output names are generated deterministically.
Output types follow a deliberately small canonical mapping:
QC result |
OpenQASM output |
|---|---|
Returned |
|
|
|
Other |
|
|
|
Other integers of 2–63 bits |
|
|
|
Outputs preserve function-result order, including mixed scalar and register results and constant-zero integers. Returning the same register more than once is diagnosed because OpenQASM outputs cannot preserve that aliasing. Programs without results keep generated classical temporaries in a local scope to avoid implicit outputs. Unused measurement results need no temporary.
Import and export do not preserve uint, fixed-angle spelling or width,
scalar-versus-one-element bit spelling, or scalar output names. Integer
computations use explicit int[N]/uint[N] casts, so signedness is chosen by
each MLIR operation rather than inferred from its source register. Truncation,
sign/zero extension, arithmetic, bitwise operations, comparisons, shifts, and
integer selection are supported. Selection uses a fixed-width bit mask and does
not allocate a temporary register. The frontend accepts the casts and
expressions emitted by the exporter, including Boolean/integer conversions.
Export limitations¶
Export requires one defined, argument-free entry function. Additional functions
must be private, defined gate functions with leading f64 parameters followed
by scalar qubit arguments and no results. Gate functions may contain supported
scalar expressions, quantum operations, calls, and loops. Measurement, reset,
barrier, allocation, classical storage, conditionals, switches, and
arith.select are rejected in gate functions. For-loop bounds in gate functions
must remain constant. Gate while loops require a pure condition region and no
loop-carried values.
The exporter rejects arbitrary CFGs, multi-block SCF regions, recursive or
unresolved calls, dynamic qubit indices, dynamic for-loop steps, unsigned
scf.for comparisons, general memrefs, unsupported integer widths, unknown
operations, and non-unitary content inside modifier regions. CBit loads, stores,
whole-register reads and writes, fixed-width bitwise operations, dynamic bit
indices, SCF results, and loop-carried values are supported in the entry
function. Multi-operation modifier bodies must have a target qubit and cannot
capture additional qubits from an enclosing scope.
OpenQASM export supports arbitrary bit-register widths for bitwise operations,
unsigned comparisons, popcount, rotl, and rotr. Scalar arithmetic, integer
casts, signed comparisons, and logical shifts require widths of at most 64 bits.
Rotation counts must be constant or represented by at most 64 bits, optionally
zero-extended to the register width. Qiskit interoperability uses the common
subset described in the Python compiler documentation.
The exporter stores used scalar expressions and bit-register reads in local variables at their definition. These variables preserve snapshots across later writes and nested regions and prevent repeated expansion of shared expressions. Wide bit-vector expressions use local bit registers. Zero-initialized registers use one exact-width bit-string initializer. Shift interpretation is determined by the MLIR operation, not by the history of its operands. Arithmetic right shifts are encoded with unsigned bitwise operations and explicit sign-bit biasing.
Inline expressions, including those in gate functions, have a nesting limit of 256 and an expansion budget of 4,096 values per expression. The total width of classical registers, including wide snapshots, is limited to 1,048,576 bits. Import limits affine proofs to 256 levels and 4,096 distinct expressions per proof and QC emission to 10,000,000 inserted operations. Textual expansion is limited to 1,000,000 statements and 1,000,000 file-include expansions, including empty files. Standard-library includes count as statements. Include nesting is limited to 64 levels. Exceeding any bound produces a diagnostic and no program.
The exporter does not reconstruct the runtime checks created for dynamic indices
or checked integer arithmetic. Surviving assertions, checked-index control flow,
or live poison values cause an explicit diagnostic. Programs with static qubit
and bit indices and supported integer/Boolean casts can be exported and parsed
again through the strict frontend. Integer-to-floating-point conversions support
this round trip. Compile-time floating-point-to-integer conversions remain
outside the input subset. Floating-point != uses unordered-or-not-equal
semantics, including NaNs; ordered-not-equal MLIR comparisons are rejected.
Programs that rely on the input safety machinery must continue through another
output path such as QIR.