Skip to main content

cqlib_core/compile/transform/
target_basis.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0.
6// You may obtain a copy of this license in the LICENSE.txt file in
7// the root directory of this source tree or at
8// http://www.apache.org/licenses/LICENSE-2.0.
9//
10// Any modifications or derivative works of this code must retain this
11// copyright notice, and modified files need to carry a notice indicating
12// that they have been altered from the originals.
13
14//! Deterministic target-basis lowering.
15//!
16//! This pass owns correctness-oriented translation to a physical instruction
17//! basis. It uses single-operation decomposition rules as a planning graph and
18//! then lowers each gate-like operation linearly. Optimizing rewrites such as
19//! cancellation, merge, and commutation remain in the knowledge rewriter.
20//!
21//! One parameter-aware specialization sits on top of the static rule graph:
22//! fixed-parameter `U` gates are re-synthesized numerically by the `euler_1q`
23//! module using continuous-axis or half-rotation candidates with degenerate
24//! angles simplified. The dynamic candidate is used only when its physical
25//! output cost is strictly below the static plan's; ties keep the static path.
26
27use crate::circuit::{
28    Circuit, ClassicalControlOp, Instruction, Operation, Parameter, ParameterValue, Qubit,
29    StandardGate, ValueClassicalControlOp, ValueControlBody, ValueInstruction, ValueOperation,
30    ValueSwitchCase,
31};
32use crate::compile::CompilerError;
33use crate::compile::knowledge::rule::{Rule, RuleItem};
34use crate::compile::knowledge::{
35    KnowledgeInstructionKey, MatchedReplacement, RuleId, RuleKind, RuleLibrary,
36};
37use crate::compile::transform::analysis::WorkflowCircuitAnalysis;
38use crate::compile::transform::decompose::unitary::euler_1q::{
39    Euler1qCandidate, synthesize_euler_1q_candidates,
40};
41use crate::compile::transform::decompose::unitary::synthesize_numeric_1q_unitary;
42use crate::compile::transform::lowering_support::{
43    LowerableOperation, LoweringTarget, OperationSequenceLowerer,
44};
45use crate::compile::transform::rebuild::{CircuitRebuildContext, ClassicalRemap};
46use crate::compile::transform::transformer::{PassApplicability, WorkflowPass};
47use crate::compile::transform::{
48    Canonicalizer, CircuitAnalysis, OptimizeOneQubitRuns, TransformOutcome, Transformer,
49};
50use smallvec::{SmallVec, smallvec};
51use std::collections::{HashMap, HashSet};
52use std::sync::Arc;
53
54const MAX_PLANNING_ROUNDS: usize = 128;
55
56/// Lowers a circuit to an explicit gate-like target instruction basis.
57#[derive(Debug, Clone)]
58pub struct TargetBasisLowerer {
59    target_basis: Arc<[Instruction]>,
60    plans: Arc<LoweringPlans>,
61}
62
63/// Canonical identity of a standard-gate target basis.
64///
65/// The signature is order- and duplicate-insensitive because target-basis
66/// lowering treats the configured instructions as a capability set. `GPhase`
67/// is omitted because it is implicit and has no effect on generated templates.
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub struct TargetBasisSignature {
70    gates: Vec<u8>,
71}
72
73/// Cost of lowering a fixed standard-gate operation sequence to a target basis.
74///
75/// Global-phase operations are intentionally excluded: they have no physical
76/// gate cost and do not occupy a qubit wire.
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
78pub struct TargetBasisCost {
79    pub two_qubit_ops: usize,
80    pub depth: usize,
81    pub total_ops: usize,
82    pub parameterized_ops: usize,
83}
84
85/// Reusable, exact target-basis cost evaluator.
86///
87/// The evaluator shares the same lowerer and plans used by
88/// [`TargetBasisLowerer`]. It therefore measures the concrete output of the
89/// active rule library rather than maintaining a separate heuristic
90/// approximation for synthesis choices.
91#[derive(Debug, Clone)]
92pub struct TargetBasisCostModel {
93    signature: TargetBasisSignature,
94    lowerer: Arc<TargetBasisLowerer>,
95}
96
97impl PartialEq for TargetBasisCostModel {
98    fn eq(&self, other: &Self) -> bool {
99        self.signature == other.signature
100    }
101}
102
103impl Eq for TargetBasisCostModel {}
104
105#[derive(Debug, Clone)]
106struct LoweringPlans {
107    physical_keys: HashSet<KnowledgeInstructionKey>,
108    plan_by_key: HashMap<KnowledgeInstructionKey, GatePlan>,
109    target_display: String,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113struct GatePlan {
114    rule_id: RuleId,
115    cost: PlanCost,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
119struct PlanCost {
120    rank: usize,
121    total_ops: usize,
122    two_qubit_ops: usize,
123    parameterized_ops: usize,
124    rule_id: usize,
125}
126
127struct CircuitLowerer<'a> {
128    source: &'a Circuit,
129    plans: &'a LoweringPlans,
130    library: &'a RuleLibrary,
131    rebuild: CircuitRebuildContext,
132    changed: bool,
133}
134
135impl TargetBasisLowerer {
136    /// Creates a target-basis lowerer from a non-empty gate-like basis.
137    pub fn new(target_basis: Vec<Instruction>) -> Result<Self, CompilerError> {
138        Self::from_shared_basis(target_basis.into())
139    }
140
141    /// Creates a lowerer while retaining a shared target-basis allocation.
142    pub(crate) fn from_shared_basis(
143        target_basis: Arc<[Instruction]>,
144    ) -> Result<Self, CompilerError> {
145        if target_basis.is_empty() {
146            return Err(CompilerError::InvalidInput(
147                "target-basis lowering requires a non-empty target basis".to_string(),
148            ));
149        }
150
151        let library = RuleLibrary::builtin_rules()
152            .map_err(|err| CompilerError::InvariantViolation(err.to_string()))?;
153        let plans = LoweringPlans::build(&target_basis, library)?;
154
155        Ok(Self {
156            target_basis,
157            plans: Arc::new(plans),
158        })
159    }
160
161    /// Returns the configured target instruction basis in insertion order.
162    pub fn target_basis(&self) -> &[Instruction] {
163        self.target_basis.as_ref()
164    }
165
166    /// Returns whether translating `circuit` can change or reject its
167    /// gate-like operations.
168    ///
169    /// This is a conservative scheduling check. It recursively inspects
170    /// structured control-flow bodies but does not replace terminal
171    /// target-basis validation. Explicit `GPhase` operations require
172    /// translation because lowering folds them into the enclosing phase.
173    pub fn requires_lowering(&self, circuit: &Circuit) -> bool {
174        operations_require_lowering(circuit.operations(), self.plans.as_ref())
175    }
176}
177
178impl WorkflowPass for TargetBasisLowerer {
179    fn applicability(&self, analysis: &WorkflowCircuitAnalysis) -> PassApplicability {
180        let requires_lowering = analysis.has_extended_gate_like_operations()
181            || analysis.standard_gates().any(|gate| {
182                gate == StandardGate::GPhase
183                    || !self
184                        .plans
185                        .is_physical(&KnowledgeInstructionKey::Standard(gate))
186            });
187        if requires_lowering {
188            PassApplicability::Run
189        } else {
190            PassApplicability::ProvenNoOp("circuit already satisfies the explicit target basis")
191        }
192    }
193}
194
195fn operations_require_lowering(operations: &[Operation], plans: &LoweringPlans) -> bool {
196    operations
197        .iter()
198        .any(|operation| instruction_requires_lowering(&operation.instruction, plans))
199}
200
201fn instruction_requires_lowering(instruction: &Instruction, plans: &LoweringPlans) -> bool {
202    match instruction {
203        Instruction::Standard(StandardGate::GPhase) => true,
204        Instruction::Standard(gate) => {
205            !plans.is_physical(&KnowledgeInstructionKey::Standard(*gate))
206        }
207        // Keep the check conservative for standalone lowerers that may accept
208        // multi-controlled target instructions. The workflow decomposes these
209        // before translation, and running the lowerer preserves its error and
210        // normalization behavior for every extended gate-like instruction.
211        Instruction::McGate(_) | Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => true,
212        Instruction::ClassicalControl(control) => control_flow_requires_lowering(control, plans),
213        Instruction::ClassicalData(_) | Instruction::Directive(_) | Instruction::Delay => false,
214    }
215}
216
217fn control_flow_requires_lowering(control: &ClassicalControlOp, plans: &LoweringPlans) -> bool {
218    control
219        .bodies()
220        .any(|body| operations_require_lowering(body.operations(), plans))
221}
222
223impl TargetBasisSignature {
224    /// Builds a canonical signature from standard target gates.
225    pub fn from_standard_gates(gates: &[StandardGate]) -> Self {
226        let mut gates = gates
227            .iter()
228            .filter(|gate| **gate != StandardGate::GPhase)
229            .map(|gate| *gate as u8)
230            .collect::<Vec<_>>();
231        gates.sort_unstable();
232        gates.dedup();
233        Self { gates }
234    }
235}
236
237impl TargetBasisCostModel {
238    /// Builds an exact cost model for a non-empty standard-gate target basis.
239    pub fn new(target_basis: Vec<Instruction>) -> Result<Self, CompilerError> {
240        let lowerer = Arc::new(TargetBasisLowerer::new(target_basis)?);
241        Self::from_lowerer(lowerer)
242    }
243
244    /// Builds an exact cost model from an existing target-basis lowerer.
245    pub fn from_lowerer(lowerer: Arc<TargetBasisLowerer>) -> Result<Self, CompilerError> {
246        let gates = lowerer
247            .target_basis()
248            .iter()
249            .map(|instruction| match instruction {
250                Instruction::Standard(gate) => Ok(*gate),
251                _ => Err(CompilerError::InvalidInput(format!(
252                    "target-basis cost model requires standard instructions, got {instruction:?}"
253                ))),
254            })
255            .collect::<Result<Vec<_>, _>>()?;
256        Ok(Self {
257            signature: TargetBasisSignature::from_standard_gates(&gates),
258            lowerer,
259        })
260    }
261
262    /// Returns the canonical target identity used by synthesis caches.
263    pub const fn signature(&self) -> &TargetBasisSignature {
264        &self.signature
265    }
266
267    /// Returns the configured target instruction basis in insertion order.
268    pub fn target_basis(&self) -> &[Instruction] {
269        self.lowerer.target_basis()
270    }
271
272    /// Lowers fixed standard-gate operations and returns the exact resulting
273    /// target-basis cost.
274    ///
275    /// The supplied operations must reference only `qubits`, carry fixed
276    /// finite parameters, and contain no classical control. These constraints
277    /// match the numeric two-qubit synthesis and resynthesis callers.
278    pub fn cost_of_fixed_operations(
279        &self,
280        qubits: Vec<Qubit>,
281        operations: Vec<ValueOperation>,
282    ) -> Result<TargetBasisCost, CompilerError> {
283        Self::count_lowered(&self.lower_fixed_operations(qubits, operations)?)
284    }
285
286    /// Two-qubit scoring includes one existing local cleanup pass. The 1Q
287    /// optimizer calls `cost_of_fixed_operations`, so scoring never recurses.
288    pub(crate) fn cost_after_local_cleanup(
289        &self,
290        qubits: Vec<Qubit>,
291        operations: Vec<ValueOperation>,
292    ) -> Result<TargetBasisCost, CompilerError> {
293        let lowered = self.lower_fixed_operations(qubits, operations)?;
294        Self::count_lowered(&self.cleanup_lowered(lowered)?)
295    }
296
297    fn lower_fixed_operations(
298        &self,
299        qubits: Vec<Qubit>,
300        operations: Vec<ValueOperation>,
301    ) -> Result<Circuit, CompilerError> {
302        let source = Circuit::from_operations(qubits, operations, None, None)
303            .map_err(CompilerError::Circuit)?;
304        let lowered = match self.lowerer.transform(&source, None)? {
305            TransformOutcome::Unchanged => source,
306            TransformOutcome::Changed(lowered) => lowered,
307        };
308        Ok(lowered)
309    }
310
311    fn cleanup_lowered(&self, mut circuit: Circuit) -> Result<Circuit, CompilerError> {
312        let canonicalizer = Canonicalizer::production();
313        let optimizer = OptimizeOneQubitRuns::basis_with_cost_model(Arc::new(self.clone()));
314        for pass in [&canonicalizer as &dyn Transformer, &optimizer] {
315            if let TransformOutcome::Changed(next) = pass.transform(&circuit, None)? {
316                circuit = next;
317            }
318        }
319        if self.lowerer.requires_lowering(&circuit)
320            && let TransformOutcome::Changed(next) = self.lowerer.transform(&circuit, None)?
321        {
322            circuit = next;
323        }
324        if let TransformOutcome::Changed(next) = canonicalizer.transform(&circuit, None)? {
325            circuit = next;
326        }
327        Ok(circuit)
328    }
329
330    fn count_lowered(lowered: &Circuit) -> Result<TargetBasisCost, CompilerError> {
331        let mut cost = TargetBasisCost::default();
332        let mut depths = HashMap::new();
333        for operation in lowered.operations() {
334            let Instruction::Standard(gate) = operation.instruction else {
335                return Err(CompilerError::InvariantViolation(
336                    "target-basis lowering emitted a non-standard operation while estimating cost"
337                        .to_string(),
338                ));
339            };
340            if gate == StandardGate::GPhase {
341                continue;
342            }
343            cost.total_ops += 1;
344            if !operation.params.is_empty() {
345                cost.parameterized_ops += 1;
346            }
347            if operation.qubits.len() == 2 {
348                cost.two_qubit_ops += 1;
349            }
350            if operation.qubits.is_empty() {
351                continue;
352            }
353            let next_depth = operation
354                .qubits
355                .iter()
356                .filter_map(|qubit| depths.get(qubit))
357                .max()
358                .copied()
359                .unwrap_or(0)
360                + 1;
361            for qubit in &operation.qubits {
362                depths.insert(*qubit, next_depth);
363            }
364            cost.depth = cost.depth.max(next_depth);
365        }
366        Ok(cost)
367    }
368}
369
370impl Transformer for TargetBasisLowerer {
371    fn name(&self) -> &'static str {
372        "target_basis_lowering"
373    }
374
375    fn transform(
376        &self,
377        circuit: &Circuit,
378        _analysis: Option<&CircuitAnalysis>,
379    ) -> Result<TransformOutcome, CompilerError> {
380        let library = RuleLibrary::builtin_rules()
381            .map_err(|err| CompilerError::InvariantViolation(err.to_string()))?;
382        CircuitLowerer::run(circuit, self.plans.as_ref(), library)
383    }
384}
385
386impl LoweringPlans {
387    fn build(target_basis: &[Instruction], library: &RuleLibrary) -> Result<Self, CompilerError> {
388        let mut physical_keys = HashSet::with_capacity(target_basis.len());
389        for instruction in target_basis {
390            let Some(key) = KnowledgeInstructionKey::from_instruction(instruction) else {
391                return Err(CompilerError::InvalidInput(format!(
392                    "unsupported target-basis instruction {instruction:?}"
393                )));
394            };
395            physical_keys.insert(key);
396        }
397
398        let target_display = target_basis
399            .iter()
400            .map(Instruction::to_string)
401            .collect::<Vec<_>>()
402            .join(", ");
403        let mut cost_by_key = physical_keys
404            .iter()
405            .cloned()
406            .map(|key| {
407                let cost = if key.is_implicit() {
408                    PlanCost::zero(key_rule_sort_value(&key))
409                } else {
410                    PlanCost {
411                        rank: 0,
412                        total_ops: 1,
413                        two_qubit_ops: key.num_qubits().is_some_and(|num| num == 2) as usize,
414                        parameterized_ops: key.num_params().is_some_and(|num_params| num_params > 0)
415                            as usize,
416                        rule_id: key_rule_sort_value(&key),
417                    }
418                };
419                (key, cost)
420            })
421            .collect::<HashMap<_, _>>();
422        cost_by_key.insert(
423            KnowledgeInstructionKey::Standard(StandardGate::GPhase),
424            PlanCost::zero(usize::MAX),
425        );
426
427        let candidates = collect_candidate_rules(library);
428        let mut plan_by_key = HashMap::new();
429
430        for _ in 0..MAX_PLANNING_ROUNDS {
431            let mut changed = false;
432            for &rule_id in &candidates {
433                let rule = library.get(rule_id).ok_or_else(|| {
434                    CompilerError::InvariantViolation(format!(
435                        "missing target-basis lowering rule id {}",
436                        rule_id.as_usize()
437                    ))
438                })?;
439                let source_item = &rule.operations[0];
440                let source_key =
441                    KnowledgeInstructionKey::from_instruction(&source_item.instruction)
442                        .ok_or_else(|| {
443                            CompilerError::InvariantViolation(format!(
444                                "unsupported source instruction in target-basis rule {:?}",
445                                source_item.instruction
446                            ))
447                        })?;
448                if physical_keys.contains(&source_key) || source_key.is_implicit() {
449                    continue;
450                }
451
452                let Some(candidate_cost) =
453                    rewrite_cost(&rule.target, &cost_by_key, rule_id.as_usize())
454                else {
455                    continue;
456                };
457                let current = cost_by_key.get(&source_key).copied();
458                if current.is_none_or(|current| candidate_cost < current) {
459                    cost_by_key.insert(source_key.clone(), candidate_cost);
460                    plan_by_key.insert(
461                        source_key,
462                        GatePlan {
463                            rule_id,
464                            cost: candidate_cost,
465                        },
466                    );
467                    changed = true;
468                }
469            }
470
471            if !changed {
472                return Ok(Self {
473                    physical_keys,
474                    plan_by_key,
475                    target_display,
476                });
477            }
478        }
479
480        Err(CompilerError::InvariantViolation(
481            "target-basis lowering planning did not converge".to_string(),
482        ))
483    }
484
485    fn is_physical(&self, key: &KnowledgeInstructionKey) -> bool {
486        self.physical_keys.contains(key)
487    }
488
489    fn plan_for(&self, key: &KnowledgeInstructionKey) -> Option<GatePlan> {
490        self.plan_by_key.get(key).copied()
491    }
492}
493
494impl PlanCost {
495    const fn zero(rule_id: usize) -> Self {
496        Self {
497            rank: 0,
498            total_ops: 0,
499            two_qubit_ops: 0,
500            parameterized_ops: 0,
501            rule_id,
502        }
503    }
504}
505
506/// Orders dynamic Euler candidates by physical output cost, with the family
507/// declaration order as the final deterministic tie-break.
508fn compare_dynamic_candidates(
509    left: &Euler1qCandidate,
510    right: &Euler1qCandidate,
511) -> std::cmp::Ordering {
512    let (left_total, left_parameterized) = left.physical_cost();
513    let (right_total, right_parameterized) = right.physical_cost();
514    (left_total, left_parameterized, left.family).cmp(&(
515        right_total,
516        right_parameterized,
517        right.family,
518    ))
519}
520
521/// Returns true only when the dynamic candidate's physical output is strictly
522/// cheaper than the static plan's. Family order never participates here, so a
523/// tie keeps the static path. `PlanCost.rank` is a planning-internal metric
524/// and is ignored.
525fn dynamic_strictly_beats_static(candidate: &Euler1qCandidate, static_cost: &PlanCost) -> bool {
526    let (total_ops, parameterized_ops) = candidate.physical_cost();
527    (
528        0usize, // dynamic single-qubit synthesis never emits two-qubit gates
529        total_ops,
530        parameterized_ops,
531    ) < (
532        static_cost.two_qubit_ops,
533        static_cost.total_ops,
534        static_cost.parameterized_ops,
535    )
536}
537
538impl<'a> CircuitLowerer<'a> {
539    fn run(
540        source: &'a Circuit,
541        plans: &'a LoweringPlans,
542        library: &'a RuleLibrary,
543    ) -> Result<TransformOutcome, CompilerError> {
544        let rebuild = CircuitRebuildContext::new(source);
545        let root_classical = rebuild.root_classical().clone();
546        let mut lowerer = Self {
547            source,
548            plans,
549            library,
550            rebuild,
551            changed: false,
552        };
553        let mut phase_delta = Parameter::from(0.0);
554        let mut operations = Vec::with_capacity(source.operations().len());
555        lowerer.lower_sequence(
556            source.operations(),
557            &root_classical,
558            LoweringTarget::top_level(&mut operations, &mut phase_delta),
559        )?;
560
561        let global_phase = &source.global_phase() + &phase_delta;
562        let circuit = lowerer
563            .rebuild
564            .finish(source.qubits(), operations, global_phase)?;
565
566        Ok(if lowerer.changed {
567            TransformOutcome::Changed(circuit)
568        } else {
569            TransformOutcome::Unchanged
570        })
571    }
572
573    fn lower_operation(
574        &mut self,
575        operation: &Operation,
576        classical_remap: &ClassicalRemap,
577        target: &mut LoweringTarget<'_>,
578    ) -> Result<(), CompilerError> {
579        match &operation.instruction {
580            Instruction::Standard(_) | Instruction::McGate(_) => {
581                let params =
582                    CircuitRebuildContext::resolve_source_params(self.source, &operation.params)?;
583                let op = LowerableOperation {
584                    instruction: operation.instruction.clone(),
585                    qubits: operation.qubits.clone(),
586                    params,
587                    label: operation.label.clone(),
588                };
589                self.lower_gate_like(op, target)
590            }
591            Instruction::ClassicalControl(control) => {
592                let instruction = self.lower_control_flow(control, classical_remap)?;
593                let qubits = instruction.used_qubits().into_iter().collect();
594                target.push(ValueOperation {
595                    instruction: ValueInstruction::ClassicalControl(instruction),
596                    qubits,
597                    params: SmallVec::new(),
598                    label: operation.label.clone(),
599                });
600                Ok(())
601            }
602            Instruction::UnitaryGate(_) | Instruction::CircuitGate(_) => {
603                Err(CompilerError::InvalidInput(format!(
604                    "cannot lower {} to target basis {{{}}}: definitions and unitaries must be decomposed before target-basis translation",
605                    operation.instruction, self.plans.target_display
606                )))
607            }
608            Instruction::ClassicalData(_) | Instruction::Directive(_) | Instruction::Delay => {
609                let operation = self.rebuild.remap_preserved_operation(
610                    self.source,
611                    operation,
612                    classical_remap,
613                )?;
614                target.push(operation);
615                Ok(())
616            }
617        }
618    }
619
620    fn lower_gate_like(
621        &mut self,
622        operation: LowerableOperation,
623        target: &mut LoweringTarget<'_>,
624    ) -> Result<(), CompilerError> {
625        let key =
626            KnowledgeInstructionKey::from_instruction(&operation.instruction).ok_or_else(|| {
627                CompilerError::InvariantViolation(format!(
628                    "missing target-basis key for gate-like instruction {}",
629                    operation.instruction
630                ))
631            })?;
632        if key.is_implicit() {
633            self.changed = true;
634            target.accumulate_phase(operation.gphase_param()?);
635            return Ok(());
636        }
637        if self.plans.is_physical(&key) {
638            self.emit_physical_gate(operation, target);
639            return Ok(());
640        }
641        if matches!(
642            operation.instruction,
643            Instruction::Standard(StandardGate::U)
644        ) && self.try_lower_fixed_u(&operation, &key, target)?
645        {
646            return Ok(());
647        }
648
649        let Some(plan) = self.plans.plan_for(&key) else {
650            return Err(CompilerError::InvalidInput(format!(
651                "cannot lower {} to target basis {{{}}}: no decomposition plan",
652                operation.instruction, self.plans.target_display
653            )));
654        };
655        let rule = self.library.get(plan.rule_id).ok_or_else(|| {
656            CompilerError::InvariantViolation(format!(
657                "missing target-basis lowering rule id {}",
658                plan.rule_id.as_usize()
659            ))
660        })?;
661        let replacements = instantiate_single_source_rule(rule, &operation)?;
662        self.changed = true;
663        for replacement in replacements {
664            self.lower_replacement(replacement, target)?;
665        }
666        Ok(())
667    }
668
669    fn lower_replacement(
670        &mut self,
671        replacement: MatchedReplacement,
672        target: &mut LoweringTarget<'_>,
673    ) -> Result<(), CompilerError> {
674        self.lower_gate_like(
675            LowerableOperation {
676                instruction: replacement.instruction,
677                qubits: replacement.qubits,
678                params: SmallVec::from_vec(replacement.params.into_vec()),
679                label: None,
680            },
681            target,
682        )
683    }
684
685    fn emit_physical_gate(
686        &mut self,
687        operation: LowerableOperation,
688        target: &mut LoweringTarget<'_>,
689    ) {
690        target.push(ValueOperation {
691            instruction: ValueInstruction::from_instruction(operation.instruction),
692            qubits: operation.qubits,
693            params: operation.params,
694            label: operation.label,
695        });
696    }
697
698    /// Lowers a fixed-parameter `U` through parameter-aware Euler synthesis
699    /// when the result strictly beats the static rule plan. Returns `Ok(false)`
700    /// for symbolic parameters, missing families, or when the static plan is
701    /// at least as good, leaving the operation on the static path.
702    fn try_lower_fixed_u(
703        &mut self,
704        operation: &LowerableOperation,
705        key: &KnowledgeInstructionKey,
706        target: &mut LoweringTarget<'_>,
707    ) -> Result<bool, CompilerError> {
708        let mut angles = [0.0; 3];
709        if operation.params.len() != 3 {
710            return Ok(false);
711        }
712        for (slot, param) in angles.iter_mut().zip(&operation.params) {
713            let ParameterValue::Fixed(value) = param else {
714                return Ok(false);
715            };
716            if !value.is_finite() {
717                return Ok(false);
718            }
719            *slot = *value;
720        }
721        let matrix = StandardGate::U
722            .matrix(&angles)
723            .map_err(CompilerError::Circuit)?;
724        let decomposition = synthesize_numeric_1q_unitary(&matrix)?;
725        let is_available = |gate: StandardGate| {
726            self.plans
727                .physical_keys
728                .contains(&KnowledgeInstructionKey::Standard(gate))
729        };
730        let best = synthesize_euler_1q_candidates(decomposition, &is_available)?
731            .into_iter()
732            .min_by(compare_dynamic_candidates);
733        let Some(best) = best else {
734            return Ok(false);
735        };
736        if let Some(plan) = self.plans.plan_for(key) {
737            debug_assert_eq!(
738                plan.cost.two_qubit_ops, 0,
739                "single-qubit U lowering plan must not contain two-qubit gates"
740            );
741            if !dynamic_strictly_beats_static(&best, &plan.cost) {
742                return Ok(false);
743            }
744        }
745
746        self.changed = true;
747        for gate in &best.gates {
748            debug_assert!(
749                is_available(gate.gate),
750                "euler synthesis emitted {} outside the target basis",
751                gate.gate
752            );
753            let params = match gate.param {
754                Some(value) => smallvec![ParameterValue::Fixed(value)],
755                None => SmallVec::new(),
756            };
757            target.push(ValueOperation {
758                instruction: ValueInstruction::from_instruction(Instruction::Standard(gate.gate)),
759                qubits: operation.qubits.clone(),
760                params,
761                // Decomposed gates never inherit the source label, matching the
762                // static rule path: labels act as optimization boundaries.
763                label: None,
764            });
765        }
766        target.accumulate_phase(Parameter::from(best.global_phase));
767        Ok(true)
768    }
769
770    fn lower_control_flow(
771        &mut self,
772        control: &ClassicalControlOp,
773        classical_remap: &ClassicalRemap,
774    ) -> Result<ValueClassicalControlOp, CompilerError> {
775        Ok(match control {
776            ClassicalControlOp::If(op) => {
777                let mut then_body = Vec::with_capacity(op.then_body().operations().len());
778                let mut then_phase = Parameter::from(0.0);
779                self.lower_sequence(
780                    op.then_body().operations(),
781                    classical_remap,
782                    LoweringTarget::control_flow_body(&mut then_body, &mut then_phase),
783                )?;
784                self.prepend_body_phase(&mut then_body, then_phase);
785
786                let else_body = op
787                    .else_body()
788                    .map(|body| {
789                        let mut lowered = Vec::with_capacity(body.operations().len());
790                        let mut phase = Parameter::from(0.0);
791                        self.lower_sequence(
792                            body.operations(),
793                            classical_remap,
794                            LoweringTarget::control_flow_body(&mut lowered, &mut phase),
795                        )?;
796                        self.prepend_body_phase(&mut lowered, phase);
797                        Ok::<_, CompilerError>(ValueControlBody::new(lowered))
798                    })
799                    .transpose()?;
800
801                ValueClassicalControlOp::If {
802                    condition: classical_remap.remap_expr(op.condition())?,
803                    then_body: ValueControlBody::new(then_body),
804                    else_body,
805                }
806            }
807            ClassicalControlOp::While(op) => {
808                let mut body = Vec::with_capacity(op.body().operations().len());
809                let mut phase = Parameter::from(0.0);
810                self.lower_sequence(
811                    op.body().operations(),
812                    classical_remap,
813                    LoweringTarget::control_flow_body(&mut body, &mut phase),
814                )?;
815                self.prepend_body_phase(&mut body, phase);
816                ValueClassicalControlOp::While {
817                    condition: classical_remap.remap_expr(op.condition())?,
818                    body: ValueControlBody::new(body),
819                }
820            }
821            ClassicalControlOp::For(op) => {
822                let mut body = Vec::with_capacity(op.body().operations().len());
823                let mut phase = Parameter::from(0.0);
824                self.lower_sequence(
825                    op.body().operations(),
826                    classical_remap,
827                    LoweringTarget::control_flow_body(&mut body, &mut phase),
828                )?;
829                self.prepend_body_phase(&mut body, phase);
830                ValueClassicalControlOp::For {
831                    var: classical_remap.remap_var(op.var())?,
832                    start: classical_remap.remap_expr(op.start())?,
833                    stop: classical_remap.remap_expr(op.stop())?,
834                    step: classical_remap.remap_expr(op.step())?,
835                    body: ValueControlBody::new(body),
836                }
837            }
838            ClassicalControlOp::Switch(op) => {
839                let cases = op
840                    .cases()
841                    .iter()
842                    .map(|case| {
843                        let mut lowered = Vec::with_capacity(case.body().operations().len());
844                        let mut phase = Parameter::from(0.0);
845                        self.lower_sequence(
846                            case.body().operations(),
847                            classical_remap,
848                            LoweringTarget::control_flow_body(&mut lowered, &mut phase),
849                        )?;
850                        self.prepend_body_phase(&mut lowered, phase);
851                        Ok::<_, CompilerError>(ValueSwitchCase::new(
852                            case.value(),
853                            ValueControlBody::new(lowered),
854                        ))
855                    })
856                    .collect::<Result<Vec<_>, _>>()?;
857                let default = op
858                    .default()
859                    .map(|body| {
860                        let mut lowered = Vec::with_capacity(body.operations().len());
861                        let mut phase = Parameter::from(0.0);
862                        self.lower_sequence(
863                            body.operations(),
864                            classical_remap,
865                            LoweringTarget::control_flow_body(&mut lowered, &mut phase),
866                        )?;
867                        self.prepend_body_phase(&mut lowered, phase);
868                        Ok::<_, CompilerError>(ValueControlBody::new(lowered))
869                    })
870                    .transpose()?;
871                ValueClassicalControlOp::Switch {
872                    target: classical_remap.remap_expr(op.target())?,
873                    cases,
874                    default,
875                }
876            }
877            ClassicalControlOp::Break => ValueClassicalControlOp::Break,
878            ClassicalControlOp::Continue => ValueClassicalControlOp::Continue,
879        })
880    }
881
882    fn prepend_body_phase(&mut self, body: &mut Vec<ValueOperation>, phase: Parameter) {
883        if phase.is_zero() {
884            return;
885        }
886        self.changed = true;
887        body.insert(
888            0,
889            ValueOperation {
890                instruction: ValueInstruction::from_instruction(Instruction::Standard(
891                    StandardGate::GPhase,
892                )),
893                qubits: SmallVec::new(),
894                params: smallvec![ParameterValue::from(phase)],
895                label: None,
896            },
897        );
898    }
899}
900
901impl OperationSequenceLowerer for CircuitLowerer<'_> {
902    fn lower_one_operation(
903        &mut self,
904        operation: &Operation,
905        classical_remap: &ClassicalRemap,
906        target: &mut LoweringTarget<'_>,
907    ) -> Result<(), CompilerError> {
908        self.lower_operation(operation, classical_remap, target)
909    }
910}
911
912fn collect_candidate_rules(library: &RuleLibrary) -> Vec<RuleId> {
913    [RuleKind::Decompose, RuleKind::HardwareNative]
914        .into_iter()
915        .flat_map(|kind| library.rules_by_kind(kind).iter().copied())
916        .filter(|rule_id| {
917            let Some(rule) = library.get(*rule_id) else {
918                return false;
919            };
920            rule.operations.len() == 1
921                && rule
922                    .conditions
923                    .as_ref()
924                    .is_none_or(|conditions| conditions.is_empty())
925                && source_params_are_directly_instantiable(&rule.operations[0])
926        })
927        .collect()
928}
929
930fn rewrite_cost(
931    target: &[crate::compile::knowledge::rule::RuleItem],
932    cost_by_key: &HashMap<KnowledgeInstructionKey, PlanCost>,
933    rule_id: usize,
934) -> Option<PlanCost> {
935    let mut rank = 0usize;
936    let mut total_ops = 0usize;
937    let mut two_qubit_ops = 0usize;
938    let mut parameterized_ops = 0usize;
939
940    for item in target {
941        let key = KnowledgeInstructionKey::from_instruction(&item.instruction)?;
942        if key.is_implicit() {
943            continue;
944        }
945        let cost = cost_by_key.get(&key).copied()?;
946        rank = rank.max(cost.rank);
947        total_ops = total_ops.saturating_add(cost.total_ops);
948        two_qubit_ops = two_qubit_ops.saturating_add(cost.two_qubit_ops);
949        parameterized_ops = parameterized_ops.saturating_add(cost.parameterized_ops);
950    }
951
952    Some(PlanCost {
953        rank: rank.saturating_add(1),
954        total_ops,
955        two_qubit_ops,
956        parameterized_ops,
957        rule_id,
958    })
959}
960
961fn source_params_are_directly_instantiable(source: &RuleItem) -> bool {
962    source
963        .params
964        .as_deref()
965        .unwrap_or(&[])
966        .iter()
967        .all(|param| match param {
968            ParameterValue::Fixed(_) => false,
969            ParameterValue::Param(parameter) => parameter.as_symbol().is_some(),
970        })
971}
972
973fn instantiate_single_source_rule(
974    rule: &Rule,
975    operation: &LowerableOperation,
976) -> Result<Vec<MatchedReplacement>, CompilerError> {
977    debug_assert_eq!(rule.operations.len(), 1);
978    let source = rule.operations.first().ok_or_else(|| {
979        CompilerError::InvariantViolation(format!(
980            "target-basis lowering rule {} has no source operation",
981            rule.name
982        ))
983    })?;
984
985    let mut qubit_bindings = HashMap::with_capacity(source.qubits.len());
986    for (&rule_qubit, &actual_qubit) in source.qubits.iter().zip(&operation.qubits) {
987        qubit_bindings.insert(rule_qubit, actual_qubit);
988    }
989
990    let source_params = source.params.as_deref().unwrap_or(&[]);
991    if source_params.len() != operation.params.len() {
992        return Err(CompilerError::InvariantViolation(format!(
993            "planned target-basis rule {} parameter arity does not match {}",
994            rule.name, operation.instruction
995        )));
996    }
997
998    let mut parameter_bindings: HashMap<String, Parameter> = HashMap::new();
999    for (pattern, actual) in source_params.iter().zip(&operation.params) {
1000        match pattern {
1001            ParameterValue::Fixed(expected) => {
1002                let actual = Parameter::from(actual);
1003                if !Parameter::from(*expected)
1004                    .provably_equal(&actual, crate::compile::PARAMETER_EQ_TOLERANCE)
1005                {
1006                    return Err(CompilerError::InvariantViolation(format!(
1007                        "planned target-basis rule {} fixed parameter does not match {}",
1008                        rule.name, operation.instruction
1009                    )));
1010                }
1011            }
1012            ParameterValue::Param(pattern) => {
1013                let Some(symbol) = pattern.as_symbol() else {
1014                    return Err(CompilerError::InvariantViolation(format!(
1015                        "target-basis rule {} uses a non-symbol source parameter",
1016                        rule.name
1017                    )));
1018                };
1019                let actual = Parameter::from(actual);
1020                if let Some(bound) = parameter_bindings.get(&symbol) {
1021                    if !bound.provably_equal(&actual, crate::compile::PARAMETER_EQ_TOLERANCE) {
1022                        return Err(CompilerError::InvariantViolation(format!(
1023                            "planned target-basis rule {} repeated parameter does not match {}",
1024                            rule.name, operation.instruction
1025                        )));
1026                    }
1027                } else {
1028                    parameter_bindings.insert(symbol, actual);
1029                }
1030            }
1031        }
1032    }
1033
1034    let mut replacements = Vec::with_capacity(rule.target.len());
1035    for item in &rule.target {
1036        let key =
1037            KnowledgeInstructionKey::from_instruction(&item.instruction).ok_or_else(|| {
1038                CompilerError::InvariantViolation(format!(
1039                    "unsupported target instruction in target-basis rule {:?}",
1040                    item.instruction
1041                ))
1042            })?;
1043        let qubits = item
1044            .qubits
1045            .iter()
1046            .map(|rule_qubit| {
1047                qubit_bindings.get(rule_qubit).copied().ok_or_else(|| {
1048                    CompilerError::InvariantViolation(format!(
1049                        "target-basis rule {} references unbound qubit {}",
1050                        rule.name, rule_qubit
1051                    ))
1052                })
1053            })
1054            .collect::<Result<SmallVec<[Qubit; 3]>, _>>()?;
1055        let params = item
1056            .params
1057            .as_deref()
1058            .unwrap_or(&[])
1059            .iter()
1060            .map(|param| instantiate_rule_param(param, &parameter_bindings, &rule.name))
1061            .collect::<Result<SmallVec<[ParameterValue; 3]>, _>>()?;
1062
1063        replacements.push(MatchedReplacement {
1064            instruction: item.instruction.clone(),
1065            qubits,
1066            params,
1067            key,
1068        });
1069    }
1070
1071    Ok(replacements)
1072}
1073
1074fn instantiate_rule_param(
1075    param: &ParameterValue,
1076    bindings: &HashMap<String, Parameter>,
1077    rule_name: &str,
1078) -> Result<ParameterValue, CompilerError> {
1079    let parameter = match param {
1080        ParameterValue::Fixed(value) => Parameter::from(*value),
1081        ParameterValue::Param(parameter) => {
1082            for symbol in parameter.get_symbols() {
1083                if !bindings.contains_key(&symbol) {
1084                    return Err(CompilerError::InvariantViolation(format!(
1085                        "target-basis rule {rule_name} references unbound parameter {symbol}"
1086                    )));
1087                }
1088            }
1089            parameter.substitute_many(bindings)
1090        }
1091    };
1092    Ok(ParameterValue::from(parameter))
1093}
1094
1095fn key_rule_sort_value(key: &KnowledgeInstructionKey) -> usize {
1096    match key {
1097        KnowledgeInstructionKey::Standard(gate) => *gate as usize,
1098        KnowledgeInstructionKey::McGate(gate) => 10_000 + gate.num_qubits(),
1099    }
1100}
1101
1102#[cfg(test)]
1103#[path = "./target_basis_test.rs"]
1104mod target_basis_test;