qcdiff_core/lib.rs
1//! Semantic diff for OpenQASM 3 circuits.
2//!
3//! Parses two programs with Qiskit's [`oq3_semantics`], lowers each to a
4//! normalized stream of operations, and aligns the streams so that comment,
5//! whitespace, and declaration-order changes do not count as differences.
6//! This is the library behind the `qcdiff` CLI.
7//!
8//! ```
9//! use qcdiff_core::{diff, extract_source};
10//!
11//! let old = extract_source(
12//! "OPENQASM 3.0; include \"stdgates.inc\"; qubit q; h q;",
13//! Some("old.qasm"),
14//! )
15//! .unwrap();
16//! let new = extract_source(
17//! "OPENQASM 3.0; include \"stdgates.inc\"; qubit q; x q;",
18//! Some("new.qasm"),
19//! )
20//! .unwrap();
21//!
22//! let result = diff(&old, &new);
23//! assert!(result.has_changes());
24//! ```
25
26mod diff;
27mod stream;
28
29pub use diff::{diff, Change, DiffLine, DiffResult};
30pub use stream::{extract, Extracted, Op, Summary};
31
32use oq3_semantics::syntax_to_semantics::{parse_source_string, ParseResult};
33use oq3_source_file::SourceString;
34
35/// Why a source could not be turned into an operation stream.
36pub enum Error {
37 /// The version statement declares OpenQASM 2, which has different
38 /// grammar and semantics.
39 Qasm2,
40 /// Parse or semantic errors; print them with [`ParseErrors::print`].
41 Parse(ParseErrors),
42}
43
44impl std::fmt::Display for Error {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Error::Qasm2 => write!(f, "OpenQASM 2 is not supported"),
48 Error::Parse(_) => write!(f, "parse or semantic errors"),
49 }
50 }
51}
52
53impl std::fmt::Debug for Error {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{self}")
56 }
57}
58
59impl std::error::Error for Error {}
60
61/// Diagnostics from one source, kept so the caller decides when to print.
62/// Boxed: the parse result is large and lives on the error path only.
63pub struct ParseErrors(Box<ParseResult<SourceString>>);
64
65impl ParseErrors {
66 /// Pretty-print the parser's diagnostics.
67 pub fn print(&self) {
68 self.0.print_errors();
69 }
70}
71
72/// Parse OpenQASM 3 source and extract its normalized operation stream.
73/// `name` is used in diagnostics only.
74pub fn extract_source(source: &str, name: Option<&str>) -> Result<Extracted, Error> {
75 if is_qasm2(source) {
76 return Err(Error::Qasm2);
77 }
78 let search: Option<&[&str]> = None;
79 let parsed = parse_source_string(source, name, search);
80 if parsed.any_errors() {
81 return Err(Error::Parse(ParseErrors(Box::new(parsed))));
82 }
83 Ok(extract(parsed.program(), parsed.symbol_table()))
84}
85
86/// True when the version statement declares major version 2. Decided on the
87/// parsed-out version line rather than substring search over the whole file.
88pub fn is_qasm2(source: &str) -> bool {
89 for line in source.lines() {
90 let t = line.trim();
91 if t.is_empty() || t.starts_with("//") {
92 continue;
93 }
94 if let Some(rest) = t.strip_prefix("OPENQASM") {
95 let version = rest.trim_start().trim_end_matches(';').trim();
96 return version.starts_with('2');
97 }
98 // First effective statement is not a version declaration: OQ3 allows
99 // omitting it; treat as OQ3.
100 return false;
101 }
102 false
103}