Skip to main content

cqlib_core/compile/transform/rewrite/
rewriter.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//! Transformer entry point and circuit rebuild logic for knowledge rewrite.
14
15use crate::circuit::operation::ValueOperation;
16use crate::circuit::{
17    Circuit, CircuitParam, ClassicalControlOp, Instruction, Operation, Parameter, ParameterValue,
18    Qubit, StandardGate, ValueClassicalControlOp, ValueControlBody, ValueInstruction,
19    ValueSwitchCase,
20};
21use crate::compile::error::CompilerError;
22use crate::compile::knowledge::library::RuleLibrary;
23use crate::compile::knowledge::matcher::KnowledgeInstructionKey as RewriteInstructionKey;
24use crate::compile::transform::rewrite::basis::{TargetContext, validate_final_target};
25use crate::compile::transform::rewrite::config::RewriteConfig;
26use crate::compile::transform::rewrite::diagnostics::KnowledgeRewriteDiagnostics;
27use crate::compile::transform::rewrite::matcher::{
28    BlockMatchCache, CompiledRuleSet, PatchPlanStep, ReplacementItem, RewritePatch,
29    RunActiveRuleSet, is_gphase_instruction, patch_application_plan, resolve_operation_param,
30    select_rewrites_for_anchor_ranges,
31};
32
33use crate::compile::transform::lowering_support::LoweringTarget;
34use crate::compile::transform::rebuild::{CircuitRebuildContext, ClassicalRemap};
35use crate::compile::transform::{TransformOutcome, Transformer};
36use smallvec::SmallVec;
37use std::borrow::Cow;
38use std::collections::{HashMap, HashSet};
39use std::hash::Hash;
40use std::ops::Range;
41use std::sync::{Arc, OnceLock};
42
43static BUILTIN_COMPILED_RULES: OnceLock<Result<Arc<CompiledRuleSet>, String>> = OnceLock::new();
44/// Operation count (recursed into control-flow bodies) above which
45/// [`KnowledgeRewriter`] prefers the incremental engines over per-round full
46/// scans.
47///
48/// Strictly speaking the incremental engines are correct at any size, but
49/// below this threshold a full scan is cheap enough that workspace
50/// bookkeeping (match-cache construction, dirty-range tracking) costs more
51/// than it saves. This intentionally deviates from an "incremental by
52/// default" policy to protect small-circuit latency. The knob is independent
53/// of the workload thresholds in `matcher.rs`, which control rayon
54/// parallelism once a scan happens; adjust each only against its own
55/// measurements.
56pub(super) const SMALL_CIRCUIT_FULL_SCAN_THRESHOLD: usize = 4_096;
57
58/// Execution strategy for the rewrite fixpoint.
59///
60/// `Auto` is the production default and picks per circuit size; see
61/// [`SMALL_CIRCUIT_FULL_SCAN_THRESHOLD`]. The pinned variants exist only for
62/// differential tests and are not compiled into production builds.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum RewriteExecutionPolicy {
65    Auto,
66    #[cfg(test)]
67    FullScan,
68    #[cfg(test)]
69    Incremental,
70}
71
72/// Aggregate statistics produced by one knowledge rewrite run.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct KnowledgeRewriteStats {
75    /// Number of fixpoint rounds actually executed.
76    pub rounds_executed: u8,
77    /// Number of selected rule patches emitted into rebuilt sequences.
78    pub rules_applied: usize,
79    /// Number of operation sequences whose selected patch set was non-empty.
80    pub changed_sequences: usize,
81    /// Whether the run observed a stable round before hitting `max_rounds`.
82    pub reached_fixpoint: bool,
83}
84
85impl KnowledgeRewriteStats {
86    fn merge_round(&mut self, other: &RoundStats) {
87        self.rules_applied += other.rules_applied;
88        self.changed_sequences += other.changed_sequences;
89    }
90}
91
92/// Public result for running the rewriter directly.
93#[derive(Debug, Clone)]
94pub struct KnowledgeRewriteResult {
95    pub circuit: Circuit,
96    pub changed: bool,
97    pub stats: KnowledgeRewriteStats,
98    /// Incremental-workset and condition-evaluation counters for this run.
99    pub diagnostics: KnowledgeRewriteDiagnostics,
100}
101
102/// Workflow-only result which leaves a stable input circuit in place instead
103/// of cloning it solely to populate the standalone result shape.
104pub(crate) struct KnowledgeRewriteSessionResult {
105    pub(crate) circuit: Option<Circuit>,
106    pub(crate) changed: bool,
107    pub(crate) stats: KnowledgeRewriteStats,
108    pub(crate) diagnostics: KnowledgeRewriteDiagnostics,
109}
110
111struct LinearRewriteResult {
112    circuit: Option<Circuit>,
113    changed: bool,
114    stats: KnowledgeRewriteStats,
115    diagnostics: KnowledgeRewriteDiagnostics,
116}
117
118impl LinearRewriteResult {
119    fn into_public(self, source: &Circuit) -> KnowledgeRewriteResult {
120        KnowledgeRewriteResult {
121            circuit: self.circuit.unwrap_or_else(|| source.clone()),
122            changed: self.changed,
123            stats: self.stats,
124            diagnostics: self.diagnostics,
125        }
126    }
127
128    fn into_session(self) -> KnowledgeRewriteSessionResult {
129        KnowledgeRewriteSessionResult {
130            circuit: self.circuit,
131            changed: self.changed,
132            stats: self.stats,
133            diagnostics: self.diagnostics,
134        }
135    }
136}
137
138#[derive(Debug, Clone, Default)]
139struct RoundStats {
140    rules_applied: usize,
141    changed_sequences: usize,
142    representation_changes: usize,
143    diagnostics: KnowledgeRewriteDiagnostics,
144}
145
146impl RoundStats {
147    fn changed(&self) -> bool {
148        self.rules_applied > 0 || self.representation_changes > 0
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
153enum ScopeSegment {
154    IfThen(usize),
155    IfElse(usize),
156    WhileBody(usize),
157    ForBody(usize),
158    SwitchCase { control: usize, case: usize },
159    SwitchDefault(usize),
160}
161
162#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
163struct ScopeId(Vec<ScopeSegment>);
164
165impl ScopeId {
166    fn child(&self, segment: ScopeSegment) -> Self {
167        let mut path = self.0.clone();
168        path.push(segment);
169        Self(path)
170    }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
174struct BlockId {
175    scope: ScopeId,
176    slot: usize,
177}
178
179#[derive(Debug, Clone)]
180enum BlockWorkset {
181    Full,
182    Ranges(Vec<Range<usize>>),
183}
184
185#[derive(Debug, Clone, Default)]
186struct RewriteWorkset {
187    blocks: HashMap<BlockId, BlockWorkset>,
188    full_scopes: HashSet<ScopeId>,
189    caches: HashMap<BlockId, Arc<BlockMatchCache>>,
190}
191
192/// Cached matching state for the workflow's single flat rewrite block.
193///
194/// The owning [`KnowledgeRewriteSession`](super::session::KnowledgeRewriteSession)
195/// advances this cache through verified transform edits, so a later rewrite
196/// resolves and indexes only replacement operations.
197#[derive(Debug, Clone)]
198pub(crate) struct LinearRewriteWorkspace {
199    cache: BlockMatchCache,
200}
201
202impl LinearRewriteWorkspace {
203    pub(super) fn after_linear_replacements(
204        self,
205        after: &Circuit,
206        replacements: &[super::OperationReplacement],
207        qubits_renamed: bool,
208    ) -> Option<Self> {
209        Some(Self {
210            cache: self
211                .cache
212                .after_linear_replacements(after, replacements, qubits_renamed)?,
213        })
214    }
215}
216
217impl RewriteWorkset {
218    fn block_ranges(&self, block: &BlockId) -> Option<&[Range<usize>]> {
219        if self.full_scopes.contains(&block.scope) {
220            return None;
221        }
222        match self.blocks.get(block) {
223            Some(BlockWorkset::Full) => None,
224            Some(BlockWorkset::Ranges(ranges)) => Some(ranges),
225            None => Some(&[]),
226        }
227    }
228
229    fn mark_full_scope(&mut self, scope: ScopeId) {
230        self.blocks.retain(|block, _| block.scope != scope);
231        self.caches.retain(|block, _| block.scope != scope);
232        self.full_scopes.insert(scope);
233    }
234}
235
236/// Transformer that optimizes circuits using the compiler knowledge base.
237#[derive(Debug, Clone)]
238pub struct KnowledgeRewriter {
239    config: RewriteConfig,
240    execution_policy: RewriteExecutionPolicy,
241}
242
243impl KnowledgeRewriter {
244    /// Creates a knowledge rewriter with the supplied configuration.
245    pub fn new(config: RewriteConfig) -> Self {
246        Self {
247            config,
248            execution_policy: RewriteExecutionPolicy::Auto,
249        }
250    }
251
252    /// Creates a knowledge rewriter using conservative production defaults.
253    pub fn production() -> Self {
254        Self::new(RewriteConfig::production())
255    }
256
257    /// Creates a knowledge rewriter using explicit lowering defaults.
258    pub fn lowering() -> Self {
259        Self::new(RewriteConfig::lowering())
260    }
261
262    pub const fn config(&self) -> &RewriteConfig {
263        &self.config
264    }
265
266    /// Returns the maximum forward matcher reach for this configuration.
267    ///
268    /// Workflow-lifetime rewrite certificates store this exact value beside
269    /// their clean-anchor proof. Later mutation invalidation must use the
270    /// stored proof reach rather than deriving a potentially narrower value
271    /// from a subsequent configuration.
272    pub(crate) fn proof_reach(&self) -> Result<usize, CompilerError> {
273        Ok(builtin_compiled_rules()?.max_match_reach(&self.config))
274    }
275
276    /// Returns whether clean-anchor proofs produced by this rewriter survive
277    /// a bijective renaming of runtime qubits.
278    ///
279    /// The current rule language binds dense rule-local qubit labels and its
280    /// conditions observe parameters only. It cannot name a physical qubit or
281    /// inspect a device domain. Keep this capability explicit so a future
282    /// domain-aware rule model must opt out before routing edits can
283    /// advance its proofs.
284    pub(crate) fn proof_is_qubit_bijection_invariant(&self) -> Result<bool, CompilerError> {
285        Ok(builtin_compiled_rules()?.is_qubit_bijection_invariant())
286    }
287
288    #[cfg(test)]
289    pub(crate) fn force_full_scan(mut self) -> Self {
290        self.execution_policy = RewriteExecutionPolicy::FullScan;
291        self
292    }
293
294    #[cfg(test)]
295    pub(crate) fn force_incremental(mut self) -> Self {
296        self.execution_policy = RewriteExecutionPolicy::Incremental;
297        self
298    }
299
300    /// Runs knowledge-based local rewrite to a fixpoint or round limit.
301    ///
302    /// # Examples
303    ///
304    /// ```rust
305    /// use cqlib_core::circuit::{Circuit, Qubit};
306    /// use cqlib_core::compile::transform::KnowledgeRewriter;
307    ///
308    /// let mut circuit = Circuit::new(1);
309    /// circuit.x(Qubit::new(0)).unwrap();
310    /// circuit.x(Qubit::new(0)).unwrap();
311    ///
312    /// let result = KnowledgeRewriter::production().run(&circuit).unwrap();
313    /// assert!(result.stats.rounds_executed >= 1);
314    /// assert!(result.stats.reached_fixpoint);
315    /// let _rewritten = result.circuit;
316    /// ```
317    pub fn run(&self, circuit: &Circuit) -> Result<KnowledgeRewriteResult, CompilerError> {
318        self.run_internal(circuit, true)
319    }
320
321    fn run_internal(
322        &self,
323        circuit: &Circuit,
324        collect_diagnostics: bool,
325    ) -> Result<KnowledgeRewriteResult, CompilerError> {
326        if self.config.max_rounds() == 0 {
327            return Err(CompilerError::InvalidInput(
328                "rewrite max_rounds must be greater than zero".to_string(),
329            ));
330        }
331
332        let rules = builtin_compiled_rules()?;
333        let target_context = TargetContext::from_config(&self.config, rules.as_ref())?;
334        let incremental = match self.execution_policy {
335            RewriteExecutionPolicy::Auto => {
336                recursive_operation_count(circuit.operations()) >= SMALL_CIRCUIT_FULL_SCAN_THRESHOLD
337            }
338            #[cfg(test)]
339            RewriteExecutionPolicy::FullScan => false,
340            #[cfg(test)]
341            RewriteExecutionPolicy::Incremental => true,
342        };
343        if incremental && is_linear_workspace_eligible(circuit) {
344            return self
345                .run_linear_workspace(
346                    circuit,
347                    rules.as_ref(),
348                    target_context.as_ref(),
349                    None,
350                    None,
351                    collect_diagnostics,
352                )
353                .map(|(result, _)| result.into_public(circuit));
354        }
355        let active_rules = rules.active_rules(&self.config, target_context.as_ref());
356
357        let mut current = circuit.clone();
358        let mut aggregate = KnowledgeRewriteStats::default();
359        let mut diagnostics = KnowledgeRewriteDiagnostics::default();
360        let mut changed = false;
361        let mut workset: Option<RewriteWorkset> = None;
362
363        for round in 1..=self.config.max_rounds() {
364            aggregate.rounds_executed = round;
365            let (next, round_stats, next_workset) = RoundRewriter::run(
366                &current,
367                rules.as_ref(),
368                &active_rules,
369                &self.config,
370                target_context.as_ref(),
371                workset.as_ref(),
372                collect_diagnostics,
373            )?;
374            diagnostics.merge(round_stats.diagnostics);
375            if !round_stats.changed() {
376                aggregate.reached_fixpoint = true;
377                break;
378            }
379
380            changed = true;
381            aggregate.merge_round(&round_stats);
382            current = next;
383            workset = incremental.then_some(next_workset);
384        }
385
386        validate_final_target(&current, &self.config)?;
387
388        Ok(KnowledgeRewriteResult {
389            circuit: current,
390            changed,
391            stats: aggregate,
392            diagnostics,
393        })
394    }
395
396    /// Workflow entry point which always establishes a reusable flat
397    /// workspace when the circuit shape permits it, independent of the
398    /// standalone small-circuit policy.
399    pub(crate) fn run_for_session(
400        &self,
401        circuit: &Circuit,
402        reconciled: Option<(Vec<Range<usize>>, Option<LinearRewriteWorkspace>)>,
403        collect_diagnostics: bool,
404    ) -> Result<
405        (
406            KnowledgeRewriteSessionResult,
407            Option<LinearRewriteWorkspace>,
408        ),
409        CompilerError,
410    > {
411        if !is_linear_workspace_eligible(circuit) {
412            let fell_back_from_incremental = reconciled.is_some();
413            return self
414                .run_internal(circuit, collect_diagnostics)
415                .map(|mut result| {
416                    if collect_diagnostics && fell_back_from_incremental {
417                        result.diagnostics.full_scan_fallbacks =
418                            result.diagnostics.full_scan_fallbacks.saturating_add(1);
419                    }
420                    let circuit = result.changed.then_some(result.circuit);
421                    (
422                        KnowledgeRewriteSessionResult {
423                            circuit,
424                            changed: result.changed,
425                            stats: result.stats,
426                            diagnostics: result.diagnostics,
427                        },
428                        None,
429                    )
430                });
431        }
432        if self.config.max_rounds() == 0 {
433            return Err(CompilerError::InvalidInput(
434                "rewrite max_rounds must be greater than zero".to_string(),
435            ));
436        }
437        let rules = builtin_compiled_rules()?;
438        let target_context = TargetContext::from_config(&self.config, rules.as_ref())?;
439        let (ranges, workspace) = reconciled.map_or((None, None), |(ranges, workspace)| {
440            (Some(ranges), workspace)
441        });
442        self.run_linear_workspace(
443            circuit,
444            rules.as_ref(),
445            target_context.as_ref(),
446            ranges,
447            workspace,
448            collect_diagnostics,
449        )
450        .map(|(result, workspace)| (result.into_session(), Some(workspace)))
451    }
452
453    /// Runs the rewrite fixpoint on a flat, gate-only circuit without
454    /// rebuilding a [`Circuit`] per round.
455    ///
456    /// Eligible circuits (see [`is_linear_workspace_eligible`]) form a single
457    /// rewrite block, so the fixpoint runs on one persistent operation vector
458    /// plus a [`BlockMatchCache`]: each round scans only the dirty anchor
459    /// ranges produced by the previous round's patches, splices the selected
460    /// patches in place, and
461    /// folds global-phase replacements into a running phase delta. The final
462    /// [`Circuit`] is materialized exactly once, after the fixpoint.
463    ///
464    /// Statistics intentionally match the round-based engine
465    /// ([`RoundRewriter`]) field by field: one selected patch counts as one
466    /// applied rule, a patch-producing round counts as one changed sequence
467    /// (the whole circuit is a single block), and the final candidate-free
468    /// round still counts toward `rounds_executed`. The
469    /// FullScan/Incremental differential tests guard this equivalence.
470    fn run_linear_workspace(
471        &self,
472        circuit: &Circuit,
473        rules: &CompiledRuleSet,
474        target_context: Option<&TargetContext>,
475        initial_anchor_ranges: Option<Vec<Range<usize>>>,
476        workspace: Option<LinearRewriteWorkspace>,
477        collect_diagnostics: bool,
478    ) -> Result<(LinearRewriteResult, LinearRewriteWorkspace), CompilerError> {
479        let mut operations = Cow::Borrowed(circuit.operations());
480        let mut cache = match workspace {
481            Some(workspace) if workspace.cache.len() == operations.len() => workspace.cache,
482            _ => BlockMatchCache::new_with_diagnostics(circuit, &operations, collect_diagnostics)?,
483        };
484        let mut anchor_ranges = initial_anchor_ranges;
485        let mut aggregate = KnowledgeRewriteStats::default();
486        let diagnostics_before = cache.diagnostics();
487        let mut changed = false;
488        let mut phase_delta = Parameter::from(0.0);
489        let max_match_reach = rules.max_match_reach(&self.config);
490        let active_rules = rules.active_rules(&self.config, target_context);
491
492        for round in 1..=self.config.max_rounds() {
493            aggregate.rounds_executed = round;
494            let patches = select_rewrites_for_anchor_ranges(
495                operations.as_ref(),
496                &cache,
497                rules,
498                &active_rules,
499                &self.config,
500                target_context,
501                anchor_ranges.as_deref(),
502            )?;
503            if patches.is_empty() {
504                aggregate.reached_fixpoint = true;
505                break;
506            }
507
508            changed = true;
509            aggregate.rules_applied = aggregate.rules_applied.saturating_add(patches.len());
510            aggregate.changed_sequences = aggregate.changed_sequences.saturating_add(1);
511            let next_cache = cache
512                .into_rewritten(operations.as_ref(), &patches)
513                .ok_or_else(|| {
514                    CompilerError::InvariantViolation(
515                        "linear rewrite workspace could not materialize its selected patches"
516                            .to_string(),
517                    )
518                })?;
519            let (next_ranges, expected_len) =
520                dirty_ranges_after_patches(operations.len(), &patches, max_match_reach);
521            // The dirty ranges are clamped to this length; a mismatch with the
522            // rewritten block would silently drop dirty anchors, so it is a
523            // hard invariant, not a debug assertion.
524            if expected_len != next_cache.len() {
525                return Err(CompilerError::InvariantViolation(format!(
526                    "linear rewrite dirty-range length {expected_len} does not match rewritten block length {}",
527                    next_cache.len()
528                )));
529            }
530            let (next_operations, round_phase) =
531                apply_linear_patches(operations.as_ref(), &patches)?;
532            phase_delta = &phase_delta + &round_phase;
533            operations = Cow::Owned(next_operations);
534            cache = next_cache;
535            anchor_ranges = Some(next_ranges);
536        }
537
538        let output = if changed {
539            let value_operations = operations
540                .into_owned()
541                .into_iter()
542                .enumerate()
543                .map(|(position, operation)| ValueOperation {
544                    instruction: ValueInstruction::from_instruction(operation.instruction),
545                    qubits: operation.qubits,
546                    params: cache
547                        .params(position)
548                        .iter()
549                        .cloned()
550                        .map(ParameterValue::from)
551                        .collect(),
552                    label: operation.label,
553                })
554                .collect();
555            Some(CircuitRebuildContext::new(circuit).finish(
556                circuit.qubits(),
557                value_operations,
558                &circuit.global_phase() + &phase_delta,
559            )?)
560        } else {
561            None
562        };
563        validate_final_target(output.as_ref().unwrap_or(circuit), &self.config)?;
564        let mut diagnostics = KnowledgeRewriteDiagnostics::default();
565        diagnostics.merge_matcher(cache.diagnostics().saturating_delta(diagnostics_before));
566        Ok((
567            LinearRewriteResult {
568                circuit: output,
569                changed,
570                stats: aggregate,
571                diagnostics,
572            },
573            LinearRewriteWorkspace { cache },
574        ))
575    }
576}
577
578fn builtin_compiled_rules() -> Result<Arc<CompiledRuleSet>, CompilerError> {
579    match BUILTIN_COMPILED_RULES.get_or_init(|| {
580        let library = RuleLibrary::builtin_rules().map_err(|err| err.to_string())?;
581        CompiledRuleSet::from_library(library)
582            .map(Arc::new)
583            .map_err(|err| err.to_string())
584    }) {
585        Ok(rules) => Ok(Arc::clone(rules)),
586        Err(message) => Err(CompilerError::InvariantViolation(message.clone())),
587    }
588}
589
590// Transformer integration exposes only the generic transform outcome; direct
591// callers should use `KnowledgeRewriter::run` when rewrite statistics matter.
592impl Transformer for KnowledgeRewriter {
593    fn name(&self) -> &'static str {
594        "knowledge_rewrite"
595    }
596
597    fn transform(
598        &self,
599        circuit: &Circuit,
600        _analysis: Option<&crate::compile::transform::CircuitAnalysis>,
601    ) -> Result<TransformOutcome, CompilerError> {
602        let result = self.run(circuit)?;
603        Ok(if result.changed {
604            TransformOutcome::Changed(result.circuit)
605        } else {
606            TransformOutcome::Unchanged
607        })
608    }
609}
610
611/// Rewrites a circuit with the supplied configuration.
612pub fn rewrite_circuit(
613    circuit: &Circuit,
614    config: RewriteConfig,
615) -> Result<KnowledgeRewriteResult, CompilerError> {
616    KnowledgeRewriter::new(config).run(circuit)
617}
618
619struct RoundRewriter<'a> {
620    source: &'a Circuit,
621    rules: &'a CompiledRuleSet,
622    active_rules: &'a RunActiveRuleSet,
623    config: &'a RewriteConfig,
624    target_context: Option<&'a TargetContext>,
625    collect_diagnostics: bool,
626    workset: Option<&'a RewriteWorkset>,
627    next_workset: RewriteWorkset,
628    max_match_reach: usize,
629    rebuild: CircuitRebuildContext,
630    stats: RoundStats,
631}
632
633impl<'a> RoundRewriter<'a> {
634    fn run(
635        source: &'a Circuit,
636        rules: &'a CompiledRuleSet,
637        active_rules: &'a RunActiveRuleSet,
638        config: &'a RewriteConfig,
639        target_context: Option<&'a TargetContext>,
640        workset: Option<&'a RewriteWorkset>,
641        collect_diagnostics: bool,
642    ) -> Result<(Circuit, RoundStats, RewriteWorkset), CompilerError> {
643        let mut rewriter = Self {
644            source,
645            rules,
646            active_rules,
647            config,
648            target_context,
649            collect_diagnostics,
650            workset,
651            next_workset: RewriteWorkset::default(),
652            max_match_reach: rules.max_match_reach(config),
653            rebuild: CircuitRebuildContext::new(source),
654            stats: RoundStats::default(),
655        };
656        let mut phase_delta = Parameter::from(0.0);
657        let root_classical = rewriter.rebuild.root_classical().clone();
658        let mut operations = Vec::with_capacity(source.operations().len());
659
660        rewriter.apply_sequence(
661            source.operations(),
662            &root_classical,
663            &ScopeId::default(),
664            LoweringTarget::top_level(&mut operations, &mut phase_delta),
665        )?;
666        let global_phase = &source.global_phase() + &phase_delta;
667        let circuit = rewriter
668            .rebuild
669            .finish(source.qubits(), operations, global_phase)?;
670
671        Ok((circuit, rewriter.stats, rewriter.next_workset))
672    }
673
674    fn apply_sequence(
675        &mut self,
676        operations: &[Operation],
677        classical_remap: &ClassicalRemap,
678        scope: &ScopeId,
679        mut target: LoweringTarget<'_>,
680    ) -> Result<(), CompilerError> {
681        let mut cursor = 0;
682        let mut block_slot = 0;
683        let mut control_slot = 0;
684        while cursor < operations.len() {
685            if RewriteInstructionKey::from_instruction(&operations[cursor].instruction).is_none() {
686                let current_control_slot = control_slot;
687                if matches!(
688                    operations[cursor].instruction,
689                    Instruction::ClassicalControl(_)
690                ) {
691                    control_slot += 1;
692                }
693                self.emit_original_operation(
694                    &operations[cursor],
695                    classical_remap,
696                    scope,
697                    current_control_slot,
698                    &mut target,
699                )?;
700                cursor += 1;
701                block_slot += 1;
702                continue;
703            }
704
705            let block_start = cursor;
706            while cursor < operations.len()
707                && RewriteInstructionKey::from_instruction(&operations[cursor].instruction)
708                    .is_some()
709            {
710                cursor += 1;
711            }
712
713            let block = &operations[block_start..cursor];
714            let block_id = BlockId {
715                scope: scope.clone(),
716                slot: block_slot,
717            };
718            let ranges = self
719                .workset
720                .map(|workset| workset.block_ranges(&block_id))
721                .unwrap_or(None);
722            let cache = match self
723                .workset
724                .filter(|workset| !workset.full_scopes.contains(scope))
725                .and_then(|workset| workset.caches.get(&block_id))
726            {
727                Some(cache) if cache.len() == block.len() => Arc::clone(cache),
728                _ => Arc::new(BlockMatchCache::new_with_diagnostics(
729                    self.source,
730                    block,
731                    self.collect_diagnostics,
732                )?),
733            };
734            let diagnostics_before = cache.diagnostics();
735            let patches = select_rewrites_for_anchor_ranges(
736                block,
737                cache.as_ref(),
738                self.rules,
739                self.active_rules,
740                self.config,
741                self.target_context,
742                ranges,
743            )?;
744            self.stats
745                .diagnostics
746                .merge_matcher(cache.diagnostics().saturating_delta(diagnostics_before));
747            if patches.is_empty() {
748                self.next_workset
749                    .caches
750                    .insert(block_id.clone(), Arc::clone(&cache));
751                if target.is_top_level()
752                    && block
753                        .iter()
754                        .any(|operation| is_gphase_instruction(&operation.instruction))
755                {
756                    self.next_workset.mark_full_scope(scope.clone());
757                }
758                for operation in block {
759                    self.emit_original_operation(
760                        operation,
761                        classical_remap,
762                        scope,
763                        control_slot,
764                        &mut target,
765                    )?;
766                }
767            } else {
768                self.stats.changed_sequences += 1;
769                self.record_next_block_workset(&block_id, block, &patches);
770                if let Some(next_cache) = cache.rewritten(block, &patches) {
771                    self.next_workset
772                        .caches
773                        .insert(block_id.clone(), Arc::new(next_cache));
774                }
775                self.emit_rewritten_block(block, patches, classical_remap, &mut target)?;
776            }
777        }
778
779        Ok(())
780    }
781
782    /// Records the next round's rescan state for a block that just had
783    /// patches applied.
784    ///
785    /// Global-phase involvement (in the block or in any replacement) and
786    /// block-summary changes both invalidate anchor-level incremental state,
787    /// so the block (or its whole scope) is marked for a full rescan;
788    /// otherwise only the dirty ranges around the applied patches are
789    /// rescanned. See [`dirty_ranges_after_patches`] for the range math.
790    fn record_next_block_workset(
791        &mut self,
792        block_id: &BlockId,
793        block: &[Operation],
794        patches: &[RewritePatch],
795    ) {
796        let contains_gphase = block
797            .iter()
798            .any(|operation| is_gphase_instruction(&operation.instruction))
799            || patches.iter().any(|patch| {
800                patch
801                    .replacements
802                    .iter()
803                    .any(|replacement| is_gphase_instruction(&replacement.instruction))
804            });
805        if contains_gphase {
806            self.next_workset.mark_full_scope(block_id.scope.clone());
807            return;
808        }
809
810        if self.block_summary_changes(block, patches) {
811            self.next_workset
812                .blocks
813                .insert(block_id.clone(), BlockWorkset::Full);
814            return;
815        }
816
817        let (ranges, _) = dirty_ranges_after_patches(block.len(), patches, self.max_match_reach);
818        self.next_workset
819            .blocks
820            .insert(block_id.clone(), BlockWorkset::Ranges(ranges));
821    }
822
823    fn block_summary_changes(&self, block: &[Operation], patches: &[RewritePatch]) -> bool {
824        let mut old_keys = HashMap::<RewriteInstructionKey, usize>::new();
825        let mut old_qubits = HashMap::<Qubit, usize>::new();
826        for operation in block {
827            if let Some(key) = RewriteInstructionKey::from_instruction(&operation.instruction) {
828                *old_keys.entry(key).or_default() += 1;
829            }
830            for &qubit in &operation.qubits {
831                *old_qubits.entry(qubit).or_default() += 1;
832            }
833        }
834        let mut new_keys = old_keys.clone();
835        let mut new_qubits = old_qubits.clone();
836        for patch in patches {
837            for &position in &patch.matched_positions {
838                let operation = &block[position];
839                if let Some(key) = RewriteInstructionKey::from_instruction(&operation.instruction) {
840                    decrement_count(&mut new_keys, &key);
841                }
842                for qubit in &operation.qubits {
843                    decrement_count(&mut new_qubits, qubit);
844                }
845            }
846            for replacement in &patch.replacements {
847                *new_keys.entry(replacement.key.clone()).or_default() += 1;
848                for &qubit in &replacement.qubits {
849                    *new_qubits.entry(qubit).or_default() += 1;
850                }
851            }
852        }
853
854        old_keys.keys().collect::<HashSet<_>>() != new_keys.keys().collect::<HashSet<_>>()
855            || old_qubits.keys().collect::<HashSet<_>>()
856                != new_qubits.keys().collect::<HashSet<_>>()
857    }
858
859    fn emit_rewritten_block(
860        &mut self,
861        block: &[Operation],
862        patches: Vec<RewritePatch>,
863        classical_remap: &ClassicalRemap,
864        target: &mut LoweringTarget<'_>,
865    ) -> Result<(), CompilerError> {
866        // Drives off the shared patch application plan; see
867        // `patch_application_plan` for the application order.
868        for step in patch_application_plan(block.len(), &patches)? {
869            match step {
870                PatchPlanStep::Replacements(patch) => {
871                    self.stats.rules_applied += 1;
872                    for replacement in &patch.replacements {
873                        self.emit_replacement(replacement, target)?;
874                    }
875                }
876                PatchPlanStep::DropMatched => {}
877                PatchPlanStep::Keep(position) => {
878                    let operation = &block[position];
879                    self.emit_operation(
880                        operation.instruction.clone(),
881                        operation.qubits.clone(),
882                        operation.params.as_slice(),
883                        operation.label.clone(),
884                        classical_remap,
885                        target,
886                    )?;
887                }
888            }
889        }
890
891        Ok(())
892    }
893
894    fn emit_original_operation(
895        &mut self,
896        operation: &Operation,
897        classical_remap: &ClassicalRemap,
898        scope: &ScopeId,
899        control_slot: usize,
900        target: &mut LoweringTarget<'_>,
901    ) -> Result<(), CompilerError> {
902        if !self.config.recurses_control_flow() {
903            return self.emit_preserved_operation(operation, classical_remap, target);
904        }
905
906        if let Instruction::ClassicalControl(control) = &operation.instruction {
907            let instruction =
908                self.rewrite_control_flow(control, classical_remap, scope, control_slot)?;
909            let qubits = instruction.used_qubits().into_iter().collect();
910            return self.emit_value_operation(
911                ValueInstruction::ClassicalControl(instruction),
912                qubits,
913                CircuitRebuildContext::resolve_source_params(
914                    self.source,
915                    operation.params.as_slice(),
916                )?,
917                operation.label.clone(),
918                target,
919            );
920        }
921
922        self.emit_preserved_operation(operation, classical_remap, target)
923    }
924
925    fn rewrite_control_flow(
926        &mut self,
927        control: &ClassicalControlOp,
928        classical_remap: &ClassicalRemap,
929        parent_scope: &ScopeId,
930        control_slot: usize,
931    ) -> Result<ValueClassicalControlOp, CompilerError> {
932        let cc = match control {
933            ClassicalControlOp::If(op) => {
934                let then_scope = parent_scope.child(ScopeSegment::IfThen(control_slot));
935                let mut then_body = Vec::with_capacity(op.then_body().operations().len());
936                let mut then_phase = Parameter::from(0.0);
937                self.apply_sequence(
938                    op.then_body().operations(),
939                    classical_remap,
940                    &then_scope,
941                    LoweringTarget::control_flow_body(&mut then_body, &mut then_phase),
942                )?;
943                if !then_phase.is_zero() {
944                    self.next_workset.mark_full_scope(then_scope);
945                }
946                self.prepend_body_phase(&mut then_body, then_phase);
947
948                let else_body = op
949                    .else_body()
950                    .map(|body| {
951                        let else_scope = parent_scope.child(ScopeSegment::IfElse(control_slot));
952                        let mut rewritten = Vec::with_capacity(body.operations().len());
953                        let mut body_phase = Parameter::from(0.0);
954                        self.apply_sequence(
955                            body.operations(),
956                            classical_remap,
957                            &else_scope,
958                            LoweringTarget::control_flow_body(&mut rewritten, &mut body_phase),
959                        )?;
960                        if !body_phase.is_zero() {
961                            self.next_workset.mark_full_scope(else_scope);
962                        }
963                        self.prepend_body_phase(&mut rewritten, body_phase);
964                        Ok::<_, CompilerError>(rewritten)
965                    })
966                    .transpose()?;
967
968                ValueClassicalControlOp::If {
969                    condition: classical_remap.remap_expr(op.condition())?,
970                    then_body: ValueControlBody::new(then_body),
971                    else_body: else_body.map(ValueControlBody::new),
972                }
973            }
974            ClassicalControlOp::While(op) => {
975                let body_scope = parent_scope.child(ScopeSegment::WhileBody(control_slot));
976                let mut body = Vec::with_capacity(op.body().operations().len());
977                let mut body_phase = Parameter::from(0.0);
978                self.apply_sequence(
979                    op.body().operations(),
980                    classical_remap,
981                    &body_scope,
982                    LoweringTarget::control_flow_body(&mut body, &mut body_phase),
983                )?;
984                if !body_phase.is_zero() {
985                    self.next_workset.mark_full_scope(body_scope);
986                }
987                self.prepend_body_phase(&mut body, body_phase);
988
989                ValueClassicalControlOp::While {
990                    condition: classical_remap.remap_expr(op.condition())?,
991                    body: ValueControlBody::new(body),
992                }
993            }
994            ClassicalControlOp::For(op) => {
995                let body_scope = parent_scope.child(ScopeSegment::ForBody(control_slot));
996                let mut body = Vec::with_capacity(op.body().operations().len());
997                let mut body_phase = Parameter::from(0.0);
998                self.apply_sequence(
999                    op.body().operations(),
1000                    classical_remap,
1001                    &body_scope,
1002                    LoweringTarget::control_flow_body(&mut body, &mut body_phase),
1003                )?;
1004                if !body_phase.is_zero() {
1005                    self.next_workset.mark_full_scope(body_scope);
1006                }
1007                self.prepend_body_phase(&mut body, body_phase);
1008
1009                ValueClassicalControlOp::For {
1010                    var: classical_remap.remap_var(op.var())?,
1011                    start: classical_remap.remap_expr(op.start())?,
1012                    stop: classical_remap.remap_expr(op.stop())?,
1013                    step: classical_remap.remap_expr(op.step())?,
1014                    body: ValueControlBody::new(body),
1015                }
1016            }
1017            ClassicalControlOp::Switch(op) => {
1018                let cases = op
1019                    .cases()
1020                    .iter()
1021                    .enumerate()
1022                    .map(|(case_index, case)| {
1023                        let case_scope = parent_scope.child(ScopeSegment::SwitchCase {
1024                            control: control_slot,
1025                            case: case_index,
1026                        });
1027                        let mut rewritten = Vec::with_capacity(case.body().operations().len());
1028                        let mut body_phase = Parameter::from(0.0);
1029                        self.apply_sequence(
1030                            case.body().operations(),
1031                            classical_remap,
1032                            &case_scope,
1033                            LoweringTarget::control_flow_body(&mut rewritten, &mut body_phase),
1034                        )?;
1035                        if !body_phase.is_zero() {
1036                            self.next_workset.mark_full_scope(case_scope);
1037                        }
1038                        self.prepend_body_phase(&mut rewritten, body_phase);
1039                        Ok::<_, CompilerError>(ValueSwitchCase::new(
1040                            case.value(),
1041                            ValueControlBody::new(rewritten),
1042                        ))
1043                    })
1044                    .collect::<Result<Vec<_>, _>>()?;
1045                let default = op
1046                    .default()
1047                    .map(|body| {
1048                        let default_scope =
1049                            parent_scope.child(ScopeSegment::SwitchDefault(control_slot));
1050                        let mut rewritten = Vec::with_capacity(body.operations().len());
1051                        let mut body_phase = Parameter::from(0.0);
1052                        self.apply_sequence(
1053                            body.operations(),
1054                            classical_remap,
1055                            &default_scope,
1056                            LoweringTarget::control_flow_body(&mut rewritten, &mut body_phase),
1057                        )?;
1058                        if !body_phase.is_zero() {
1059                            self.next_workset.mark_full_scope(default_scope);
1060                        }
1061                        self.prepend_body_phase(&mut rewritten, body_phase);
1062                        Ok::<_, CompilerError>(ValueControlBody::new(rewritten))
1063                    })
1064                    .transpose()?;
1065
1066                ValueClassicalControlOp::Switch {
1067                    target: classical_remap.remap_expr(op.target())?,
1068                    cases,
1069                    default,
1070                }
1071            }
1072            ClassicalControlOp::Break => ValueClassicalControlOp::Break,
1073            ClassicalControlOp::Continue => ValueClassicalControlOp::Continue,
1074        };
1075
1076        Ok(cc)
1077    }
1078
1079    fn emit_operation(
1080        &mut self,
1081        instruction: Instruction,
1082        qubits: SmallVec<[Qubit; 3]>,
1083        params: &[CircuitParam],
1084        label: Option<Box<str>>,
1085        classical_remap: &ClassicalRemap,
1086        target: &mut LoweringTarget<'_>,
1087    ) -> Result<(), CompilerError> {
1088        if is_gphase_instruction(&instruction) {
1089            if target.is_top_level() {
1090                self.stats.representation_changes += 1;
1091            }
1092            target.accumulate_phase(self.source_gphase_param(params)?);
1093            return Ok(());
1094        }
1095
1096        let instruction = self
1097            .rebuild
1098            .remap_non_control_instruction(&instruction, classical_remap)?;
1099        let params = CircuitRebuildContext::resolve_source_params(self.source, params)?;
1100        self.emit_value_operation(instruction, qubits, params, label, target)
1101    }
1102
1103    fn emit_preserved_operation(
1104        &mut self,
1105        operation: &Operation,
1106        classical_remap: &ClassicalRemap,
1107        target: &mut LoweringTarget<'_>,
1108    ) -> Result<(), CompilerError> {
1109        if is_gphase_instruction(&operation.instruction) {
1110            if target.is_top_level() {
1111                self.stats.representation_changes += 1;
1112            }
1113            target.accumulate_phase(self.source_gphase_param(&operation.params)?);
1114            return Ok(());
1115        }
1116
1117        let operation =
1118            self.rebuild
1119                .remap_preserved_operation(self.source, operation, classical_remap)?;
1120        target.push(operation);
1121        Ok(())
1122    }
1123
1124    fn emit_value_operation(
1125        &mut self,
1126        instruction: ValueInstruction,
1127        qubits: SmallVec<[Qubit; 3]>,
1128        params: SmallVec<[ParameterValue; 1]>,
1129        label: Option<Box<str>>,
1130        target: &mut LoweringTarget<'_>,
1131    ) -> Result<(), CompilerError> {
1132        target.push(ValueOperation {
1133            instruction,
1134            qubits,
1135            params,
1136            label,
1137        });
1138        Ok(())
1139    }
1140
1141    fn emit_replacement(
1142        &mut self,
1143        replacement: &ReplacementItem,
1144        target: &mut LoweringTarget<'_>,
1145    ) -> Result<(), CompilerError> {
1146        if is_gphase_instruction(&replacement.instruction) {
1147            target.accumulate_phase(Self::replacement_gphase_param(replacement)?);
1148            return Ok(());
1149        }
1150
1151        let params = replacement.params.iter().cloned().collect();
1152        self.emit_value_operation(
1153            ValueInstruction::from_instruction(replacement.instruction.clone()),
1154            replacement.qubits.clone(),
1155            params,
1156            None,
1157            target,
1158        )
1159    }
1160
1161    fn prepend_body_phase(&mut self, body: &mut Vec<ValueOperation>, phase: Parameter) {
1162        if phase.is_zero() {
1163            return;
1164        }
1165
1166        body.insert(
1167            0,
1168            ValueOperation {
1169                instruction: ValueInstruction::from_instruction(Instruction::Standard(
1170                    StandardGate::GPhase,
1171                )),
1172                qubits: SmallVec::new(),
1173                params: smallvec::smallvec![ParameterValue::from(phase)],
1174                label: None,
1175            },
1176        );
1177    }
1178
1179    fn source_gphase_param(&self, params: &[CircuitParam]) -> Result<Parameter, CompilerError> {
1180        if params.len() != 1 {
1181            return Err(CompilerError::InvariantViolation(
1182                "GPhase operation must contain one parameter".to_string(),
1183            ));
1184        }
1185        resolve_operation_param(self.source, &params[0], 0)
1186    }
1187
1188    fn replacement_gphase_param(replacement: &ReplacementItem) -> Result<Parameter, CompilerError> {
1189        let phase = replacement.params.first().ok_or_else(|| {
1190            CompilerError::InvariantViolation(
1191                "GPhase replacement must contain one parameter".to_string(),
1192            )
1193        })?;
1194        Ok(Parameter::from(phase))
1195    }
1196}
1197
1198/// Returns whether the circuit can run on the linear workspace engine.
1199///
1200/// Eligible circuits have no classical storage and consist of a single
1201/// rewrite block: every operation has a rewrite instruction key (no barriers,
1202/// measurements, control flow, or other unkeyed instructions) and none is a
1203/// global-phase operation, whose absorption into the phase delta the
1204/// workspace does not model in its operation vector.
1205pub(super) fn is_linear_workspace_eligible(circuit: &Circuit) -> bool {
1206    circuit.classical_vars().is_empty()
1207        && circuit.classical_values().is_empty()
1208        && circuit.operations().iter().all(|operation| {
1209            RewriteInstructionKey::from_instruction(&operation.instruction).is_some()
1210                && !is_gphase_instruction(&operation.instruction)
1211        })
1212}
1213
1214/// Computes the dirty anchor ranges for the round after `patches` is applied
1215/// to a block, plus the block length after application.
1216///
1217/// Matching at an anchor reads at most `max_match_reach` operations ahead
1218/// (see [`CompiledRuleSet::max_match_reach`]), so a patch covering
1219/// `first..=last` can only change match results for anchors in
1220/// `[first - max_match_reach, last]`. Ranges are computed in the
1221/// post-application coordinate space by tracking the running length delta,
1222/// and each patch's full span — including the skipped positions between its
1223/// matched ones — is dirtied because the selector's overlap rule owns the
1224/// whole span.
1225///
1226/// Global-phase replacements never occupy a block position (they are folded
1227/// into the phase delta), so they are excluded from the inserted count. In
1228/// the degenerate case where every range clamps away but operations remain
1229/// (only reachable with a zero maximum reach), a small prefix range is
1230/// returned so the next round never starts from an unjustified "fully clean"
1231/// state; those extra anchors cannot produce candidates a full scan would
1232/// not also find.
1233fn dirty_ranges_after_patches(
1234    old_len: usize,
1235    patches: &[RewritePatch],
1236    max_match_reach: usize,
1237) -> (Vec<Range<usize>>, usize) {
1238    let mut ranges = Vec::with_capacity(patches.len());
1239    let mut delta = 0isize;
1240    let mut new_len = old_len as isize;
1241    for patch in patches {
1242        let inserted = patch
1243            .replacements
1244            .iter()
1245            .filter(|replacement| !is_gphase_instruction(&replacement.instruction))
1246            .count() as isize;
1247        let removed = patch.matched_positions.len() as isize;
1248        let new_start = (patch.first_position as isize + delta).max(0) as usize;
1249        delta += inserted - removed;
1250        new_len += inserted - removed;
1251        let after_span = (patch.last_position as isize + 1 + delta).max(0) as usize;
1252        let dirty_start = new_start.saturating_sub(max_match_reach);
1253        let dirty_end = after_span.max(new_start.saturating_add(1));
1254        ranges.push(dirty_start..dirty_end);
1255    }
1256
1257    let new_len = new_len.max(0) as usize;
1258    for range in &mut ranges {
1259        range.end = range.end.min(new_len);
1260    }
1261    ranges.retain(|range| range.start < range.end);
1262    merge_ranges(&mut ranges);
1263    if ranges.is_empty() && new_len > 0 {
1264        ranges.push(0..new_len.min(max_match_reach.saturating_add(1)));
1265    }
1266    (ranges, new_len)
1267}
1268
1269/// Splices the selected patches into the linear workspace operation vector.
1270///
1271/// Drives off the shared [`patch_application_plan`]; see its documentation
1272/// for the application order. Global-phase replacements are folded into the
1273/// returned phase delta instead of occupying a position. Replacement
1274/// operations carry placeholder `Fixed(0.0)` parameters because the real
1275/// resolved parameters live in the round's [`BlockMatchCache`]; matching and
1276/// final materialization only ever read parameter values from the cache,
1277/// while the placeholder preserves the correct parameter count for local
1278/// cost accounting.
1279fn apply_linear_patches(
1280    operations: &[Operation],
1281    patches: &[RewritePatch],
1282) -> Result<(Vec<Operation>, Parameter), CompilerError> {
1283    let steps = patch_application_plan(operations.len(), patches)?;
1284    let mut output = Vec::with_capacity(operations.len());
1285    let mut phase_delta = Parameter::from(0.0);
1286    let mut operations = operations.iter();
1287    for step in steps {
1288        match step {
1289            PatchPlanStep::Replacements(patch) => {
1290                for replacement in &patch.replacements {
1291                    if is_gphase_instruction(&replacement.instruction) {
1292                        phase_delta =
1293                            &phase_delta + &RoundRewriter::replacement_gphase_param(replacement)?;
1294                        continue;
1295                    }
1296                    output.push(Operation {
1297                        instruction: replacement.instruction.clone(),
1298                        qubits: replacement.qubits.clone(),
1299                        params: replacement
1300                            .params
1301                            .iter()
1302                            .map(|_| CircuitParam::Fixed(0.0))
1303                            .collect(),
1304                        label: None,
1305                    });
1306                }
1307            }
1308            PatchPlanStep::DropMatched => {
1309                operations.next();
1310            }
1311            PatchPlanStep::Keep(_) => {
1312                output.push(
1313                    operations
1314                        .next()
1315                        .expect(
1316                            "patch application plan produces exactly one step per block position",
1317                        )
1318                        .clone(),
1319                );
1320            }
1321        }
1322    }
1323    Ok((output, phase_delta))
1324}
1325
1326fn decrement_count<K>(counts: &mut HashMap<K, usize>, key: &K)
1327where
1328    K: Eq + Hash,
1329{
1330    let remove = counts.get_mut(key).is_some_and(|count| {
1331        *count = count.saturating_sub(1);
1332        *count == 0
1333    });
1334    if remove {
1335        counts.remove(key);
1336    }
1337}
1338
1339fn merge_ranges(ranges: &mut Vec<Range<usize>>) {
1340    ranges.sort_by_key(|range| range.start);
1341    let mut merged = Vec::<Range<usize>>::with_capacity(ranges.len());
1342    for range in ranges.drain(..) {
1343        if let Some(previous) = merged.last_mut()
1344            && range.start <= previous.end
1345        {
1346            previous.end = previous.end.max(range.end);
1347        } else {
1348            merged.push(range);
1349        }
1350    }
1351    *ranges = merged;
1352}
1353
1354fn recursive_operation_count(operations: &[Operation]) -> usize {
1355    let mut count = operations.len();
1356    for operation in operations {
1357        if let Instruction::ClassicalControl(control) = &operation.instruction {
1358            for body in control.bodies() {
1359                count = count.saturating_add(recursive_operation_count(body.operations()));
1360            }
1361        }
1362    }
1363    count
1364}