Skip to main content

qcdiff_core/
stream.rs

1//! Extraction of a normalized, linear operation stream from an OpenQASM 3
2//! semantic ASG. The stream is what gets diffed: declarations and other
3//! non-executable statements contribute to summary counts only.
4
5use oq3_semantics::asg;
6use oq3_semantics::symbols::{SymbolId, SymbolIdResult, SymbolTable, SymbolType};
7use oq3_semantics::types::{IsConst, Type};
8use std::collections::hash_map::DefaultHasher;
9use std::hash::{Hash, Hasher};
10
11/// One executable operation in source order.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Op {
14    /// Operation class: "gate", "measure", "reset", "barrier", "ctrl-flow",
15    /// "gatedef", "delay", "assign", "expr".
16    pub kind: &'static str,
17    /// Gate or construct name ("h", "cx", "if", gate-def name, ...).
18    pub name: String,
19    /// Canonical operand rendering (qubits, and `-> target` for measure).
20    pub operands: String,
21    /// Canonical parameter rendering; empty when the op takes none. For
22    /// opaque constructs this is a content hash, so a body change shows up
23    /// as a parameter-level change (`~`) rather than insert/delete noise.
24    pub params: String,
25}
26
27impl Op {
28    /// Alignment key: everything except parameters. Two ops with equal keys
29    /// but different `params` render as a modification (`~`).
30    pub fn align_key(&self) -> String {
31        format!("{} {} {}", self.kind, self.name, self.operands)
32    }
33
34    pub fn render(&self) -> String {
35        let mut s = format!("{} {}", self.name, self.operands);
36        if !self.params.is_empty() {
37            s.push_str(&format!(" ({})", self.params));
38        }
39        s.trim_end().to_string()
40    }
41}
42
43/// Counts that describe a whole program, independent of the stream diff.
44#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct Summary {
46    pub qubits: usize,
47    pub bits: usize,
48    pub total_ops: usize,
49    pub two_qubit_gates: usize,
50    pub gate_defs: usize,
51}
52
53pub struct Extracted {
54    pub ops: Vec<Op>,
55    pub summary: Summary,
56}
57
58fn short_hash<T: Hash>(value: &T) -> String {
59    let mut h = DefaultHasher::new();
60    value.hash(&mut h);
61    format!("#{:016x}", h.finish())
62}
63
64// Debug output embeds SymbolId numbers, which renumber when an unrelated
65// declaration moves, so resolve them to symbol names before hashing.
66fn hash_debug<T: std::fmt::Debug>(value: &T, table: &SymbolTable) -> String {
67    let names = symbol_names(table);
68    let raw = format!("{value:?}");
69    let mut out = String::with_capacity(raw.len());
70    let mut rest = raw.as_str();
71    const TAG: &str = "SymbolId(";
72    while let Some(pos) = rest.find(TAG) {
73        out.push_str(&rest[..pos]);
74        rest = &rest[pos + TAG.len()..];
75        let parsed = rest.find(')').and_then(|close| {
76            rest[..close]
77                .parse::<usize>()
78                .ok()
79                .filter(|n| *n < names.len())
80                .map(|n| (close, n))
81        });
82        match parsed {
83            Some((close, n)) => {
84                out.push_str("sym:");
85                out.push_str(&names[n]);
86                rest = &rest[close + 1..];
87            }
88            // Not a table id (e.g. literal text in a source string): keep as is.
89            None => out.push_str(TAG),
90        }
91    }
92    out.push_str(rest);
93    short_hash(&out)
94}
95
96// The table exposes no length and indexing out of range panics; a probe
97// binding on a clone returns the next SymbolId, which is the count.
98fn symbol_names(table: &SymbolTable) -> Vec<String> {
99    let count = table
100        .clone()
101        .new_binding("__qcdiff_count_probe", &Type::Bool(IsConst::False))
102        .map(|id| symbol_id_value(&id))
103        .unwrap_or(0);
104    let mut id = SymbolId::new();
105    (0..count)
106        .map(|_| table[&id.post_increment()].name().to_string())
107        .collect()
108}
109
110fn symbol_id_value(id: &SymbolId) -> usize {
111    format!("{id:?}")
112        .trim_start_matches("SymbolId(")
113        .trim_end_matches(')')
114        .parse()
115        .unwrap_or(0)
116}
117
118fn sym_name(id: &SymbolIdResult, table: &SymbolTable) -> String {
119    match id {
120        Ok(sid) => table[sid].name().to_string(),
121        Err(_) => "<unresolved>".to_string(),
122    }
123}
124
125fn lvalue_str(lv: &asg::LValue, table: &SymbolTable) -> String {
126    match lv {
127        asg::LValue::Identifier(id) => sym_name(id, table),
128        asg::LValue::IndexedIdentifier(ii) => indexed_identifier_str(ii, table),
129    }
130}
131
132fn indexed_identifier_str(ii: &asg::IndexedIdentifier, table: &SymbolTable) -> String {
133    let mut s = sym_name(ii.identifier(), table);
134    for idx in ii.indexes() {
135        s.push_str(&format!("[{}]", hash_or_indexes(idx, table)));
136    }
137    s
138}
139
140fn hash_or_indexes(idx: &asg::IndexOperator, table: &SymbolTable) -> String {
141    match idx {
142        asg::IndexOperator::SetExpression(se) => exprs_str(se.expressions(), table),
143        asg::IndexOperator::ExpressionList(el) => exprs_str(&el.expressions, table),
144    }
145}
146
147fn exprs_str(exprs: &[asg::TExpr], table: &SymbolTable) -> String {
148    exprs
149        .iter()
150        .map(|e| expr_str(e, table))
151        .collect::<Vec<_>>()
152        .join(",")
153}
154
155/// Render each qubit expression and record it in the referenced-qubit set, so
156/// programs on physical qubits (`$0`), which declare nothing, still report a
157/// qubit count.
158fn exprs_str_tracked(
159    exprs: &[asg::TExpr],
160    table: &SymbolTable,
161    referenced: &mut std::collections::BTreeSet<String>,
162) -> String {
163    let rendered: Vec<String> = exprs.iter().map(|e| expr_str(e, table)).collect();
164    for q in &rendered {
165        referenced.insert(q.clone());
166    }
167    rendered.join(",")
168}
169
170/// Canonical, whitespace-free rendering of an expression. Falls back to a
171/// stable content hash for constructs v0 does not render; the hash keeps the
172/// diff deterministic without pretending to pretty-print them.
173fn expr_str(texpr: &asg::TExpr, table: &SymbolTable) -> String {
174    use asg::Expr;
175    match texpr.expression() {
176        Expr::Literal(lit) => literal_str(lit, table),
177        Expr::Identifier(id) => sym_name(id, table),
178        Expr::IndexedIdentifier(ii) => indexed_identifier_str(ii, table),
179        Expr::GateOperand(go) => gate_operand_str(go, table),
180        Expr::BinaryExpr(be) => format!(
181            "({}{}{})",
182            expr_str(be.left(), table),
183            binary_op_str(be.op()),
184            expr_str(be.right(), table)
185        ),
186        Expr::UnaryExpr(ue) => format!(
187            "({}{})",
188            unary_op_str(ue.op()),
189            expr_str(ue.operand(), table)
190        ),
191        Expr::MeasureExpression(me) => format!("measure {}", expr_str(me.operand(), table)),
192        // IndexExpression exposes no field accessors in oq3_semantics 0.7,
193        // so it participates as an opaque content hash.
194        Expr::IndexExpression(ie) => hash_debug(&ie, table),
195        Expr::HardwareQubit(hq) => format!("${}", hq.identifier()),
196        other => hash_debug(&other, table),
197    }
198}
199
200fn literal_str(lit: &asg::Literal, table: &SymbolTable) -> String {
201    use asg::Literal as L;
202    match lit {
203        L::Int(i) => format!("{}{}", if *i.sign() { "" } else { "-" }, i.value()),
204        L::Float(f) => f.value().to_string(),
205        L::Bool(b) => format!("{:?}", b.value()),
206        other => hash_debug(&other, table),
207    }
208}
209
210fn binary_op_str(op: &asg::BinaryOp) -> &'static str {
211    use asg::{ArithOp, BinaryOp, CmpOp};
212    match op {
213        BinaryOp::ArithOp(a) => match a {
214            ArithOp::Add => "+",
215            ArithOp::Sub => "-",
216            ArithOp::Mul => "*",
217            ArithOp::Div => "/",
218            ArithOp::Mod => "%",
219            _ => "ยท",
220        },
221        BinaryOp::CmpOp(c) => match c {
222            CmpOp::Eq => "==",
223            CmpOp::Neq => "!=",
224        },
225        BinaryOp::ConcatenationOp => "++",
226    }
227}
228
229fn unary_op_str(op: &asg::UnaryOp) -> &'static str {
230    use asg::UnaryOp;
231    match op {
232        UnaryOp::Minus => "-",
233        UnaryOp::Not => "!",
234        UnaryOp::BitNot => "~",
235    }
236}
237
238fn gate_operand_str(go: &asg::GateOperand, table: &SymbolTable) -> String {
239    match go {
240        asg::GateOperand::Identifier(id) => sym_name(id, table),
241        asg::GateOperand::IndexedIdentifier(ii) => indexed_identifier_str(ii, table),
242        asg::GateOperand::HardwareQubit(hq) => format!("${}", hq.identifier()),
243    }
244}
245
246fn modifier_prefix(mods: &[asg::GateModifier], table: &SymbolTable) -> String {
247    let mut s = String::new();
248    for m in mods {
249        match m {
250            asg::GateModifier::Inv => s.push_str("inv@"),
251            asg::GateModifier::Pow(p) => s.push_str(&format!("pow({})@", expr_str(p, table))),
252            asg::GateModifier::Ctrl(n) => match n {
253                Some(e) => s.push_str(&format!("ctrl({})@", expr_str(e, table))),
254                None => s.push_str("ctrl@"),
255            },
256            asg::GateModifier::NegCtrl(n) => match n {
257                Some(e) => s.push_str(&format!("negctrl({})@", expr_str(e, table))),
258                None => s.push_str("negctrl@"),
259            },
260        }
261    }
262    s
263}
264
265/// Number of qubits a declared quantum symbol contributes.
266fn qubit_width(ty: &Type) -> usize {
267    match ty {
268        Type::Qubit => 1,
269        Type::QubitArray(dims) => dims.dims().iter().product(),
270        _ => 0,
271    }
272}
273
274fn bit_width(ty: &Type) -> usize {
275    match ty {
276        Type::Bit(_) => 1,
277        Type::BitArray(dims, _) => dims.dims().iter().product(),
278        _ => 0,
279    }
280}
281
282pub fn extract(program: &asg::Program, table: &SymbolTable) -> Extracted {
283    let mut ops = Vec::new();
284    let mut summary = Summary::default();
285    let mut referenced = std::collections::BTreeSet::new();
286
287    for stmt in program.stmts() {
288        extract_stmt(stmt, table, &mut ops, &mut summary, &mut referenced);
289    }
290
291    // Physical-qubit programs (post-transpile `$n` operands) declare no
292    // qubits; the distinct referenced operands are the honest count then.
293    summary.qubits = summary.qubits.max(referenced.len());
294
295    summary.total_ops = ops.len();
296    summary.two_qubit_gates = ops
297        .iter()
298        .filter(|op| op.kind == "gate" && op.operands.split(',').count() >= 2)
299        .count();
300    Extracted { ops, summary }
301}
302
303fn extract_stmt(
304    stmt: &asg::Stmt,
305    table: &SymbolTable,
306    ops: &mut Vec<Op>,
307    summary: &mut Summary,
308    referenced: &mut std::collections::BTreeSet<String>,
309) {
310    use asg::Stmt;
311    match stmt {
312        Stmt::GateCall(gc) => {
313            let name = format!(
314                "{}{}",
315                modifier_prefix(gc.modifiers(), table),
316                sym_name(gc.name(), table)
317            );
318            ops.push(Op {
319                kind: "gate",
320                name,
321                operands: exprs_str_tracked(gc.qubits(), table, referenced),
322                params: gc.params().map(|p| exprs_str(p, table)).unwrap_or_default(),
323            });
324        }
325        Stmt::GPhaseCall(g) => ops.push(Op {
326            kind: "gate",
327            name: "gphase".into(),
328            operands: String::new(),
329            params: expr_str(g.arg(), table),
330        }),
331        Stmt::ModifiedGPhaseCall(g) => ops.push(Op {
332            kind: "gate",
333            name: format!("{}gphase", modifier_prefix(g.modifiers(), table)),
334            operands: String::new(),
335            params: expr_str(g.arg(), table),
336        }),
337        Stmt::Reset(r) => ops.push(Op {
338            kind: "reset",
339            name: "reset".into(),
340            operands: {
341                let q = expr_str(r.gate_operand(), table);
342                referenced.insert(q.clone());
343                q
344            },
345            params: String::new(),
346        }),
347        Stmt::Barrier(b) => ops.push(Op {
348            kind: "barrier",
349            name: "barrier".into(),
350            operands: b
351                .qubits()
352                .map(|q| exprs_str_tracked(q, table, referenced))
353                .unwrap_or_else(|| "(all)".into()),
354            params: String::new(),
355        }),
356        Stmt::Assignment(a) => {
357            // `c = measure q;` is the dominant executable assignment; render
358            // it as a measure op. Everything else is an opaque classical
359            // assignment whose value change shows as `~`.
360            if let asg::Expr::MeasureExpression(me) = a.rvalue().expression() {
361                ops.push(Op {
362                    kind: "measure",
363                    name: "measure".into(),
364                    operands: {
365                        let q = expr_str(me.operand(), table);
366                        referenced.insert(q.clone());
367                        format!("{} -> {}", q, lvalue_str(a.lvalue(), table))
368                    },
369                    params: String::new(),
370                });
371            } else {
372                ops.push(Op {
373                    kind: "assign",
374                    name: lvalue_str(a.lvalue(), table),
375                    operands: String::new(),
376                    params: expr_str(a.rvalue(), table),
377                });
378            }
379        }
380        Stmt::ExprStmt(texpr) => {
381            if let asg::Expr::MeasureExpression(me) = texpr.expression() {
382                ops.push(Op {
383                    kind: "measure",
384                    name: "measure".into(),
385                    operands: expr_str(me.operand(), table),
386                    params: String::new(),
387                });
388            } else {
389                ops.push(Op {
390                    kind: "expr",
391                    name: "expr".into(),
392                    operands: String::new(),
393                    params: expr_str(texpr, table),
394                });
395            }
396        }
397        // Control flow is opaque in v0: the condition (where cheap) is part
398        // of the alignment key, the body participates only as a content
399        // hash, so a body edit reports as one modified unit.
400        Stmt::If(i) => ops.push(Op {
401            kind: "ctrl-flow",
402            name: "if".into(),
403            operands: expr_str(i.condition(), table),
404            params: hash_debug(&(i.then_branch(), i.else_branch()), table),
405        }),
406        Stmt::While(w) => ops.push(Op {
407            kind: "ctrl-flow",
408            name: "while".into(),
409            operands: expr_str(w.condition(), table),
410            params: hash_debug(&w.loop_body(), table),
411        }),
412        Stmt::ForStmt(f) => ops.push(Op {
413            kind: "ctrl-flow",
414            name: "for".into(),
415            operands: sym_name(f.loop_var(), table),
416            params: hash_debug(&(f.iterable(), f.loop_body()), table),
417        }),
418        Stmt::SwitchCaseStmt(s) => ops.push(Op {
419            kind: "ctrl-flow",
420            name: "switch".into(),
421            operands: String::new(),
422            params: hash_debug(&s, table),
423        }),
424        Stmt::Block(b) => ops.push(Op {
425            kind: "ctrl-flow",
426            name: "block".into(),
427            operands: String::new(),
428            params: hash_debug(&b, table),
429        }),
430        Stmt::GateDefinition(gd) => {
431            summary.gate_defs += 1;
432            ops.push(Op {
433                kind: "gatedef",
434                name: sym_name(gd.name(), table),
435                operands: String::new(),
436                params: hash_debug(&gd, table),
437            });
438        }
439        Stmt::Delay(_) => ops.push(Op {
440            kind: "delay",
441            name: "delay".into(),
442            operands: String::new(),
443            params: hash_debug(&stmt, table),
444        }),
445        Stmt::DeclareQuantum(d) => {
446            if let Ok(sid) = d.name() {
447                summary.qubits += qubit_width(table[sid].symbol_type());
448            }
449        }
450        Stmt::DeclareClassical(d) => {
451            if let Ok(sid) = d.name() {
452                summary.bits += bit_width(table[sid].symbol_type());
453            }
454        }
455        Stmt::AnnotatedStmt(a) => extract_stmt(a.statement(), table, ops, summary, referenced),
456        // Declarations, includes, pragmas, io declarations: summary-only or
457        // ignored; they do not participate in the executable stream.
458        _ => {}
459    }
460}