RIR Interpretive Execution¶
Overview¶
RIR (Register-level Intermediate Representation) is the register-level
intermediate representation of the quantum programming language QECC.Lang
(the pyqecclang package). An RIR program is a versioned JSON document:
it declares typed named registers ( bits / uint / sint /
rational with bit widths), QRAM resources, and a module graph — the
module bodies mix gate-level primitives with structured control nodes
( Call / Repeat / Control / Adjoint ).
PySparQ is the natural interpreter for RIR. This is no coincidence:
RIR itself is register-level: it talks about “registers”, “views”, “integer-word addition” and “QRAM loading”, rather than physical qubits and gate sequences;
PySparQ’s programming model happens to be precisely register-level programming (Register Level Programming): named typed registers, native register arithmetic operators, and native QRAM queries.
Executing RIR therefore requires no extra “lowering” stage:
pysparq.run_rir interprets the JSON document directly on a
SparseState, expands the module graph at interpretation time, and maps
register-level operations onto native PySparQ operators whenever the
operands align with whole registers. The interpreter consumes only the
JSON document and does not depend on the pyqecclang package itself, so
the two implementations can cross-validate each other.
Concept Correspondence¶
RIR concepts correspond one-to-one to native PySparQ capabilities:
RIR concept |
Native PySparQ counterpart |
|---|---|
|
|
|
|
uncontrolled |
|
controlled |
|
gate broadcast over a view ( |
|
|
chains of controlled |
QRAM resource + |
|
|
multi-bit conditioning via |
|
inlined at interpretation time: argument views are bound to the callee through register renaming |
|
replayed at interpretation time (the serialized JSON stays unexpanded) |
|
reverse traversal + per-operation inversion (angles negated, modular subtraction, self-inverse gates) |
|
|
Automatic Processing Pipeline¶
Execution of run_rir is organized into five phases, all of them
carried out automatically for the user:
RIR JSON document
│ 1. Load and structural validation (version, entry, uniqueness of module names)
▼
Resource assembly: entry registers → native named registers; QRAM resources → qutrit-tree materialization
│ 2. Event-stream expansion (Call inlining / Repeat replay / Adjoint reversal / Control accumulation)
▼
Sparse-state execution: whole-register alignment → native operators; register slices → bit-by-bit fallback decomposition
│ 3. Result readout: amplitudes aggregated by the integer values of the entry registers
▼
RIRResult (registers + amplitudes, optional dense statevector)
A few key points:
The module graph is expanded at interpretation time.
Callexecutes in place by binding the argument views onto the callee’s formal registers through register renaming;Repeatreplays its body when executed;Adjointtraverses the body in reverse and inverts every operation; the conditions of nestedControlnodes accumulate by logical conjunction into a multi-bit condition.Native operators are preferred over decomposition. For example, when the operand of
add_constfully covers a register (start 0 and width equal to the declared width), the interpreter callsAdd_ConstUInt_InPlacedirectly instead of decomposing it into a multi-gate chain; the uncontrolled global phase goes throughGlobalPhase, and QRAM loading goes through the nativeQRAMLoad.View semantics are guaranteed by the fallback path. When an operand is a register slice, the interpreter falls back to bit-by-bit decomposition, and modular addition wraps around within the view width, consistent with the RIR specification.
Everything is cleaned up automatically after execution.
run_rirguaranteesSystem.clear()on exit; if the global register table is not empty before the call, an error is raised immediately.
Basic Usage¶
The entry API is provided by pysparq.rir and is also exported in the
top-level namespace:
import pysparq as ps
result = ps.run_rir(document, memory) # use the default budgets
result = ps.run_rir(document, memory, max_steps=500_000, max_states=4_096)
result = ps.run_rir_file("program.rir.json", memory={"rom": [1, 2, 4, 7]})
document = ps.load_rir("program.rir.json") # dict / JSON string / file path
document may be an already-decoded dict, a JSON string, or a file
path; memory binds contents to the QRAM resource names declared by the
entry module one by one, and each resource accepts either a full word
sequence or a sparse {address: word} mapping (cells left out are
zero).
The return value is an RIRResult:
result.registers # (("a", 1), ("b", 1)) —— entry registers (name, width), in declaration order
result.amplitudes # {(0, 0): 0.707..., (1, 1): 0.707...}
# the keys are tuples of the registers' integer values (in declaration order)
result.statevector() # dense little-endian state vector, index = value(r0) + (value(r1) << width(r0)) + ...
In RIR’s JSON encoding every record is an object carrying a tag
field, and all fields must be written out (including null and
empty lists). For example, an H-broadcast instruction acting on a
two-bit integer register:
{
"tag": "Primitive",
"op": "h",
"operands": [{
"tag": "Ref",
"parts": [{"tag": "Span", "register": "address", "start": 0, "width": 2}],
"type": {"tag": "RegType", "kind": "uint", "width": 2}
}],
"angle": null,
"value": null
}
Examples¶
The examples below share a set of helper functions that build RIR nodes
(written the same way as in PySparQ/test/test_rir.py); they are plain
dicts and depend on no extra packages:
def ref(register, start, width, kind="uint"):
return {
"tag": "Ref",
"parts": [{"tag": "Span", "register": register, "start": start, "width": width}],
"type": {"tag": "RegType", "kind": kind, "width": width},
}
def prim(op, operands, angle=None, value=None):
return {"tag": "Primitive", "op": op, "operands": operands, "angle": angle, "value": value}
def register(name, width, kind="uint"):
return {"tag": "Register", "name": name, "type": {"tag": "RegType", "kind": kind, "width": width}}
def module(name, registers, body, locals_=None, resources=None):
return {
"tag": "Module", "name": name, "registers": registers,
"locals": locals_ or [], "resources": resources or [],
"body": body, "attributes": [],
}
def program(entry, modules):
return {"tag": "Program", "entry": entry, "modules": modules, "version": "0.3"}
def call(module_name, arguments, resources=()):
return {"tag": "Call", "module": module_name, "arguments": arguments, "resources": list(resources)}
Example 1: Gate-level program and coherent control (Bell state)¶
An h broadcast followed by a Control block: the interpreter maps
h onto Rot_Bool bit by bit, and turns the Control (a quantum
condition on the value 1) into a multi-bit coherent condition rather than
a classical branch.
import pysparq as ps
bell = program("main", [module(
"main",
[register("a", 1, "bits"), register("b", 1, "bits")],
[
prim("h", [ref("a", 0, 1, "bits")]),
{"tag": "Control", "register": ref("a", 0, 1, "bits"), "value": 1,
"body": [prim("x", [ref("b", 0, 1, "bits")])]},
],
)])
result = ps.run_rir(bell)
print(result.registers) # (('a', 1), ('b', 1))
print(result.amplitudes) # {(0, 0): (0.7071067811865476+0j), (1, 1): (0.7071067811865476+0j)}
The condition value of Control may be any unsigned bit pattern; bits
whose value is 0 are temporarily flipped with an X gate by the interpreter
and flipped back once the body has executed, staying coherent throughout.
Example 2: The module graph (Call / Repeat / Adjoint) and native arithmetic mapping¶
Define a module inc that adds 1 to an integer register; the entry
module calls it three times and then applies one adjoint. Call is
inlined automatically, Repeat is replayed automatically, and
Adjoint reverses automatically, turning add_const 1 into a modular
subtraction of 1:
inc = module("inc", [register("x", 4)], [prim("add_const", [ref("x", 0, 4)], value=1)])
main = module(
"main",
[register("x", 4)],
[
{"tag": "Repeat", "count": 3, "body": [call("inc", [ref("x", 0, 4)])]},
{"tag": "Adjoint", "body": [call("inc", [ref("x", 0, 4)])]},
],
)
result = ps.run_rir(program("main", [main, inc]))
print(result.amplitudes) # {(2,): (1+0j)} —— add 1 three times, then the adjoint subtracts 1
This is where the core benefit of the “natural interpreter” shows: the
operand of add_const fully covers register x, so the interpreter
applies a single native Add_ConstUInt_InPlace and skips the gate-level
decomposition; for the adjoint, the constant is negated modulo
(-1 mod 2^4 = 15), which is again a single native arithmetic.
Example 3: Register views and module arguments¶
An RIR Ref may refer to register slices. The formal registers of the
callee are bound to whatever view the caller passes in; the interpreter
does not assume that the arguments are contiguous physical qubits:
bump = module("bump", [register("lo", 2)], [prim("add_const", [ref("lo", 0, 2)], value=1)])
main = module(
"main",
[register("x", 4)],
[
prim("x", [ref("x", 2, 2)]), # set the upper two bits to 1 → x = 0b1100
call("bump", [ref("x", 0, 2)]), # pass only the lower two bits to the callee
],
)
result = ps.run_rir(program("main", [main, bump]))
print(result.amplitudes) # {(13,): (1+0j)} —— lower two bits 00 + 1 → x = 0b1101
When add_const acts on a slice, the interpreter falls back to
bit-by-bit decomposition, and the addition wraps around within the
view width. For example, adding 1 to the lower two bits of
x = 0b0011 wraps the view value 3 + 1 around to 0, yielding
x = 0:
wrap = module(
"main",
[register("x", 4)],
[
prim("x", [ref("x", 0, 1)]),
prim("x", [ref("x", 1, 1)]), # x = 0b0011
prim("add_const", [ref("x", 0, 2)], value=1),
],
)
result = ps.run_rir(program("main", [wrap]))
print(result.amplitudes) # {(0,): (1+0j)}
Example 4: QRAM loading¶
The entry module declares a QRAM resource (address width 2, data width 3)
and performs the load after a Hadamard broadcast over the address
register. The memory contents are not part of the program JSON; they are
bound at execution time. The interpreter does it all automatically: the
QRAM resource is materialized as a QRAMCircuit_qutrit, and Load
copies into a temporary register through reversible XOR, then performs the
native QRAMLoad query and fully uncomputes it:
values = {
"tag": "Resource", "name": "values",
"type": {"tag": "QRAM", "address_width": 2, "data_width": 3},
}
main = module(
"main",
[register("address", 2), register("data", 3)],
[
prim("h", [ref("address", 0, 2)]),
{"tag": "Load", "resource": "values",
"address": ref("address", 0, 2), "data": ref("data", 0, 3)},
],
resources=[values],
)
result = ps.run_rir(program("main", [main]), memory={"values": [1, 2, 4, 7]})
print(result.amplitudes)
# the amplitude of each branch ≈ 0.5 (up to floating-point tail differences):
# {(0, 1): (0.5+0j), (1, 2): (0.5+0j), (2, 4): (0.5+0j), (3, 7): (0.5+0j)}
Each of the four address branches picks up the corresponding word with
probability 1/2. memory also accepts sparse mappings; for example,
{"values": {2: 5}} writes only a single cell and leaves the others
zero.
When a dense state vector is needed, statevector() exports it
following RIR’s index convention (little-endian concatenation in
declaration order):
vector = result.statevector() # 32-dimensional: address(2 bits) + data(3 bits)
print(vector[(2 << 2) | 1]) # ≈ 0.5 —— address=1, data=2
print(vector[(7 << 2) | 3]) # ≈ 0.5 —— address=3, data=7
Execution Budgets and Safety Properties¶
The interpreter enforces two kinds of budgets; exceeding one raises an
RIRError instead of truncating silently:
max_steps(default1_000_000): the module-graph expansion budget. ARepeatis charged its body cost multiplied by the repeat count, and aCallis charged the callee’s cost; the budget is checked before taking over the global register table.max_states(default65_536): a budget on the number of sparse-state basis states, checked after every operator application.
There are also structural checks:
materialization is refused when a single QRAM’s
address_widthexceeds 20 (i.e. more than 2^20 cells);memorymust correspond one-to-one with the QRAM resources declared by the entry module, and both addresses and words are range-checked;a
Module.localsprivate workspace must be clean again when the module exits, otherwise “local register not uncomputed” is raised — this is the runtime obligation that the RIR specification delegates to the simulator;the global register table must be empty before the call (
run_rirperformsSystem.clear()automatically on exit).
Cross-validation with pyqecclang¶
On the pyqecclang side there are two independent execution paths:
run_pysparq: an event adapter that interprets the Python-level IR objects directly;run_pysparq_rir: serializes the program to JSON and hands it over topysparq.run_rir.
The interpreter in this repository (PySparQ/pysparq/rir.py)
deliberately consumes only JSON documents and does not import
pyqecclang, so the two paths together with the OriginIR-ext export
can serve as three-way cross-checking baselines for one another.
Regression tests live in PySparQ/test/test_rir.py.
API Reference¶
For detailed API documentation, see RIR Interpreter API Reference.
中文版 ===
RIR 解释执行¶
概述¶
RIR(Register-level Intermediate Representation)是量子编程语言
QECC.Lang( pyqecclang 包)的寄存器级中间表示。一个 RIR 程序是带版本的
JSON 文档:它声明带类型的命名寄存器( bits / uint / sint /
rational 与位宽)、QRAM 资源,以及一个模块图——模块体由门级基元与结构化
控制节点( Call / Repeat / Control / Adjoint )混合组成。
PySparQ 是 RIR 的**天然解释器**。这不是巧合:
RIR 本身就是**寄存器级**的:它谈论的是”寄存器”、”视图”、”整数字加法”和 “QRAM 加载”,而不是物理量子位与门序列;
PySparQ 的编程模型恰好是**寄存器级编程**(Register Level Programming): 命名类型化寄存器、原生寄存器算术算子与原生 QRAM 查询。
因此 RIR 的执行不需要额外的”降低”阶段: pysparq.run_rir 把 JSON 文档
直接解释在 SparseState 上,在解释期展开模块图,并在操作数与整个寄存器
对齐时把寄存器级操作映射为 PySparQ 原生算子。解释器只消费 JSON 文档,不依赖
pyqecclang 包本身,因此两套实现可以互相交叉验证。
概念对应¶
RIR 概念与 PySparQ 原生能力一一对应:
RIR 概念 |
PySparQ 原生对应 |
|---|---|
|
|
整个寄存器上的 |
|
非受控 |
|
受控 |
|
视图上的门广播( |
|
|
受控 |
QRAM 资源 + |
|
|
|
|
解释期内联:实参视图经寄存器重命名绑定到被调模块 |
|
解释期重放执行(序列化 JSON 不展开) |
|
反向遍历 + 逐操作取逆(角度取负、模减法、自逆门) |
|
|
自动处理流程¶
run_rir 的执行分为五个阶段,全部对用户自动完成:
RIR JSON 文档
│ 1. 载入与结构校验(版本、入口、模块名唯一性)
▼
资源装配:入口寄存器 → 原生命名寄存器;QRAM 资源 → qutrit 树物化
│ 2. 事件流展开(Call 内联 / Repeat 重放 / Adjoint 反向 / Control 累积)
▼
稀疏态执行:整寄存器对齐 → 原生算子;寄存器切片 → 逐位回退分解
│ 3. 结果读出:按入口寄存器的整数值聚合振幅
▼
RIRResult(registers + amplitudes,可选稠密 statevector)
几个关键点:
模块图在解释期展开。
Call通过寄存器重命名把实参视图绑定到被调模块 的形式寄存器上原地执行;Repeat在执行时重放其主体;Adjoint反向 遍历主体并对每条操作取逆;嵌套Control的条件按逻辑合取累积成多比特 条件。能用原生算子就不分解。例如
add_const的操作数完整覆盖一个寄存器 (起点 0 且宽度等于声明宽度)时,解释器直接调用Add_ConstUInt_InPlace,而不是把它分解成多门链;非受控全局相位走GlobalPhase,QRAM 加载走原生QRAMLoad。视图语义由回退路径保证。当操作数是寄存器切片时,解释器回退到逐位 分解,模加法在**视图宽度**内回绕,与 RIR 规范一致。
执行完自动清理。
run_rir在退出时保证System.clear();调用前 若全局寄存器表非空则直接报错。
基本用法¶
入口 API 由 pysparq.rir 提供,并已在顶层命名空间导出:
import pysparq as ps
result = ps.run_rir(document, memory) # 使用默认预算
result = ps.run_rir(document, memory, max_steps=500_000, max_states=4_096)
result = ps.run_rir_file("program.rir.json", memory={"rom": [1, 2, 4, 7]})
document = ps.load_rir("program.rir.json") # dict / JSON 字符串 / 文件路径
document 可以是已解码的 dict、JSON 字符串或文件路径; memory 按
入口模块声明的 QRAM 资源名逐一绑定内容,每个资源接受完整字序列或稀疏的
{地址: 字} 映射(未给出的单元为零)。
返回值是 RIRResult :
result.registers # (("a", 1), ("b", 1)) —— 入口寄存器 (名字, 位宽),按声明序
result.amplitudes # {(0, 0): 0.707..., (1, 1): 0.707...}
# 键是各寄存器整数值组成的元组(按声明序)
result.statevector() # 稠密小端状态向量,索引 = value(r0) + (value(r1) << width(r0)) + ...
RIR 的 JSON 编码里每条记录都是带 tag 字段的对象,且**所有字段都要写出**
(包括 null 与空列表)。例如一条作用于两位整数寄存器的 H 广播指令:
{
"tag": "Primitive",
"op": "h",
"operands": [{
"tag": "Ref",
"parts": [{"tag": "Span", "register": "address", "start": 0, "width": 2}],
"type": {"tag": "RegType", "kind": "uint", "width": 2}
}],
"angle": null,
"value": null
}
案例¶
下面的案例共用一组构造 RIR 节点的辅助函数(与 PySparQ/test/test_rir.py
中的写法一致),它们只是普通 dict,不依赖任何额外包:
def ref(register, start, width, kind="uint"):
return {
"tag": "Ref",
"parts": [{"tag": "Span", "register": register, "start": start, "width": width}],
"type": {"tag": "RegType", "kind": kind, "width": width},
}
def prim(op, operands, angle=None, value=None):
return {"tag": "Primitive", "op": op, "operands": operands, "angle": angle, "value": value}
def register(name, width, kind="uint"):
return {"tag": "Register", "name": name, "type": {"tag": "RegType", "kind": kind, "width": width}}
def module(name, registers, body, locals_=None, resources=None):
return {
"tag": "Module", "name": name, "registers": registers,
"locals": locals_ or [], "resources": resources or [],
"body": body, "attributes": [],
}
def program(entry, modules):
return {"tag": "Program", "entry": entry, "modules": modules, "version": "0.3"}
def call(module_name, arguments, resources=()):
return {"tag": "Call", "module": module_name, "arguments": arguments, "resources": list(resources)}
案例 1:门级程序与相干控制(Bell 态)¶
一条 h 广播加一段 Control :解释器把 h 逐位映射为 Rot_Bool ,
把 Control (值为 1 的量子条件)转化为多比特相干条件,而不是经典分支。
import pysparq as ps
bell = program("main", [module(
"main",
[register("a", 1, "bits"), register("b", 1, "bits")],
[
prim("h", [ref("a", 0, 1, "bits")]),
{"tag": "Control", "register": ref("a", 0, 1, "bits"), "value": 1,
"body": [prim("x", [ref("b", 0, 1, "bits")])]},
],
)])
result = ps.run_rir(bell)
print(result.registers) # (('a', 1), ('b', 1))
print(result.amplitudes) # {(0, 0): (0.7071067811865476+0j), (1, 1): (0.7071067811865476+0j)}
Control 的条件值可以是任意无符号位模式;值为 0 的位会被解释器用 X 门
临时翻转、执行完主体再翻回,全程保持相干。
案例 2:模块图(Call / Repeat / Adjoint)与原生算术映射¶
定义一个把整数字寄存器加 1 的模块 inc ,入口模块调用它三次再做一次伴随。
Call 被自动内联, Repeat 被自动重放, Adjoint 自动反向并把
add_const 1 变成模减 1:
inc = module("inc", [register("x", 4)], [prim("add_const", [ref("x", 0, 4)], value=1)])
main = module(
"main",
[register("x", 4)],
[
{"tag": "Repeat", "count": 3, "body": [call("inc", [ref("x", 0, 4)])]},
{"tag": "Adjoint", "body": [call("inc", [ref("x", 0, 4)])]},
],
)
result = ps.run_rir(program("main", [main, inc]))
print(result.amplitudes) # {(2,): (1+0j)} —— 3 次加 1 后再伴随减 1
这里体现了”天然解释器”的核心收益: add_const 的操作数完整覆盖寄存器
x ,解释器直接施加一条原生 Add_ConstUInt_InPlace ,跳过门级分解;
伴随时把常数取模取负( -1 mod 2^4 = 15 ),仍是一条原生算术。
案例 3:寄存器视图与模块传参¶
RIR 的 Ref 可以引用寄存器切片。被调模块的形式寄存器绑定到调用方给出的
任意视图上,解释器不假定实参是连续的物理量子位:
bump = module("bump", [register("lo", 2)], [prim("add_const", [ref("lo", 0, 2)], value=1)])
main = module(
"main",
[register("x", 4)],
[
prim("x", [ref("x", 2, 2)]), # 高两位置 1 → x = 0b1100
call("bump", [ref("x", 0, 2)]), # 只把低两位传给被调模块
],
)
result = ps.run_rir(program("main", [main, bump]))
print(result.amplitudes) # {(13,): (1+0j)} —— 低两位 00 + 1 → x = 0b1101
当 add_const 作用在切片上时,解释器回退到逐位分解,加法在**视图宽度**
内回绕。例如对 x = 0b0011 的低两位加 1,视图值 3 + 1 回绕为 0,得到
x = 0 :
wrap = module(
"main",
[register("x", 4)],
[
prim("x", [ref("x", 0, 1)]),
prim("x", [ref("x", 1, 1)]), # x = 0b0011
prim("add_const", [ref("x", 0, 2)], value=1),
],
)
result = ps.run_rir(program("main", [wrap]))
print(result.amplitudes) # {(0,): (1+0j)}
案例 4:QRAM 加载¶
入口模块声明 QRAM 资源(地址宽 2、数据宽 3),对地址寄存器做 Hadamard 广播
后加载。内存内容不属于程序 JSON,而是执行时绑定。解释器自动完成:QRAM 资源
物化为 QRAMCircuit_qutrit , Load 通过可逆 XOR 复制到临时寄存器后执行
原生 QRAMLoad 查询并完整反算:
values = {
"tag": "Resource", "name": "values",
"type": {"tag": "QRAM", "address_width": 2, "data_width": 3},
}
main = module(
"main",
[register("address", 2), register("data", 3)],
[
prim("h", [ref("address", 0, 2)]),
{"tag": "Load", "resource": "values",
"address": ref("address", 0, 2), "data": ref("data", 0, 3)},
],
resources=[values],
)
result = ps.run_rir(program("main", [main]), memory={"values": [1, 2, 4, 7]})
print(result.amplitudes)
# 各分支振幅 ≈ 0.5(浮点尾差略):
# {(0, 1): (0.5+0j), (1, 2): (0.5+0j), (2, 4): (0.5+0j), (3, 7): (0.5+0j)}
四个地址分支各以 1/2 概率取到对应的字。 memory 也接受稀疏映射,例如
{"values": {2: 5}} 只写一个单元,其余单元为零。
需要稠密状态向量时, statevector() 按 RIR 的索引约定(声明序小端拼接)
导出:
vector = result.statevector() # 32 维:address(2 位) + data(3 位)
print(vector[(2 << 2) | 1]) # ≈ 0.5 —— address=1, data=2
print(vector[(7 << 2) | 3]) # ≈ 0.5 —— address=3, data=7
执行预算与安全属性¶
解释器施加两类预算,超限抛出 RIRError 而不是静默截断:
max_steps(默认1_000_000):模块图展开预算。Repeat的代价按 次数乘主体代价计,Call按被调模块代价计;预算在接管全局寄存器表**之前** 检查。max_states(默认65_536):稀疏态基矢数量预算,每次施加算子后检查。
另有结构性检查:
单个 QRAM 的
address_width超过 20(即超过 2^20 个单元)时拒绝物化;memory必须与入口模块声明的 QRAM 资源一一对应,地址与字都做范围检查;Module.locals私有工作区在模块退出时必须复净,否则报 “local register not uncomputed”——这是 RIR 规范交给模拟器的运行期义务;调用前全局寄存器表必须为空(
run_rir退出时自动System.clear())。
与 pyqecclang 的交叉验证¶
pyqecclang 侧有两条独立的执行路径:
run_pysparq:事件适配器,直接解释 Python 层的 IR 对象;run_pysparq_rir:把程序序列化为 JSON 后交给pysparq.run_rir。
本仓库的解释器( PySparQ/pysparq/rir.py )刻意只消费 JSON 文档、不导入
pyqecclang ,因此两条路径加上 OriginIR-ext 导出可以三方互为对拍基准。
回归测试见 PySparQ/test/test_rir.py 。
API 参考¶
详细 API 文档请参考 RIR Interpreter API Reference。