Skip to main content

cqlib_core/compile/knowledge/
matcher.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//! Shared structural matcher for compiler knowledge rules.
14//!
15//! This module owns the semantics of matching a knowledge [`RuleItem`] against a
16//! concrete gate-like operation: instruction identity, one-to-one qubit binding,
17//! symbolic parameter binding, condition evaluation, and rewrite target
18//! instantiation. It intentionally does not implement transform policy such as
19//! commutation-aware search, rewrite windows, target-basis filtering, local cost
20//! comparison, or patch selection.
21
22use crate::circuit::{Instruction, MCGate, Parameter, ParameterValue, Qubit, StandardGate};
23use crate::compile::PARAMETER_EQ_TOLERANCE;
24use crate::compile::knowledge::rule::{Condition, Rule, RuleItem};
25use smallvec::SmallVec;
26use std::collections::HashMap;
27
28/// Instruction subset supported by knowledge-rule structural matching.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub enum KnowledgeInstructionKey {
31    Standard(StandardGate),
32    McGate(MCGate),
33}
34
35impl KnowledgeInstructionKey {
36    /// Returns a matcher key for gate-like instructions supported by rules.
37    pub fn from_instruction(instruction: &Instruction) -> Option<Self> {
38        match instruction {
39            Instruction::Standard(gate) => Some(Self::Standard(*gate)),
40            Instruction::McGate(gate) => Some(Self::McGate(gate.as_ref().clone())),
41            _ => None,
42        }
43    }
44
45    /// Reconstructs the instruction represented by this key.
46    pub(crate) fn to_instruction(&self) -> Instruction {
47        match self {
48            Self::Standard(gate) => Instruction::Standard(*gate),
49            Self::McGate(gate) => Instruction::McGate(Box::new(gate.clone())),
50        }
51    }
52
53    pub(crate) fn is_implicit(&self) -> bool {
54        matches!(self, Self::Standard(StandardGate::GPhase))
55    }
56
57    pub(crate) fn num_qubits(&self) -> Option<usize> {
58        Some(match self {
59            Self::Standard(gate) => gate.num_qubits(),
60            Self::McGate(gate) => gate.num_qubits(),
61        })
62    }
63
64    pub(crate) fn num_params(&self) -> Option<usize> {
65        Some(match self {
66            Self::Standard(gate) => gate.num_params(),
67            Self::McGate(gate) => gate.num_params(),
68        })
69    }
70}
71
72/// Borrowed concrete operation input for rule matching.
73#[derive(Debug, Clone, Copy)]
74pub struct ConcreteOperationView<'a> {
75    pub instruction: &'a Instruction,
76    pub qubits: &'a [Qubit],
77    pub params: &'a [Parameter],
78}
79
80impl<'a> ConcreteOperationView<'a> {
81    pub const fn new(
82        instruction: &'a Instruction,
83        qubits: &'a [Qubit],
84        params: &'a [Parameter],
85    ) -> Self {
86        Self {
87            instruction,
88            qubits,
89            params,
90        }
91    }
92
93    pub fn key(&self) -> Option<KnowledgeInstructionKey> {
94        KnowledgeInstructionKey::from_instruction(self.instruction)
95    }
96}
97
98/// Mutable bindings produced while matching a rule instance.
99#[derive(Debug, Clone, Default)]
100pub struct MatchBindings {
101    qubits: HashMap<u32, Qubit>,
102    reverse_qubits: HashMap<Qubit, u32>,
103    params: HashMap<String, Parameter>,
104    insertion_log: Vec<BindingInsertion>,
105}
106
107#[derive(Debug, Clone)]
108enum BindingInsertion {
109    Qubit { rule: u32, actual: Qubit },
110    Parameter(String),
111}
112
113impl PartialEq for MatchBindings {
114    fn eq(&self, other: &Self) -> bool {
115        self.qubits == other.qubits
116            && self.reverse_qubits == other.reverse_qubits
117            && self.params == other.params
118    }
119}
120
121impl Eq for MatchBindings {}
122
123impl MatchBindings {
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Removes all bindings while retaining the allocated matching workspace.
129    pub(crate) fn clear(&mut self) {
130        self.qubits.clear();
131        self.reverse_qubits.clear();
132        self.params.clear();
133        self.insertion_log.clear();
134    }
135
136    /// Returns all rule-local qubit bindings.
137    pub fn qubits(&self) -> &HashMap<u32, Qubit> {
138        &self.qubits
139    }
140
141    /// Returns the concrete qubit bound to a rule-local qubit label.
142    pub fn qubit(&self, rule_qubit: u32) -> Option<Qubit> {
143        self.qubits.get(&rule_qubit).copied()
144    }
145
146    /// Returns all symbolic parameter bindings.
147    pub fn params(&self) -> &HashMap<String, Parameter> {
148        &self.params
149    }
150
151    /// Returns the concrete parameter bound to a rule symbol.
152    pub fn param(&self, symbol: &str) -> Option<&Parameter> {
153        self.params.get(symbol)
154    }
155
156    pub(crate) fn checkpoint(&self) -> usize {
157        self.insertion_log.len()
158    }
159
160    pub(crate) fn rollback(&mut self, checkpoint: usize) {
161        while self.insertion_log.len() > checkpoint {
162            match self
163                .insertion_log
164                .pop()
165                .expect("binding insertion log length was checked")
166            {
167                BindingInsertion::Qubit { rule, actual } => {
168                    self.qubits.remove(&rule);
169                    self.reverse_qubits.remove(&actual);
170                }
171                BindingInsertion::Parameter(symbol) => {
172                    self.params.remove(&symbol);
173                }
174            }
175        }
176    }
177
178    fn bind_qubits(&mut self, item: &RuleItem, concrete: ConcreteOperationView<'_>) -> bool {
179        for (&rule_qubit, &actual_qubit) in item.qubits.iter().zip(concrete.qubits) {
180            if let Some(bound) = self.qubits.get(&rule_qubit) {
181                if *bound != actual_qubit {
182                    return false;
183                }
184            } else if let Some(other_rule_qubit) = self.reverse_qubits.get(&actual_qubit) {
185                if *other_rule_qubit != rule_qubit {
186                    return false;
187                }
188            } else {
189                self.qubits.insert(rule_qubit, actual_qubit);
190                self.reverse_qubits.insert(actual_qubit, rule_qubit);
191                self.insertion_log.push(BindingInsertion::Qubit {
192                    rule: rule_qubit,
193                    actual: actual_qubit,
194                });
195            }
196        }
197        true
198    }
199
200    fn bind_parameters(&mut self, item: &RuleItem, concrete: ConcreteOperationView<'_>) -> bool {
201        let rule_params = item.params.as_deref().unwrap_or(&[]);
202        if rule_params.len() != concrete.params.len() {
203            return false;
204        }
205
206        rule_params
207            .iter()
208            .zip(concrete.params)
209            .all(|(rule_param, actual)| self.match_parameter(rule_param, actual))
210    }
211
212    fn match_parameter(&mut self, rule_param: &ParameterValue, actual: &Parameter) -> bool {
213        match rule_param {
214            ParameterValue::Fixed(value) => {
215                Parameter::from(*value).provably_equal(actual, PARAMETER_EQ_TOLERANCE)
216            }
217            ParameterValue::Param(pattern) => {
218                if let Some(symbol) = pattern.as_symbol() {
219                    if let Some(bound) = self.params.get(&symbol) {
220                        return bound.provably_equal(actual, PARAMETER_EQ_TOLERANCE);
221                    }
222                    self.params.insert(symbol.clone(), actual.clone());
223                    self.insertion_log.push(BindingInsertion::Parameter(symbol));
224                    return true;
225                }
226
227                let substituted = pattern.substitute_many(&self.params);
228                substituted.get_symbols().is_empty()
229                    && substituted.provably_equal(actual, PARAMETER_EQ_TOLERANCE)
230            }
231        }
232    }
233}
234
235/// One rewrite target item after applying match bindings.
236#[derive(Debug, Clone)]
237pub struct MatchedReplacement {
238    pub instruction: Instruction,
239    pub qubits: SmallVec<[Qubit; 3]>,
240    pub params: SmallVec<[ParameterValue; 3]>,
241    pub key: KnowledgeInstructionKey,
242}
243
244/// Errors produced by rule matching and target instantiation.
245#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
246pub enum MatchError {
247    #[error("unsupported rule instruction: {instruction}")]
248    UnsupportedRuleInstruction { instruction: String },
249    #[error("rewrite qubit {qubit} is not bound by the match")]
250    UnboundRewriteQubit { qubit: u32 },
251    #[error("rewrite symbol {symbol} is not bound by the match")]
252    UnboundRewriteSymbol { symbol: String },
253}
254
255/// Matches one rule item against a concrete operation.
256///
257/// The match is transactional: when this function returns `Ok(false)`, the
258/// supplied bindings are left unchanged.
259pub fn match_rule_item(
260    item: &RuleItem,
261    concrete: ConcreteOperationView<'_>,
262    bindings: &mut MatchBindings,
263) -> Result<bool, MatchError> {
264    let Some(item_key) = KnowledgeInstructionKey::from_instruction(&item.instruction) else {
265        return Err(MatchError::UnsupportedRuleInstruction {
266            instruction: item.instruction.to_string(),
267        });
268    };
269    let Some(concrete_key) = concrete.key() else {
270        return Ok(false);
271    };
272    match_rule_item_with_keys(item, &item_key, &concrete_key, concrete, bindings)
273}
274
275/// Internal matcher entry point for callers that already cache instruction keys.
276pub(crate) fn match_rule_item_with_keys(
277    item: &RuleItem,
278    item_key: &KnowledgeInstructionKey,
279    concrete_key: &KnowledgeInstructionKey,
280    concrete: ConcreteOperationView<'_>,
281    bindings: &mut MatchBindings,
282) -> Result<bool, MatchError> {
283    if item_key != concrete_key || item.qubits.len() != concrete.qubits.len() {
284        return Ok(false);
285    }
286
287    let checkpoint = bindings.checkpoint();
288    if !bindings.bind_qubits(item, concrete) {
289        bindings.rollback(checkpoint);
290        return Ok(false);
291    }
292    if !bindings.bind_parameters(item, concrete) {
293        bindings.rollback(checkpoint);
294        return Ok(false);
295    }
296
297    Ok(true)
298}
299
300/// Returns whether all rule conditions hold under current bindings.
301pub fn conditions_hold(conditions: Option<&[Condition]>, bindings: &MatchBindings) -> bool {
302    conditions.unwrap_or(&[]).iter().all(|condition| {
303        condition_symbols_bound(condition, bindings)
304            && match condition {
305                Condition::Eq(lhs, rhs) => {
306                    let lhs = lhs.substitute_many(&bindings.params);
307                    let rhs = rhs.substitute_many(&bindings.params);
308                    lhs.provably_equal(&rhs, PARAMETER_EQ_TOLERANCE)
309                }
310                Condition::EqMod(lhs, rhs, modulus) => {
311                    let lhs = lhs.substitute_many(&bindings.params);
312                    let rhs = rhs.substitute_many(&bindings.params);
313                    let modulus = modulus.substitute_many(&bindings.params);
314                    lhs.provably_equal_modulo(&rhs, &modulus, PARAMETER_EQ_TOLERANCE)
315                }
316            }
317    })
318}
319
320/// Instantiates a rewrite target using existing match bindings.
321pub fn instantiate_target(
322    target: &[RuleItem],
323    bindings: &MatchBindings,
324) -> Result<Vec<MatchedReplacement>, MatchError> {
325    let mut replacements = Vec::with_capacity(target.len());
326
327    for item in target {
328        let Some(key) = KnowledgeInstructionKey::from_instruction(&item.instruction) else {
329            return Err(MatchError::UnsupportedRuleInstruction {
330                instruction: item.instruction.to_string(),
331            });
332        };
333        let qubits = item
334            .qubits
335            .iter()
336            .map(|rule_qubit| {
337                bindings
338                    .qubit(*rule_qubit)
339                    .ok_or(MatchError::UnboundRewriteQubit { qubit: *rule_qubit })
340            })
341            .collect::<Result<SmallVec<[_; 3]>, _>>()?;
342        let params = item
343            .params
344            .as_deref()
345            .unwrap_or(&[])
346            .iter()
347            .map(|value| instantiate_parameter(value, bindings))
348            .collect::<Result<SmallVec<[_; 3]>, _>>()?;
349
350        replacements.push(MatchedReplacement {
351            instruction: item.instruction.clone(),
352            qubits,
353            params,
354            key,
355        });
356    }
357
358    Ok(replacements)
359}
360
361/// Matches a rule against an adjacent sequence of concrete operations.
362///
363/// This helper does not search across intervening operations. Callers that need
364/// commutation-aware or cost-aware matching should build that policy on top of
365/// `match_rule_item`.
366pub fn rule_matches_operations(
367    rule: &Rule,
368    operations: &[ConcreteOperationView<'_>],
369) -> Result<Option<MatchBindings>, MatchError> {
370    if rule.operations.len() != operations.len() {
371        return Ok(None);
372    }
373
374    let mut bindings = MatchBindings::new();
375    for (item, operation) in rule.operations.iter().zip(operations) {
376        if !match_rule_item(item, *operation, &mut bindings)? {
377            return Ok(None);
378        }
379    }
380
381    if !conditions_hold(rule.conditions.as_deref(), &bindings) {
382        return Ok(None);
383    }
384
385    Ok(Some(bindings))
386}
387
388fn condition_symbols_bound(condition: &Condition, bindings: &MatchBindings) -> bool {
389    match condition {
390        Condition::Eq(lhs, rhs) => symbols_bound(lhs, bindings) && symbols_bound(rhs, bindings),
391        Condition::EqMod(lhs, rhs, modulus) => {
392            symbols_bound(lhs, bindings)
393                && symbols_bound(rhs, bindings)
394                && symbols_bound(modulus, bindings)
395        }
396    }
397}
398
399fn symbols_bound(parameter: &Parameter, bindings: &MatchBindings) -> bool {
400    parameter
401        .get_symbols()
402        .iter()
403        .all(|symbol| bindings.params.contains_key(symbol))
404}
405
406fn instantiate_parameter(
407    value: &ParameterValue,
408    bindings: &MatchBindings,
409) -> Result<ParameterValue, MatchError> {
410    let parameter = match value {
411        ParameterValue::Fixed(value) => Parameter::from(*value),
412        ParameterValue::Param(parameter) => {
413            for symbol in parameter.get_symbols() {
414                if !bindings.params.contains_key(&symbol) {
415                    return Err(MatchError::UnboundRewriteSymbol { symbol });
416                }
417            }
418            parameter.substitute_many(&bindings.params)
419        }
420    };
421    Ok(ParameterValue::from(parameter))
422}
423
424#[cfg(test)]
425#[path = "./matcher_test.rs"]
426mod matcher_test;