Expand description
Typed, side-effect-free runtime classical expressions.
This module defines a strongly-typed expression AST for classical
computation that lives alongside quantum operations in a dynamic circuit.
Expressions read runtime ClassicalVar and ClassicalValue handles, but
they never measure qubits, write classical storage, or transfer control.
Control-flow statements (IfOp, WhileOp, ForOp, SwitchOp)
consume these expressions to make branching decisions.
§Type system
Every expression has a static ClassicalType:
| Type | Width | Description |
|---|---|---|
Bool | 1 | Logical boolean |
Bit | 1 | Single measured/assigned bit |
UInt(w) | w | Unsigned integer (non-zero width) |
BitVec(w) | w | Ordered bit-vector (non-zero width) |
Bit and Bool are intentionally distinct: control-flow conditions
require Bool, so a measured Bit must be explicitly converted via
ClassicalExpr::to_bool. BitVec interprets index 0 as the
least-significant bit; ClassicalExpr::to_uint provides a little-endian
conversion to UInt.
§Building expressions
Expressions are constructed from literals, runtime classical handles, and
typed builders on ClassicalExpr. ClassicalVar and ClassicalValue
handles can be used directly as expression sources; direct operator use is
equivalent to converting the handle with .expr() first.
§Leaf nodes
| Constructor | Produces | Description |
|---|---|---|
var(v) | type of v | Reads the current runtime value of a mutable ClassicalVar |
value(v) | type of v | Reads an immutable ClassicalValue produced by measurement |
bool_literal(b) | Bool | Compile-time boolean constant |
bit_literal(b) | Bit | Compile-time bit constant (false = 0, true = 1) |
uint_literal(w, v) | UInt(w) | Compile-time unsigned integer (width ≤ 128, value fits in width) |
bit_vec_literal(w, v) | BitVec(w) | Compile-time bit-vector, same constraints as uint_literal |
use cqlib_core::circuit::{ClassicalExpr, ClassicalType, ClassicalVar};
let flag = ClassicalVar::new(Default::default(), 0, ClassicalType::Bool);
let expr = flag.expr(); // Bool
let lit = ClassicalExpr::bool_literal(true); // Bool
let num = ClassicalExpr::uint_literal(8, 42).unwrap(); // UInt(8)§Unary operations
| Operation | Signature | Produces |
|---|---|---|
!expr | Bool → Bool, Bit → Bit | same type as input |
try_not(expr) | Bool → Bool, Bit → Bit | same type as input |
Negation on Bool is logical negation; on Bit it is bitwise inversion.
Use try_not when invalid input should return
CircuitError instead of
panicking through the operator trait.
let cond = !flag; // Bool§Binary operations
| Operation | Signature | Produces |
|---|---|---|
lhs & rhs / try_and(lhs, rhs) | Bool×Bool → Bool, Bit×Bit → Bit | same type |
lhs | rhs / try_or(lhs, rhs) | same | same type |
lhs ^ rhs / try_xor(lhs, rhs) | same | same type |
Both operands must have the same type. Bool and Bit cannot be mixed.
let a = ClassicalVar::new(cid, 0, ClassicalType::Bool);
let b = ClassicalVar::new(cid, 1, ClassicalType::Bool);
let both = a & b; // Bool§Comparison operations
| Constructor | Signature | Produces |
|---|---|---|
eq(lhs, rhs) | any type × same type | Bool |
ne(lhs, rhs) | any type × same type | Bool |
lt(lhs, rhs) | UInt(w)×UInt(w) | Bool |
le(lhs, rhs) | UInt(w)×UInt(w) | Bool |
gt(lhs, rhs) | UInt(w)×UInt(w) | Bool |
ge(lhs, rhs) | UInt(w)×UInt(w) | Bool |
Equality and inequality work on all types (including BitVec).
Ordered comparisons (lt, le, gt, ge) are restricted to UInt.
let x = ClassicalExpr::var(ClassicalVar::new(cid, 0, ClassicalType::uint(8).unwrap()));
let y = ClassicalExpr::var(ClassicalVar::new(cid, 1, ClassicalType::uint(8).unwrap()));
let is_less = ClassicalExpr::lt(x, y).unwrap(); // Bool§Type casts
| Constructor | Signature | Produces |
|---|---|---|
expr.to_bool() / bit_to_bool(expr) | Bit → Bool | Bool |
expr.to_uint() / bit_vec_to_uint(expr) | BitVec(w) → UInt(w) | UInt(w) |
Casts are explicit — there is no implicit promotion. to_uint
interprets bit index 0 as the least-significant bit.
let measured = ClassicalExpr::value(ClassicalValue::new(cid, 0, ClassicalType::Bit));
let condition = measured.to_bool().unwrap(); // Bool§Conditional selection
| Constructor | Signature | Produces |
|---|---|---|
select(cond, then, else) | Bool × T × T → T | same type as branches |
Both branches must have the same type. The condition must be Bool.
let cond = ClassicalExpr::bool_literal(true);
let a = ClassicalExpr::uint_literal(8, 10).unwrap();
let b = ClassicalExpr::uint_literal(8, 20).unwrap();
let chosen = ClassicalExpr::select(cond, a, b).unwrap(); // UInt(8)§Bit extraction and manipulation
| Constructor | Signature | Produces |
|---|---|---|
extract_bit(val, idx) | UInt(w)×u32 → Bit, BitVec(w)×u32 → Bit | Bit |
extract_bits(val, off, w) | UInt×u32×u32 → BitVec(w), BitVec×u32×u32 → BitVec(w) | BitVec(w) |
concat(parts) | [Bit or BitVec] → BitVec(total_width) | BitVec |
pack_bits(bits) | [Bit] → BitVec(n) | BitVec(n) |
Index 0 is the least-significant bit. concat places the first part in
the least-significant output bits. pack_bits places the first bit at
index 0.
let lsb = ClassicalExpr::extract_bit(bv.clone(), 0).unwrap(); // Bit — bit 0
let hi = ClassicalExpr::extract_bits(bv, 4, 4).unwrap(); // BitVec(4) — bits [4..8)
let a = ClassicalExpr::bit_literal(true);
let b = ClassicalExpr::bit_literal(false);
let packed = ClassicalExpr::pack_bits([a, b]).unwrap(); // BitVec(2), a at bit 0§Expression introspection
| Method | Returns |
|---|---|
ty() | ClassicalType — the static type of this expression |
kind() | &ClassicalExprKind — the AST node variant |
vars() | BTreeSet<ClassicalVar> — all mutable variables read |
values() | BTreeSet<ClassicalValue> — all immutable values read |
remap_classical_ids(var_map, val_map) | ClassicalExpr — clone with remapped handles |
simplified() | ClassicalExpr — structurally simplified copy |
is_bool_true() / is_bool_false() | bool — literal predicate |
is_bit_true() / is_bit_false() | bool — literal predicate |
§Simplification
simplify() performs bottom-up algebraic simplification without evaluating
runtime variable or value reads. It is idempotent: applying it twice
yields the same expression.
§Rules (23 total)
Double negation:
| Pattern | Result |
|---|---|
not(not(x)) | x |
Identity elements:
| Pattern | Result |
|---|---|
and(x, true) / and(true, x) | x |
or(x, false) / or(false, x) | x |
xor(x, false) / xor(false, x) | x |
Idempotence and self-inverse:
| Pattern | Result |
|---|---|
and(x, x) | x |
or(x, x) | x |
xor(x, x) | false |
Complement:
| Pattern | Result |
|---|---|
and(x, not(x)) | false |
or(x, not(x)) | true |
xor(x, not(x)) | true |
Comparison reflexivity:
| Pattern | Result |
|---|---|
eq(x, x) / le(x, x) / ge(x, x) | true |
ne(x, x) / lt(x, x) / gt(x, x) | false |
Select and cast folding:
| Pattern | Result |
|---|---|
select(true, a, b) | a |
select(false, a, b) | b |
bit_to_bool(bit_literal(v)) | bool_literal(v) |
bit_vec_to_uint(bit_vec_literal{w, v}) | uint_literal(w, v) |
use cqlib_core::circuit::ClassicalExpr;
// not(not(a)) → a
let a = ClassicalExpr::bool_literal(true);
let expr = ClassicalExpr::try_not(ClassicalExpr::try_not(a.clone()).unwrap()).unwrap();
assert_eq!(expr.simplified(), a);
// and(b, true) → b
let b = ClassicalExpr::bool_literal(false);
let expr = ClassicalExpr::try_and(b.clone(), ClassicalExpr::bool_literal(true)).unwrap();
assert_eq!(expr.simplified(), b);
// eq(x, x) → true
let x = ClassicalExpr::uint_literal(8, 42).unwrap();
let expr = ClassicalExpr::eq(x.clone(), x).unwrap();
assert_eq!(expr.simplified(), ClassicalExpr::bool_literal(true));§Non-goals
Simplification deliberately does not perform:
- Value-dependent constant folding —
and(x, false) → falsewould eliminate a runtime variable read and belongs to a higher-level optimization pass. - Commutation normalization —
and(a, b)is not reordered toand(b, a). - Bit-width-aware extract/concat inversion —
extract_bits(concat(a, b), ...)is not reduced.
§Integration with control flow
The typical flow from measurement to control:
measure q[0] → value: Bit
value.to_bool() → condition: Bool
if condition { ... }use cqlib_core::circuit::{Circuit, Qubit};
fn build() -> Result<Circuit, Box<dyn std::error::Error>> {
let mut circuit = Circuit::new(2);
let q0 = Qubit::new(0);
let q1 = Qubit::new(1);
let measured = circuit.measure(q0)?;
let condition = measured.expr().to_bool()?;
circuit.if_(condition, |body| {
body.x(q1)?;
Ok(())
})?;
Ok(circuit)
}For loop-carried state, allocate a ClassicalVar and store into it:
use cqlib_core::circuit::{Circuit, ClassicalExpr, ClassicalType, Qubit};
fn build() -> Result<Circuit, Box<dyn std::error::Error>> {
let mut circuit = Circuit::new(1);
let q0 = Qubit::new(0);
let running = circuit.var(ClassicalType::Bool);
circuit.store(running, ClassicalExpr::bool_literal(true))?;
circuit.while_(running.expr(), |body| {
let measured = body.measure(q0)?;
body.store(running, measured.expr().to_bool()?)?;
Ok(())
})?;
Ok(circuit)
}Re-exports§
pub use expr::ClassicalBinaryOp;pub use expr::ClassicalCast;pub use expr::ClassicalCompareOp;pub use expr::ClassicalExpr;pub use expr::ClassicalExprKind;pub use expr::ClassicalExprNode;pub use expr::ClassicalUnaryOp;pub use simplify::simplify;