Skip to main content

cqlib_core/compile/knowledge/rule_dsl/
lower.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//! Lowering logic from AST ([`RuleDef`], [`GatePattern`]) to runtime rule structures.
14
15use crate::circuit::gate::StandardGate;
16use crate::circuit::{Instruction, MCGate, ParameterValue};
17use crate::compile::knowledge::rule::{Rule, RuleItem, RuleValidationError};
18use crate::compile::knowledge::rule_dsl::ast::{GatePattern, GateSpec, RuleDef};
19use smallvec::SmallVec;
20use std::collections::HashSet;
21
22/// Errors that can occur when lowering AST nodes to runtime [`Rule`] structures.
23#[derive(Debug, Clone, PartialEq, thiserror::Error)]
24pub enum LowerError {
25    /// A rule must contain at least one match operation.
26    #[error("rule match block is empty")]
27    EmptyMatch,
28    /// The gate name does not correspond to a known [`StandardGate`].
29    #[error("unknown gate: {0}")]
30    UnknownGate(String),
31    /// A parameter expression could not be parsed or evaluated.
32    #[error("invalid expression: {0}")]
33    InvalidExpr(String),
34    /// A multi-qubit pattern references the same qubit more than once.
35    #[error("duplicate qubit {qubit} in gate {gate}")]
36    DuplicateQubit {
37        /// Gate name used for the error message.
38        gate: String,
39        /// Duplicated qubit index.
40        qubit: u32,
41    },
42    /// A rewrite or require expression references a symbol not bound by match patterns.
43    #[error("unbound symbol {symbol} in {context}")]
44    UnboundSymbol {
45        /// Unbound symbol name.
46        symbol: String,
47        /// DSL context where the symbol was found.
48        context: &'static str,
49    },
50    /// A rewrite operation references a qubit label not bound by match patterns.
51    #[error("unbound qubit {qubit} in {context}")]
52    UnboundQubit {
53        /// Unbound rule-local qubit label.
54        qubit: u32,
55        /// DSL context where the qubit label was found.
56        context: &'static str,
57    },
58    /// The number of qubits does not match the gate definition.
59    #[error("wrong qubit count for {gate}: expected {expected}, got {got}")]
60    WrongQubitCount {
61        /// Gate name used for the error message.
62        gate: String,
63        /// Expected qubit count.
64        expected: usize,
65        /// Actual qubit count found in the pattern.
66        got: usize,
67    },
68    /// The number of parameters does not match the gate definition.
69    #[error("wrong parameter count for {gate}: expected {expected}, got {got}")]
70    WrongParamCount {
71        /// Gate name used for the error message.
72        gate: String,
73        /// Expected parameter count.
74        expected: usize,
75        /// Actual parameter count found in the pattern.
76        got: usize,
77    },
78    /// A lowered rule violates the runtime rule invariants.
79    #[error("invalid lowered rule: {0}")]
80    InvalidRule(RuleValidationError),
81}
82
83impl GatePattern {
84    /// Lowers this surface pattern into a runtime match [`RuleItem`].
85    ///
86    /// The lowering process:
87    /// 1. Resolves the pattern gate to a standard gate or an [`MCGate`].
88    /// 2. Validates that `qubits.len()` equals the gate's expected qubit count.
89    /// 3. Validates that `params.len()` equals the gate's expected parameter count.
90    /// 4. Attempts to evaluate each parameter to a constant; if that fails,
91    ///    the parameter is kept as a symbolic [`ParameterValue::Param`].
92    pub fn into_pattern_op(self) -> Result<RuleItem, LowerError> {
93        let lowered = lower_gate_pattern(self)?;
94
95        Ok(RuleItem {
96            instruction: lowered.instruction,
97            qubits: lowered.qubits,
98            params: if lowered.params.is_empty() {
99                None
100            } else {
101                Some(lowered.params)
102            },
103        })
104    }
105
106    /// Lowers this surface pattern into a runtime rewrite [`RuleItem`].
107    ///
108    /// The lowering process is identical to [`into_pattern_op`](Self::into_pattern_op)
109    /// except it is used for rewrite block diagnostics and validation.
110    pub fn into_build_op(self) -> Result<RuleItem, LowerError> {
111        let lowered = lower_gate_pattern(self)?;
112
113        Ok(RuleItem {
114            instruction: lowered.instruction,
115            qubits: lowered.qubits,
116            params: if lowered.params.is_empty() {
117                None
118            } else {
119                Some(lowered.params)
120            },
121        })
122    }
123}
124
125impl RuleDef {
126    /// Lowers this AST rule into the runtime [`Rule`] type.
127    ///
128    pub fn into_rule(self) -> Result<Rule, LowerError> {
129        if self.match_ops.is_empty() {
130            return Err(LowerError::EmptyMatch);
131        }
132
133        let mut bound_symbols = HashSet::new();
134        for op in &self.match_ops {
135            collect_bound_symbols(op, &mut bound_symbols);
136        }
137
138        let mut bound_qubits = HashSet::new();
139        for op in &self.match_ops {
140            collect_bound_qubits(op, &mut bound_qubits);
141        }
142
143        for op in &self.rewrite_ops {
144            validate_gate_symbols(op, &bound_symbols, "rewrite")?;
145            validate_gate_qubits(op, &bound_qubits, "rewrite")?;
146        }
147        for condition in &self.conditions {
148            validate_condition_symbols(condition, &bound_symbols)?;
149        }
150
151        let name = self.name;
152        let operations = self
153            .match_ops
154            .into_iter()
155            .map(GatePattern::into_pattern_op)
156            .collect::<Result<SmallVec<[_; 4]>, _>>()?;
157        let target = self
158            .rewrite_ops
159            .into_iter()
160            .map(GatePattern::into_build_op)
161            .collect::<Result<SmallVec<[_; 4]>, _>>()?;
162
163        let rule = Rule {
164            name,
165            operations,
166            conditions: if self.conditions.is_empty() {
167                None
168            } else {
169                Some(SmallVec::from_vec(self.conditions))
170            },
171            target,
172        };
173        rule.validate().map_err(LowerError::InvalidRule)?;
174        Ok(rule)
175    }
176}
177
178struct LoweredGate {
179    instruction: Instruction,
180    qubits: SmallVec<[u32; 3]>,
181    params: SmallVec<[ParameterValue; 1]>,
182}
183
184fn lower_gate_pattern(pattern: GatePattern) -> Result<LoweredGate, LowerError> {
185    let display_name = pattern.gate.display_name();
186    let instruction = lower_gate_spec(pattern.gate)?;
187    let (expected_qubits, expected_params) = instruction
188        .gate_arity()
189        .expect("rule DSL lowering emits only fixed-arity gates");
190
191    if pattern.qubits.len() != expected_qubits {
192        return Err(LowerError::WrongQubitCount {
193            gate: display_name,
194            expected: expected_qubits,
195            got: pattern.qubits.len(),
196        });
197    }
198    if pattern.params.len() != expected_params {
199        return Err(LowerError::WrongParamCount {
200            gate: display_name,
201            expected: expected_params,
202            got: pattern.params.len(),
203        });
204    }
205    validate_unique_qubits(&display_name, &pattern.qubits)?;
206
207    let params = pattern
208        .params
209        .into_iter()
210        .map(ParameterValue::from)
211        .collect();
212
213    Ok(LoweredGate {
214        instruction,
215        qubits: SmallVec::from_vec(pattern.qubits),
216        params,
217    })
218}
219
220fn lower_gate_spec(gate: GateSpec) -> Result<Instruction, LowerError> {
221    match gate {
222        GateSpec::Standard { gate_name } => Ok(Instruction::Standard(parse_gate_name(&gate_name)?)),
223        GateSpec::MultiControlled {
224            base_gate_name,
225            added_controls,
226        } => {
227            let base_gate = parse_gate_name(&base_gate_name)?;
228            Ok(Instruction::McGate(Box::new(MCGate::new(
229                added_controls,
230                base_gate,
231            ))))
232        }
233    }
234}
235
236fn validate_unique_qubits(gate: &str, qubits: &[u32]) -> Result<(), LowerError> {
237    let mut seen = SmallVec::<[u32; 3]>::new();
238    for &qubit in qubits {
239        if seen.contains(&qubit) {
240            return Err(LowerError::DuplicateQubit {
241                gate: gate.to_string(),
242                qubit,
243            });
244        }
245        seen.push(qubit);
246    }
247    Ok(())
248}
249
250fn collect_bound_symbols(pattern: &GatePattern, bound_symbols: &mut HashSet<String>) {
251    for param in &pattern.params {
252        for symbol in param.get_symbols() {
253            if !is_builtin_symbol(&symbol) {
254                bound_symbols.insert(symbol);
255            }
256        }
257    }
258}
259
260fn collect_bound_qubits(pattern: &GatePattern, bound_qubits: &mut HashSet<u32>) {
261    bound_qubits.extend(pattern.qubits.iter().copied());
262}
263
264fn validate_gate_symbols(
265    pattern: &GatePattern,
266    bound_symbols: &HashSet<String>,
267    context: &'static str,
268) -> Result<(), LowerError> {
269    for param in &pattern.params {
270        validate_param_symbols(param, bound_symbols, context)?;
271    }
272    Ok(())
273}
274
275fn validate_gate_qubits(
276    pattern: &GatePattern,
277    bound_qubits: &HashSet<u32>,
278    context: &'static str,
279) -> Result<(), LowerError> {
280    for &qubit in &pattern.qubits {
281        if !bound_qubits.contains(&qubit) {
282            return Err(LowerError::UnboundQubit { qubit, context });
283        }
284    }
285    Ok(())
286}
287
288fn validate_condition_symbols(
289    condition: &crate::compile::knowledge::rule::Condition,
290    bound_symbols: &HashSet<String>,
291) -> Result<(), LowerError> {
292    match condition {
293        crate::compile::knowledge::rule::Condition::Eq(lhs, rhs) => {
294            validate_param_symbols(lhs, bound_symbols, "require")?;
295            validate_param_symbols(rhs, bound_symbols, "require")
296        }
297        crate::compile::knowledge::rule::Condition::EqMod(lhs, rhs, modulus) => {
298            validate_param_symbols(lhs, bound_symbols, "require")?;
299            validate_param_symbols(rhs, bound_symbols, "require")?;
300            validate_param_symbols(modulus, bound_symbols, "require")
301        }
302    }
303}
304
305fn validate_param_symbols(
306    param: &crate::circuit::Parameter,
307    bound_symbols: &HashSet<String>,
308    context: &'static str,
309) -> Result<(), LowerError> {
310    for symbol in param.get_symbols() {
311        if !is_builtin_symbol(&symbol) && !bound_symbols.contains(&symbol) {
312            return Err(LowerError::UnboundSymbol { symbol, context });
313        }
314    }
315    Ok(())
316}
317
318fn is_builtin_symbol(symbol: &str) -> bool {
319    matches!(symbol, "π" | "pi" | "e")
320}
321
322/// Maps a gate name string to its [`StandardGate`] variant.
323fn parse_gate_name(name: &str) -> Result<StandardGate, LowerError> {
324    match name {
325        "I" => Ok(StandardGate::I),
326        "H" => Ok(StandardGate::H),
327        "RX" => Ok(StandardGate::RX),
328        "RXX" => Ok(StandardGate::RXX),
329        "RXY" => Ok(StandardGate::RXY),
330        "RY" => Ok(StandardGate::RY),
331        "RYY" => Ok(StandardGate::RYY),
332        "RZ" => Ok(StandardGate::RZ),
333        "RZX" => Ok(StandardGate::RZX),
334        "RZZ" => Ok(StandardGate::RZZ),
335        "S" => Ok(StandardGate::S),
336        "SDG" => Ok(StandardGate::SDG),
337        "SWAP" => Ok(StandardGate::SWAP),
338        "T" => Ok(StandardGate::T),
339        "TDG" => Ok(StandardGate::TDG),
340        "U" => Ok(StandardGate::U),
341        "X" => Ok(StandardGate::X),
342        "XY" => Ok(StandardGate::XY),
343        "X2P" => Ok(StandardGate::X2P),
344        "X2M" => Ok(StandardGate::X2M),
345        "XY2P" => Ok(StandardGate::XY2P),
346        "XY2M" => Ok(StandardGate::XY2M),
347        "Y" => Ok(StandardGate::Y),
348        "Y2P" => Ok(StandardGate::Y2P),
349        "Y2M" => Ok(StandardGate::Y2M),
350        "Z" => Ok(StandardGate::Z),
351        "Phase" => Ok(StandardGate::Phase),
352        "GPhase" => Ok(StandardGate::GPhase),
353        "CX" => Ok(StandardGate::CX),
354        "CCX" => Ok(StandardGate::CCX),
355        "CY" => Ok(StandardGate::CY),
356        "CZ" => Ok(StandardGate::CZ),
357        "CRX" => Ok(StandardGate::CRX),
358        "CRY" => Ok(StandardGate::CRY),
359        "CRZ" => Ok(StandardGate::CRZ),
360        "FSIM" => Ok(StandardGate::FSIM),
361        _ => Err(LowerError::UnknownGate(name.to_string())),
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::compile::knowledge::rule_dsl::parser::Parser;
369
370    fn lower_single_rule(source: &str) -> Result<Rule, LowerError> {
371        let mut parser = Parser::new(source).unwrap();
372        let mut rules = parser.parse_rule_file().unwrap();
373        assert_eq!(rules.len(), 1);
374        rules.remove(0).into_rule()
375    }
376
377    #[test]
378    fn reject_empty_match_block() {
379        let err = lower_single_rule(
380            r#"rule bad {
381                match {}
382                rewrite { H 0 }
383            }"#,
384        )
385        .unwrap_err();
386        assert!(matches!(err, LowerError::EmptyMatch));
387    }
388
389    #[test]
390    fn reject_duplicate_qubits() {
391        let err = lower_single_rule(
392            r#"rule bad {
393                match { CX 0 0 }
394                rewrite {}
395            }"#,
396        )
397        .unwrap_err();
398        assert!(matches!(
399            err,
400            LowerError::DuplicateQubit { gate, qubit } if gate == "CX" && qubit == 0
401        ));
402    }
403
404    #[test]
405    fn reject_unbound_rewrite_symbol() {
406        let err = lower_single_rule(
407            r#"rule bad {
408                match { RZ(a) 0 }
409                rewrite { RZ(c) 0 }
410            }"#,
411        )
412        .unwrap_err();
413        assert!(matches!(
414            err,
415            LowerError::UnboundSymbol { symbol, context } if symbol == "c" && context == "rewrite"
416        ));
417    }
418
419    #[test]
420    fn reject_unbound_rewrite_qubit() {
421        let err = lower_single_rule(
422            r#"rule bad {
423                match { H 0 }
424                rewrite { H 1 }
425            }"#,
426        )
427        .unwrap_err();
428        assert!(matches!(
429            err,
430            LowerError::UnboundQubit { qubit, context } if qubit == 1 && context == "rewrite"
431        ));
432    }
433
434    #[test]
435    fn allow_rewrite_to_reuse_match_qubit() {
436        let rule = lower_single_rule(
437            r#"rule ok {
438                match { H 0 }
439                rewrite { X 0 }
440            }"#,
441        )
442        .unwrap();
443        assert_eq!(rule.target[0].qubits.as_slice(), &[0]);
444    }
445
446    #[test]
447    fn allow_rewrite_to_reorder_bound_match_qubits() {
448        let rule = lower_single_rule(
449            r#"rule ok {
450                match { CX 0 1 }
451                rewrite { CX 1 0 }
452            }"#,
453        )
454        .unwrap();
455        assert_eq!(rule.target[0].qubits.as_slice(), &[1, 0]);
456    }
457
458    #[test]
459    fn lower_gphase_rule_ok() {
460        let rule = lower_single_rule(
461            r#"rule merge_gphase {
462                match { GPhase(a), GPhase(b) }
463                rewrite { GPhase(a + b) }
464            }"#,
465        )
466        .unwrap();
467        assert_eq!(rule.name, "merge_gphase");
468        assert_eq!(rule.operations.len(), 2);
469        assert!(rule.operations[0].qubits.is_empty());
470        assert_eq!(rule.target.len(), 1);
471        assert!(rule.target[0].qubits.is_empty());
472    }
473
474    #[test]
475    fn lower_multi_controlled_gate_rule_ok() {
476        let rule = lower_single_rule(
477            r#"rule decompose_m3cx {
478                match { MCX[3] 0 1 2 3 }
479                rewrite { CCX 0 1 2 }
480            }"#,
481        )
482        .unwrap();
483
484        let Instruction::McGate(gate) = &rule.operations[0].instruction else {
485            panic!("expected MCGate");
486        };
487        assert_eq!(gate.num_ctrl_qubits(), 3);
488        assert_eq!(*gate.base_gate(), StandardGate::X);
489        assert_eq!(rule.operations[0].qubits.as_slice(), &[0, 1, 2, 3]);
490    }
491
492    #[test]
493    fn lower_parameterized_multi_controlled_gate_rule_ok() {
494        let rule = lower_single_rule(
495            r#"rule decompose_m2rz {
496                match { MCRZ[2](theta) 0 1 2 }
497                rewrite { CRZ(theta) 1 2 }
498            }"#,
499        )
500        .unwrap();
501
502        let Instruction::McGate(gate) = &rule.operations[0].instruction else {
503            panic!("expected MCGate");
504        };
505        assert_eq!(gate.num_ctrl_qubits(), 2);
506        assert_eq!(*gate.base_gate(), StandardGate::RZ);
507        assert_eq!(rule.operations[0].params.as_ref().unwrap().len(), 1);
508    }
509
510    #[test]
511    fn reject_multi_controlled_gate_wrong_qubit_count() {
512        let err = lower_single_rule(
513            r#"rule bad {
514                match { MCX[3] 0 1 2 }
515                rewrite {}
516            }"#,
517        )
518        .unwrap_err();
519        assert!(matches!(
520            err,
521            LowerError::WrongQubitCount { gate, expected: 4, got: 3 } if gate == "MCX[3]"
522        ));
523    }
524
525    #[test]
526    fn reject_multi_controlled_gate_wrong_param_count() {
527        let err = lower_single_rule(
528            r#"rule bad {
529                match { MCRZ[2] 0 1 2 }
530                rewrite {}
531            }"#,
532        )
533        .unwrap_err();
534        assert!(matches!(
535            err,
536            LowerError::WrongParamCount { gate, expected: 1, got: 0 } if gate == "MCRZ[2]"
537        ));
538    }
539
540    #[test]
541    fn reject_multi_controlled_gate_unknown_base_gate() {
542        let err = lower_single_rule(
543            r#"rule bad {
544                match { MCNOPE[1] 0 1 }
545                rewrite {}
546            }"#,
547        )
548        .unwrap_err();
549        assert!(matches!(err, LowerError::UnknownGate(gate) if gate == "NOPE"));
550    }
551
552    #[test]
553    fn reject_gphase_with_qubit() {
554        let err = lower_single_rule(
555            r#"rule bad {
556                match { GPhase(a) 0 }
557                rewrite {}
558            }"#,
559        )
560        .unwrap_err();
561        assert!(matches!(
562            err,
563            LowerError::WrongQubitCount { gate, expected: 0, got: 1 } if gate == "GPhase"
564        ));
565    }
566
567    #[test]
568    fn reject_unbound_condition_symbol() {
569        let err = lower_single_rule(
570            r#"rule bad {
571                match { RZ(a) 0 }
572                require { c == 0 }
573                rewrite {}
574            }"#,
575        )
576        .unwrap_err();
577        assert!(matches!(
578            err,
579            LowerError::UnboundSymbol { symbol, context } if symbol == "c" && context == "require"
580        ));
581    }
582}