Bloq IR¶
Bloq IR stores the compiled fault-tolerant physical circuits, their placements, detector checks, readouts, and classical control. Backends consume it without the source block graph. Templates, loops, and retries remain structured.
This chapter introduces the data structure, its text and binary formats, and the APIs for reading, querying, editing, and checking a program.
Design in one picture¶
Bloq IR is a dependency graph over two shared pools:
flowchart LR pool["Template pool<br/>circuits + flows"] --> q["Quantum nodes<br/>placed instances"] bundles["Detector bundle pool<br/>shared checks"] --> q q -->|Order| c["Classical nodes<br/>readouts + logic"] c -->|Value| c2["Classical nodes"] c2 -->|Value guard| q2["Quantum nodes"] q -->|Quantum| q2
Edges record dependencies. Node ids and coordinates do not specify execution times.
Repeated work |
IR representation |
|---|---|
Fixed circuit repetitions |
Named |
Dynamic retries |
|
Circuits therefore cost storage once per distinct template, not once per use. A 128-bit controlled adder at distance 3 stores 11,372 quantum nodes over 90 templates. Classical definitions also share storage, while each invocation retains its own bindings and activation.
Storing each body once eliminates duplicated code. Downstream compilers can optimize the body once and consume the loop structure directly.
Data structure¶
Program¶
A Bloq program contains:
Part |
Contents |
|---|---|
Metadata |
Key–value pairs, such as |
Template pool |
Circuit templates, indexed by |
Detector bundle pool |
Shared detector recipes, indexed by |
Logical inputs and outputs |
Boundary operators at their owning instances |
Top-level graph |
Nodes, edges, and nested retry bodies |
Templates and instances¶
A template holds a physical circuit on relative qubit coordinates and everything derived from it once:
Field |
Meaning |
|---|---|
|
Gates, measurements, |
|
Checks closed inside the template |
|
Loop-carried detector state for |
|
Post-selection parities for T-state retries |
|
Stabilizer flows the template leaves open for its neighbors |
An instance places a template at a qubit offset and records its source block. Instances share circuit data and own separate measurement records.
Record address |
Example |
|---|---|
Template-local measurement |
|
Measurement at a specific instance |
|
Nodes¶
Every node has a kind and a provenance that links it back to the source.
Kind |
Variants |
Purpose |
|---|---|---|
Quantum |
— |
Placed instances, cross-instance detectors, bundle uses, restarts, and guards |
Classical |
|
Readout recipe built from measurements and boundary operators |
|
Boolean expression over input slots |
|
|
Rejects the shot when its condition holds |
|
Region |
|
Retries its body until a restart predicate accepts |
Provenance |
Node it marks |
|---|---|
|
Quantum node for source blocks sharing physical qubits on a layer |
|
Realignment layer of a temporal Hadamard pipe |
|
Compiler-inserted replacement of a spatial Port |
|
Memory rounds inserted after compilation, with their count |
|
Readout for a correlation surface |
|
Lowered source action, such as a named measurement |
|
Resolved selector of a branch or selective cap |
|
X or Z frame bit for an output Port |
Guards and activation¶
A guard is a Boolean condition that decides which work participates in an
execution. A measurement or Compute node supplies the bit through a Value
edge. Compound conditions, such as NOT s or a AND b, are computed before
they reach the guarded node.
Control |
When true |
When false |
|---|---|---|
Quantum membership ( |
Includes the listed instances, detectors, bundle uses, and restart checks |
Omits them. Skipped instances produce no gates or measurement records |
Classical activation ( |
Evaluates the node normally |
Exports zero and no boundary bindings, without reading records, querying a decoder, or discarding the shot |
Retry-region activation |
Executes the retry body |
Skips the body and exports zero with no boundary bindings |
Quantum-edge guard |
Enables the patch connection |
Omits that connection |
Quantum members absent from every guard are common to all choices. Classical nodes without activation always evaluate. Guard bits must be available before the controlled work starts. An activation bit enables evaluation; it is not XORed into the readout parity.
For example, a selective cap can choose between X and Z readout using s:
|
Quantum membership |
Classical activation |
|---|---|---|
False |
|
The X-readout fragment evaluates; the Z fragment contributes zero |
True |
|
The Z-readout fragment evaluates; the X fragment contributes zero |
Each readout fragment uses the same condition as its measurement instance, so it never reads records from an unselected alternative. An inactive fragment’s zero means no contribution, not a measured logical zero.
In text IR, guard 0 i5 refers to input slot 0, whose Boolean value controls
instance i5. A classical node’s when v0 uses slot 0 for activation.
Edges¶
Edge |
Payload |
Meaning |
|---|---|---|
|
Temporal pipes, padding templates, optional guard |
A patch passes from one node to the next |
|
Input slot, output port, and role |
|
|
Input slot and role |
Includes raw parity and boundary bindings from an observable fragment |
|
— |
The target waits for the source to finish |
Quantum edges retain one-round and looped memory templates for their seams. Synchronization uses them to insert memory rounds without the source graph.
Detectors, observables, and frames¶
Detectors check measurement parities at three levels:
Stored in |
Checks |
|---|---|
Template |
Records within one circuit |
Quantum node |
Records across template instances |
Detector bundle |
A shared recipe whose owner slots bind to concrete instances at each use |
Observables combine measurement parities with boundary operators:
Form |
Purpose |
Decoder query |
|---|---|---|
Indexed |
Complete readout |
One query shared by both output ports |
Unindexed |
Reusable readout fragment |
None |
A Compose edge adds a fragment’s raw parity and boundary bindings to its
parent. It adds no separate decoder correction. Shared fragments keep their
own activation and instance owners. The optimizer merges compatible leaves
or inlines them into their parents.
flowchart LR fragments["Readout fragments"] -->|Compose| observable["Indexed Observable<br/>one decoder query"] observable --> corrected["Corrected<br/>raw parity XOR Flip"] observable --> flip["Flip<br/>decoder estimate"]
Reference |
Carries |
|---|---|
|
Indexed observable’s corrected bit, or an ordinary producer’s Boolean result |
|
Indexed observable’s decoder estimate |
|
Raw parity and boundary bindings. Boolean |
Output frames describe corrections for live logical outputs:
Property |
Representation |
|---|---|
Frame bit |
|
X/Z pair per output Port |
Returned by |
Exported result |
Retained even when its node has no outgoing edges |
T-state retries use a body-local Compute node to form
restart = Flip₀ OR Flip₁. Either predicted flip triggers a retry.
Physical post-selection failures also trigger retries.
Bloq IR Format¶
Format |
Extension |
Use |
|---|---|---|
Text IR |
|
Inspection, review, and diffs |
Binary IR |
|
Compact program exchange |
Text Bloq IR¶
Text IR starts with BLOQIR 1 and lists the program’s parts in order. Lines
starting with # and blank lines are ignored. Parse errors report one-based
line numbers. The excerpts below come from a compiled logical T gate.
Header and boundaries. Metadata, then logical inputs and outputs with their owning instance and X/Z operators:
BLOQIR 1
metadata bloq_compile.code_distance u64 3
metadata bloq_compile.convention string fixed-bulk
logical-input (0,0,0) instance i2 x X(1,3)*X(3,3)*X(5,3) z Z(3,1)*Z(3,3)*Z(3,5)
Templates. A circuit on relative coordinates, then its detectors, loop
states, and boundary flows. REPEAT 2 b1 runs the named body b1 twice:
template t9 {
circuit {
R (2,0) (4,2) (2,4) (4,6)
...
REPEAT 2 b1
}
body b1 { ... }
detector body(b1) m8*s0 @ (2,0)
loop(b1,s0) init m0 next m8
flow Z(1,1)*Z(3,1) -> _ meas m0 center (2,0)
}
Detector bundles. Owner slots o0, o1 are bound at each use:
bundle b0 owners t2 t3 {
detector o0:m0*o1:m0 @ (2,0)
}
Graph. Quantum nodes list instances, bundle uses, and guards. The
selective cap’s node enables instance i5 on guard slot 0 and i6 on slot 1:
graph {
n2 quantum {
instance i5 t5 @ (8,0)
instance i6 t6 @ (8,0)
use b3 i4 i5 @ (8,0)
use b4 i4 i6 @ (8,0)
guard 0 i5 b0
guard 1 i6 b1
from blocks (1,0,2)
}
The T-state factory stores its body in a retry region. Its restart expression
ORs the two observable Flip outputs:
n4 rus in0 source n4 {
body {
n1 quantum { ... restart i1:m11*i1:m13 ... }
n2 observable 0 measurements i1:m15*... operators i1 input ..., i1 output ...
n3 observable 1 measurements i1:m16*... operators i1 input ..., i1 output ...
n4 compute in0 | in1
n2 -> n4 value 0 flip
n3 -> n4 value 1 flip
}
}
Classical nodes name their records, operators, expressions, activation
(when v0), and provenance:
n13 observable 2 measurements i1:m15*...*i4:m4 operators i1 input ... from action 0
n7 compute in0 from selector selective%20%5B1%2C0%2C2%5D
n15 observable fragment measurements i1:m15*...*i6:m7 operators i1 input ... when v0
n24 compute in0 from frame z (0,0,2)
Edges come last. A quantum edge names its pipe and its padding templates:
n1 -> n2 quantum (1,0,1)>(1,0,2) padding offset (8,0) one t8 loop t9
n12 -> n13 compose 2
n13 -> n24 value 0
n13 -> n25 value 0 flip
n1 -> n3 order
}
Binary Bloq IR¶
Binary IR stores the same program compactly. Text and binary convert into each other exactly. The CLI converts binary IR to text with:
bloq emit program.bloq --backend ir-text -o program.bloqir
Save, load, and convert¶
import bloq
program = bloq.compile(bloq.GalleryItem.X_MEMORY.load(), distance=3)
program.save("memory.bloq")
loaded = bloq.Bloq.load("memory.bloq")
loaded.save("memory.bloqir")
use bloq::{graph::GalleryItem, ir::Bloq};
let program = bloq::compile::compile(&GalleryItem::XMemory.build(), 3)?;
std::fs::write("memory.bloq", program.to_binary())?;
let loaded = Bloq::from_binary(&std::fs::read("memory.bloq")?)?;
std::fs::write("memory.bloqir", loaded.to_text())?;
save and load choose the codec from the file extension. Decoders reject
malformed structure, such as dangling endpoints or duplicate ids, but do not
check every semantic rule. Call validate() for that.
APIs¶
The tables list the Python names. Rust provides the same operations on
bloq::ir::Bloq. The Rust API gives their exact
signatures.
Read and traverse¶
Operation |
API |
|---|---|
Program counts |
|
Top-level nodes and edges |
|
Every level, including region bodies |
|
Classical dataflow |
|
Templates and bundles |
|
Physical layout |
|
A schedule that respects every edge |
|
Node ids are local to one graph level. walk() reports each node with its
path, the chain of (region node, body) hops from the top level:
for entry in program.walk():
kind = entry.node.kind
if isinstance(kind, bloq.ir.BloqNodeKind.Classical):
print(entry.path, entry.id, kind.node)
[(4, 'body')] 2 ClassicalNode.Observable(index=0, measurements=5, operators=2)
[(4, 'body')] 3 ClassicalNode.Observable(index=1, measurements=4, operators=2)
[(4, 'body')] 4 ClassicalNode.Compute(expr=or(in(0), in(1)))
[] 5 ClassicalNode.Compute(expr=in(0))
[] 6 ClassicalNode.Compute(expr=parity(true, in(0)))
[] 7 ClassicalNode.Compute(expr=in(0))
[] 13 ClassicalNode.Observable(index=2, measurements=13, operators=2)
[] 15 ClassicalNode.Observable(index=None, measurements=10, operators=1)
[] 17 ClassicalNode.Observable(index=None, measurements=3, operators=1)
[] 23 ClassicalNode.Observable(index=3, measurements=5, operators=2)
[] 24 ClassicalNode.Compute(expr=in(0))
[] 26 ClassicalNode.Observable(index=4, measurements=0, operators=2)
[] 27 ClassicalNode.Compute(expr=in(0))
Templates are reached through instances:
node_id, quantum = program.quantum_nodes()[0]
instance = quantum.instances[0]
template = program.template(instance.template_id)
Query structure¶
These queries name the landmarks a backend usually needs. Each fails with a structural error if the program does not have the expected shape, rather than returning a plausible but wrong node.
Question |
API |
|---|---|
Output frame bits per output Port |
|
Logical outputs and their owning instances |
|
Seams feeding guarded quantum alternatives |
|
Region nodes of a kind |
|
Unique quantum node with no outgoing quantum edge |
|
Node realizing a source block |
|
Node identity that survives recompilation |
|
Resolve classical values¶
API |
Result |
|---|---|
|
Measurement parity, constant sign, and decoder observables |
|
Boolean value under a fixed assignment |
For nonlinear choices, supply one assignment method:
Argument |
Assignment |
|---|---|
|
Gives every predicate leaf the same Boolean value |
|
Fixes individual corrected observable values |
resolution = program.resolve_classical(13)
# <ClassicalResolution measurements=13 sign=False observables=[2]>
Edit and specialize¶
Operation |
API |
|---|---|
Fix structural choices, keeping the original |
|
Check for unresolved guarded alternatives |
|
Insert memory rounds |
|
Change a template’s round count |
|
Unroll every |
|
Edits are transactional: a failed edit leaves the program unchanged. Synchronization explains the memory-round edits with examples.
Check and visualize¶
Operation |
API |
|---|---|
Full semantic audit |
|
Per-node circuit that a backend would emit |
|
Dependency graph as SVG |
|
Compilation, emission, and VM lowering perform their own local checks.
validate() is the separate whole-program audit. Use it for IR from another
producer or IR edited by hand.
Visualize the graph¶
Export the nested dependency view used by Bloq Editor as a self-contained SVG:
from pathlib import Path
Path("t-gate-ir.svg").write_text(program.to_svg(include_classical=True), encoding="utf-8")
std::fs::write("t-gate-ir.svg", program.to_svg(true))?;
bloq view t-gate.bloqir --svg --include-classical -o t-gate-ir.svg
Shown |
Not shown |
|---|---|
Nested graphs, dependencies, and activation conditions |
Runtime retry attempts and execution times |
What a consumer must preserve¶
Information |
Required interpretation |
|---|---|
Loop structure |
Preserve repetition counts, loop-carried detector state, and retry conditions when transforming loop bodies |
Detector sign |
Evaluate the stored constant together with the records |
|
Corrected logical parity versus the decoder correction alone |
|
Includes recipe parity and bindings without adding the child’s decoder flip |
Fragment activation |
Inactive contributions produce no record reads or boundary bindings |
Expression operands |
Every declared operand must be available, including unselected |
Retry records |
Records and accepted exports belong to their attempt |
Logical output |
Keep its owning instance, X/Z operators, and frame pair |
Equal physical support at two times does not identify the same logical output. A consumer must keep the boundary’s owning instance and dependencies.
Continue with Backend Emission to turn a program into a Stim circuit or verify dynamic control in simulation. The Python API and Rust API document every method.