Skip to main content

bloq_vm/
lower.rs

1//! Lower Bloq IR into the backend-independent VM instruction set.
2
3use std::collections::BTreeSet;
4use std::sync::Arc;
5
6use bloq_ir::circuit::{
7    DetectorParity, DetectorTerm, GateType, NoiseModel, Op, PauliBasis, PauliMap,
8};
9use bloq_ir::lowering::{
10    InstanceMeasurement, InstantiationOptions, NodeEmissionPlan, NodeTemplateInstanceMergeError,
11    TemplateDetectorScope, TemplateInstanceId,
12};
13use bloq_ir::{
14    Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, BodySelector, BoundaryFace, ClassicalExpr,
15    ClassicalNode, CoordinateOverflowError, CycleDetected, FlattenError, LevelPath,
16    MemoryRoundTarget, NodeDetectorParity, NodeProvenance, ObservableOutput, PipePadding,
17    RegionNode, SubGraph, TemporalPipeRef, ValueRef,
18};
19use glam::{IVec2, IVec3};
20use rustc_hash::FxHashMap;
21
22use crate::instruction::*;
23
24/// Default maximum number of reachable alternatives for one guarded quantum stage.
25pub const DEFAULT_MAX_QUANTUM_VARIANTS: usize = 4_096;
26
27type ScopedNode = (LevelPath, BloqNodeId);
28#[derive(Clone)]
29struct DynamicWait {
30    siblings: Vec<ScopedNode>,
31    hoist_after: Option<ScopedNode>,
32    origin: TaskOrigin,
33}
34type DynamicWaits = FxHashMap<ScopedNode, DynamicWait>;
35
36fn task_origin(function: TaskFunction, members: impl IntoIterator<Item = IVec3>) -> TaskOrigin {
37    let members = members
38        .into_iter()
39        .map(|member| member.to_array())
40        .collect::<BTreeSet<_>>();
41    let sites = members
42        .iter()
43        .map(|member| [member[0], member[1]])
44        .collect::<BTreeSet<_>>();
45    TaskOrigin {
46        function,
47        sites: sites.into_iter().collect(),
48        members: members.into_iter().collect(),
49    }
50}
51
52fn pipe_origin(pipes: impl IntoIterator<Item = TemporalPipeRef>) -> TaskOrigin {
53    task_origin(
54        TaskFunction::Memory,
55        pipes.into_iter().flat_map(|pipe| [pipe.src, pipe.dst]),
56    )
57}
58
59fn merge_origins<'a>(
60    function: TaskFunction,
61    origins: impl IntoIterator<Item = &'a TaskOrigin>,
62) -> TaskOrigin {
63    task_origin(
64        function,
65        origins
66            .into_iter()
67            .flat_map(|origin| origin.members.iter().copied().map(IVec3::from_array)),
68    )
69}
70
71#[derive(Clone)]
72struct TerminalHold {
73    owner: ScopedNode,
74    wait: ScopedNode,
75    bindings: Vec<ScopedNode>,
76}
77
78/// Bounded lowering and timing options.
79#[derive(Debug, Clone, Copy)]
80pub struct LoweringConfig<'a> {
81    /// Uniform duration assigned to every nonempty quantum moment. This must
82    /// exceed the runtime clock tolerance.
83    pub gate_duration: f64,
84    /// Streaming-decoder latency, in physical memory rounds.
85    pub decoder_latency_rounds: u32,
86    /// Optional post-merge circuit noise. The IR instantiator applies it once.
87    pub noise: Option<&'a NoiseModel>,
88    /// Absolute source-release epochs. Prepared Y sources are shifted so they
89    /// finish with ordinary Clifford source work.
90    pub source_timing: SourceTiming,
91    /// Maximum reachable alternatives materialized for one guarded quantum stage.
92    pub max_quantum_variants: usize,
93}
94
95/// Absolute release epochs for independently schedulable source classes.
96#[derive(Debug, Clone, Copy, Default, PartialEq)]
97pub struct SourceTiming {
98    /// Factory source release epoch.
99    pub factory: f64,
100    /// External logical-input release epoch.
101    pub input: f64,
102    /// Clifford source release epoch and Prepared-Y completion reference.
103    pub clifford: f64,
104}
105
106impl Default for LoweringConfig<'_> {
107    fn default() -> Self {
108        Self {
109            gate_duration: 1.0,
110            decoder_latency_rounds: DEFAULT_DECODER_LATENCY_ROUNDS,
111            noise: None,
112            source_timing: SourceTiming::default(),
113            max_quantum_variants: DEFAULT_MAX_QUANTUM_VARIANTS,
114        }
115    }
116}
117
118/// Failure to produce a finite, standalone VM program.
119#[derive(Debug, thiserror::Error)]
120#[non_exhaustive]
121pub enum LowerError {
122    /// An IR emission plan could not be materialized.
123    #[error("Bloq IR materialization failed: {0}")]
124    InvalidProgram(#[from] bloq_ir::BloqValidationError),
125    /// Repeat flattening failed or exceeded its configured IR resource bound.
126    #[error("Bloq IR flattening failed: {0}")]
127    Flatten(#[from] FlattenError),
128    /// Materializing an exact dynamic memory frontier failed.
129    #[error("dynamic join memory insertion failed: {0}")]
130    Edit(#[from] bloq_ir::EditError),
131    /// A region boundary did not have the unique escape required for padding.
132    #[error("dynamic join region structure is unsupported: {0}")]
133    Structure(#[from] bloq_ir::StructureError),
134    /// Output-cut or decode-closure analysis failed.
135    #[error("execution closure analysis failed: {0}")]
136    Analysis(#[from] crate::ExecError),
137    /// Existing IR moment lanes could not be aligned exactly.
138    #[error("moment alignment failed: {0}")]
139    Alignment(#[from] bloq_ir::MomentAlignmentError),
140    /// A node could not be instantiated or noised exactly.
141    #[error("quantum stage instantiation failed: {0}")]
142    Instantiation(#[from] NodeTemplateInstanceMergeError),
143    /// A shared detector use has an invalid bundle or owner binding.
144    #[error("{0}")]
145    DetectorBundle(#[from] bloq_ir::DetectorBundleError),
146    /// A graph level has no topological execution order.
147    #[error("Bloq IR graph is cyclic: {0}")]
148    Cycle(#[from] CycleDetected),
149    /// A placed coordinate overflowed the IR coordinate lattice.
150    #[error("{0}")]
151    CoordinateOverflow(#[from] CoordinateOverflowError),
152    /// Lowering configuration contains an invalid duration or a derived sum overflowed.
153    #[error(
154        "gate duration must be finite and exceed the runtime clock tolerance, and derived durations must stay finite"
155    )]
156    InvalidDuration,
157    /// A configured or derived source release is negative or non-finite.
158    #[error("source release times must be finite and non-negative")]
159    InvalidSourceTiming,
160    /// Dense VM identifiers exhausted `u32`.
161    #[error("{0} identifier space exhausted")]
162    IdOverflow(&'static str),
163    /// A conditional stage would require too many materialized choices.
164    #[error(
165        "quantum node {node} has {guards} guard inputs: {variants} reachable choices exceed limit {limit}"
166    )]
167    QuantumVariantLimit {
168        /// Level-local node id.
169        node: u32,
170        /// Number of guard inputs.
171        guards: usize,
172        /// Observed reachable choice count, stopping at the first excess.
173        variants: usize,
174        /// Configured maximum.
175        limit: usize,
176    },
177    /// A permitted choice count could not fit or allocate its choice table.
178    #[error("cannot allocate {variants} quantum alternatives: {source}")]
179    QuantumVariantAllocation {
180        /// Required reachable choice count.
181        variants: usize,
182        /// Capacity or allocation failure from the choice table.
183        #[source]
184        source: std::collections::TryReserveError,
185    },
186    /// A record was referenced outside its producing emission plan.
187    #[error("measurement record {instance}:{measurement} has no VM identity")]
188    MissingRecord {
189        /// Template instance id.
190        instance: u32,
191        /// Template-local measurement id.
192        measurement: u32,
193    },
194    /// A value edge expected a bit from a binding-only or sink task.
195    #[error("node {node} value slot {slot} is not backed by a VM bit")]
196    MissingBit {
197        /// Consuming level-local node id.
198        node: u32,
199        /// Missing input slot.
200        slot: u32,
201    },
202    /// A declared logical output has no lowered frame register.
203    #[error("logical output {0:?} has no complete lowered frame")]
204    MissingOutputFrame([i32; 3]),
205    /// A declared logical input has no lowered owning task.
206    #[error("logical input instance {0} has no lowered VM task")]
207    MissingInputInstance(u32),
208    /// A declared logical output has no physical owning node.
209    #[error("logical output instance {0} has no physical owner")]
210    MissingOutputInstance(u32),
211    /// Dynamic execution cannot defer this reused output boundary to program end.
212    #[error("dynamic VM output boundary is consumed before terminal reporting")]
213    ReusedOutputBoundary,
214    /// Dynamic retries are supported only for source-isolated RUS regions.
215    #[error("RUS node {0} has an incoming quantum seam and is not source-isolated")]
216    NonSourceRus(u32),
217    /// A dynamic readiness edge lacks complete persisted one-round padding.
218    #[error("RUS readiness seam {from}->{to} has no complete one-round memory template")]
219    MissingMemoryTemplate {
220        /// Source region node.
221        from: u32,
222        /// Dependent node.
223        to: u32,
224    },
225    /// A source RUS output seam lost its physical decoder-hold cycle.
226    #[error("RUS node {0} has an outgoing padded seam but no decoder hold cycle")]
227    MissingDecoderHold(u32),
228    /// A flattened emission plan unexpectedly retained a loop or loop-state term.
229    #[error("flattened quantum stream retains unsupported {0}")]
230    Unflattened(&'static str),
231    /// A materialized memory frontier could not be initialized exactly.
232    #[error("dynamic memory frontier detector has {0} padding records; expected one")]
233    InvalidFrontier(usize),
234}
235
236/// Insert one authored memory round as a movable detector frontier on every
237/// quantum input of a real join and every physical RUS output. The VM may
238/// execute that node zero or many times, but the ordinary IR edit performs the
239/// hard part once: source and consumer detector rows are recomposed against
240/// the padding records.
241fn materialize_dynamic_waits(bloq: &mut Bloq) -> Result<DynamicWaits, LowerError> {
242    let mut targets = Vec::new();
243    let mut seen = BTreeSet::new();
244    for (path, level) in bloq.levels() {
245        for to in level.node_ids() {
246            let incoming = level.incoming(to).collect::<Vec<_>>();
247            let sources = incoming
248                .iter()
249                .map(|edge| edge.source)
250                .collect::<BTreeSet<_>>();
251            for edge in incoming {
252                let BloqEdge::Quantum(quantum) = edge.edge else {
253                    continue;
254                };
255                let source_rus = matches!(
256                    level.node(edge.source).and_then(BloqNode::try_region),
257                    Some(RegionNode::RepeatUntilSuccess { .. })
258                );
259                if sources.len() < 2 && !source_rus {
260                    continue;
261                }
262                if quantum.pipes.is_empty()
263                    || quantum.pipes.iter().any(|pipe| pipe.padding.is_none())
264                {
265                    return Err(LowerError::MissingMemoryTemplate {
266                        from: edge.source.0,
267                        to: edge.target.0,
268                    });
269                }
270                if seen.insert((path.clone(), edge.source, edge.target)) {
271                    targets.push((
272                        path.clone(),
273                        edge.source,
274                        edge.target,
275                        sources.clone(),
276                        pipe_origin(quantum.pipes.iter().map(|pipe| pipe.pipe)),
277                    ));
278                }
279            }
280        }
281    }
282
283    let mut waits = DynamicWaits::default();
284    for (path, from, to, sources, origin) in targets {
285        let siblings = sources
286            .iter()
287            .copied()
288            .filter(|source| *source != from)
289            .map(|node| (path.clone(), node))
290            .collect();
291        let source_region = bloq
292            .level_at(&path)
293            .and_then(|level| level.node(from))
294            .and_then(BloqNode::try_region)
295            .is_some();
296        let (wait_path, wait) = if source_region {
297            let body_path = path.child(from, BodySelector::Body);
298            let escape = bloq
299                .level_at(&body_path)
300                .expect("region body path exists")
301                .quantum_tail()?;
302            let wait = bloq.insert_memory_rounds(
303                MemoryRoundTarget::After {
304                    path: body_path.clone(),
305                    node: escape,
306                },
307                1,
308            )?;
309            (body_path, wait)
310        } else {
311            let wait = bloq.insert_memory_rounds(
312                MemoryRoundTarget::Edge {
313                    path: path.clone(),
314                    from,
315                    to,
316                },
317                1,
318            )?;
319            (path.clone(), wait)
320        };
321        waits.insert(
322            (wait_path, wait),
323            DynamicWait {
324                siblings,
325                hoist_after: source_region.then_some((path, from)),
326                origin,
327            },
328        );
329    }
330    Ok(waits)
331}
332
333fn materialize_terminal_holds(
334    bloq: &mut Bloq,
335    waits: &mut DynamicWaits,
336) -> Result<Vec<TerminalHold>, LowerError> {
337    let top = LevelPath::default();
338    let frames = bloq
339        .output_frames()
340        .into_iter()
341        .map(|frame| (frame.port, [frame.x, frame.z]))
342        .collect::<FxHashMap<_, _>>();
343    let mut targets = Vec::new();
344    for output in bloq.logical_outputs() {
345        let owner = bloq.levels().find_map(|(path, level)| {
346            level.quantum_nodes().find_map(|(node, quantum)| {
347                quantum
348                    .instances
349                    .iter()
350                    .any(|instance| instance.id == output.instance)
351                    .then(|| (path.clone(), node))
352            })
353        });
354        let Some((path, owner)) = owner else {
355            return Err(LowerError::MissingOutputInstance(output.instance.0));
356        };
357        if targets.iter().any(|(existing_path, existing, _, _, _, _)| {
358            *existing_path == path && *existing == owner
359        }) {
360            continue;
361        }
362        let level = bloq
363            .level_at(&path)
364            .expect("logical output owner path remains valid");
365        let (from, edge) = level
366            .incoming(owner)
367            .find_map(|edge| match edge.edge {
368                BloqEdge::Quantum(quantum) => Some((edge.source, quantum)),
369                BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
370            })
371            .ok_or(LowerError::MissingMemoryTemplate {
372                from: owner.0,
373                to: owner.0,
374            })?;
375        if edge.pipes.is_empty() || edge.pipes.iter().any(|pipe| pipe.padding.is_none()) {
376            return Err(LowerError::MissingMemoryTemplate {
377                from: from.0,
378                to: owner.0,
379            });
380        }
381        let frame_nodes = frames
382            .get(&output.port)
383            .copied()
384            .ok_or(LowerError::MissingOutputFrame(output.port.to_array()))?;
385        let bindings = bloq
386            .levels()
387            .flat_map(|(binding_path, level)| {
388                level
389                    .nodes()
390                    .filter_map(move |(node, data)| match data.try_classical() {
391                        Some(classical)
392                            if classical.operators().iter().any(|operator| {
393                                operator.face == BoundaryFace::Output
394                                    && operator.instance == output.instance
395                            }) =>
396                        {
397                            Some((binding_path.clone(), node))
398                        }
399                        _ => None,
400                    })
401            })
402            .collect::<Vec<_>>();
403        targets.push((
404            path,
405            owner,
406            from,
407            frame_nodes,
408            bindings,
409            pipe_origin(edge.pipes.iter().map(|pipe| pipe.pipe)),
410        ));
411    }
412
413    let mut holds = Vec::new();
414    for (path, owner, from, frame_nodes, bindings, origin) in targets {
415        let wait = bloq.insert_memory_rounds(
416            MemoryRoundTarget::Edge {
417                path: path.clone(),
418                from,
419                to: owner,
420            },
421            1,
422        )?;
423        let wait = (path.clone(), wait);
424        waits.insert(
425            wait.clone(),
426            DynamicWait {
427                siblings: frame_nodes
428                    .into_iter()
429                    .map(|frame| (top.clone(), frame))
430                    .collect(),
431                hoist_after: None,
432                origin,
433            },
434        );
435        holds.push(TerminalHold {
436            owner: (path, owner),
437            wait,
438            bindings,
439        });
440    }
441    Ok(holds)
442}
443
444/// Lower `bloq` to a self-contained VM program.
445///
446/// The input is cloned because repeat expansion is a lowering concern. Noise is
447/// passed through [`InstantiationOptions::noisy`], after each selected stage is
448/// merged, rather than reimplemented by the VM.
449///
450/// # Errors
451///
452/// Returns [`LowerError`] when required materialization exceeds a bound or an
453/// IR construct cannot be represented without loss. Full Bloq well-formedness
454/// validation is available separately through [`Bloq::validate`].
455pub fn lower(bloq: &Bloq, config: &LoweringConfig<'_>) -> Result<Program, LowerError> {
456    if !(config.gate_duration.is_finite() && config.gate_duration > crate::runtime::TIME_EPSILON) {
457        return Err(LowerError::InvalidDuration);
458    }
459    if [
460        config.source_timing.factory,
461        config.source_timing.input,
462        config.source_timing.clifford,
463    ]
464    .into_iter()
465    .any(|release| !release.is_finite() || release < 0.0)
466    {
467        return Err(LowerError::InvalidSourceTiming);
468    }
469    let mut bloq = bloq.clone();
470    let mut dynamic_waits = materialize_dynamic_waits(&mut bloq)?;
471    let terminal_holds = materialize_terminal_holds(&mut bloq, &mut dynamic_waits)?;
472    bloq.flatten()?;
473    if !crate::closure::plan_early_output_reads(&bloq)?.is_empty()
474        || crate::closure::logical_output_cuts(&bloq)?
475            .iter()
476            .any(|cuts| !cuts.is_empty())
477    {
478        return Err(LowerError::ReusedOutputBoundary);
479    }
480    crate::closure::guard_decode_observable_closure(&bloq)?;
481    let options = config
482        .noise
483        .map_or_else(InstantiationOptions::default, InstantiationOptions::noisy);
484    let plans = bloq.emission_plans(&options)?;
485    let instance_templates = bloq
486        .levels()
487        .flat_map(|(_, level)| {
488            level.quantum_nodes().flat_map(|(_, quantum)| {
489                quantum
490                    .instances
491                    .iter()
492                    .map(|instance| (instance.id, instance.template_id))
493            })
494        })
495        .collect();
496    let layout = bloq.sorted_layout_coords()?;
497    let qubit_count = u32::try_from(layout.len()).map_err(|_| LowerError::IdOverflow("qubit"))?;
498    let coords = layout
499        .iter()
500        .enumerate()
501        .map(|(index, &coord)| (coord, index as QubitId))
502        .collect();
503    let mut cx = LowerCx {
504        bloq: &bloq,
505        config,
506        options,
507        plans: &plans,
508        coords,
509        tasks: Vec::new(),
510        task_origins: Vec::new(),
511        next_bit: 0,
512        next_record: 0,
513        next_resource: 0,
514        records: FxHashMap::default(),
515        readout_parities: FxHashMap::default(),
516        boolean_functions: BooleanFunctions::default(),
517        instance_templates,
518        dynamic_waits,
519        node_tasks: FxHashMap::default(),
520        instance_tasks: FxHashMap::default(),
521        observable_parts: FxHashMap::default(),
522        terminal_holds,
523        pending_waits: Vec::new(),
524        hoisted_waits: FxHashMap::default(),
525    };
526    let top_round_duration = cx
527        .bloq
528        .pipe_padding()
529        .map(|padding| cx.one_round_duration(*padding))
530        .collect::<Result<Vec<_>, _>>()?
531        .into_iter()
532        .reduce(f64::max)
533        .unwrap_or(config.gate_duration);
534    let entry = cx
535        .lower_level(
536            bloq.top(),
537            &LevelPath::default(),
538            None,
539            top_round_duration,
540            None,
541        )?
542        .stream;
543    cx.rewire_terminal_holds()?;
544    cx.finish_waits()?;
545    cx.assign_source_releases()?;
546    let inputs = cx.lower_inputs()?;
547    let outputs = cx.lower_outputs()?;
548    Ok(Program {
549        qubit_count,
550        bit_count: cx.next_bit,
551        record_count: cx.next_record,
552        tasks: cx.tasks,
553        task_origins: cx.task_origins,
554        entry,
555        inputs,
556        outputs,
557        decoder_latency_rounds: config.decoder_latency_rounds,
558    })
559}
560
561#[derive(Clone, Copy)]
562struct LoweredNode {
563    entry: TaskId,
564    ready: TaskId,
565    exit: TaskId,
566    bit: Option<BitId>,
567    flip: Option<BitId>,
568    raw: Option<BitId>,
569}
570
571impl LoweredNode {
572    fn output(&self, output: ObservableOutput) -> Option<BitId> {
573        match output {
574            ObservableOutput::Corrected => self.bit,
575            ObservableOutput::Flip => self.flip,
576        }
577    }
578}
579
580struct LoweredLevel {
581    stream: Stream,
582    nodes: FxHashMap<BloqNodeId, LoweredNode>,
583}
584
585struct LowerCx<'a, 'c> {
586    bloq: &'a Bloq,
587    config: &'c LoweringConfig<'c>,
588    options: InstantiationOptions<'c>,
589    plans: &'a bloq_ir::ValidatedPlans,
590    coords: FxHashMap<IVec2, QubitId>,
591    tasks: Vec<Task>,
592    task_origins: Vec<TaskOrigin>,
593    next_bit: BitId,
594    next_record: RecordId,
595    next_resource: ResourceId,
596    records: FxHashMap<InstanceMeasurement, RecordId>,
597    readout_parities: FxHashMap<Vec<InstanceMeasurement>, RecordParity>,
598    boolean_functions: BooleanFunctions,
599    instance_templates: FxHashMap<TemplateInstanceId, bloq_ir::TemplateId>,
600    dynamic_waits: DynamicWaits,
601    node_tasks: FxHashMap<ScopedNode, LoweredNode>,
602    instance_tasks: FxHashMap<TemplateInstanceId, TaskId>,
603    observable_parts: FxHashMap<TaskId, Vec<TaskId>>,
604    terminal_holds: Vec<TerminalHold>,
605    pending_waits: Vec<(TaskId, Vec<ScopedNode>)>,
606    hoisted_waits: FxHashMap<ScopedNode, (TaskId, Vec<ScopedNode>)>,
607}
608
609impl LowerCx<'_, '_> {
610    fn quantum_origin(&self, node: &BloqNode, source: Option<SourceRole>) -> TaskOrigin {
611        let quantum = node.expect_quantum();
612        let boundary_instances = self
613            .bloq
614            .logical_inputs()
615            .iter()
616            .map(|input| input.instance)
617            .chain(
618                self.bloq
619                    .logical_outputs()
620                    .iter()
621                    .map(|output| output.instance),
622            )
623            .collect::<BTreeSet<_>>();
624        let ideal_boundary = !quantum.instances.is_empty()
625            && quantum
626                .instances
627                .iter()
628                .all(|instance| boundary_instances.contains(&instance.id));
629        match &node.provenance {
630            NodeProvenance::BlockComponent { members } => task_origin(
631                if ideal_boundary {
632                    TaskFunction::IdealBoundary
633                } else if source == Some(SourceRole::PreparedY) {
634                    TaskFunction::PreparedY
635                } else if !quantum.guards.is_empty() {
636                    TaskFunction::AdaptiveMeasurement
637                } else {
638                    TaskFunction::Cube
639                },
640                members.iter().map(|member| member.pos),
641            ),
642            NodeProvenance::TemporalPipe { pipe } => task_origin(
643                if pipe.hadamard {
644                    TaskFunction::TemporalHadamard
645                } else {
646                    TaskFunction::Synthetic
647                },
648                [pipe.src, pipe.dst],
649            ),
650            NodeProvenance::SpatialPortSubstitution { source, .. } => {
651                task_origin(TaskFunction::IdealBoundary, [*source])
652            }
653            NodeProvenance::MemoryPadding { pipe, .. } => pipe_origin(std::iter::once(*pipe)),
654            NodeProvenance::Generator { .. }
655            | NodeProvenance::Action { .. }
656            | NodeProvenance::BranchSelector { .. }
657            | NodeProvenance::OutputFrame { .. }
658            | NodeProvenance::None => TaskOrigin::default(),
659        }
660    }
661
662    fn align_level(
663        &mut self,
664        level: &SubGraph,
665        nodes: &FxHashMap<BloqNodeId, LoweredNode>,
666    ) -> Result<(), LowerError> {
667        let mut lanes = Vec::new();
668        for (node, quantum) in level.quantum_nodes() {
669            let Some(lowered) = nodes.get(&node) else {
670                continue;
671            };
672            let Some(stream) = instruction_stream(&self.tasks[lowered.entry as usize].instruction)
673            else {
674                continue;
675            };
676            let moments = stream
677                .moments
678                .iter()
679                .map(|moment| moment.kind.map(ir_moment_kind))
680                .collect();
681            let mut qubits = self
682                .bloq
683                .node_qubits(&level[node])?
684                .into_iter()
685                .collect::<Vec<_>>();
686            qubits.sort_by_key(|qubit| (qubit.x, qubit.y));
687            // Use the persisted timeline and graph dependencies from the real
688            // node; the lowered stream only supplies its already split moments.
689            let _ = quantum;
690            lanes.push(bloq_ir::MomentLane {
691                node,
692                moments,
693                qubits,
694            });
695        }
696        if lanes.len() < 2 {
697            return Ok(());
698        }
699        let aligned = bloq_ir::align_moment_lanes(level, lanes)?;
700        let mut positions = FxHashMap::<BloqNodeId, FxHashMap<usize, (i64, usize)>>::default();
701        for layer in aligned {
702            for (slot, aligned) in layer.slots.into_iter().enumerate() {
703                for entry in aligned.entries {
704                    positions
705                        .entry(entry.node)
706                        .or_default()
707                        .insert(entry.moment, (layer.layer, slot));
708                }
709            }
710        }
711        let lane_tasks = nodes
712            .iter()
713            .filter(|(node, _)| level[**node].try_quantum().is_some())
714            .map(|(&node, lowered)| (node, lowered.entry))
715            .collect::<FxHashMap<_, _>>();
716        for (node, mut positions) in positions {
717            let task = nodes[&node].entry as usize;
718            let incoming_quantum = level
719                .incoming(node)
720                .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
721            let causally_started = lane_tasks.iter().any(|(&predecessor, &predecessor_task)| {
722                predecessor != node
723                    && (level.has_path(predecessor, node)
724                        || self.tasks[task].dependencies.contains(&predecessor_task))
725            });
726            if causally_started || !incoming_quantum {
727                let mut first = FxHashMap::<i64, usize>::default();
728                for &(layer, slot) in positions.values() {
729                    first
730                        .entry(layer)
731                        .and_modify(|current| *current = (*current).min(slot))
732                        .or_insert(slot);
733                }
734                for (layer, slot) in positions.values_mut() {
735                    *slot -= first[layer];
736                }
737            }
738            let ideal = level[node]
739                .expect_quantum()
740                .instances
741                .iter()
742                .filter(|instance| instance.provenance.is_spatial_port_substitution())
743                .flat_map(|instance| {
744                    self.bloq.templates()[instance.template_id]
745                        .qubits()
746                        .iter()
747                        .filter_map(|&qubit| self.qubit(qubit, instance.offset).ok())
748                })
749                .collect::<BTreeSet<_>>();
750            let idle_qubits = self.tasks[task]
751                .qubits
752                .iter()
753                .copied()
754                .filter(|qubit| !ideal.contains(qubit))
755                .collect::<Box<[_]>>();
756            rewrite_instruction_streams(
757                &mut self.tasks[task].instruction,
758                &positions,
759                self.config.gate_duration,
760                &idle_qubits,
761                self.config.noise.map_or(0.0, |noise| noise.p_idle),
762            )?;
763            self.tasks[task].duration = instruction_duration(&self.tasks[task].instruction)?;
764        }
765        Ok(())
766    }
767
768    fn lower_outputs(&self) -> Result<Vec<LogicalOutput>, LowerError> {
769        let top = LevelPath::default();
770        let frames = self
771            .bloq
772            .output_frames()
773            .into_iter()
774            .map(|frame| (frame.port.to_array(), frame))
775            .collect::<FxHashMap<_, _>>();
776        self.bloq
777            .logical_outputs()
778            .iter()
779            .map(|output| {
780                let port = output.port.to_array();
781                let frame = frames
782                    .get(&port)
783                    .ok_or(LowerError::MissingOutputFrame(port))?;
784                let frame_x = self
785                    .node_tasks
786                    .get(&(top.clone(), frame.x))
787                    .and_then(|node| node.bit)
788                    .ok_or(LowerError::MissingOutputFrame(port))?;
789                let frame_z = self
790                    .node_tasks
791                    .get(&(top.clone(), frame.z))
792                    .and_then(|node| node.bit)
793                    .ok_or(LowerError::MissingOutputFrame(port))?;
794                Ok(LogicalOutput {
795                    port,
796                    x: self.pauli_product(&output.x, IVec2::ZERO)?,
797                    z: self.pauli_product(&output.z, IVec2::ZERO)?,
798                    frame_x,
799                    frame_z,
800                })
801            })
802            .collect()
803    }
804
805    fn finish_waits(&mut self) -> Result<(), LowerError> {
806        for (task, siblings) in &self.pending_waits {
807            let until = siblings
808                .iter()
809                .map(|sibling| {
810                    self.node_tasks
811                        .get(sibling)
812                        .map(|node| node.ready)
813                        .ok_or(LowerError::Unflattened("dynamic join sibling"))
814                })
815                .collect::<Result<Vec<_>, _>>()?
816                .into_boxed_slice();
817            let Instruction::WaitFor {
818                until: task_until, ..
819            } = &mut self.tasks[*task as usize].instruction
820            else {
821                unreachable!("dynamic wait task retains its instruction")
822            };
823            *task_until = until;
824        }
825        Ok(())
826    }
827
828    fn rewire_terminal_holds(&mut self) -> Result<(), LowerError> {
829        for hold in &self.terminal_holds {
830            let owner = self
831                .node_tasks
832                .get(&hold.owner)
833                .ok_or(LowerError::Unflattened("terminal output owner"))?
834                .exit;
835            let wait = self
836                .node_tasks
837                .get(&hold.wait)
838                .ok_or(LowerError::Unflattened("terminal output wait"))?
839                .entry;
840            let predecessors = self.tasks[wait as usize].dependencies.clone();
841            for binding in &hold.bindings {
842                let binding = self
843                    .node_tasks
844                    .get(binding)
845                    .ok_or(LowerError::Unflattened("terminal output binding"))?
846                    .entry;
847                for task in std::iter::once(binding).chain(
848                    self.observable_parts
849                        .get(&binding)
850                        .into_iter()
851                        .flatten()
852                        .copied(),
853                ) {
854                    let dependencies = self.tasks[task as usize]
855                        .dependencies
856                        .iter()
857                        .copied()
858                        .flat_map(|dependency| {
859                            if dependency == owner {
860                                predecessors.to_vec()
861                            } else {
862                                vec![dependency]
863                            }
864                        })
865                        .collect::<BTreeSet<_>>()
866                        .into_iter()
867                        .collect();
868                    self.tasks[task as usize].dependencies = dependencies;
869                }
870            }
871        }
872        Ok(())
873    }
874
875    fn assign_source_releases(&mut self) -> Result<(), LowerError> {
876        let reference_duration = self
877            .tasks
878            .iter()
879            .filter(|task| task.source == Some(SourceRole::Clifford))
880            .map(|task| task.duration)
881            .fold(0.0, f64::max);
882        for task in &mut self.tasks {
883            task.release = match task.source {
884                Some(SourceRole::Factory) => self.config.source_timing.factory,
885                Some(SourceRole::LogicalInput) => self.config.source_timing.input,
886                Some(SourceRole::Clifford) => self.config.source_timing.clifford,
887                Some(SourceRole::PreparedY) => {
888                    self.config.source_timing.clifford
889                        + (reference_duration - task.duration).max(0.0)
890                }
891                None => 0.0,
892            };
893            if !task.release.is_finite() {
894                return Err(LowerError::InvalidSourceTiming);
895            }
896        }
897        Ok(())
898    }
899
900    fn lower_inputs(&self) -> Result<Vec<LogicalInput>, LowerError> {
901        self.bloq
902            .logical_inputs()
903            .iter()
904            .map(|input| {
905                let (template_id, offset) = self
906                    .bloq
907                    .levels()
908                    .flat_map(|(_, level)| level.quantum_nodes())
909                    .flat_map(|(_, quantum)| quantum.instances.iter())
910                    .find(|instance| instance.id == input.instance)
911                    .map(|instance| (instance.template_id, instance.offset))
912                    .ok_or(LowerError::MissingInputInstance(input.instance.0))?;
913                let task = *self
914                    .instance_tasks
915                    .get(&input.instance)
916                    .ok_or(LowerError::MissingInputInstance(input.instance.0))?;
917                let stabilizers = self.bloq.templates()[template_id]
918                    .boundary_flows
919                    .iter()
920                    .filter(|flow| flow.start.is_empty() && !flow.end.is_empty())
921                    .map(|flow| {
922                        let mut product = self.pauli_product(&flow.end, offset)?;
923                        product.negative = flow.sign;
924                        Ok(product)
925                    })
926                    .collect::<Result<Vec<_>, LowerError>>()?;
927                let data_qubits = stabilizers
928                    .iter()
929                    .flat_map(|stabilizer| stabilizer.terms.iter().map(|(qubit, _)| *qubit))
930                    .collect::<BTreeSet<_>>()
931                    .into_iter()
932                    .collect::<Vec<_>>()
933                    .into_boxed_slice();
934                Ok(LogicalInput {
935                    port: input.port.to_array(),
936                    task,
937                    data_qubits,
938                    stabilizers: stabilizers.into_boxed_slice(),
939                    x: self.pauli_product(&input.x, IVec2::ZERO)?,
940                    z: self.pauli_product(&input.z, IVec2::ZERO)?,
941                })
942            })
943            .collect()
944    }
945
946    fn lower_level(
947        &mut self,
948        level: &SubGraph,
949        path: &LevelPath,
950        enclosing_rus: Option<ResourceId>,
951        decoder_round_duration: f64,
952        restart_source: Option<ValueRef>,
953    ) -> Result<LoweredLevel, LowerError> {
954        let implicit_demands = crate::closure::implicit_correction_demands(level, restart_source);
955        let mut nodes = FxHashMap::<BloqNodeId, LoweredNode>::default();
956        let mut stream = Vec::with_capacity(level.node_count());
957        let mut last_qubit = FxHashMap::<QubitId, TaskId>::default();
958        let mut discard_barrier = None;
959        let mut hoisted_here = Vec::new();
960
961        for id in level.deterministic_emit_order()? {
962            let node = &level[id];
963            let input_bits = self.input_bits(level, id, &nodes)?;
964            let mut activation = node
965                .activation
966                .map(|slot| self.required_slot(id, slot, &input_bits))
967                .transpose()?;
968            let mut dependencies = level
969                .incoming(id)
970                .filter_map(|edge| {
971                    nodes.get(&edge.source).map(|producer| match edge.edge {
972                        BloqEdge::Compose { .. } => producer.entry,
973                        _ => producer.exit,
974                    })
975                })
976                .collect::<BTreeSet<_>>();
977            if let Some(barrier) = discard_barrier {
978                dependencies.insert(barrier);
979            }
980            if self.dynamic_waits.contains_key(&(path.clone(), id))
981                && let Some(guard) = level.incoming(id).find_map(|edge| match edge.edge {
982                    BloqEdge::Quantum(quantum) => quantum.guard,
983                    BloqEdge::Value { .. } | BloqEdge::Compose { .. } | BloqEdge::Order => None,
984                })
985            {
986                let guard_output = guard.output;
987                let guard = nodes[&guard.node];
988                activation = Some(guard.output(guard_output).ok_or(LowerError::MissingBit {
989                    node: id.0,
990                    slot: guard.entry,
991                })?);
992                dependencies.insert(guard.exit);
993            }
994
995            let lowered = match &node.kind {
996                BloqNodeKind::Quantum(_) => {
997                    let wait = self.dynamic_waits.get(&(path.clone(), id)).cloned();
998                    let lowered = self.lower_quantum_node(
999                        level,
1000                        id,
1001                        path,
1002                        node,
1003                        dependencies,
1004                        activation,
1005                        &input_bits,
1006                        wait.is_some(),
1007                        &mut stream,
1008                        &mut last_qubit,
1009                    )?;
1010                    if let Some(wait) = wait {
1011                        self.set_task_origin(lowered.entry, wait.origin.clone());
1012                        if let Some(owner) = wait.hoist_after {
1013                            hoisted_here.push(lowered.entry);
1014                            self.hoisted_waits
1015                                .insert(owner, (lowered.entry, wait.siblings));
1016                        } else {
1017                            self.pending_waits.push((lowered.entry, wait.siblings));
1018                        }
1019                    }
1020                    lowered
1021                }
1022                BloqNodeKind::Classical(classical) => {
1023                    let classical = classical.as_ref();
1024                    if matches!(classical, ClassicalNode::Discard { .. }) {
1025                        dependencies.extend(stream.iter().copied());
1026                    }
1027                    let observable = matches!(classical, ClassicalNode::Observable { .. });
1028                    let decoded =
1029                        matches!(classical, ClassicalNode::Observable { index: Some(_), .. })
1030                            && crate::closure::needs_correction(level, id, &implicit_demands);
1031                    let output = produces_bit(classical)
1032                        .then(|| self.alloc_bit())
1033                        .transpose()?;
1034                    let flip = decoded.then(|| self.alloc_bit()).transpose()?;
1035                    let raw_output = if decoded {
1036                        Some(self.alloc_bit()?)
1037                    } else {
1038                        output
1039                    };
1040                    let mut local_tasks = Vec::new();
1041                    let instruction = match classical {
1042                        ClassicalNode::Compute { expr } => Instruction::Eval(
1043                            self.boolean_functions.lower(expr, &input_bits, id)?,
1044                        ),
1045                        ClassicalNode::Observable {
1046                            index,
1047                            measurements,
1048                            operators,
1049                        } => {
1050                            let mut bits = Vec::new();
1051                            let mut bindings = Vec::new();
1052                            for input in level.data_inputs(id) {
1053                                let producer = nodes[&input.producer];
1054                                if let Some(bit) = match input.output {
1055                                    None => producer.raw,
1056                                    Some(output) => producer.output(output),
1057                                } {
1058                                    bits.push(bit);
1059                                }
1060                                if input.output.is_none()
1061                                    && task_produces_bindings(
1062                                        &self.tasks[producer.entry as usize].instruction,
1063                                    )
1064                                {
1065                                    bindings.push(producer.entry);
1066                                }
1067                            }
1068                            if !measurements.is_empty() {
1069                                let bit = self.alloc_bit()?;
1070                                let parity = self.readout_parity(measurements)?;
1071                                let task = self.push_task(Task {
1072                                    label: task_label(path, id, "observable_measurements"),
1073                                    release: 0.0,
1074                                    source: None,
1075                                    dependencies: dependencies.iter().copied().collect(),
1076                                    activation,
1077                                    output: Some(bit),
1078                                    qubits: Box::new([]),
1079                                    duration: 0.0,
1080                                    instruction: Instruction::Accumulate(parity),
1081                                })?;
1082                                self.set_task_origin(
1083                                    task,
1084                                    task_origin(TaskFunction::Classical, std::iter::empty()),
1085                                );
1086                                stream.push(task);
1087                                local_tasks.push(task);
1088                                dependencies.insert(task);
1089                                bits.push(bit);
1090                            }
1091                            if !operators.is_empty() {
1092                                let boundaries = operators
1093                                    .iter()
1094                                    .map(|operator| {
1095                                        Ok(BoundaryBinding {
1096                                            input: operator.face == BoundaryFace::Input,
1097                                            operator: self
1098                                                .pauli_product(&operator.operator, IVec2::ZERO)?,
1099                                        })
1100                                    })
1101                                    .collect::<Result<Vec<_>, LowerError>>()?;
1102                                let task = self.push_task(Task {
1103                                    label: task_label(path, id, "observable_boundaries"),
1104                                    release: 0.0,
1105                                    source: None,
1106                                    dependencies: dependencies.iter().copied().collect(),
1107                                    activation,
1108                                    output: None,
1109                                    qubits: Box::new([]),
1110                                    duration: 0.0,
1111                                    instruction: Instruction::Bind(boundaries.into_boxed_slice()),
1112                                })?;
1113                                self.set_task_origin(
1114                                    task,
1115                                    task_origin(TaskFunction::Classical, std::iter::empty()),
1116                                );
1117                                stream.push(task);
1118                                local_tasks.push(task);
1119                                dependencies.insert(task);
1120                                bindings.push(task);
1121                            }
1122                            match index {
1123                                Some(index) => Instruction::Observable {
1124                                    index: *index,
1125                                    bits: bits.into_boxed_slice(),
1126                                    bindings: bindings.into_boxed_slice(),
1127                                },
1128                                None => Instruction::ReadoutRecipe {
1129                                    bits: bits.into_boxed_slice(),
1130                                    bindings: bindings.into_boxed_slice(),
1131                                },
1132                            }
1133                        }
1134                        ClassicalNode::Discard { condition } => Instruction::Discard(
1135                            self.boolean_functions.lower(condition, &input_bits, id)?,
1136                        ),
1137                    };
1138                    let task = self.push_task(Task {
1139                        label: task_label(path, id, "classical"),
1140                        release: 0.0,
1141                        source: None,
1142                        dependencies: dependencies.into_iter().collect(),
1143                        activation,
1144                        output: raw_output,
1145                        qubits: Box::new([]),
1146                        duration: 0.0,
1147                        instruction,
1148                    })?;
1149                    self.set_task_origin(
1150                        task,
1151                        task_origin(TaskFunction::Classical, std::iter::empty()),
1152                    );
1153                    stream.push(task);
1154                    let completed = if let ClassicalNode::Observable { index, .. } = classical {
1155                        let raw = raw_output.expect("observable allocates a raw bit");
1156                        self.observable_parts.insert(task, local_tasks);
1157                        if let (Some(index), Some(output), Some(flip)) = (index, output, flip) {
1158                            let request = DecodeRequest {
1159                                output: ObservableOutput::Corrected,
1160                                observable: task,
1161                                index: *index,
1162                                raw,
1163                                round_duration: decoder_round_duration,
1164                                timing: if enclosing_rus.is_some() {
1165                                    DecodeTiming::FactoryCompletion
1166                                } else {
1167                                    DecodeTiming::Measurement
1168                                },
1169                            };
1170                            let mut completed = task;
1171                            for (output, bit, role) in [
1172                                (ObservableOutput::Corrected, output, "decode"),
1173                                (ObservableOutput::Flip, flip, "flip"),
1174                            ] {
1175                                completed = self.push_task(Task {
1176                                    label: task_label(path, id, role),
1177                                    release: 0.0,
1178                                    source: None,
1179                                    dependencies: Box::new([task]),
1180                                    activation,
1181                                    output: Some(bit),
1182                                    qubits: Box::new([]),
1183                                    duration: 0.0,
1184                                    instruction: Instruction::Decode(DecodeRequest {
1185                                        output,
1186                                        ..request
1187                                    }),
1188                                })?;
1189                                self.set_task_origin(
1190                                    completed,
1191                                    task_origin(TaskFunction::Classical, std::iter::empty()),
1192                                );
1193                                stream.push(completed);
1194                            }
1195                            completed
1196                        } else {
1197                            task
1198                        }
1199                    } else {
1200                        task
1201                    };
1202                    if matches!(classical, ClassicalNode::Discard { .. }) {
1203                        discard_barrier = Some(task);
1204                    }
1205                    LoweredNode {
1206                        entry: task,
1207                        ready: completed,
1208                        exit: completed,
1209                        bit: output,
1210                        flip,
1211                        raw: if observable { raw_output } else { output },
1212                    }
1213                }
1214                BloqNodeKind::Region(region) => {
1215                    if level
1216                        .incoming(id)
1217                        .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
1218                    {
1219                        return Err(LowerError::NonSourceRus(id.0));
1220                    }
1221                    let before_bit = self.next_bit;
1222                    let before_record = self.next_record;
1223                    let RegionNode::RepeatUntilSuccess {
1224                        body: body_graph,
1225                        restart_condition: condition,
1226                        restart_source,
1227                    } = region;
1228                    let restart_source = *restart_source;
1229                    let resource = self.alloc_resource()?;
1230                    let child_path = path.child(id, BodySelector::Body);
1231                    let child_round_duration = self
1232                        .rus_round_duration(level, id)?
1233                        .unwrap_or(decoder_round_duration);
1234                    let lowered_body = self.lower_level(
1235                        body_graph,
1236                        &child_path,
1237                        Some(resource),
1238                        child_round_duration,
1239                        restart_source,
1240                    )?;
1241                    let cultivation_exits = body_graph
1242                        .quantum_nodes()
1243                        .filter_map(|(node, _)| {
1244                            let task = lowered_body.nodes.get(&node)?.exit;
1245                            matches!(
1246                                self.tasks[task as usize].instruction,
1247                                Instruction::Quantum(_)
1248                                    | Instruction::MemoryRounds { .. }
1249                                    | Instruction::Idle { .. }
1250                            )
1251                            .then_some(task)
1252                        })
1253                        .filter(|task| {
1254                            !body_graph.quantum_nodes().any(|(node, _)| {
1255                                let Some(candidate) = lowered_body.nodes.get(&node) else {
1256                                    return false;
1257                                };
1258                                matches!(
1259                                    self.tasks[candidate.entry as usize].instruction,
1260                                    Instruction::Quantum(_)
1261                                        | Instruction::MemoryRounds { .. }
1262                                        | Instruction::Idle { .. }
1263                                ) && candidate.entry != *task
1264                                    && self.tasks[candidate.entry as usize]
1265                                        .dependencies
1266                                        .contains(task)
1267                            })
1268                        })
1269                        .collect::<Vec<_>>()
1270                        .into_boxed_slice();
1271                    let physical_rus = !cultivation_exits.is_empty();
1272                    if physical_rus {
1273                        for &task in &lowered_body.stream.tasks {
1274                            if matches!(
1275                                self.tasks[task as usize].instruction,
1276                                Instruction::Quantum(_)
1277                                    | Instruction::MemoryRounds { .. }
1278                                    | Instruction::Idle { .. }
1279                            ) {
1280                                self.task_origins[task as usize].function =
1281                                    TaskFunction::FactoryBody;
1282                            }
1283                        }
1284                    }
1285                    let region_origin = merge_origins(
1286                        if physical_rus {
1287                            TaskFunction::Factory
1288                        } else {
1289                            TaskFunction::Control
1290                        },
1291                        lowered_body
1292                            .stream
1293                            .tasks
1294                            .iter()
1295                            .map(|&task| &self.task_origins[task as usize]),
1296                    );
1297                    let body = lowered_body.stream;
1298                    let output = Some(self.alloc_bit()?);
1299                    let decoder_hold = self
1300                        .hoisted_waits
1301                        .get(&(path.clone(), id))
1302                        .and_then(|(task, _)| match &self.tasks[*task as usize].instruction {
1303                            Instruction::WaitFor { memory, .. } => Some(memory.clone()),
1304                            _ => None,
1305                        })
1306                        .unwrap_or_default();
1307                    if physical_rus && decoder_hold.is_empty() {
1308                        return Err(LowerError::MissingDecoderHold(id.0));
1309                    }
1310                    let source_bit = restart_source
1311                        .map(|source| {
1312                            lowered_body
1313                                .nodes
1314                                .get(&source.node)
1315                                .and_then(|node| node.output(source.output))
1316                                .ok_or(LowerError::MissingBit {
1317                                    node: id.0,
1318                                    slot: source.node.0,
1319                                })
1320                        })
1321                        .transpose()?;
1322                    let mut restart_inputs = input_bits.clone();
1323                    let mut slots = Vec::new();
1324                    condition.for_each_input(&mut |slot| slots.push(slot));
1325                    if let Some(source_bit) = source_bit {
1326                        for slot in slots {
1327                            restart_inputs.entry(slot).or_insert(source_bit);
1328                        }
1329                    }
1330                    let owned_qubits: Box<[QubitId]> = body
1331                        .tasks
1332                        .iter()
1333                        .flat_map(|&task| self.tasks[task as usize].qubits.iter().copied())
1334                        .collect::<BTreeSet<_>>()
1335                        .into_iter()
1336                        .collect::<Vec<_>>()
1337                        .into_boxed_slice();
1338                    let qubits = owned_qubits.clone();
1339                    let instruction = Instruction::Rus {
1340                        body,
1341                        restart: self
1342                            .boolean_functions
1343                            .lower(condition, &restart_inputs, id)?,
1344                        owned_qubits,
1345                        attempt_bits: (before_bit..self.next_bit).collect(),
1346                        attempt_records: (before_record..self.next_record).collect(),
1347                        resource,
1348                        retry_prepare: QuantumStream::default(),
1349                        decoder_hold,
1350                        cultivation_exits,
1351                    };
1352                    // A supported RUS is a true source: it has no incoming
1353                    // seam (checked above) and no earlier task owns its support.
1354                    // Its first attempt therefore starts on fresh backend |0>
1355                    // qubits; later attempts replay the authored preparation.
1356                    if qubits.iter().any(|qubit| last_qubit.contains_key(qubit)) {
1357                        return Err(LowerError::NonSourceRus(id.0));
1358                    }
1359                    let task = self.push_task(Task {
1360                        label: task_label(path, id, region.kind_name()),
1361                        release: 0.0,
1362                        source: physical_rus.then_some(SourceRole::Factory),
1363                        dependencies: dependencies.into_iter().collect(),
1364                        activation,
1365                        output,
1366                        qubits: qubits.clone(),
1367                        duration: 0.0,
1368                        instruction,
1369                    })?;
1370                    self.set_task_origin(task, region_origin);
1371                    stream.push(task);
1372                    let mut exit = {
1373                        let signal = self.push_task(Task {
1374                            label: task_label(path, id, "ready"),
1375                            release: 0.0,
1376                            source: None,
1377                            dependencies: Box::new([task]),
1378                            activation: None,
1379                            output: None,
1380                            qubits: Box::new([]),
1381                            duration: 0.0,
1382                            instruction: Instruction::SignalReady { resource },
1383                        })?;
1384                        self.set_task_origin(
1385                            signal,
1386                            task_origin(TaskFunction::Control, std::iter::empty()),
1387                        );
1388                        stream.push(signal);
1389                        signal
1390                    };
1391                    let ready = exit;
1392                    if let Some((template, siblings)) =
1393                        self.hoisted_waits.remove(&(path.clone(), id))
1394                    {
1395                        let mut hoisted = self.tasks[template as usize].clone();
1396                        let origin = self.task_origins[template as usize].clone();
1397                        hoisted.label = task_label(path, id, "accepted-join-wait");
1398                        hoisted.dependencies = Box::new([exit]);
1399                        let hoisted = self.push_task(hoisted)?;
1400                        self.set_task_origin(hoisted, origin);
1401                        stream.push(hoisted);
1402                        self.pending_waits.push((hoisted, siblings));
1403                        exit = hoisted;
1404                    }
1405                    for &qubit in qubits.iter() {
1406                        last_qubit.insert(qubit, exit);
1407                    }
1408                    LoweredNode {
1409                        entry: task,
1410                        ready,
1411                        exit,
1412                        bit: output,
1413                        flip: None,
1414                        raw: output,
1415                    }
1416                }
1417            };
1418            nodes.insert(id, lowered);
1419            self.node_tasks.insert((path.clone(), id), lowered);
1420        }
1421
1422        for removed in &hoisted_here {
1423            let replacements = self.tasks[*removed as usize].dependencies.clone();
1424            for &task in &stream {
1425                if task == *removed {
1426                    continue;
1427                }
1428                let dependencies = &mut self.tasks[task as usize].dependencies;
1429                if dependencies.contains(removed) {
1430                    let mut rewritten = dependencies
1431                        .iter()
1432                        .copied()
1433                        .filter(|dependency| dependency != removed)
1434                        .chain(replacements.iter().copied())
1435                        .collect::<BTreeSet<_>>();
1436                    *dependencies = std::mem::take(&mut rewritten).into_iter().collect();
1437                }
1438            }
1439        }
1440        stream.retain(|task| !hoisted_here.contains(task));
1441        self.align_level(level, &nodes)?;
1442
1443        Ok(LoweredLevel {
1444            stream: Stream {
1445                tasks: stream.into_boxed_slice(),
1446                value: level
1447                    .value_output()
1448                    .and_then(|value| nodes[&value.node].output(value.output)),
1449                bindings: level
1450                    .boundary_outputs()
1451                    .iter()
1452                    .map(|node| nodes[node].entry)
1453                    .collect(),
1454            },
1455            nodes,
1456        })
1457    }
1458
1459    #[expect(
1460        clippy::too_many_arguments,
1461        reason = "one node lowering carries its graph context"
1462    )]
1463    fn lower_quantum_node(
1464        &mut self,
1465        level: &SubGraph,
1466        id: BloqNodeId,
1467        path: &LevelPath,
1468        node: &BloqNode,
1469        mut dependencies: BTreeSet<TaskId>,
1470        activation: Option<BitId>,
1471        input_bits: &FxHashMap<u32, BitId>,
1472        dynamic_wait: bool,
1473        stream: &mut Vec<TaskId>,
1474        last_qubit: &mut FxHashMap<QubitId, TaskId>,
1475    ) -> Result<LoweredNode, LowerError> {
1476        self.bloq
1477            .check_detector_bundle_bindings(node.expect_quantum(), |instance| {
1478                self.instance_templates.get(&instance).copied()
1479            })?;
1480        let mut alternatives = self.quantum_alternatives(level, path, id, node, input_bits)?;
1481        let top_level_source = *path == LevelPath::default()
1482            && !level
1483                .incoming(id)
1484                .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)));
1485        let input_instances = self
1486            .bloq
1487            .logical_inputs()
1488            .iter()
1489            .map(|input| input.instance)
1490            .collect::<BTreeSet<_>>();
1491        let output_instances = self
1492            .bloq
1493            .logical_outputs()
1494            .iter()
1495            .map(|output| output.instance)
1496            .collect::<BTreeSet<_>>();
1497        let quantum = node.expect_quantum();
1498        let owns_input = quantum
1499            .instances
1500            .iter()
1501            .any(|instance| input_instances.contains(&instance.id));
1502        let prepares_y = alternatives.iter().any(|alternative| {
1503            alternative.stream.moments.iter().any(|moment| {
1504                moment.operations.iter().any(|operation| {
1505                    matches!(
1506                        operation,
1507                        QuantumOp::Reset {
1508                            basis: Pauli::Y,
1509                            ..
1510                        }
1511                    )
1512                })
1513            })
1514        });
1515        let source = top_level_source.then_some(if owns_input {
1516            SourceRole::LogicalInput
1517        } else if prepares_y {
1518            SourceRole::PreparedY
1519        } else {
1520            SourceRole::Clifford
1521        });
1522        let mut origin = self.quantum_origin(node, source);
1523        if matches!(node.provenance, NodeProvenance::MemoryPadding { .. }) {
1524            let pipes = level
1525                .incoming(id)
1526                .chain(level.outgoing(id))
1527                .flat_map(|edge| edge.edge.pipes())
1528                .map(|pipe| pipe.pipe)
1529                .collect::<Vec<_>>();
1530            if !pipes.is_empty() {
1531                origin = pipe_origin(pipes);
1532            }
1533        }
1534        if !quantum.instances.is_empty()
1535            && quantum.instances.iter().all(|instance| {
1536                input_instances.contains(&instance.id) || output_instances.contains(&instance.id)
1537            })
1538        {
1539            for alternative in &mut alternatives {
1540                for moment in &mut alternative.stream.moments {
1541                    moment.duration = 0.0;
1542                }
1543            }
1544        }
1545        let qubits = alternatives
1546            .iter()
1547            .flat_map(|alternative| alternative.stream.moments.iter())
1548            .flat_map(|moment| moment.operations.iter())
1549            .flat_map(op_qubits)
1550            .collect::<BTreeSet<_>>();
1551        for qubit in &qubits {
1552            if let Some(previous) = last_qubit.get(qubit) {
1553                dependencies.insert(*previous);
1554            }
1555        }
1556        let duration = alternatives
1557            .iter()
1558            .try_fold(0.0_f64, |longest, alternative| {
1559                stream_duration(&alternative.stream).map(|duration| longest.max(duration))
1560            })?;
1561        let rounds = match node.provenance {
1562            NodeProvenance::MemoryPadding { rounds, .. } => Some(rounds),
1563            _ => None,
1564        };
1565        let main_instruction = match (dynamic_wait, &node.provenance, alternatives.as_slice()) {
1566            (true, _, [alternative]) => Instruction::WaitFor {
1567                until: Box::new([]),
1568                memory: Box::new([self.lower_inserted_memory_cycle(node, alternative)?]),
1569            },
1570            (false, NodeProvenance::MemoryPadding { .. }, [alternative]) => {
1571                Instruction::MemoryRounds {
1572                    rounds: rounds.expect("matched padding provenance"),
1573                    stream: alternative.stream.clone(),
1574                    detectors: alternative.detectors.clone(),
1575                }
1576            }
1577            (false, _, _) => Instruction::Quantum(QuantumTask {
1578                alternatives: alternatives.into_boxed_slice(),
1579            }),
1580            (true, _, _) => return Err(LowerError::Unflattened("guarded dynamic memory")),
1581        };
1582        let main = self.push_task(Task {
1583            label: task_label(path, id, "quantum"),
1584            release: 0.0,
1585            source,
1586            dependencies: dependencies.into_iter().collect(),
1587            activation,
1588            output: None,
1589            qubits: qubits.iter().copied().collect(),
1590            duration: if dynamic_wait { 0.0 } else { duration },
1591            instruction: main_instruction,
1592        })?;
1593        self.set_task_origin(main, origin);
1594        for instance in &quantum.instances {
1595            self.instance_tasks.insert(instance.id, main);
1596        }
1597        stream.push(main);
1598        for qubit in &qubits {
1599            last_qubit.insert(*qubit, main);
1600        }
1601        Ok(LoweredNode {
1602            entry: main,
1603            ready: main,
1604            exit: main,
1605            bit: None,
1606            flip: None,
1607            raw: None,
1608        })
1609    }
1610
1611    fn quantum_alternatives(
1612        &mut self,
1613        level: &SubGraph,
1614        path: &LevelPath,
1615        id: BloqNodeId,
1616        node: &BloqNode,
1617        inputs: &FxHashMap<u32, BitId>,
1618    ) -> Result<Vec<QuantumAlternative>, LowerError> {
1619        let quantum = node.expect_quantum();
1620        if quantum.guards.is_empty() {
1621            let plan = match self.plans.get(path, id) {
1622                Some(plan) => plan.clone(),
1623                None => node.emission_plan_with_options(self.bloq.templates(), self.options)?,
1624            };
1625            return Ok(vec![self.lower_alternative(node, &plan, Box::new([]))?]);
1626        }
1627        let slots = quantum
1628            .guards
1629            .iter()
1630            .map(|guard| guard.input)
1631            .collect::<BTreeSet<_>>()
1632            .into_iter()
1633            .collect::<Vec<_>>();
1634        let producers = level
1635            .value_inputs(id)
1636            .map(|input| (input.slot, input.producer))
1637            .collect::<FxHashMap<_, _>>();
1638        let predicates = slots
1639            .iter()
1640            .map(|slot| {
1641                producers.get(slot).copied().ok_or(
1642                    NodeTemplateInstanceMergeError::MissingMembershipInput(*slot),
1643                )
1644            })
1645            .collect::<Result<Vec<_>, _>>()?;
1646        let mut assignments = bloq_ir::lowering::PredicateAnalysis::new(level).values_up_to(
1647            &predicates,
1648            self.config.max_quantum_variants.saturating_add(1),
1649        )?;
1650        let variants = assignments.len();
1651        if variants > self.config.max_quantum_variants {
1652            return Err(LowerError::QuantumVariantLimit {
1653                node: id.0,
1654                guards: slots.len(),
1655                variants,
1656                limit: self.config.max_quantum_variants,
1657            });
1658        }
1659        // Retain the old truth-table order: the first slot varies fastest.
1660        assignments.sort_unstable_by(|left, right| left.iter().rev().cmp(right.iter().rev()));
1661        let mut out = Vec::new();
1662        out.try_reserve_exact(variants)
1663            .map_err(|source| LowerError::QuantumVariantAllocation { variants, source })?;
1664        for assignment in assignments {
1665            let values = slots
1666                .iter()
1667                .copied()
1668                .zip(assignment)
1669                .collect::<FxHashMap<_, _>>();
1670            let selected = node.select_quantum_members(|slot| values.get(&slot).copied())?;
1671            let plan = selected.emission_plan_with_options(self.bloq.templates(), self.options)?;
1672            let when = slots
1673                .iter()
1674                .map(|slot| Ok((self.required_slot(id, *slot, inputs)?, values[slot])))
1675                .collect::<Result<Vec<_>, LowerError>>()?
1676                .into_boxed_slice();
1677            out.push(self.lower_alternative(&selected, &plan, when)?);
1678        }
1679        let all_records = out
1680            .iter()
1681            .flat_map(|alternative| alternative.selected_records.iter().copied())
1682            .collect::<BTreeSet<_>>();
1683        for alternative in &mut out {
1684            let selected = alternative
1685                .selected_records
1686                .iter()
1687                .copied()
1688                .collect::<BTreeSet<_>>();
1689            alternative.excluded_records = all_records.difference(&selected).copied().collect();
1690        }
1691        Ok(out)
1692    }
1693
1694    fn lower_alternative(
1695        &mut self,
1696        node: &BloqNode,
1697        plan: &NodeEmissionPlan,
1698        when: Box<[(BitId, bool)]>,
1699    ) -> Result<QuantumAlternative, LowerError> {
1700        let grouped = plan.grouped_measurements();
1701        let mut local_records = FxHashMap::<u32, Box<[RecordId]>>::default();
1702        for (local, sources) in grouped {
1703            let records = sources
1704                .into_iter()
1705                .map(|source| self.record(source))
1706                .collect::<Result<Vec<_>, _>>()?;
1707            local_records.insert(local, records.into_boxed_slice());
1708        }
1709        let stream = self.lower_ops(
1710            plan.circuit
1711                .body(plan.circuit.entry_body())
1712                .expect("emission plan entry body")
1713                .ops(),
1714            IVec2::ZERO,
1715            &local_records,
1716        )?;
1717        let quantum = node.expect_quantum();
1718        let templates = plan.templates(self.bloq.templates());
1719        let mut detectors = Vec::new();
1720        let mut restarts = Vec::new();
1721        for instance in &quantum.instances {
1722            let template = &templates[instance.template_id];
1723            for detector in &template.detectors {
1724                if detector.scope != TemplateDetectorScope::TopLevel {
1725                    return Err(LowerError::Unflattened("repeat detector"));
1726                }
1727                detectors.push(self.template_parity(instance.id, &detector.parity)?);
1728            }
1729            restarts.extend(
1730                template
1731                    .restarts
1732                    .iter()
1733                    .map(|restart| self.template_parity(instance.id, &restart.parity))
1734                    .collect::<Result<Vec<_>, _>>()?,
1735            );
1736        }
1737        detectors.extend(
1738            self.bloq
1739                .node_detectors(quantum)?
1740                .map(|detector| self.node_parity(&detector.parity()))
1741                .collect::<Result<Vec<_>, _>>()?,
1742        );
1743        restarts.extend(
1744            quantum
1745                .restarts
1746                .iter()
1747                .map(|restart| self.node_parity(&restart.parity))
1748                .collect::<Result<Vec<_>, _>>()?,
1749        );
1750        Ok(QuantumAlternative {
1751            when,
1752            stream,
1753            selected_records: local_records
1754                .values()
1755                .flat_map(|records| records.iter().copied())
1756                .collect::<BTreeSet<_>>()
1757                .into_iter()
1758                .collect(),
1759            excluded_records: Box::new([]),
1760            detectors: detectors.into_boxed_slice(),
1761            restarts: restarts.into_boxed_slice(),
1762        })
1763    }
1764
1765    fn lower_ops(
1766        &self,
1767        ops: &[Op],
1768        offset: IVec2,
1769        records: &FxHashMap<u32, Box<[RecordId]>>,
1770    ) -> Result<QuantumStream, LowerError> {
1771        let mut moments = Vec::new();
1772        let mut current = Vec::new();
1773        let mut current_kind = None;
1774        let mut timed = false;
1775        let flush = |current: &mut Vec<QuantumOp>,
1776                     current_kind: &mut Option<MomentKind>,
1777                     timed: &mut bool,
1778                     moments: &mut Vec<Moment>| {
1779            if !current.is_empty() {
1780                moments.push(Moment {
1781                    kind: current_kind.take(),
1782                    duration: if *timed {
1783                        self.config.gate_duration
1784                    } else {
1785                        0.0
1786                    },
1787                    operations: std::mem::take(current).into_boxed_slice(),
1788                });
1789            } else if *timed {
1790                moments.push(Moment {
1791                    kind: current_kind.take(),
1792                    duration: self.config.gate_duration,
1793                    operations: Box::new([]),
1794                });
1795            }
1796            *current_kind = None;
1797            *timed = false;
1798        };
1799        for op in ops {
1800            if let Some(kind) = vm_moment_kind(op) {
1801                if current_kind.is_some_and(|current| current != kind) {
1802                    flush(&mut current, &mut current_kind, &mut timed, &mut moments);
1803                }
1804                current_kind = Some(kind);
1805            }
1806            match op {
1807                Op::Tick => flush(&mut current, &mut current_kind, &mut timed, &mut moments),
1808                Op::Repeat { .. } => return Err(LowerError::Unflattened("circuit repeat")),
1809                Op::Gate { gate, qubits } if gate.is_two_qubit_gate() => {
1810                    timed = true;
1811                    let (control_basis, target_basis) = gate
1812                        .two_qubit_bases()
1813                        .expect("two-qubit gate has Pauli bases");
1814                    for pair in qubits.as_chunks::<2>().0 {
1815                        current.push(QuantumOp::Gate2 {
1816                            control_basis: pauli(control_basis),
1817                            target_basis: pauli(target_basis),
1818                            control: self.qubit(pair[0], offset)?,
1819                            target: self.qubit(pair[1], offset)?,
1820                        });
1821                    }
1822                }
1823                Op::Gate { gate, qubits } => {
1824                    timed = true;
1825                    for &qubit in qubits {
1826                        let qubit = self.qubit(qubit, offset)?;
1827                        if let Some(op) = gate1(*gate, qubit) {
1828                            current.push(op);
1829                        }
1830                    }
1831                }
1832                Op::Measure {
1833                    basis,
1834                    qubits,
1835                    measurements,
1836                    flip_probability,
1837                } => {
1838                    timed = true;
1839                    for (&qubit, measurement) in qubits.iter().zip(measurements) {
1840                        current.push(QuantumOp::Measure {
1841                            observable: PauliProduct {
1842                                negative: false,
1843                                terms: Box::new([(self.qubit(qubit, offset)?, pauli(*basis))]),
1844                            },
1845                            records: records[measurement].clone(),
1846                            flip_probability: *flip_probability,
1847                        });
1848                    }
1849                }
1850                Op::MPP {
1851                    products,
1852                    measurements,
1853                } => {
1854                    timed = true;
1855                    for (product, measurement) in products.iter().zip(measurements) {
1856                        current.push(QuantumOp::Measure {
1857                            observable: self.pauli_product(product, offset)?,
1858                            records: records[measurement].clone(),
1859                            flip_probability: 0.0,
1860                        });
1861                    }
1862                }
1863                Op::ConditionalPauli(corrections) => {
1864                    timed |= !corrections.is_empty();
1865                    for correction in corrections {
1866                        current.push(QuantumOp::ConditionalPauli {
1867                            basis: pauli(correction.pauli),
1868                            qubit: self.qubit(correction.target, offset)?,
1869                            control: records[&correction.control][0],
1870                        });
1871                    }
1872                }
1873                Op::Depolarize1 {
1874                    probability,
1875                    qubits,
1876                } => current.push(QuantumOp::Depolarize1 {
1877                    probability: *probability,
1878                    qubits: qubits
1879                        .iter()
1880                        .map(|&qubit| self.qubit(qubit, offset))
1881                        .collect::<Result<Vec<_>, _>>()?
1882                        .into_boxed_slice(),
1883                }),
1884                Op::Depolarize2 {
1885                    probability,
1886                    qubits,
1887                } => current.push(QuantumOp::Depolarize2 {
1888                    probability: *probability,
1889                    pairs: qubits
1890                        .as_chunks::<2>()
1891                        .0
1892                        .iter()
1893                        .map(|pair| {
1894                            Ok((self.qubit(pair[0], offset)?, self.qubit(pair[1], offset)?))
1895                        })
1896                        .collect::<Result<Vec<_>, LowerError>>()?
1897                        .into_boxed_slice(),
1898                }),
1899                Op::PauliError {
1900                    probability,
1901                    pauli: basis,
1902                    qubits,
1903                } => current.push(QuantumOp::PauliError {
1904                    probability: *probability,
1905                    basis: pauli(*basis),
1906                    qubits: qubits
1907                        .iter()
1908                        .map(|&qubit| self.qubit(qubit, offset))
1909                        .collect::<Result<Vec<_>, _>>()?
1910                        .into_boxed_slice(),
1911                }),
1912            }
1913        }
1914        flush(&mut current, &mut current_kind, &mut timed, &mut moments);
1915        let mut compact: Vec<Moment> = Vec::with_capacity(moments.len());
1916        let mut leading_noise = Vec::new();
1917        for mut moment in moments {
1918            if moment.kind.is_none() && moment.duration == 0.0 {
1919                if let Some(previous) = compact.last_mut() {
1920                    previous.operations = std::mem::take(&mut previous.operations)
1921                        .into_vec()
1922                        .into_iter()
1923                        .chain(moment.operations.into_vec())
1924                        .collect();
1925                } else {
1926                    leading_noise.extend(moment.operations.into_vec());
1927                }
1928                continue;
1929            }
1930            if !leading_noise.is_empty() {
1931                moment.operations = std::mem::take(&mut leading_noise)
1932                    .into_iter()
1933                    .chain(moment.operations.into_vec())
1934                    .collect();
1935            }
1936            compact.push(moment);
1937        }
1938        if !leading_noise.is_empty() {
1939            compact.push(Moment {
1940                kind: None,
1941                duration: 0.0,
1942                operations: leading_noise.into_boxed_slice(),
1943            });
1944        }
1945        moments = compact;
1946        if self.config.noise.is_none()
1947            && !ops.iter().any(|op| {
1948                matches!(
1949                    op,
1950                    Op::Depolarize1 { .. } | Op::Depolarize2 { .. } | Op::PauliError { .. }
1951                )
1952            })
1953        {
1954            let authored = bloq_ir::aligned_moment_segments(ops)?
1955                .into_iter()
1956                .filter_map(|segment| segment.map(|segment| segment.kind))
1957                .collect::<Vec<_>>();
1958            let lowered = moments
1959                .iter()
1960                .filter_map(|moment| moment.kind.map(ir_moment_kind))
1961                .collect::<Vec<_>>();
1962            if authored != lowered {
1963                return Err(LowerError::Unflattened("moment segmentation drift"));
1964            }
1965        }
1966        Ok(QuantumStream {
1967            moments: moments.into_boxed_slice(),
1968        })
1969    }
1970
1971    fn lower_inserted_memory_cycle(
1972        &self,
1973        node: &BloqNode,
1974        alternative: &QuantumAlternative,
1975    ) -> Result<MemoryCycle, LowerError> {
1976        let selected = alternative
1977            .selected_records
1978            .iter()
1979            .copied()
1980            .collect::<BTreeSet<_>>();
1981        let mut initializers = Vec::new();
1982        for detector in alternative.detectors.iter() {
1983            let owned = detector
1984                .records
1985                .iter()
1986                .copied()
1987                .filter(|record| selected.contains(record))
1988                .collect::<Vec<_>>();
1989            match owned.as_slice() {
1990                [] => {}
1991                [record] => initializers.push(FrontierInitializer {
1992                    record: *record,
1993                    parity: RecordParity {
1994                        records: detector
1995                            .records
1996                            .iter()
1997                            .copied()
1998                            .filter(|other| other != record)
1999                            .collect(),
2000                        constant: detector.constant,
2001                    },
2002                }),
2003                _ => return Err(LowerError::InvalidFrontier(owned.len())),
2004            }
2005        }
2006
2007        let mut boundary_flows = Vec::new();
2008        for instance in &node.expect_quantum().instances {
2009            let template = &self.bloq.templates()[instance.template_id];
2010            for flow in &template.boundary_flows {
2011                let parity = self.instance_parity(
2012                    flow.measurements
2013                        .iter()
2014                        .map(|&measurement| InstanceMeasurement {
2015                            instance: instance.id,
2016                            measurement,
2017                        }),
2018                    flow.sign,
2019                )?;
2020                boundary_flows.push(BoundaryFlow {
2021                    start: self.pauli_product(&flow.start, instance.offset)?,
2022                    end: self.pauli_product(&flow.end, instance.offset)?,
2023                    frontier_record: (parity.records.len() == 1).then_some(parity.records[0]),
2024                    parity,
2025                });
2026            }
2027        }
2028        Ok(MemoryCycle {
2029            round_duration: stream_duration(&alternative.stream)?,
2030            stream: alternative.stream.clone(),
2031            detectors: alternative.detectors.clone(),
2032            boundary_flows: boundary_flows.into_boxed_slice(),
2033            initializers: initializers.into_boxed_slice(),
2034        })
2035    }
2036
2037    fn rus_round_duration(
2038        &self,
2039        level: &SubGraph,
2040        region: BloqNodeId,
2041    ) -> Result<Option<f64>, LowerError> {
2042        let mut duration = None::<f64>;
2043        for padding in level
2044            .outgoing(region)
2045            .flat_map(|edge| edge.edge.pipes())
2046            .filter_map(|seam| seam.padding)
2047        {
2048            let total = self.one_round_duration(padding)?;
2049            duration = Some(duration.map_or(total, |old| old.max(total)));
2050        }
2051        Ok(duration)
2052    }
2053
2054    fn one_round_duration(&self, padding: PipePadding) -> Result<f64, LowerError> {
2055        let template = &self.bloq.templates()[padding.one_round];
2056        let ops = template
2057            .circuit
2058            .body(template.circuit.entry_body())
2059            .expect("one-round memory template has an entry body")
2060            .ops();
2061        let mut moment_timed = false;
2062        let mut total = 0.0;
2063        for op in ops {
2064            match op {
2065                Op::Tick => {
2066                    if moment_timed {
2067                        total = add_duration(total, self.config.gate_duration)?;
2068                    }
2069                    moment_timed = false;
2070                }
2071                Op::Repeat { .. } => {
2072                    return Err(LowerError::Unflattened("one-round memory template"));
2073                }
2074                Op::Gate { .. } | Op::Measure { .. } | Op::MPP { .. } => moment_timed = true,
2075                Op::ConditionalPauli(corrections) => {
2076                    moment_timed |= !corrections.is_empty();
2077                }
2078                Op::Depolarize1 { .. } | Op::Depolarize2 { .. } | Op::PauliError { .. } => {}
2079            }
2080        }
2081        if moment_timed {
2082            total = add_duration(total, self.config.gate_duration)?;
2083        }
2084        Ok(total)
2085    }
2086
2087    fn input_bits(
2088        &self,
2089        level: &SubGraph,
2090        id: BloqNodeId,
2091        nodes: &FxHashMap<BloqNodeId, LoweredNode>,
2092    ) -> Result<FxHashMap<u32, BitId>, LowerError> {
2093        level
2094            .value_inputs(id)
2095            .map(|input| {
2096                nodes
2097                    .get(&input.producer)
2098                    .and_then(|node| node.output(input.output.expect("value port")))
2099                    .map(|bit| (input.slot, bit))
2100                    .ok_or(LowerError::MissingBit {
2101                        node: id.0,
2102                        slot: input.slot,
2103                    })
2104            })
2105            .collect()
2106    }
2107
2108    fn required_slot(
2109        &self,
2110        node: BloqNodeId,
2111        slot: u32,
2112        inputs: &FxHashMap<u32, BitId>,
2113    ) -> Result<BitId, LowerError> {
2114        inputs
2115            .get(&slot)
2116            .copied()
2117            .ok_or(LowerError::MissingBit { node: node.0, slot })
2118    }
2119
2120    fn push_task(&mut self, task: Task) -> Result<TaskId, LowerError> {
2121        let id = u32::try_from(self.tasks.len()).map_err(|_| LowerError::IdOverflow("task"))?;
2122        self.tasks.push(task);
2123        self.task_origins.push(TaskOrigin::default());
2124        Ok(id)
2125    }
2126
2127    fn set_task_origin(&mut self, task: TaskId, origin: TaskOrigin) {
2128        self.task_origins[task as usize] = origin;
2129    }
2130
2131    fn alloc_bit(&mut self) -> Result<BitId, LowerError> {
2132        let id = self.next_bit;
2133        self.next_bit = id.checked_add(1).ok_or(LowerError::IdOverflow("bit"))?;
2134        Ok(id)
2135    }
2136
2137    fn alloc_resource(&mut self) -> Result<ResourceId, LowerError> {
2138        let id = self.next_resource;
2139        self.next_resource = id
2140            .checked_add(1)
2141            .ok_or(LowerError::IdOverflow("resource"))?;
2142        Ok(id)
2143    }
2144
2145    fn record(&mut self, source: InstanceMeasurement) -> Result<RecordId, LowerError> {
2146        if let Some(&record) = self.records.get(&source) {
2147            return Ok(record);
2148        }
2149        let record = self.next_record;
2150        self.next_record = record
2151            .checked_add(1)
2152            .ok_or(LowerError::IdOverflow("record"))?;
2153        self.records.insert(source, record);
2154        Ok(record)
2155    }
2156
2157    fn lookup_record(&self, source: InstanceMeasurement) -> Result<RecordId, LowerError> {
2158        self.records
2159            .get(&source)
2160            .copied()
2161            .ok_or(LowerError::MissingRecord {
2162                instance: source.instance.0,
2163                measurement: source.measurement,
2164            })
2165    }
2166
2167    fn instance_parity(
2168        &self,
2169        measurements: impl IntoIterator<Item = InstanceMeasurement>,
2170        constant: bool,
2171    ) -> Result<RecordParity, LowerError> {
2172        Ok(RecordParity {
2173            records: measurements
2174                .into_iter()
2175                .map(|measurement| self.lookup_record(measurement))
2176                .collect::<Result<Vec<_>, _>>()?
2177                .into(),
2178            constant,
2179        })
2180    }
2181
2182    fn readout_parity(
2183        &mut self,
2184        records: &[InstanceMeasurement],
2185    ) -> Result<RecordParity, LowerError> {
2186        if let Some(parity) = self.readout_parities.get(records) {
2187            return Ok(parity.clone());
2188        }
2189        let parity = self.instance_parity(records.iter().copied(), false)?;
2190        self.readout_parities
2191            .insert(records.to_vec(), parity.clone());
2192        Ok(parity)
2193    }
2194
2195    fn template_parity(
2196        &self,
2197        instance: TemplateInstanceId,
2198        parity: &DetectorParity,
2199    ) -> Result<RecordParity, LowerError> {
2200        let mut measurements = Vec::new();
2201        for term in parity.terms() {
2202            match *term {
2203                DetectorTerm::Measurement(measurement) => measurements.push(InstanceMeasurement {
2204                    instance,
2205                    measurement,
2206                }),
2207                DetectorTerm::LoopState(_) => {
2208                    return Err(LowerError::Unflattened("loop-carried detector state"));
2209                }
2210            }
2211        }
2212        self.instance_parity(measurements, parity.sign())
2213    }
2214
2215    fn node_parity(&self, parity: &NodeDetectorParity) -> Result<RecordParity, LowerError> {
2216        let mut measurements = Vec::new();
2217        for term in parity.terms() {
2218            match *term {
2219                DetectorTerm::Measurement(measurement) => measurements.push(measurement),
2220                DetectorTerm::LoopState(_) => {
2221                    return Err(LowerError::Unflattened("loop-carried detector state"));
2222                }
2223            }
2224        }
2225        self.instance_parity(measurements, parity.sign())
2226    }
2227
2228    fn qubit(&self, coordinate: IVec2, offset: IVec2) -> Result<QubitId, LowerError> {
2229        let coordinate = bloq_ir::circuit::checked_translate_coordinate(coordinate, offset)?;
2230        self.coords
2231            .get(&coordinate)
2232            .copied()
2233            .ok_or(CoordinateOverflowError { coordinate, offset }.into())
2234    }
2235
2236    fn pauli_product(&self, product: &PauliMap, offset: IVec2) -> Result<PauliProduct, LowerError> {
2237        Ok(PauliProduct {
2238            negative: false,
2239            terms: product
2240                .iter()
2241                .filter(|(_, basis)| **basis != bloq_ir::circuit::Pauli::I)
2242                .map(|(coordinate, basis)| {
2243                    Ok((self.qubit(*coordinate, offset)?, pauli_map_basis(*basis)))
2244                })
2245                .collect::<Result<Vec<_>, LowerError>>()?
2246                .into_boxed_slice(),
2247        })
2248    }
2249}
2250
2251fn produces_bit(node: &ClassicalNode) -> bool {
2252    !matches!(node, ClassicalNode::Discard { .. })
2253}
2254
2255fn task_label(path: &LevelPath, node: BloqNodeId, role: &str) -> String {
2256    format!("{path:?}/n{}:{role}", node.0)
2257}
2258
2259fn add_duration(total: f64, duration: f64) -> Result<f64, LowerError> {
2260    let total = total + duration;
2261    total
2262        .is_finite()
2263        .then_some(total)
2264        .ok_or(LowerError::InvalidDuration)
2265}
2266
2267fn stream_duration(stream: &QuantumStream) -> Result<f64, LowerError> {
2268    stream
2269        .moments
2270        .iter()
2271        .try_fold(0.0, |total, moment| add_duration(total, moment.duration))
2272}
2273
2274fn instruction_duration(instruction: &Instruction) -> Result<f64, LowerError> {
2275    match instruction {
2276        Instruction::Quantum(task) => task
2277            .alternatives
2278            .iter()
2279            .map(|alternative| stream_duration(&alternative.stream))
2280            .try_fold(0.0_f64, |longest, duration| {
2281                duration.map(|duration| longest.max(duration))
2282            }),
2283        Instruction::MemoryRounds { stream, .. } => stream_duration(stream),
2284        _ => Ok(0.0),
2285    }
2286}
2287
2288fn instruction_stream(instruction: &Instruction) -> Option<&QuantumStream> {
2289    match instruction {
2290        Instruction::Quantum(task) => {
2291            let first = &task.alternatives.first()?.stream;
2292            if first
2293                .moments
2294                .iter()
2295                .any(|moment| moment.kind.is_none() && moment.duration == 0.0)
2296            {
2297                return None;
2298            }
2299            task.alternatives
2300                .iter()
2301                .all(|alternative| {
2302                    alternative
2303                        .stream
2304                        .moments
2305                        .iter()
2306                        .map(|moment| moment.kind)
2307                        .eq(first.moments.iter().map(|moment| moment.kind))
2308                })
2309                .then_some(first)
2310        }
2311        Instruction::MemoryRounds { stream, .. }
2312            if !stream
2313                .moments
2314                .iter()
2315                .any(|moment| moment.kind.is_none() && moment.duration == 0.0) =>
2316        {
2317            Some(stream)
2318        }
2319        // Dynamic waits align at runtime against the sibling completion event.
2320        Instruction::MemoryRounds { .. }
2321        | Instruction::WaitFor { .. }
2322        | Instruction::Eval(_)
2323        | Instruction::Accumulate(_)
2324        | Instruction::Bind(_)
2325        | Instruction::ReadoutRecipe { .. }
2326        | Instruction::Observable { .. }
2327        | Instruction::Decode(_)
2328        | Instruction::Discard(_)
2329        | Instruction::Rus { .. }
2330        | Instruction::SignalReady { .. }
2331        | Instruction::Idle { .. } => None,
2332    }
2333}
2334
2335fn rewrite_instruction_streams(
2336    instruction: &mut Instruction,
2337    positions: &FxHashMap<usize, (i64, usize)>,
2338    duration: f64,
2339    idle_qubits: &[QubitId],
2340    idle_probability: f64,
2341) -> Result<(), LowerError> {
2342    let idle = || Moment {
2343        kind: None,
2344        duration,
2345        operations: (idle_probability != 0.0 && !idle_qubits.is_empty())
2346            .then(|| QuantumOp::Depolarize1 {
2347                probability: idle_probability,
2348                qubits: idle_qubits.into(),
2349            })
2350            .into_iter()
2351            .collect(),
2352    };
2353    let rewrite = |stream: &mut QuantumStream| {
2354        if stream.moments.len() != positions.len() {
2355            return Err(LowerError::Unflattened("guarded moment-lane shape"));
2356        }
2357        let old = std::mem::take(&mut stream.moments).into_vec();
2358        let mut moments = Vec::new();
2359        let mut previous = None;
2360        for (index, moment) in old.into_iter().enumerate() {
2361            let &(layer, slot) = positions
2362                .get(&index)
2363                .ok_or(LowerError::Unflattened("aligned moment index"))?;
2364            let start = match previous {
2365                Some((previous_layer, previous_slot)) if layer == previous_layer => {
2366                    previous_slot + 1
2367                }
2368                _ => 0,
2369            };
2370            for _ in start..slot {
2371                moments.push(idle());
2372            }
2373            moments.push(moment);
2374            previous = Some((layer, slot));
2375        }
2376        stream.moments = moments.into_boxed_slice();
2377        Ok(())
2378    };
2379    match instruction {
2380        Instruction::Quantum(task) => {
2381            for alternative in &mut task.alternatives {
2382                rewrite(&mut alternative.stream)?;
2383            }
2384        }
2385        Instruction::MemoryRounds { stream, .. } => rewrite(stream)?,
2386        _ => {}
2387    }
2388    Ok(())
2389}
2390
2391fn ir_moment_kind(kind: MomentKind) -> bloq_ir::MomentKind {
2392    match kind {
2393        MomentKind::Reset => bloq_ir::MomentKind::Reset,
2394        MomentKind::Rotation => bloq_ir::MomentKind::Rotation,
2395        MomentKind::Interaction => bloq_ir::MomentKind::Interaction,
2396        MomentKind::Measurement => bloq_ir::MomentKind::Measurement,
2397    }
2398}
2399
2400fn task_produces_bindings(instruction: &Instruction) -> bool {
2401    matches!(
2402        instruction,
2403        Instruction::Bind(_)
2404            | Instruction::ReadoutRecipe { .. }
2405            | Instruction::Observable { .. }
2406            | Instruction::Rus { .. }
2407    )
2408}
2409
2410#[derive(Default)]
2411struct BooleanFunctions {
2412    definitions: FxHashMap<ClassicalExpr, (Arc<BoolOp>, Box<[u32]>)>,
2413}
2414
2415impl BooleanFunctions {
2416    fn lower(
2417        &mut self,
2418        expr: &ClassicalExpr,
2419        inputs: &FxHashMap<u32, BitId>,
2420        node: BloqNodeId,
2421    ) -> Result<BoolOp, LowerError> {
2422        // Scalars have no body allocation to share and remain direct operations.
2423        if matches!(expr, ClassicalExpr::In(_) | ClassicalExpr::Const(_)) {
2424            return lower_expr(expr, inputs, node);
2425        }
2426        if !self.definitions.contains_key(expr) {
2427            let mut slots = BTreeSet::new();
2428            expr.for_each_input(&mut |slot| {
2429                slots.insert(slot);
2430            });
2431            let slots = slots.into_iter().collect::<Box<[_]>>();
2432            let local = slots
2433                .iter()
2434                .enumerate()
2435                .map(|(index, &slot)| {
2436                    Ok((
2437                        slot,
2438                        u32::try_from(index)
2439                            .map_err(|_| LowerError::IdOverflow("function input"))?,
2440                    ))
2441                })
2442                .collect::<Result<FxHashMap<_, _>, LowerError>>()?;
2443            let body = Arc::new(lower_expr(expr, &local, node)?);
2444            self.definitions.insert(expr.clone(), (body, slots));
2445        }
2446        let (body, slots) = &self.definitions[expr];
2447        let inputs = slots
2448            .iter()
2449            .map(|&slot| {
2450                inputs
2451                    .get(&slot)
2452                    .copied()
2453                    .ok_or(LowerError::MissingBit { node: node.0, slot })
2454            })
2455            .collect::<Result<Box<[_]>, _>>()?;
2456        Ok(BoolOp::Call {
2457            body: Arc::clone(body),
2458            inputs,
2459        })
2460    }
2461}
2462
2463fn lower_expr(
2464    expr: &ClassicalExpr,
2465    inputs: &FxHashMap<u32, BitId>,
2466    node: BloqNodeId,
2467) -> Result<BoolOp, LowerError> {
2468    let input = |slot| {
2469        inputs
2470            .get(&slot)
2471            .copied()
2472            .ok_or(LowerError::MissingBit { node: node.0, slot })
2473    };
2474    Ok(match expr {
2475        ClassicalExpr::In(slot) => BoolOp::Copy(input(*slot)?),
2476        ClassicalExpr::Const(value) => BoolOp::Const(*value),
2477        ClassicalExpr::Not(inner) => BoolOp::Not(Box::new(lower_expr(inner, inputs, node)?)),
2478        ClassicalExpr::Parity {
2479            inputs: slots,
2480            constant,
2481        } => BoolOp::Parity {
2482            inputs: slots
2483                .iter()
2484                .map(|&slot| input(slot))
2485                .collect::<Result<Vec<_>, _>>()?
2486                .into_boxed_slice(),
2487            constant: *constant,
2488        },
2489        ClassicalExpr::Xor(operands) => BoolOp::Xor(
2490            operands
2491                .iter()
2492                .map(|expr| lower_expr(expr, inputs, node))
2493                .collect::<Result<Vec<_>, _>>()?
2494                .into_boxed_slice(),
2495        ),
2496        ClassicalExpr::And(operands) => BoolOp::And(
2497            operands
2498                .iter()
2499                .map(|expr| lower_expr(expr, inputs, node))
2500                .collect::<Result<Vec<_>, _>>()?
2501                .into_boxed_slice(),
2502        ),
2503        ClassicalExpr::Or(operands) => BoolOp::Or(
2504            operands
2505                .iter()
2506                .map(|expr| lower_expr(expr, inputs, node))
2507                .collect::<Result<Vec<_>, _>>()?
2508                .into_boxed_slice(),
2509        ),
2510        ClassicalExpr::Select(operands) => {
2511            let [condition, when_false, when_true] = operands.as_ref();
2512            BoolOp::Select {
2513                condition: Box::new(lower_expr(condition, inputs, node)?),
2514                when_false: Box::new(lower_expr(when_false, inputs, node)?),
2515                when_true: Box::new(lower_expr(when_true, inputs, node)?),
2516            }
2517        }
2518    })
2519}
2520
2521fn pauli(basis: PauliBasis) -> Pauli {
2522    match basis {
2523        PauliBasis::X => Pauli::X,
2524        PauliBasis::Y => Pauli::Y,
2525        PauliBasis::Z => Pauli::Z,
2526    }
2527}
2528
2529fn pauli_map_basis(basis: bloq_ir::circuit::Pauli) -> Pauli {
2530    match basis {
2531        bloq_ir::circuit::Pauli::X => Pauli::X,
2532        bloq_ir::circuit::Pauli::Y => Pauli::Y,
2533        bloq_ir::circuit::Pauli::Z => Pauli::Z,
2534        bloq_ir::circuit::Pauli::I => unreachable!("identity was filtered"),
2535    }
2536}
2537
2538fn vm_moment_kind(op: &Op) -> Option<MomentKind> {
2539    match op {
2540        Op::Gate { gate, .. } if gate.is_reset() => Some(MomentKind::Reset),
2541        Op::Gate { gate, .. } if gate.is_two_qubit_gate() => Some(MomentKind::Interaction),
2542        Op::Gate { .. } => Some(MomentKind::Rotation),
2543        Op::Measure { .. } | Op::MPP { .. } => Some(MomentKind::Measurement),
2544        Op::Tick
2545        | Op::Repeat { .. }
2546        | Op::ConditionalPauli(_)
2547        | Op::Depolarize1 { .. }
2548        | Op::Depolarize2 { .. }
2549        | Op::PauliError { .. } => None,
2550    }
2551}
2552
2553fn gate1(gate: GateType, qubit: QubitId) -> Option<QuantumOp> {
2554    use Clifford1 as C;
2555    use GateType as G;
2556    Some(match gate {
2557        G::RX => QuantumOp::Reset {
2558            basis: Pauli::X,
2559            qubit,
2560        },
2561        G::RY => QuantumOp::Reset {
2562            basis: Pauli::Y,
2563            qubit,
2564        },
2565        G::RZ => QuantumOp::Reset {
2566            basis: Pauli::Z,
2567            qubit,
2568        },
2569        G::I => return None,
2570        G::X => QuantumOp::Pauli {
2571            basis: Pauli::X,
2572            qubit,
2573        },
2574        G::Y => QuantumOp::Pauli {
2575            basis: Pauli::Y,
2576            qubit,
2577        },
2578        G::Z => QuantumOp::Pauli {
2579            basis: Pauli::Z,
2580            qubit,
2581        },
2582        G::T_YZ | G::T_YZ_DAG => QuantumOp::T {
2583            basis: Pauli::X,
2584            qubit,
2585            adjoint: gate == G::T_YZ_DAG,
2586        },
2587        G::T_XZ | G::T_XZ_DAG => QuantumOp::T {
2588            basis: Pauli::Y,
2589            qubit,
2590            adjoint: gate == G::T_XZ_DAG,
2591        },
2592        G::T | G::T_DAG => QuantumOp::T {
2593            basis: Pauli::Z,
2594            qubit,
2595            adjoint: gate == G::T_DAG,
2596        },
2597        G::H => QuantumOp::Gate1 { gate: C::H, qubit },
2598        G::H_XY => QuantumOp::Gate1 {
2599            gate: C::H_XY,
2600            qubit,
2601        },
2602        G::H_YZ => QuantumOp::Gate1 {
2603            gate: C::H_YZ,
2604            qubit,
2605        },
2606        G::H_NXY => QuantumOp::Gate1 {
2607            gate: C::H_NXY,
2608            qubit,
2609        },
2610        G::H_NXZ => QuantumOp::Gate1 {
2611            gate: C::H_NXZ,
2612            qubit,
2613        },
2614        G::H_NYZ => QuantumOp::Gate1 {
2615            gate: C::H_NYZ,
2616            qubit,
2617        },
2618        G::SQRT_X => QuantumOp::Gate1 {
2619            gate: C::SQRT_X,
2620            qubit,
2621        },
2622        G::SQRT_X_DAG => QuantumOp::Gate1 {
2623            gate: C::SQRT_X_DAG,
2624            qubit,
2625        },
2626        G::SQRT_Y => QuantumOp::Gate1 {
2627            gate: C::SQRT_Y,
2628            qubit,
2629        },
2630        G::SQRT_Y_DAG => QuantumOp::Gate1 {
2631            gate: C::SQRT_Y_DAG,
2632            qubit,
2633        },
2634        G::S => QuantumOp::Gate1 { gate: C::S, qubit },
2635        G::S_DAG => QuantumOp::Gate1 {
2636            gate: C::S_DAG,
2637            qubit,
2638        },
2639        G::C_XYZ => QuantumOp::Gate1 {
2640            gate: C::C_XYZ,
2641            qubit,
2642        },
2643        G::C_ZYX => QuantumOp::Gate1 {
2644            gate: C::C_ZYX,
2645            qubit,
2646        },
2647        G::C_NXYZ => QuantumOp::Gate1 {
2648            gate: C::C_NXYZ,
2649            qubit,
2650        },
2651        G::C_XNYZ => QuantumOp::Gate1 {
2652            gate: C::C_XNYZ,
2653            qubit,
2654        },
2655        G::C_XYNZ => QuantumOp::Gate1 {
2656            gate: C::C_XYNZ,
2657            qubit,
2658        },
2659        G::C_NZYX => QuantumOp::Gate1 {
2660            gate: C::C_NZYX,
2661            qubit,
2662        },
2663        G::C_ZNYX => QuantumOp::Gate1 {
2664            gate: C::C_ZNYX,
2665            qubit,
2666        },
2667        G::C_ZYNX => QuantumOp::Gate1 {
2668            gate: C::C_ZYNX,
2669            qubit,
2670        },
2671        G::XCX | G::XCY | G::XCZ | G::YCX | G::YCY | G::YCZ | G::CX | G::CY | G::CZ => {
2672            unreachable!("two-qubit gates are handled first")
2673        }
2674    })
2675}
2676
2677fn op_qubits(op: &QuantumOp) -> Box<dyn Iterator<Item = QubitId> + '_> {
2678    match op {
2679        QuantumOp::Gate1 { qubit, .. }
2680        | QuantumOp::Pauli { qubit, .. }
2681        | QuantumOp::T { qubit, .. }
2682        | QuantumOp::Reset { qubit, .. }
2683        | QuantumOp::ConditionalPauli { qubit, .. } => Box::new(std::iter::once(*qubit)),
2684        QuantumOp::Gate2 {
2685            control, target, ..
2686        } => Box::new([*control, *target].into_iter()),
2687        QuantumOp::Measure { observable, .. } => {
2688            Box::new(observable.terms.iter().map(|(qubit, _)| *qubit))
2689        }
2690        QuantumOp::Depolarize1 { qubits, .. } | QuantumOp::PauliError { qubits, .. } => {
2691            Box::new(qubits.iter().copied())
2692        }
2693        QuantumOp::Depolarize2 { pairs, .. } => {
2694            Box::new(pairs.iter().flat_map(|&(left, right)| [left, right]))
2695        }
2696    }
2697}
2698
2699#[cfg(test)]
2700mod tests {
2701    use std::collections::BTreeMap;
2702
2703    use super::*;
2704
2705    #[test]
2706    fn shared_boolean_functions_keep_invocation_bindings_and_sparse_slots() {
2707        let expr = ClassicalExpr::Select(Box::new([
2708            ClassicalExpr::In(9),
2709            ClassicalExpr::In(2),
2710            ClassicalExpr::parity([2, 2, 7], true),
2711        ]));
2712        let mut functions = BooleanFunctions::default();
2713        let first = functions
2714            .lower(
2715                &expr,
2716                &[(2, 20), (7, 70), (9, 90)].into_iter().collect(),
2717                BloqNodeId(0),
2718            )
2719            .unwrap();
2720        let second = functions
2721            .lower(
2722                &expr,
2723                &[(2, 21), (7, 71), (9, 91)].into_iter().collect(),
2724                BloqNodeId(1),
2725            )
2726            .unwrap();
2727        let (
2728            BoolOp::Call {
2729                body: a,
2730                inputs: first,
2731            },
2732            BoolOp::Call {
2733                body: b,
2734                inputs: second,
2735            },
2736        ) = (first, second)
2737        else {
2738            panic!("compound expressions lower to shared functions");
2739        };
2740        assert!(Arc::ptr_eq(&a, &b));
2741        assert_eq!(first.as_ref(), &[20, 70, 90]);
2742        assert_eq!(second.as_ref(), &[21, 71, 91]);
2743        assert!(matches!(
2744            functions.lower(
2745                &expr,
2746                &[(2, 20), (7, 70)].into_iter().collect(),
2747                BloqNodeId(3)
2748            ),
2749            Err(LowerError::MissingBit { node: 3, slot: 9 })
2750        ));
2751    }
2752    use bloq_compile::{CompileConfig, CompileContext};
2753    use bloq_graph::GalleryItem;
2754    use bloq_ir::circuit::CoordCircuit;
2755    use bloq_ir::lowering::BloqTemplate;
2756
2757    #[test]
2758    fn unrelated_decoder_query_does_not_retry_without_an_authored_predicate() {
2759        let mut body = SubGraph::new();
2760        let observable = body.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2761        let result = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2762            expr: ClassicalExpr::In(0),
2763        }));
2764        body.add_edge(observable, result, BloqEdge::flip(0));
2765        body.set_value_output(Some(result.into()));
2766        let mut bloq = Bloq::new();
2767        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2768            body,
2769            restart_condition: ClassicalExpr::Const(false),
2770            restart_source: None,
2771        }));
2772        bloq.validate().unwrap();
2773        let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2774        for rejected_accuracy in [1.0, 0.0] {
2775            let result = crate::runtime::run(
2776                &program,
2777                crate::runtime::RuntimeConfig {
2778                    decoder: crate::decoder::MockDecoderConfig {
2779                        acceptance_script: vec![false],
2780                        accepted_accuracy: 1.0,
2781                        rejected_accuracy,
2782                        ..Default::default()
2783                    },
2784                    ..Default::default()
2785                },
2786            )
2787            .unwrap();
2788            assert_eq!(result.artifact.retries.len(), 1);
2789            assert!(result.artifact.retries[0].accepted);
2790            assert_eq!(result.artifact.decoder_decisions.len(), 1);
2791            assert!(!result.artifact.decoder_decisions[0].decision.accepted);
2792            assert_eq!(
2793                result.artifact.decoder_decisions[0].decision.flip,
2794                rejected_accuracy == 0.0
2795            );
2796        }
2797    }
2798
2799    #[test]
2800    fn rus_retries_only_for_the_authored_flip_predicate() {
2801        let mut body = SubGraph::new();
2802        let observables: Vec<_> = (0..3)
2803            .map(|index| body.add_node(BloqNode::classical(ClassicalNode::observable(index))))
2804            .collect();
2805        let predicate = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2806            expr: ClassicalExpr::Or(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
2807        }));
2808        for (slot, &observable) in observables[..2].iter().enumerate() {
2809            body.add_edge(observable, predicate, BloqEdge::flip(slot as u32));
2810        }
2811        let mut bloq = Bloq::new();
2812        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2813            body,
2814            restart_condition: ClassicalExpr::In(0),
2815            restart_source: Some(predicate.into()),
2816        }));
2817        bloq.validate().unwrap();
2818        let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2819        assert!(!program.instructions().any(|instruction| matches!(
2820            instruction,
2821            Instruction::Decode(DecodeRequest { index: 2, .. })
2822        )));
2823        for flips in [
2824            [false, false, false],
2825            [true, false, false],
2826            [false, true, false],
2827            [true, true, false],
2828            [false, false, true],
2829        ] {
2830            let result = crate::runtime::run(
2831                &program,
2832                crate::runtime::RuntimeConfig {
2833                    decoder: crate::decoder::MockDecoderConfig {
2834                        acceptance_script: flips
2835                            .into_iter()
2836                            .map(|flip| !flip)
2837                            .chain([true; 3])
2838                            .collect(),
2839                        accepted_accuracy: 1.0,
2840                        rejected_accuracy: 0.0,
2841                        ..Default::default()
2842                    },
2843                    ..Default::default()
2844                },
2845            )
2846            .unwrap();
2847            let retry = flips[0] || flips[1];
2848            assert_eq!(
2849                result
2850                    .artifact
2851                    .retries
2852                    .iter()
2853                    .map(|attempt| attempt.accepted)
2854                    .collect::<Vec<_>>(),
2855                if retry { vec![false, true] } else { vec![true] }
2856            );
2857            assert_eq!(
2858                result.artifact.decoder_decisions[..2]
2859                    .iter()
2860                    .map(|record| record.decision.flip)
2861                    .collect::<Vec<_>>(),
2862                flips[..2]
2863            );
2864            assert_eq!(
2865                result.artifact.decoder_decisions.len(),
2866                if retry { 4 } else { 2 }
2867            );
2868        }
2869    }
2870
2871    #[test]
2872    fn observable_restart_source_requests_correction_without_replacing_body_result() {
2873        let mut body = SubGraph::new();
2874        let restart_source = body.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2875        let result = body.add_node(BloqNode::classical(ClassicalNode::Compute {
2876            expr: ClassicalExpr::Const(true),
2877        }));
2878        body.set_value_output(Some(result.into()));
2879        let mut bloq = Bloq::new();
2880        bloq.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
2881            body,
2882            restart_condition: ClassicalExpr::In(0),
2883            restart_source: Some(restart_source.into()),
2884        }));
2885        bloq.validate().unwrap();
2886        let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2887        let decode = program
2888            .tasks
2889            .iter()
2890            .find(|task| matches!(task.instruction, Instruction::Decode(_)))
2891            .expect("restart source requests its corrected bit");
2892        let (body, restart) = program
2893            .instructions()
2894            .find_map(|instruction| match instruction {
2895                Instruction::Rus { body, restart, .. } => Some((body, restart)),
2896                _ => None,
2897            })
2898            .unwrap();
2899        let corrected = decode.output.unwrap();
2900        assert_eq!(restart, &BoolOp::Copy(corrected));
2901        assert_ne!(body.value, Some(corrected));
2902        assert!(body.value.is_some());
2903    }
2904
2905    #[test]
2906    fn composition_requests_no_solve_until_corrected_value_is_used() {
2907        for corrected_consumer in [false, true] {
2908            let mut bloq = Bloq::new();
2909            let value = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2910                expr: ClassicalExpr::Const(true),
2911            }));
2912            let observable = bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)));
2913            bloq.add_edge(value, observable, BloqEdge::value(0));
2914            let raw = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
2915                Vec::new(),
2916                Vec::new(),
2917            )));
2918            bloq.add_edge(observable, raw, BloqEdge::compose(0));
2919            if corrected_consumer {
2920                let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2921                    expr: ClassicalExpr::In(0),
2922                }));
2923                bloq.add_edge(observable, consumer, BloqEdge::value(0));
2924            }
2925            let program = lower(&bloq, &LoweringConfig::default()).unwrap();
2926            assert_eq!(
2927                program
2928                    .instructions()
2929                    .filter(|instruction| matches!(instruction, Instruction::Decode(_)))
2930                    .count(),
2931                2 * usize::from(corrected_consumer),
2932            );
2933            let (raw_task_id, raw_task) = program
2934                .tasks
2935                .iter()
2936                .enumerate()
2937                .find(|(_, task)| {
2938                    matches!(task.instruction, Instruction::Observable { index: 0, .. })
2939                })
2940                .unwrap();
2941            let projection = program
2942                .tasks
2943                .iter()
2944                .find(|task| task.label == task_label(&LevelPath::default(), raw, "classical"))
2945                .unwrap();
2946            assert_eq!(
2947                projection.instruction,
2948                Instruction::ReadoutRecipe {
2949                    bits: Box::new([raw_task.output.unwrap()]),
2950                    bindings: Box::new([raw_task_id as TaskId])
2951                }
2952            );
2953        }
2954    }
2955
2956    fn guarded_stage(bloq: &mut Bloq, guards: u32) -> BloqNodeId {
2957        let template = crate::test_support::one_qubit_template(bloq, GateType::H);
2958        let mut stage = BloqNode::from_members(vec![]);
2959        let quantum = stage.expect_quantum_mut();
2960        for input in 0..guards {
2961            let instance = TemplateInstanceId(input);
2962            quantum
2963                .instances
2964                .push(bloq_ir::lowering::TemplateInstance::new(
2965                    instance,
2966                    template,
2967                    IVec2::new(input as i32, 0),
2968                ));
2969            quantum.guards.push(bloq_ir::QuantumGuard {
2970                input,
2971                instances: vec![instance],
2972                ..Default::default()
2973            });
2974        }
2975        bloq.add_node(stage)
2976    }
2977
2978    #[test]
2979    fn constant_guards_do_not_require_an_exponential_choice_table() {
2980        for limit in [1, usize::MAX] {
2981            let guards = 2 * usize::BITS;
2982            let mut bloq = Bloq::new();
2983            let quantum = guarded_stage(&mut bloq, guards);
2984            for input in 0..guards {
2985                let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
2986                    expr: ClassicalExpr::Const(true),
2987                }));
2988                bloq.add_edge(producer, quantum, BloqEdge::value(input));
2989            }
2990            let program = lower(
2991                &bloq,
2992                &LoweringConfig {
2993                    max_quantum_variants: limit,
2994                    ..Default::default()
2995                },
2996            )
2997            .unwrap();
2998            let alternatives = program
2999                .tasks
3000                .iter()
3001                .find_map(|task| match &task.instruction {
3002                    Instruction::Quantum(quantum) => Some(&quantum.alternatives),
3003                    _ => None,
3004                })
3005                .unwrap();
3006            assert_eq!(alternatives.len(), 1);
3007            assert_eq!(alternatives[0].when.len(), guards as usize);
3008            assert!(alternatives[0].when.iter().all(|&(_, value)| value));
3009        }
3010    }
3011
3012    #[test]
3013    fn correlated_guard_variants_keep_runtime_inputs_and_the_reachable_cap() {
3014        let mut bloq = Bloq::new();
3015        let raw = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3016            Vec::new(),
3017            Vec::new(),
3018        )));
3019        let inverse = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3020            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
3021        }));
3022        bloq.add_edge(raw, inverse, BloqEdge::value(0));
3023        let stage = guarded_stage(&mut bloq, 128);
3024        for input in 0..128 {
3025            bloq.add_edge(
3026                if input % 2 == 0 { raw } else { inverse },
3027                stage,
3028                BloqEdge::value(input),
3029            );
3030        }
3031        bloq.validate().unwrap();
3032        let program = lower(
3033            &bloq,
3034            &LoweringConfig {
3035                max_quantum_variants: 2,
3036                ..Default::default()
3037            },
3038        )
3039        .unwrap();
3040        let alternatives = program
3041            .tasks
3042            .iter()
3043            .find_map(|task| match &task.instruction {
3044                Instruction::Quantum(quantum) => Some(&quantum.alternatives),
3045                _ => None,
3046            })
3047            .unwrap();
3048        assert_eq!(alternatives.len(), 2);
3049        for (index, alternative) in alternatives.iter().enumerate() {
3050            assert_eq!(alternative.when.len(), 128);
3051            assert!(
3052                alternative
3053                    .when
3054                    .iter()
3055                    .enumerate()
3056                    .all(|(slot, &(_, value))| value == ((slot + index) % 2 == 0))
3057            );
3058            assert_eq!(
3059                alternative
3060                    .stream
3061                    .moments
3062                    .iter()
3063                    .map(|moment| moment.operations.len())
3064                    .sum::<usize>(),
3065                64
3066            );
3067        }
3068        let artifact = crate::run(&program, crate::RuntimeConfig::default())
3069            .unwrap()
3070            .artifact;
3071        assert_eq!(artifact.stop_reason, None);
3072        assert!(artifact.events.iter().any(|event| matches!(
3073            event,
3074            crate::runtime::ExecutionEvent::QuantumAlternative { alternative: 1, .. }
3075        )));
3076        for limit in [0, 1] {
3077            let error = lower(
3078                &bloq,
3079                &LoweringConfig {
3080                    max_quantum_variants: limit,
3081                    ..Default::default()
3082                },
3083            )
3084            .unwrap_err();
3085            assert!(matches!(error, LowerError::QuantumVariantLimit {
3086                variants, limit: observed_limit, ..
3087            } if variants == limit + 1 && observed_limit == limit));
3088        }
3089    }
3090
3091    #[test]
3092    fn independent_guard_variants_remain_bounded() {
3093        for guards in [12, 13] {
3094            let mut bloq = Bloq::new();
3095            let quantum = guarded_stage(&mut bloq, guards);
3096            for input in 0..guards {
3097                let producer = bloq.add_node(BloqNode::classical(
3098                    ClassicalNode::observable_fragment(Vec::new(), Vec::new()),
3099                ));
3100                bloq.add_edge(producer, quantum, BloqEdge::value(input));
3101            }
3102            let result = lower(&bloq, &LoweringConfig::default());
3103            if guards == 12 {
3104                let program = result.unwrap();
3105                assert!(program.tasks.iter().any(|task| matches!(
3106                    &task.instruction,
3107                    Instruction::Quantum(quantum)
3108                        if quantum.alternatives.len() == DEFAULT_MAX_QUANTUM_VARIANTS
3109                )));
3110            } else {
3111                assert!(matches!(
3112                    result,
3113                    Err(LowerError::QuantumVariantLimit {
3114                        guards: 13,
3115                        variants: 4097,
3116                        limit: DEFAULT_MAX_QUANTUM_VARIANTS,
3117                        ..
3118                    })
3119                ));
3120            }
3121        }
3122    }
3123
3124    #[test]
3125    fn vm_materialization_does_not_run_the_full_ir_audit() {
3126        let mut circuit = CoordCircuit::new();
3127        circuit.do_gate(GateType::H, [IVec2::ZERO]).unwrap();
3128        let mut bloq = Bloq::new();
3129        let template = bloq.add_template(BloqTemplate::new(circuit));
3130        let quantum = bloq.add_node(crate::test_support::quantum_node(template, 0, IVec2::ZERO));
3131        let constant = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3132            expr: ClassicalExpr::Const(true),
3133        }));
3134        bloq.add_edge(constant, quantum, BloqEdge::value(0));
3135
3136        assert!(matches!(
3137            bloq.validate(),
3138            Err(bloq_ir::BloqValidationError::UnusedQuantumValueInput {
3139                node,
3140                slot: 0
3141            }) if node == quantum
3142        ));
3143        crate::prepared::PreparedProgramCore::build(&bloq).unwrap();
3144        lower(&bloq, &LoweringConfig::default()).unwrap();
3145    }
3146
3147    #[test]
3148    fn multi_pipe_origin_keeps_every_site_and_deduplicates_time() {
3149        let origin = pipe_origin([
3150            TemporalPipeRef {
3151                src: IVec3::new(2, 3, 0),
3152                dst: IVec3::new(2, 3, 1),
3153                hadamard: false,
3154            },
3155            TemporalPipeRef {
3156                src: IVec3::new(4, 5, 2),
3157                dst: IVec3::new(4, 5, 3),
3158                hadamard: true,
3159            },
3160        ]);
3161        assert_eq!(origin.function, TaskFunction::Memory);
3162        assert_eq!(origin.sites.as_ref(), [[2, 3], [4, 5]]);
3163        assert_eq!(
3164            origin.members.as_ref(),
3165            [[2, 3, 0], [2, 3, 1], [4, 5, 2], [4, 5, 3]]
3166        );
3167    }
3168
3169    #[test]
3170    fn thth_sources_and_live_output_hold_have_causal_metadata() {
3171        let bloq = CompileContext::new(CompileConfig::new(3))
3172            .compile(&GalleryItem::THTH.build())
3173            .unwrap()
3174            .bloq;
3175        let program = lower(&bloq, &LoweringConfig::default()).unwrap();
3176        assert_eq!(program.task_origins.len(), program.tasks.len());
3177        for origin in &program.task_origins {
3178            assert!(origin.sites.windows(2).all(|pair| pair[0] < pair[1]));
3179            assert!(origin.members.windows(2).all(|pair| pair[0] < pair[1]));
3180        }
3181
3182        let sources = |role| {
3183            program
3184                .tasks
3185                .iter()
3186                .filter(|task| task.source == Some(role))
3187                .collect::<Vec<_>>()
3188        };
3189        let factories = sources(SourceRole::Factory);
3190        assert_eq!(factories.len(), 2);
3191        assert!(factories.iter().all(|task| task.release == 0.0));
3192        let input = sources(SourceRole::LogicalInput);
3193        assert_eq!(input.len(), 1);
3194        assert_eq!((input[0].release, input[0].duration), (0.0, 0.0));
3195        let clifford = sources(SourceRole::Clifford);
3196        assert_eq!(clifford.len(), 1);
3197        assert_eq!(clifford[0].release, 0.0);
3198        let prepared_y = sources(SourceRole::PreparedY);
3199        assert_eq!(prepared_y.len(), 2);
3200        assert!(prepared_y.iter().all(|task| {
3201            task.release + task.duration == clifford[0].release + clifford[0].duration
3202        }));
3203        let source_geometry = program
3204            .tasks
3205            .iter()
3206            .zip(&program.task_origins)
3207            .filter_map(|(task, origin)| task.source.map(|source| (source, origin)))
3208            .collect::<Vec<_>>();
3209        assert_eq!(
3210            source_geometry
3211                .iter()
3212                .filter(|(source, _)| *source == SourceRole::PreparedY)
3213                .flat_map(|(_, origin)| origin.sites.iter().copied())
3214                .collect::<BTreeSet<_>>(),
3215            BTreeSet::from([[3, 0], [3, 2]]),
3216        );
3217        assert!(source_geometry.iter().all(|(source, origin)| match source {
3218            SourceRole::Factory => origin.function == TaskFunction::Factory,
3219            SourceRole::LogicalInput => origin.function == TaskFunction::IdealBoundary,
3220            SourceRole::PreparedY => origin.function == TaskFunction::PreparedY,
3221            SourceRole::Clifford => {
3222                origin.function == TaskFunction::Cube
3223                    && origin.sites.as_ref() == [[-1, 0], [-1, 1], [-1, 2], [0, 2]]
3224            }
3225        }));
3226        let functions = program
3227            .task_origins
3228            .iter()
3229            .map(|origin| origin.function)
3230            .collect::<Vec<_>>();
3231        for function in [
3232            TaskFunction::FactoryBody,
3233            TaskFunction::TemporalHadamard,
3234            TaskFunction::Memory,
3235            TaskFunction::AdaptiveMeasurement,
3236            TaskFunction::IdealBoundary,
3237        ] {
3238            assert!(functions.contains(&function), "missing {function:?}");
3239        }
3240        assert!(
3241            program
3242                .task_origins
3243                .iter()
3244                .filter(|origin| origin.function == TaskFunction::Memory)
3245                .all(|origin| !origin.sites.is_empty())
3246        );
3247
3248        assert_eq!(program.inputs.len(), 1);
3249        let input = &program.inputs[0];
3250        assert_eq!(
3251            program.tasks[input.task as usize].source,
3252            Some(SourceRole::LogicalInput)
3253        );
3254        assert!(!input.data_qubits.is_empty());
3255        assert!(!input.stabilizers.is_empty());
3256        assert!(!input.x.terms.is_empty() && !input.z.terms.is_empty());
3257
3258        for factory in factories {
3259            let Instruction::Rus {
3260                body,
3261                owned_qubits,
3262                retry_prepare,
3263                decoder_hold,
3264                cultivation_exits,
3265                ..
3266            } = &factory.instruction
3267            else {
3268                panic!("factory source is not RUS")
3269            };
3270            assert!(retry_prepare.moments.is_empty());
3271            let mut first_use = BTreeMap::<QubitId, bool>::new();
3272            for &task in &body.tasks {
3273                let Instruction::Quantum(quantum) = &program.tasks[task as usize].instruction
3274                else {
3275                    continue;
3276                };
3277                let [alternative] = quantum.alternatives.as_ref() else {
3278                    panic!("cultivation body quantum task is unguarded")
3279                };
3280                for moment in &alternative.stream.moments {
3281                    for operation in &moment.operations {
3282                        let (qubits, reset) = match operation {
3283                            QuantumOp::Reset { qubit, .. } => (vec![*qubit], true),
3284                            QuantumOp::Gate1 { qubit, .. }
3285                            | QuantumOp::Pauli { qubit, .. }
3286                            | QuantumOp::T { qubit, .. }
3287                            | QuantumOp::ConditionalPauli { qubit, .. } => (vec![*qubit], false),
3288                            QuantumOp::Gate2 {
3289                                control, target, ..
3290                            } => (vec![*control, *target], false),
3291                            QuantumOp::Measure { observable, .. } => (
3292                                observable.terms.iter().map(|(qubit, _)| *qubit).collect(),
3293                                false,
3294                            ),
3295                            QuantumOp::Depolarize1 { .. }
3296                            | QuantumOp::Depolarize2 { .. }
3297                            | QuantumOp::PauliError { .. } => continue,
3298                        };
3299                        for qubit in qubits {
3300                            first_use.entry(qubit).or_insert(reset);
3301                        }
3302                    }
3303                }
3304            }
3305            assert_eq!(
3306                first_use.keys().copied().collect::<BTreeSet<_>>(),
3307                owned_qubits.iter().copied().collect(),
3308                "every owned factory qubit has an authored first use"
3309            );
3310            assert!(
3311                first_use.values().all(|reset| *reset),
3312                "every owned factory qubit is reset before first use"
3313            );
3314            assert!(!cultivation_exits.is_empty());
3315            assert!(cultivation_exits.iter().all(|&task| {
3316                body.tasks.contains(&task)
3317                    && matches!(
3318                        program.tasks[task as usize].instruction,
3319                        Instruction::Quantum(_)
3320                            | Instruction::MemoryRounds { .. }
3321                            | Instruction::Idle { .. }
3322                    )
3323            }));
3324            assert!(!decoder_hold.is_empty());
3325            assert!(
3326                decoder_hold.iter().all(|cycle| {
3327                    cycle.round_duration == 6.0 && !cycle.stream.moments.is_empty()
3328                })
3329            );
3330            assert!(body.tasks.iter().any(|&task| {
3331                matches!(
3332                    program.tasks[task as usize].instruction,
3333                    Instruction::Decode(DecodeRequest {
3334                        timing: DecodeTiming::FactoryCompletion,
3335                        ..
3336                    })
3337                )
3338            }));
3339        }
3340
3341        let output = &program.outputs[0];
3342        let frame_tasks = [output.frame_x, output.frame_z].map(|bit| {
3343            program
3344                .tasks
3345                .iter()
3346                .position(|task| task.output == Some(bit))
3347                .unwrap() as TaskId
3348        });
3349        let terminal_hold =
3350            program
3351                .tasks
3352                .iter()
3353                .enumerate()
3354                .find_map(|(task, data)| match &data.instruction {
3355                    Instruction::WaitFor { until, memory }
3356                        if frame_tasks.iter().all(|frame| until.contains(frame)) =>
3357                    {
3358                        Some((task as TaskId, memory))
3359                    }
3360                    _ => None,
3361                });
3362        let (terminal_hold_task, terminal_hold) =
3363            terminal_hold.expect("output remains in QEC until both frames resolve");
3364        assert!(terminal_hold.iter().all(|cycle| {
3365            cycle.round_duration == 6.0
3366                && !cycle.stream.moments.is_empty()
3367                && !cycle.initializers.is_empty()
3368        }));
3369        let output_boundary = program
3370            .tasks
3371            .iter()
3372            .find(|task| task.dependencies.contains(&terminal_hold_task))
3373            .expect("terminal hold releases the output boundary");
3374        assert_eq!(output_boundary.duration, 0.0);
3375        let Instruction::Quantum(output_boundary) = &output_boundary.instruction else {
3376            panic!("output boundary is quantum")
3377        };
3378        assert!(output_boundary.alternatives.iter().all(|alternative| {
3379            alternative
3380                .stream
3381                .moments
3382                .iter()
3383                .all(|moment| moment.duration == 0.0)
3384        }));
3385    }
3386
3387    #[test]
3388    fn zero_latency_factory_keeps_the_reusable_gap_frontier() {
3389        let bloq = CompileContext::new(CompileConfig::new(3))
3390            .compile(&GalleryItem::T.build())
3391            .unwrap()
3392            .bloq;
3393        let program = lower(
3394            &bloq,
3395            &LoweringConfig {
3396                decoder_latency_rounds: 0,
3397                ..LoweringConfig::default()
3398            },
3399        )
3400        .unwrap();
3401        assert_eq!(program.decoder_latency_rounds, 0);
3402        let gaps = program
3403            .tasks
3404            .iter()
3405            .filter_map(|task| match &task.instruction {
3406                Instruction::Rus { decoder_hold, .. } => Some(decoder_hold),
3407                _ => None,
3408            })
3409            .collect::<Vec<_>>();
3410        assert_eq!(gaps.len(), 1);
3411        for gap in gaps {
3412            assert!(gap.iter().all(|cycle| {
3413                !cycle.stream.moments.is_empty()
3414                    && !cycle.initializers.is_empty()
3415                    && cycle
3416                        .boundary_flows
3417                        .iter()
3418                        .any(|flow| flow.frontier_record.is_some())
3419            }));
3420        }
3421    }
3422
3423    #[test]
3424    fn postmerge_noise_does_not_add_vm_time() {
3425        let bloq = CompileContext::new(CompileConfig::new(3))
3426            .compile(&GalleryItem::T.build())
3427            .unwrap()
3428            .bloq;
3429        let ideal = lower(&bloq, &LoweringConfig::default()).unwrap();
3430        let noise = NoiseModel::uniform_depolarizing(1e-4);
3431        let noisy = lower(
3432            &bloq,
3433            &LoweringConfig {
3434                noise: Some(&noise),
3435                ..LoweringConfig::default()
3436            },
3437        )
3438        .unwrap();
3439
3440        assert_eq!(ideal.tasks.len(), noisy.tasks.len());
3441        for (ideal, noisy) in ideal.tasks.iter().zip(&noisy.tasks) {
3442            assert_eq!(ideal.label, noisy.label);
3443            assert_eq!(ideal.duration, noisy.duration, "{}", ideal.label);
3444        }
3445    }
3446
3447    #[test]
3448    fn rejects_unrepresentable_durations() {
3449        let bloq = CompileContext::new(CompileConfig::new(3))
3450            .compile(&GalleryItem::CNOT.build())
3451            .unwrap()
3452            .bloq;
3453        for gate_duration in [crate::runtime::TIME_EPSILON, f64::MAX] {
3454            let error = lower(
3455                &bloq,
3456                &LoweringConfig {
3457                    gate_duration,
3458                    ..LoweringConfig::default()
3459                },
3460            )
3461            .unwrap_err();
3462            assert!(matches!(error, LowerError::InvalidDuration));
3463        }
3464    }
3465    #[test]
3466    fn fragment_flip_requires_a_complete_observable_without_a_full_audit() {
3467        let mut bloq = Bloq::new();
3468        let fragment = bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
3469            Vec::new(),
3470            Vec::new(),
3471        )));
3472        let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
3473            expr: ClassicalExpr::In(0),
3474        }));
3475        bloq.add_edge(fragment, consumer, BloqEdge::flip(0));
3476        assert!(
3477            matches!(lower(&bloq, &LoweringConfig::default()), Err(LowerError::MissingBit { node, slot: 0 }) if node == consumer.0)
3478        );
3479    }
3480}