Skip to main content

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}