Skip to main content

cqlib_core/compile/knowledge/
rule.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2025-2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Runtime model for knowledge-base rewrite rules.
14//!
15//! This module describes what a rule is.  Rule equivalence checking lives in
16//! `rule_equivalence` because matrix construction, parameter sampling, and
17//! numerical comparison are validation strategies rather than rule structure.
18
19use crate::circuit::{Instruction, MCGate, Parameter, ParameterValue, StandardGate};
20use crate::compile::PARAMETER_EQ_TOLERANCE;
21use smallvec::SmallVec;
22use std::collections::{BTreeSet, HashSet};
23
24/// One gate-like item in a rule match or rewrite block.
25#[derive(Debug, Clone)]
26pub struct RuleItem {
27    /// Gate-like instruction matched or emitted by this item.
28    pub instruction: Instruction,
29    /// Rule-local qubit labels used by this item.
30    pub qubits: SmallVec<[u32; 3]>,
31    /// Optional symbolic or fixed parameters for this item.
32    pub params: Option<SmallVec<[ParameterValue; 1]>>,
33}
34
35impl RuleItem {
36    /// Builds a standard-gate rule item.
37    pub fn standard(gate: StandardGate, qubits: &[u32], params: Vec<ParameterValue>) -> Self {
38        Self {
39            instruction: Instruction::Standard(gate),
40            qubits: SmallVec::from_slice(qubits),
41            params: if params.is_empty() {
42                None
43            } else {
44                Some(SmallVec::from_vec(params))
45            },
46        }
47    }
48
49    /// Builds a multi-controlled-gate rule item.
50    pub fn mc_gate(gate: MCGate, qubits: &[u32], params: Vec<ParameterValue>) -> Self {
51        Self {
52            instruction: Instruction::McGate(Box::new(gate)),
53            qubits: SmallVec::from_slice(qubits),
54            params: if params.is_empty() {
55                None
56            } else {
57                Some(SmallVec::from_vec(params))
58            },
59        }
60    }
61
62    /// Collect free symbols referenced by this item's symbolic parameters.
63    pub fn symbols(&self) -> HashSet<String> {
64        let mut symbols = HashSet::new();
65        if let Some(params) = &self.params {
66            for param in params {
67                if let ParameterValue::Param(param) = param {
68                    symbols.extend(param.get_symbols());
69                }
70            }
71        }
72
73        symbols
74    }
75
76    /// Validate invariants that depend only on this item, not on its rule block.
77    pub fn validate(&self) -> Result<(), RuleValidationError> {
78        let (instruction, expected_qubits, expected_params) = match &self.instruction {
79            Instruction::Standard(gate) => (gate.to_string(), gate.num_qubits(), gate.num_params()),
80            Instruction::McGate(gate) => (
81                format!(
82                    "MC{}[{}]",
83                    gate.base_gate(),
84                    gate.num_qubits() - gate.base_gate().num_qubits()
85                ),
86                gate.num_qubits(),
87                gate.num_params(),
88            ),
89            other => {
90                return Err(RuleValidationError::UnsupportedInstruction {
91                    instruction: other.to_string(),
92                });
93            }
94        };
95
96        if self.qubits.len() != expected_qubits {
97            return Err(RuleValidationError::WrongQubitCount {
98                instruction: instruction.clone(),
99                expected: expected_qubits,
100                got: self.qubits.len(),
101            });
102        }
103
104        let got_params = self.params.as_ref().map_or(0, SmallVec::len);
105        if got_params != expected_params {
106            return Err(RuleValidationError::WrongParamCount {
107                instruction: instruction.clone(),
108                expected: expected_params,
109                got: got_params,
110            });
111        }
112
113        let mut seen = SmallVec::<[u32; 3]>::new();
114        for &qubit in &self.qubits {
115            if seen.contains(&qubit) {
116                return Err(RuleValidationError::DuplicateQubit {
117                    instruction: instruction.clone(),
118                    qubit,
119                });
120            }
121            seen.push(qubit);
122        }
123
124        Ok(())
125    }
126
127    /// Returns whether this item describes the same instruction, qubit labels,
128    /// and provably equivalent parameter expressions as another rule item.
129    pub fn equivalent_to(&self, other: &Self) -> bool {
130        let lhs_params = self.params.as_deref().unwrap_or(&[]);
131        let rhs_params = other.params.as_deref().unwrap_or(&[]);
132        self.instruction == other.instruction
133            && self.qubits == other.qubits
134            && lhs_params.len() == rhs_params.len()
135            && lhs_params.iter().zip(rhs_params).all(|(lhs, rhs)| {
136                let lhs: Parameter = lhs.into();
137                lhs.provably_equal(&rhs.into(), PARAMETER_EQ_TOLERANCE)
138            })
139    }
140}
141
142#[derive(Debug, Clone)]
143/// Parameter constraints required for a rewrite rule to apply.
144pub enum Condition {
145    /// Requires two parameter expressions to be equal.
146    Eq(Parameter, Parameter),
147    /// Requires `lhs == rhs mod modulus`.
148    EqMod(Parameter, Parameter, Parameter),
149}
150
151impl Condition {
152    /// Collect free symbols referenced by this condition.
153    pub fn symbols(&self) -> HashSet<String> {
154        let mut symbols = HashSet::new();
155        match self {
156            Condition::Eq(lhs, rhs) => {
157                symbols.extend(lhs.get_symbols());
158                symbols.extend(rhs.get_symbols());
159            }
160            Condition::EqMod(lhs, rhs, modulus) => {
161                symbols.extend(lhs.get_symbols());
162                symbols.extend(rhs.get_symbols());
163                symbols.extend(modulus.get_symbols());
164            }
165        }
166
167        symbols
168    }
169}
170
171#[derive(Debug, Clone)]
172/// Runtime rewrite rule: match operations plus optional conditions and target.
173pub struct Rule {
174    /// Stable human-readable rule name.
175    pub name: String,
176    /// Operations to match in the source circuit.
177    pub operations: SmallVec<[RuleItem; 4]>,
178    /// Parameter constraints that must hold before applying the rewrite.
179    pub conditions: Option<SmallVec<[Condition; 2]>>,
180    /// Operations that replace the matched block.
181    pub target: SmallVec<[RuleItem; 4]>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
185pub enum RuleValidationError {
186    /// A rule must match at least one source operation.
187    #[error("rule match block is empty")]
188    EmptyMatch,
189    /// The rule contains an instruction that the rule runtime cannot match.
190    #[error("unsupported instruction: {instruction}")]
191    UnsupportedInstruction { instruction: String },
192    /// A rule item uses the wrong number of qubits for its instruction.
193    #[error("wrong qubit count for {instruction}: expected {expected}, got {got}")]
194    WrongQubitCount {
195        instruction: String,
196        expected: usize,
197        got: usize,
198    },
199    /// A rule item uses the wrong number of parameters for its instruction.
200    #[error("wrong parameter count for {instruction}: expected {expected}, got {got}")]
201    WrongParamCount {
202        instruction: String,
203        expected: usize,
204        got: usize,
205    },
206    /// A single rule item repeats one rule-local qubit label.
207    #[error("duplicate qubit {qubit} in {instruction}")]
208    DuplicateQubit { instruction: String, qubit: u32 },
209    /// A rewrite target references a qubit label absent from the match block.
210    #[error("rewrite qubit {qubit} is not bound by the match block")]
211    UnboundRewriteQubit { qubit: u32 },
212    /// A rewrite parameter references a symbol absent from the match block.
213    #[error("rewrite symbol {symbol} is not bound by the match block")]
214    UnboundRewriteSymbol { symbol: String },
215    /// A condition references a symbol absent from the match block.
216    #[error("condition symbol {symbol} is not bound by the match block")]
217    UnboundConditionSymbol { symbol: String },
218    /// Rule-local qubit labels must form a dense range starting at zero.
219    #[error("qubit labels must be dense from 0, found {labels:?}")]
220    NonDenseQubitLabels { labels: Vec<u32> },
221}
222
223impl Rule {
224    /// Builds a rule with no conditions.
225    pub fn new(name: &str, ops: Vec<RuleItem>, target: Vec<RuleItem>) -> Rule {
226        Rule {
227            name: name.to_string(),
228            operations: SmallVec::from_vec(ops),
229            conditions: None,
230            target: SmallVec::from_vec(target),
231        }
232    }
233
234    /// Validate structural invariants required by the rule runtime.
235    ///
236    /// This checks whether a rule is well-formed enough to be dumped, verified,
237    /// and eventually matched. It does not prove that the rewrite preserves
238    /// quantum semantics; matrix equivalence checking lives in
239    /// `rule_equivalence`.
240    pub fn validate(&self) -> Result<(), RuleValidationError> {
241        if self.operations.is_empty() {
242            return Err(RuleValidationError::EmptyMatch);
243        }
244
245        // Item-level checks come first so later block checks can assume valid gate arity.
246        for item in &self.operations {
247            item.validate()?;
248        }
249        for item in &self.target {
250            item.validate()?;
251        }
252
253        let match_qubits = self.operation_qubits();
254        // Rule-local qubit labels are normalized to a dense 0..n range.
255        validate_dense_qubit_labels(&match_qubits)?;
256
257        // Rewrites may use a subset of matched qubits, but cannot introduce new ones.
258        for item in &self.target {
259            for &qubit in &item.qubits {
260                if !match_qubits.contains(&qubit) {
261                    return Err(RuleValidationError::UnboundRewriteQubit { qubit });
262                }
263            }
264        }
265
266        let bound_symbols = self.operation_symbols();
267        // Rewrite parameters must be expressions over symbols bound by the match block.
268        for symbol in self.target_symbols() {
269            if !bound_symbols.contains(&symbol) {
270                return Err(RuleValidationError::UnboundRewriteSymbol { symbol });
271            }
272        }
273
274        // Conditions constrain matched symbols; they cannot introduce new parameters.
275        for symbol in self.condition_symbols() {
276            if !bound_symbols.contains(&symbol) {
277                return Err(RuleValidationError::UnboundConditionSymbol { symbol });
278            }
279        }
280
281        Ok(())
282    }
283
284    /// Determine the number of qubits from the rule's operations and target.
285    pub fn num_qubits(&self) -> usize {
286        let mut op_qubits = self.operation_qubits();
287        op_qubits.extend(self.target_qubits());
288        // Minimum 1 for GPhase-only rules.
289        op_qubits.len().max(1)
290    }
291
292    /// Collect all free symbol names from parameters and conditions.
293    pub fn collect_free_symbols(&self) -> HashSet<String> {
294        let mut symbols = self.operation_symbols();
295        symbols.extend(self.target_symbols());
296        symbols.extend(self.condition_symbols());
297        symbols
298    }
299
300    /// Collect qubit labels referenced by the rewrite target.
301    pub fn target_qubits(&self) -> BTreeSet<u32> {
302        self.target
303            .iter()
304            .flat_map(|qs| qs.qubits.iter().cloned())
305            .collect()
306    }
307
308    /// Collect qubit labels referenced by the match block.
309    pub fn operation_qubits(&self) -> BTreeSet<u32> {
310        self.operations
311            .iter()
312            .flat_map(|qs| qs.qubits.iter().cloned())
313            .collect()
314    }
315
316    /// Collect symbols bound by the match block.
317    pub fn operation_symbols(&self) -> HashSet<String> {
318        self.operations.iter().flat_map(RuleItem::symbols).collect()
319    }
320
321    /// Collect symbols referenced by the rewrite target.
322    pub fn target_symbols(&self) -> HashSet<String> {
323        self.target.iter().flat_map(RuleItem::symbols).collect()
324    }
325
326    /// Collect symbols referenced by all rule conditions.
327    pub fn condition_symbols(&self) -> HashSet<String> {
328        self.conditions
329            .iter()
330            .flatten()
331            .flat_map(Condition::symbols)
332            .collect()
333    }
334}
335
336fn validate_dense_qubit_labels(labels: &BTreeSet<u32>) -> Result<(), RuleValidationError> {
337    for (expected, actual) in labels.iter().copied().enumerate() {
338        if expected as u32 != actual {
339            return Err(RuleValidationError::NonDenseQubitLabels {
340                labels: labels.iter().copied().collect(),
341            });
342        }
343    }
344    Ok(())
345}
346
347#[cfg(test)]
348#[path = "./rule_test.rs"]
349mod rule_test;