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::fromOpenQASMString(source);
auto fromFile = mlir::QCProgram::fromOpenQASMFile("program.qasm");

The lower-level mlir::qc::translateOpenQASMToQC importer accepts OpenQASMImportOptions. Its gatePolicy field selects the gate policy, and maxOperations limits the number of inserted QC operations (1,000,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_openqasm_str(source)
from_file = QCProgram.from_openqasm_file("program.qasm")
qiskit_circuit = QCProgram.from_openqasm_str(source).to_qiskit()

Python convenience functions such as compile_program recognize OpenQASM source strings by the OPENQASM header. For versionless source, construct a QCProgram with from_openqasm_str first. The explicit constructors and .qasm file imports accept versionless input directly.

mqt-cc recognizes .qasm files automatically. Use --input-format=openqasm when the filename does not identify the format:

mqt-cc program.qasm
mqt-cc --input-format=openqasm program.txt

The default output checkpoint is --emit=qc, which emits QC MLIR after the default compiler pipeline. Use --emit=qc-import to inspect the imported QC before cleanup or QCO optimization. MLIR is the representation shared by the QC, QCO, and jeff dialects, so each output checkpoint names its dialect.

Input support

OpenQASM concept

Support and restrictions

Versions and includes

Versionless input and versions 2.0, 3.0, and 3.1 are accepted within the supported subset below. stdgates.inc, qelib1.inc, and nested textual includes are supported.

Classical types

bit, bool, int, uint, and float declarations are supported, including integer widths 1–64. Initialized compile-time angle[N] values support widths 1–52. Other sized numeric declarations, general arrays, complex values, and aliases are not yet supported.

Outputs

Explicit output declarations are preserved in source order. Without any explicit output, global classical variables become outputs.

Gates

Language gates, the standard libraries, custom gates, broadcasting, and inv, ctrl, negctrl, and pow modifiers are supported. Custom definitions remain private QC functions instead of being expanded at every use. Recursive custom gates are rejected.

Quantum statements

Measurement, reset, barrier, logical qubits, and physical qubits are supported. The QC translation 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 ~, &, |, ^, <<, >>, popcount, rotl, and rotr.

Structured control

if, switch, supported range-based for, and while. break exits the innermost enclosing loop; continue advances to its next iteration. Both may appear inside conditional and switch bodies.

Dynamic indexing

Classical bit indices can be dynamic and must remain in bounds. A nonconstant qubit index must be a proven affine expression as described below.

Unsupported language areas

Subroutines, extern, calibration and timing constructs, input declarations, and arbitrary arrays are diagnosed.

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. Leading zeros count toward that width; the rightmost digit initializes bit zero. For example, bit[6] b = "00_1101" has value 13, and b[3:-1:1] has value "011": the first selected bit becomes bit zero of the slice. These are the OpenQASM bit-register conventions.

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. The importer diagnoses statically invalid inputs and emits no runtime assertions. Runtime classical indices must be in bounds after negative-index wrapping, range steps must be nonzero, and integer arithmetic powers require nonnegative exponents. Integer pow modifier exponents must be exactly representable as f64, the QC/QCO exponent type. These are program preconditions; violating them has no guaranteed diagnostic or result.

Runtime integer arithmetic, including powers, 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.

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 negative indices relative to the register width when it can prove their sign. It rejects measurement-derived values, indices whose sign is unknown, 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.

Register operands support inclusive slices q[first:last] and q[first:step:last], including negative indices and negative steps. Omitted endpoints select the register ends in the step’s direction. With the default step, q[:last] means q[0:last] and includes last; q[:2] selects qubits 0, 1, and 2. Constant slices expand in selection order. Gates broadcast over slices; a slice does not supply multiple control arguments to ctrl(n).

Slice lengths and steps must be statically known. Bounds may use the same proven affine expressions as scalar qubit indices. For example, q[i:i] selects q[i], and q[i:i+1] selects two qubits when both are in bounds. A slice remains a register operand, so all register operands of a broadcast must have the same length, including one-element slices. Measurement-selected ranges and unproved lengths, bounds, or operand distinctness are rejected during analysis. Slice lowering adds no runtime assertions or wider integer arithmetic.

Classical slices support fixed-width bit-vector expressions and assignments. Their first selected bit becomes bit zero of the value. Assignments snapshot the source before writing, so overlapping copies are safe. Constant selections use scalar bit operations; selecting a whole register in order uses a register read or write. Compound assignments to indexed bits or slices are not supported.

Reads through nonconstant classical indices require the whole source register to be initialized. Such writes do not prove whole-register initialization, including measurement writes. Constant selections track initialization per bit.

For convenience, import also accepts three-part slices with an omitted final bound and measurement between a scalar and a one-element register. Export expands slices and emits scalar measurements that follow the OpenQASM range grammar and measurement types.

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 "mqt/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::fromOpenQASMFile("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_openqasm_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. Logical qubit and CBit indices can be dynamic. Mapped programs require static physical qubits; indexed tensor loops must be specialized before export.

Quantum operations

Measurement, reset, barrier, deallocation, global phase, and QC unitary operations. The exporter uses standard gates where available; for example, sxdg becomes inv @ sx and u and u2 use a shared helper that compensates the OpenQASM 3 U global phase.

Reusable gates

Private functions with leading f64 parameters followed by scalar qubit arguments and no results. Straight-line mqt.unitary functions use qc.call; loop-containing gate functions use func.call.

Gate modifiers

Nested ctrl, inv, and pow. A multi-operation modifier body with target qubits becomes a private generated gate.

Scalar values

Integers of widths 1–64, f64, and internal index values, including arithmetic, comparisons, Boolean operations, value-preserving casts, and supported math functions.

Constant tables

Reads from non-empty constant rank-one f64 tensors. Constant indices and splats become scalar values; other reads use switches with shared cases for equal values.

Structured control

scf.if, scf.index_switch, signed scf.for with positive constant steps, and general two-region scf.while, including scalar arguments and results. Index switches use native switch, case, and default.

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, inclusive integer ranges use 64-bit arithmetic, including unsigned bounds, negative steps, break, and continue. The loop tests the unsigned distance to the endpoint before continuing, so a final increment that wraps cannot cause another iteration. Range lowering does not require wider integer types 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 !cbit.reg<N>

bit[N]

i1 produced directly by measure

bit

Other i1

bool

i64 or index

int

Other integers of 2–63 bits

uint[N]

f64

float

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.

Measurements write directly to a named register when their result has one store and that store can occur at the measurement without crossing a conflicting classical access. This also preserves registers for grouped measurements after routing. Measurement order is unchanged; other cases retain temporary bits.

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 1,000,000,000 inserted operations. Textual expansion is limited to 100,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 rejects unsupported operations, including explicit cf.assert operations and live poison values. It does not silently discard them. Programs with supported qubit indices, dynamic bit indices, and 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.